Skip to content

Commit

Permalink
✨Implement Scroll App connect flow for Safari (#17987)
Browse files Browse the repository at this point in the history
* amp-access-scroll: Implement Scroll App connect flow for Safari

* Coordinate with the Scroll App on user action via iframe link

* Rename TLD to eTLD for scroll config property
  • Loading branch information
dbow authored and Dima Voytenko committed Sep 24, 2018
1 parent 34e6e54 commit 2b04e46
Showing 1 changed file with 152 additions and 38 deletions.
190 changes: 152 additions & 38 deletions extensions/amp-access-scroll/0.1/scroll-impl.js
Expand Up @@ -21,6 +21,7 @@ import {createElementWithAttributes} from '../../../src/dom';
import {dict} from '../../../src/utils/object';
import {getMode} from '../../../src/mode';
import {installStylesForDoc} from '../../../src/style-installer';
import {parseQueryString} from '../../../src/url';

const TAG = 'amp-access-scroll-elt';

Expand Down Expand Up @@ -85,15 +86,15 @@ const analyticsConfig = connectHostname => {
};

/**
* The TLD for scroll URLs.
* The eTLD for scroll URLs in development mode.
*
* Enables amp-access-scroll to work with dev/staging environments.
*
* @param {!JsonObject} config
* @return {string}
*/
const scrollTld = config => {
return getMode().development && config['tld'] ? config['tld'] : '.scroll.com';
const devEtld = config => {
return getMode().development && config['etld'] ? config['etld'] : '';
};

/**
Expand All @@ -103,7 +104,21 @@ const scrollTld = config => {
* @return {string}
*/
const connectHostname = config => {
return `https://connect${scrollTld(config)}`;
return `https://connect${devEtld(config) || '.scroll.com'}`;
};

/**
* The scroll web server hostname.
*
* @param {!JsonObject} config
* @return {string}
*/
const scrollHostname = config => {
const devScrollEtld = devEtld(config);
if (devScrollEtld) {
return `https://scroll${devScrollEtld}`;
}
return 'https://scroll.com';
};

/**
Expand Down Expand Up @@ -133,24 +148,98 @@ export class ScrollAccessVendor extends AccessClientAdapter {

/** @override */
authorize() {
// TODO(dbow): Handle timeout?
return super.authorize()
.then(response => {
const isStory = this.ampdoc.getRootNode().querySelector(
'amp-story[standalone]');
if (response && response.scroll && !isStory) {
const config = this.accessSource_.getAdapterConfig();
new ScrollElement(this.ampdoc).show(this.accessSource_, config);
addAnalytics(this.ampdoc, config);
if (response && response.scroll) {
if (!isStory) {
const config = this.accessSource_.getAdapterConfig();
new ScrollElement(this.ampdoc).handleScrollUser(
this.accessSource_, config);
addAnalytics(this.ampdoc, config);
}
} else {
if (
response &&
response.blocker &&
ScrollContentBlocker.shouldCheck(this.ampdoc)
) {
new ScrollContentBlocker(this.ampdoc, this.accessSource_).check();
}
}
return response;
});
}
}

/**
* UI for logged-in Scroll users.
* Coordinate with the Scroll App's Content Blocker on Safari browsers.
*/
class ScrollContentBlocker {
/**
* @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
* @return {boolean}
* @static
*/
static shouldCheck(ampdoc) {
const queryParams = parseQueryString(ampdoc.win.location.search);
return !queryParams['scrollnoblockerrefresh'];
}

/**
* @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
* @param {!../../amp-access/0.1/amp-access-source.AccessSource} accessSource
*/
constructor(ampdoc, accessSource) {
/** @const @private {!../../../src/service/ampdoc-impl.AmpDoc} */
this.ampdoc_ = ampdoc;

/** @const @private {!../../amp-access/0.1/amp-access-source.AccessSource} */
this.accessSource_ = accessSource;
}

/**
* Check if the Scroll App blocks the resource request.
*/
check() {
Services.xhrFor(this.ampdoc_.win)
.fetchJson('https://block.scroll.com/check.json')
.then(() => false, e => this.blockedByScrollApp_(e.message))
.then(blockedByScrollApp => {
if (blockedByScrollApp === true) {
// TODO(dbow): Ideally we would automatically redirect to the page
// here, but for now we are adding a button so we redirect on user
// action.
new ScrollElement(this.ampdoc_).addActivateButton(
this.accessSource_, this.accessSource_.getAdapterConfig());
}
});
}

/**
* Whether the given error message indicates the Scroll App blocked the
* request.
*
* @param {string} message
* @return {boolean}
* @private
*/
blockedByScrollApp_(message) {
return (
message.indexOf(
'XHR Failed fetching (https://block.scroll.com/...): ' +
'Resource blocked by content blocker'
) === 0
);
}
}

/**
* UI for Scroll users.
*
* Presents a fixed bar at the bottom of the screen.
* Presents a fixed bar at the bottom of the screen with Scroll content.
*/
class ScrollElement {
/**
Expand All @@ -162,23 +251,10 @@ class ScrollElement {
/** @const {!../../../src/service/ampdoc-impl.AmpDoc} */
this.ampdoc_ = ampdoc;

/** @const {!Element} */
this.placeholder_ = document.createElement('div');
this.placeholder_.classList.add('amp-access-scroll-bar');
this.placeholder_.classList.add('amp-access-scroll-placeholder');
const img = document.createElement('img');
img.setAttribute('src',
'https://static.scroll.com/assets/icn-scroll-logo.svg');
img.setAttribute('layout', 'fixed');
img.setAttribute('width', 26);
img.setAttribute('height', 26);
this.placeholder_.appendChild(img);
ampdoc.getBody().appendChild(this.placeholder_);


/** @const {!Element} */
this.scrollBar_ = document.createElement('div');
this.scrollBar_.classList.add('amp-access-scroll-bar');

/** @const {!Element} */
this.iframe_ = document.createElement('iframe');
this.iframe_.setAttribute('scrolling', 'no');
Expand All @@ -192,26 +268,64 @@ class ScrollElement {
'allow-popups-to-escape-sandbox');
this.scrollBar_.appendChild(this.iframe_);
ampdoc.getBody().appendChild(this.scrollBar_);

// Promote to fixed layer.
Services.viewportForDoc(ampdoc).addToFixedLayer(this.scrollBar_);
}

/**
* Add a scrollbar placeholder and then load the scrollbar URL in the iframe.
*
* @param {!../../amp-access/0.1/amp-access-source.AccessSource} accessSource
* @param {!JsonObject} vendorConfig
*/
show(accessSource, vendorConfig) {
const SCROLLBAR_URL = `${connectHostname(vendorConfig)}/amp/scrollbar`
+ '?rid=READER_ID'
+ '&cid=CLIENT_ID(scroll1)'
+ '&c=CANONICAL_URL'
+ '&o=AMPDOC_URL';
Services.viewportForDoc(this.ampdoc_).addToFixedLayer(this.scrollBar_)
.then(() => accessSource.buildUrl(SCROLLBAR_URL, false))
.then(scrollbarUrl => {
this.iframe_.onload = () => {
this.ampdoc_.getBody().removeChild(this.placeholder_);
};
this.iframe_.setAttribute('src', scrollbarUrl);
});
handleScrollUser(accessSource, vendorConfig) {
// Add a placeholder element to display while scrollbar iframe loads.
const placeholder = document.createElement('div');
placeholder.classList.add('amp-access-scroll-bar');
placeholder.classList.add('amp-access-scroll-placeholder');
const img = document.createElement('img');
img.setAttribute('src',
'https://static.scroll.com/assets/icn-scroll-logo.svg');
img.setAttribute('layout', 'fixed');
img.setAttribute('width', 26);
img.setAttribute('height', 26);
placeholder.appendChild(img);
this.ampdoc_.getBody().appendChild(placeholder);

// Set iframe to scrollbar URL.
accessSource.buildUrl((
`${connectHostname(vendorConfig)}/amp/scrollbar`
+ '?rid=READER_ID'
+ '&cid=CLIENT_ID(scroll1)'
+ '&c=CANONICAL_URL'
+ '&o=AMPDOC_URL'
), false).then(scrollbarUrl => {
this.iframe_.onload = () => {
// On iframe load, remove placeholder element.
this.ampdoc_.getBody().removeChild(placeholder);
};
this.iframe_.setAttribute('src', scrollbarUrl);
});
}

/**
* Add link to the Scroll App connect page.
*
* @param {!../../amp-access/0.1/amp-access-source.AccessSource} accessSource
* @param {!JsonObject} vendorConfig
*/
addActivateButton(accessSource, vendorConfig) {
accessSource.buildUrl((
`${scrollHostname(vendorConfig)}/activateamp`
+ '?rid=READER_ID'
+ '&cid=CLIENT_ID(scroll1)'
+ '&c=CANONICAL_URL'
+ '&o=AMPDOC_URL'
+ '&x=QUERY_PARAM(scrollx)'
), false).then(url => {
this.iframe_.setAttribute('src', url);
});
}
}

Expand Down

0 comments on commit 2b04e46

Please sign in to comment.