@@ -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