Skip to content

Commit a05325e

Browse files
committed
add 3 day local feature trial
1 parent 3d14a2c commit a05325e

File tree

9 files changed

+142
-21
lines changed

9 files changed

+142
-21
lines changed

src/constants.storage.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { GraphBranchesVisibility, ViewShowBranchComparison } from './config';
22
import type { AIProviders } from './constants.ai';
33
import type { IntegrationId } from './constants.integrations';
4-
import type { TrackedUsage, TrackedUsageKeys } from './constants.telemetry';
4+
import type { Sources, TrackedUsage, TrackedUsageKeys } from './constants.telemetry';
55
import type { GroupableTreeViewTypes } from './constants.views';
66
import type { Environment } from './container';
77
import type { Subscription } from './plus/gk/account/subscription';
@@ -76,7 +76,9 @@ export type GlobalStorage = {
7676
'launchpad:indicator:hasInteracted': string;
7777
'launchpadView:groups:expanded': StoredLaunchpadGroup[];
7878
'graph:searchMode': StoredGraphSearchMode;
79-
} & { [key in `confirm:ai:tos:${AIProviders}`]: boolean } & {
79+
} & { [key in `plus:featurePreviewTrial:${Sources}:consumedDays`]: { startedOn: string; expiresOn: string }[] } & {
80+
[key in `confirm:ai:tos:${AIProviders}`]: boolean;
81+
} & {
8082
[key in `provider:authentication:skip:${string}`]: boolean;
8183
} & { [key in `gk:${string}:checkin`]: Stored<StoredGKCheckInResponse> } & {
8284
[key in `gk:${string}:organizations`]: Stored<StoredOrganization[]>;

src/constants.telemetry.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,8 @@ export type TelemetryEvents = {
379379
| 'resend-verification'
380380
| 'pricing'
381381
| 'start-preview-trial'
382-
| 'upgrade';
382+
| 'upgrade'
383+
| `start-${Sources}-preview-trial`;
383384
}
384385
| {
385386
action: 'visibility';

src/plus/gk/account/subscriptionService.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,6 @@ export class SubscriptionService implements Disposable {
242242
registerCommand(Commands.PlusManage, (src?: Source) => this.manage(src)),
243243
registerCommand(Commands.PlusShowPlans, (src?: Source) => this.showPlans(src)),
244244
registerCommand(Commands.PlusStartPreviewTrial, (src?: Source) => this.startPreviewTrial(src)),
245-
registerCommand(Commands.PlusStartFeaturePreviewTrial, (src?: Source) =>
246-
this.startFeaturePreviewTrial(src),
247-
),
248245
registerCommand(Commands.PlusReactivateProTrial, (src?: Source) => this.reactivateProTrial(src)),
249246
registerCommand(Commands.PlusResendVerification, (src?: Source) => this.resendVerification(src)),
250247
registerCommand(Commands.PlusUpgrade, (src?: Source) => this.upgrade(src)),
@@ -730,10 +727,6 @@ export class SubscriptionService implements Disposable {
730727
}, 1);
731728
}
732729

733-
@gate(() => '')
734-
@log()
735-
async startFeaturePreviewTrial(_: Source | undefined): Promise<void> {}
736-
737730
@log()
738731
async upgrade(source: Source | undefined): Promise<void> {
739732
const scope = getLogScope();

src/plus/webviews/graph/graphWebview.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {
2020
import { GlyphChars } from '../../../constants';
2121
import { Commands } from '../../../constants.commands';
2222
import type { StoredGraphFilters, StoredGraphRefType } from '../../../constants.storage';
23+
import { proPreviewLengthInDays, proTrialLengthInDays } from '../../../constants.subscription';
2324
import type { GraphShownTelemetryContext, GraphTelemetryContext, TelemetryEvents } from '../../../constants.telemetry';
2425
import type { Container } from '../../../container';
2526
import { CancellationError } from '../../../errors';
@@ -97,6 +98,7 @@ import { getSearchQueryComparisonKey, parseSearchQuery } from '../../../git/sear
9798
import { splitGitCommitMessage } from '../../../git/utils/commit-utils';
9899
import { ReferencesQuickPickIncludes, showReferencePicker } from '../../../quickpicks/referencePicker';
99100
import { showRepositoryPicker } from '../../../quickpicks/repositoryPicker';
101+
import { createFromDateDelta } from '../../../system/date';
100102
import { gate } from '../../../system/decorators/gate';
101103
import { debug, log } from '../../../system/decorators/log';
102104
import type { Deferrable } from '../../../system/function';
@@ -136,6 +138,7 @@ import type {
136138
DidGetCountParams,
137139
DidGetRowHoverParams,
138140
DidSearchParams,
141+
DidSetFeaturePreviewTrialParams,
139142
DoubleClickedParams,
140143
GetMissingAvatarsParams,
141144
GetMissingRefsMetadataParams,
@@ -206,6 +209,7 @@ import {
206209
DidChangeWorkingTreeNotification,
207210
DidFetchNotification,
208211
DidSearchNotification,
212+
DidSetFeaturePreviewTrialNotification,
209213
DoubleClickedCommandType,
210214
EnsureRowRequest,
211215
GetCountsRequest,
@@ -294,6 +298,7 @@ export class GraphWebviewProvider implements WebviewProvider<State, State, Graph
294298
[DidChangeSubscriptionNotification, this.notifyDidChangeSubscription],
295299
[DidChangeWorkingTreeNotification, this.notifyDidChangeWorkingTree],
296300
[DidFetchNotification, this.notifyDidFetch],
301+
[DidSetFeaturePreviewTrialNotification, this.notifyDidSetFeaturePreviewTrial],
297302
]);
298303
private _refsMetadata: Map<string, GraphRefMetadata | null> | null | undefined;
299304
private _search: GitSearch | undefined;
@@ -681,11 +686,58 @@ export class GraphWebviewProvider implements WebviewProvider<State, State, Graph
681686
this.copyWorkingChangesToWorktree,
682687
),
683688
this.host.registerWebviewCommand('gitlens.graph.generateCommitMessage', this.generateCommitMessage),
689+
this.host.registerWebviewCommand('gitlens.graph.startFeaturePreviewTrial', this.startFeaturePreviewTrial),
684690
);
685691

686692
return commands;
687693
}
688694

