forked from ampproject/amphtml
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add amp-viewer-assistance extension (ampproject#15195).
Change-Id: I78dac3ec9f85c3bbd1ceb4ce88a61b3f7d0dc539
- Loading branch information
Showing
13 changed files
with
528 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
171 changes: 171 additions & 0 deletions
171
extensions/amp-viewer-assistance/0.1/amp-viewer-assistance.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
import {ActionTrust} from '../../../src/action-constants'; | ||
import {Services} from '../../../src/services'; | ||
import {dev, user} from '../../../src/log'; | ||
import {dict} from '../../../src/utils/object'; | ||
import {isExperimentOn} from '../../../src/experiments'; | ||
import {tryParseJson} from '../../../src/json'; | ||
|
||
|
||
/** @const {string} */ | ||
const TAG = 'amp-viewer-assistance'; | ||
|
||
/** @const {string} */ | ||
const GSI_TOKEN_PROVIDER = 'actions-on-google-gsi'; | ||
|
||
export class AmpViewerAssistance { | ||
/** | ||
* @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc | ||
*/ | ||
constructor(ampdoc) { | ||
const assistanceElement = ampdoc.getElementById('amp-viewer-assistance'); | ||
|
||
/** @const @private {boolean} */ | ||
this.enabled_ = !!assistanceElement; | ||
if (!this.enabled_) { | ||
return; | ||
} | ||
|
||
/** @const @private */ | ||
this.ampdoc_ = ampdoc; | ||
|
||
/** @const @private {!Element} */ | ||
this.assistanceElement_ = dev().assertElement(assistanceElement); | ||
|
||
/** @const @private {!JsonObject} */ | ||
this.configJson_ = tryParseJson(this.assistanceElement_.textContent, e => { | ||
throw user().createError( | ||
'Failed to parse "amp-viewer-assistance" JSON: ' + e); | ||
}); | ||
|
||
/** @private @const {!../../../src/service/viewer-impl.Viewer} */ | ||
this.viewer_ = Services.viewerForDoc(ampdoc); | ||
|
||
/** @private @const {!../../../src/service/action-impl.ActionService} */ | ||
this.action_ = Services.actionServiceForDoc(assistanceElement); | ||
|
||
/** @private @const {!../../../src/service/vsync-impl.Vsync} */ | ||
this.vsync_ = Services.vsyncFor(ampdoc.win); | ||
} | ||
|
||
/** | ||
* @param {!../../../src/service/action-impl.ActionInvocation} invocation | ||
* @return {?Promise} | ||
* @private | ||
*/ | ||
actionHandler_(invocation) { | ||
const {method, args} = invocation; | ||
if (method == 'updateActionState' && !!args) { | ||
this.viewer_.sendMessageAwaitResponse(method, args).catch(error => { | ||
user().error(TAG, error.toString()); | ||
}); | ||
} else if (method == 'signIn') { | ||
this.requestSignIn_(); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
/** | ||
* @private | ||
* @restricted | ||
*/ | ||
start_() { | ||
if (!this.enabled_) { | ||
user().info( | ||
TAG, 'Invalid AMP Action - no "id=amp-viewer-assistance" element'); | ||
return this; | ||
} | ||
return this.viewer_.isTrustedViewer().then(isTrustedViewer => { | ||
if (!isTrustedViewer && | ||
!isExperimentOn(this.ampdoc_.win, 'amp-viewer-assistance-untrusted')) { | ||
this.enabled_ = false; | ||
user().info(TAG, 'Disabling AMP Action since viewer is not trusted'); | ||
return this; | ||
} | ||
this.action_.installActionHandler( | ||
this.assistanceElement_, this.actionHandler_.bind(this), | ||
ActionTrust.HIGH); | ||
|
||
this.getIdTokenPromise(); | ||
|
||
this.viewer_.sendMessage('viewerAssistanceConfig',dict({ | ||
'config': this.configJson_, | ||
})); | ||
return this; | ||
}); | ||
} | ||
|
||
/** | ||
* @return {!Promise<string>} | ||
*/ | ||
getIdTokenPromise() { | ||
return this.viewer_.sendMessageAwaitResponse('getAccessTokenPassive', dict({ | ||
// For now there's only 1 provider option, so we just hard code it | ||
'providers': [GSI_TOKEN_PROVIDER], | ||
})) | ||
.then(token => { | ||
this.setIdTokenStatus_(Boolean(token)); | ||
return token; | ||
}).catch(() => { | ||
this.setIdTokenStatus_(/*available=*/false); | ||
}); | ||
} | ||
|
||
/** | ||
* @private | ||
*/ | ||
requestSignIn_() { | ||
this.viewer_.sendMessageAwaitResponse('requestSignIn', dict({ | ||
'providers': [GSI_TOKEN_PROVIDER], | ||
})).then(token => { | ||
user().info(TAG, 'Token: ' + token); | ||
if (token) { | ||
this.setIdTokenStatus_(/*available=*/true); | ||
this.action_.trigger( | ||
this.assistanceElement_, 'signedIn', null, ActionTrust.HIGH); | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Toggles the CSS classes related to the status of the identity token. | ||
* @private | ||
* @param {boolean} available | ||
*/ | ||
setIdTokenStatus_(available) { | ||
this.toggleTopClass_( | ||
'amp-viewer-assistance-identity-available', available); | ||
this.toggleTopClass_( | ||
'amp-viewer-assistance-identity-unavailable', !available); | ||
} | ||
|
||
/** | ||
* Gets the root element of the AMP doc. | ||
* @return {!Element} | ||
* @private | ||
*/ | ||
getRootElement_() { | ||
const root = this.ampdoc_.getRootNode(); | ||
return dev().assertElement(root.documentElement || root.body || root); | ||
} | ||
|
||
/** | ||
* Toggles a class on the root element of the AMP doc. | ||
* @param {string} className | ||
* @param {boolean} on | ||
* @private | ||
*/ | ||
toggleTopClass_(className, on) { | ||
this.vsync_.mutate(() => { | ||
this.getRootElement_().classList.toggle(className, on); | ||
}); | ||
|
||
} | ||
} | ||
|
||
// Register the extension services. | ||
AMP.extension(TAG, '0.1', function(AMP) { | ||
AMP.registerServiceForDoc('amp-viewer-assistance', function(ampdoc) { | ||
return new AmpViewerAssistance(ampdoc).start_(); | ||
}); | ||
}); |
168 changes: 168 additions & 0 deletions
168
extensions/amp-viewer-assistance/0.1/test/test-amp-viewer-assistance.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
import {ActionInvocation} from '../../../../src/service/action-impl'; | ||
import {AmpViewerAssistance} from '../amp-viewer-assistance'; | ||
import {mockServiceForDoc} from '../../../../testing/test-helper'; | ||
|
||
describes.fakeWin('AmpViewerAssistance', { | ||
amp: true, | ||
location: 'https://pub.com/doc1', | ||
}, env => { | ||
let document; | ||
let ampdoc; | ||
let element; | ||
let viewerMock; | ||
|
||
beforeEach(() => { | ||
ampdoc = env.ampdoc; | ||
document = env.win.document; | ||
viewerMock = mockServiceForDoc(env.sandbox, env.ampdoc, 'viewer', [ | ||
'isTrustedViewer', | ||
'sendMessage', | ||
'sendMessageAwaitResponse', | ||
]); | ||
viewerMock.isTrustedViewer.returns(Promise.resolve(true)); | ||
viewerMock.sendMessageAwaitResponse.returns(Promise.resolve('idToken')); | ||
|
||
element = document.createElement('script'); | ||
element.setAttribute('id', 'amp-viewer-assistance'); | ||
element.setAttribute('type', 'application/json'); | ||
document.body.appendChild(element); | ||
}); | ||
|
||
it('should disable service when no config', () => { | ||
document.body.removeChild(element); | ||
const service = new AmpViewerAssistance(ampdoc); | ||
expect(service.enabled_).to.be.false; | ||
expect(service.assistanceElement_).to.be.undefined; | ||
}); | ||
|
||
it('should disable service when the viewer is not trusted', () => { | ||
viewerMock.isTrustedViewer.returns(Promise.resolve(false)); | ||
const config = { | ||
'providerId': 'foo-bar', | ||
}; | ||
element.textContent = JSON.stringify(config); | ||
const service = new AmpViewerAssistance(ampdoc); | ||
return service.start_().then(() => { | ||
expect(service.enabled_).to.be.false; | ||
}); | ||
}); | ||
|
||
it('should fail if config is malformed', () => { | ||
expect(() => { | ||
new AmpViewerAssistance(ampdoc); | ||
}).to.throw(Error); | ||
}); | ||
|
||
it('should send the config to the viewer', () => { | ||
const config = { | ||
'providerId': 'foo-bar', | ||
}; | ||
element.textContent = JSON.stringify(config); | ||
const service = new AmpViewerAssistance(ampdoc); | ||
expect(service.enabled_).to.be.true; | ||
expect(service.assistanceElement_).to.equal(element); | ||
const sendMessageStub = service.viewer_.sendMessage; | ||
return service.start_().then(() => { | ||
expect(sendMessageStub).to.be.calledOnce; | ||
expect(sendMessageStub.firstCall.args[0]).to | ||
.equal('viewerAssistanceConfig'); | ||
expect(sendMessageStub.firstCall.args[1]).to.deep.equal({ | ||
'config': config, | ||
}); | ||
}); | ||
}); | ||
|
||
it('should send updateActionState to the viewer', () => { | ||
const config = { | ||
'providerId': 'foo-bar', | ||
}; | ||
element.textContent = JSON.stringify(config); | ||
const service = new AmpViewerAssistance(ampdoc); | ||
const sendMessageStub = service.viewer_.sendMessageAwaitResponse; | ||
const invocationArgs = { | ||
'foo': 'bar', | ||
}; | ||
return service.start_().then(() => { | ||
sendMessageStub.resetHistory(); | ||
const invocation = new ActionInvocation( | ||
element, 'updateActionState', invocationArgs); | ||
service.actionHandler_(invocation); | ||
expect(sendMessageStub).to.be.calledOnce; | ||
expect(sendMessageStub.firstCall.args[0]).to.equal('updateActionState'); | ||
expect(sendMessageStub.firstCall.args[1]).to.deep.equal(invocationArgs); | ||
}); | ||
}); | ||
|
||
it('should fail to send updateActionState if args are missing', () => { | ||
const config = { | ||
'providerId': 'foo-bar', | ||
}; | ||
element.textContent = JSON.stringify(config); | ||
const service = new AmpViewerAssistance(ampdoc); | ||
const sendMessageStub = service.viewer_.sendMessage; | ||
return service.start_().then(() => { | ||
sendMessageStub.reset(); | ||
const invocation = new ActionInvocation(element, 'updateActionState'); | ||
service.actionHandler_(invocation); | ||
expect(sendMessageStub).to.not.be.called; | ||
}); | ||
}); | ||
|
||
it('should send handle the signIn action', () => { | ||
const config = { | ||
'providerId': 'foo-bar', | ||
}; | ||
element.textContent = JSON.stringify(config); | ||
const service = new AmpViewerAssistance(ampdoc); | ||
const sendMessageStub = service.viewer_.sendMessageAwaitResponse; | ||
return service.start_().then(() => { | ||
sendMessageStub.resetHistory(); | ||
sendMessageStub.returns(Promise.reject()); | ||
const invocation = new ActionInvocation(element, 'signIn'); | ||
service.actionHandler_(invocation); | ||
expect(sendMessageStub).to.be.calledOnce; | ||
expect(sendMessageStub.firstCall.args[0]).to.equal('requestSignIn'); | ||
expect(sendMessageStub.firstCall.args[1]).to.deep.equal({ | ||
providers: ['actions-on-google-gsi'], | ||
}); | ||
}); | ||
}); | ||
|
||
it('should make IDENTITY_TOKEN available through a promise', () => { | ||
const config = { | ||
'providerId': 'foo-bar', | ||
}; | ||
element.textContent = JSON.stringify(config); | ||
const service = new AmpViewerAssistance(ampdoc); | ||
return service.start_() | ||
.then(() => service.getIdTokenPromise()) | ||
.then(token => expect(token).to.equal('idToken')); | ||
}); | ||
|
||
it('should set the css classes if IDENTITY_TOKEN is unavailable', () => { | ||
const config = { | ||
'providerId': 'foo-bar', | ||
}; | ||
element.textContent = JSON.stringify(config); | ||
const service = new AmpViewerAssistance(ampdoc); | ||
service.vsync_ = { | ||
mutate: callback => { | ||
callback(); | ||
}, | ||
}; | ||
const sendMessageStub = service.viewer_.sendMessageAwaitResponse; | ||
sendMessageStub.returns(Promise.reject()); | ||
return service.getIdTokenPromise().then(() => { | ||
expect(sendMessageStub).to.be.calledOnce; | ||
expect(sendMessageStub.firstCall.args[0]).to.equal( | ||
'getAccessTokenPassive'); | ||
expect(sendMessageStub.firstCall.args[1]).to.deep.equal({ | ||
providers: ['actions-on-google-gsi'], | ||
}); | ||
expect(document.documentElement).not.to.have.class( | ||
'amp-viewer-assistance-identity-available'); | ||
expect(document.documentElement).to.have.class( | ||
'amp-viewer-assistance-identity-unavailable'); | ||
}); | ||
}); | ||
}); |
38 changes: 38 additions & 0 deletions
38
extensions/amp-viewer-assistance/0.1/test/validator-amp-viewer-assistance.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<!-- | ||
Copyright 2015 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. | ||
--> | ||
<!-- | ||
Test Description: | ||
This tests for amp-viewer-assistance syntax. | ||
--> | ||
<!doctype html> | ||
<html ⚡> | ||
<head> | ||
<meta charset="utf-8"> | ||
<link rel="canonical" href="./regular-html-version.html"> | ||
<meta name="viewport" content="width=device-width,minimum-scale=1"> | ||
<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> | ||
<script async src="https://cdn.ampproject.org/v0.js"></script> | ||
<script async custom-element="amp-viewer-assistance" src="https://cdn.ampproject.org/v0/amp-viewer-assistance-0.1.js"></script> | ||
<script id="amp-viewer-assistance" type="application/json"> | ||
{ | ||
"contents": "currently untested" | ||
} | ||
</script> | ||
</head> | ||
<body> | ||
Hello, world. | ||
</body> | ||
</html> |
Oops, something went wrong.