Skip to content

Commit 26d96e1

Browse files
Updates purchase path and applies promos
1 parent eb8ab34 commit 26d96e1

File tree

12 files changed

+172
-34
lines changed

12 files changed

+172
-34
lines changed

package.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17493,9 +17493,19 @@
1749317493
},
1749417494
{
1749517495
"view": "gitlens.views.worktrees",
17496-
"contents": "[Upgrade to Pro](command:gitlens.plus.upgrade?%7B%22source%22%3A%22worktrees%22%7D)\n\nYour Pro trial has ended. Please upgrade for full access to Worktrees and other Pro features.\nSpecial: 1st seat of Pro is now 50%+ off.",
17496+
"contents": "[Upgrade to Pro](command:gitlens.plus.upgrade?%7B%22source%22%3A%22worktrees%22%7D)\n\nYour Pro trial has ended. Please upgrade for full access to Worktrees and other Pro features.",
1749717497
"when": "gitlens:plus:required && gitlens:plus:state == 4"
1749817498
},
17499+
{
17500+
"view": "gitlens.views.worktrees",
17501+
"contents": "Special: 1st seat of Pro is now 50%+ off.",
17502+
"when": "gitlens:plus:required && gitlens:plus:state == 4 && (gitlens:promo == pro50 || !gitlens:promo)"
17503+
},
17504+
{
17505+
"view": "gitlens.views.worktrees",
17506+
"contents": "Sale: Save up to 80% on GitLens Pro - lowest price of the year!",
17507+
"when": "gitlens:plus:required && gitlens:plus:state == 4 && gitlens:promo == devex-days"
17508+
},
1749917509
{
1750017510
"view": "gitlens.views.worktrees",
1750117511
"contents": "[Continue](command:gitlens.plus.reactivateProTrial?%7B%22source%22%3A%22worktrees%22%7D)\n\nReactivate your Pro trial and experience Worktrees and all the new Pro features — free for another 7 days!",

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,7 @@ export type ContextKeys = {
703703
'gitlens:plus:required': boolean;
704704
'gitlens:plus:state': SubscriptionState;
705705
'gitlens:prerelease': boolean;
706+
'gitlens:promo': string;
706707
'gitlens:readonly': boolean;
707708
'gitlens:repos:withRemotes': string[];
708709
'gitlens:repos:withHostingIntegrations': string[];

src/plus/gk/account/promos.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { SubscriptionState } from './subscription';
2+
3+
export interface Promo {
4+
readonly key: string;
5+
readonly states?: SubscriptionState[];
6+
readonly expiresOn?: number;
7+
readonly startsOn?: number;
8+
9+
readonly code?: string;
10+
readonly title?: string;
11+
readonly description?: string;
12+
readonly descriptionCTA?: string;
13+
readonly descriptionIntro?: string;
14+
readonly url?: string;
15+
readonly command?: string;
16+
}
17+
18+
// Must be ordered by applicable order
19+
const promos: Promo[] = [
20+
{
21+
key: 'devex-days',
22+
expiresOn: new Date('2024-09-05T06:59:00.000Z').getTime(),
23+
states: [
24+
SubscriptionState.FreePlusInTrial,
25+
SubscriptionState.FreePlusTrialExpired,
26+
SubscriptionState.FreePlusTrialReactivationEligible,
27+
],
28+
code: 'DEVEXDAYS24',
29+
description: 'Save up to 80% on GitLens Pro - lowest price of the year!',
30+
descriptionIntro: 'Sale',
31+
},
32+
33+
{
34+
key: 'pro50',
35+
states: [
36+
SubscriptionState.Free,
37+
SubscriptionState.FreeInPreviewTrial,
38+
SubscriptionState.FreePlusInTrial,
39+
SubscriptionState.FreePlusTrialExpired,
40+
SubscriptionState.FreePlusTrialReactivationEligible,
41+
],
42+
description: '1st seat of Pro is now 50%+ off.',
43+
descriptionCTA: 'See your special price.',
44+
},
45+
];
46+
47+
export function getApplicablePromo(state: number): Promo | undefined {
48+
const now = Date.now();
49+
for (const promo of promos) {
50+
if (
51+
(promo.states == null || promo.states.includes(state)) &&
52+
(promo.expiresOn == null || promo.expiresOn > now) &&
53+
(promo.startsOn == null || promo.startsOn < now)
54+
) {
55+
return promo;
56+
}
57+
}
58+
59+
return undefined;
60+
}

src/plus/gk/account/subscriptionService.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { ensurePlusFeaturesEnabled } from '../utils';
5050
import { AuthenticationContext } from './authenticationConnection';
5151
import { authenticationProviderId, authenticationProviderScopes } from './authenticationProvider';
5252
import type { Organization } from './organization';
53+
import { getApplicablePromo } from './promos';
5354
import type { Subscription } from './subscription';
5455
import {
5556
assertSubscriptionState,
@@ -746,8 +747,11 @@ export class SubscriptionService implements Disposable {
746747
if (this._subscription.account == null) {
747748
this.showPlans(source);
748749
} else {
750+
const promoCode = getApplicablePromo(this._subscription.state)?.code;
749751
const activeOrgId = this._subscription.activeOrganization?.id;
750-
const query = `source=gitlens&product=gitlens${activeOrgId != null ? `&org=${activeOrgId}` : ''}`;
752+
const query = `source=gitlens&product=gitlens${promoCode != null ? `&promoCode=${promoCode}` : ''}${
753+
activeOrgId != null ? `&org=${activeOrgId}` : ''
754+
}`;
751755
try {
752756
const token = await this.container.accountAuthentication.getExchangeToken(
753757
SubscriptionUpdatedUriPathPrefix,
@@ -1171,6 +1175,9 @@ export class SubscriptionService implements Disposable {
11711175
subscription.state = computeSubscriptionState(subscription);
11721176
assertSubscriptionState(subscription);
11731177

1178+
const promo = getApplicablePromo(subscription.state);
1179+
void setContext('gitlens:promo', promo?.key);
1180+
11741181
const previous = this._subscription as typeof this._subscription | undefined; // Can be undefined here, since we call this in the constructor
11751182
// Check the previous and new subscriptions are exactly the same
11761183
const matches = previous != null && JSON.stringify(previous) === JSON.stringify(subscription);

src/webviews/apps/home/home.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@
3030
>Special: 1st seat of Pro is now 50%+ off. See your special price.</a
3131
>
3232
</div>
33+
<div class="promo-banner promo-banner--eyebrow" id="promo-devex-days" hidden>
34+
<a
35+
class="promo-banner__link"
36+
href="command:gitlens.plus.upgrade"
37+
title="Sale: Save up to 80% on GitLens Pro - lowest price of the year!"
38+
><gl-devex-days-svg></gl-devex-days-svg>Sale: Save up to 80% on GitLens Pro - lowest price of the
39+
year!</a
40+
>
41+
</div>
3342
<nav class="inline-nav" id="links" aria-label="Help and Resources">
3443
<div class="inline-nav__group">
3544
<gl-tooltip hoist>

src/webviews/apps/home/home.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import '../shared/components/button';
1818
import '../shared/components/code-icon';
1919
import '../shared/components/feature-badge';
2020
import '../shared/components/overlays/tooltip';
21+
import './svg-devExDays';
2122

2223
export class HomeApp extends App<State> {
2324
constructor() {
@@ -63,7 +64,7 @@ export class HomeApp extends App<State> {
6364
break;
6465

6566
case DidChangeSubscription.is(msg):
66-
this.state.promoStates = msg.params.promoStates;
67+
this.state.promoKey = msg.params.promoKey;
6768
this.state.subscription = msg.params.subscription;
6869
this.setState(this.state);
6970
this.updatePromos();
@@ -152,12 +153,11 @@ export class HomeApp extends App<State> {
152153
}
153154

154155
private updatePromos() {
155-
const {
156-
promoStates: { hs2023, pro50 },
157-
} = this.state;
156+
const { promoKey } = this.state;
158157

159-
setElementVisibility('promo-hs2023', hs2023);
160-
setElementVisibility('promo-pro50', pro50);
158+
setElementVisibility('promo-hs2023', promoKey === 'hs2023');
159+
setElementVisibility('promo-pro50', promoKey === 'pro50');
160+
setElementVisibility('promo-devex-days', promoKey === 'devex-days');
161161
}
162162

163163
private updateOrgSettings() {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { css, html, LitElement } from 'lit';
2+
import { customElement } from 'lit/decorators.js';
3+
4+
@customElement('gl-devex-days-svg')
5+
export class DevexDaysSvg extends LitElement {
6+
static override styles = [
7+
css`
8+
svg {
9+
max-width: 8rem;
10+
height: auto;
11+
vertical-align: text-bottom;
12+
margin-inline-end: 0.4rem;
13+
}
14+
`,
15+
];
16+
override render() {
17+
return html`
18+
<!-- Don't reformat or let prettier reformat the SVG otherwise whitespace will get added incorrect and screw up the positioning -->
19+
<!-- a-prettier-ignore -->
20+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 138 25">
21+
<path
22+
d="M64.06 13.26c0 .73-.55 1.32-1.24 1.32-.68 0-1.24-.6-1.24-1.32 0-.73.56-1.33 1.24-1.33.69 0 1.24.6 1.24 1.33ZM29.84 19.64h-1.66l-4.2-13h1.57l3.47 11.06L32.5 6.65h1.55l-4.2 12.99ZM22.93 19.64H15.6c-1.65 0-2.43-.4-2.43-2.5v-8c0-2.1.78-2.5 2.43-2.5h5.38c1.66 0 2.44.4 2.44 2.5v2.96c0 2.1-.78 2.5-2.44 2.5h-6.3v2.51c0 1.04.33 1.06 2.14 1.06h6.1v1.47Zm-1-7.56V9.2c0-.69-.13-1.07-1.01-1.07h-5.23c-.88 0-1.01.38-1.01 1.07v3.94h6.24c.88 0 1.01-.38 1.01-1.06ZM8.79 19.64H3.4c-1.66 0-2.43-.4-2.43-2.5v-8c0-2.1.77-2.5 2.43-2.5h6.33V.75h1.48v16.4c0 2.1-.77 2.5-2.43 2.5Zm.95-2.53V8.13H3.5c-.89 0-1.02.38-1.02 1.07v7.91c0 .69.13 1.06 1.02 1.06h5.22c.89 0 1.02-.37 1.02-1.06ZM109.26 19.64h-6.7v-1.47h6.61c.89 0 1.02-.37 1.02-1.06v-2.2c0-.68-.13-1.06-1.02-1.06h-4.7c-1.65 0-2.42-.4-2.42-2.5v-2.2c0-2.1.77-2.5 2.43-2.5h6.66v1.48h-6.6c-.88 0-1.01.38-1.01 1.07v2.12c0 .64.13 1.07.95 1.07h4.78c1.66 0 2.43.4 2.43 2.5v2.24c0 2.1-.77 2.5-2.43 2.5ZM95.77 24.36H94.4l1.48-4.72-4.43-13h1.6l3.54 10.73 3.43-10.72h1.5l-5.75 17.71ZM88.22 19.64h-5.38c-1.66 0-2.43-.4-2.43-2.5v-3.12c0-2.1.77-2.5 2.43-2.5h6.33V9.2c0-.69-.14-1.07-1.02-1.07h-7.5V6.65h7.57c1.66 0 2.43.4 2.43 2.5v7.98c0 2.1-.77 2.5-2.43 2.5Zm.95-2.53V13h-6.24c-.89 0-1.02.38-1.02 1.06v3.05c0 .69.13 1.06 1.02 1.06h5.22c.88 0 1.02-.37 1.02-1.06ZM76.27 19.64h-5.38c-1.66 0-2.43-.4-2.43-2.5V9.14c0-2.1.77-2.5 2.43-2.5h6.33V.74h1.48v16.4c0 2.1-.77 2.5-2.43 2.5Zm.95-2.53V8.13h-6.24c-.89 0-1.02.38-1.02 1.07v7.91c0 .69.13 1.06 1.02 1.06h5.22c.89 0 1.02-.37 1.02-1.06ZM45.6 19.64h-7.33c-1.66 0-2.43-.4-2.43-2.5v-8c0-2.1.77-2.5 2.43-2.5h5.38c1.66 0 2.43.4 2.43 2.5v2.96c0 2.1-.77 2.5-2.43 2.5h-5.53v1.92c0 .8.2.83 1.17.83h6.3v2.29Zm-1.77-8.15v-1.7c0-.69-.13-.83-.8-.83h-4.11c-.67 0-.8.14-.8.83v2.52h4.91c.66 0 .8-.14.8-.82ZM137.37 19.64h-2.65V15.4h-7.97v-2.84l4.98-9.99h3.05l-5.13 10.11h5.07V6.65h2.65v12.99ZM125.83 19.64h-10.18V16.6l6.8-6.56c.86-.85.9-1.4.9-2.08V6c0-.69-.13-.71-.69-.71h-3.65c-.55 0-.68.02-.68.7v1.68h-2.68v-2.6c0-2.1.77-2.5 2.43-2.5h5.5c1.65 0 2.42.4 2.42 2.5v2.63c0 2.1-.02 2.9-1.37 4.2l-5.24 5.01h6.44v2.72ZM46.42 22.65c0-.63.48-1.14 1.07-1.14h10.1c.6 0 1.08.5 1.08 1.14 0 .63-.48 1.14-1.07 1.14H47.49c-.59 0-1.07-.51-1.07-1.14ZM47.54 17.98l2.99-4.46-3-5.03c-.42-.7.05-1.61.83-1.61h1.6c.35 0 .68.2.85.52l3.46 6.38-3.45 5.4a.97.97 0 0 1-.81.46h-1.67c-.8 0-1.27-.97-.8-1.66Z"
23+
fill="currentColor"
24+
/>
25+
<path
26+
opacity=".5"
27+
d="m57.8 17.98-2.99-4.46 3-5.03c.42-.7-.05-1.61-.83-1.61h-1.6c-.35 0-.68.2-.85.52l-3.46 6.38 3.45 5.4c.18.29.49.46.81.46H57c.8 0 1.27-.97.8-1.66Z"
28+
fill="currentColor"
29+
/>
30+
</svg>
31+
`;
32+
}
33+
}

src/webviews/apps/plus/account/components/account-content.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { css, html, LitElement, nothing } from 'lit';
22
import { customElement, property } from 'lit/decorators.js';
33
import { when } from 'lit/directives/when.js';
44
import { urls } from '../../../../../constants';
5+
import { getApplicablePromo } from '../../../../../plus/gk/account/promos';
56
import type { Subscription } from '../../../../../plus/gk/account/subscription';
67
import {
78
getSubscriptionPlanName,
@@ -122,9 +123,12 @@ export class AccountContent extends LitElement {
122123
.special {
123124
font-size: smaller;
124125
margin-top: 0.8rem;
125-
opacity: 0.6;
126126
text-align: center;
127127
}
128+
129+
.special-dim {
130+
opacity: 0.6;
131+
}
128132
`,
129133
];
130134

@@ -242,6 +246,8 @@ export class AccountContent extends LitElement {
242246
}
243247

244248
private renderAccountState() {
249+
const promo = this.state ? getApplicablePromo(this.state) : undefined;
250+
245251
switch (this.state) {
246252
case SubscriptionState.Paid:
247253
return html`
@@ -295,7 +301,11 @@ export class AccountContent extends LitElement {
295301
<button-container>
296302
<gl-button full href="command:gitlens.plus.upgrade">Upgrade to Pro</gl-button>
297303
</button-container>
298-
<p class="special">Special: <b>1st seat of Pro is now 50%+ off.</b></p>
304+
${promo?.description
305+
? html`<p class="special ${promo.key === 'pro50' ? 'special-dim' : ''}">
306+
${promo.descriptionIntro ?? 'Special'}: <b>${promo.description}</b><br />
307+
</p>`
308+
: nothing}
299309
${this.renderIncludesDevEx()}
300310
`;
301311
}
@@ -306,7 +316,11 @@ export class AccountContent extends LitElement {
306316
<button-container>
307317
<gl-button full href="command:gitlens.plus.upgrade">Upgrade to Pro</gl-button>
308318
</button-container>
309-
<p class="special">Special: <b>1st seat of Pro is now 50%+ off.</b></p>
319+
${promo?.description
320+
? html`<p class="${promo.key === 'pro50' ? 'special dim' : 'special dim'}">
321+
${promo.descriptionIntro ?? 'Special'}: <b>${promo.description}</b><br />
322+
</p>`
323+
: nothing}
310324
${this.renderIncludesDevEx()}
311325
`;
312326

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { css, html, LitElement, nothing } from 'lit';
22
import { customElement, property, query } from 'lit/decorators.js';
33
import type { Source } from '../../../../../constants';
44
import { Commands } from '../../../../../constants';
5+
import { getApplicablePromo } from '../../../../../plus/gk/account/promos';
56
import { SubscriptionState } from '../../../../../plus/gk/account/subscription';
67
import type { GlButton } from '../../../shared/components/button';
78
import { linkStyles } from './vscode.css';
@@ -65,7 +66,7 @@ export class GlFeatureGatePlusState extends LitElement {
6566
text-align: center;
6667
}
6768
68-
:host([appearance='welcome']) .special {
69+
:host([appearance='welcome']) .special-dim {
6970
opacity: 0.6;
7071
}
7172
`,
@@ -100,6 +101,7 @@ export class GlFeatureGatePlusState extends LitElement {
100101

101102
this.hidden = false;
102103
const appearance = (this.appearance ?? 'alert') === 'alert' ? 'alert' : nothing;
104+
const promo = this.state ? getApplicablePromo(this.state) : undefined;
103105

104106
switch (this.state) {
105107
case SubscriptionState.VerificationRequired:
@@ -167,7 +169,11 @@ export class GlFeatureGatePlusState extends LitElement {
167169
${this.featureWithArticleIfNeeded ? `${this.featureWithArticleIfNeeded} and other ` : ''}Pro
168170
features.
169171
</p>
170-
<p class="special">Special: <b>1st seat of Pro is now 50%+ off.</b><br /></p>`;
172+
${promo?.description
173+
? html`<p class="special ${promo.key === 'pro50' ? 'special-dim' : ''}">
174+
${promo.descriptionIntro ?? 'Special'}: <b>${promo.description}</b><br />
175+
</p>`
176+
: nothing}`;
171177

172178
case SubscriptionState.FreePlusTrialReactivationEligible:
173179
return html`

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
33
import { customElement, property } from 'lit/decorators.js';
44
import type { Source } from '../../../../constants';
55
import { Commands } from '../../../../constants';
6+
import { getApplicablePromo } from '../../../../plus/gk/account/promos';
67
import type { Subscription } from '../../../../plus/gk/account/subscription';
78
import {
89
getSubscriptionPlanName,
@@ -122,10 +123,13 @@ export class GlFeatureBadge extends LitElement {
122123
.popup-content .actions .special {
123124
font-size: smaller;
124125
margin-top: 0.8rem;
125-
opacity: 0.6;
126126
text-align: center;
127127
}
128128
129+
.popup-content .actions .special-dim {
130+
opacity: 0.6;
131+
}
132+
129133
.hint {
130134
border-bottom: 1px dashed currentColor;
131135
}
@@ -332,6 +336,8 @@ export class GlFeatureBadge extends LitElement {
332336
}
333337

334338
private renderUpgradeActions(leadin?: TemplateResult) {
339+
const promo = this.state ? getApplicablePromo(this.state) : undefined;
340+
335341
return html`<div class="actions">
336342
${leadin ?? nothing}
337343
<gl-button
@@ -340,7 +346,11 @@ export class GlFeatureBadge extends LitElement {
340346
href="${generateCommandLink(Commands.PlusUpgrade, this.source)}"
341347
>Upgrade to Pro</gl-button
342348
>
343-
<p class="special">Special: <b>1st seat of Pro is now 50%+ off.</b><br /></p>
349+
${promo?.description
350+
? html`<p class="special ${promo.key === 'pro50' ? 'special-dim' : ''}">
351+
${promo.descriptionIntro ?? 'Special'}: <b>${promo.description}</b><br />
352+
</p>`
353+
: nothing}
344354
</div>`;
345355
}
346356
}

0 commit comments

Comments
 (0)