695+
async startFeaturePreviewTrial() {
696+
const timestamp = new Date();
697+
const consumedDays: { startedOn: string; expiresOn: string }[] = this.container.storage.get(
698+
`plus:featurePreviewTrial:graph:consumedDays`,
699+
[],
700+
);
701+
702+
// If it's still in the 24h trial, end here
703+
if (consumedDays.length > 0 && new Date(consumedDays[consumedDays.length - 1].expiresOn) > timestamp) {
704+
return;
705+
}
706+
707+
if (consumedDays.length >= proPreviewLengthInDays) {
708+
void window.showInformationMessage(
709+
`You have already used your ${proPreviewLengthInDays} days of previewing local Pro features.`,
710+
);
711+
return;
712+
}
713+
714+
await this.container.storage.store(`plus:featurePreviewTrial:graph:consumedDays`, [
715+
...(consumedDays ?? []),
716+
{
717+
startedOn: timestamp.toISOString(),
718+
expiresOn: createFromDateDelta(timestamp, { days: 1 }).toISOString(),
719+
},
720+
]);
721+
722+
if (this.container.telemetry.enabled) {
723+
this.container.telemetry.sendEvent(
724+
'subscription/action',
725+
{ action: `start-graph-preview-trial` },
726+
{ source: 'graph' },
727+
);
728+
}
729+
730+
void window.showInformationMessage(
731+
`You can now preview local Pro features for 1 day${
732+
consumedDays.length + 1 < proPreviewLengthInDays
733+
? `, up to ${proPreviewLengthInDays - (consumedDays.length + 1)} more days`
734+
: ''
735+
}, or [start your free ${proTrialLengthInDays}-day Pro trial](command:gitlens.plus.signUp "Start Pro Trial") for full access to Pro features.`,
736+
);
737+
738+
void this.notifyDidSetFeaturePreviewTrial();
739+
}
740+
689741
onWindowFocusChanged(focused: boolean): void {
690742
this.isWindowFocused = focused;
691743
}
@@ -1874,6 +1926,16 @@ export class GraphWebviewProvider implements WebviewProvider<State, State, Graph
18741926
});
18751927
}
18761928

