Skip to content

Commit 29d440a

Browse files
ergunshDevtools-frontend LUCI CQ
authored andcommitted
[GdpIntegration] Update SyncSection to render GDP profile
Fixed: 436202677 Change-Id: Ie20f519f8d1b0602100a32f46816f2db88c42746 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6905365 Reviewed-by: Simon Zünd <[email protected]> Commit-Queue: Ergün Erdoğmuş <[email protected]>
1 parent 81e40ab commit 29d440a

File tree

11 files changed

+387
-11
lines changed

11 files changed

+387
-11
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ grd_files_bundled_sources = [
160160
"front_end/Images/frame-crossed.svg",
161161
"front_end/Images/frame-icon.svg",
162162
"front_end/Images/frame.svg",
163+
"front_end/Images/gdp-logo-standalone.svg",
163164
"front_end/Images/gear-filled.svg",
164165
"front_end/Images/gear.svg",
165166
"front_end/Images/gears.svg",

config/gni/devtools_image_files.gni

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ devtools_svg_sources = [
162162
"frame-crossed.svg",
163163
"frame-icon.svg",
164164
"frame.svg",
165+
"gdp-logo-standalone.svg",
165166
"gear-filled.svg",
166167
"gear.svg",
167168
"gears.svg",
Lines changed: 9 additions & 0 deletions
Loading

front_end/core/host/GdpClient.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import {InspectorFrontendHostInstance} from './InspectorFrontendHost.js';
66
import type {DispatchHttpRequestRequest, DispatchHttpRequestResult} from './InspectorFrontendHostAPI.js';
77

8-
enum SubscriptionStatus {
8+
export enum SubscriptionStatus {
99
ENABLED = 'SUBSCRIPTION_STATE_ENABLED',
1010
PENDING = 'SUBSCRIPTION_STATE_PENDING',
1111
CANCELED = 'SUBSCRIPTION_STATE_CANCELED',
@@ -14,7 +14,7 @@ enum SubscriptionStatus {
1414
ON_HOLD = 'SUBSCRIPTION_STATE_ACCOUNT_ON_HOLD',
1515
}
1616

17-
enum SubscriptionTier {
17+
export enum SubscriptionTier {
1818
PREMIUM_ANNUAL = 'SUBSCRIPTION_TIER_PREMIUM_ANNUAL',
1919
PREMIUM_MONTHLY = 'SUBSCRIPTION_TIER_PREMIUM_MONTHLY',
2020
PRO_ANNUAL = 'SUBSCRIPTION_TIER_PRO_ANNUAL',
@@ -35,13 +35,15 @@ interface CheckElibigilityResponse {
3535
createProfile: EligibilityStatus;
3636
}
3737

38-
interface Profile {
38+
export interface Profile {
3939
// Resource name of the profile.
4040
// Format: profiles/{obfuscated_profile_id}
4141
name: string;
4242
activeSubscription?: {
4343
subscriptionStatus: SubscriptionStatus,
44-
subscrionTier: SubscriptionTier,
44+
// To ensure forward compatibility, we accept any string, allowing the server to
45+
// introduce new subscription tiers without breaking older clients.
46+
subscriptionTier: SubscriptionTier|string,
4547
};
4648
}
4749

front_end/core/root/Runtime.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,11 @@ interface GlobalAiButton {
474474
promotionEnabled: boolean;
475475
}
476476

477+
interface GdpProfiles {
478+
enabled: boolean;
479+
starterBadgeEnabled: boolean;
480+
}
481+
477482
/**
478483
* The host configuration that we expect from the DevTools back-end.
479484
*
@@ -515,6 +520,7 @@ export type HostConfig = Platform.TypeScriptUtilities.RecursivePartial<{
515520
devToolsAiSubmenuPrompts: AiSubmenuPrompts,
516521
devToolsIpProtectionInDevTools: IpProtectionInDevTools,
517522
devToolsGlobalAiButton: GlobalAiButton,
523+
devToolsGdpProfiles: GdpProfiles,
518524
}>;
519525

520526
/**

front_end/entrypoints/main/main-meta.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,11 @@ const UIStrings = {
192192
* of syncing DevTools settings via Chrome Sync.
193193
*/
194194
enableSync: 'Enable settings sync',
195+
/**
196+
* @description Label for a checkbox in the settings UI. Allows developers to opt-in/opt-out
197+
* of receiving Google Developer Program (GDP) badges based on their activity in Chrome DevTools.
198+
*/
199+
receiveBadges: 'Receive badges',
195200
/**
196201
* @description A command available in the command menu to perform searches, for example in the
197202
* elements panel, as user types, rather than only when they press Enter.
@@ -790,6 +795,16 @@ Common.Settings.registerSettingExtension({
790795
reloadRequired: true,
791796
});
792797

798+
Common.Settings.registerSettingExtension({
799+
category: Common.Settings.SettingCategory.ACCOUNT,
800+
settingName: 'receive-gdp-badges',
801+
settingType: Common.Settings.SettingType.BOOLEAN,
802+
storageType: Common.Settings.SettingStorageType.SYNCED,
803+
title: i18nLazyString(UIStrings.receiveBadges),
804+
defaultValue: false,
805+
reloadRequired: true,
806+
});
807+
793808
Common.Settings.registerSettingExtension({
794809
storageType: Common.Settings.SettingStorageType.SYNCED,
795810
settingName: 'user-shortcuts',

front_end/panels/settings/SettingsScreen.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,8 @@ export class GenericSettingsTab extends UI.Widget.VBox implements SettingsTab {
339339
this.syncSection.data = {
340340
syncInfo,
341341
syncSetting: Common.Settings.moduleSetting('sync-preferences') as Common.Settings.Setting<boolean>,
342+
receiveBadgesSetting: Common.Settings.Settings.instance().moduleSetting('receive-gdp-badges'),
343+
gdpProfile: undefined,
342344
};
343345
if (!syncInfo.isSyncActive || !syncInfo.arePreferencesSynced) {
344346
this.#updateSyncSectionTimerId = window.setTimeout(this.updateSyncSection.bind(this), 500);

front_end/panels/settings/components/SyncSection.test.ts

Lines changed: 155 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +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 {renderElementIntoDOM} from '../../../testing/DOMHelpers.js';
6-
import {createFakeSetting, describeWithLocale} from '../../../testing/EnvironmentHelpers.js';
7+
import {createFakeSetting, describeWithLocale, updateHostConfig} from '../../../testing/EnvironmentHelpers.js';
78
import * as RenderCoordinator from '../../../ui/components/render_coordinator/render_coordinator.js';
89
import * as SettingComponents from '../../../ui/components/settings/settings.js';
910

@@ -22,12 +23,14 @@ async function renderSyncSection(data: PanelComponents.SyncSection.SyncSectionDa
2223
describeWithLocale('SyncSection', () => {
2324
it('shows a warning tooltip when sync is not active and the user is signed in', async () => {
2425
const syncSetting = createFakeSetting<boolean>('setting', true);
26+
const receiveBadgesSetting = createFakeSetting<boolean>('receive-badges', true);
2527
const {shadowRoot} = await renderSyncSection({
2628
syncInfo: {
2729
isSyncActive: false,
2830
accountEmail: '[email protected]',
2931
},
30-
syncSetting
32+
syncSetting,
33+
receiveBadgesSetting,
3134
});
3235
const warning = shadowRoot.querySelector('devtools-tooltip');
3336
assert.instanceOf(warning, HTMLElement);
@@ -36,13 +39,15 @@ describeWithLocale('SyncSection', () => {
3639

3740
it('shows a warning tooltip when sync is active but preferences bucket is not synced', async () => {
3841
const syncSetting = createFakeSetting<boolean>('setting', true);
42+
const receiveBadgesSetting = createFakeSetting<boolean>('receive-badges', true);
3943
const {shadowRoot} = await renderSyncSection({
4044
syncInfo: {
4145
isSyncActive: true,
4246
arePreferencesSynced: false,
4347
accountEmail: '[email protected]',
4448
},
45-
syncSetting
49+
receiveBadgesSetting,
50+
syncSetting,
4651
});
4752

4853
const warning = shadowRoot.querySelector('devtools-tooltip');
@@ -53,12 +58,14 @@ describeWithLocale('SyncSection', () => {
5358

5459
it('disables the checkbox when sync is not active', async () => {
5560
const syncSetting = createFakeSetting<boolean>('setting', true);
61+
const receiveBadgesSetting = createFakeSetting<boolean>('receive-badges', true);
5662
const {shadowRoot} = await renderSyncSection({
5763
syncInfo: {
5864
isSyncActive: false,
5965
accountEmail: '[email protected]',
6066
},
61-
syncSetting
67+
receiveBadgesSetting,
68+
syncSetting,
6269
});
6370

6471
const settingCheckbox = shadowRoot.querySelector('setting-checkbox');
@@ -73,13 +80,15 @@ describeWithLocale('SyncSection', () => {
7380

7481
it('shows the avatar and email of the logged in user', async () => {
7582
const syncSetting = createFakeSetting<boolean>('setting', true);
83+
const receiveBadgesSetting = createFakeSetting<boolean>('receive-badges', true);
7684
const {shadowRoot} = await renderSyncSection({
7785
syncInfo: {
7886
isSyncActive: true,
7987
arePreferencesSynced: true,
8088
accountEmail: '[email protected]',
8189
accountImage: '<png encoded as base64>',
8290
},
91+
receiveBadgesSetting,
8392
syncSetting,
8493
});
8594

@@ -94,11 +103,13 @@ describeWithLocale('SyncSection', () => {
94103

95104
it('shows not signed in if the user is not logged in', async () => {
96105
const syncSetting = createFakeSetting<boolean>('setting', true);
106+
const receiveBadgesSetting = createFakeSetting<boolean>('receive-badges', true);
97107
const {shadowRoot} = await renderSyncSection({
98108
syncInfo: {
99109
isSyncActive: false,
100110
arePreferencesSynced: false,
101111
},
112+
receiveBadgesSetting,
102113
syncSetting,
103114
});
104115

@@ -107,4 +118,144 @@ describeWithLocale('SyncSection', () => {
107118

108119
assert.include(email.innerText, 'not signed into Chrome');
109120
});
121+
122+
it('does not render the GDP section if the feature is disabled', async () => {
123+
updateHostConfig({}); // Disable feature
124+
const syncSetting = createFakeSetting<boolean>('setting', true);
125+
const receiveBadgesSetting = createFakeSetting<boolean>('receive-badges', true);
126+
const {shadowRoot} = await renderSyncSection({
127+
syncInfo: {
128+
isSyncActive: true,
129+
arePreferencesSynced: true,
130+
accountEmail: '[email protected]',
131+
},
132+
syncSetting,
133+
receiveBadgesSetting,
134+
});
135+
const gdpSection = shadowRoot.querySelector('.gdp-profile-container');
136+
assert.isNull(gdpSection);
137+
});
138+
139+
describe('Google Developer Program profile', () => {
140+
beforeEach(() => {
141+
updateHostConfig({
142+
devToolsGdpProfiles: {
143+
enabled: true,
144+
},
145+
});
146+
});
147+
148+
afterEach(() => {
149+
updateHostConfig({});
150+
});
151+
152+
it('renders the sign-up state when the user does not have a GDP profile', async () => {
153+
const syncSetting = createFakeSetting<boolean>('setting', true);
154+
const receiveBadgesSetting = createFakeSetting<boolean>('receive-badges', true);
155+
const {shadowRoot} = await renderSyncSection({
156+
syncInfo: {
157+
isSyncActive: true,
158+
arePreferencesSynced: true,
159+
accountEmail: '[email protected]',
160+
},
161+
syncSetting,
162+
receiveBadgesSetting,
163+
gdpProfile: undefined, // No profile
164+
});
165+
const gdpSection = shadowRoot.querySelector('.gdp-profile-container');
166+
assert.instanceOf(gdpSection, HTMLElement);
167+
168+
const signUpButton = gdpSection.querySelector('devtools-button');
169+
assert.instanceOf(signUpButton, HTMLElement);
170+
assert.strictEqual(signUpButton.innerText, 'Sign up');
171+
172+
const brandHeader = gdpSection.querySelector('.gdp-profile-header');
173+
assert.instanceOf(brandHeader, HTMLElement);
174+
assert.include(brandHeader.innerText, 'Google Developer Program');
175+
});
176+
177+
it('renders the profile details with standard plan', async () => {
178+
const syncSetting = createFakeSetting<boolean>('setting', true);
179+
const receiveBadgesSetting = createFakeSetting<boolean>('receive-badges', true);
180+
const gdpProfile: Host.GdpClient.Profile = {name: 'profile-name'}; // No active subscription
181+
const {shadowRoot} = await renderSyncSection({
182+
syncInfo: {
183+
isSyncActive: true,
184+
arePreferencesSynced: true,
185+
accountEmail: '[email protected]',
186+
},
187+
syncSetting,
188+
receiveBadgesSetting,
189+
gdpProfile,
190+
});
191+
192+
const gdpSection = shadowRoot.querySelector('.gdp-profile-container');
193+
assert.instanceOf(gdpSection, HTMLElement);
194+
195+
const planDetails = gdpSection.querySelector('.plan-details');
196+
assert.instanceOf(planDetails, HTMLElement);
197+
assert.include(planDetails.innerText, 'Standard plan');
198+
199+
const viewProfileLink = gdpSection.querySelector('x-link');
200+
assert.instanceOf(viewProfileLink, HTMLElement);
201+
assert.strictEqual(viewProfileLink.innerText, 'View profile');
202+
203+
const receiveBadgesCheckbox = gdpSection.querySelector('setting-checkbox');
204+
assert.instanceOf(receiveBadgesCheckbox, SettingComponents.SettingCheckbox.SettingCheckbox);
205+
});
206+
207+
const subscriptionTiers = [
208+
{
209+
tier: Host.GdpClient.SubscriptionTier.PREMIUM_ANNUAL,
210+
expectedText: 'Premium (Annual)',
211+
},
212+
{
213+
tier: Host.GdpClient.SubscriptionTier.PREMIUM_MONTHLY,
214+
expectedText: 'Premium (Monthly)',
215+
},
216+
{
217+
tier: Host.GdpClient.SubscriptionTier.PRO_ANNUAL,
218+
expectedText: 'Pro (Annual)',
219+
},
220+
{
221+
tier: Host.GdpClient.SubscriptionTier.PRO_MONTHLY,
222+
expectedText: 'Pro (Monthly)',
223+
},
224+
{
225+
tier: 'unknown-tier',
226+
expectedText: 'Unknown plan',
227+
},
228+
];
229+
230+
for (const {tier, expectedText} of subscriptionTiers) {
231+
it(`renders the profile details with ${expectedText} plan`, async () => {
232+
const syncSetting = createFakeSetting<boolean>('setting', true);
233+
const receiveBadgesSetting = createFakeSetting<boolean>('receive-badges', true);
234+
const gdpProfile: Host.GdpClient.Profile = {
235+
name: 'profile-name',
236+
activeSubscription: {
237+
subscriptionStatus: Host.GdpClient.SubscriptionStatus.ENABLED,
238+
subscriptionTier: tier,
239+
},
240+
};
241+
const {shadowRoot} = await renderSyncSection({
242+
syncInfo: {
243+
isSyncActive: true,
244+
arePreferencesSynced: true,
245+
accountEmail: '[email protected]',
246+
},
247+
syncSetting,
248+
receiveBadgesSetting,
249+
gdpProfile,
250+
});
251+
252+
const gdpSection = shadowRoot.querySelector('.gdp-profile-container');
253+
assert.instanceOf(gdpSection, HTMLElement);
254+
255+
const planDetails = gdpSection.querySelector('.plan-details');
256+
assert.instanceOf(planDetails, HTMLElement);
257+
assert.include(planDetails.innerText, expectedText);
258+
});
259+
}
260+
});
110261
});

0 commit comments

Comments
 (0)