Skip to content

Commit e3795f8

Browse files
Adds debug commands and utilities to mock/restore subscription state (#3624)
* Adds debug commands and utilities to mock/restore subscription state * Removes redundant change * Removes debugging case for preview trial
1 parent c800ebc commit e3795f8

File tree

10 files changed

+352
-27
lines changed

10 files changed

+352
-27
lines changed

package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5661,6 +5661,16 @@
56615661
"title": "Refresh Repository Access",
56625662
"category": "GitLens"
56635663
},
5664+
{
5665+
"command": "gitlens.plus.simulateSubscriptionState",
5666+
"title": "Simulate Subscription State (Debugging)",
5667+
"category": "GitLens"
5668+
},
5669+
{
5670+
"command": "gitlens.plus.restoreSubscriptionState",
5671+
"title": "Restore Subscription State (Debugging)",
5672+
"category": "GitLens"
5673+
},
56645674
{
56655675
"command": "gitlens.gk.switchOrganization",
56665676
"title": "Switch Organization...",
@@ -9792,6 +9802,14 @@
97929802
"command": "gitlens.plus.refreshRepositoryAccess",
97939803
"when": "gitlens:enabled"
97949804
},
9805+
{
9806+
"command": "gitlens.plus.simulateSubscriptionState",
9807+
"when": "gitlens:enabled && gitlens:debugging"
9808+
},
9809+
{
9810+
"command": "gitlens.plus.restoreSubscriptionState",
9811+
"when": "gitlens:enabled && gitlens:debugging"
9812+
},
97959813
{
97969814
"command": "gitlens.gk.switchOrganization",
97979815
"when": "gitlens:gk:hasOrganizations"

src/@types/global.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export declare global {
2+
declare const DEBUG: boolean;
3+
24
export type PartialDeep<T> = T extends Record<string, unknown> ? { [K in keyof T]?: PartialDeep<T[K]> } : T;
35
export type Optional<T, K extends keyof T> = Omit<T, K> & { [P in K]?: T[P] };
46
export type PickPartialDeep<T, K extends keyof T> = Omit<Partial<T>, K> & { [P in K]?: Partial<T[P]> };

src/constants.commands.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ export const enum Commands {
151151
PlusStartPreviewTrial = 'gitlens.plus.startPreviewTrial',
152152
PlusUpgrade = 'gitlens.plus.upgrade',
153153
PlusValidate = 'gitlens.plus.validate',
154+
PlusSimulateSubscriptionState = 'gitlens.plus.simulateSubscriptionState',
155+
PlusRestoreSubscriptionState = 'gitlens.plus.restoreSubscriptionState',
154156
QuickOpenFileHistory = 'gitlens.quickOpenFileHistory',
155157
RefreshLaunchpad = 'gitlens.launchpad.refresh',
156158
RefreshGraph = 'gitlens.graph.refresh',

src/constants.storage.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,13 @@ export interface Stored<T, SchemaVersion extends number = 1> {
112112
timestamp?: number;
113113
}
114114

115+
export type StoredGKLicenses = Partial<Record<StoredGKLicenseType, StoredGKLicense>>;
116+
115117
export interface StoredGKCheckInResponse {
116118
user: StoredGKUser;
117119
licenses: {
118-
paidLicenses: Record<StoredGKLicenseType, StoredGKLicense>;
119-
effectiveLicenses: Record<StoredGKLicenseType, StoredGKLicense>;
120+
paidLicenses: StoredGKLicenses;
121+
effectiveLicenses: StoredGKLicenses;
120122
};
121123
}
122124

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import { window } from 'vscode';
2+
import { Commands } from '../../../constants.commands';
3+
import { SubscriptionPlanId, SubscriptionState } from '../../../constants.subscription';
4+
import type { Container } from '../../../container';
5+
import { registerCommand } from '../../../system/vscode/command';
6+
import { configuration } from '../../../system/vscode/configuration';
7+
import type { GKCheckInResponse, GKLicenses, GKLicenseType, GKUser } from '../checkin';
8+
import { getSubscriptionFromCheckIn } from '../checkin';
9+
import { getPreviewTrialAndDays } from '../utils';
10+
import { getSubscriptionPlan } from './subscription';
11+
import type { SubscriptionService } from './subscriptionService';
12+
13+
class AccountDebug {
14+
constructor(
15+
private readonly container: Container,
16+
private readonly subscriptionStub: {
17+
getSession: () => SubscriptionService['_session'];
18+
getSubscription: () => SubscriptionService['_subscription'];
19+
onDidCheckIn: SubscriptionService['_onDidCheckIn'];
20+
changeSubscription: SubscriptionService['changeSubscription'];
21+
getStoredSubscription: SubscriptionService['getStoredSubscription'];
22+
},
23+
) {
24+
this.container.context.subscriptions.push(
25+
registerCommand(Commands.PlusSimulateSubscriptionState, () => this.simulateSubscriptionState()),
26+
registerCommand(Commands.PlusRestoreSubscriptionState, () => this.restoreSubscriptionState()),
27+
);
28+
}
29+
30+
private async simulateSubscriptionState() {
31+
if (
32+
!this.container.debugging ||
33+
this.subscriptionStub.getSession() == null ||
34+
this.subscriptionStub.getSubscription() == null
35+
) {
36+
return;
37+
}
38+
39+
// Show a quickpick to select a subscription state to simulate
40+
const picks: { label: string; state: SubscriptionState; reactivatedTrial?: boolean; expiredPaid?: boolean }[] =
41+
[
42+
{ label: 'Free', state: SubscriptionState.Free },
43+
{ label: 'Free In Preview Trial', state: SubscriptionState.FreeInPreviewTrial },
44+
{ label: 'Free Preview Trial Expired', state: SubscriptionState.FreePreviewTrialExpired },
45+
{ label: 'Free+ In Trial', state: SubscriptionState.FreePlusInTrial },
46+
{
47+
label: 'Free+ In Trial (Reactivated)',
48+
state: SubscriptionState.FreePlusInTrial,
49+
reactivatedTrial: true,
50+
},
51+
{ label: 'Free+ Trial Expired', state: SubscriptionState.FreePlusTrialExpired },
52+
{
53+
label: 'Free+ Trial Reactivation Eligible',
54+
state: SubscriptionState.FreePlusTrialReactivationEligible,
55+
},
56+
{ label: 'Paid', state: SubscriptionState.Paid },
57+
// TODO: Update this subscription state once we have a "paid expired" state availale
58+
{ label: 'Paid Expired', state: SubscriptionState.Paid, expiredPaid: true },
59+
{ label: 'Verification Required', state: SubscriptionState.VerificationRequired },
60+
];
61+
62+
const pick = await window.showQuickPick(picks, {
63+
title: 'Simulate Subscription State',
64+
placeHolder: 'Select the subscription state to simulate',
65+
});
66+
if (pick == null) return;
67+
const { state: subscriptionState, reactivatedTrial, expiredPaid } = pick;
68+
69+
const organizations = (await this.container.organizations.getOrganizations()) ?? [];
70+
let activeOrganizationId = configuration.get('gitKraken.activeOrganizationId') ?? undefined;
71+
if (activeOrganizationId === '' || (activeOrganizationId == null && organizations.length === 1)) {
72+
activeOrganizationId = organizations[0].id;
73+
}
74+
75+
const simulatedCheckInData: GKCheckInResponse = getSimulatedCheckInResponse(
76+
{
77+
id: this.subscriptionStub.getSubscription()?.account?.id ?? '',
78+
name: '',
79+
email: '',
80+
status: subscriptionState === SubscriptionState.VerificationRequired ? 'pending' : 'activated',
81+
createdDate: new Date().toISOString(),
82+
},
83+
subscriptionState,
84+
'gitkraken_v1-pro',
85+
{
86+
organizationId: activeOrganizationId,
87+
trial: { reactivatedTrial: reactivatedTrial },
88+
expiredPaid: expiredPaid,
89+
},
90+
);
91+
this.subscriptionStub.onDidCheckIn.fire();
92+
let simulatedSubscription = getSubscriptionFromCheckIn(
93+
simulatedCheckInData,
94+
organizations,
95+
activeOrganizationId,
96+
);
97+
98+
if (
99+
subscriptionState === SubscriptionState.FreeInPreviewTrial ||
100+
subscriptionState === SubscriptionState.FreePreviewTrialExpired
101+
) {
102+
simulatedSubscription = {
103+
...simulatedSubscription,
104+
plan: {
105+
...simulatedSubscription.plan,
106+
actual: getSubscriptionPlan(
107+
SubscriptionPlanId.Free,
108+
false,
109+
0,
110+
undefined,
111+
new Date(simulatedSubscription.plan.actual.startedOn),
112+
),
113+
effective: getSubscriptionPlan(
114+
SubscriptionPlanId.Free,
115+
false,
116+
0,
117+
undefined,
118+
new Date(simulatedSubscription.plan.effective.startedOn),
119+
),
120+
},
121+
};
122+
const { previewTrial: simulatedPreviewTrial } = getPreviewTrialAndDays();
123+
if (subscriptionState === SubscriptionState.FreePreviewTrialExpired) {
124+
simulatedPreviewTrial.startedOn = new Date(Date.now() - 2000).toISOString();
125+
simulatedPreviewTrial.expiresOn = new Date(Date.now() - 1000).toISOString();
126+
}
127+
128+
simulatedSubscription.previewTrial = simulatedPreviewTrial;
129+
}
130+
131+
this.subscriptionStub.changeSubscription(
132+
{
133+
...this.subscriptionStub.getSubscription(),
134+
...simulatedSubscription,
135+
},
136+
{ store: false },
137+
);
138+
}
139+
140+
private restoreSubscriptionState() {
141+
if (!this.container.debugging || this.subscriptionStub.getSession() == null) return;
142+
this.subscriptionStub.changeSubscription(this.subscriptionStub.getStoredSubscription(), { store: false });
143+
}
144+
}
145+
146+
function getSimulatedPaidLicenseResponse(
147+
organizationId?: string | undefined,
148+
type: GKLicenseType = 'gitkraken_v1-pro',
149+
status: 'active' | 'cancelled' | 'non-renewing' = 'active',
150+
): GKLicenses {
151+
const oneYear = 365 * 24 * 60 * 60 * 1000;
152+
const tenSeconds = 10 * 1000;
153+
// start 10 seconds ago
154+
let start = new Date(Date.now() - tenSeconds);
155+
// end in 1 year
156+
let end = new Date(start.getTime() + oneYear);
157+
if (status === 'cancelled') {
158+
// set start and end back 1 year
159+
start = new Date(start.getTime() - oneYear);
160+
end = new Date(end.getTime() - oneYear);
161+
}
162+
163+
return {
164+
[type satisfies GKLicenseType]: {
165+
latestStatus: status,
166+
latestStartDate: start.toISOString(),
167+
latestEndDate: end.toISOString(),
168+
organizationId: organizationId,
169+
reactivationCount: undefined,
170+
nextOptInDate: undefined,
171+
},
172+
};
173+
}
174+
175+
function getSimulatedTrialLicenseResponse(
176+
organizationId?: string,
177+
type: GKLicenseType = 'gitkraken_v1-pro',
178+
status: 'active-new' | 'active-reactivated' | 'expired' | 'expired-reactivatable' = 'active-new',
179+
durationDays: number = 7,
180+
): GKLicenses {
181+
const tenSeconds = 10 * 1000;
182+
const oneDay = 24 * 60 * 60 * 1000;
183+
const duration = durationDays * oneDay;
184+
const tenSecondsAgo = new Date(Date.now() - tenSeconds);
185+
// start 10 seconds ago
186+
let start = tenSecondsAgo;
187+
// end using durationDays
188+
let end = new Date(start.getTime() + duration);
189+
if (status === 'expired' || status === 'expired-reactivatable') {
190+
// set start and end back durationDays
191+
start = new Date(start.getTime() - duration);
192+
end = new Date(end.getTime() - duration);
193+
}
194+
195+
return {
196+
[type satisfies GKLicenseType]: {
197+
latestStatus: status,
198+
latestStartDate: start.toISOString(),
199+
latestEndDate: end.toISOString(),
200+
organizationId: organizationId,
201+
reactivationCount: status === 'active-reactivated' ? 1 : 0,
202+
nextOptInDate: status === 'expired-reactivatable' ? tenSecondsAgo.toISOString() : undefined,
203+
},
204+
};
205+
}
206+
207+
function getSimulatedCheckInResponse(
208+
user: GKUser,
209+
targetSubscriptionState: SubscriptionState,
210+
targetSubscriptionType: GKLicenseType = 'gitkraken_v1-pro',
211+
// TODO: Remove 'expiredPaid' option and replace logic with targetSubscriptionState once we support a Paid Expired state
212+
options?: {
213+
organizationId?: string;
214+
trial?: { reactivatedTrial?: boolean; durationDays?: number };
215+
expiredPaid?: boolean;
216+
},
217+
): GKCheckInResponse {
218+
const tenSecondsAgo = new Date(Date.now() - 10 * 1000);
219+
const paidLicenseData =
220+
targetSubscriptionState === SubscriptionState.Paid
221+
? // TODO: Update this line once we support a Paid Expired state
222+
getSimulatedPaidLicenseResponse(
223+
options?.organizationId,
224+
targetSubscriptionType,
225+
options?.expiredPaid ? 'cancelled' : 'active',
226+
)
227+
: {};
228+
let trialLicenseStatus: 'active-new' | 'active-reactivated' | 'expired' | 'expired-reactivatable' = 'active-new';
229+
switch (targetSubscriptionState) {
230+
case SubscriptionState.FreePlusTrialExpired:
231+
trialLicenseStatus = 'expired';
232+
break;
233+
case SubscriptionState.FreePlusTrialReactivationEligible:
234+
trialLicenseStatus = 'expired-reactivatable';
235+
break;
236+
case SubscriptionState.FreePlusInTrial:
237+
trialLicenseStatus = options?.trial?.reactivatedTrial ? 'active-reactivated' : 'active-new';
238+
break;
239+
}
240+
const trialLicenseData =
241+
targetSubscriptionState === SubscriptionState.FreePlusInTrial ||
242+
targetSubscriptionState === SubscriptionState.FreePlusTrialExpired ||
243+
targetSubscriptionState === SubscriptionState.FreePlusTrialReactivationEligible
244+
? getSimulatedTrialLicenseResponse(
245+
options?.organizationId,
246+
targetSubscriptionType,
247+
trialLicenseStatus,
248+
options?.trial?.durationDays,
249+
)
250+
: {};
251+
return {
252+
user: user,
253+
licenses: {
254+
paidLicenses: paidLicenseData,
255+
effectiveLicenses: trialLicenseData,
256+
},
257+
nextOptInDate:
258+
targetSubscriptionState === SubscriptionState.FreePlusTrialReactivationEligible
259+
? tenSecondsAgo.toISOString()
260+
: undefined,
261+
};
262+
}
263+
264+
export function registerAccountDebug(
265+
container: Container,
266+
subscriptionStub: {
267+
getSession: () => SubscriptionService['_session'];
268+
getSubscription: () => SubscriptionService['_subscription'];
269+
onDidCheckIn: SubscriptionService['_onDidCheckIn'];
270+
changeSubscription: SubscriptionService['changeSubscription'];
271+
getStoredSubscription: SubscriptionService['getStoredSubscription'];
272+
},
273+
): void {
274+
if (!container.debugging) return;
275+
276+
new AccountDebug(container, subscriptionStub);
277+
}

0 commit comments

Comments
 (0)