1929+
@debug()
1930+
private async notifyDidSetFeaturePreviewTrial() {
1931+
if (!this.host.ready || !this.host.visible) {
1932+
this.host.addPendingIpcNotification(DidSetFeaturePreviewTrialNotification, this._ipcNotificationMap, this);
1933+
return false;
1934+
}
1935+
1936+
return this.host.notify(DidSetFeaturePreviewTrialNotification, this.getStoredGraphPreviewTrial());
1937+
}
1938+
18771939
@debug()
18781940
private async notifyDidChangeWorkingTree() {
18791941
if (!this.host.ready || !this.host.visible) {
@@ -2516,6 +2578,8 @@ export class GraphWebviewProvider implements WebviewProvider<State, State, Graph
25162578

25172579
const defaultSearchMode = this.container.storage.get('graph:searchMode') ?? 'normal';
25182580

2581+
const graphPreviewTrial = this.getStoredGraphPreviewTrial();
2582+
25192583
return {
25202584
...this.host.baseWebviewState,
25212585
windowFocused: this.isWindowFocused,
@@ -2563,6 +2627,7 @@ export class GraphWebviewProvider implements WebviewProvider<State, State, Graph
25632627
nonce: this.host.cspNonce,
25642628
workingTreeStats: getSettledValue(workingStatsResult) ?? { added: 0, deleted: 0, modified: 0 },
25652629
defaultSearchMode: defaultSearchMode,
2630+
graphPreviewTrial: graphPreviewTrial,
25662631
};
25672632
}
25682633

@@ -2837,6 +2902,18 @@ export class GraphWebviewProvider implements WebviewProvider<State, State, Graph
28372902
return refs;
28382903
}
28392904

