diff --git a/README.md b/README.md index fe76ad68ecda5..54965230a5843 100644 --- a/README.md +++ b/README.md @@ -293,7 +293,7 @@ Yes. All features are free to use on all repos, **except** for `Pro` features, w While GitLens offers a remarkable set of free features, a subset of `Pro` features tailored for professional developers and teams, require a trial or paid plan for use on privately-hosted repos — use on local or publicly-hosted repos is free for everyone. Additionally `Preview` features may require a paid plan in the future and some, if cloud-backed, require a GitKraken account. -Preview `Pro` features instantly for free for 3 days without an account, or start a free GitLens Pro trial to get an additional 7 days and gain access to `Pro` features to experience the full power of GitLens. +Preview `Pro` features instantly for free for 3 days without an account, or start a free GitLens Pro trial to get an additional 14 days and gain access to `Pro` features to experience the full power of GitLens. ## Are `Pro` and `Preview` features free to use? diff --git a/package.json b/package.json index 26ed4f07a2973..4b16f76472ca1 100644 --- a/package.json +++ b/package.json @@ -5726,11 +5726,6 @@ "title": "Sign Up for GitKraken...", "category": "GitLens" }, - { - "command": "gitlens.plus.startPreviewTrial", - "title": "Preview Pro", - "category": "GitLens" - }, { "command": "gitlens.plus.reactivateProTrial", "title": "Reactivate Pro Trial", @@ -10276,10 +10271,6 @@ "command": "gitlens.plus.signUp", "when": "!gitlens:plus" }, - { - "command": "gitlens.plus.startPreviewTrial", - "when": "!gitlens:plus" - }, { "command": "gitlens.plus.reactivateProTrial", "when": "gitlens:plus:state == 5" @@ -19322,7 +19313,7 @@ }, { "view": "gitlens.views.drafts", - "contents": "Cloud Patches ᴘʀᴇᴠɪᴇᴡ — easily and securely share code with your teammates or other developers, accessible from anywhere, streamlining your workflow with better collaboration." + "contents": "Cloud Patches ᴘʀᴇᴠɪᴇᴡ — easily and securely share code with your teammates and other developers without adding noise to your repository." }, { "view": "gitlens.views.drafts", @@ -19331,16 +19322,17 @@ }, { "view": "gitlens.views.drafts", - "contents": "[Start Pro Trial](command:gitlens.plus.signUp?%7B%22source%22%3A%22cloud-patches%22%7D)\n\nStart your free 7-day Pro trial to try Cloud Patches and other Pro features, or [sign in](command:gitlens.plus.login?%7B%22source%22%3A%22cloud-patches%22%7D).", + "contents": "Preview feature - requires an account and may require [GitLens Pro](https://help.gitkraken.com/gitlens/gitlens-community-vs-gitlens-pro/) in the future.\n[Try GitLens Pro](command:gitlens.plus.signUp?%7B%22source%22%3A%22cloud-patches%22%7D)\n\nGet 14 days of GitLens Pro for free - no credit card required. Or [sign in](command:gitlens.plus.login?%7B%22source%22%3A%22cloud-patches%22%7D).", "when": "!gitlens:plus" }, { - "view": "gitlens.views.drafts", - "contents": "Preview feature ☁️ — requires an account and may require a paid plan in the future." + "view": "gitlens.views.launchpad", + "contents": "[Launchpad](command:gitlens.views.launchpad.info \"Learn about Launchpad\") — organizes your pull requests into actionable groups to help you focus and keep your team unblocked." }, { "view": "gitlens.views.launchpad", - "contents": "[Launchpad](command:gitlens.views.launchpad.info \"Learn about Launchpad\") — organizes your pull requests into actionable groups to help you focus and keep your team unblocked." + "contents": "Pro feature — requires a paid plan for use on privately-hosted repos.", + "when": "!gitlens:plus" }, { "view": "gitlens.views.scm.grouped", @@ -19369,23 +19361,13 @@ }, { "view": "gitlens.views.launchpad", - "contents": "[Continue](command:gitlens.plus.startPreviewTrial?%7B%22source%22%3A%22launchpad-view%22%7D)\n\nContinuing gives you 3 days to preview Launchpad and other local Pro features for 3 days. [Start 7-day Pro trial](command:gitlens.plus.signUp?%7B%22source%22%3A%22launchpad-view%22%7D) or [sign in](command:gitlens.plus.login?%7B%22source%22%3A%22launchpad-view%22%7D) for full access to Pro features.", - "when": "!gitlens:launchpad:connect && gitlens:plus:required && gitlens:plus:state == 0" - }, - { - "view": "gitlens.views.scm.grouped", - "contents": "[Continue](command:gitlens.plus.startPreviewTrial?%7B%22source%22%3A%22launchpad-view%22%7D)\n\nContinuing gives you 3 days to preview Launchpad and other local Pro features for 3 days. [Start 7-day Pro trial](command:gitlens.plus.signUp?%7B%22source%22%3A%22launchpad-view%22%7D) or [sign in](command:gitlens.plus.login?%7B%22source%22%3A%22launchpad-view%22%7D) for full access to Pro features.", - "when": "!gitlens:launchpad:connect && gitlens:plus:required && gitlens:plus:state == 0 && gitlens:views:scm:grouped:view == launchpad" - }, - { - "view": "gitlens.views.launchpad", - "contents": "[Start Pro Trial](command:gitlens.plus.signUp?%7B%22source%22%3A%22launchpad-view%22%7D)\n\nStart your free 7-day Pro trial to try Launchpad and other Pro features, or [sign in](command:gitlens.plus.login?%7B%22source%22%3A%22launchpad-view%22%7D).", - "when": "!gitlens:launchpad:connect && gitlens:plus:required && gitlens:plus:state == 2" + "contents": "[Try GitLens Pro](command:gitlens.plus.signUp?%7B%22source%22%3A%22launchpad-view%22%7D)\n\nGet 14 days of GitLens Pro for free - no credit card required. Or [sign in](command:gitlens.plus.login?%7B%22source%22%3A%22launchpad-view%22%7D).", + "when": "!gitlens:launchpad:connect && gitlens:plus:required && (gitlens:plus:state == 2 || gitlens:plus:state == 0)" }, { "view": "gitlens.views.scm.grouped", - "contents": "[Start Pro Trial](command:gitlens.plus.signUp?%7B%22source%22%3A%22launchpad-view%22%7D)\n\nStart your free 7-day Pro trial to try Launchpad and other Pro features, or [sign in](command:gitlens.plus.login?%7B%22source%22%3A%22launchpad-view%22%7D).", - "when": "!gitlens:launchpad:connect && gitlens:plus:required && gitlens:plus:state == 2 && gitlens:views:scm:grouped:view == launchpad" + "contents": "[Try GitLens Pro](command:gitlens.plus.signUp?%7B%22source%22%3A%22launchpad-view%22%7D)\n\nGet 14 days of GitLens Pro for free - no credit card required. Or [sign in](command:gitlens.plus.login?%7B%22source%22%3A%22launchpad-view%22%7D).", + "when": "!gitlens:launchpad:connect && gitlens:plus:required && (gitlens:plus:state == 0 || gitlens:plus:state == 2) && gitlens:views:scm:grouped:view == launchpad" }, { "view": "gitlens.views.launchpad", @@ -19429,19 +19411,14 @@ }, { "view": "gitlens.views.launchpad", - "contents": "[Continue](command:gitlens.plus.reactivateProTrial?%7B%22source%22%3A%22launchpad-view%22%7D)\n\nReactivate your Pro trial and experience Launchpad and all the new Pro features — free for another 7 days!", + "contents": "[Continue](command:gitlens.plus.reactivateProTrial?%7B%22source%22%3A%22launchpad-view%22%7D)\n\nReactivate your Pro trial and experience Launchpad and all the new Pro features — free for another 14 days!", "when": "!gitlens:launchpad:connect && gitlens:plus:required && gitlens:plus:state == 5" }, { "view": "gitlens.views.scm.grouped", - "contents": "[Continue](command:gitlens.plus.reactivateProTrial?%7B%22source%22%3A%22launchpad-view%22%7D)\n\nReactivate your Pro trial and experience Launchpad and all the new Pro features — free for another 7 days!", + "contents": "[Continue](command:gitlens.plus.reactivateProTrial?%7B%22source%22%3A%22launchpad-view%22%7D)\n\nReactivate your Pro trial and experience Launchpad and all the new Pro features — free for another 14 days!", "when": "!gitlens:launchpad:connect && gitlens:plus:required && gitlens:plus:state == 5 && gitlens:views:scm:grouped:view == launchpad" }, - { - "view": "gitlens.views.launchpad", - "contents": "Pro feature — requires a paid plan for use on privately-hosted repos.", - "when": "!gitlens:launchpad:connect" - }, { "view": "gitlens.views.scm.grouped", "contents": "Pro feature — requires a paid plan for use on privately-hosted repos.", @@ -19458,7 +19435,7 @@ }, { "view": "gitlens.views.workspaces", - "contents": "[Start Pro Trial](command:gitlens.plus.signUp?%7B%22source%22%3A%22workspaces%22%7D)\n\nStart your free 7-day Pro trial to try GitKraken (GK) Workspaces and other Pro features, or [sign in](command:gitlens.plus.login?%7B%22source%22%3A%22workspaces%22%7D).", + "contents": "Use on privately-hosted repos requires [GitLens Pro](https://help.gitkraken.com/gitlens/gitlens-community-vs-gitlens-pro/).\n[Try GitLens Pro](command:gitlens.plus.signUp?%7B%22source%22%3A%22workspaces%22%7D)\n\nGet 14 days of GitLens Pro for free - no credit card required. Or [sign in](command:gitlens.plus.login?%7B%22source%22%3A%22workspaces%22%7D).", "when": "!gitlens:plus" }, { @@ -19467,7 +19444,7 @@ }, { "view": "gitlens.views.worktrees", - "contents": "[Worktrees](https://help.gitkraken.com/gitlens/side-bar/#worktrees-view-pro) ᴾᴿᴼ — minimize context switching by allowing you to work on multiple branches simultaneously." + "contents": "[Worktrees](https://help.gitkraken.com/gitlens/side-bar/#worktrees-view-pro) ᴾᴿᴼ — minimize context switching by working on multiple branches simultaneously." }, { "view": "gitlens.views.scm.grouped", @@ -19496,27 +19473,17 @@ }, { "view": "gitlens.views.worktrees", - "contents": "[Continue](command:gitlens.plus.startPreviewTrial?%7B%22source%22%3A%22worktrees%22%7D)\n\nContinuing gives you 3 days to preview Worktrees and other local Pro features for 3 days. [Start 7-day Pro trial](command:gitlens.plus.signUp?%7B%22source%22%3A%22worktrees%22%7D) or [sign in](command:gitlens.plus.login?%7B%22source%22%3A%22worktrees%22%7D) for full access to Pro features.", - "when": "gitlens:plus:required && gitlens:plus:state == 0" - }, - { - "view": "gitlens.views.scm.grouped", - "contents": "[Continue](command:gitlens.plus.startPreviewTrial?%7B%22source%22%3A%22worktrees%22%7D)\n\nContinuing gives you 3 days to preview Worktrees and other local Pro features for 3 days. [Start 7-day Pro trial](command:gitlens.plus.signUp?%7B%22source%22%3A%22worktrees%22%7D) or [sign in](command:gitlens.plus.login?%7B%22source%22%3A%22worktrees%22%7D) for full access to Pro features.", - "when": "gitlens:plus:required && gitlens:plus:state == 0 && gitlens:views:scm:grouped:view == worktrees" - }, - { - "view": "gitlens.views.worktrees", - "contents": "[Start Pro Trial](command:gitlens.plus.signUp?%7B%22source%22%3A%22worktrees%22%7D)\n\nStart your free 7-day Pro trial to try Worktrees and other Pro features, or [sign in](command:gitlens.plus.login?%7B%22source%22%3A%22worktrees%22%7D).", - "when": "gitlens:plus:required && gitlens:plus:state == 2" + "contents": "Use on privately-hosted repos requires [GitLens Pro](https://help.gitkraken.com/gitlens/gitlens-community-vs-gitlens-pro/).\n[Try GitLens Pro](command:gitlens.plus.signUp?%7B%22source%22%3A%22worktrees%22%7D)\n\nGet 14 days of GitLens Pro for free - no credit card required. Or [sign in](command:gitlens.plus.login?%7B%22source%22%3A%22worktrees%22%7D).", + "when": "gitlens:plus:required && (gitlens:plus:state == 2 || gitlens:plus:state == 0)" }, { "view": "gitlens.views.scm.grouped", - "contents": "[Start Pro Trial](command:gitlens.plus.signUp?%7B%22source%22%3A%22worktrees%22%7D)\n\nStart your free 7-day Pro trial to try Worktrees and other Pro features, or [sign in](command:gitlens.plus.login?%7B%22source%22%3A%22worktrees%22%7D).", - "when": "gitlens:plus:required && gitlens:plus:state == 2 && gitlens:views:scm:grouped:view == worktrees" + "contents": "[Try GitLens Pro](command:gitlens.plus.signUp?%7B%22source%22%3A%22worktrees%22%7D)\n\nGet 14 days of GitLens Pro for free - no credit card required. Or [sign in](command:gitlens.plus.login?%7B%22source%22%3A%22worktrees%22%7D).", + "when": "gitlens:plus:required && (gitlens:plus:state == 0 || gitlens:plus:state == 2) && gitlens:views:scm:grouped:view == worktrees" }, { "view": "gitlens.views.worktrees", - "contents": "[Upgrade to Pro](command:gitlens.plus.upgrade?%7B%22source%22%3A%22worktrees%22%7D)", + "contents": "Use on privately-hosted repos requires [GitLens Pro](https://help.gitkraken.com/gitlens/gitlens-community-vs-gitlens-pro?%7B%22source%22%3A%22worktrees%22%7D).\n\n[Upgrade to Pro](command:gitlens.plus.upgrade?%7B%22source%22%3A%22worktrees%22%7D)", "when": "gitlens:plus:required && gitlens:plus:state == 4" }, { @@ -19544,11 +19511,6 @@ "contents": "Save more than 55% during our GitLens 16 sale!", "when": "gitlens:plus:required && gitlens:plus:state == 4 && gitlens:promo == gitlens16 && gitlens:views:scm:grouped:view == worktrees" }, - { - "view": "gitlens.views.worktrees", - "contents": "Your Pro trial has ended. Please upgrade for full access to Worktrees and other Pro features.", - "when": "gitlens:plus:required && gitlens:plus:state == 4" - }, { "view": "gitlens.views.scm.grouped", "contents": "Your Pro trial has ended. Please upgrade for full access to Worktrees and other Pro features.", @@ -19556,18 +19518,14 @@ }, { "view": "gitlens.views.worktrees", - "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!", + "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 14 days!", "when": "gitlens:plus:required && gitlens:plus:state == 5" }, { "view": "gitlens.views.scm.grouped", - "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!", + "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 14 days!", "when": "gitlens:plus:required && gitlens:plus:state == 5 && gitlens:views:scm:grouped:view == worktrees" }, - { - "view": "gitlens.views.worktrees", - "contents": "Pro feature — requires a paid plan for use on privately-hosted repos." - }, { "view": "gitlens.views.scm.grouped", "contents": "Pro feature — requires a paid plan for use on privately-hosted repos.", diff --git a/src/commands/quickCommand.steps.ts b/src/commands/quickCommand.steps.ts index 72729c473ebfc..01168508eb927 100644 --- a/src/commands/quickCommand.steps.ts +++ b/src/commands/quickCommand.steps.ts @@ -2637,9 +2637,9 @@ export async function* ensureAccessStep< const promo = getApplicablePromo(access.subscription.current.state, 'gate'); const detail = promo?.quickpick.detail; - placeholder = 'Pro feature — requires a trial or paid plan for use on privately-hosted repos'; + placeholder = 'Pro feature — requires a trial or GitLens Pro for use on privately-hosted repos'; if (isSubscriptionPaidPlan(access.subscription.required) && access.subscription.current.account != null) { - placeholder = 'Pro feature — requires a paid plan for use on privately-hosted repos'; + placeholder = 'Pro feature — requires GitLens Pro for use on privately-hosted repos'; directives.push( createDirectiveQuickPickItem(Directive.RequiresPaidSubscription, true, { detail: detail }), createQuickPickSeparator(), diff --git a/src/constants.storage.ts b/src/constants.storage.ts index faada399620b4..ad8a0bcca327a 100644 --- a/src/constants.storage.ts +++ b/src/constants.storage.ts @@ -1,7 +1,7 @@ import type { GraphBranchesVisibility, ViewShowBranchComparison } from './config'; import type { AIProviders } from './constants.ai'; import type { IntegrationId } from './constants.integrations'; -import type { TrackedUsage, TrackedUsageKeys } from './constants.telemetry'; +import type { Sources, TrackedUsage, TrackedUsageKeys } from './constants.telemetry'; import type { GroupableTreeViewTypes } from './constants.views'; import type { Environment } from './container'; import type { Subscription } from './plus/gk/account/subscription'; @@ -78,6 +78,10 @@ export type GlobalStorage = { 'graph:searchMode': StoredGraphSearchMode; 'views:scm:grouped:welcome:dismissed': boolean; } & { [key in `confirm:ai:tos:${AIProviders}`]: boolean } & { + [key in `plus:featurePreviewTrial:${Sources}:consumedDays`]: { startedOn: string; expiresOn: string }[]; +} & { + [key in `confirm:ai:tos:${AIProviders}`]: boolean; +} & { [key in `provider:authentication:skip:${string}`]: boolean; } & { [key in `gk:${string}:checkin`]: Stored } & { [key in `gk:${string}:organizations`]: Stored; diff --git a/src/constants.subscription.ts b/src/constants.subscription.ts index a7fc5e5349e36..1dcef837048aa 100644 --- a/src/constants.subscription.ts +++ b/src/constants.subscription.ts @@ -1,5 +1,5 @@ export const proPreviewLengthInDays = 3; -export const proTrialLengthInDays = 7; +export const proTrialLengthInDays = 14; export type PromoKeys = 'gitlens16' | 'pro50'; diff --git a/src/constants.telemetry.ts b/src/constants.telemetry.ts index f9483f00af88d..71b2365e3d8d4 100644 --- a/src/constants.telemetry.ts +++ b/src/constants.telemetry.ts @@ -413,7 +413,8 @@ export type TelemetryEvents = { | 'resend-verification' | 'pricing' | 'start-preview-trial' - | 'upgrade'; + | 'upgrade' + | `start-${Sources}-preview-trial`; } | { action: 'visibility'; diff --git a/src/constants.ts b/src/constants.ts index 8ea0def7fd01d..f1d326c4dc8d3 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -178,6 +178,7 @@ export const urls = Object.freeze({ interactiveCodeHistory: `https://help.gitkraken.com/gitlens/gitlens-start-here/?${utm}#interactive-code-history`, startIntegrations: `https://help.gitkraken.com/gitlens/gitlens-start-here/?${utm}#improve-workflows-with-integrations`, streamlineCollaboration: `https://help.gitkraken.com/gitlens/gitlens-start-here/?${utm}#streamline-collaboration`, + gitlensProVsCommunity: `https://help.gitkraken.com/gitlens/gitlens-community-vs-gitlens-pro/?${utm}`, }); export type WalkthroughSteps = diff --git a/src/plus/gk/account/subscriptionService.ts b/src/plus/gk/account/subscriptionService.ts index b5cd8912ed1aa..0ad5f9894ea9a 100644 --- a/src/plus/gk/account/subscriptionService.ts +++ b/src/plus/gk/account/subscriptionService.ts @@ -315,46 +315,27 @@ export class SubscriptionService implements Disposable { } = this._subscription; if (account?.verified === false) { - const days = getSubscriptionTimeRemaining(this._subscription, 'days') ?? proTrialLengthInDays; - const verify: MessageItem = { title: 'Resend Email' }; - const learn: MessageItem = { title: 'See Pro Features' }; const confirm: MessageItem = { title: 'Continue', isCloseAffordance: true }; + const result = await window.showInformationMessage( - isSubscriptionPaid(this._subscription) - ? `You are now on the ${actual.name} plan. \n\nYou must first verify your email. Once verified, you will have full access to Pro features.` - : `Welcome to your ${ - effective.name - } Trial.\n\nYou must first verify your email. Once verified, you will have full access to Pro features for ${ - days < 1 ? '<1 more day' : pluralize('day', days, { infix: ' more ' }) - }.`, - { - modal: true, - detail: `Your ${ - isSubscriptionPaid(this._subscription) ? 'plan' : 'trial' - } also includes access to the GitKraken DevEx platform, unleashing powerful Git visualization & productivity capabilities everywhere you work: IDE, desktop, browser, and terminal.`, - }, + 'Welcome to GitLens', + { modal: true, detail: 'Verify the email we just sent you to start your Pro trial.' }, verify, - learn, confirm, ); if (result === verify) { void this.resendVerification(source); - } else if (result === learn) { - void this.learnAboutPro({ source: 'prompt', detail: { action: 'trial-started-verify-email' } }, source); } } else if (isSubscriptionPaid(this._subscription)) { - const learn: MessageItem = { title: 'See Pro Features' }; + const learn: MessageItem = { title: 'Learn More' }; const confirm: MessageItem = { title: 'Continue', isCloseAffordance: true }; const result = await window.showInformationMessage( `You are now on the ${actual.name} plan and have full access to Pro features.`, - { - modal: true, - detail: 'Your plan also includes access to the GitKraken DevEx platform, unleashing powerful Git visualization & productivity capabilities everywhere you work: IDE, desktop, browser, and terminal.', - }, - learn, + { modal: true }, confirm, + learn, ); if (result === learn) { @@ -363,7 +344,7 @@ export class SubscriptionService implements Disposable { } else if (isSubscriptionTrial(this._subscription)) { const days = getSubscriptionTimeRemaining(this._subscription, 'days') ?? 0; - const learn: MessageItem = { title: 'See Pro Features' }; + const learn: MessageItem = { title: 'Learn More' }; const confirm: MessageItem = { title: 'Continue', isCloseAffordance: true }; const result = await window.showInformationMessage( `Welcome to your ${effective.name} Trial.\n\nYou now have full access to Pro features for ${ @@ -382,13 +363,13 @@ export class SubscriptionService implements Disposable { } } else { const upgrade: MessageItem = { title: 'Upgrade to Pro' }; - const learn: MessageItem = { title: 'See Pro Features' }; + const learn: MessageItem = { title: 'Community vs. Pro' }; const confirm: MessageItem = { title: 'Continue', isCloseAffordance: true }; const result = await window.showInformationMessage( `You are now on the ${actual.name} plan.`, { modal: true, - detail: 'You only have access to Pro features on publicly-hosted repos. For full access to Pro features, please upgrade to a paid plan.\nA paid plan also includes access to the GitKraken DevEx platform, unleashing powerful Git visualization & productivity capabilities everywhere you work: IDE, desktop, browser, and terminal.', + detail: 'You only have access to Pro features on publicly-hosted repos. For full access to Pro features, please upgrade to a paid plan.', }, upgrade, learn, @@ -711,19 +692,14 @@ export class SubscriptionService implements Disposable { this.changeSubscription(subscription); setTimeout(async () => { - const confirm: MessageItem = { title: 'Continue' }; - const learn: MessageItem = { title: 'See Pro Features' }; - const result = await window.showInformationMessage( - `You can now preview local Pro features for ${ + await window.showInformationMessage( + `You can now preview the Commit Graph on privately-hosted repos for ${ days < 1 ? '1 day' : pluralize('day', days) - }, or [start your free ${proTrialLengthInDays}-day Pro trial](command:gitlens.plus.signUp "Start Pro Trial") for full access to Pro features.`, - confirm, - learn, + }, or [start your free ${proTrialLengthInDays}-day Pro trial](command:gitlens.plus.signUp "Start Pro Trial") for full access to all [GitLens Pro](${ + urls.gitlensProVsCommunity + }) features.`, + { title: 'Continue' }, ); - - if (result === learn) { - void this.learnAboutPro({ source: 'notification', detail: { action: 'preview-started' } }, source); - } }, 1); } diff --git a/src/plus/webviews/graph/graphWebview.ts b/src/plus/webviews/graph/graphWebview.ts index 4a69e82a51d0d..3036f1b4b2514 100644 --- a/src/plus/webviews/graph/graphWebview.ts +++ b/src/plus/webviews/graph/graphWebview.ts @@ -20,6 +20,7 @@ import type { import { GlyphChars } from '../../../constants'; import { Commands } from '../../../constants.commands'; import type { StoredGraphFilters, StoredGraphRefType } from '../../../constants.storage'; +import { proPreviewLengthInDays } from '../../../constants.subscription'; import type { GraphShownTelemetryContext, GraphTelemetryContext, TelemetryEvents } from '../../../constants.telemetry'; import type { Container } from '../../../container'; import { CancellationError } from '../../../errors'; @@ -97,6 +98,7 @@ import { getSearchQueryComparisonKey, parseSearchQuery } from '../../../git/sear import { splitGitCommitMessage } from '../../../git/utils/commit-utils'; import { ReferencesQuickPickIncludes, showReferencePicker } from '../../../quickpicks/referencePicker'; import { showRepositoryPicker } from '../../../quickpicks/repositoryPicker'; +import { createFromDateDelta } from '../../../system/date'; import { gate } from '../../../system/decorators/gate'; import { debug, log } from '../../../system/decorators/log'; import type { Deferrable } from '../../../system/function'; @@ -136,6 +138,7 @@ import type { DidGetCountParams, DidGetRowHoverParams, DidSearchParams, + DidSetFeaturePreviewTrialParams, DoubleClickedParams, GetMissingAvatarsParams, GetMissingRefsMetadataParams, @@ -206,6 +209,7 @@ import { DidChangeWorkingTreeNotification, DidFetchNotification, DidSearchNotification, + DidSetFeaturePreviewTrialNotification, DoubleClickedCommandType, EnsureRowRequest, GetCountsRequest, @@ -294,6 +298,7 @@ export class GraphWebviewProvider implements WebviewProvider | null | undefined; private _search: GitSearch | undefined; @@ -681,11 +686,50 @@ export class GraphWebviewProvider implements WebviewProvider 0 && new Date(consumedDays[consumedDays.length - 1].expiresOn) > timestamp) { + return; + } + + if (consumedDays.length >= proPreviewLengthInDays) { + void window.showInformationMessage( + `You have already used your ${proPreviewLengthInDays} days of previewing local Pro features.`, + ); + return; + } + + await this.container.storage.store(`plus:featurePreviewTrial:graph:consumedDays`, [ + ...(consumedDays ?? []), + { + startedOn: timestamp.toISOString(), + expiresOn: createFromDateDelta(timestamp, { days: 1 }).toISOString(), + }, + ]); + + if (this.container.telemetry.enabled) { + this.container.telemetry.sendEvent( + 'subscription/action', + { action: `start-graph-preview-trial` }, + { source: 'graph' }, + ); + } + + void this.notifyDidSetFeaturePreviewTrial(); + } + onWindowFocusChanged(focused: boolean): void { this.isWindowFocused = focused; } @@ -1874,6 +1918,16 @@ export class GraphWebviewProvider implements WebviewProvider 0 && + storedValue.length <= proPreviewLengthInDays && + new Date(storedValue[storedValue.length - 1].expiresOn) > new Date(), + }; + } + private updateIncludeOnlyRefs( repoPath: string | undefined, { branchesVisibility, refs }: UpdateIncludedRefsParams, diff --git a/src/plus/webviews/graph/protocol.ts b/src/plus/webviews/graph/protocol.ts index f0cfd83c1b5e4..d946565c6a04e 100644 --- a/src/plus/webviews/graph/protocol.ts +++ b/src/plus/webviews/graph/protocol.ts @@ -25,6 +25,7 @@ import type { import type { Config, DateStyle, GraphBranchesVisibility } from '../../../config'; import type { SupportedCloudIntegrationIds } from '../../../constants.integrations'; import type { SearchQuery } from '../../../constants.search'; +import type { Sources } from '../../../constants.telemetry'; import type { RepositoryVisibility } from '../../../git/gitProvider'; import type { GitTrackingState } from '../../../git/models/branch'; import type { GitGraphRowType } from '../../../git/models/graph'; @@ -92,6 +93,7 @@ export const supportedRefMetadataTypes: GraphRefMetadataType[] = ['upstream', 'p export interface State extends WebviewState { windowFocused?: boolean; + webroot?: string; repositories?: GraphRepository[]; selectedRepository?: string; selectedRepositoryVisibility?: RepositoryVisibility; @@ -129,6 +131,7 @@ export interface State extends WebviewState { bottom: number; }; theming?: { cssVariables: CssVariables; themeOpacityFactor: number }; + graphPreviewTrial?: { consumedDays: { startedOn: string; expiresOn: string }[]; isActive: boolean }; } export interface BranchState extends GitTrackingState { @@ -380,6 +383,16 @@ export interface DidSearchParams { export const SearchRequest = new IpcRequest(scope, 'search'); // NOTIFICATIONS +export interface DidSetFeaturePreviewTrialParams { + feature: Sources; + consumedDays: { startedOn: string; expiresOn: string }[]; + isActive: boolean; +} + +export const DidSetFeaturePreviewTrialNotification = new IpcNotification( + scope, + 'featurePreviewTrial/didSet', +); export interface DidChangeRepoConnectionParams { repositories?: GraphRepository[]; diff --git a/src/quickpicks/items/directive.ts b/src/quickpicks/items/directive.ts index ba5b010f0dc62..773560aa02344 100644 --- a/src/quickpicks/items/directive.ts +++ b/src/quickpicks/items/directive.ts @@ -1,5 +1,6 @@ import type { QuickPickItem, ThemeIcon, Uri } from 'vscode'; import { proPreviewLengthInDays, proTrialLengthInDays } from '../../constants.subscription'; +import { pluralize } from '../../system/string'; export enum Directive { Back, @@ -64,8 +65,11 @@ export function createDirectiveQuickPickItem( detail = `Continuing gives you ${proPreviewLengthInDays} days to preview this and other local Pro features`; break; case Directive.StartProTrial: - label = 'Start Pro Trial'; - detail = `Start your free ${proTrialLengthInDays}-day Pro trial for full access to Pro features`; + label = 'Try GitLens Pro'; + detail = `Get ${pluralize( + 'day', + proTrialLengthInDays, + )} of GitLens Pro trial for free - no credit card required.`; break; case Directive.RequiresVerification: label = 'Resend Email'; @@ -74,9 +78,9 @@ export function createDirectiveQuickPickItem( case Directive.RequiresPaidSubscription: label = 'Upgrade to Pro'; if (detail != null) { - description ??= ' \u2014\u00a0\u00a0 a paid plan is required to use this Pro feature'; + description ??= ' \u2014\u00a0\u00a0 GitLens Pro is required to use this feature'; } else { - detail = 'Upgrading to a paid plan is required to use this Pro feature'; + detail = 'Upgrading to GitLens Pro is required to use this feature'; } break; } diff --git a/src/webviews/apps/media/graph-commit-search.png b/src/webviews/apps/media/graph-commit-search.png new file mode 100644 index 0000000000000..b2cf3e00fc1be Binary files /dev/null and b/src/webviews/apps/media/graph-commit-search.png differ diff --git a/src/webviews/apps/media/graph-minimap.png b/src/webviews/apps/media/graph-minimap.png new file mode 100644 index 0000000000000..216f32274379c Binary files /dev/null and b/src/webviews/apps/media/graph-minimap.png differ diff --git a/src/webviews/apps/plus/graph/GraphWrapper.tsx b/src/webviews/apps/plus/graph/GraphWrapper.tsx index 9f95da0833fca..1ee78e65c799f 100644 --- a/src/webviews/apps/plus/graph/GraphWrapper.tsx +++ b/src/webviews/apps/plus/graph/GraphWrapper.tsx @@ -64,6 +64,7 @@ import { DidChangeWorkingTreeNotification, DidFetchNotification, DidSearchNotification, + DidSetFeaturePreviewTrialNotification, } from '../../../../plus/webviews/graph/protocol'; import { createCommandLink } from '../../../../system/commands'; import { filterMap, first, groupByFilterMap, join } from '../../../../system/iterable'; @@ -282,6 +283,8 @@ export function GraphWrapper({ const [windowFocused, setWindowFocused] = useState(state.windowFocused); const [allowed, setAllowed] = useState(state.allowed ?? false); const [subscription, setSubscription] = useState(state.subscription); + const [graphPreviewTrial, setGraphPreviewTrial] = useState(state.graphPreviewTrial); + // search state const searchEl = useRef(null); const [searchQuery, setSearchQuery] = useState(undefined); @@ -318,6 +321,10 @@ export function GraphWrapper({ setStyleProps(state.theming); } break; + case DidSetFeaturePreviewTrialNotification: + setGraphPreviewTrial(state.graphPreviewTrial); + setAllowed(state.graphPreviewTrial?.isActive || allowed); + break; case DidChangeAvatarsNotification: setAvatars(state.avatars); break; @@ -1530,10 +1537,17 @@ export function GraphWrapper({

