Skip to content

Commit 97f4e16

Browse files
ergunshDevtools-frontend LUCI CQ
authored andcommitted
[GdpIntegration] Render badges in BadgeNotification
Bug: 436201262 Change-Id: I0a738d6c49284c6d86af5a35788bc7c0020764f8 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6919231 Reviewed-by: Natallia Harshunova <[email protected]> Commit-Queue: Ergün Erdoğmuş <[email protected]> Auto-Submit: Ergün Erdoğmuş <[email protected]>
1 parent ecc1700 commit 97f4e16

File tree

11 files changed

+245
-12
lines changed

11 files changed

+245
-12
lines changed

front_end/models/badges/Badge.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as Badges from './badges.js';
99
class TestBadge extends Badges.Badge {
1010
override name = 'badges/test-badge';
1111
override title = 'test-badge-title';
12+
override imageUri = 'image-uri';
1213
override interestedActions: readonly Badges.BadgeAction[] = [
1314
Badges.BadgeAction.PERFORMANCE_INSIGHT_CLICKED,
1415
] as const;

front_end/models/badges/Badge.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export abstract class Badge {
2626

2727
abstract readonly name: string;
2828
abstract readonly title: string;
29+
abstract readonly imageUri: string;
2930
abstract readonly interestedActions: readonly BadgeAction[];
3031
readonly isStarterBadge: boolean = false;
3132

front_end/models/badges/DOMDetectiveBadge.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
import {Badge, BadgeAction} from './Badge.js';
66

7+
const DOM_DETECTIVE_BADGE_IMAGE_URI = new URL('../../Images/gdp-logo-standalone.svg', import.meta.url).toString();
78
export class DOMDetectiveBadge extends Badge {
89
override readonly name = 'awards/dom-detective-badge';
910
override readonly title = 'DOM Detective';
11+
override readonly imageUri = DOM_DETECTIVE_BADGE_IMAGE_URI;
1012

1113
override readonly interestedActions = [
1214
BadgeAction.DOM_ELEMENT_OR_ATTRIBUTE_EDITED,

front_end/models/badges/SpeedsterBadge.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
import {Badge, BadgeAction} from './Badge.js';
66

7+
const SPEEDSTER_BADGE_URI = new URL('../../Images/gdp-logo-standalone.svg', import.meta.url).toString();
78
export class SpeedsterBadge extends Badge {
89
// TODO(ergunsh): Update the name to be the actual badge for DevTools.
910
override readonly name = 'profiles/me/awards/developers.google.com%2Fprofile%2Fbadges%2Flegacy%2Ftest';
1011
override readonly title = 'Speedster';
1112
override readonly interestedActions = [BadgeAction.PERFORMANCE_INSIGHT_CLICKED] as const;
13+
override readonly imageUri = SPEEDSTER_BADGE_URI;
1214

1315
handleAction(_action: BadgeAction): void {
1416
this.trigger();

front_end/models/badges/StarterBadge.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
import {Badge, BadgeAction} from './Badge.js';
66

7+
const STARTER_BADGE_IMAGE_URI = new URL('../../Images/gdp-logo-standalone.svg', import.meta.url).toString();
78
export class StarterBadge extends Badge {
89
override readonly isStarterBadge = true;
910
// TODO(ergunsh): Update the name to be the actual badge for DevTools.
1011
override readonly name = 'profiles/me/awards/developers.google.com%2Fprofile%2Fbadges%2Fprofile%2Fcreated-profile';
1112
override readonly title = 'Chrome DevTools User';
13+
override readonly imageUri = STARTER_BADGE_IMAGE_URI;
1214

1315
// TODO(ergunsh): Add remaining non-trivial event definitions
1416
override readonly interestedActions = [

front_end/models/badges/UserBadges.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as Badges from './badges.js';
1111
class MockActivityBadge extends Badges.Badge {
1212
override name = 'badges/test-badge';
1313
override title = 'test-badge-title';
14+
override imageUri = 'test-image-uri';
1415
override interestedActions: readonly Badges.BadgeAction[] = [
1516
Badges.BadgeAction.PERFORMANCE_INSIGHT_CLICKED,
1617
] as const;
@@ -23,6 +24,7 @@ class MockActivityBadge extends Badges.Badge {
2324
class MockStarterBadge extends Badges.Badge {
2425
override name = 'badges/starter-test-badge';
2526
override title = 'starter-test-badge';
27+
override imageUri = 'starte-test-image-uri';
2628
override isStarterBadge = true;
2729
override interestedActions: readonly Badges.BadgeAction[] = [
2830
Badges.BadgeAction.CSS_RULE_MODIFIED,

front_end/models/badges/UserBadges.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,8 @@ export class UserBadges extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
137137

138138
reconcileBadgesFinishedForTest(): void {
139139
}
140+
141+
isReceiveBadgesSettingEnabled(): boolean {
142+
return Boolean(this.#receiveBadgesSetting?.get());
143+
}
140144
}

front_end/panels/common/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ devtools_module("common") {
3535
"../../core/host:bundle",
3636
"../../core/i18n:bundle",
3737
"../../core/platform:bundle",
38+
"../../models/badges:bundle",
3839
"../../models/geometry:bundle",
3940
"../../ui/components/buttons:bundle",
4041
"../../ui/components/snackbars:bundle",

front_end/panels/common/BadgeNotification.test.ts

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,61 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import * as Common from '../../core/common/common.js';
6+
import * as Host from '../../core/host/host.js';
7+
import * as Badges from '../../models/badges/badges.js';
58
import {
69
renderElementIntoDOM,
710
} from '../../testing/DOMHelpers.js';
811
import {describeWithEnvironment} from '../../testing/EnvironmentHelpers.js';
912
import {createViewFunctionStub} from '../../testing/ViewFunctionHelpers.js';
1013
import * as UI from '../../ui/legacy/legacy.js';
11-
import * as Lit from '../../ui/lit/lit.js';
1214

1315
import * as BadgeNotification from './BadgeNotification.js';
1416

15-
const {html} = Lit;
17+
class TestBadge extends Badges.Badge {
18+
override name = 'testBadge';
19+
override title = 'title';
20+
override imageUri = 'image-uri';
21+
override interestedActions: readonly Badges.BadgeAction[] = [];
22+
override handleAction(): void {
23+
throw new Error('Method not implemented.');
24+
}
25+
}
26+
27+
class TestStarterBadge extends Badges.Badge {
28+
override name = 'testStarterBadge';
29+
override title = 'starterBadgeTitle';
30+
override imageUri = 'starter-badge-image-uri';
31+
override isStarterBadge = true;
32+
override interestedActions: readonly Badges.BadgeAction[] = [];
33+
override handleAction(): void {
34+
throw new Error('Method not implemented.');
35+
}
36+
}
37+
38+
function createMockBadge(badgeCtor: new (badgeContext: Badges.BadgeContext) => Badges.Badge): Badges.Badge {
39+
return new badgeCtor({
40+
onTriggerBadge: () => {},
41+
badgeActionEventTarget: new Common.ObjectWrapper.ObjectWrapper<Badges.BadgeActionEvents>(),
42+
});
43+
}
44+
45+
function assertMessageIncludes(messageInput: HTMLElement|string, textToInclude: string): void {
46+
let actualText: string;
47+
if (messageInput instanceof HTMLElement) {
48+
actualText = messageInput.textContent;
49+
} else {
50+
actualText = messageInput;
51+
}
52+
assert.include(actualText, textToInclude);
53+
}
1654

1755
describeWithEnvironment('BadgeNotification', () => {
1856
async function createWidget(properties?: Partial<BadgeNotification.BadgeNotificationProperties>) {
1957
const view = createViewFunctionStub(BadgeNotification.BadgeNotification);
2058
const widget = new BadgeNotification.BadgeNotification(undefined, view);
21-
widget.message = properties?.message ?? html`Test message`;
59+
widget.message = properties?.message ?? 'Test message';
2260
widget.imageUri = properties?.imageUri ?? 'test.png';
2361
widget.actions = properties?.actions ?? [];
2462
widget.markAsRoot();
@@ -54,4 +92,70 @@ describeWithEnvironment('BadgeNotification', () => {
5492
view.input.onCloseClick();
5593
assert.isFalse(inspectorViewRootElementStub.contains(widget.element));
5694
});
95+
96+
it('presents an activity-based badge', async () => {
97+
const {view, widget} = await createWidget();
98+
const badge = createMockBadge(TestBadge);
99+
100+
await widget.present(badge);
101+
const input = await view.nextInput;
102+
103+
assert.strictEqual(input.imageUri, badge.imageUri);
104+
assert.lengthOf(input.actions, 2);
105+
assert.strictEqual(input.actions[0].label, 'Badge settings');
106+
assert.strictEqual(input.actions[1].label, 'View profile');
107+
assertMessageIncludes(input.message, 'It has been added to your Developer Profile.');
108+
});
109+
110+
it('presents a starter badge as an activity-based badge if the user has a profile and has enabled badges',
111+
async () => {
112+
sinon.stub(Host.GdpClient.GdpClient.instance(), 'getProfile').resolves({} as Host.GdpClient.Profile);
113+
sinon.stub(Badges.UserBadges.instance(), 'isReceiveBadgesSettingEnabled').returns(true);
114+
115+
const {view, widget} = await createWidget();
116+
const badge = createMockBadge(TestStarterBadge);
117+
118+
await widget.present(badge);
119+
const input = await view.nextInput;
120+
121+
// Should fall back to the activity-based badge flow.
122+
assert.strictEqual(input.imageUri, 'starter-badge-image-uri');
123+
assert.lengthOf(input.actions, 2);
124+
assert.strictEqual(input.actions[0].label, 'Badge settings');
125+
assert.strictEqual(input.actions[1].label, 'View profile');
126+
assertMessageIncludes(input.message, 'It has been added to your Developer Profile.');
127+
});
128+
129+
it('presents a starter badge with an opt-in message if the user has a profile but has disabled badges', async () => {
130+
sinon.stub(Host.GdpClient.GdpClient.instance(), 'getProfile').resolves({} as Host.GdpClient.Profile);
131+
sinon.stub(Badges.UserBadges.instance(), 'isReceiveBadgesSettingEnabled').returns(false);
132+
133+
const {view, widget} = await createWidget();
134+
const badge = createMockBadge(TestStarterBadge);
135+
136+
await widget.present(badge);
137+
const input = await view.nextInput;
138+
139+
assert.strictEqual(input.imageUri, badge.imageUri);
140+
assert.lengthOf(input.actions, 2);
141+
assert.strictEqual(input.actions[0].label, 'Remind me later');
142+
assert.strictEqual(input.actions[1].label, 'Receive badges');
143+
assertMessageIncludes(input.message, 'Turn on badges to claim it.');
144+
});
145+
146+
it('presents a starter badge with a create profile message if the user does not have a profile', async () => {
147+
sinon.stub(Host.GdpClient.GdpClient.instance(), 'getProfile').resolves(null);
148+
149+
const {view, widget} = await createWidget();
150+
const badge = createMockBadge(TestStarterBadge);
151+
152+
await widget.present(badge);
153+
const input = await view.nextInput;
154+
155+
assert.strictEqual(input.imageUri, badge.imageUri);
156+
assert.lengthOf(input.actions, 2);
157+
assert.strictEqual(input.actions[0].label, 'Remind me later');
158+
assert.strictEqual(input.actions[1].label, 'Create profile');
159+
assertMessageIncludes(input.message, 'Create a profile to claim your badge.');
160+
});
57161
});

front_end/panels/common/BadgeNotification.ts

Lines changed: 119 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import * as Host from '../../core/host/host.js';
56
import * as i18n from '../../core/i18n/i18n.js';
7+
import * as Badges from '../../models/badges/badges.js';
68
import * as Buttons from '../../ui/components/buttons/buttons.js';
79
import * as UI from '../../ui/legacy/legacy.js';
810
import * as Lit from '../../ui/lit/lit.js';
@@ -17,10 +19,50 @@ const UIStrings = {
1719
* @description Title for close button
1820
*/
1921
dismiss: 'Dismiss',
22+
/**
23+
* @description Activity based badge award notification text
24+
* @example {Badge Title} PH1
25+
*/
26+
activityBasedBadgeAwardMessage: 'You earned the {PH1} badge! It has been added to your Developer Profile.',
27+
/**
28+
* @description Action title for navigating to the badge settings in Google Developer Profile section
29+
*/
30+
badgeSettings: 'Badge settings',
31+
/**
32+
* @description Action title for opening the Google Developer Program profile page of the user in a new tab
33+
*/
34+
viewProfile: 'View profile',
35+
/**
36+
* @description Starter badge award notification text when the user has a Google Developer Program profile but did not enable receiving badges in DevTools yet
37+
* @example {Badge Title} PH1
38+
* @example {Google Developer Program link} PH2
39+
*/
40+
starterBadgeAwardMessageSettingDisabled: 'You earned the {PH1} badge for the {PH2}! Turn on badges to claim it.',
41+
/**
42+
* @description Starter badge award notification text when the user does not have a Google Developer Program profile.
43+
* @example {Badge Title} PH1
44+
* @example {Google Developer Program link} PH2
45+
*/
46+
starterBadgeAwardMessageNoGdpProfile:
47+
'You earned the {PH1} badge for the {PH2}! Create a profile to claim your badge.',
48+
/**
49+
* @description Action title for snoozing the starter badge.
50+
*/
51+
remindMeLater: 'Remind me later',
52+
/**
53+
* @description Action title for enabling the "Receive badges" setting
54+
*/
55+
receiveBadges: 'Receive badges',
56+
/**
57+
* @description Action title for creating a Google Developer Program profle
58+
*/
59+
createProfile: 'Create profile',
2060
} as const;
2161

2262
const str_ = i18n.i18n.registerUIStrings('panels/common/BadgeNotification.ts', UIStrings);
2363
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
64+
const i18nFormatString = i18n.i18n.getFormatLocalizedString.bind(undefined, str_);
65+
const lockedString = i18n.i18n.lockedString;
2466

2567
export interface BadgeNotificationAction {
2668
label: string;
@@ -30,7 +72,7 @@ export interface BadgeNotificationAction {
3072
}
3173

3274
export interface BadgeNotificationProperties {
33-
message: Lit.LitTemplate;
75+
message: HTMLElement|string;
3476
imageUri: string;
3577
actions: BadgeNotificationAction[];
3678
}
@@ -80,7 +122,7 @@ const DEFAULT_VIEW = (input: ViewInput, _output: undefined, target: HTMLElement)
80122
type View = typeof DEFAULT_VIEW;
81123

82124
export class BadgeNotification extends UI.Widget.Widget {
83-
message: Lit.LitTemplate = html``;
125+
message: HTMLElement|string = '';
84126
imageUri = '';
85127
actions: BadgeNotificationAction[] = [];
86128

@@ -91,13 +133,81 @@ export class BadgeNotification extends UI.Widget.Widget {
91133
this.#view = view;
92134
}
93135

94-
static show(properties: BadgeNotificationProperties): BadgeNotification {
95-
const widget = new BadgeNotification();
96-
widget.message = properties.message;
97-
widget.imageUri = properties.imageUri;
98-
widget.actions = properties.actions;
99-
widget.show(UI.InspectorView.InspectorView.instance().element);
100-
return widget;
136+
async present(badge: Badges.Badge): Promise<void> {
137+
if (badge.isStarterBadge) {
138+
await this.#presentStarterBadge(badge);
139+
} else {
140+
this.#presentActivityBasedBadge(badge);
141+
}
142+
}
143+
144+
#show(properties: BadgeNotificationProperties): void {
145+
this.message = properties.message;
146+
this.imageUri = properties.imageUri;
147+
this.actions = properties.actions;
148+
this.requestUpdate();
149+
this.show(UI.InspectorView.InspectorView.instance().element);
150+
}
151+
152+
async #presentStarterBadge(badge: Badges.Badge): Promise<void> {
153+
const gdpProfile = await Host.GdpClient.GdpClient.instance().getProfile();
154+
const receiveBadgesSettingEnabled = Badges.UserBadges.instance().isReceiveBadgesSettingEnabled();
155+
const googleDeveloperProgramLink = UI.XLink.XLink.create(
156+
'https://developers.google.com/program', lockedString('Google Developer Program'), 'badge-link', undefined,
157+
'gdp.program-link');
158+
159+
// If the user already has a GDP profile and the receive badges setting enabled,
160+
// starter badge behaves as if it's an activity based badge.
161+
if (gdpProfile && receiveBadgesSettingEnabled) {
162+
this.#presentActivityBasedBadge(badge);
163+
return;
164+
}
165+
166+
// If the user already has a GDP profile and the receive badges setting disabled,
167+
// starter badge behaves as a nudge for opting into receiving badges.
168+
if (gdpProfile && !receiveBadgesSettingEnabled) {
169+
this.#show({
170+
message: i18nFormatString(
171+
UIStrings.starterBadgeAwardMessageSettingDisabled, {PH1: badge.title, PH2: googleDeveloperProgramLink}),
172+
actions: [
173+
{
174+
label: i18nString(UIStrings.remindMeLater),
175+
onClick: () => {/* To implement */},
176+
},
177+
{label: i18nString(UIStrings.receiveBadges), onClick: () => { /* To implement */ }}
178+
],
179+
imageUri: badge.imageUri,
180+
});
181+
return;
182+
}
183+
184+
// The user does not have a GDP profile, starter badge acts as a nudge for creating a GDP profile.
185+
this.#show({
186+
message: i18nFormatString(
187+
UIStrings.starterBadgeAwardMessageNoGdpProfile, {PH1: badge.title, PH2: googleDeveloperProgramLink}),
188+
actions: [
189+
{
190+
label: i18nString(UIStrings.remindMeLater),
191+
onClick: () => {/* TODO(ergunsh): Implement */},
192+
},
193+
{label: i18nString(UIStrings.createProfile), onClick: () => { /* TODO(ergunsh): Implement */ }}
194+
],
195+
imageUri: badge.imageUri,
196+
});
197+
}
198+
199+
#presentActivityBasedBadge(badge: Badges.Badge): void {
200+
this.#show({
201+
message: i18nString(UIStrings.activityBasedBadgeAwardMessage, {PH1: badge.title}),
202+
actions: [
203+
{
204+
label: i18nString(UIStrings.badgeSettings),
205+
onClick: () => {/* TODO(ergunsh): Implement */},
206+
},
207+
{label: i18nString(UIStrings.viewProfile), onClick: () => { /* TODO(ergunsh): Implement */ }}
208+
],
209+
imageUri: badge.imageUri,
210+
});
101211
}
102212

103213
#close = (): void => {

0 commit comments

Comments
 (0)