2905+
private getStoredGraphPreviewTrial(): DidSetFeaturePreviewTrialParams {
2906+
const storedValue = this.container.storage.get(`plus:featurePreviewTrial:graph:consumedDays`, []);
2907+
return {
2908+
feature: 'graph',
2909+
consumedDays: storedValue,
2910+
isActive:
2911+
storedValue.length > 0 &&
2912+
storedValue.length <= proPreviewLengthInDays &&
2913+
new Date(storedValue[storedValue.length - 1].expiresOn) > new Date(),
2914+
};
2915+
}
2916+
28402917
private updateIncludeOnlyRefs(
28412918
repoPath: string | undefined,
28422919
{ branchesVisibility, refs }: UpdateIncludedRefsParams,

src/plus/webviews/graph/protocol.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type {
2525
import type { Config, DateStyle, GraphBranchesVisibility } from '../../../config';
2626
import type { SupportedCloudIntegrationIds } from '../../../constants.integrations';
2727
import type { SearchQuery } from '../../../constants.search';
28+
import type { Source, Sources } from '../../../constants.telemetry';
2829
import type { RepositoryVisibility } from '../../../git/gitProvider';
2930
import type { GitTrackingState } from '../../../git/models/branch';
3031
import type { GitGraphRowType } from '../../../git/models/graph';
@@ -129,6 +130,7 @@ export interface State extends WebviewState {
129130
bottom: number;
130131
};
131132
theming?: { cssVariables: CssVariables; themeOpacityFactor: number };
133+
graphPreviewTrial?: { consumedDays: { startedOn: string; expiresOn: string }[]; isActive: boolean };
132134
}
133135

134136
export interface BranchState extends GitTrackingState {
@@ -380,6 +382,16 @@ export interface DidSearchParams {
380382
export const SearchRequest = new IpcRequest<SearchParams, DidSearchParams>(scope, 'search');
381383

382384
// NOTIFICATIONS
385+
export interface DidSetFeaturePreviewTrialParams {
386+
feature: Sources;
387+
consumedDays: { startedOn: string; expiresOn: string }[];
388+
isActive: boolean;
389+
}
390+
391+
export const DidSetFeaturePreviewTrialNotification = new IpcNotification<DidSetFeaturePreviewTrialParams>(
392+
scope,
393+
'featurePreviewTrial/didSet',
394+
);
383395

384396
export interface DidChangeRepoConnectionParams {
385397
repositories?: GraphRepository[];

src/webviews/apps/plus/graph/GraphWrapper.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import {
6464
DidChangeWorkingTreeNotification,
6565
DidFetchNotification,
6666
DidSearchNotification,
67+
DidSetFeaturePreviewTrialNotification,
6768
} from '../../../../plus/webviews/graph/protocol';
6869
import { createCommandLink } from '../../../../system/commands';
6970
import { filterMap, first, groupByFilterMap, join } from '../../../../system/iterable';
@@ -282,6 +283,8 @@ export function GraphWrapper({
282283
const [windowFocused, setWindowFocused] = useState(state.windowFocused);
283284
const [allowed, setAllowed] = useState(state.allowed ?? false);
284285
const [subscription, setSubscription] = useState<Subscription | undefined>(state.subscription);
286+
const [graphPreviewTrial, setGraphPreviewTrial] = useState(state.graphPreviewTrial);
287+
285288
// search state
286289
const searchEl = useRef<GlSearchBox>(null);
287290
const [searchQuery, setSearchQuery] = useState<SearchQuery | undefined>(undefined);
@@ -318,6 +321,10 @@ export function GraphWrapper({
318321
setStyleProps(state.theming);
319322
}
320323
break;
324+
case DidSetFeaturePreviewTrialNotification:
325+
setGraphPreviewTrial(state.graphPreviewTrial);
326+
setAllowed(state.graphPreviewTrial?.isActive || allowed);
327+
break;
321328
case DidChangeAvatarsNotification:
322329
setAvatars(state.avatars);
323330
break;
@@ -1531,6 +1538,12 @@ export function GraphWrapper({
15311538
<GlFeatureGate
15321539
className="graph-app__gate"
15331540
allowFeaturePreviewTrial={true}
1541+
featureInPreviewTrial={graphPreviewTrial ? { graph: graphPreviewTrial } : undefined}
1542+
featurePreviewTrialCommandLink={createWebviewCommandLink(
1543+
'gitlens.graph.startFeaturePreviewTrial',
1544+
state.webviewId,
1545+
state.webviewInstanceId,
1546+
)}
15341547
appearance="alert"
15351548
featureWithArticleIfNeeded="the Commit Graph"
15361549
source={{ source: 'graph', detail: 'gate' }}

src/webviews/apps/plus/graph/graph.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
DidChangeWorkingTreeNotification,
3939
DidFetchNotification,
4040
DidSearchNotification,
41+
DidSetFeaturePreviewTrialNotification,
4142
DoubleClickedCommandType,
4243
EnsureRowRequest,
4344
GetMissingAvatarsCommand,
@@ -165,7 +166,11 @@ export class GraphApp extends App<State> {
165166
this.state.avatars = msg.params.avatars;
166167
this.setState(this.state, DidChangeAvatarsNotification);
167168
break;
168-
169+
case DidSetFeaturePreviewTrialNotification.is(msg):
170+
this.state.graphPreviewTrial = { consumedDays: msg.params.consumedDays, isActive: msg.params.isActive };
171+
this.state.allowed = msg.params.isActive || this.state.allowed;
172+
this.setState(this.state, DidSetFeaturePreviewTrialNotification);
173+
break;
169174
case DidChangeBranchStateNotification.is(msg):
170175
this.state.branchState = msg.params.branchState;
171176
this.setState(this.state, DidChangeBranchStateNotification);

src/webviews/apps/plus/shared/components/feature-gate-plus-state.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { css, html, LitElement, nothing } from 'lit';
22
import { customElement, property, query } from 'lit/decorators.js';
33
import { Commands } from '../../../../../constants.commands';
44
import { proPreviewLengthInDays, proTrialLengthInDays, SubscriptionState } from '../../../../../constants.subscription';
5-
import type { Source } from '../../../../../constants.telemetry';
5+
import type { Source, Sources } from '../../../../../constants.telemetry';
66
import type { Promo } from '../../../../../plus/gk/account/promos';
77
import { getApplicablePromo } from '../../../../../plus/gk/account/promos';
88
import { pluralize } from '../../../../../system/string';
@@ -72,6 +72,14 @@ export class GlFeatureGatePlusState extends LitElement {
7272
@property({ type: Boolean })
7373
allowFeaturePreviewTrial?: boolean;
7474

75+
@property({ type: Object })
76+
featureInPreviewTrial?: {
77+
[key in Sources]?: { consumedDays: { startedOn: string; expiresOn: string }[]; isActive: boolean };
78+
};
79+
80+
@property({ type: String })
81+
featurePreviewTrialCommandLink?: string;
82+
7583
@property({ type: String })
7684
appearance?: 'alert' | 'welcome';
7785

@@ -99,8 +107,10 @@ export class GlFeatureGatePlusState extends LitElement {
99107
this.hidden = false;
100108
const appearance = (this.appearance ?? 'alert') === 'alert' ? 'alert' : nothing;
101109
const promo = this.state ? getApplicablePromo(this.state, 'gate') : undefined;
102-
const consumedDays = 0;
103-
//this.container.storage.get(`plus:featurePreviewTrial:${this.source?.source}:consumedDays`) ?? 0;
110+
let consumedDaysCount = 0;
111+
if (this.source?.source) {
112+
consumedDaysCount = this.featureInPreviewTrial?.[this.source.source]?.consumedDays?.length ?? 0;
113+
}
104114

105115
switch (this.state) {
106116
case SubscriptionState.VerificationRequired:
@@ -124,17 +134,15 @@ export class GlFeatureGatePlusState extends LitElement {
124134

125135
case SubscriptionState.Community:
126136
if (this.allowFeaturePreviewTrial) {
137+
const daysLeft = proPreviewLengthInDays - consumedDaysCount;
127138
return html`
128-
<gl-button
129-
appearance="${appearance}"
130-
href="${generateCommandLink(Commands.PlusStartFeaturePreviewTrial, this.source)}"
139+
<gl-button appearance="${appearance}" href="${this.featurePreviewTrialCommandLink}"
131140
>Continue</gl-button
132141
>
133142
<p>
134-
Continuing gives you ${proPreviewLengthInDays - consumedDays}
135-
day${proPreviewLengthInDays - consumedDays !== 1 ? 's' : ''} to preview
143+
Continuing gives you ${pluralize('day', daysLeft)} to preview
136144
${this.featureWithArticleIfNeeded
137-
? `${this.featureWithArticleIfNeeded} and other `
145+
? `${this.featureWithArticleIfNeeded} and other `
138146
: ''}local
139147
Pro features.<br />
140148
${appearance !== 'alert' ? html`<br />` : ''} For full access to Pro features

src/webviews/apps/shared/components/feature-gate.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { css, html, LitElement } from 'lit';
22
import { customElement, property } from 'lit/decorators.js';
33
import type { SubscriptionState } from '../../../../constants.subscription';
4-
import type { Source } from '../../../../constants.telemetry';
4+
import type { Source, Sources } from '../../../../constants.telemetry';
55
import { isSubscriptionStatePaidOrTrial } from '../../../../plus/gk/account/subscription';
66
import '../../plus/shared/components/feature-gate-plus-state';
77

@@ -93,6 +93,14 @@ export class GlFeatureGate extends LitElement {
9393
@property({ type: Boolean })
9494
allowFeaturePreviewTrial?: boolean;
9595

96+
@property({ type: Object })
97+
featureInPreviewTrial?: {
98+
[key in Sources]?: { consumedDays: { startedOn: string; expiresOn: string }[]; isActive: boolean };
99+
};
100+
101+
@property({ type: String })
102+
featurePreviewTrialCommandLink?: string;
103+
96104
@property({ reflect: true })
97105
appearance?: 'alert' | 'welcome';
98106

@@ -130,6 +138,8 @@ export class GlFeatureGate extends LitElement {
130138
.source=${this.source}
131139
.state=${this.state}
132140
.allowFeaturePreviewTrial=${this.allowFeaturePreviewTrial}
141+
.featureInPreviewTrial=${this.featureInPreviewTrial}
142+
featurePreviewTrialCommandLink=${this.featurePreviewTrialCommandLink}
133143
></gl-feature-gate-plus-state>
134144
</section>
135145
`;

0 commit comments

Comments
 (0)