UNPKG

chrome-devtools-frontend

Version:
340 lines (304 loc) • 11.4 kB
// Copyright 2025 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../core/common/common.js'; import * as Host from '../../core/host/host.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as Badges from '../../models/badges/badges.js'; import * as Buttons from '../../ui/components/buttons/buttons.js'; import * as UIHelpers from '../../ui/helpers/helpers.js'; import * as uiI18n from '../../ui/i18n/i18n.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as Lit from '../../ui/lit/lit.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import badgeNotificationStyles from './badgeNotification.css.js'; import * as GdpSignUpDialog from './GdpSignUpDialog.js'; const {html, render} = Lit; const UIStrings = { /** * @description Title for close button */ close: 'Close', /** * @description Activity based badge award notification text * @example {Badge Title} PH1 */ activityBasedBadgeAwardMessage: 'You earned the {PH1} badge! It’s been added to your Developer Profile.', /** * @description Action title for navigating to the badge settings in Google Developer Profile section */ manageSettings: 'Manage settings', /** * @description Action title for opening the Google Developer Program profile page of the user in a new tab */ viewProfile: 'View profile', /** * @description Starter badge award notification text when the user has a Google Developer Program profile but did not enable receiving badges in DevTools yet * @example {Badge Title} PH1 * @example {Google Developer Program link} PH2 */ starterBadgeAwardMessageSettingDisabled: 'You earned the {PH1} badge for the {PH2}! Turn on badges to claim it.', /** * @description Starter badge award notification text when the user does not have a Google Developer Program profile. * @example {Badge Title} PH1 * @example {Google Developer Program link} PH2 */ starterBadgeAwardMessageNoGdpProfile: 'You earned the {PH1} badge for the {PH2}! Create a profile to claim your badge.', /** * @description Action title for snoozing the starter badge. */ remindMeLater: 'Remind me later', /** * @description Action title for enabling the "Receive badges" setting */ receiveBadges: 'Turn on badges', /** * @description Action title for creating a Google Developer Program profle */ createProfile: 'Create profile', } as const; const str_ = i18n.i18n.registerUIStrings('panels/common/BadgeNotification.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const i18nFormatString = uiI18n.getFormatLocalizedString.bind(undefined, str_); const lockedString = i18n.i18n.lockedString; const LEFT_OFFSET = 5; const BOTTOM_OFFSET = 5; const AUTO_CLOSE_TIME_IN_MS = 30000; export interface BadgeNotificationAction { label: string; jslogContext: string; title?: string; onClick: () => void; } export interface BadgeNotificationProperties { message: HTMLElement|string; jslogContext: string; imageUri: string; actions: BadgeNotificationAction[]; isStarterBadge: boolean; } export interface ViewInput extends BadgeNotificationProperties { onDismissClick: () => void; } // clang-format off const DEFAULT_VIEW = (input: ViewInput, _output: undefined, target: HTMLElement): void => { const actionButtons = input.actions.map(property => { return html`<devtools-button class="notification-button" @click=${() => property.onClick()} jslog=${VisualLogging.action(property.jslogContext).track({click: true})} .variant=${Buttons.Button.Variant.TEXT} .title=${property.title ?? ''} .inverseColorTheme=${true} >${property.label}</devtools-button>`; }); const crossButton = html`<devtools-button class="dismiss notification-button" @click=${input.onDismissClick} jslog=${VisualLogging.action('badge-notification.dismiss').track({click: true})} aria-label=${i18nString(UIStrings.close)} .iconName=${'cross'} .variant=${Buttons.Button.Variant.ICON} .title=${i18nString(UIStrings.close)} .inverseColorTheme=${true} ></devtools-button>`; render(html` <style>${badgeNotificationStyles}</style> <div class="container" jslog=${VisualLogging.dialog('badge-notification')}> <div class="badge-container" jslog=${VisualLogging.item(input.jslogContext)}> <img class="badge-image" role="presentation" src=${input.imageUri}> </div> <div class="action-and-text-container"> <div class="label-container"> <div class="message">${input.message}</div> ${crossButton} </div> <div class="long-action-container">${actionButtons}</div> </div> </div> `, target); }; // clang-format on type View = typeof DEFAULT_VIEW; function revealBadgeSettings(): void { void Common.Revealer.reveal(Common.Settings.moduleSetting('receive-gdp-badges')); } export class BadgeNotification extends UI.Widget.Widget { jslogContext = ''; message: HTMLElement|string = ''; imageUri = ''; actions: BadgeNotificationAction[] = []; isStarterBadge = false; #autoCloseTimeout?: number; #view: View; constructor(element?: HTMLElement, view: View = DEFAULT_VIEW) { super(element); this.#view = view; // eslint-disable-next-line this.contentElement.role = 'alert'; this.markAsRoot(); } async present(badge: Badges.Badge): Promise<void> { if (badge.isStarterBadge) { await this.#presentStarterBadge(badge); } else { this.#presentActivityBasedBadge(badge); } } #positionNotification(): void { const boundingRect = this.contentElement.getBoundingClientRect(); const container = UI.UIUtils.getDevToolsBoundingElement(); this.contentElement.positionAt( LEFT_OFFSET, container.clientHeight - boundingRect.height - BOTTOM_OFFSET, container); } #show(properties: BadgeNotificationProperties): void { this.message = properties.message; this.imageUri = properties.imageUri; this.actions = properties.actions; this.isStarterBadge = properties.isStarterBadge; this.jslogContext = properties.jslogContext; this.requestUpdate(); this.show(document.body); void this.updateComplete.then(() => { this.#positionNotification(); }); if (this.#autoCloseTimeout) { window.clearTimeout(this.#autoCloseTimeout); } this.#autoCloseTimeout = window.setTimeout(this.#onAutoClose, AUTO_CLOSE_TIME_IN_MS); } async #presentStarterBadge(badge: Badges.Badge): Promise<void> { const getProfileResponse = await Host.GdpClient.GdpClient.instance().getProfile(); // The `getProfile` call failed and returned a `null`. // For that case, we don't show anything. if (!getProfileResponse) { return; } const hasGdpProfile = Boolean(getProfileResponse.profile); const receiveBadgesSettingEnabled = Badges.UserBadges.instance().isReceiveBadgesSettingEnabled(); const googleDeveloperProgramLink = UI.XLink.XLink.create( 'https://developers.google.com/program', lockedString('Google Developer Program'), 'badge-link', undefined, 'program-link'); // If the user already has a GDP profile and the receive badges setting enabled, // starter badge behaves as if it's an activity based badge. if (hasGdpProfile && receiveBadgesSettingEnabled) { this.#presentActivityBasedBadge(badge); return; } // If the user already has a GDP profile and the receive badges setting disabled, // starter badge behaves as a nudge for opting into receiving badges. if (hasGdpProfile && !receiveBadgesSettingEnabled) { this.#show({ message: i18nFormatString( UIStrings.starterBadgeAwardMessageSettingDisabled, {PH1: badge.title, PH2: googleDeveloperProgramLink}), jslogContext: badge.jslogContext, actions: [ { label: i18nString(UIStrings.remindMeLater), jslogContext: 'remind-me-later', onClick: () => { this.detach(); Badges.UserBadges.instance().snoozeStarterBadge(); }, }, { label: i18nString(UIStrings.receiveBadges), jslogContext: 'receive-badges', onClick: () => { this.detach(); revealBadgeSettings(); } } ], imageUri: badge.imageUri, isStarterBadge: true, }); return; } // The user does not have a GDP profile, starter badge acts as a nudge for creating a GDP profile. this.#show({ message: i18nFormatString( UIStrings.starterBadgeAwardMessageNoGdpProfile, {PH1: badge.title, PH2: googleDeveloperProgramLink}), jslogContext: badge.jslogContext, actions: [ { label: i18nString(UIStrings.remindMeLater), jslogContext: 'remind-me-later', onClick: () => { this.detach(); Badges.UserBadges.instance().snoozeStarterBadge(); }, }, { label: i18nString(UIStrings.createProfile), jslogContext: 'create-profile', onClick: () => { this.detach(); GdpSignUpDialog.GdpSignUpDialog.show({ // We want to consider cancelling from the starter badge as a "snooze" for starter badge. onCancel: () => Badges.UserBadges.instance().snoozeStarterBadge(), }); } } ], imageUri: badge.imageUri, isStarterBadge: true, }); } #presentActivityBasedBadge(badge: Badges.Badge): void { this.#show({ message: i18nString(UIStrings.activityBasedBadgeAwardMessage, {PH1: badge.title}), jslogContext: badge.jslogContext, actions: [ { label: i18nString(UIStrings.manageSettings), jslogContext: 'manage-settings', onClick: () => { this.detach(); revealBadgeSettings(); }, }, { label: i18nString(UIStrings.viewProfile), jslogContext: 'view-profile', onClick: () => { UIHelpers.openInNewTab(Host.GdpClient.GOOGLE_DEVELOPER_PROGRAM_PROFILE_LINK); } } ], imageUri: badge.imageUri, isStarterBadge: badge.isStarterBadge, }); } override onDetach(): void { window.clearTimeout(this.#autoCloseTimeout); } #onDismissClick = (): void => { this.detach(); if (this.isStarterBadge) { Badges.UserBadges.instance().dismissStarterBadge(); } }; #onAutoClose = (): void => { this.detach(); if (this.isStarterBadge) { Badges.UserBadges.instance().snoozeStarterBadge(); } }; override wasShown(): void { super.wasShown(); this.requestUpdate(); } override performUpdate(): void { const viewInput: ViewInput = { message: this.message, imageUri: this.imageUri, actions: this.actions, isStarterBadge: this.isStarterBadge, onDismissClick: this.#onDismissClick, jslogContext: this.jslogContext, }; this.#view(viewInput, undefined, this.contentElement); } }