Skip to content

Commit

Permalink
Refactor button rendering for story ads (pt 1) (#30635)
Browse files Browse the repository at this point in the history
* refactor

* fix tests

* new tests

* types

* test clean up

* capitialization

* starts with

* dep check
  • Loading branch information
calebcordry committed Oct 14, 2020
1 parent 73b3f0e commit 008ce60
Show file tree
Hide file tree
Showing 6 changed files with 439 additions and 176 deletions.
2 changes: 1 addition & 1 deletion build-system/test-configs/dep-check-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ exports.rules = [
'extensions/amp-story-auto-ads/0.1/story-ad-page.js->extensions/amp-ad-exit/0.1/config.js',
// TODO(ccordry): remove this after createShadowRootWithStyle is moved to src
'extensions/amp-story-auto-ads/0.1/amp-story-auto-ads.js->extensions/amp-story/1.0/utils.js',
'extensions/amp-story-auto-ads/0.1/story-ad-page.js->extensions/amp-story/1.0/utils.js',
'extensions/amp-story-auto-ads/0.1/story-ad-ui.js->extensions/amp-story/1.0/utils.js',
// Story education
'extensions/amp-story-education/0.1/amp-story-education.js->extensions/amp-story/1.0/amp-story-store-service.js',
'extensions/amp-story-education/0.1/amp-story-education.js->extensions/amp-story/1.0/utils.js',
Expand Down
214 changes: 55 additions & 159 deletions extensions/amp-story-auto-ads/0.1/story-ad-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,34 @@
* limitations under the License.
*/

import {
A4AVarNames,
getStoryAdMetadataFromDoc,
getStoryAdMetadataFromElement,
localizeCtaText,
maybeCreateAttribution,
validateCtaMetadata,
} from './story-ad-ui';
import {
AnalyticsEvents,
AnalyticsVars,
STORY_AD_ANALYTICS,
} from './story-ad-analytics';
import {CommonSignals} from '../../../src/common-signals';
import {CtaTypes} from './story-ad-localization';
import {
StateProperty,
UIType,
} from '../../amp-story/1.0/amp-story-store-service';
import {assertConfig} from '../../amp-ad-exit/0.1/config';
import {assertHttpsUrl} from '../../../src/url';
import {CSS as attributionCSS} from '../../../build/amp-story-auto-ads-attribution-0.1.css';
import {
createElementWithAttributes,
elementByTag,
isJsonScriptTag,
iterateCursor,
openWindowDialog,
toggleAttribute,
} from '../../../src/dom';
import {createShadowRootWithStyle} from '../../amp-story/1.0/utils';
import {dev, user, userAssert} from '../../../src/log';
import {dict} from '../../../src/utils/object';
import {getFrameDoc, getStoryAdMetaTags} from './utils';
import {dev, devAssert, user, userAssert} from '../../../src/log';
import {dict, map} from '../../../src/utils/object';
import {getFrameDoc} from './utils';
import {getServicePromiseForDoc} from '../../../src/service';
import {parseJson} from '../../../src/json';
import {setStyle} from '../../../src/style';
Expand All @@ -56,32 +58,12 @@ const GLASS_PANE_CLASS = 'i-amphtml-glass-pane';
/** @const {string} */
const DESKTOP_FULLBLEED_CLASS = 'i-amphtml-story-ad-fullbleed';

/** @const {string} */
const CTA_META_PREFIX = 'amp-cta-';

/** @const {string} */
const A4A_VARS_META_PREFIX = 'amp4ads-vars-';

/** @enum {string} */
const PageAttributes = {
LOADING: 'i-amphtml-loading',
IFRAME_BODY_VISIBLE: 'amp-story-visible',
};

/** @enum {string} */
const DataAttrs = {
CTA_TYPE: 'data-vars-ctatype',
CTA_URL: 'data-vars-ctaurl',
};

/** @enum {string} */
const A4AVarNames = {
ATTRIBUTION_ICON: 'attribution-icon',
ATTRIBUTION_URL: 'attribution-url',
CTA_TYPE: 'cta-type',
CTA_URL: 'cta-url',
};

export class StoryAdPage {
/**
* @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
Expand Down Expand Up @@ -128,15 +110,9 @@ export class StoryAdPage {
/** @private {?Document} */
this.adDoc_ = null;

/** @private {?string} */
this.ampAdExitOutlink_ = null;

/** @private {boolean} */
this.loaded_ = false;

/** @private @const {!JsonObject} */
this.a4aVars_ = dict();

/** @private @const {!Array<Function>} */
this.loadCallbacks_ = [];

Expand Down Expand Up @@ -252,42 +228,33 @@ export class StoryAdPage {
*/
maybeCreateCta() {
return Promise.resolve().then(() => {
// FIE only. Template ads have no iframe, and we can't access x-domain iframe.
if (this.adDoc_) {
this.extractA4AVars_();
this.readAmpAdExit_();
}

// If making a CTA layer we need a button name & outlink url.
const ctaUrl =
this.ampAdExitOutlink_ ||
this.a4aVars_[A4AVarNames.CTA_URL] ||
this.adElement_.getAttribute(DataAttrs.CTA_URL);

const ctaType =
this.a4aVars_[A4AVarNames.CTA_TYPE] ||
this.adElement_.getAttribute(DataAttrs.CTA_TYPE);
const uiMetadata = map();

if (!ctaUrl || !ctaType) {
user().error(
TAG,
'Both CTA Type & CTA Url are required in ad response.'
// Template Ads.
if (!this.adDoc_) {
Object.assign(
uiMetadata,
getStoryAdMetadataFromElement(devAssert(this.adElement_))
);
} else {
Object.assign(
uiMetadata,
getStoryAdMetadataFromDoc(this.adDoc_),
// TODO(ccordry): Depricate when possible.
this.readAmpAdExit_()
);
return false;
}

let ctaText;
// CTA picked from predefined choices.
if (CtaTypes[ctaType]) {
const ctaLocalizedStringId = CtaTypes[ctaType];
ctaText = this.localizationService_.getLocalizedString(
ctaLocalizedStringId
);
} else {
// Custom CTA text - Should already be localized.
ctaText = ctaType;
if (!validateCtaMetadata(uiMetadata)) {
return false;
}

const ctaText =
localizeCtaText(
uiMetadata[A4AVarNames.CTA_TYPE],
this.localizationService_
) || '';

// Store the cta-type as an accesible var for any further pings.
this.analytics_.then((analytics) =>
analytics.setVar(
Expand All @@ -297,13 +264,23 @@ export class StoryAdPage {
)
);

try {
this.maybeCreateAttribution_();
} catch (e) {
// Failure due to missing adchoices icon or url.
return false;
if (
(this.adChoicesIcon_ = maybeCreateAttribution(
this.win_,
uiMetadata,
devAssert(this.pageElement_)
))
) {
this.storeService_.subscribe(
StateProperty.UI_STATE,
(uiState) => {
this.onUIStateUpdate_(uiState);
},
true /** callToInitialize */
);
}

const ctaUrl = uiMetadata[A4AVarNames.CTA_URL];
return this.createCtaLayer_(ctaUrl, ctaText);
});
}
Expand Down Expand Up @@ -424,25 +401,6 @@ export class StoryAdPage {
});
}

/**
* Find all `amp4ads-vars-` & `amp-cta-` prefixed meta tags and store them
* in single obj.
* @private
*/
extractA4AVars_() {
const storyMetaTags = getStoryAdMetaTags(this.adDoc_);
iterateCursor(storyMetaTags, (tag) => {
const {name, content} = tag;
if (name.startsWith(CTA_META_PREFIX)) {
const key = name.split('amp-')[1];
this.a4aVars_[key] = content;
} else if (name.startsWith(A4A_VARS_META_PREFIX)) {
const key = name.split(A4A_VARS_META_PREFIX)[1];
this.a4aVars_[key] = content;
}
});
}

/**
* TODO(#24080) Remove this when story ads have full ad network support.
* This in intended to be a temporary hack so we can can support
Expand All @@ -452,6 +410,7 @@ export class StoryAdPage {
* If there are multiple exits present, behavior is unpredictable due to
* JSON parse.
* @private
* @return {!Object}
*/
readAmpAdExit_() {
const ampAdExit = elementByTag(
Expand All @@ -472,73 +431,19 @@ export class StoryAdPage {
'be inside a <script> tag with type="application/json"'
);
const config = assertConfig(parseJson(child.textContent));
const target = config['targets'][Object.keys(config['targets'])[0]];
this.ampAdExitOutlink_ = target['finalUrl'];
const target =
config['targets'] &&
Object.keys(config['targets']) &&
config['targets'][Object.keys(config['targets'])[0]];
const finalUrl = target && target['finalUrl'];
return target ? {[A4AVarNames.CTA_URL]: finalUrl} : {};
} catch (e) {
dev().error(TAG, e);
return {};
}
}
}

/**
* Create attribution if creative contains the appropriate meta tags.
* @private
*/
maybeCreateAttribution_() {
const href = this.a4aVars_[A4AVarNames.ATTRIBUTION_URL];
const src = this.a4aVars_[A4AVarNames.ATTRIBUTION_ICON];

// Ad attribution is optional, but need both to render.
if (!href && !src) {
return;
}

assertHttpsUrl(
href,
dev().assertElement(this.pageElement_),
'amp-story-auto-ads attribution url'
);

assertHttpsUrl(
src,
dev().assertElement(this.pageElement_),
'amp-story-auto-ads attribution icon'
);

const root = createElementWithAttributes(
this.doc_,
'div',
dict({
'role': 'button',
'class': 'i-amphtml-attribution-host',
})
);

this.adChoicesIcon_ = createElementWithAttributes(
this.doc_,
'img',
dict({
'class': 'i-amphtml-story-ad-attribution',
'src': src,
})
);
this.storeService_.subscribe(
StateProperty.UI_STATE,
(uiState) => {
this.onUIStateUpdate_(uiState);
},
true /** callToInitialize */
);

this.adChoicesIcon_.addEventListener(
'click',
this.handleAttributionClick_.bind(this, href)
);

createShadowRootWithStyle(root, this.adChoicesIcon_, attributionCSS);
this.pageElement_.appendChild(root);
}

/**
* Reacts to UI state updates and passes the information along as
* attributes to the shadowed attribution icon.
Expand All @@ -556,15 +461,6 @@ export class StoryAdPage {
);
}

/**
* @private
* @param {string} href
* @param {!Event} unusedEvent
*/
handleAttributionClick_(href, unusedEvent) {
openWindowDialog(this.win_, href, '_blank');
}

/**
* Construct an analytics event and trigger it.
* @param {string} eventType
Expand Down

0 comments on commit 008ce60

Please sign in to comment.