Skip to content

Commit

Permalink
initial draft amp-mraid implementation (ampproject#19531)
Browse files Browse the repository at this point in the history
* initial draft amp-mraid implementation

* responding to reviewer comments:

* switched amp-mraid from a custom element to a host api
* minor things

* rework validation doc

* move MraidService to its own file, minor tweaks

* finish splitting out mraid service

* rework error handling

* document why impedance matching isn't needed

* not_supported -> unsupported

* make lint and type checker happy
  • Loading branch information
jeffkaufman authored and nbeloglazov committed Feb 12, 2019
1 parent 16b53a7 commit f1e30f2
Show file tree
Hide file tree
Showing 9 changed files with 486 additions and 30 deletions.
2 changes: 1 addition & 1 deletion build-system/tasks/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ function compile(entryModuleFilenames, outputDir, outputFilename, options) {
// Needed for AmpViewerIntegrationVariableService
'extensions/amp-viewer-integration/**/*.js',
'src/*.js',
'src/!(inabox)*/**/*.js',
'src/**/*.js',
'!third_party/babel/custom-babel-helpers.js',
// Exclude since it's not part of the runtime/extension binaries.
'!extensions/amp-access/0.1/amp-login-done.js',
Expand Down
1 change: 1 addition & 0 deletions bundles.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ exports.extensionBundles = [
{name: 'amp-youtube', version: '0.1', type: TYPES.MEDIA},
{name: 'amp-mowplayer', version: '0.1', type: TYPES.MEDIA},
{name: 'amp-powr-player', version: '0.1', type: TYPES.MEDIA},
{name: 'amp-mraid', version: '0.1', type: TYPES.AD},
];

exports.aliasBundles = [
Expand Down
72 changes: 72 additions & 0 deletions examples/mraid/inabox-mraid.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<!doctype html>
<html ⚡4ads>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<style amp4ads-boilerplate>body{visibility:hidden}</style>
<meta name="amp4ads-id" content="vendor=doubleclick,type=impression-id,value=12345">
<script async custom-element="amp-analytics" src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"></script>
<script async host-api="amp-mraid" src="https://cdn.ampproject.org/v0/amp-mraid-0.1.js"></script>
<script async src="https://cdn.ampproject.org/v0.js"></script>
<!-- <script async src="https://cdn.ampproject.org/amp4ads-v0.js"></script> -->
<style amp-custom>
.left {
text-align: left;
}
.right {
text-align: right;
}
</style>
</head>
<body>
<div class="left">
<amp-img id="img-1" width="150" height="100"
src="https://lh3.googleusercontent.com/5rcQ32ml8E5ONp9f9-Rf78IofLb9QjS5_0mqsY1zEFc=w300-h50-no">
</amp-img>
</div>
<div class="right">
<a href="https://google.com" target="_blank">
<amp-img id="img-2" width="150" height="100"
src="https://lh3.googleusercontent.com/5rcQ32ml8E5ONp9f9-Rf78IofLb9QjS5_0mqsY1zEFc=w300-h80-no">
</amp-img>
</a>
</div>
<amp-pixel src="https://www.google.com/?cid=CLIENT_ID(a)"></amp-pixel>
<amp-analytics>
<script type="application/json">
{
"transport": {"beacon": false, "xhrpost": false},
"requests": {
"visibility": "/${type}?cid=CLIENT_ID(a)&elementX=${elementX}&elementY=${elementY}&elementWidth=${elementWidth}&elementHeight=${elementHeight}&totalTime=${totalTime}&totalVisibleTime=${totalVisibleTime}&maxContinuousVisibleTime=${maxContinuousVisibleTime}&loadTimeVisibility=${loadTimeVisibility}&backgrounded=${backgrounded}&backgroundedAtStart=${backgroundedAtStart}&firstSeenTime=${firstSeenTime}&lastSeenTime=${lastSeenTime}&firstVisibleTime=${firstVisibleTime}&lastVisibleTime=${lastVisibleTime}&minVisiblePercentage=${minVisiblePercentage}&maxVisiblePercentage=${maxVisiblePercentage}&intersectionRatio=${intersectionRatio}"
},
"triggers": {
"visible": {
"on": "visible",
"request": "visibility",
"vars": { "type": "visible" }
},
"rootVisible": {
"on": "visible",
"request": "visibility",
"visibilitySpec": {
"selector": ":root",
"visiblePercentageMin": 0
},
"vars": { "type": "rootVisible" }
},
"imgVisible": {
"on": "visible",
"request": "visibility",
"visibilitySpec": {
"selector": "#img-2",
"selectionMethod": "scope",
"visiblePercentageMin": 0
},
"vars": { "type": "img2Visible" }
}
}
}
</script>
</amp-analytics>
</body>
</html>
25 changes: 25 additions & 0 deletions examples/mraid/mraid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Fake mraid.js that lets us pretend to be in a mobile app environment.
window.mraid = {};
window.mraid.getState = function() {
return Math.random() < 0.5 ? 'ready' : 'loading';
};
window.mraid.addEventListener = function(event, callback) {
if (event === 'ready') {
window.setTimeout(1000, callback);
} else if (event === 'exposureChange') {
window.setTimeout(1000, function() {
callback(.7, null, null);
});
} else {
console.log('unknown event ' + event);
}
};
window.mraid.close = function() {
console.log('close');
};
window.mraid.open = function (url) {
console.log('open ' + url);
};
window.mraid.expand = function() {
console.log('expand');
};
176 changes: 176 additions & 0 deletions extensions/amp-mraid/0.1/amp-mraid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* Copyright 2018 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* @fileoverview Connects AMP Host Services to MRAID to allow invoking native
* APIs from mobile app ad webviews.
*
* Example:
* <code>
* <script async host-api="amp-mraid"
* fallback-on="mismatch"></script>
* </code>
*
*/

import {
HostServiceError,
HostServices,
} from '../../../src/inabox/host-services';
import {MraidService} from './mraid-service';
import {dev} from '../../../src/log';
import {getMode} from '../../../src/mode';

const TAG = 'amp-mraid';
const FALLBACK_ON = 'fallback-on';

/**
* String representations of the HostServicesErrors that can be used in the
* 'fallback-on' attribute.
*
* @const @enum {number}
*/
const FallbackErrorNames = {
'mismatch': HostServiceError.MISMATCH,
'unsupported': HostServiceError.UNSUPPORTED,
};

/**
* Loads mraid.js if available, and once it's loaded looks good, configures an
* MraidService to handle visibility, fullscreen, and exit.
*/
export class MraidInitializer {
/**
* @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
*/
constructor(ampdoc) {
/** @private {!../../../src/service/ampdoc-impl.AmpDoc} */
this.ampdoc_ = ampdoc;

/** @private {!Array<number>} */
this.fallbackOn_ = [];

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

/** @private */
this.mraid_ = null;

const ampMraidScripts = this.ampdoc_.getHeadNode().querySelectorAll(
'script[host-api="amp-mraid"]');
if (ampMraidScripts.length > 1) {
dev().error(TAG, 'Multiple amp-mraid scripts.');
return;
} else if (ampMraidScripts.length < 1) {
dev().error(TAG, 'Missing amp-mraid scripts.');
return;
}
const element = ampMraidScripts[0];

if (getMode().runtime !== 'inabox') {
dev().error(TAG, 'Only supported with Inabox');
return;
}

this.fallbackOn_ = [];
const fallbackOnErrorNames =
(element.getAttribute(FALLBACK_ON) || '').split(' ');
for (let i = 0; i < fallbackOnErrorNames.length; i++) {
const errorName = fallbackOnErrorNames[i];
if (errorName) {
if (!(errorName in FallbackErrorNames)) {
dev().error(TAG, `Unknown ${FALLBACK_ON} "${errorName}"`);
return;
}
this.fallbackOn_.push(FallbackErrorNames[errorName]);
}
}

// It looks like we're initiating a network load for mraid from a relative
// url, but this will actually be intercepted by the mobile app SDK and
// handled locally.
const mraidJs = document.createElement('script');
mraidJs.setAttribute('type', 'text/javascript');
mraidJs.setAttribute('src', 'mraid.js');
mraidJs.addEventListener('load', () => {
this.mraidLoadSuccess_();
});
mraidJs.addEventListener('error', () => {
this.handleError_(HostServiceError.MISMATCH);
});
const head = document.getElementsByTagName('head').item(0);
head.appendChild(mraidJs);
}

/**
* @param {number} hostServiceError
*/
handleError_(hostServiceError) {
if (!this.registeredWithHostServices_ &&
this.fallbackOn_.includes(hostServiceError)) {
this.declineService_();
}
// TODO: send error ping
}

/**
* Runs when MRAID reports that it is ready.
*/
mraidReady_() {
const mraidService = new MraidService(this.mraid_);

HostServices.installVisibilityServiceForDoc(
this.ampdoc_, () => mraidService);
HostServices.installFullscreenServiceForDoc(
this.ampdoc_, () => mraidService);
HostServices.installExitServiceForDoc(
this.ampdoc_, () => mraidService);

this.registeredWithHostServices_ = true;
}

/**
* Runs if mraid.js was loaded successfully.
*/
mraidLoadSuccess_() {
const mraid = window['mraid'];
if (!mraid || !mraid.getState || !mraid.addEventListener
|| !mraid.close || !mraid.open || !mraid.expand) {
this.handleError_(HostServiceError.UNSUPPORTED);
return;
}
this.mraid_ = mraid;
if (mraid.getState() === 'loading') {
mraid.addEventListener('ready', () => {
this.mraidReady_();
});
} else {
this.mraidReady_();
}
}

/**
* Stub for handling the case when we want to allow fallback to the standard
* web way of doing things.
*/
declineService_() {
// Needs API change
}
}

AMP.extension(TAG, '0.1', AMP => {
AMP.registerServiceForDoc(TAG, MraidInitializer);
});

0 comments on commit f1e30f2

Please sign in to comment.