Skip to content

Commit d40056c

Browse files
committed
Adds gating to the commit graph
1 parent 13ae92d commit d40056c

File tree

6 files changed

+329
-33
lines changed

6 files changed

+329
-33
lines changed

src/plus/webviews/graph/graphWebview.ts

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { configuration } from '../../../configuration';
77
import { Commands, ContextKeys } from '../../../constants';
88
import type { Container } from '../../../container';
99
import { setContext } from '../../../context';
10+
import { PlusFeatures } from '../../../features';
1011
import type { GitCommit } from '../../../git/models/commit';
1112
import type { GitGraph } from '../../../git/models/graph';
1213
import type { Repository, RepositoryChangeEvent } from '../../../git/models/repository';
@@ -22,12 +23,14 @@ import { RepositoryFolderNode } from '../../../views/nodes/viewNode';
2223
import type { IpcMessage } from '../../../webviews/protocol';
2324
import { onIpc } from '../../../webviews/protocol';
2425
import { WebviewBase } from '../../../webviews/webviewBase';
26+
import type { SubscriptionChangeEvent } from '../../subscription/subscriptionService';
2527
import { ensurePlusFeaturesEnabled } from '../../subscription/utils';
2628
import type { GraphCompositeConfig, GraphRepository, State } from './protocol';
2729
import {
2830
DidChangeCommitsNotificationType,
2931
DidChangeGraphConfigurationNotificationType,
3032
DidChangeNotificationType,
33+
DidChangeSubscriptionNotificationType,
3134
DismissPreviewCommandType,
3235
GetMoreCommitsCommandType,
3336
UpdateColumnCommandType,
@@ -69,6 +72,7 @@ export class GraphWebview extends WebviewBase<State> {
6972
return this._selection;
7073
}
7174

75+
private _etagSubscription?: number;
7276
private _etagRepository?: number;
7377
private _repositoryEventsDisposable: Disposable | undefined;
7478
private _repositoryGraph?: GitGraph;
@@ -88,12 +92,16 @@ export class GraphWebview extends WebviewBase<State> {
8892
'graphWebview',
8993
Commands.ShowGraphPage,
9094
);
91-
this.disposables.push(configuration.onDidChange(this.onConfigurationChanged, this), {
92-
dispose: () => {
93-
this._statusBarItem?.dispose();
94-
void this._repositoryEventsDisposable?.dispose();
95+
this.disposables.push(
96+
configuration.onDidChange(this.onConfigurationChanged, this),
97+
{
98+
dispose: () => {
99+
this._statusBarItem?.dispose();
100+
void this._repositoryEventsDisposable?.dispose();
101+
},
95102
},
96-
});
103+
this.container.subscription.onDidChange(this.onSubscriptionChanged, this),
104+
);
97105

98106
this.onConfigurationChanged();
99107
}
@@ -232,6 +240,13 @@ export class GraphWebview extends WebviewBase<State> {
232240
this.updateState();
233241
}
234242

243+
private onSubscriptionChanged(e: SubscriptionChangeEvent) {
244+
if (e.etag === this._etagSubscription) return;
245+
246+
this._etagSubscription = e.etag;
247+
void this.notifyDidChangeSubscription();
248+
}
249+
235250
private onThemeChanged(theme: ColorTheme) {
236251
if (this._theme != null) {
237252
if (
@@ -340,6 +355,17 @@ export class GraphWebview extends WebviewBase<State> {
340355
});
341356
}
342357

358+
@debug()
359+
private async notifyDidChangeSubscription() {
360+
if (!this.isReady || !this.visible) return false;
361+
362+
const access = await this.container.git.access(PlusFeatures.Graph, this.repository?.path);
363+
return this.notify(DidChangeSubscriptionNotificationType, {
364+
subscription: access.subscription.current,
365+
allowed: access.allowed,
366+
});
367+
}
368+
343369
@debug()
344370
private async notifyDidChangeCommits() {
345371
if (!this.isReady || !this.visible) return false;
@@ -385,6 +411,13 @@ export class GraphWebview extends WebviewBase<State> {
385411
// If we have a set of data refresh to the same set
386412
const limit = this._repositoryGraph?.paging?.limit ?? config.defaultItemLimit;
387413

414+
// only check on private
415+
const access = await this.container.git.access(PlusFeatures.Graph, this.repository?.path);
416+
// TODO: probably not the right place to set this
417+
if (this._etagSubscription == null) {
418+
this._etagSubscription = this.container.subscription.etag;
419+
}
420+
388421
const data = await this.container.git.getCommitsForGraph(
389422
this.repository.path,
390423
this._panel!.webview.asWebviewUri,
@@ -396,6 +429,9 @@ export class GraphWebview extends WebviewBase<State> {
396429
previewBanner: this.previewBanner,
397430
repositories: formatRepositories(this.container.git.openRepositories),
398431
selectedRepository: this.repository.path,
432+
selectedVisibility: access.visibility,
433+
subscription: access.subscription.current,
434+
allowed: access.allowed,
399435
rows: data.rows,
400436
paging: {
401437
startingCursor: data.paging?.startingCursor,

src/plus/webviews/graph/protocol.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import type { CommitType, GraphRow, Remote } from '@gitkraken/gitkraken-components';
22
import type { GraphColumnConfig, GraphConfig } from '../../../config';
3+
import type { RepositoryVisibility } from '../../../git/gitProvider';
4+
import type { Subscription } from '../../../subscription';
35
import { IpcCommandType, IpcNotificationType } from '../../../webviews/protocol';
46

57
export interface State {
68
repositories?: GraphRepository[];
79
selectedRepository?: string;
10+
selectedVisibility?: RepositoryVisibility;
11+
subscription?: Subscription;
12+
allowed?: boolean;
813
rows?: GraphRow[];
914
paging?: GraphPaging;
1015
config?: GraphCompositeConfig;
@@ -94,6 +99,14 @@ export const DidChangeGraphConfigurationNotificationType = new IpcNotificationTy
9499
'graph/configuration/didChange',
95100
);
96101

102+
export interface DidChangeSubscriptionParams {
103+
subscription: Subscription;
104+
allowed: boolean;
105+
}
106+
export const DidChangeSubscriptionNotificationType = new IpcNotificationType<DidChangeSubscriptionParams>(
107+
'graph/subscription/didChange',
108+
);
109+
97110
export interface DidChangeCommitsParams {
98111
rows: GraphRow[];
99112
paging?: GraphPaging;

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

Lines changed: 150 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import type {
1414
GraphRepository,
1515
State,
1616
} from '../../../../plus/webviews/graph/protocol';
17+
import type { Subscription } from '../../../../subscription';
18+
import { SubscriptionState } from '../../../../subscription';
19+
import { fromNow } from '../../shared/date';
1720

1821
export interface GraphWrapperProps extends State {
1922
nonce?: string;
@@ -114,6 +117,8 @@ export function GraphWrapper({
114117
repositories = [],
115118
rows = [],
116119
selectedRepository,
120+
subscription,
121+
allowed,
117122
config,
118123
paging,
119124
onSelectRepository,
@@ -139,7 +144,12 @@ export function GraphWrapper({
139144
const [mainWidth, setMainWidth] = useState<number>();
140145
const [mainHeight, setMainHeight] = useState<number>();
141146
const mainRef = useRef<HTMLElement>(null);
142-
const [showBanner, setShowBanner] = useState(previewBanner);
147+
// banner
148+
const [showPreview, setShowPreview] = useState(previewBanner);
149+
// account
150+
const [showAccount, setShowAccount] = useState(true);
151+
const [isAllowed, setIsAllowed] = useState(allowed ?? false);
152+
const [subscriptionSnapshot, setSubscriptionSnapshot] = useState<Subscription | undefined>(subscription);
143153
// repo selection UI
144154
const [repoExpanded, setRepoExpanded] = useState(false);
145155

@@ -173,6 +183,8 @@ export function GraphWrapper({
173183
setPagingState(state.paging);
174184
setIsLoading(false);
175185
setStyleProps(getStyleProps(state.mixedColumnColors));
186+
setIsAllowed(state.allowed ?? false);
187+
setSubscriptionSnapshot(state.subscription);
176188
}
177189

178190
useEffect(() => {
@@ -207,15 +219,135 @@ export function GraphWrapper({
207219
onSelectionChange?.(graphRows.map(r => r.sha));
208220
};
209221

210-
const handleDismissBanner = () => {
211-
setShowBanner(false);
222+
const handleDismissPreview = () => {
223+
setShowPreview(false);
212224
onDismissPreview?.();
213225
};
214226

227+
const handleDismissAccount = () => {
228+
setShowAccount(false);
229+
};
230+
231+
const renderAlertContent = () => {
232+
if (subscriptionSnapshot == null) return;
233+
234+
let icon = 'account';
235+
let modifier = '';
236+
let content;
237+
let actions;
238+
switch (subscriptionSnapshot.state) {
239+
case SubscriptionState.Free:
240+
case SubscriptionState.Paid:
241+
return;
242+
case SubscriptionState.FreeInPreview:
243+
icon = 'calendar';
244+
modifier = 'neutral';
245+
content = (
246+
<>
247+
<p className="alert__title">Trial Preview</p>
248+
<p className="alert__message">
249+
You're able to view the Commit Graph with any repository until your preview expires
250+
{subscriptionSnapshot.previewTrial
251+
? ` ${fromNow(new Date(subscriptionSnapshot.previewTrial.expiresOn))}`
252+
: ''}
253+
.
254+
</p>
255+
</>
256+
);
257+
break;
258+
case SubscriptionState.FreePreviewExpired:
259+
icon = 'warning';
260+
modifier = 'warning';
261+
content = (
262+
<>
263+
<p className="alert__title">Extend Your Trial</p>
264+
<p className="alert__message">Sign in to extend your free trial an additional 7-days.</p>
265+
</>
266+
);
267+
actions = (
268+
<>
269+
<a className="alert-action" href="command:gitlens.plus.loginOrSignUp">
270+
Try for 7-days
271+
</a>{' '}
272+
<a className="alert-action" href="command:gitlens.plus.purchase">
273+
View Plans
274+
</a>
275+
</>
276+
);
277+
break;
278+
case SubscriptionState.FreePlusInTrial:
279+
icon = 'calendar';
280+
modifier = 'neutral';
281+
content = (
282+
<>
283+
<p className="alert__title">Extended Trial</p>
284+
<p className="alert__message">
285+
You're able to view the Commit Graph with any repository until your trial expires
286+
{subscriptionSnapshot.previewTrial
287+
? ` ${fromNow(new Date(subscriptionSnapshot.previewTrial.expiresOn))}`
288+
: ''}
289+
.
290+
</p>
291+
</>
292+
);
293+
break;
294+
case SubscriptionState.FreePlusTrialExpired:
295+
icon = 'warning';
296+
modifier = 'warning';
297+
content = (
298+
<>
299+
<p className="alert__title">Trial Expired</p>
300+
<p className="alert__message">
301+
Upgrade your account to use the Commit Graph and other GitLens+ features on private repos.
302+
</p>
303+
<p>
304+
<a className="alert-action" href="command:gitlens.plus.purchase">
305+
Upgrade Your Account
306+
</a>
307+
</p>
308+
</>
309+
);
310+
break;
311+
case SubscriptionState.VerificationRequired:
312+
icon = 'unverified';
313+
modifier = 'warning';
314+
content = (
315+
<>
316+
<p className="alert__title">Please verify your email</p>
317+
<p className="alert__message">Please verify the email for the account you created.</p>
318+
</>
319+
);
320+
actions = (
321+
<>
322+
<a className="alert-action" href="command:gitlens.plus.resendVerification">
323+
Resend Verification Email
324+
</a>
325+
<a className="alert-action" href="command:gitlens.plus.validate">
326+
Refresh Verification Status
327+
</a>
328+
</>
329+
);
330+
break;
331+
}
332+
333+
return (
334+
<div className={`alert${modifier !== '' ? ` alert--${modifier}` : ''}`}>
335+
<span className={`alert__icon codicon codicon-${icon}`}></span>
336+
<div className="alert__content">{content}</div>
337+
{actions && <div className="alert__actions">{actions}</div>}
338+
{isAllowed && (
339+
<button className="alert__dismiss" type="button" onClick={() => handleDismissAccount()}>
340+
<span className="codicon codicon-chrome-close"></span>
341+
</button>
342+
)}
343+
</div>
344+
);
345+
};
346+
215347
return (
216348
<>
217-
{showBanner && (
218-
<section className="graph-app__banner">
349+
<section className="graph-app__banners">
350+
{showPreview && (
219351
<div className="alert">
220352
<span className="alert__icon codicon codicon-search"></span>
221353
<div className="alert__content">
@@ -230,13 +362,20 @@ export function GraphWrapper({
230362
.
231363
</p>
232364
</div>
233-
<button className="alert__action" type="button" onClick={() => handleDismissBanner()}>
365+
<button className="alert__dismiss" type="button" onClick={() => handleDismissPreview()}>
234366
<span className="codicon codicon-chrome-close"></span>
235367
</button>
236368
</div>
237-
</section>
238-
)}
239-
<main ref={mainRef} id="main" className="graph-app__main">
369+
)}
370+
{showAccount && renderAlertContent()}
371+
</section>
372+
<main
373+
ref={mainRef}
374+
id="main"
375+
className={`graph-app__main${!isAllowed ? ' is-gated' : ''}`}
376+
aria-hidden={!isAllowed}
377+
>
378+
{!isAllowed && <div className="graph-app__cover"></div>}
240379
{currentRepository !== undefined ? (
241380
<>
242381
{mainWidth !== undefined && mainHeight !== undefined && (
@@ -263,7 +402,7 @@ export function GraphWrapper({
263402
<p>No repository is selected</p>
264403
)}
265404
</main>
266-
<footer className="actionbar graph-app__footer">
405+
<footer className={`actionbar graph-app__footer${!isAllowed ? ' is-gated' : ''}`} aria-hidden={!isAllowed}>
267406
<div className="actionbar__group">
268407
<div className="actioncombo">
269408
<button
@@ -321,7 +460,7 @@ export function GraphWrapper({
321460
)}
322461
</div>
323462
</div>
324-
{graphList.length > 0 && (
463+
{isAllowed && graphList.length > 0 && (
325464
<span className="actionbar__details">
326465
showing {graphList.length} item{graphList.length ? 's' : ''}
327466
</span>

0 commit comments

Comments
 (0)