diff --git a/src/webviews/apps/plus/graph/graph.tsx b/src/webviews/apps/plus/graph/graph.tsx index f215c20b40da0..e10d3bee29ec1 100644 --- a/src/webviews/apps/plus/graph/graph.tsx +++ b/src/webviews/apps/plus/graph/graph.tsx @@ -38,6 +38,7 @@ import { DidChangeWorkingTreeNotification, DidFetchNotification, DidSearchNotification, + DidSetFeaturePreviewTrialNotification, DoubleClickedCommandType, EnsureRowRequest, GetMissingAvatarsCommand, @@ -165,7 +166,11 @@ export class GraphApp extends App { this.state.avatars = msg.params.avatars; this.setState(this.state, DidChangeAvatarsNotification); break; - + case DidSetFeaturePreviewTrialNotification.is(msg): + this.state.graphPreviewTrial = { consumedDays: msg.params.consumedDays, isActive: msg.params.isActive }; + this.state.allowed = msg.params.isActive || this.state.allowed; + this.setState(this.state, DidSetFeaturePreviewTrialNotification); + break; case DidChangeBranchStateNotification.is(msg): this.state.branchState = msg.params.branchState; this.setState(this.state, DidChangeBranchStateNotification); diff --git a/src/webviews/apps/plus/shared/components/feature-gate-plus-state.ts b/src/webviews/apps/plus/shared/components/feature-gate-plus-state.ts index 0f995ad28fc07..1aeb0f1210a02 100644 --- a/src/webviews/apps/plus/shared/components/feature-gate-plus-state.ts +++ b/src/webviews/apps/plus/shared/components/feature-gate-plus-state.ts @@ -1,8 +1,10 @@ +import type { TemplateResult } from 'lit'; import { css, html, LitElement, nothing } from 'lit'; import { customElement, property, query } from 'lit/decorators.js'; +import { urls } from '../../../../../constants'; import { Commands } from '../../../../../constants.commands'; -import { proTrialLengthInDays, SubscriptionState } from '../../../../../constants.subscription'; -import type { Source } from '../../../../../constants.telemetry'; +import { proPreviewLengthInDays, proTrialLengthInDays, SubscriptionState } from '../../../../../constants.subscription'; +import type { Source, Sources } from '../../../../../constants.telemetry'; import type { Promo } from '../../../../../plus/gk/account/promos'; import { getApplicablePromo } from '../../../../../plus/gk/account/promos'; import { pluralize } from '../../../../../system/string'; @@ -69,6 +71,14 @@ export class GlFeatureGatePlusState extends LitElement { @query('gl-button') private readonly button!: GlButton; + @property({ type: Object }) + featureInPreviewTrial?: { + [key in Sources]?: { consumedDays: { startedOn: string; expiresOn: string }[]; isActive: boolean }; + }; + + @property({ type: String }) + featurePreviewTrialCommandLink?: string; + @property({ type: String }) appearance?: 'alert' | 'welcome'; @@ -81,6 +91,9 @@ export class GlFeatureGatePlusState extends LitElement { @property({ attribute: false, type: Number }) state?: SubscriptionState; + @property({ type: String }) + webroot?: string; + protected override firstUpdated() { if (this.appearance === 'alert') { queueMicrotask(() => this.button.focus()); @@ -96,6 +109,11 @@ export class GlFeatureGatePlusState extends LitElement { this.hidden = false; const appearance = (this.appearance ?? 'alert') === 'alert' ? 'alert' : nothing; const promo = this.state ? getApplicablePromo(this.state, 'gate') : undefined; + let consumedDaysCount = 0; + const feature = this.source?.source; + if (feature) { + consumedDaysCount = this.featureInPreviewTrial?.[feature]?.consumedDays?.length ?? 0; + } switch (this.state) { case SubscriptionState.VerificationRequired: @@ -118,52 +136,42 @@ export class GlFeatureGatePlusState extends LitElement { `; case SubscriptionState.Community: - return html` - Continue -

- Continuing gives you 3 days to preview - ${this.featureWithArticleIfNeeded ? `${this.featureWithArticleIfNeeded} and other ` : ''}local - Pro features.
- ${appearance !== 'alert' ? html`
` : ''} For full access to Pro features - start your free ${proTrialLengthInDays}-day Pro trial - or - sign in. -

- `; - case SubscriptionState.ProPreviewExpired: - return html` + if ( + this.state === SubscriptionState.Community && + feature && + this.featureInPreviewTrial?.[feature] && + proPreviewLengthInDays - consumedDaysCount > 0 + ) { + return html` + ${this.getFeaturePreviewModalFor(feature, proPreviewLengthInDays - consumedDaysCount)} + `; + } + + return html`

+ Use on privately-hosted repos requires + GitLens Pro. +

Start Pro Trial Try GitLens Pro 

- Start your free ${proTrialLengthInDays}-day Pro trial to try - ${this.featureWithArticleIfNeeded ? `${this.featureWithArticleIfNeeded} and other ` : ''}Pro - features, or + Get ${proTrialLengthInDays} days of GitLens Pro for free - no credit card required. Or sign in. -

- `; +

`; case SubscriptionState.ProTrialExpired: - return html` + Use on privately-hosted repos requires GitLens Pro. +

+ Upgrade to Pro - ${this.renderPromo(promo)} -

- Your Pro trial has ended. Please upgrade for full access to - ${this.featureWithArticleIfNeeded ? `${this.featureWithArticleIfNeeded} and other ` : ''}Pro - features. -

`; + ${this.renderPromo(promo)}`; case SubscriptionState.ProTrialReactivationEligible: return html` @@ -186,6 +194,80 @@ export class GlFeatureGatePlusState extends LitElement { private renderPromo(promo: Promo | undefined) { return html``; } + + private getFeaturePreviewModalFor(feature: Sources, daysLeft: number) { + const appearance = (this.appearance ?? 'alert') === 'alert' ? 'alert' : nothing; + let partial: TemplateResult<1> | undefined; + switch (feature) { + case 'graph': + switch (daysLeft) { + case 2: + partial = html`

Try Commit Search

+

+ Search for commits in your repo by author, commit message, SHA, file, change, or type. + Turn on the commit filter to show only commits that match your query. +

+

+ Graph Commit Search +

`; + break; + case 1: + partial = html` +

Try the Graph Minimap

+

+ Visualize the amount of changes to a repository over time, and inspect specific points + in the history to locate branches, stashes, tags and pull requests. +

+

+ Graph Minimap +

+ `; + break; + } + return html` + ${partial} + Continue +

+ Continuing gives you ${pluralize('day', daysLeft)} to preview + ${this.featureWithArticleIfNeeded ? `${this.featureWithArticleIfNeeded} on` : ''} + privately-hosted repositories.
+ ${appearance !== 'alert' ? html`
` : ''} For full access to all GitLens Pro features, + start your free ${proTrialLengthInDays}-day Pro trial + - no credit card required. Or + sign in. +

+ `; + default: + return html` + Continue +

+ Continuing gives you ${pluralize('day', daysLeft)} to preview + ${this.featureWithArticleIfNeeded ? `${this.featureWithArticleIfNeeded} on` : ''} + privately-hosted repositories.
+ ${appearance !== 'alert' ? html`
` : ''} For full access to all GitLens Pro features, + start your free ${proTrialLengthInDays}-day Pro trial + - no credit card required. Or + sign in. +

+ `; + } + } } function generateCommandLink(command: Commands, source: Source | undefined) { diff --git a/src/webviews/apps/plus/shared/components/home-account-content.ts b/src/webviews/apps/plus/shared/components/home-account-content.ts index 43e14d99f2e28..fa916c4f971f8 100644 --- a/src/webviews/apps/plus/shared/components/home-account-content.ts +++ b/src/webviews/apps/plus/shared/components/home-account-content.ts @@ -361,11 +361,14 @@ export class GLHomeAccountContent extends LitElement { case SubscriptionState.ProTrialExpired: return html` `; @@ -395,15 +398,16 @@ export class GLHomeAccountContent extends LitElement { return html` `; } diff --git a/src/webviews/apps/plus/shared/components/vscode.css.ts b/src/webviews/apps/plus/shared/components/vscode.css.ts index c06e6459adb31..b33ad6693330c 100644 --- a/src/webviews/apps/plus/shared/components/vscode.css.ts +++ b/src/webviews/apps/plus/shared/components/vscode.css.ts @@ -2,14 +2,18 @@ import { css } from 'lit'; export const linkStyles = css` a { - color: var(--link-foreground); - text-decoration: var(--link-decoration-default, none); + border: 0; + color: var(--color-link-foreground); + font-weight: 400; + outline: none; + text-decoration: none; } - a:focus { - outline-color: var(--focus-border); + a:not([href]):not([tabindex]):focus, + a:not([href]):not([tabindex]):hover { + color: inherit; + text-decoration: none; } - a:hover { - color: var(--link-foreground-active); - text-decoration: underline; + a:focus { + outline-color: var(--color-focus-border); } `; diff --git a/src/webviews/apps/shared/components/feature-gate.ts b/src/webviews/apps/shared/components/feature-gate.ts index 9bba2a5095de6..9f0f87e0e912a 100644 --- a/src/webviews/apps/shared/components/feature-gate.ts +++ b/src/webviews/apps/shared/components/feature-gate.ts @@ -1,7 +1,7 @@ import { css, html, LitElement } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import type { SubscriptionState } from '../../../../constants.subscription'; -import type { Source } from '../../../../constants.telemetry'; +import type { Source, Sources } from '../../../../constants.telemetry'; import { isSubscriptionStatePaidOrTrial } from '../../../../plus/gk/account/subscription'; import '../../plus/shared/components/feature-gate-plus-state'; @@ -90,6 +90,14 @@ export class GlFeatureGate extends LitElement { } `; + @property({ type: Object }) + featureInPreviewTrial?: { + [key in Sources]?: { consumedDays: { startedOn: string; expiresOn: string }[]; isActive: boolean }; + }; + + @property({ type: String }) + featurePreviewTrialCommandLink?: string; + @property({ reflect: true }) appearance?: 'alert' | 'welcome'; @@ -105,6 +113,9 @@ export class GlFeatureGate extends LitElement { @property({ type: Boolean }) visible?: boolean; + @property({ type: String }) + webroot?: string; + override render() { if (!this.visible || (this.state != null && isSubscriptionStatePaidOrTrial(this.state))) { this.hidden = true; @@ -117,15 +128,26 @@ export class GlFeatureGate extends LitElement { : 'welcome'; this.hidden = false; + + const featureInTrial = this.source?.source; + const featureInTrialInfo = featureInTrial ? this.featureInPreviewTrial?.[featureInTrial] : undefined; + const shouldHideFeature = + featureInTrial === 'graph' && + featureInTrialInfo && + featureInTrialInfo.consumedDays.length > 0 && + featureInTrialInfo.consumedDays.length < 3; return html`
- +
`;