How to Use AMP with Arc XP Subscriptions
Using AMP with Arc XP Subscriptions paywall is fairly straightforward. You will need to use the <amp-subscription>
tag provided by Google and tell it to use an iframe as its service provider. The iframe will load a document which will in turn load the paywall’s rules & rules evaluator. From here on we will refer to this file as p.html and the evaluator will be referred to as p.js.
Caveats
- You must host the p.html file on the same domain as your login page, so that p.js can access
localStorage
orcookies
which store the user’sJWT
. This is only relevant if you want to customize p.html - It is also necessary to store the JWT in a place where p.js can access it. If you are using our default implementation, you will need to store the
JWT
inlocalStorage["ArcId.USER_INFO"]
which is where the Identity SDK stores it. - If you want to use 3rd party entitlement or identity services you will need to modify p.html and host it yourself.
- Apple’s Intelligent Tracking Prevention’s changes to the browser storage APIs and how AMP cache serves AMP content to client browsers have now made it impossible for Arc Identity SDKs and p.js from accessing values in localStorage/cookies for users on all browsers on iOS v14+. See AMP READER_ID section below for guidance on the workaround.
Quick Start
The AMP document must load the following extension.
<script async custom-element="amp-subscriptions" src="https://cdn.ampproject.org/v0/amp-subscriptions-0.1.js"></script>
The extension must be passed data like this:
<script> { "services": [ { "type": "iframe", "iframeSrc": "//yoursite.com/arc/subs/p.html", "iframeVars": [ "READER_ID", "CANONICAL_URL", "AMPDOC_URL", "SOURCE_URL", "DOCUMENT_REFERRER" ], "actions":{ "login": "//your-site.com/#/login", "subscribe": "//your-site.com/#/offer/default" }, "data": { "contentType": "story", "section": "technology", "contentRestriction": "premium", "contentId": "123456" "apiOrigin": "api-demo-demo-prod.cdn.arcpublishing.com" } } ], "fallbackEntitlement": { "source": "fallback", "granted": false, "grantReason": "METERING" } }</script>
A breakdown of the JSON above follows:
{ "services": [ { "type": "iframe", // You MUST set this to type="iframe" "iframeSrc": "//yoursite.com/arc/subs/p.html", // this must be the location of p.html "iframeVars": [ "READER_ID", "CANONICAL_URL", "AMPDOC_URL", "SOURCE_URL", "DOCUMENT_REFERRER" ], "actions":{ "login": "//your-site.com/#/login", "subscribe": "//your-site.com/#/offer/default" }, "data": { // This is how you will pass information down to p.js "contentType": "story", // required "section": "technology", // required "apiOrigin": "api-demo-demo-prod.cdn.arcpublishing.com", // required "contentRestriction": "premium", // optional, unless your rules require it "contentId": "123456" // optional but recommended, if not passed, iframeVars.CANONICAL_URL will be used. // You can pass more information here if you are using a custom implementation // of p.html. This data will come in as `config.data` in the "connect" method // of the controller } } ], "fallbackEntitlement": { // This must be set up in case p.html or p.js doesn't load "source": "fallback", "granted": false, // you can set this to true if you want the fallback to allow reads. "grantReason": "METERING" }}
The rest of the AMP document must then use AMP attributes to show or hide content depending on the verdict. As a simple example:
<div subscriptions-action="login" subscriptions-display="NOT data.loggedIn">Login</div>
<section subscriptions-section="content-not-granted"> Login or subscribe to read more.</section>
<div subscriptions-section="content" class="article-body" itemprop="articleBody"> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ullamcorper turpis vel commodo scelerisque. Phasellus luctus nunc ut elit cursus, et imperdiet diam vehicula. Duis et nisi sed urna blandit bibendum et sit amet erat. Suspendisse potenti. Curabitur consequat volutpat arcu nec elementum. Etiam a turpis ac libero varius condimentum. Maecenas sollicitudin felis aliquam tortor vulputate, ac posuere velit semper. </p></div>
<template type="amp-mustache" subscriptions-dialog subscriptions-display="data.loggedIn"> Dialog for Subscribers</template><template type="amp-mustache" subscriptions-dialog subscriptions-display="NOT data.loggedIn"> <div>Dialog for non logged in users<div> <div subscriptions-display="NOT data.loggedIn" subscriptions-action="subscribe" subscriptions-decorate>Login</div> <div subscriptions-display="NOT data.subscribed">You are not a subscriber!</div></template>
Implementation
Custom p.html
As mentioned above, for custom builds, we have generated a p.html
file that you can modify to suit your needs. You will be responsible for hosting this file on a domain that matches your login page. This is a good option if you are not using our Identity SDK
, have third-party authentication or entitlement services, or want to have complete control over the data passed between the iframe and the amp document.
You can use these file as a starting point:
Arc XP Commerce Supported Solution
Alongside the p.js file which you already should have access to you will also find a p.html which is customized to work with the generated p.js file. The goal of the officially supported p.html is that you won’t need to change anything. You will simply point the <amp-subscription>
extension to it and set up the rest of the AMP documents to use its attributes. This solution is good if you are using our Identity SDK, Identity API and Sales API.
How it works
From a high level, this is how the AMP version of the Arc XP Paywall works:
- AMP Document loads.
<amp-subscription>
tag is declared with“type”: “iframe”
and the location of p.html specified among other details (for example, content type, location of offers page, etc.).- p.html loads the Arc Paywall script (for example, p.js).
<amp-subscription>
sends anauthorize
event to the iframe.- p.html runs the paywall rules evaluator and returns a Promise with a verdict.
- AMP document reacts appropriately.
AMP READER_ID
AMP will often load publisher content from either an AMP CDN on an AMP domain or from Google AMP Viewer. On these domains, our paywall scripts are loaded in an iframe from the publisher’s public origin. In these contexts, p.js is treated as a 3rd party and Apple’s ITP now blocks 3rd party resources from accessing script writable storage (localStorage, javascript cookies, etc.). This impacts our AMP Paywall because we make use of either cookies or localStorage for managing user tokens and logged in state.
In order to get around this latest change, on Google’s recommendation, we’re leveraging the AMP Reader ID, a persistent and unique ID for the user. Google will pass the READER_ID to any publisher resources loaded in an AMP context as a URL query param.
A new Sdk-Amp handles accepting that READER_ID and saving it to cookies or localStorage from the publisher domain. When the user is in an authenticated state in that domain, the READER_ID also gets associated to the Arc user’s identity record. To leverage this functionality, you’ll want to call AMP.checkAMPReaderID(options, readerId)
on login, heartbeat and options calls. After sign up and login, leverage AMP’s RETURN_URL (placeholder for the return URL specified by the AMP runtime for a Login Dialog to return to) to navigate back to the AMP page. The RETURN_URL query substitution can be used to specify the query parameter for the return URL, for example, ?returnURL=RETURN_URL_
. If the RETURN_URL substitution is not specified, it will be injected automatically with the default query parameter name of “return”.
p.html now handles passing the READER_ID to p.js. When p.js finds a READER_ID, it will be used to fetch entitlements for the current user based on that READER_ID that was associated to the user’s Arc Identity.
Implementation steps
-
Enable AMP READER_ID from paywall overview screen.
-
Install AMP (Arc’s AMP SDK package) from arc-publishing/sdk-amp.
import AMP from 'arc-publishing/sdk-amp' -
Make call to AMP.options(), passing apiOrigin and injectable Identity. Ensure an AMP.options call is made prior to calling AMP.checkAMPReaderID.
AMP.options({Identity: Identity,apiOrigin: 'https://api-example-example-sandbox.cdn.arcpublishing.com',}); -
After login, heartbeat and options calls, call checkAMPReaderID(). This method takes one argument, readerId. Google passes the READER_ID to any publisher resources loaded in an AMP context as a URL query param that defaults to “readerId”.
const url_string = window.location.href;const url = new URL(url_string);const readerId = url.searchParams.get(“readerId”);const ampReaderId = await AMP.checkAMPReaderID(readerId); -
Return to the AMP page using AMP’s RETURN_URL, which defaults to query param ‘return’. See AMP Documentation for more info and configuration options.
// Identity.login....then(() => { if (return) { window.location.replace(`${decodeURI(return)}#success=true`); return; }}).catch(error => { if (returnUrl) { window.location.replace(`${decodeURI(return)}#success=false`); return; }});
Below are code examples encompassing steps outlined above.
<!--AMP page markup--> { "services": [ { "type": "iframe", "iframeSrc": "/amp/p.html", "iframeVars": [ "READER_ID", "CANONICAL_URL", "AMPDOC_URL", "SOURCE_URL", "DOCUMENT_REFERRER", "RETURN_URL" ], "actions":{ "login": "/#/loginL", "subscribe": "/#/offer/live" }, "data": { "contentType": "story", "section": "technology", "contentRestriction": "premium", "apiOrigin": 'https://api-example-example-sandbox.cdn.arcpublishing.com', "identityApiOrigin": 'https://api-example-example-sandbox.cdn.arcpublishing.com' } }, ], "fallbackEntitlement": "example" }
//Login component exampleimport { useEffect } from 'react'import Identity from 'arc-publishing/sdk-identity';import AMP from 'arc-publishing/sdk-amp'
const search = new URLSearchParams(window.location.search);const { return: returnUrl, readerId } = Object.fromEntries(urlSearchParams.entries());
useEffect(() => { AMP.options({ Identity, apiOrigin: 'https://api-example-example-sandbox.cdn.arc-publishing.com' }); }, []);
const onLogin = () => { return Identity.login(username, password, { rememberMe, recaptchaToken }) .then(() => { AMP.checkAMPReaderID( readerId ).then(() => { if (returnUrl) { window.location.replace(`${decodeURI(returnUrl)}#success=true`); return; } history.push('/profile'); }) }) .catch(error => { if (returnUrl) { window.location.replace(`${decodeURI(returnUrl)}#success=false`); return; } });}
Differences between Arc Paywall without AMP and with AMP
The main difference between the two implementations is that the AMP page cannot run arbitrary javascript (for the most part). This means that ArcredP.Run() will not run the paywallFunction
when a rule evaluates as true. You must use the AMP constructs which will let you display a paywall, hide content, or direct a user somewhere else (see: AMP Subscriptions or check below for a simple example.)
Additionally, because of the AMP limitation of not being able to run arbitrary javascript in the document and also because Google hosts AMP pages in their own servers, the Arc Paywall cannot use typical constructs like localStorage, cookies, and the DOM to find the information it needs to render a verdict. This is why the script runs in an iframe and you must pass page-data like the content type and section down to it.
Resources
- AMP Access Client-Side Via iFrame GitHhub Repository (intent to implement: amp-access iframe)
- amp-access-iframe-api (amp-access-iframe API)
- ampproject/amp-iframe NPM Package (Deprecated) (necessary api to communicate w/ AMP document — this is already in p.html)
- Access iframe messaging and handshake implementation GitHub File
- Extensions: amp-access/0.1/iframe-api/iframe-api.js GitHub File