diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 97e60c55d4b4f..d44176811e546 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -1,5 +1,6 @@ export declare global { declare const DEBUG: boolean; + declare const GL_PROMO_URI: string | undefined; export type PartialDeep = T extends Record ? { [K in keyof T]?: PartialDeep } : T; export type Optional = Omit & { [P in K]?: T[P] }; diff --git a/src/commands/quickCommand.steps.ts b/src/commands/quickCommand.steps.ts index fc96a40e22864..ca4fd713a8a71 100644 --- a/src/commands/quickCommand.steps.ts +++ b/src/commands/quickCommand.steps.ts @@ -45,7 +45,7 @@ import { } from '../git/utils/reference.utils'; import { getHighlanderProviderName } from '../git/utils/remote.utils'; import { createRevisionRange, isRevisionRange } from '../git/utils/revision.utils'; -import { getApplicablePromo } from '../plus/gk/utils/promo.utils'; +import { getApplicablePromo } from '../plus/gk/account/promos'; import { isSubscriptionPaidPlan, isSubscriptionPreviewTrialExpired } from '../plus/gk/utils/subscription.utils'; import type { LaunchpadCommandArgs } from '../plus/launchpad/launchpad'; import { @@ -2651,7 +2651,7 @@ export async function* ensureAccessStep< } else { if (access.subscription.required == null) return access; - const promo = getApplicablePromo(access.subscription.current.state, 'gate'); + const promo = await getApplicablePromo(access.subscription.current.state, 'gate'); const detail = promo?.quickpick.detail; placeholder = 'Pro feature — requires a trial or GitLens Pro for use on privately-hosted repos'; diff --git a/src/constants.promos.ts b/src/constants.promos.ts index 32e7c64971f0b..572dd0e9bcaf8 100644 --- a/src/constants.promos.ts +++ b/src/constants.promos.ts @@ -1,24 +1 @@ -import { SubscriptionState } from './constants.subscription'; -import type { Promo } from './plus/gk/models/promo'; - -export type PromoKeys = 'pro50'; - -// Must be ordered by applicable order -export const promos: Promo[] = [ - { - key: 'pro50', - states: [ - SubscriptionState.Community, - SubscriptionState.ProPreview, - SubscriptionState.ProPreviewExpired, - SubscriptionState.ProTrial, - SubscriptionState.ProTrialExpired, - SubscriptionState.ProTrialReactivationEligible, - ], - command: { tooltip: 'Save 55% or more on your 1st seat of Pro.' }, - locations: ['account', 'badge', 'gate'], - quickpick: { - detail: '$(star-full) Save 55% or more on your 1st seat of Pro', - }, - }, -]; +export type PromoKeys = 'pro50' | 'gkholiday'; diff --git a/src/plus/gk/account/promos.ts b/src/plus/gk/account/promos.ts new file mode 100644 index 0000000000000..d550d8700192b --- /dev/null +++ b/src/plus/gk/account/promos.ts @@ -0,0 +1,214 @@ +import fetch from 'node-fetch'; +import type { PromoKeys } from '../../../constants.promos'; +import { SubscriptionState } from '../../../constants.subscription'; +import { wait } from '../../../system/promise'; +import { pickApplicablePromo } from '../utils/promo.utils'; + +export type PromoLocation = 'account' | 'badge' | 'gate' | 'home'; + +export interface Promo { + readonly key: PromoKeys; + readonly code?: string; + readonly states?: SubscriptionState[]; + readonly expiresOn?: number; + readonly startsOn?: number; + + readonly command?: { + command?: `command:${string}`; + tooltip: string; + }; + readonly locations?: PromoLocation[]; + readonly quickpick: { detail: string }; +} + +function isValidDate(d: Date) { + // @ts-expect-error isNaN expects number, but works with Date instance + return d instanceof Date && !isNaN(d); +} + +type Modify = Omit & R; +type SerializedPromo = Modify< + Promo, + { + startsOn?: string; + expiresOn?: string; + states?: string[]; + } +>; + +function deserializePromo(input: object): Promo[] { + try { + const object = input as Array; + const validPromos: Array = []; + if (typeof object !== 'object' || !Array.isArray(object)) { + throw new Error('deserializePromo: input is not array'); + } + const allowedPromoKeys: Record = { gkholiday: true, pro50: true }; + for (const promoItem of object) { + let states: SubscriptionState[] | undefined = undefined; + let locations: PromoLocation[] | undefined = undefined; + if (!promoItem.key || !allowedPromoKeys[promoItem.key]) { + console.warn('deserializePromo: promo item with no id detected and skipped'); + continue; + } + if (!promoItem.quickpick?.detail) { + console.warn( + `deserializePromo: no detail provided for promo with key ${promoItem.key} detected and skipped`, + ); + continue; + } + if (promoItem.states && !Array.isArray(promoItem.states)) { + console.warn( + `deserializePromo: promo with key ${promoItem.key} is skipped because of incorrect states value`, + ); + continue; + } + if (promoItem.states) { + states = []; + for (const state of promoItem.states) { + // @ts-expect-error unsafe work with enum object + if (Object.hasOwn(SubscriptionState, state)) { + // @ts-expect-error unsafe work with enum object + states.push(SubscriptionState[state]); + } else { + console.warn( + `deserializePromo: invalid state value "${state}" detected and skipped at promo with key ${promoItem.key}`, + ); + } + } + } + if (promoItem.locations && !Array.isArray(promoItem.locations)) { + console.warn( + `deserializePromo: promo with key ${promoItem.key} is skipped because of incorrect locations value`, + ); + continue; + } + if (promoItem.locations) { + locations = []; + const allowedLocations: Record = { + account: true, + badge: true, + gate: true, + home: true, + }; + for (const location of promoItem.locations) { + if (allowedLocations[location]) { + locations.push(location); + } else { + console.warn( + `deserializePromo: invalid location value "${location}" detected and skipped at promo with key ${promoItem.key}`, + ); + } + } + } + if (promoItem.code && typeof promoItem.code !== 'string') { + console.warn( + `deserializePromo: promo with key ${promoItem.key} is skipped because of incorrect code value`, + ); + continue; + } + if ( + promoItem.command && + (typeof promoItem.command.tooltip !== 'string' || + (promoItem.command.command && typeof promoItem.command.command !== 'string')) + ) { + console.warn( + `deserializePromo: promo with key ${promoItem.key} is skipped because of incorrect code value`, + ); + continue; + } + if ( + promoItem.expiresOn && + (typeof promoItem.expiresOn !== 'string' || !isValidDate(new Date(promoItem.expiresOn))) + ) { + console.warn( + `deserializePromo: promo with key ${promoItem.key} is skipped because of incorrect expiresOn value: ISO date string is expected`, + ); + continue; + } + if ( + promoItem.startsOn && + (typeof promoItem.startsOn !== 'string' || !isValidDate(new Date(promoItem.startsOn))) + ) { + console.warn( + `deserializePromo: promo with key ${promoItem.key} is skipped because of incorrect startsOn value: ISO date string is expected`, + ); + continue; + } + validPromos.push({ + ...promoItem, + expiresOn: promoItem.expiresOn ? new Date(promoItem.expiresOn).getTime() : undefined, + startsOn: promoItem.startsOn ? new Date(promoItem.startsOn).getTime() : undefined, + states: states, + locations: locations, + }); + } + return validPromos; + } catch (e) { + throw new Error(`deserializePromo: Could not deserialize promo: ${e.message ?? e}`); + } +} + +export class PromoProvider { + private _isInitialized: boolean = false; + private _initPromise: Promise | undefined; + private _promo: Array | undefined; + constructor() { + void this.waitForFirstRefreshInitialized(); + } + + private async waitForFirstRefreshInitialized() { + if (this._isInitialized) { + return; + } + if (!this._initPromise) { + this._initPromise = this.initialize().then(() => { + this._isInitialized = true; + }); + } + await this._initPromise; + } + + async initialize(): Promise { + await wait(1000); + if (this._isInitialized) { + return; + } + try { + console.log('PromoProvider GL_PROMO_URI', GL_PROMO_URI); + if (!GL_PROMO_URI) { + throw new Error('No GL_PROMO_URI env variable provided'); + } + const jsonBody = JSON.parse(await fetch(GL_PROMO_URI).then(x => x.text())); + this._promo = deserializePromo(jsonBody); + } catch (e) { + console.error('PromoProvider error', e); + } + } + + async getPromoList(): Promise { + try { + await this.waitForFirstRefreshInitialized(); + return this._promo!; + } catch { + return undefined; + } + } + + async getApplicablePromo( + state: number | undefined, + location?: PromoLocation, + key?: PromoKeys, + ): Promise { + try { + await this.waitForFirstRefreshInitialized(); + return pickApplicablePromo(this._promo, state, location, key); + } catch { + return undefined; + } + } +} + +export const promoProvider = new PromoProvider(); + +export const getApplicablePromo = promoProvider.getApplicablePromo.bind(promoProvider); diff --git a/src/plus/gk/subscriptionService.ts b/src/plus/gk/subscriptionService.ts index 2b764135f6433..9a68571593d4b 100644 --- a/src/plus/gk/subscriptionService.ts +++ b/src/plus/gk/subscriptionService.ts @@ -63,6 +63,7 @@ import { flatten } from '../../system/object'; import { pauseOnCancelOrTimeout } from '../../system/promise'; import { pluralize } from '../../system/string'; import { satisfies } from '../../system/version'; +import { getApplicablePromo } from './account/promos'; import { LoginUriPathPrefix } from './authenticationConnection'; import { authenticationProviderScopes } from './authenticationProvider'; import type { GKCheckInResponse } from './models/checkin'; @@ -71,7 +72,6 @@ import type { Subscription } from './models/subscription'; import type { ServerConnection } from './serverConnection'; import { ensurePlusFeaturesEnabled } from './utils/-webview/plus.utils'; import { getSubscriptionFromCheckIn } from './utils/checkin.utils'; -import { getApplicablePromo } from './utils/promo.utils'; import { assertSubscriptionState, computeSubscriptionState, @@ -893,7 +893,7 @@ export class SubscriptionService implements Disposable { const hasAccount = this._subscription.account != null; - const promoCode = getApplicablePromo(this._subscription.state)?.code; + const promoCode = (await getApplicablePromo(this._subscription.state))?.code; if (promoCode != null) { query.set('promoCode', promoCode); } @@ -1375,8 +1375,9 @@ export class SubscriptionService implements Disposable { subscription.state = computeSubscriptionState(subscription); assertSubscriptionState(subscription); - const promo = getApplicablePromo(subscription.state); - void setContext('gitlens:promo', promo?.key); + void getApplicablePromo(subscription.state).then(promo => { + void setContext('gitlens:promo', promo?.key); + }); const previous = this._subscription as typeof this._subscription | undefined; // Can be undefined here, since we call this in the constructor // Check the previous and new subscriptions are exactly the same diff --git a/src/plus/gk/utils/promo.utils.ts b/src/plus/gk/utils/promo.utils.ts index af63d01d8c601..d528a134a252f 100644 --- a/src/plus/gk/utils/promo.utils.ts +++ b/src/plus/gk/utils/promo.utils.ts @@ -1,16 +1,17 @@ import type { PromoKeys } from '../../../constants.promos'; -import { promos } from '../../../constants.promos'; +import type { SubscriptionState } from '../../../constants.subscription'; import type { Promo, PromoLocation } from '../models/promo'; -export function getApplicablePromo( - state: number | undefined, +export const pickApplicablePromo = ( + promoList: Promo[] | undefined, + subscriptionState: SubscriptionState | undefined, location?: PromoLocation, key?: PromoKeys, -): Promo | undefined { - if (state == null) return undefined; +): Promo | undefined => { + if (subscriptionState == null || !promoList) return undefined; - for (const promo of promos) { - if ((key == null || key === promo.key) && isPromoApplicable(promo, state)) { + for (const promo of promoList) { + if ((key == null || key === promo.key) && isPromoApplicable(promo, subscriptionState)) { if (location == null || promo.locations == null || promo.locations.includes(location)) { return promo; } @@ -20,7 +21,7 @@ export function getApplicablePromo( } return undefined; -} +}; function isPromoApplicable(promo: Promo, state: number): boolean { const now = Date.now(); diff --git a/src/webviews/apps/home/components/promo-banner.ts b/src/webviews/apps/home/components/promo-banner.ts index 70f4c4552bd12..1581be911c2fe 100644 --- a/src/webviews/apps/home/components/promo-banner.ts +++ b/src/webviews/apps/home/components/promo-banner.ts @@ -1,11 +1,11 @@ import { consume } from '@lit/context'; import { css, html, LitElement, nothing } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; -import type { Promo } from '../../../../plus/gk/models/promo'; -import { getApplicablePromo } from '../../../../plus/gk/utils/promo.utils'; +import type { Promo } from '../../../../plus/gk/account/promos'; import type { State } from '../../../home/protocol'; -import { stateContext } from '../context'; import '../../shared/components/promo'; +import { promoContext } from '../../shared/context'; +import { stateContext } from '../context'; @customElement('gl-promo-banner') export class GlPromoBanner extends LitElement { @@ -33,21 +33,25 @@ export class GlPromoBanner extends LitElement { private _state!: State; @property({ type: Boolean, reflect: true, attribute: 'has-promo' }) - get hasPromos(): boolean | undefined { - return this.promo == null ? undefined : true; - } + hasPromos?: boolean; + + @consume({ context: promoContext, subscribe: true }) + private readonly getApplicablePromo!: typeof promoContext.__context__; - get promo(): Promo | undefined { - return getApplicablePromo(this._state.subscription.state, 'home'); + getPromo(): Promo | undefined { + const promo = this.getApplicablePromo(this._state.subscription.state, 'home'); + this.hasPromos = promo == null ? undefined : true; + return promo; } - override render(): unknown { - if (!this.promo) { + override render() { + const promo = this.getPromo(); + if (!promo) { return nothing; } return html` - + `; } } diff --git a/src/webviews/apps/plus/shared/components/account-chip.ts b/src/webviews/apps/plus/shared/components/account-chip.ts index 8d987230437ed..cf9d8ef60ce9a 100644 --- a/src/webviews/apps/plus/shared/components/account-chip.ts +++ b/src/webviews/apps/plus/shared/components/account-chip.ts @@ -6,7 +6,6 @@ import { urls } from '../../../../../constants'; import { proTrialLengthInDays, SubscriptionPlanId, SubscriptionState } from '../../../../../constants.subscription'; import type { Source } from '../../../../../constants.telemetry'; import type { Promo } from '../../../../../plus/gk/models/promo'; -import { getApplicablePromo } from '../../../../../plus/gk/utils/promo.utils'; import { getSubscriptionPlanTier, getSubscriptionStateName, @@ -17,13 +16,14 @@ import { createCommandLink } from '../../../../../system/commands'; import { pluralize } from '../../../../../system/string'; import type { State } from '../../../../home/protocol'; import { stateContext } from '../../../home/context'; -import type { GlPopover } from '../../../shared/components/overlays/popover.react'; -import { elementBase, linkBase } from '../../../shared/components/styles/lit/base.css'; -import { chipStyles } from './chipStyles'; import '../../../shared/components/button'; import '../../../shared/components/button-container'; import '../../../shared/components/code-icon'; import '../../../shared/components/overlays/popover'; +import type { GlPopover } from '../../../shared/components/overlays/popover.react'; +import { elementBase, linkBase } from '../../../shared/components/styles/lit/base.css'; +import { promoContext } from '../../../shared/context'; +import { chipStyles } from './chipStyles'; @customElement('gl-account-chip') export class GLAccountChip extends LitElement { @@ -395,8 +395,11 @@ export class GLAccountChip extends LitElement { `; } + @consume({ context: promoContext, subscribe: true }) + private readonly getApplicablePromo!: typeof promoContext.__context__; + private renderAccountState() { - const promo = getApplicablePromo(this.subscriptionState, 'account'); + const promo = this.getApplicablePromo(this.subscriptionState, 'account'); switch (this.subscriptionState) { case SubscriptionState.Paid: diff --git a/src/webviews/apps/plus/shared/components/feature-gate-plus-state.ts b/src/webviews/apps/plus/shared/components/feature-gate-plus-state.ts index fc70056c5628a..1f102fe7c3984 100644 --- a/src/webviews/apps/plus/shared/components/feature-gate-plus-state.ts +++ b/src/webviews/apps/plus/shared/components/feature-gate-plus-state.ts @@ -1,3 +1,5 @@ +import { consume } from '@lit/context'; +import type { TemplateResult } from 'lit'; import { css, html, LitElement, nothing } from 'lit'; import { customElement, property, query } from 'lit/decorators.js'; import { urls } from '../../../../../constants'; @@ -12,12 +14,12 @@ import type { Source } from '../../../../../constants.telemetry'; import type { FeaturePreview } from '../../../../../features'; import { getFeaturePreviewStatus } from '../../../../../features'; import type { Promo } from '../../../../../plus/gk/models/promo'; -import { getApplicablePromo } from '../../../../../plus/gk/utils/promo.utils'; import { pluralize } from '../../../../../system/string'; -import type { GlButton } from '../../../shared/components/button'; -import { linkStyles } from './vscode.css'; import '../../../shared/components/button'; +import type { GlButton } from '../../../shared/components/button'; import '../../../shared/components/promo'; +import { promoContext } from '../../../shared/context'; +import { linkStyles } from './vscode.css'; declare global { interface HTMLElementTagNameMap { @@ -112,7 +114,10 @@ export class GlFeatureGatePlusState extends LitElement { } } - override render(): unknown { + @consume({ context: promoContext, subscribe: true }) + private readonly getApplicablePromo!: typeof promoContext.__context__; + + override render(): TemplateResult | undefined { if (this.state == null) { this.hidden = true; return undefined; @@ -120,7 +125,7 @@ export class GlFeatureGatePlusState extends LitElement { this.hidden = false; const appearance = (this.appearance ?? 'alert') === 'alert' ? 'alert' : nothing; - const promo = this.state ? getApplicablePromo(this.state, 'gate') : undefined; + const promo = this.state ? this.getApplicablePromo(this.state, 'gate') : undefined; switch (this.state) { case SubscriptionState.VerificationRequired: diff --git a/src/webviews/apps/shared/app.ts b/src/webviews/apps/shared/app.ts index e0f009470ebf8..ee00c2acbbb90 100644 --- a/src/webviews/apps/shared/app.ts +++ b/src/webviews/apps/shared/app.ts @@ -1,13 +1,19 @@ import { provide } from '@lit/context'; import { html, LitElement } from 'lit'; -import { property } from 'lit/decorators.js'; +import { property, state } from 'lit/decorators.js'; import type { CustomEditorIds, WebviewIds, WebviewViewIds } from '../../../constants.views'; +import { pickApplicablePromo } from '../../../plus/gk/utils/promo.utils'; import type { Deferrable } from '../../../system/function'; import { debounce } from '../../../system/function'; import type { WebviewFocusChangedParams } from '../../protocol'; -import { DidChangeWebviewFocusNotification, WebviewFocusChangedCommand, WebviewReadyCommand } from '../../protocol'; +import { + DidChangeWebviewFocusNotification, + DidPromoInitialized, + WebviewFocusChangedCommand, + WebviewReadyCommand, +} from '../../protocol'; import { GlElement } from './components/element'; -import { ipcContext, LoggerContext, loggerContext, telemetryContext, TelemetryContext } from './context'; +import { ipcContext, LoggerContext, loggerContext, promoContext, telemetryContext, TelemetryContext } from './context'; import type { Disposable } from './events'; import { HostIpc } from './ipc'; @@ -36,6 +42,10 @@ export abstract class GlApp< @provide({ context: loggerContext }) protected _logger!: LoggerContext; + @provide({ context: promoContext }) + @state() + protected _getApplicablePromo: typeof promoContext.__context__ = () => undefined; + @provide({ context: telemetryContext }) protected _telemetry!: TelemetryContext; @@ -75,6 +85,9 @@ export abstract class GlApp< case DidChangeWebviewFocusNotification.is(msg): window.dispatchEvent(new CustomEvent(msg.params.focused ? 'webview-focus' : 'webview-blur')); break; + case DidPromoInitialized.is(msg): + this._getApplicablePromo = pickApplicablePromo.bind(null, msg.params.promo); + break; } }), this._ipc, diff --git a/src/webviews/apps/shared/components/feature-badge.ts b/src/webviews/apps/shared/components/feature-badge.ts index 49872f29b344b..877c418c4b904 100644 --- a/src/webviews/apps/shared/components/feature-badge.ts +++ b/src/webviews/apps/shared/components/feature-badge.ts @@ -1,3 +1,4 @@ +import { consume } from '@lit/context'; import type { TemplateResult } from 'lit'; import { css, html, LitElement, nothing, unsafeCSS } from 'lit'; import { customElement, property } from 'lit/decorators.js'; @@ -7,7 +8,6 @@ import { proTrialLengthInDays, SubscriptionPlanId, SubscriptionState } from '../ import type { Source } from '../../../../constants.telemetry'; import type { Promo } from '../../../../plus/gk/models/promo'; import type { Subscription } from '../../../../plus/gk/models/subscription'; -import { getApplicablePromo } from '../../../../plus/gk/utils/promo.utils'; import { getSubscriptionPlanName, getSubscriptionTimeRemaining, @@ -15,12 +15,13 @@ import { isSubscriptionStateTrial, } from '../../../../plus/gk/utils/subscription.utils'; import { pluralize } from '../../../../system/string'; -import type { GlPopover } from './overlays/popover'; -import { focusOutline } from './styles/lit/a11y.css'; -import { elementBase, linkBase } from './styles/lit/base.css'; +import { promoContext } from '../context'; import './overlays/popover'; +import type { GlPopover } from './overlays/popover'; import './overlays/tooltip'; import './promo'; +import { focusOutline } from './styles/lit/a11y.css'; +import { elementBase, linkBase } from './styles/lit/base.css'; declare global { interface HTMLElementTagNameMap { @@ -329,8 +330,11 @@ export class GlFeatureBadge extends LitElement { `; } + @consume({ context: promoContext, subscribe: true }) + getApplicablePromo!: typeof promoContext.__context__; + private renderUpgradeActions(leadin?: TemplateResult) { - const promo = getApplicablePromo(this.state, 'badge'); + const promo = this.getApplicablePromo(this.state, 'badge'); return html`
${leadin ?? nothing} diff --git a/src/webviews/apps/shared/components/promo.ts b/src/webviews/apps/shared/components/promo.ts index c87f3bbcfb67f..1a6a6375bfcb6 100644 --- a/src/webviews/apps/shared/components/promo.ts +++ b/src/webviews/apps/shared/components/promo.ts @@ -83,7 +83,8 @@ export class GlPromo extends LitElement { return html`Save 55% or more on your 1st seat of Pro`; - + case 'gkholiday': + return nothing; default: { debugger; typeCheck(promo.key); diff --git a/src/webviews/apps/shared/context.ts b/src/webviews/apps/shared/context.ts index 411c63a80f2e4..0f84fb6a9e897 100644 --- a/src/webviews/apps/shared/context.ts +++ b/src/webviews/apps/shared/context.ts @@ -1,4 +1,7 @@ import { createContext } from '@lit/context'; +import type { PromoKeys } from '../../../constants.promos'; +import type { SubscriptionState } from '../../../constants.subscription'; +import type { Promo, PromoLocation } from '../../../plus/gk/account/promos'; import { Logger } from '../../../system/logger'; import type { LogScope } from '../../../system/logger.scope'; import { getNewLogScope } from '../../../system/logger.scope'; @@ -61,3 +64,11 @@ export class TelemetryContext implements Disposable { export const ipcContext = createContext('ipc'); export const loggerContext = createContext('logger'); export const telemetryContext = createContext('telemetry'); +export const promoContext = + createContext< + ( + subscriptionState: SubscriptionState | undefined, + location?: PromoLocation, + key?: PromoKeys, + ) => Promo | undefined + >('promo'); diff --git a/src/webviews/protocol.ts b/src/webviews/protocol.ts index a587ab2c51914..43f3b888ada4d 100644 --- a/src/webviews/protocol.ts +++ b/src/webviews/protocol.ts @@ -10,6 +10,7 @@ import type { WebviewViewIds, WebviewViewTypes, } from '../constants.views'; +import type { Promo } from '../plus/gk/models/promo'; import type { ConfigPath, ConfigPathValue, Path, PathValue } from '../system/-webview/configuration'; export type IpcScope = 'core' | CustomEditorTypes | WebviewTypes | WebviewViewTypes; @@ -72,6 +73,7 @@ export class IpcNotification extends IpcCall {} // COMMANDS export const WebviewReadyCommand = new IpcCommand('core', 'webview/ready'); +export const ReadPromoCommand = new IpcCommand('core', 'webview/promo/read'); export interface WebviewFocusChangedParams { focused: boolean; @@ -143,6 +145,8 @@ export const DidChangeWebviewFocusNotification = new IpcCommand('core', 'webview/promo/initialized'); + export interface DidChangeConfigurationParams { config: Config; customSettings: Record; diff --git a/src/webviews/webviewController.ts b/src/webviews/webviewController.ts index efc1e366bf042..237e89748e206 100644 --- a/src/webviews/webviewController.ts +++ b/src/webviews/webviewController.ts @@ -5,6 +5,8 @@ import type { Commands } from '../constants.commands'; import type { WebviewTelemetryContext } from '../constants.telemetry'; import type { CustomEditorTypes, WebviewIds, WebviewTypes, WebviewViewIds, WebviewViewTypes } from '../constants.views'; import type { Container } from '../container'; +import type { PromoProvider } from '../plus/gk/account/promos'; +import { promoProvider } from '../plus/gk/account/promos'; import { executeCommand, executeCoreCommand } from '../system/-webview/command'; import { setContext } from '../system/-webview/context'; import { getScopedCounter } from '../system/counter'; @@ -29,6 +31,7 @@ import type { import { DidChangeHostWindowFocusNotification, DidChangeWebviewFocusNotification, + DidPromoInitialized, ExecuteCommand, ipcPromiseSettled, TelemetrySendEventCommand, @@ -129,6 +132,7 @@ export class WebviewController< ): Promise> { const controller = new WebviewController( container, + promoProvider, commandRegistrar, descriptor, instanceId, @@ -162,6 +166,8 @@ export class WebviewController< private constructor( private readonly container: Container, + private readonly promoProvider: PromoProvider, + private readonly _commandRegistrar: WebviewCommandRegistrar, private readonly descriptor: GetWebviewDescriptor, public readonly instanceId: string | undefined, @@ -448,6 +454,9 @@ export class WebviewController< this._ready = true; this.sendPendingIpcNotifications(); this.provider.onReady?.(); + void this.promoProvider.getPromoList().then(promo => { + void this.notify(DidPromoInitialized, { promo: promo }); + }); break; diff --git a/webpack.config.mjs b/webpack.config.mjs index 358d3a9e6bfaf..2c9a63e75e951 100644 --- a/webpack.config.mjs +++ b/webpack.config.mjs @@ -84,6 +84,7 @@ function getExtensionConfig(target, mode, env) { new CleanPlugin({ cleanOnceBeforeBuildPatterns: ['!dist/webviews/**'] }), new DefinePlugin({ DEBUG: mode === 'development', + GL_PROMO_URI: `"${process.env['GL_PROMO_URI']}"`, }), new ForkTsCheckerPlugin({ async: false, @@ -322,6 +323,7 @@ function getWebviewsConfig(mode, env) { ), new DefinePlugin({ DEBUG: mode === 'development', + GL_PROMO_URI: `"${process.env['GL_PROMO_URI']}"`, }), new ForkTsCheckerPlugin({ async: false,