Skip to content

Commit

Permalink
Cid optout action (ampproject#9952)
Browse files Browse the repository at this point in the history
  • Loading branch information
aghassemi authored Jun 30, 2017
1 parent fdc7ec2 commit 4b9718e
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 37 deletions.
1 change: 1 addition & 0 deletions build-system/tasks/presubmit-checks.js
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ var forbiddenTerms = {
message: requiresReviewPrivacy,
whitelist: [
'src/services.js',
'src/service/cid-impl.js',
'extensions/amp-user-notification/0.1/amp-user-notification.js',
'extensions/amp-app-banner/0.1/amp-app-banner.js',
],
Expand Down
109 changes: 75 additions & 34 deletions extensions/amp-user-notification/0.1/amp-user-notification.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,40 @@ export class AmpUserNotification extends AMP.BaseElement {
constructor(element) {
super(element);

/** @private @const {?UrlReplacements} */
this.urlReplacements_ = null;
/** @private {?string} */
this.ampUserId_ = null;

/** @private @const {?UserNotificationManager} */
this.userNotificationManager_ = null;
/** @private {?string} */
this.elementId_ = null;

/** @private {?function()} */
this.dialogResolve_ = null;

/** @private {Promise} */
this.dialogPromise_ = new Promise(resolve => {
this.dialogResolve_ = resolve;
});

/** @const @private {?Promise<!Storage>} */
/** @private {?string} */
this.dismissHref_ = null;

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

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

/** @private {string} */
this.storageKey_ = '';

/** @private {?Promise<!Storage>} */
this.storagePromise_ = null;

/** @private {?UserNotificationManager} */
this.userNotificationManager_ = null;

/** @private {?../../../src/service/url-replacements-impl.UrlReplacements} */
this.urlReplacements_ = null;
}

/** @override */
Expand All @@ -112,30 +138,16 @@ export class AmpUserNotification extends AMP.BaseElement {
'userNotificationManager');
}

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

/** @private {function()} */
this.dialogResolve_ = null;

/** @private {!Promise} */
this.dialogPromise_ = new Promise(resolve => {
this.dialogResolve_ = resolve;
});

this.elementId_ = user().assert(this.element.id,
'amp-user-notification should have an id.');

/** @private @const {string} */
this.storageKey_ = 'amp-user-notification:' + this.elementId_;

/** @private @const {?string} */
this.showIfHref_ = this.element.getAttribute('data-show-if-href');
if (this.showIfHref_) {
assertHttpsUrl(this.showIfHref_, this.element);
}

/** @private @const {?string} */
this.dismissHref_ = this.element.getAttribute('data-dismiss-href');
if (this.dismissHref_) {
assertHttpsUrl(this.dismissHref_, this.element);
Expand All @@ -149,14 +161,15 @@ export class AmpUserNotification extends AMP.BaseElement {

const persistDismissal = this.element.getAttribute(
'data-persist-dismissal');
/** @private @const {boolean} */

this.persistDismissal_ = (
persistDismissal != 'false' && persistDismissal != 'no');

this.userNotificationManager_
.registerUserNotification(this.elementId_, this);

this.registerAction('dismiss', this.dismiss.bind(this));
this.registerAction('dismiss', () => this.dismiss(/*forceNoPersist*/false));
this.registerAction('optoutOfCid', () => this.optoutOfCid_());
}

/**
Expand All @@ -167,12 +180,13 @@ export class AmpUserNotification extends AMP.BaseElement {
* @private
*/
buildGetHref_(ampUserId) {
const showIfHref = dev().assert(this.showIfHref_);
const showIfHref = dev().assertString(this.showIfHref_);
return this.urlReplacements_.expandAsync(showIfHref).then(href => {
return addParamsToUrl(href, {
elementId: this.elementId_,
ampUserId,
const data = /** @type {!JsonObject} */({
'elementId': this.elementId_,
'ampUserId': ampUserId,
});
return addParamsToUrl(href, data);
});
}

Expand Down Expand Up @@ -200,14 +214,14 @@ export class AmpUserNotification extends AMP.BaseElement {
* @return {!Promise}
*/
postDismissEnpoint_() {
return xhrFor(this.win).fetchJson(dev().assert(this.dismissHref_), {
return xhrFor(this.win).fetchJson(dev().assertString(this.dismissHref_), {
method: 'POST',
credentials: 'include',
requireAmpResponseSourceOrigin: false,
body: {
body: /** @type {!JsonObject} */({
'elementId': this.elementId_,
'ampUserId': this.ampUserId_,
},
}),
});
}

Expand All @@ -232,12 +246,27 @@ export class AmpUserNotification extends AMP.BaseElement {
}

/**
* Get async cid service.
* Opts the user out of cid issuance and dismisses the notification.
* @private
*/
optoutOfCid_() {
return this.getCidService_()
.then(cid => cid.optOut())
.then(() => this.dismiss(/*forceNoPersist*/false), reason => {
dev().error(TAG,
'Failed to opt out of Cid', reason);
// If optout fails, dismiss notification without persisting.
this.dismiss(/*forceNoPersist*/true);
});
}

/**
* Get async cid.
* @return {!Promise}
* @private
*/
getAsyncCid_() {
return cidForDoc(this.element).then(cid => {
return this.getCidService_().then(cid => {
// `amp-user-notification` is our cid scope, while we give it a resolved
// promise for the 2nd argument so that the 3rd argument (the
// persistentConsent) is the one used to resolve getting
Expand All @@ -252,6 +281,15 @@ export class AmpUserNotification extends AMP.BaseElement {
});
}

/**
* Get cid service.
* @return {!Promise}
* @private
*/
getCidService_() {
return cidForDoc(this.element);
}

/** @override */
shouldShow() {
return this.isDismissed().then(dismissed => {
Expand Down Expand Up @@ -302,20 +340,23 @@ export class AmpUserNotification extends AMP.BaseElement {

/** @override */
activate() {
this.dismiss();
this.dismiss(/*forceNoPersist*/false);
}

/**
* Hides the current user notification and invokes the `dialogResolve_`
* method. Removes the `.amp-active` class from the element.
*
* @param {boolean} forceNoPersist If true, dismissal won't be persisted
* regardless of 'data-persist-dismissal''s value
*/
dismiss() {
dismiss(forceNoPersist) {
this.element.classList.remove('amp-active');
this.element.classList.add('amp-hidden');
this.dialogResolve_();
this.getViewport().removeFromFixedLayer(this.element);

if (this.persistDismissal_) {
if (this.persistDismissal_ && !forceNoPersist) {
// Store and post.
this.storagePromise_.then(storage => {
storage.set(this.storageKey_, true);
Expand Down Expand Up @@ -348,7 +389,7 @@ export class UserNotificationManager {
/** @private @const {!Object<string,!UserNotificationDeferDef>} */
this.deferRegistry_ = Object.create(null);

/** @private @const {!Viewer} */
/** @private @const {!../../../src/service/viewer-impl.Viewer} */
this.viewer_ = viewerForDoc(this.win.document);

/** @private @const {!Promise} */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,7 @@ describes.realWin('amp-user-notification', {
it('should be able to get AmpUserNotification object by ID', () => {
const element = getUserNotification();
const userNotification = new AmpUserNotification(element);
userNotification.dialogResolve_();
service.registerUserNotification('n1', userNotification);
return expect(service.get('n1')).to.eventually.equal(userNotification);
});
Expand Down Expand Up @@ -582,4 +583,48 @@ describes.realWin('amp-user-notification', {
expect(get().then).to.be.function;
});
});

describe('optOutOfCid', () => {
const cidMock = {
optOut() {
return optoutPromise;
},
};
let dismissSpy;
let optoutPromise;
let optOutOfCidStub;

beforeEach(() => {
optOutOfCidStub = sandbox.spy(cidMock, 'optOut');
});

it('should call cid.optOut() and dismiss', () => {
const element = getUserNotification({id: 'n1'});
const impl = element.implementation_;
impl.buildCallback();
optoutPromise = Promise.resolve();
impl.getCidService_ = () => { return Promise.resolve(cidMock); };
dismissSpy = sandbox.spy(impl, 'dismiss');

return impl.optoutOfCid_().then(() => {
expect(dismissSpy).to.be.calledWithExactly(false);
expect(optOutOfCidStub).to.be.calledOnce;
});
});

it('should dissmiss without persistence if cid.optOut() fails', () => {
const element = getUserNotification({id: 'n1'});
const impl = element.implementation_;
impl.buildCallback();
optoutPromise = Promise.reject('failed');
impl.getCidService_ = () => { return Promise.resolve(cidMock); };
dismissSpy = sandbox.spy(impl, 'dismiss');

return impl.optoutOfCid_().then(() => {
expect(dismissSpy).to.be.calledWithExactly(true);
expect(optOutOfCidStub).to.be.calledOnce;
});
});

});
});
4 changes: 4 additions & 0 deletions extensions/amp-user-notification/amp-user-notification.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,10 @@ The `amp-user-notification` exposes the following actions that you can use [AMP
<td>dismiss (default)</td>
<td>Closes the user notification; see <a href="#usage">usage</a> for more details.</td>
</tr>
<tr>
<td>optoutOfCid</td>
<td>User will be opted out of Client ID generation for all scopes.</td>
</tr>
</table>

## Delaying Client ID generation until the notification is acknowledged
Expand Down
2 changes: 1 addition & 1 deletion gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ declareExtension('amp-slides', '0.1', false, 'NO_TYPE_CHECK');
declareExtension('amp-social-share', '0.1', true);
declareExtension('amp-timeago', '0.1', false);
declareExtension('amp-twitter', '0.1', false);
declareExtension('amp-user-notification', '0.1', true, 'NO_TYPE_CHECK');
declareExtension('amp-user-notification', '0.1', true);
declareExtension('amp-vimeo', '0.1', false, 'NO_TYPE_CHECK');
declareExtension('amp-vine', '0.1', false, 'NO_TYPE_CHECK');
declareExtension('amp-viz-vega', '0.1', true);
Expand Down
Loading

0 comments on commit 4b9718e

Please sign in to comment.