@@ -8,18 +8,26 @@ import { getLoggableName, Logger } from '../../system/logger';
88import { startLogScope } from '../../system/logger.scope' ;
99import type { Validator } from '../../system/validation' ;
1010import { createValidator , Is } from '../../system/validation' ;
11- import type { Promo , PromoLocation } from './models/promo' ;
11+ import type { Promo , PromoLocation , PromoPlans } from './models/promo' ;
1212import type { ServerConnection } from './serverConnection' ;
1313
1414type Config = {
1515 promos : Promo [ ] ;
1616} ;
1717
1818type ConfigJson = {
19- v : number ;
20- promos : PromoJson [ ] ;
19+ /** @deprecated this doesn't provide value, but we need to keep it for old clients */
20+ v ?: number ;
21+ promos ?: PromoJson [ ] ;
22+ promosV2 ?: PromoV2Json [ ] ;
2123} ;
22- type PromoJson = Replace < Promo , 'expiresOn' | 'startsOn' , string | undefined > ;
24+ type PromoJson = Replace < Promo , 'plan' | 'expiresOn' | 'startsOn' , string | undefined > & {
25+ v ?: number ;
26+ plan ?: PromoPlans ;
27+ } ;
28+ type PromoV2Json = Replace < Promo , 'expiresOn' | 'startsOn' , string | undefined > & { v : number | undefined } ;
29+
30+ const maxKnownPromoVersion = 2 ;
2331
2432export class ProductConfigProvider {
2533 private readonly _lazyConfig : Lazy < Promise < Config > > ;
@@ -35,32 +43,25 @@ export class ProductConfigProvider {
3543 statusCode : undefined as number | undefined ,
3644 } ;
3745
46+ if ( DEBUG ) {
47+ try {
48+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- using @ts-ignore instead of @ts-expect-error because if `product.json` is found then @ts-expect-error will complain because its not an error anymore
49+ // @ts -ignore
50+ const data = ( await import ( '../../../product.json' , { with : { type : 'json' } } ) ) . default ;
51+ const config = getConfig ( data ) ;
52+ if ( config != null ) return config ;
53+
54+ debugger ;
55+ } catch { }
56+ }
57+
3858 try {
3959 const rsp = await connection . fetchGkConfig ( 'product.json' ) ;
4060 if ( rsp . ok ) {
4161 data = await rsp . json ( ) ;
4262
43- const validator = createConfigValidator ( ) ;
44- if ( validator ( data ) ) {
45- const promos = data . promos . map (
46- d =>
47- ( {
48- key : d . key ,
49- code : d . code ,
50- states : d . states ,
51- expiresOn : d . expiresOn == null ? undefined : new Date ( d . expiresOn ) . getTime ( ) ,
52- startsOn : d . startsOn == null ? undefined : new Date ( d . startsOn ) . getTime ( ) ,
53- locations : d . locations ,
54- content : d . content ,
55- percentile : d . percentile ,
56- } ) satisfies Promo ,
57- ) ;
58-
59- const config : Config = { promos : promos } ;
60- await container . storage . store ( 'product:config' , { data : config , v : 1 , timestamp : Date . now ( ) } ) ;
61-
62- return config ;
63- }
63+ const config = getConfig ( data ) ;
64+ if ( config != null ) return config ;
6465
6566 failed . validation = true ;
6667 } else {
@@ -80,13 +81,19 @@ export class ProductConfigProvider {
8081 } ) ;
8182
8283 const stored = container . storage . get ( 'product:config' ) ;
83- if ( stored ?. data != null ) return stored . data ;
84+ if ( stored ?. data != null ) {
85+ return {
86+ ...stored . data ,
87+ promos : stored . data . promos . map ( p => ( { ...p , plan : p . plan ?? 'pro' } ) satisfies Promo ) ,
88+ } satisfies Config ;
89+ }
8490
8591 // If all else fails, return a default set of promos
8692 return {
8793 promos : [
8894 {
8995 key : 'pro50' ,
96+ plan : 'pro' ,
9097 states : [
9198 SubscriptionState . Community ,
9299 SubscriptionState . Trial ,
@@ -95,13 +102,10 @@ export class ProductConfigProvider {
95102 ] ,
96103 locations : [ 'home' , 'account' , 'badge' , 'gate' ] ,
97104 content : {
98- quickpick : {
99- detail : '$(star-full) Save 50% on GitLens Pro' ,
100- } ,
105+ modal : { detail : 'Save 50% on GitLens Pro' } ,
106+ quickpick : { detail : '$(star-full) Save 50% on GitLens Pro' } ,
101107 webview : {
102- info : {
103- html : '<b>Save 50%</b> on GitLens Pro' ,
104- } ,
108+ info : { html : '<b>Save 50%</b> on GitLens Pro' } ,
105109 link : {
106110 html : '<b>Save 50%</b> on GitLens Pro' ,
107111 title : 'Upgrade now and Save 50% on GitLens Pro' ,
@@ -114,11 +118,15 @@ export class ProductConfigProvider {
114118 } ) ;
115119 }
116120
117- async getApplicablePromo ( state : number | undefined , location ?: PromoLocation ) : Promise < Promo | undefined > {
121+ async getApplicablePromo (
122+ state : SubscriptionState | undefined ,
123+ plan : PromoPlans ,
124+ location ?: PromoLocation ,
125+ ) : Promise < Promo | undefined > {
118126 if ( state == null ) return undefined ;
119127
120128 const promos = await this . getPromos ( ) ;
121- return getApplicablePromo ( promos , state , location ) ;
129+ return getApplicablePromo ( promos , state , plan , location ) ;
122130 }
123131
124132 private getConfig ( ) : Promise < Config > {
@@ -145,6 +153,10 @@ function createConfigValidator(): Validator<ConfigJson> {
145153 SubscriptionState . Paid ,
146154 ) ;
147155
156+ const isModal = createValidator ( {
157+ detail : Is . String ,
158+ } ) ;
159+
148160 const isQuickPick = createValidator ( {
149161 detail : Is . String ,
150162 } ) ;
@@ -168,11 +180,14 @@ function createConfigValidator(): Validator<ConfigJson> {
168180 } ) ;
169181
170182 const isContent = createValidator ( {
183+ modal : isModal ,
171184 quickpick : isQuickPick ,
172185 webview : Is . Optional ( isWebview ) ,
173186 } ) ;
174187
175188 const promoValidator = createValidator < PromoJson > ( {
189+ v : Is . Optional ( Is . Number ) ,
190+ plan : Is . Optional ( Is . Enum < PromoPlans > ( 'pro' , 'advanced' , 'teams' , 'enterprise' ) ) ,
176191 key : Is . String ,
177192 code : Is . Optional ( Is . String ) ,
178193 states : Is . Optional ( Is . Array ( isState ) ) ,
@@ -183,17 +198,36 @@ function createConfigValidator(): Validator<ConfigJson> {
183198 percentile : Is . Optional ( Is . Number ) ,
184199 } ) ;
185200
186- return createValidator < ConfigJson > ( {
201+ const promoV2Validator = createValidator < PromoV2Json > ( {
187202 v : Is . Number ,
188- promos : Is . Array ( promoValidator ) ,
203+ key : Is . String ,
204+ code : Is . Optional ( Is . String ) ,
205+ plan : Is . Enum < PromoPlans > ( 'pro' , 'advanced' , 'teams' , 'enterprise' ) ,
206+ states : Is . Optional ( Is . Array ( isState ) ) ,
207+ expiresOn : Is . Optional ( Is . String ) ,
208+ startsOn : Is . Optional ( Is . String ) ,
209+ locations : Is . Optional ( Is . Array ( isLocation ) ) ,
210+ content : Is . Optional ( isContent ) ,
211+ percentile : Is . Optional ( Is . Number ) ,
212+ } ) ;
213+
214+ return createValidator < ConfigJson > ( {
215+ v : Is . Optional ( Is . Number ) ,
216+ promos : Is . Optional ( Is . Array ( promoValidator ) ) ,
217+ promosV2 : Is . Optional ( Is . Array ( promoV2Validator ) ) ,
189218 } ) ;
190219}
191220
192- function getApplicablePromo ( promos : Promo [ ] , state : number | undefined , location ?: PromoLocation ) : Promo | undefined {
221+ function getApplicablePromo (
222+ promos : Promo [ ] ,
223+ state : SubscriptionState | undefined ,
224+ plan : PromoPlans ,
225+ location ?: PromoLocation ,
226+ ) : Promo | undefined {
193227 if ( state == null ) return undefined ;
194228
195229 for ( const promo of promos ) {
196- if ( isPromoApplicable ( promo , state ) ) {
230+ if ( isPromoApplicable ( promo , state , plan ) ) {
197231 if ( location == null || promo . locations == null || promo . locations . includes ( location ) ) {
198232 return promo ;
199233 }
@@ -204,10 +238,37 @@ function getApplicablePromo(promos: Promo[], state: number | undefined, location
204238 return undefined ;
205239}
206240
207- function isPromoApplicable ( promo : Promo , state : number ) : boolean {
241+ function getConfig ( data : unknown ) : Config | undefined {
242+ const validator = createConfigValidator ( ) ;
243+ if ( ! validator ( data ) ) return undefined ;
244+
245+ const promos = ( data . promosV2 ?? data . promos ?? [ ] )
246+ // Filter out promos that we don't know how to handle
247+ . filter ( d => d . v == null || d . v <= maxKnownPromoVersion )
248+ . map (
249+ d =>
250+ ( {
251+ key : d . key ,
252+ code : d . code ,
253+ plan : d . plan ?? 'pro' ,
254+ states : d . states ,
255+ expiresOn : d . expiresOn == null ? undefined : new Date ( d . expiresOn ) . getTime ( ) ,
256+ startsOn : d . startsOn == null ? undefined : new Date ( d . startsOn ) . getTime ( ) ,
257+ locations : d . locations ,
258+ content : d . content ,
259+ percentile : d . percentile ,
260+ } ) satisfies Promo ,
261+ ) ;
262+
263+ const config : Config = { promos : promos } ;
264+ return config ;
265+ }
266+
267+ function isPromoApplicable ( promo : Promo , state : SubscriptionState , plan : PromoPlans ) : boolean {
208268 const now = Date . now ( ) ;
209269
210270 return (
271+ ( promo . plan == null || promo . plan === plan ) &&
211272 ( promo . states == null || promo . states . includes ( state ) ) &&
212273 ( promo . expiresOn == null || promo . expiresOn > now ) &&
213274 ( promo . startsOn == null || promo . startsOn < now ) &&
0 commit comments