Skip to content

Commit 1fa3602

Browse files
committed
Improves new upgrade flow UI/UX
- Makes plan parameter explicit in upgrade calls - Consolidates plan comparison logic - Improves account chip UI for upgrade options - Adds plan parameter to subscription upgrade commands
1 parent 21c0f52 commit 1fa3602

File tree

15 files changed

+152
-111
lines changed

15 files changed

+152
-111
lines changed

docs/telemetry-events.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1484,7 +1484,7 @@ void
14841484
'repoPrivacy': 'private' | 'public' | 'local',
14851485
'repository.visibility': 'private' | 'public' | 'local',
14861486
// Provided for compatibility with other GK surfaces
1487-
'source': 'account' | 'subscription' | 'graph' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'code-suggest' | 'ai' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'commandPalette' | 'deeplink' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'remoteProvider' | 'startWork' | 'trial-indicator' | 'scm-input' | 'walkthrough' | 'whatsnew' | 'worktrees'
1487+
'source': 'account' | 'subscription' | 'graph' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'code-suggest' | 'ai' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'commandPalette' | 'deeplink' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'remoteProvider' | 'startWork' | 'trial-indicator' | 'scm-input' | 'walkthrough' | 'whatsnew' | 'worktrees'
14881488
}
14891489
```
14901490

src/commands/quickWizard.base.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Disposable, InputBox, QuickInputButton, QuickPick, QuickPickItem } from 'vscode';
22
import { InputBoxValidationSeverity, QuickInputButtons, window } from 'vscode';
33
import type { GlCommands } from '../constants.commands';
4+
import { SubscriptionPlanId } from '../constants.subscription';
45
import { Container } from '../container';
56
import { Directive, isDirective, isDirectiveQuickPickItem } from '../quickpicks/items/directive';
67
import { configuration } from '../system/-webview/configuration';
@@ -746,7 +747,7 @@ export abstract class QuickWizardCommandBase extends GlCommandBase {
746747
}
747748

748749
case Directive.RequiresPaidSubscription:
749-
void Container.instance.subscription.upgrade({
750+
void Container.instance.subscription.upgrade(SubscriptionPlanId.Pro, {
750751
source: 'quick-wizard',
751752
detail: {
752753
action: rootStep.command?.key,

src/commands/walkthroughs.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { urls } from '../constants';
33
import type { GlCommands } from '../constants.commands';
44
import type { Source, Sources } from '../constants.telemetry';
55
import type { Container } from '../container';
6+
import type { SubscriptionUpgradeCommandArgs } from '../plus/gk/models/subscription';
67
import type { LaunchpadCommandArgs } from '../plus/launchpad/launchpad';
78
import { command, executeCommand } from '../system/-webview/command';
89
import { openUrl, openWalkthrough as openWalkthroughCore } from '../system/-webview/vscode';
@@ -80,7 +81,7 @@ export class WalkthroughPlusUpgradeCommand extends GlCommandBase {
8081
name: 'plus/upgrade',
8182
command: command,
8283
});
83-
executeCommand<Source>(command, { source: 'walkthrough' });
84+
executeCommand<SubscriptionUpgradeCommandArgs>(command, { source: 'walkthrough' });
8485
}
8586
}
8687

src/constants.telemetry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,8 @@ export type Sources =
934934
| 'cloud-patches'
935935
| 'commandPalette'
936936
| 'deeplink'
937+
| 'feature-badge'
938+
| 'feature-gate'
937939
| 'graph'
938940
| 'home'
939941
| 'inspect'

src/plus/gk/models/subscription.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { SubscriptionPlanId, SubscriptionState } from '../../../constants.subscription';
2+
import type { Source } from '../../../constants.telemetry';
23
import type { Organization } from './organization';
34

45
export type FreeSubscriptionPlans = Extract<
@@ -50,3 +51,7 @@ export interface SubscriptionPreviewTrial {
5051
readonly startedOn: string;
5152
readonly expiresOn: string;
5253
}
54+
55+
export interface SubscriptionUpgradeCommandArgs extends Source {
56+
plan?: SubscriptionPlanId;
57+
}

src/plus/gk/subscriptionService.ts

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -71,19 +71,20 @@ import { authenticationProviderScopes } from './authenticationProvider';
7171
import type { GKCheckInResponse } from './models/checkin';
7272
import type { Organization } from './models/organization';
7373
import type { Promo } from './models/promo';
74-
import type { Subscription } from './models/subscription';
74+
import type { Subscription, SubscriptionUpgradeCommandArgs } from './models/subscription';
7575
import type { ServerConnection } from './serverConnection';
7676
import { ensurePlusFeaturesEnabled } from './utils/-webview/plus.utils';
7777
import { getConfiguredActiveOrganizationId, updateActiveOrganizationId } from './utils/-webview/subscription.utils';
7878
import { getSubscriptionFromCheckIn } from './utils/checkin.utils';
7979
import {
8080
assertSubscriptionState,
81+
compareSubscriptionPlans,
8182
computeSubscriptionState,
8283
getCommunitySubscription,
8384
getPreviewSubscription,
8485
getSubscriptionPlan,
8586
getSubscriptionPlanName,
86-
getSubscriptionPlanPriority,
87+
getSubscriptionPlanTierType,
8788
getSubscriptionStateString,
8889
getSubscriptionTimeRemaining,
8990
getTimeRemaining,
@@ -357,11 +358,8 @@ export class SubscriptionService implements Disposable {
357358
registerCommand('gitlens.plus.startPreviewTrial', (src?: Source) => this.startPreviewTrial(src)),
358359
registerCommand('gitlens.plus.reactivateProTrial', (src?: Source) => this.reactivateProTrial(src)),
359360
registerCommand('gitlens.plus.resendVerification', (src?: Source) => this.resendVerification(src)),
360-
registerCommand('gitlens.plus.upgrade', (srcAndPlan?: Source & { plan?: SubscriptionPlanId }) =>
361-
this.upgrade(
362-
srcAndPlan ? { source: srcAndPlan.source, detail: srcAndPlan.detail } : undefined,
363-
srcAndPlan?.plan,
364-
),
361+
registerCommand('gitlens.plus.upgrade', (args?: SubscriptionUpgradeCommandArgs) =>
362+
this.upgrade(args?.plan, args ? { source: args.source, detail: args.detail } : undefined),
365363
),
366364

367365
registerCommand('gitlens.plus.hide', (src?: Source) => this.setProFeaturesVisibility(false, src)),
@@ -548,7 +546,7 @@ export class SubscriptionService implements Disposable {
548546
);
549547

550548
if (result === upgrade) {
551-
void this.upgrade(source);
549+
void this.upgrade(SubscriptionPlanId.Pro, source);
552550
} else if (result === learn) {
553551
void this.learnAboutPro({ source: 'prompt', detail: { action: 'trial-ended' } }, source);
554552
}
@@ -657,9 +655,10 @@ export class SubscriptionService implements Disposable {
657655
@gate(() => '')
658656
@log()
659657
async reactivateProTrial(source: Source | undefined): Promise<void> {
660-
if (!(await ensurePlusFeaturesEnabled())) return;
661658
const scope = getLogScope();
662659

660+
if (!(await ensurePlusFeaturesEnabled())) return;
661+
663662
if (this.container.telemetry.enabled) {
664663
this.container.telemetry.sendEvent('subscription/action', { action: 'reactivate' }, source);
665664
}
@@ -892,11 +891,13 @@ export class SubscriptionService implements Disposable {
892891
}
893892

894893
@log()
895-
async upgrade(source: Source | undefined, plan?: SubscriptionPlanId): Promise<boolean> {
894+
async upgrade(plan: SubscriptionPlanId | undefined, source: Source | undefined): Promise<boolean> {
896895
const scope = getLogScope();
897896

898897
if (!(await ensurePlusFeaturesEnabled())) return false;
899898

899+
plan ??= SubscriptionPlanId.Pro;
900+
900901
let aborted = false;
901902
const promo = await this.container.productConfig.getApplicablePromo(this._subscription.state);
902903

@@ -920,14 +921,13 @@ export class SubscriptionService implements Disposable {
920921

921922
const hasAccount = this._subscription.account != null;
922923
if (hasAccount) {
923-
// Do a pre-check-in to see if we've already upgraded to a paid plan.
924+
// Do a pre-check-in to see if we've already upgraded to a paid plan
924925
try {
925926
const session = await this.ensureSession(false, source);
926927
if (session != null) {
927928
if (
928929
(await this.checkUpdatedSubscription(source)) === SubscriptionState.Paid &&
929-
getSubscriptionPlanPriority(this._subscription.plan.effective.id) >=
930-
getSubscriptionPlanPriority(plan ?? SubscriptionPlanId.Pro)
930+
compareSubscriptionPlans(this._subscription.plan.effective.id, plan) >= 0
931931
) {
932932
return true;
933933
}
@@ -938,20 +938,7 @@ export class SubscriptionService implements Disposable {
938938
const query = new URLSearchParams();
939939
query.set('source', 'gitlens');
940940
query.set('product', 'gitlens');
941-
942-
let planType = 'PRO';
943-
switch (plan) {
944-
case SubscriptionPlanId.Advanced:
945-
planType = 'ADVANCED';
946-
break;
947-
case SubscriptionPlanId.Teams:
948-
planType = 'TEAMS';
949-
break;
950-
case SubscriptionPlanId.Enterprise:
951-
planType = 'ENTERPRISE';
952-
break;
953-
}
954-
query.set('planType', planType);
941+
query.set('planType', getSubscriptionPlanTierType(plan));
955942

956943
if (promo?.code != null) {
957944
query.set('promoCode', promo.code);

src/plus/gk/utils/-webview/acount.utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Uri } from 'vscode';
22
import { window } from 'vscode';
3+
import { SubscriptionPlanId } from '../../../../constants.subscription';
34
import type { Source } from '../../../../constants.telemetry';
45
import type { Container } from '../../../../container';
56
import type { PlusFeatures } from '../../../../features';
@@ -142,7 +143,7 @@ export async function ensureFeatureAccess(
142143
);
143144

144145
if (result === upgrade) {
145-
if (await container.subscription.upgrade(source)) {
146+
if (await container.subscription.upgrade(SubscriptionPlanId.Pro, source)) {
146147
continue;
147148
}
148149
}

src/plus/gk/utils/-webview/plus.utils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { MessageItem } from 'vscode';
22
import { window } from 'vscode';
3-
import { proTrialLengthInDays } from '../../../../constants.subscription';
3+
import { proTrialLengthInDays, SubscriptionPlanId } from '../../../../constants.subscription';
44
import type { Source } from '../../../../constants.telemetry';
55
import type { Container } from '../../../../container';
66
import { configuration } from '../../../../system/-webview/configuration';
@@ -28,6 +28,7 @@ export async function ensurePlusFeaturesEnabled(): Promise<boolean> {
2828
await configuration.updateEffective('plusFeatures.enabled', true);
2929
return true;
3030
}
31+
3132
export async function ensurePaidPlan(
3233
container: Container,
3334
title: string,
@@ -100,7 +101,7 @@ export async function ensurePaidPlan(
100101
);
101102

102103
if (result === upgrade) {
103-
void container.subscription.upgrade(source);
104+
void container.subscription.upgrade(SubscriptionPlanId.Pro, source);
104105
}
105106
}
106107

src/plus/gk/utils/checkin.utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { SubscriptionPlanId } from '../../../constants.subscription';
22
import type { GKCheckInResponse, GKLicense, GKLicenseType } from '../models/checkin';
33
import type { Organization } from '../models/organization';
44
import type { Subscription } from '../models/subscription';
5-
import { getSubscriptionPlan, getSubscriptionPlanPriority } from './subscription.utils';
5+
import { compareSubscriptionPlans, getSubscriptionPlan, getSubscriptionPlanPriority } from './subscription.utils';
66

77
export function getSubscriptionFromCheckIn(
88
data: GKCheckInResponse,
@@ -129,7 +129,7 @@ export function getSubscriptionFromCheckIn(
129129
);
130130
}
131131

132-
if (effective == null || getSubscriptionPlanPriority(actual.id) >= getSubscriptionPlanPriority(effective.id)) {
132+
if (effective == null || compareSubscriptionPlans(actual.id, effective.id) >= 0) {
133133
effective = { ...actual };
134134
}
135135

src/plus/gk/utils/subscription.utils.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ import type { PaidSubscriptionPlans, Subscription, SubscriptionPlan } from '../m
55

66
export const SubscriptionUpdatedUriPathPrefix = 'did-update-subscription';
77

8+
export function compareSubscriptionPlans(
9+
planA: SubscriptionPlanId | undefined,
10+
planB: SubscriptionPlanId | undefined,
11+
): number {
12+
return getSubscriptionPlanPriority(planA) - getSubscriptionPlanPriority(planB);
13+
}
14+
815
export function getSubscriptionStateName(
916
state: SubscriptionState,
1017
planId?: SubscriptionPlanId,
@@ -19,10 +26,9 @@ export function getSubscriptionStateName(
1926
case SubscriptionState.ProTrial:
2027
return `${getSubscriptionPlanName(SubscriptionPlanId.Pro)} Trial`;
2128
// return `${getSubscriptionPlanName(
22-
// effectivePlanId != null &&
23-
// getSubscriptionPlanPriority(effectivePlanId) >
24-
// getSubscriptionPlanPriority(planId ?? SubscriptionPlanId.Pro)
25-
// ? effectivePlanId
29+
// _effectivePlanId != null &&
30+
// compareSubscriptionPlans(_effectivePlanId, planId ?? SubscriptionPlanId.Pro) > 0
31+
// ? _effectivePlanId
2632
// : planId ?? SubscriptionPlanId.Pro,
2733
// )} Trial`;
2834
case SubscriptionState.ProTrialExpired:
@@ -68,10 +74,7 @@ export function computeSubscriptionState(subscription: Optional<Subscription, 's
6874

6975
if (account?.verified === false) return SubscriptionState.VerificationRequired;
7076

71-
if (
72-
actual.id === effective.id ||
73-
getSubscriptionPlanPriority(actual.id) > getSubscriptionPlanPriority(effective.id)
74-
) {
77+
if (actual.id === effective.id || compareSubscriptionPlans(actual.id, effective.id) > 0) {
7578
switch (actual.id === effective.id ? effective.id : actual.id) {
7679
case SubscriptionPlanId.Community:
7780
return preview == null ? SubscriptionState.Community : SubscriptionState.ProPreviewExpired;
@@ -93,7 +96,7 @@ export function computeSubscriptionState(subscription: Optional<Subscription, 's
9396
}
9497

9598
// If you have a paid license, any trial license higher tier than your paid license is considered paid
96-
if (getSubscriptionPlanPriority(actual.id) > getSubscriptionPlanPriority(SubscriptionPlanId.CommunityWithAccount)) {
99+
if (compareSubscriptionPlans(actual.id, SubscriptionPlanId.CommunityWithAccount) > 0) {
97100
return SubscriptionState.Paid;
98101
}
99102
switch (effective.id) {
@@ -161,6 +164,20 @@ export function getSubscriptionPlanTier(
161164
return 'Community';
162165
}
163166
}
167+
168+
export function getSubscriptionPlanTierType(id: SubscriptionPlanId): 'PRO' | 'ADVANCED' | 'TEAMS' | 'ENTERPRISE' {
169+
switch (id) {
170+
case SubscriptionPlanId.Advanced:
171+
return 'ADVANCED';
172+
case SubscriptionPlanId.Teams:
173+
return 'TEAMS';
174+
case SubscriptionPlanId.Enterprise:
175+
return 'ENTERPRISE';
176+
default:
177+
return 'PRO';
178+
}
179+
}
180+
164181
const plansPriority = new Map<SubscriptionPlanId | undefined, number>([
165182
[undefined, -1],
166183
[SubscriptionPlanId.Community, 0],

0 commit comments

Comments
 (0)