Dialog for non subscribed users, the action of this button is configured by the RTC url
+
+ Single SKU
+
Subscribe
+
+
+
+
+ You are already subscribed so you won't see buttons here.
+
+
+
diff --git a/extensions/amp-subscriptions-google/0.1/amp-subscriptions-google.css b/extensions/amp-subscriptions-google/0.1/amp-subscriptions-google.css
index 8808215c1a73..2d3d41660884 100644
--- a/extensions/amp-subscriptions-google/0.1/amp-subscriptions-google.css
+++ b/extensions/amp-subscriptions-google/0.1/amp-subscriptions-google.css
@@ -15,3 +15,14 @@
*/
@import url("../../../third_party/subscriptions-project/swg-button.css");
+
+
+/**
+ * Disabled Action when realtime config has not loaded yet
+ * Can be overridden by publisher CSS
+ */
+[subscriptions-action][subscriptions-google-rtc] {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
diff --git a/extensions/amp-subscriptions-google/0.1/amp-subscriptions-google.js b/extensions/amp-subscriptions-google/0.1/amp-subscriptions-google.js
index 4c4f43408963..bec5a4efcf97 100644
--- a/extensions/amp-subscriptions-google/0.1/amp-subscriptions-google.js
+++ b/extensions/amp-subscriptions-google/0.1/amp-subscriptions-google.js
@@ -35,18 +35,25 @@ import {
} from '../../amp-subscriptions/0.1/entitlement';
import {Services} from '../../../src/services';
import {SubscriptionsScoreFactor} from '../../amp-subscriptions/0.1/constants.js';
+import {UrlBuilder} from '../../amp-subscriptions/0.1/url-builder';
import {WindowInterface} from '../../../src/window-interface';
+import {assertHttpsUrl, parseUrlDeprecated} from '../../../src/url';
import {experimentToggles, isExperimentOn} from '../../../src/experiments';
import {getData} from '../../../src/event-helper';
+import {getMode} from '../../../src/mode';
+import {getValueForExpr} from '../../../src/json';
import {installStylesForDoc} from '../../../src/style-installer';
-import {parseUrlDeprecated} from '../../../src/url';
+
+import {devAssert, user, userAssert} from '../../../src/log';
import {startsWith} from '../../../src/string';
-import {userAssert} from '../../../src/log';
const TAG = 'amp-subscriptions-google';
const PLATFORM_ID = 'subscribe.google.com';
const GOOGLE_DOMAIN_RE = /(^|\.)google\.(com?|[a-z]{2}|com?\.[a-z]{2}|cat)$/;
+/** @const */
+const SERVICE_TIMEOUT = 3000;
+
const SWG_EVENTS_TO_SUPPRESS = {
[AnalyticsEvent.IMPRESSION_PAYWALL]: true,
[AnalyticsEvent.IMPRESSION_PAGE_LOAD]: true,
@@ -104,6 +111,12 @@ export class GoogleSubscriptionsPlatform {
*/
this.serviceAdapter_ = serviceAdapter;
+ /** @private {!../../../src/service/ampdoc-impl.AmpDoc} */
+ this.ampdoc_ = ampdoc;
+
+ /** @private @const {!../../../src/service/vsync-impl.Vsync} */
+ this.vsync_ = Services.vsyncFor(ampdoc.win);
+
/**
* @private @const
* {!../../amp-subscriptions/0.1/analytics.SubscriptionAnalytics}
@@ -113,6 +126,18 @@ export class GoogleSubscriptionsPlatform {
this.handleAnalyticsEvent_.bind(this)
);
+ /** @private @const {!UrlBuilder} */
+ this.urlBuilder_ = new UrlBuilder(
+ this.ampdoc_,
+ this.serviceAdapter_.getReaderId('local')
+ );
+
+ /** @const @private {!../../../src/service/timer-impl.Timer} */
+ this.timer_ = Services.timerFor(this.ampdoc_.win);
+
+ //* @const @private */
+ this.fetcher_ = new AmpFetcher(ampdoc.win);
+
// Map AMP experiments prefixed with 'swg-' to SwG experiments.
const ampExperimentsForSwg = Object.keys(experimentToggles(ampdoc.win))
.filter(
@@ -246,6 +271,15 @@ export class GoogleSubscriptionsPlatform {
// Install styles.
installStylesForDoc(ampdoc, CSS, () => {}, false, TAG);
+
+ /** @private @const {string} */
+ this.skuMapUrl_ = this.serviceConfig_['skuMapUrl'] || null;
+
+ /** @private {JsonObject} */
+ this.skuMap_ = /** @type {!JsonObject} */ ({});
+
+ /** @private {!Promise} */
+ this.rtcPromise_ = this.maybeFetchRealTimeConfig();
}
/**
@@ -309,7 +343,9 @@ export class GoogleSubscriptionsPlatform {
this.getServiceId()
);
} else {
- this.maybeComplete_(this.serviceAdapter_.delegateActionToLocal('login'));
+ this.maybeComplete_(
+ this.serviceAdapter_.delegateActionToLocal('login', null)
+ );
}
}
@@ -333,7 +369,7 @@ export class GoogleSubscriptionsPlatform {
/** @private */
onNativeSubscribeRequest_() {
this.maybeComplete_(
- this.serviceAdapter_.delegateActionToLocal(Action.SUBSCRIBE)
+ this.serviceAdapter_.delegateActionToLocal(Action.SUBSCRIBE, null)
);
}
@@ -349,6 +385,59 @@ export class GoogleSubscriptionsPlatform {
});
}
+ /**
+ * Fetch a real time config if appropriate.
+ *
+ * Note that we don't return the skuMap, instead we save it.
+ * The creates an intentional race condition. If the server
+ * doesn't return a skuMap before the user clicks the subscribe button
+ * the button wil lbe disabled.
+ *
+ * We can't wait for the skumap in the button click becasue it will be popup blocked.
+ *
+ * @return {!Promise}
+ */
+ maybeFetchRealTimeConfig() {
+ let timeout = SERVICE_TIMEOUT;
+ if (getMode().development || getMode().localDev) {
+ timeout = SERVICE_TIMEOUT * 2;
+ }
+
+ if (!this.skuMapUrl_) {
+ return Promise.resolve();
+ }
+
+ assertHttpsUrl(this.skuMapUrl_, 'skuMapUrl must be valid https Url');
+ // RTC is never pre-render safe
+ return this.ampdoc_
+ .whenFirstVisible()
+ .then(() =>
+ this.urlBuilder_.buildUrl(
+ /** @type {string } */ (this.skuMapUrl_),
+ /* useAuthData */ false
+ )
+ )
+ .then((url) =>
+ this.timer_.timeoutPromise(
+ timeout,
+ this.fetcher_.fetchCredentialedJson(url)
+ )
+ )
+ .then((resJson) => {
+ userAssert(
+ resJson['subscribe.google.com'],
+ 'skuMap does not contain subscribe.google.com section'
+ );
+ this.skuMap_ = resJson['subscribe.google.com'];
+ })
+ .catch((reason) => {
+ throw user().createError(
+ `fetch skuMap failed for ${PLATFORM_ID}`,
+ reason
+ );
+ });
+ }
+
/**
* @param {!SubscribeResponseInterface} response
* @param {string} eventType
@@ -521,30 +610,69 @@ export class GoogleSubscriptionsPlatform {
}
/** @override */
- executeAction(action) {
+ executeAction(action, sourceId) {
/**
- * The contribute and subscribe flows are not called
- * directly with a sku to avoid baking sku detail into
- * a page that may be cached for an extended time.
- * Instead we use showOffers and showContributionOptions
- * which get sku info from the server.
- *
* Note: we do handle events form the contribute and
* subscribe flows elsewhere since they are invoked after
* offer selection.
*/
+ let mappedSku, carouselOptions;
+ /*
+ * If the id of the source element (sourceId) is in a map supplied via
+ * the skuMap Url we use that to lookup which sku to associate this button
+ * with.
+ */
+ const rtcPending = sourceId
+ ? this.ampdoc_
+ .getElementById(sourceId)
+ .hasAttribute('subscriptions-google-rtc')
+ : false;
+ // if subscriptions-google-rtc is set then this element is configured by the
+ // rtc url but has not yet been configured so we ignore it.
+ // Once the rtc resolves the attribute is changed to subscriptions-google-rtc-set.
+ if (rtcPending) {
+ return;
+ }
+ if (sourceId && this.skuMap_) {
+ mappedSku = getValueForExpr(this.skuMap_, `${sourceId}.sku`);
+ carouselOptions = getValueForExpr(
+ this.skuMap_,
+ `${sourceId}.carouselOptions`
+ );
+ }
if (action == Action.SUBSCRIBE) {
- this.runtime_.showOffers({
- list: 'amp',
- isClosable: true,
- });
+ if (mappedSku) {
+ // publisher provided single sku
+ this.runtime_.subscribe(mappedSku);
+ } else if (carouselOptions) {
+ // publisher provided carousel options, must always be closable
+ carouselOptions.isClosable = true;
+ this.runtime_.showOffers(carouselOptions);
+ } else {
+ // no mapping just use the amp carousel
+ this.runtime_.showOffers({
+ list: 'amp',
+ isClosable: true,
+ });
+ }
return Promise.resolve(true);
}
+ // Same idea as above but it's contribute instead of subscribe
if (action == Action.CONTRIBUTE) {
- this.runtime_.showContributionOptions({
- list: 'amp',
- isClosable: true,
- });
+ if (mappedSku) {
+ // publisher provided single sku
+ this.runtime_.contribute(mappedSku);
+ } else if (carouselOptions) {
+ // publisher provided carousel options, must always be closable
+ carouselOptions.isClosable = true;
+ this.runtime_.showContributionOptions(carouselOptions);
+ } else {
+ // no mapping just use the amp carousel
+ this.runtime_.showContributionOptions({
+ list: 'amp',
+ isClosable: true,
+ });
+ }
return Promise.resolve(true);
}
if (action == Action.LOGIN) {
@@ -589,6 +717,29 @@ export class GoogleSubscriptionsPlatform {
default:
// do nothing
}
+ // enable any real time buttons once it's resolved.
+ this.rtcPromise_.then(() => {
+ this.vsync_.mutate(() =>
+ Object.keys(/** @type {!Object} */ (this.skuMap_)).forEach(
+ (elementId) => {
+ const element = this.ampdoc_.getElementById(elementId);
+ if (element) {
+ devAssert(
+ element.hasAttribute('subscriptions-google-rtc'),
+ `Trying to set real time config on element '${elementId}' with missing 'subscriptions-google-rtc' attrbute`
+ );
+ element.setAttribute('subscriptions-google-rtc-set', '');
+ element.removeAttribute('subscriptions-google-rtc');
+ } else {
+ user().warn(
+ TAG,
+ `Element "{elemendId}" in real time config not found`
+ );
+ }
+ }
+ )
+ );
+ });
}
}
diff --git a/extensions/amp-subscriptions-google/0.1/test/test-amp-subscriptions-google.js b/extensions/amp-subscriptions-google/0.1/test/test-amp-subscriptions-google.js
index 9cb44b41e7e9..5183339701da 100644
--- a/extensions/amp-subscriptions-google/0.1/test/test-amp-subscriptions-google.js
+++ b/extensions/amp-subscriptions-google/0.1/test/test-amp-subscriptions-google.js
@@ -117,6 +117,7 @@ describes.realWin('amp-subscriptions-google', {amp: true}, (env) => {
let ackStub;
let element;
let entitlementResponse;
+ let rtcButtonElement;
let win;
beforeEach(() => {
@@ -135,6 +136,9 @@ describes.realWin('amp-subscriptions-google', {amp: true}, (env) => {
env.sandbox
.stub(serviceAdapter, 'getPageConfig')
.callsFake(() => pageConfig);
+ env.sandbox
+ .stub(serviceAdapter, 'getReaderId')
+ .callsFake(() => Promise.resolve('ari1'));
const analytics = new SubscriptionAnalytics(ampdoc.getRootNode());
env.sandbox.stub(serviceAdapter, 'getAnalytics').callsFake(() => analytics);
analyticsMock = env.sandbox.mock(analytics);
@@ -180,6 +184,7 @@ describes.realWin('amp-subscriptions-google', {amp: true}, (env) => {
ConfiguredRuntime.prototype,
'showContributionOptions'
),
+ subscribe: env.sandbox.stub(ConfiguredRuntime.prototype, 'subscribe'),
showOffers: env.sandbox.stub(ConfiguredRuntime.prototype, 'showOffers'),
showAbbrvOffer: env.sandbox.stub(
ConfiguredRuntime.prototype,
@@ -373,11 +378,6 @@ describes.realWin('amp-subscriptions-google', {amp: true}, (env) => {
});
it('should start linking flow when requested', async () => {
- serviceAdapterMock
- .expects('getReaderId')
- .withExactArgs('local')
- .returns(Promise.resolve('ari1'))
- .once();
serviceAdapterMock.expects('delegateActionToLocal').never();
callback(callbacks.loginRequest)({linkRequested: true});
await 'Event loop tick';
@@ -389,7 +389,7 @@ describes.realWin('amp-subscriptions-google', {amp: true}, (env) => {
it('should delegate login when linking not requested', () => {
serviceAdapterMock
.expects('delegateActionToLocal')
- .withExactArgs(Action.LOGIN)
+ .withExactArgs(Action.LOGIN, null)
.returns(Promise.resolve(false))
.once();
callback(callbacks.loginRequest)({linkRequested: false});
@@ -400,7 +400,7 @@ describes.realWin('amp-subscriptions-google', {amp: true}, (env) => {
platform.isGoogleViewer_ = false;
serviceAdapterMock
.expects('delegateActionToLocal')
- .withExactArgs(Action.LOGIN)
+ .withExactArgs(Action.LOGIN, null)
.returns(Promise.resolve(false))
.once();
callback(callbacks.loginRequest)({linkRequested: true});
@@ -529,7 +529,7 @@ describes.realWin('amp-subscriptions-google', {amp: true}, (env) => {
it('should delegate native subscribe request', () => {
serviceAdapterMock
.expects('delegateActionToLocal')
- .withExactArgs(Action.SUBSCRIBE)
+ .withExactArgs(Action.SUBSCRIBE, null)
.returns(Promise.resolve(false))
.once();
callback(callbacks.subscribeRequest)();
@@ -539,7 +539,7 @@ describes.realWin('amp-subscriptions-google', {amp: true}, (env) => {
const loginResult = Promise.resolve(true);
serviceAdapterMock
.expects('delegateActionToLocal')
- .withExactArgs(Action.LOGIN)
+ .withExactArgs(Action.LOGIN, null)
.returns(loginResult)
.once();
callback(callbacks.loginRequest)({linkRequested: false});
@@ -552,7 +552,7 @@ describes.realWin('amp-subscriptions-google', {amp: true}, (env) => {
const loginResult = Promise.resolve(false);
serviceAdapterMock
.expects('delegateActionToLocal')
- .withExactArgs(Action.LOGIN)
+ .withExactArgs(Action.LOGIN, null)
.returns(loginResult)
.once();
callback(callbacks.loginRequest)({linkRequested: false});
@@ -565,7 +565,7 @@ describes.realWin('amp-subscriptions-google', {amp: true}, (env) => {
const loginResult = Promise.resolve(true);
serviceAdapterMock
.expects('delegateActionToLocal')
- .withExactArgs(Action.SUBSCRIBE)
+ .withExactArgs(Action.SUBSCRIBE, null)
.returns(loginResult)
.once();
callback(callbacks.subscribeRequest)();
@@ -703,6 +703,57 @@ describes.realWin('amp-subscriptions-google', {amp: true}, (env) => {
expect(executeStub).to.be.calledWith({list: 'amp', isClosable: true});
});
+ it('should do nothing if rtc mapped button is not read ', () => {
+ // rtc button
+ rtcButtonElement = env.win.document.createElement('button');
+ rtcButtonElement.setAttribute('subscriptions-google-rtc', '');
+ rtcButtonElement.id = 'rtcTestButton';
+ env.win.document.body.appendChild(rtcButtonElement);
+ platform.skuMap_ = {
+ rtcTestButton: {
+ sku: 'testSku',
+ },
+ };
+ const executeStub = platform.runtime_.subscribe;
+ platform.executeAction(Action.SUBSCRIBE, 'rtcTestButton');
+ expect(executeStub).to.not.be.called;
+ });
+
+ it('should show subscribe flow if single sku is mapped ', () => {
+ // rtc button
+ rtcButtonElement = env.win.document.createElement('button');
+ rtcButtonElement.setAttribute('subscriptions-google-rtc-set', '');
+ rtcButtonElement.id = 'rtcTestButton';
+ env.win.document.body.appendChild(rtcButtonElement);
+ platform.skuMap_ = {
+ rtcTestButton: {
+ sku: 'testSku',
+ },
+ };
+ const executeStub = platform.runtime_.subscribe;
+ platform.executeAction(Action.SUBSCRIBE, 'rtcTestButton');
+ expect(executeStub).to.be.calledWith('testSku');
+ });
+
+ it("should show offers if multiple sku's are mapped", () => {
+ // rtc button
+ rtcButtonElement = env.win.document.createElement('button');
+ rtcButtonElement.setAttribute('subscriptions-google-rtc-set', '');
+ rtcButtonElement.id = 'rtcTestButton';
+ env.win.document.body.appendChild(rtcButtonElement);
+ platform.skuMap_ = {
+ rtcTestButton: {
+ carouselOptions: {skus: ['testSku1', 'testsku2']},
+ },
+ };
+ const executeStub = platform.runtime_.showOffers;
+ platform.executeAction(Action.SUBSCRIBE, 'rtcTestButton');
+ expect(executeStub).to.be.calledWith({
+ isClosable: true,
+ skus: ['testSku1', 'testsku2'],
+ });
+ });
+
it('should show contributions if contribute action is delegated', () => {
const executeStub = platform.runtime_.showContributionOptions;
platform.executeAction(Action.CONTRIBUTE);
@@ -710,11 +761,6 @@ describes.realWin('amp-subscriptions-google', {amp: true}, (env) => {
});
it('should link accounts if login action is delegated', async () => {
- serviceAdapterMock
- .expects('getReaderId')
- .withExactArgs('local')
- .returns(Promise.resolve('ari1'))
- .once();
const executeStub = platform.runtime_.linkAccount;
platform.executeAction(Action.LOGIN);
await 'Event loop tick';
diff --git a/extensions/amp-subscriptions-google/amp-subscriptions-google.md b/extensions/amp-subscriptions-google/amp-subscriptions-google.md
index 50c1615432bd..fb12140bf4b4 100644
--- a/extensions/amp-subscriptions-google/amp-subscriptions-google.md
+++ b/extensions/amp-subscriptions-google/amp-subscriptions-google.md
@@ -64,6 +64,53 @@ The `amp-subscriptions-google` is configured as part of `amp-subscriptions` conf
```
+## Real Time Config (rtc)
+
+Real Time Config allows the publisher to specify the sku or sku's for a subscribe button at page load time. The allows user specific offers, time limited offers etc.
+
+To enable rtc add a `skuMapUrl` to the `subscribe.google.com` service.
+
+```html
+
+```
+
+The `skuMapUrl` is called on page load. It should be a map of element id's and configurations:
+
+```JSON
+{
+ "subscribe.google.com': {
+ // button that goes straight to purchase flow
+ "elementId": {
+ "sku": "sku"
+ },
+ // button that launches an offer carousel
+ "anotherElementId": {
+ 'carouselOptions': {
+ 'skus': ['basic', 'premium_monthly'],
+ }
+ }
+ }
+}
+```
+
+Each configuration corresponds to the sku or skus associated with the button.
+
+To enable a button for rtc add the `subscriptions-google-rtc` attribute. If this attribute is present the button will be disabled until the skuMapUrl request is completed. Once the skuMap is resolved the `subscriptions-google-rtc` attribute will be removed and `subscriptions-google-rtc-set` attribute added. These attributes may be used for CSS styling, however it is recommended that the button not be hidden if it will cause a page re-layout when displayed.
+
+Note: The `skuMapUrl` can be the same as the local service auth url as the JSON objects do not conflict. If the auth url is cacheable (`max-age=1` is sufficient) this will allow in a single request to the server to resove authentication and mapping.
+
## Entitlements pingback
As described in [amp-subscriptions](../amp-subscriptions/amp-subscriptions.md#pingback-endpoint), if a `pingbackUrl` is specified by the local service, the entitlements response returned by the "winning" service will be sent to the `pingbackUrl` via a POST request.
diff --git a/extensions/amp-subscriptions/0.1/amp-subscriptions.js b/extensions/amp-subscriptions/0.1/amp-subscriptions.js
index 38acf31ff818..3020f39d83c1 100644
--- a/extensions/amp-subscriptions/0.1/amp-subscriptions.js
+++ b/extensions/amp-subscriptions/0.1/amp-subscriptions.js
@@ -312,85 +312,6 @@ export class SubscriptionService {
return this.platformStore_.selectPlatformForLogin();
}
- /**
- * Reset all platforms and re-fetch entitlements after an
- * external event (for example a login)
- * @return {!Promise}
- */
- resetPlatforms() {
- return this.initialize_().then(() => {
- this.platformStore_ = this.platformStore_.resetPlatformStore();
- this.maybeAddFreeEntitlement_(this.platformStore_);
-
- this.renderer_.toggleLoading(true);
-
- this.platformStore_
- .getAvailablePlatforms()
- .forEach((subscriptionPlatform) => {
- this.fetchEntitlements_(subscriptionPlatform);
- });
- this.subscriptionAnalytics_.serviceEvent(
- SubscriptionAnalyticsEvents.PLATFORM_REAUTHORIZED,
- ''
- );
- // deprecated event fired for backward compatibility
- this.subscriptionAnalytics_.serviceEvent(
- SubscriptionAnalyticsEvents.PLATFORM_REAUTHORIZED_DEPRECATED,
- ''
- );
- this.startAuthorizationFlow_();
- });
- }
-
- /**
- * Delegates an action to local platform.
- * @param {string} action
- * @return {!Promise
}
- */
- delegateActionToLocal(action) {
- return this.delegateActionToService(action, 'local');
- }
-
- /**
- * Delegates an action to specified platform.
- * @param {string} action
- * @param {string} serviceId
- * @return {!Promise}
- */
- delegateActionToService(action, serviceId) {
- return new Promise((resolve) => {
- this.platformStore_.onPlatformResolves(serviceId, (platform) => {
- devAssert(platform, 'Platform is not registered');
- this.subscriptionAnalytics_.event(
- SubscriptionAnalyticsEvents.ACTION_DELEGATED,
- dict({
- 'action': action,
- 'serviceId': serviceId,
- }),
- dict({
- 'action': action,
- 'status': ActionStatus.STARTED,
- })
- );
- resolve(platform.executeAction(action));
- });
- });
- }
-
- /**
- * Delegate UI decoration to another service.
- * @param {!Element} element
- * @param {string} serviceId
- * @param {string} action
- * @param {?JsonObject} options
- */
- decorateServiceAction(element, serviceId, action, options) {
- this.platformStore_.onPlatformResolves(serviceId, (platform) => {
- devAssert(platform, 'Platform is not registered');
- platform.decorateUI(element, action, options);
- });
- }
-
/**
* Returns promise that resolves when page and platform configs are processed.
* @return {!Promise}
@@ -700,6 +621,82 @@ export class SubscriptionService {
return null;
}
+ /**
+ * Reset all platforms and re-fetch entitlements after an
+ * external event (for example a login)
+ */
+ resetPlatforms() {
+ this.platformStore_ = this.platformStore_.resetPlatformStore();
+ this.renderer_.toggleLoading(true);
+
+ this.platformStore_
+ .getAvailablePlatforms()
+ .forEach((subscriptionPlatform) => {
+ this.fetchEntitlements_(subscriptionPlatform);
+ });
+ this.subscriptionAnalytics_.serviceEvent(
+ SubscriptionAnalyticsEvents.PLATFORM_REAUTHORIZED,
+ ''
+ );
+ // deprecated event fired for backward compatibility
+ this.subscriptionAnalytics_.serviceEvent(
+ SubscriptionAnalyticsEvents.PLATFORM_REAUTHORIZED_DEPRECATED,
+ ''
+ );
+ this.startAuthorizationFlow_();
+ }
+
+ /**
+ * Delegates an action to local platform.
+ * @param {string} action
+ * @param {?string} sourceId
+ * @return {!Promise}
+ */
+ delegateActionToLocal(action, sourceId) {
+ return this.delegateActionToService(action, 'local', sourceId);
+ }
+
+ /**
+ * Delegates an action to specified platform.
+ * @param {string} action
+ * @param {string} serviceId
+ * @param {?string} sourceId
+ * @return {!Promise}
+ */
+ delegateActionToService(action, serviceId, sourceId = null) {
+ return new Promise((resolve) => {
+ this.platformStore_.onPlatformResolves(serviceId, (platform) => {
+ devAssert(platform, 'Platform is not registered');
+ this.subscriptionAnalytics_.event(
+ SubscriptionAnalyticsEvents.ACTION_DELEGATED,
+ dict({
+ 'action': action,
+ 'serviceId': serviceId,
+ }),
+ dict({
+ 'action': action,
+ 'status': ActionStatus.STARTED,
+ })
+ );
+ resolve(platform.executeAction(action, sourceId));
+ });
+ });
+ }
+
+ /**
+ * Delegate UI decoration to another service.
+ * @param {!Element} element
+ * @param {string} serviceId
+ * @param {string} action
+ * @param {?JsonObject} options
+ */
+ decorateServiceAction(element, serviceId, action, options) {
+ this.platformStore_.onPlatformResolves(serviceId, (platform) => {
+ devAssert(platform, 'Platform is not registered');
+ platform.decorateUI(element, action, options);
+ });
+ }
+
/**
* Adds entitlement on free pages.
* @param {PlatformStore} platformStore
diff --git a/extensions/amp-subscriptions/0.1/local-subscription-platform-base.js b/extensions/amp-subscriptions/0.1/local-subscription-platform-base.js
index 087b8b9fd7b5..6dd51f782fe2 100644
--- a/extensions/amp-subscriptions/0.1/local-subscription-platform-base.js
+++ b/extensions/amp-subscriptions/0.1/local-subscription-platform-base.js
@@ -146,7 +146,7 @@ export class LocalSubscriptionBasePlatform {
const action = element.getAttribute('subscriptions-action');
const serviceAttr = element.getAttribute('subscriptions-service');
if (serviceAttr == 'local') {
- this.executeAction(action);
+ this.executeAction(action, element.id);
} else if ((serviceAttr || 'auto') == 'auto') {
if (action == Action.LOGIN) {
// The "login" action is somewhat special b/c viewers can
@@ -154,13 +154,18 @@ export class LocalSubscriptionBasePlatform {
const platform = this.serviceAdapter_.selectPlatformForLogin();
this.serviceAdapter_.delegateActionToService(
action,
- platform.getServiceId()
+ platform.getServiceId(),
+ element.id
);
} else {
- this.executeAction(action);
+ this.executeAction(action, element.id);
}
} else if (serviceAttr) {
- this.serviceAdapter_.delegateActionToService(action, serviceAttr);
+ this.serviceAdapter_.delegateActionToService(
+ action,
+ serviceAttr,
+ element.id
+ );
}
}
}
diff --git a/extensions/amp-subscriptions/0.1/service-adapter.js b/extensions/amp-subscriptions/0.1/service-adapter.js
index abd4e70a5d85..3ae7da1454f7 100644
--- a/extensions/amp-subscriptions/0.1/service-adapter.js
+++ b/extensions/amp-subscriptions/0.1/service-adapter.js
@@ -77,20 +77,26 @@ export class ServiceAdapter {
/**
* Delegates actions to local platform.
* @param {string} action
+ * @param {?string} sourceId
* @return {!Promise}
*/
- delegateActionToLocal(action) {
- return this.delegateActionToService(action, 'local');
+ delegateActionToLocal(action, sourceId) {
+ return this.delegateActionToService(action, 'local', sourceId);
}
/**
* Delegates actions to a given service.
* @param {string} action
* @param {string} serviceId
+ * @param {?string} sourceId
* @return {!Promise}
*/
- delegateActionToService(action, serviceId) {
- return this.subscriptionService_.delegateActionToService(action, serviceId);
+ delegateActionToService(action, serviceId, sourceId) {
+ return this.subscriptionService_.delegateActionToService(
+ action,
+ serviceId,
+ sourceId
+ );
}
/**
diff --git a/extensions/amp-subscriptions/0.1/subscription-platform.js b/extensions/amp-subscriptions/0.1/subscription-platform.js
index 9c323409e129..9f2a2189d4b7 100644
--- a/extensions/amp-subscriptions/0.1/subscription-platform.js
+++ b/extensions/amp-subscriptions/0.1/subscription-platform.js
@@ -84,9 +84,10 @@ export class SubscriptionPlatform {
/**
* Executes action for the local platform.
* @param {string} unusedAction
+ * @param {?string} unusedSourceId
* @return {!Promise}
*/
- executeAction(unusedAction) {}
+ executeAction(unusedAction, unusedSourceId) {}
/**
* Returns the base score configured for the platform.
diff --git a/extensions/amp-subscriptions/0.1/viewer-subscription-platform.js b/extensions/amp-subscriptions/0.1/viewer-subscription-platform.js
index ab975a8644ec..e3cf4a5c1173 100644
--- a/extensions/amp-subscriptions/0.1/viewer-subscription-platform.js
+++ b/extensions/amp-subscriptions/0.1/viewer-subscription-platform.js
@@ -268,8 +268,8 @@ export class ViewerSubscriptionPlatform {
}
/** @override */
- executeAction(action) {
- return this.platform_.executeAction(action);
+ executeAction(action, sourceId) {
+ return this.platform_.executeAction(action, sourceId);
}
/** @override */
diff --git a/src/purifier/sanitation.js b/src/purifier/sanitation.js
index 8fb9c06da0e6..1c80eb78566a 100644
--- a/src/purifier/sanitation.js
+++ b/src/purifier/sanitation.js
@@ -192,6 +192,8 @@ export const ALLOWLISTED_ATTRS = [
'subscriptions-display',
'subscriptions-section',
'subscriptions-service',
+ // Attributes for amp-subscriptions-google.
+ 'subscriptions-google-rtc',
// Attributes for amp-nested-menu.
'amp-nested-submenu',
'amp-nested-submenu-open',
diff --git a/validator/validator-main.protoascii b/validator/validator-main.protoascii
index e01eab0bdc8b..7efdb8ba0188 100644
--- a/validator/validator-main.protoascii
+++ b/validator/validator-main.protoascii
@@ -5778,6 +5778,12 @@ attr_lists: {
name: "subscriptions-service"
requires_extension: "amp-subscriptions"
}
+ # amp-subscriptions-google specific attributes, see
+ # https://amp.dev/documentation/components/amp-subscriptions-google
+ attrs: {
+ name: "subscriptions-google-rtc"
+ requires_extension: "amp-subscriptions-google"
+ }
# amp-next-page specific attributes, see
# https://amp.dev/documentation/components/amp-next-page
attrs: {