Skip to content

Commit

Permalink
✨ [Bento] Implement bento-app-banner (#37127)
Browse files Browse the repository at this point in the history
* feature(bento-add-banner): initial template generation

* feature(bento-add-banner): created simple storybook

* feature(bento-add-banner): copied CSS from classic

* feature(bento-add-banner): added many "stubs" for standalone services

* feature(bento-add-banner): ported IOS and Android app logic

* feature(bento-add-banner): added platform-aware logic, and dismiss button storage

* feature(bento-add-banner): added UI unit tests

* feature(bento-add-banner): added IOS tests

* feature(bento-add-banner): moved sources into `component` folder

* feature(bento-add-banner): added unit tests for Android

* feature(bento-add-banner): mapped the `dismiss-button-aria-label` attribute

* feature(bento-add-banner): added minor notes to the stories

* feature(bento-add-banner): added unit tests for amp-app-banner

* feature(bento-add-banner): updated validator

* feature(bento-add-banner): extracted services to common location

* feature(bento-add-banner): added README for services

* feature(bento-add-banner): updated type defs

* feature(bento-add-banner): updated formatting

* feature(bento-add-banner): lint fix

* feature(bento-add-banner): lint ignores

* feature(bento-add-banner): lint fixes

* feature(bento-add-banner): type fixes

* feature(bento-add-banner): updated z-index file

* feature(bento-add-banner): updated validator template

* feature(bento-add-banner): simplified code with `Array.find`

* feature(bento-add-banner): parse meta content using `Array.reduce`

* feature(bento-add-banner): added more tests for android

* feature(bento-add-banner): added more tests for ios

* feature(bento-add-banner): improved tests for BentoAppBanner

* feature(bento-add-banner): added tests for Dismiss logic

* feature(bento-add-banner): added more iOS tests

* feature(bento-add-banner): tiny lint fix

* feature(bento-add-banner): improved tests for web-component mode, and fixed the button[open-button] check

* feature(bento-add-banner): removed unnecessary `waitFor`

* feature(bento-add-banner): lint fix

* feature(bento-add-banner): added whitespace in tests

* feature(bento-add-banner): ensure tests are not dependent on timers

* feature(bento-add-banner): removed obsolete `latestVersion`

* feature(bento-add-banner): use `Object.fromEntries` as suggested

* feature(bento-add-banner): removed unused CSS

* feature(bento-add-banner): use `WindowInterface.getTop` to improve unit tests

* feature(bento-add-banner): extracted querySelectorInSlot to core

* feature(bento-add-banner): removed useless services, use object-syntax instead of classes, fixed fetchJson

* feature(bento-add-banner): renamed services to utils

* feature(bento-add-banner): removed redundant win variable

* feature(bento-add-banner): extracted parsing logic into separate function for readability

* feature(bento-add-banner): renamed file to `docInfo`

* feature(bento-add-banner): improved safety of accessing localStorage

* feature(bento-add-banner): ensure proper default value is returned

* feature(bento-add-banner): removed unnecessary async

* feature(bento-add-banner): ensure localStorage still works if `key` changes

* feature(bento-add-banner): flip ternary for readability

* feature(bento-add-banner): added `self` for fetch

* feature(bento-add-banner): updated generated z-index file

* feature(bento-app-banner): ensure XHR rejects invalid status codes

* feature(bento-app-banner): removed dependencies on `/src/url`

* feature(bento-app-banner): lint sample code

* feature(bento-app-banner): minor code improvements

* feature(bento-app-banner): minor code improvements

* feature(bento-app-banner): minor code improvements

* feature(bento-app-banner): allow localStorage from custom hook

* feature(bento-app-banner): include optional `init` param for requests

* feature(bento-app-banner): improved logging

* feature(bento-app-banner): only expose `logger[info|warn|error]`

* feature(bento-app-banner): warn when localStorage.setItem fails

* feature(bento-app-banner): ensure logger can be stubbed and tested

* feature(bento-app-banner): moved `INVALID_PROTOCOLS` to proper location

* feature(bento-app-banner): ensure logger is globally disabled

* feature(bento-app-banner): ensure component inherits from AmpPreactBaseElement

* Update src/preact/utils/docInfo.js

Co-authored-by: Justin Ridgewell <justin@ridgewell.name>

Co-authored-by: scottrippey <scott.william.rippey@gmail.com>
Co-authored-by: Justin Ridgewell <justin@ridgewell.name>
  • Loading branch information
3 people committed Jan 28, 2022
1 parent 119808c commit 95d5886
Show file tree
Hide file tree
Showing 35 changed files with 1,630 additions and 47 deletions.
8 changes: 8 additions & 0 deletions build-system/compile/bundles.config.extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@
"hasCss": true
}
},
{
"name": "amp-app-banner",
"version": "1.0",
"options": {
"hasCss": true,
"bento": true
}
},
{
"name": "amp-audio",
"version": "0.1",
Expand Down
1 change: 1 addition & 0 deletions build-system/test-configs/forbidden-terms.js
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ const forbiddenTermsGlobal = {
'extensions/amp-web-push/0.1/amp-web-push-helper-frame.js',
'extensions/amp-web-push/0.1/amp-web-push-permission-dialog.js',
'src/experiments/index.js',
'src/preact/hooks/useLocalStorage.js',
'src/service/cid-impl.js',
'src/service/standard-actions-impl.js',
'src/service/storage-impl.js',
Expand Down
4 changes: 4 additions & 0 deletions css/Z_INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,12 @@
| `amp-mega-menu` | 1000 | [extensions/amp-mega-menu/0.1/amp-mega-menu.css](/extensions/amp-mega-menu/0.1/amp-mega-menu.css) |
| `amp-user-notification` | 1000 | [extensions/amp-user-notification/0.1/amp-user-notification.css](/extensions/amp-user-notification/0.1/amp-user-notification.css) |
| `i-amphtml-app-banner-top-padding` | 15 | [extensions/amp-app-banner/0.1/amp-app-banner.css](/extensions/amp-app-banner/0.1/amp-app-banner.css) |
| `bannerPadding` | 15 | [extensions/amp-app-banner/1.0/component/component.jss.js](/extensions/amp-app-banner/1.0/component/component.jss.js) |
| `.amp-app-banner-dismiss-button` | 14 | [extensions/amp-app-banner/0.1/amp-app-banner.css](/extensions/amp-app-banner/0.1/amp-app-banner.css) |
| `dismiss` | 14 | [extensions/amp-app-banner/1.0/component/component.jss.js](/extensions/amp-app-banner/1.0/component/component.jss.js) |
| `amp-app-banner` | 13 | [extensions/amp-app-banner/0.1/amp-app-banner.css](/extensions/amp-app-banner/0.1/amp-app-banner.css) |
| `amp-app-banner` | 13 | [extensions/amp-app-banner/1.0/amp-app-banner.css](/extensions/amp-app-banner/1.0/amp-app-banner.css) |
| `banner` | 13 | [extensions/amp-app-banner/1.0/component/component.jss.js](/extensions/amp-app-banner/1.0/component/component.jss.js) |
| `amp-sticky-ad-top-padding` | 12 | [extensions/amp-sticky-ad/1.0/amp-sticky-ad.css](/extensions/amp-sticky-ad/1.0/amp-sticky-ad.css) |
| `style` | 11 | [extensions/amp-ad-network-doubleclick-impl/0.1/amp-ad-network-doubleclick-impl.js](/extensions/amp-ad-network-doubleclick-impl/0.1/amp-ad-network-doubleclick-impl.js) |
| `amp-sticky-ad` | 11 | [extensions/amp-sticky-ad/1.0/amp-sticky-ad.css](/extensions/amp-sticky-ad/1.0/amp-sticky-ad.css) |
Expand Down
2 changes: 1 addition & 1 deletion examples/visual-tests/article.amp/article.amp.html
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@
<script async custom-element="amp-app-banner" src="https://cdn.ampproject.org/v0/amp-app-banner-0.1.js" data-amp-report-test="amp-app-banner.js"></script>
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
<meta name="apple-itunes-app" content="app-id=828256236, app-argument=medium://p/cb7f223fad86">
<link rel="manifest" href="medium-manifest.json">
<link rel="manifest" href="../../medium-manifest.json">
</head>
<body>
<amp-sidebar id="sidebar" layout="nodisplay">
Expand Down
45 changes: 27 additions & 18 deletions extensions/amp-app-banner/0.1/amp-app-banner.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,12 +295,7 @@ export class AmpIosAppBanner extends AbstractAppBanner {
* @private
*/
parseIosMetaContent_(metaContent) {
const parts = metaContent.replace(/\s/, '').split(',');
const config = {};
parts.forEach((part) => {
const keyValuePair = part.split('=');
config[keyValuePair[0]] = keyValuePair[1];
});
const config = this.parseKeyValues(metaContent);

const appId = config['app-id'];
const openUrl = config['app-argument'];
Expand Down Expand Up @@ -328,6 +323,22 @@ export class AmpIosAppBanner extends AbstractAppBanner {
installAppUrl
);
}

/**
* Parses a string like "key1=value1,key2=value2" into { key1: "value1", key2: "value2" }
* @param {string} metaContent
* @return {*}
*/
parseKeyValues(metaContent) {
return metaContent
.replace(/\s/, '')
.split(',')
.reduce((result, keyValue) => {
const [key, value] = keyValue.split('=');
result[key] = value;
return result;
}, {});
}
}

/**
Expand Down Expand Up @@ -471,18 +482,16 @@ export class AmpAndroidAppBanner extends AbstractAppBanner {
return;
}

for (let i = 0; i < apps.length; i++) {
const app = apps[i];
if (app['platform'] == 'play') {
const installAppUrl = `https://play.google.com/store/apps/details?id=${app['id']}`;
const openInAppUrl = this.getAndroidIntentForUrl_(app['id']);
this.setupOpenButton_(
dev().assertElement(this.openButton_),
openInAppUrl,
installAppUrl
);
return;
}
const playApp = apps.find((a) => a['platform'] === 'play');
if (playApp) {
const installAppUrl = `https://play.google.com/store/apps/details?id=${playApp['id']}`;
const openInAppUrl = this.getAndroidIntentForUrl_(playApp['id']);
this.setupOpenButton_(
dev().assertElement(this.openButton_),
openInAppUrl,
installAppUrl
);
return;
}

user().warn(
Expand Down
11 changes: 11 additions & 0 deletions extensions/amp-app-banner/1.0/amp-app-banner.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
amp-app-banner {
position: fixed !important;
bottom: 0 !important;
left: 0;
width: 100%;
max-height: 100px !important;
box-sizing: border-box;
background: #fff;
z-index: 13;
box-shadow: 0 0 5px 0 rgba(0,0,0, 0.2) !important;
}
28 changes: 28 additions & 0 deletions extensions/amp-app-banner/1.0/amp-app-banner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {isExperimentOn} from '#experiments';

import {AmpPreactBaseElement, setSuperClass} from '#preact/amp-base-element';

import {userAssert} from '#utils/log';

import {BaseElement} from './base-element';

import {CSS} from '../../../build/amp-app-banner-1.0.css';

/** @const {string} */
const TAG = 'amp-app-banner';

class AmpAppBanner extends setSuperClass(BaseElement, AmpPreactBaseElement) {
/** @override */
isLayoutSupported(layout) {
userAssert(
isExperimentOn(this.win, 'bento') ||
isExperimentOn(this.win, 'bento-app-banner'),
'expected global "bento" or specific "bento-app-banner" experiment to be enabled'
);
return super.isLayoutSupported(layout);
}
}

AMP.extension(TAG, '1.0', (AMP) => {
AMP.registerElement(TAG, AmpAppBanner, CSS);
});
25 changes: 25 additions & 0 deletions extensions/amp-app-banner/1.0/base-element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {PreactBaseElement} from '#preact/base-element';

import {BentoAppBanner} from './component/component';
import {CSS as COMPONENT_CSS} from './component/component.jss';

export class BaseElement extends PreactBaseElement {}

/** @override */
BaseElement['Component'] = BentoAppBanner;

/** @override */
BaseElement['props'] = {
'children': {passthrough: true},
'dismissButtonAriaLabel': {attr: 'dismiss-button-aria-label'},
'id': {attr: 'id'},
};

/** @override */
BaseElement['layoutSizeDefined'] = true;

/** @override */
BaseElement['usesShadowDom'] = true;

/** @override */
BaseElement['shadowCss'] = COMPONENT_CSS;
101 changes: 101 additions & 0 deletions extensions/amp-app-banner/1.0/component/android.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {WindowInterface} from '#core/window/interface';

import {logger} from '#preact/logger';
import {docInfo} from '#preact/utils/docInfo';
import {platformUtils} from '#preact/utils/platform';
import {urlUtils} from '#preact/utils/url';
import {xhrUtils} from '#preact/utils/xhr';

import {openWindowDialog} from '../../../../src/open-window-dialog';

const OPEN_LINK_TIMEOUT = 1500;

/**
* @return {{openOrInstall: function(): void, promise: Promise<Response>}|null}
*/
export function getAndroidAppInfo() {
// We want to fallback to browser builtin mechanism when possible.
const canShowBuiltinBanner =
platformUtils.isAndroid() && platformUtils.isChrome();

if (canShowBuiltinBanner) {
logger.info(
'BENTO-APP-BANNER',
'Not rendering bento-app-banner:',
'Browser supports builtin banners.'
);
return null;
}

const manifestLink = self.document.head.querySelector(
'link[rel=manifest],link[rel=origin-manifest]'
);

const missingDataSources = !manifestLink;
if (missingDataSources) {
return null;
}

const manifestHref = manifestLink.getAttribute('href');

urlUtils.assertHttpsUrl(manifestHref, undefined, 'manifest href');

const promise = xhrUtils.fetchJson(manifestHref).then(parseManifest);
return {
promise,
openOrInstall: () => {
return promise.then((manifest) => {
if (!manifest) {
return;
}
const {installAppUrl, openInAppUrl} = manifest;
setTimeout(() => {
WindowInterface.getTop(window).location.assign(installAppUrl);
}, OPEN_LINK_TIMEOUT);
openWindowDialog(window, openInAppUrl, '_top');
});
},
};
}

/**
* @param {object} manifestJson
* @return {{installAppUrl: string, openInAppUrl: string}|null}
*/
function parseManifest(manifestJson) {
const apps = manifestJson['related_applications'];
if (!apps) {
logger.error(
'BENTO-APP-BANNER',
'Invalid manifest:',
'related_applications is missing from manifest.json file'
);
return null;
}

const playApp = apps.find((a) => a['platform'] === 'play');
if (!playApp) {
logger.error(
'BENTO-APP-BANNER',
'Invalid manifest:',
'Could not find a platform=play app in manifest'
);
return null;
}

const installAppUrl = `https://play.google.com/store/apps/details?id=${playApp['id']}`;
const openInAppUrl = getAndroidIntentForUrl(playApp['id']);
return {installAppUrl, openInAppUrl};
}

/**
* @param {string} appId
* @return {string}
*/
function getAndroidIntentForUrl(appId) {
const parsedUrl = urlUtils.parse(docInfo.canonicalUrl);
const cleanProtocol = parsedUrl.protocol.replace(':', '');
const {host, pathname} = parsedUrl;

return `android-app://${appId}/${cleanProtocol}/${host}${pathname}`;
}

0 comments on commit 95d5886

Please sign in to comment.