diff --git a/README.md b/README.md index d99d0bc52ebee..9d90ed6407cf1 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ GitLens isn’t just for solo developers—it’s designed to enhance team colla ## Cloud Patches `Preview` -Easily and securely share code changes by creating a Cloud Patch from your work-in-progress, commit, or stash, and sharing a link with teammates or other developers. Cloud Patches enable early collaboration for feedback on direction and approach, reducing rework and streamlining your workflow. [Learn more](https://gitkraken.com/solutions/cloud-patches?utm_source=gitlens-extension&utm_medium=in-app-links) +Privately and securely share code changes by creating a Cloud Patch from your work-in-progress, commit, or stash, and sharing a link with specific teammates and other developers. Cloud Patches enable early collaboration for feedback on direction and approach, reducing rework and streamlining your workflow, without adding noise to your repositories. [Learn more](https://gitkraken.com/solutions/cloud-patches?utm_source=gitlens-extension&utm_medium=in-app-links) ## Code Suggest `Preview` @@ -192,7 +192,7 @@ An x-ray or developer tools Inspect into your code, focused on providing context Quick access to many GitLens features. Also the home of GitKraken teams and collaboration services (e.g. Cloud Patches, Cloud Workspaces), help, and support. - **Home** — Quick access to many features. -- [**Cloud Patches `Preview`**](#cloud-patches-preview) — Easily and securely share code with your teammates +- [**Cloud Patches `Preview`**](#cloud-patches-preview) — Privately and securely share code with specific teammates - [**Cloud Workspaces `Preview`**](#gitkraken-workspaces-preview) — Easily group and manage multiple repositories together, accessible from anywhere, streamlining your workflow. ### Source Control @@ -282,7 +282,7 @@ Use the `Generate Commit Message` command from the Source Control view's context When you're ready to unlock the full potential of GitLens and enjoy all the benefits, consider [upgrading to GitLens Pro](https://gitkraken.dev/register?product=gitlens&source=marketing_page&redirect_uri=vscode%3A%2F%2Feamodio.gitlens%2Flogin&flow=gitlens_web). With GitLens Pro, you'll gain access to [Pro features](https://gitkraken.com/gitlens/pro-features?utm_source=gitlens-extension&utm_medium=in-app-links) on privately-hosted repos. -To learn more about the additional features offered with Pro, visit the [GitLens Community vs GitLens Pro](https://help.gitkraken.com/gitlens/gitlens-community-vs-gitlens-pro/?utm_source=gitlens-extension&utm_medium=in-app-links&utm_campaign=readme&utm_term=ready-for-gitlens-pro) page. +To learn more about the additional features offered with Pro, visit the [GitLens Community vs GitLens Pro](https://help.gitkraken.com/gitlens/gitlens-community-vs-gitlens-pro/?utm_source=gitlens-extension&utm_medium=in-app-links&utm_campaign=readme&utm_term=ready-for-gitlens-pro) page. # Support and Community diff --git a/package.json b/package.json index 10f180c44e71a..f85114c681c52 100644 --- a/package.json +++ b/package.json @@ -1397,7 +1397,7 @@ "gitlens.cloudPatches.enabled": { "type": "boolean", "default": true, - "markdownDescription": "Specifies whether to enable the preview of _Cloud Patches_, which allow you to easily and securely share code with your teammates or other developers", + "markdownDescription": "Specifies whether to enable the preview of _Cloud Patches_, which allow you to privately and securely share code with specific teammates and other developers", "scope": "window", "order": 10, "tags": [ @@ -5019,7 +5019,7 @@ "gitlens.plusFeatures.enabled": { "type": "boolean", "default": true, - "markdownDescription": "Specifies whether to hide or show features that require a trial or paid plan and are not accessible given the opened repositories and current trial or plan", + "markdownDescription": "Specifies whether to hide or show features that require a trial or GitLens Pro and are not accessible given the opened repositories and current trial or plan", "scope": "window", "order": 60 }, @@ -10278,7 +10278,7 @@ }, { "command": "gitlens.plus.startPreviewTrial", - "when": "!gitlens:plus" + "when": "false && !gitlens:plus" }, { "command": "gitlens.plus.reactivateProTrial", @@ -19322,7 +19322,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 ᴘʀᴇᴠɪᴇᴡ — privately and securely share code with specific teammates and other developers, accessible from anywhere. Enhance collaboration without adding noise to your repositories." }, { "view": "gitlens.views.drafts", @@ -19331,12 +19331,13 @@ }, { "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": "[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." + "contents": "An account is required and may require [GitLens Pro](https://help.gitkraken.com/gitlens/gitlens-community-vs-gitlens-pro/) in the future.", + "when": "gitlens:plus:state != 6" }, { "view": "gitlens.views.launchpad", @@ -19369,23 +19370,23 @@ }, { "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" + "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 14-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": "false && !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" + "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 14-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": "false && !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 == 0 || gitlens:plus:state == 2)" }, { "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,24 +19430,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.", - "when": "!gitlens:launchpad:connect && gitlens:views:scm:grouped:view == launchpad" - }, { "view": "gitlens.views.workspaces", "contents": "Workspaces ᴘʀᴇᴠɪᴇᴡ — group and manage multiple repositories together, accessible from anywhere, streamlining your workflow.\n\nCreate workspaces just for yourself or share (coming soon in GitLens) them with your team for faster onboarding and better collaboration." @@ -19458,16 +19449,17 @@ }, { "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": "[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" }, { "view": "gitlens.views.workspaces", - "contents": "Preview feature ☁️ — requires an account and may require a paid plan in the future." + "contents": "An account is required and may require [GitLens Pro](https://help.gitkraken.com/gitlens/gitlens-community-vs-gitlens-pro/) in the future.", + "when": "gitlens:plus:state != 6" }, { "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,23 +19488,23 @@ }, { "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" + "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 14-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": "false && 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" + "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 14-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": "false && 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": "[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)" }, { "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", @@ -19556,22 +19548,23 @@ }, { "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." + "contents": "Use on privately-hosted repos require [GitLens Pro](https://help.gitkraken.com/gitlens/gitlens-community-vs-gitlens-pro/).", + "when": "gitlens:plus:state != 6" }, { "view": "gitlens.views.scm.grouped", - "contents": "Pro feature — requires a paid plan for use on privately-hosted repos.", - "when": "gitlens:views:scm:grouped:view == worktrees" + "contents": "Use on privately-hosted repos require [GitLens Pro](https://help.gitkraken.com/gitlens/gitlens-community-vs-gitlens-pro/).", + "when": "gitlens:plus:state != 6 && gitlens:views:scm:grouped:view == worktrees" } ], "views": { @@ -19785,6 +19778,18 @@ "title": "Get Started With GitLens", "description": "Supercharge Git and unlock untapped knowledge within your repo to better understand, write, and review code.", "steps": [ + { + "id": "get-started-community", + "title": "Welcome to GitLens", + "description": "Thank you for installing GitLens—the most popular Git extension for VS Code!\n\n**Community vs. Pro**\n\nYou're using **GitLens Community** edition: Track code changes and see who made them with features like in-editor blame annotations, hovers, CodeLens, and more—completely free.\n\n**Leverage powerful workflows with GitLens Pro**\n\nThe **GitLens Pro** edition unlocks advanced features to accelerate PR reviews, gain actionable insights with rich code visuals, and streamline team collaboration to boost productivity.\n\n[Get Started with GitLens Pro](command:gitlens.walkthrough.plus.signUp)", + "media": { + "markdown": "walkthroughs/welcome/get-started-community.md" + }, + "completionEvents": [ + "onContext:gitlens:walkthroughState:gettingStarted == true" + ], + "when": "!gitlens:plus:state || gitlens:plus:state <= 2" + }, { "id": "welcome-in-trial", "title": "Welcome to GitLens Pro", @@ -19795,24 +19800,24 @@ "completionEvents": [ "onContext:gitlens:walkthroughState:gettingStarted == true" ], - "when": "(gitlens:plus:state == 1 || gitlens:plus:state == 3)" + "when": "gitlens:plus:state == 3" }, { - "id": "welcome-paid", - "title": "Discover the Benefits of GitLens Pro", - "description": "As a **GitLens Pro** user, you have access to powerful tools that accelerate PR reviews, provide deeper code history visualizations, and streamline collaboration across your team.\n\n[Continue the Walkthrough](command:gitlens.walkthrough.openWalkthrough)\n\nTo get the most out of your **GitLens Pro** experience, complete the walkthrough and visit our Help Center for in-depth guides.\n\n**[Learn more in the Help Center](command:gitlens.walkthrough.openHelpCenter)**", + "id": "welcome-in-trial-expired", + "title": "Get the most out of GitLens", + "description": "Thanks for installing GitLens and trying out GitLens Pro.\n\nYou're now on the GitLens Community edition, our free extension that helps you track code changes and who made them, with features like in-editor blame annotations, hovers, CodeLens, and more.\n\nLearn more about the [difference between GitLens Community vs. Pro](command:gitlens.walkthrough.openCommunityVsPro).\n\n**Unlock more powerful tools with GitLens Pro**\n\n[Upgrade to GitLens Pro](command:gitlens.walkthrough.plus.upgrade)\n\nWith GitLens Pro, you can accelerate PR reviews, visualize code history in-depth, and enhance collaboration across your team. It's the perfect upgrade to streamline your VS Code workflow.", "media": { - "markdown": "walkthroughs/welcome/welcome-paid.md" + "markdown": "walkthroughs/welcome/welcome-in-trial-expired.md" }, "completionEvents": [ "onContext:gitlens:walkthroughState:gettingStarted == true" ], - "when": "gitlens:plus:state == 6" + "when": "gitlens:plus:state == 4" }, { "id": "welcome-in-trial-expired-eligible", "title": "Get the most out of GitLens ", - "description": "Thanks for installing GitLens, the #1 most downloaded Git extension in VS Code.\n\nYou're currently using **GitLens Community**, our free extension that helps you track code changes and who made them, with features like in-editor blame annotations, hovers, CodeLens, and more.\n\n**Unlock more powerful tools - Try GitLens Pro again free for 14 days.**\n\n[Get Started with GitLens Pro](command:gitlens.walkthrough.plus.upgrade)\n\nWith GitLens Pro, you can accelerate PR reviews, visualize code history in-depth, and enhance collaboration across your team. It's the perfect upgrade to streamline your VS Code workflow.", + "description": "Thanks for installing GitLens, the #1 most downloaded Git extension in VS Code.\n\nYou're currently using **GitLens Community**, our free extension that helps you track code changes and who made them, with features like in-editor blame annotations, hovers, CodeLens, and more.\n\n**Unlock more powerful tools — Try GitLens Pro again free for 14 days.**\n\n[Get Started with GitLens Pro](command:gitlens.walkthrough.plus.upgrade)\n\nWith GitLens Pro, you can accelerate PR reviews, visualize code history in-depth, and enhance collaboration across your team. It's the perfect upgrade to streamline your VS Code workflow.", "media": { "markdown": "walkthroughs/welcome/welcome-in-trial-expired-eligible.md" }, @@ -19822,28 +19827,16 @@ "when": "gitlens:plus:state == 5" }, { - "id": "welcome-in-trial-expired", - "title": "Get the most out of GitLens", - "description": "Thanks for installing GitLens and trying out GitLens Pro.\n\nYou're now on the GitLens Community edition, our free extension that helps you track code changes and who made them, with features like in-editor blame annotations, hovers, CodeLens, and more.\n\nLearn more about the [difference between GitLens Community vs. Pro](command:gitlens.walkthrough.openCommunityVsPro).\n\n**Unlock more powerful tools with GitLens Pro**\n\n[Upgrade to GitLens Pro](command:gitlens.walkthrough.plus.upgrade)\n\nWith GitLens Pro, you can accelerate PR reviews, visualize code history in-depth, and enhance collaboration across your team. It's the perfect upgrade to streamline your VS Code workflow.", - "media": { - "markdown": "walkthroughs/welcome/welcome-in-trial-expired.md" - }, - "completionEvents": [ - "onContext:gitlens:walkthroughState:gettingStarted == true" - ], - "when": "gitlens:plus:state == 4 || gitlens:plus:state == 2" - }, - { - "id": "get-started-community", - "title": "Welcome to GitLens", - "description": "Thank you for installing GitLens—the most popular Git extension for VS Code!\n\n**Community vs. Pro**\n\nYou're using **GitLens Community** edition: Track code changes and see who made them with features like in-editor blame annotations, hovers, CodeLens, and more—completely free.\n\n**Leverage powerful workflows with GitLens Pro**\n\nThe **GitLens Pro** edition unlocks advanced features to accelerate PR reviews, gain actionable insights with rich code visuals, and streamline team collaboration to boost productivity.\n\n[Get Started with GitLens Pro](command:gitlens.walkthrough.plus.signUp)", + "id": "welcome-paid", + "title": "Discover the Benefits of GitLens Pro", + "description": "As a **GitLens Pro** user, you have access to powerful tools that accelerate PR reviews, provide deeper code history visualizations, and streamline collaboration across your team.\n\n[Continue the Walkthrough](command:gitlens.walkthrough.openWalkthrough)\n\nTo get the most out of your **GitLens Pro** experience, complete the walkthrough and visit our Help Center for in-depth guides.\n\n**[Learn more in the Help Center](command:gitlens.walkthrough.openHelpCenter)**", "media": { - "markdown": "walkthroughs/welcome/get-started-community.md" + "markdown": "walkthroughs/welcome/welcome-paid.md" }, "completionEvents": [ "onContext:gitlens:walkthroughState:gettingStarted == true" ], - "when": "!gitlens:plus:state || gitlens:plus:state <= 0" + "when": "gitlens:plus:state == 6" }, { "id": "visualize-code-history", diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 808d4f2a15332..a27c74db4e095 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -8,6 +8,8 @@ export declare global { export type Mutable = { -readonly [P in keyof T]: T[P] }; export type PickMutable = Omit & { -readonly [P in K]: T[P] }; + export type EntriesType = T extends Record ? [K, V] : never; + export type ExcludeSome = Omit & { [P in K]-?: Exclude }; export type ExtractAll = { [K in keyof T]: T[K] extends U ? T[K] : never }; diff --git a/src/commands/quickCommand.steps.ts b/src/commands/quickCommand.steps.ts index 5fe9a2eb954ed..f22e25915e6cf 100644 --- a/src/commands/quickCommand.steps.ts +++ b/src/commands/quickCommand.steps.ts @@ -2648,9 +2648,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/commands/resets.ts b/src/commands/resets.ts index 3c4c1429e4c49..02322b41db826 100644 --- a/src/commands/resets.ts +++ b/src/commands/resets.ts @@ -13,7 +13,9 @@ const resetTypes = [ 'ai', 'avatars', 'integrations', + 'previews', 'repositoryAccess', + 'subscription', 'suppressedWarnings', 'usageTracking', 'workspace', @@ -73,6 +75,22 @@ export class ResetCommand extends Command { }, ]; + if (DEBUG) { + items.push( + createQuickPickSeparator('DEBUG'), + { + label: 'Reset Subscription...', + detail: 'Resets the stored subscription', + item: 'subscription', + }, + { + label: 'Reset Feature Previews...', + detail: 'Resets the stored state for feature previews', + item: 'previews', + }, + ); + } + // create a quick pick with options to clear all the different resets that GitLens supports const pick = await window.showQuickPick(items, { title: 'Reset Stored Data', @@ -102,10 +120,18 @@ export class ResetCommand extends Command { confirmationMessage = 'Are you sure you want to reset all of the stored integrations?'; confirm.title = 'Reset Integrations'; break; + case 'previews': + confirmationMessage = 'Are you sure you want to reset the stored state for feature previews?'; + confirm.title = 'Reset Feature Previews'; + break; case 'repositoryAccess': confirmationMessage = 'Are you sure you want to reset the repository access cache?'; confirm.title = 'Reset Repository Access'; break; + case 'subscription': + confirmationMessage = 'Are you sure you want to reset the stored subscription?'; + confirm.title = 'Reset Subscription'; + break; case 'suppressedWarnings': confirmationMessage = 'Are you sure you want to reset all of the suppressed warnings?'; confirm.title = 'Reset Suppressed Warnings'; @@ -170,6 +196,18 @@ export class ResetCommand extends Command { case 'workspace': await this.container.storage.resetWorkspace(); break; + default: + if (DEBUG) { + switch (reset) { + case 'subscription': + await this.container.storage.delete('premium:subscription'); + break; + case 'previews': + await this.container.storage.deleteWithPrefix('plus:preview'); + break; + } + } + break; } } } diff --git a/src/constants.commands.ts b/src/constants.commands.ts index 1e9424933e90d..13339d3d7621c 100644 --- a/src/constants.commands.ts +++ b/src/constants.commands.ts @@ -150,6 +150,7 @@ export const enum Commands { PlusShowPlans = 'gitlens.plus.showPlans', PlusSignUp = 'gitlens.plus.signUp', PlusStartPreviewTrial = 'gitlens.plus.startPreviewTrial', + PlusContinueFeaturePreview = 'gitlens.plus.continueFeaturePreview', PlusUpgrade = 'gitlens.plus.upgrade', PlusValidate = 'gitlens.plus.validate', PlusSimulateSubscription = 'gitlens.plus.simulateSubscription', diff --git a/src/constants.context.ts b/src/constants.context.ts index 231db51cc9fd5..096aef6cab852 100644 --- a/src/constants.context.ts +++ b/src/constants.context.ts @@ -19,7 +19,7 @@ export type ContextKeys = { 'gitlens:newInstall': boolean; /** Indicates that this is a new install of GitLens (anywhere for this user -- if synced settings is on) */ 'gitlens:newUserInstall': boolean; - 'gitlens:plus': SubscriptionPlanId; + 'gitlens:plus': Exclude; 'gitlens:plus:disallowedRepos': string[]; 'gitlens:plus:enabled': boolean; 'gitlens:plus:required': boolean; diff --git a/src/constants.storage.ts b/src/constants.storage.ts index faada399620b4..f6dece94467b0 100644 --- a/src/constants.storage.ts +++ b/src/constants.storage.ts @@ -4,6 +4,7 @@ import type { IntegrationId } from './constants.integrations'; import type { TrackedUsage, TrackedUsageKeys } from './constants.telemetry'; import type { GroupableTreeViewTypes } from './constants.views'; import type { Environment } from './container'; +import type { FeaturePreviews } from './features'; import type { Subscription } from './plus/gk/account/subscription'; import type { Integration } from './plus/integrations/integration'; import type { DeepLinkServiceState } from './uris/deepLinks/deepLink'; @@ -77,7 +78,9 @@ export type GlobalStorage = { 'launchpadView:groups:expanded': StoredLaunchpadGroup[]; 'graph:searchMode': StoredGraphSearchMode; 'views:scm:grouped:welcome:dismissed': boolean; -} & { [key in `confirm:ai:tos:${AIProviders}`]: boolean } & { +} & { [key in `plus:preview:${FeaturePreviews}:usages`]: StoredFeaturePreviewUsagePeriod[] } & { + [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; @@ -309,3 +312,8 @@ export type StoredLaunchpadGroup = | 'draft' | 'other' | 'snoozed'; + +export interface StoredFeaturePreviewUsagePeriod { + startedOn: string; + expiresOn: string; +} diff --git a/src/constants.subscription.ts b/src/constants.subscription.ts index a7fc5e5349e36..96274b393491b 100644 --- a/src/constants.subscription.ts +++ b/src/constants.subscription.ts @@ -1,5 +1,7 @@ -export const proPreviewLengthInDays = 3; -export const proTrialLengthInDays = 7; +export const proFeaturePreviewUsages = 3; +export const proFeaturePreviewUsageDurationInDays = 1; +export const proPreviewLengthInDays = 0; +export const proTrialLengthInDays = 14; export type PromoKeys = 'gitlens16' | 'pro50'; diff --git a/src/constants.telemetry.ts b/src/constants.telemetry.ts index f9483f00af88d..d48880080f04c 100644 --- a/src/constants.telemetry.ts +++ b/src/constants.telemetry.ts @@ -5,6 +5,7 @@ import type { Commands } from './constants.commands'; import type { IntegrationId, SupportedCloudIntegrationIds } from './constants.integrations'; import type { SubscriptionState } from './constants.subscription'; import type { CustomEditorTypes, TreeViewTypes, WebviewTypes, WebviewViewTypes } from './constants.views'; +import type { FeaturePreviews } from './features'; import type { GitContributionTiers } from './git/models/contributor'; import type { StartWorkType } from './plus/startWork/startWork'; import type { Period } from './plus/webviews/timeline/protocol'; @@ -418,7 +419,8 @@ export type TelemetryEvents = { | { action: 'visibility'; visible: boolean; - }; + } + | FeaturePreviewActionEventData; /** Sent when the subscription changes */ 'subscription/changed': SubscriptionEventData; @@ -698,3 +700,10 @@ export type TrackedUsageFeatures = | `${TreeViewTypes | WebviewViewTypes}View` | `${CustomEditorTypes}Editor`; export type TrackedUsageKeys = `${TrackedUsageFeatures}:shown` | CommandExecutionTrackedFeatures | WalkthroughUsageKeys; + +export type FeaturePreviewActionsDayEventData = Record<`day.${number}.startedOn`, string>; +export type FeaturePreviewActionEventData = { + action: `start-preview-trial:${FeaturePreviews}`; + startedOn: string; + day: number; +} & FeaturePreviewActionsDayEventData; diff --git a/src/errors.ts b/src/errors.ts index 677df91435e63..f77f170537351 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -14,7 +14,7 @@ export class AccessDeniedError extends Error { if (subscription.account?.verified === false) { message = 'Email verification required'; } else if (required != null && isSubscriptionPaidPlan(required)) { - message = 'Paid plan required'; + message = 'GitLens Pro required'; } else { message = 'Plan required'; } diff --git a/src/features.ts b/src/features.ts index b7b4d2bd8d6d8..e4238393b50a5 100644 --- a/src/features.ts +++ b/src/features.ts @@ -1,5 +1,8 @@ +import type { StoredFeaturePreviewUsagePeriod } from './constants.storage'; +import { proFeaturePreviewUsages } from './constants.subscription'; import type { RepositoryVisibility } from './git/gitProvider'; import type { RequiredSubscriptionPlans, Subscription } from './plus/gk/account/subscription'; +import { capitalize } from './system/string'; export const enum Features { Stashes = 'stashes', @@ -39,3 +42,35 @@ export const enum PlusFeatures { Graph = 'graph', Launchpad = 'launchpad', } + +export type FeaturePreviews = 'graph'; +export const featurePreviews: FeaturePreviews[] = ['graph']; + +export interface FeaturePreview { + feature: FeaturePreviews; + usages: StoredFeaturePreviewUsagePeriod[]; +} + +export function getFeaturePreviewLabel(feature: FeaturePreviews) { + switch (feature) { + case 'graph': + return 'Commit Graph'; + default: + return capitalize(feature); + } +} + +export function isFeaturePreviewActive(featurePreview?: FeaturePreview) { + const usages = featurePreview?.usages; + if (usages == null || usages.length === 0) return false; + + return usages.length <= proFeaturePreviewUsages && new Date(usages[usages.length - 1].expiresOn) > new Date(); +} + +export function isFeaturePreviewExpired(featurePreview: FeaturePreview) { + const usages = featurePreview.usages; + return ( + usages.length > proFeaturePreviewUsages || + (usages.length === proFeaturePreviewUsages && new Date(usages[usages.length - 1].expiresOn) < new Date()) + ); +} diff --git a/src/plus/gk/account/__debug__accountDebug.ts b/src/plus/gk/account/__debug__accountDebug.ts index 67bdbefe81ea8..2e448bcf0085e 100644 --- a/src/plus/gk/account/__debug__accountDebug.ts +++ b/src/plus/gk/account/__debug__accountDebug.ts @@ -1,7 +1,12 @@ import type { Disposable } from 'vscode'; import { ThemeIcon, window } from 'vscode'; import { Commands } from '../../../constants.commands'; -import { proTrialLengthInDays, SubscriptionPlanId, SubscriptionState } from '../../../constants.subscription'; +import { + proFeaturePreviewUsages, + proTrialLengthInDays, + SubscriptionPlanId, + SubscriptionState, +} from '../../../constants.subscription'; import type { Container } from '../../../container'; import type { QuickPickItemOfT } from '../../../quickpicks/items/common'; import { createQuickPickSeparator } from '../../../quickpicks/items/common'; @@ -14,7 +19,9 @@ import type { SubscriptionService } from './subscriptionService'; type SubscriptionServiceFacade = { getSubscription: () => SubscriptionService['_subscription']; + overrideFeaturePreviews: (featurePreviews: SimulatedFeaturePreviews) => void; overrideSession: (session: SubscriptionService['_session']) => void; + restoreFeaturePreviews: () => void; restoreSession: () => void; onDidCheckIn: SubscriptionService['_onDidCheckIn']; changeSubscription: SubscriptionService['changeSubscription']; @@ -25,25 +32,40 @@ export function registerAccountDebug(container: Container, service: Subscription new AccountDebug(container, service); } +interface SimulatedFeaturePreviews { + day: number; + durationSeconds: number; +} + type SimulateQuickPickItem = QuickPickItemOfT< - | { state: null; reactivatedTrial?: never; expiredPaid?: never; planId?: never } + | { state: null; reactivatedTrial?: never; expiredPaid?: never; planId?: never; featurePreview?: never } + | { + state: SubscriptionState.Community; + reactivatedTrial?: never; + expiredPaid?: never; + planId?: never; + featurePreviews?: SimulatedFeaturePreviews; + } | { state: Exclude; reactivatedTrial?: never; expiredPaid?: never; planId?: never; + featurePreviews?: never; } | { state: SubscriptionState.ProTrial; reactivatedTrial?: boolean; expiredPaid?: never; planId?: never; + featurePreviews?: never; } | { state: SubscriptionState.Paid; reactivatedTrial?: never; expiredPaid?: boolean; planId?: SubscriptionPlanId.Pro | SubscriptionPlanId.Teams | SubscriptionPlanId.Enterprise; + featurePreviews?: never; } >; @@ -69,21 +91,42 @@ class AccountDebug { label: 'Community', description: 'Community, no account', iconPath: new ThemeIcon('blank'), - item: { state: SubscriptionState.Community }, + item: { state: SubscriptionState.Community, featurePreviews: { day: 0, durationSeconds: 30 } }, + }, + { + label: 'Community: Feature Previews (Start Day 2)', + description: 'Community, no account', + iconPath: new ThemeIcon('blank'), + item: { state: SubscriptionState.Community, featurePreviews: { day: 1, durationSeconds: 30 } }, }, - createQuickPickSeparator('Preview'), { - label: 'Pro Preview', - description: 'Pro, no account', + label: 'Community: Feature Previews (Start Day 3)', + description: 'Community, no account', iconPath: new ThemeIcon('blank'), - item: { state: SubscriptionState.ProPreview }, + item: { state: SubscriptionState.Community, featurePreviews: { day: 2, durationSeconds: 30 } }, }, { - label: 'Pro Preview (Expired)', + label: 'Community: Feature Previews (Expired)', description: 'Community, no account', iconPath: new ThemeIcon('blank'), - item: { state: SubscriptionState.ProPreviewExpired }, + item: { + state: SubscriptionState.Community, + featurePreviews: { day: proFeaturePreviewUsages, durationSeconds: 30 }, + }, }, + // createQuickPickSeparator('Preview'), + // { + // label: 'Pro Preview', + // description: 'Pro, no account', + // iconPath: new ThemeIcon('blank'), + // item: { state: SubscriptionState.ProPreview }, + // }, + // { + // label: 'Pro Preview (Expired)', + // description: 'Community, no account', + // iconPath: new ThemeIcon('blank'), + // item: { state: SubscriptionState.ProPreviewExpired }, + // }, createQuickPickSeparator('Account'), { label: 'Verification Required', @@ -213,6 +256,7 @@ class AccountDebug { private endSimulation() { this.simulatingPick = undefined; + this.service.restoreFeaturePreviews(); this.service.restoreSession(); this.service.changeSubscription(this.service.getStoredSubscription(), { store: false }); } @@ -226,13 +270,18 @@ class AccountDebug { return true; } - const { state, reactivatedTrial, expiredPaid, planId } = item; + const { state, reactivatedTrial, expiredPaid, planId, featurePreviews } = item; switch (state) { case SubscriptionState.Community: case SubscriptionState.ProPreview: case SubscriptionState.ProPreviewExpired: this.service.overrideSession(null); + if (featurePreviews != null) { + this.service.overrideFeaturePreviews(featurePreviews); + } else { + this.service.restoreFeaturePreviews(); + } this.service.changeSubscription( state === SubscriptionState.Community @@ -240,9 +289,11 @@ class AccountDebug { : getPreviewSubscription(state === SubscriptionState.ProPreviewExpired ? 0 : 3), { store: false }, ); + return false; } + this.service.restoreFeaturePreviews(); this.service.restoreSession(); const subscription = this.service.getStoredSubscription(); diff --git a/src/plus/gk/account/promos.ts b/src/plus/gk/account/promos.ts index bd04d6d69f311..aa2b6dd8b004e 100644 --- a/src/plus/gk/account/promos.ts +++ b/src/plus/gk/account/promos.ts @@ -32,7 +32,7 @@ const promos: Promo[] = [ SubscriptionState.ProTrialReactivationEligible, ], startsOn: new Date('2024-11-11T06:59:00.000Z').getTime(), - expiresOn: new Date('2024-11-22T06:59:00.000Z').getTime(), + expiresOn: new Date('2024-11-24T06:59:00.000Z').getTime(), command: { tooltip: 'Save more than 55% during our GitLens 16 sale!' }, locations: ['account', 'badge', 'gate'], quickpick: { diff --git a/src/plus/gk/account/subscription.ts b/src/plus/gk/account/subscription.ts index bf5c009ad2d36..11ccb3f799d98 100644 --- a/src/plus/gk/account/subscription.ts +++ b/src/plus/gk/account/subscription.ts @@ -176,7 +176,7 @@ export function getSubscriptionPlan( export function getSubscriptionPlanName(id: SubscriptionPlanId) { switch (id) { case SubscriptionPlanId.CommunityWithAccount: - return 'GitLens Free'; + return 'GitLens Community'; case SubscriptionPlanId.Pro: return 'GitLens Pro'; case SubscriptionPlanId.Teams: diff --git a/src/plus/gk/account/subscriptionService.ts b/src/plus/gk/account/subscriptionService.ts index b5cd8912ed1aa..86da8524ebbf3 100644 --- a/src/plus/gk/account/subscriptionService.ts +++ b/src/plus/gk/account/subscriptionService.ts @@ -24,17 +24,32 @@ import type { OpenWalkthroughCommandArgs } from '../../../commands/walkthroughs' import { urls } from '../../../constants'; import type { CoreColors } from '../../../constants.colors'; import { Commands } from '../../../constants.commands'; +import type { StoredFeaturePreviewUsagePeriod } from '../../../constants.storage'; import { + proFeaturePreviewUsageDurationInDays, + proFeaturePreviewUsages, proPreviewLengthInDays, proTrialLengthInDays, SubscriptionPlanId, SubscriptionState, } from '../../../constants.subscription'; -import type { Source, TrackingContext } from '../../../constants.telemetry'; +import type { + FeaturePreviewActionEventData, + FeaturePreviewActionsDayEventData, + Source, + TrackingContext, +} from '../../../constants.telemetry'; import type { Container } from '../../../container'; import { AccountValidationError, RequestsAreBlockedTemporarilyError } from '../../../errors'; +import type { FeaturePreview, FeaturePreviews } from '../../../features'; +import { + featurePreviews, + getFeaturePreviewLabel, + isFeaturePreviewActive, + isFeaturePreviewExpired, +} from '../../../features'; import type { RepositoriesChangeEvent } from '../../../git/gitProviderService'; -import { fromNow } from '../../../system/date'; +import { createFromDateDelta, fromNow } from '../../../system/date'; import { gate } from '../../../system/decorators/gate'; import { debug, log } from '../../../system/decorators/log'; import { take } from '../../../system/event'; @@ -76,6 +91,8 @@ import { SubscriptionUpdatedUriPathPrefix, } from './subscription'; +export type FeaturePreviewChangeEvent = FeaturePreview; + export interface SubscriptionChangeEvent { readonly current: Subscription; readonly previous: Subscription; @@ -88,6 +105,11 @@ export class SubscriptionService implements Disposable { return this._onDidChange.event; } + private _onDidChangeFeaturePreview = new EventEmitter(); + get onDidChangeFeaturePreview(): Event { + return this._onDidChangeFeaturePreview.event; + } + private _onDidCheckIn = new EventEmitter(); get onDidCheckIn(): Event { return this._onDidCheckIn.event; @@ -181,9 +203,9 @@ export class SubscriptionService implements Disposable { if (DEBUG) { void import(/* webpackChunkName: "__debug__" */ './__debug__accountDebug').then(m => { - let restore: { session: AuthenticationSession | null | undefined } | undefined; + let savedSession: { session: AuthenticationSession | null | undefined } | undefined; - function setSession(this: SubscriptionService, session: AuthenticationSession | null | undefined) { + const setSession = (session: AuthenticationSession | null | undefined) => { this._sessionPromise = undefined; if (session === this._session) return; @@ -203,20 +225,104 @@ export class SubscriptionService implements Disposable { removed: previous != null && session == null ? [previous] : [], changed: previous != null && session != null ? [session] : [], }); - } + }; + + let savedFeaturePreviewOverrides: + | { + getFn: SubscriptionService['getStoredFeaturePreview'] | undefined; + setFn: SubscriptionService['storeFeaturePreview'] | undefined; + } + | undefined; m.registerAccountDebug(this.container, { getSubscription: () => this._subscription, + overrideFeaturePreviews: ({ day, durationSeconds }) => { + savedFeaturePreviewOverrides ??= { + getFn: this.getStoredFeaturePreview, + setFn: this.storeFeaturePreview, + }; + + const map = new Map(); + + this.getStoredFeaturePreview = (feature: FeaturePreviews) => { + let featurePreview = map.get(feature); + if (featurePreview == null) { + featurePreview = { + feature: feature, + usages: [], + }; + map.set(feature, featurePreview); + + if (!day) return featurePreview; + + const expired = new Date(0).toISOString(); + for (let i = 1; i <= day; i++) { + featurePreview.usages.push({ startedOn: expired, expiresOn: expired }); + } + } + + return featurePreview; + }; + + this.storeFeaturePreview = (feature: FeaturePreviews) => { + let featurePreview = map.get(feature); + if (featurePreview == null) { + featurePreview = { + feature: feature, + usages: [], + }; + map.set(feature, featurePreview); + } + + day++; + + const now = new Date(); + const expired = new Date(0).toISOString(); + + for (let i = 1; i <= day; i++) { + if (i !== day) { + featurePreview.usages.push({ startedOn: expired, expiresOn: expired }); + continue; + } + + featurePreview.usages.push({ + startedOn: now.toISOString(), + expiresOn: createFromDateDelta(now, { + seconds: durationSeconds, + }).toISOString(), + }); + } + + return Promise.resolve(); + }; + + // Fire a change for all feature previews + for (const feature of featurePreviews) { + this._onDidChangeFeaturePreview.fire(this.getStoredFeaturePreview(feature)); + } + }, + restoreFeaturePreviews: () => { + if (savedFeaturePreviewOverrides) { + this.getStoredFeaturePreview = savedFeaturePreviewOverrides.getFn!; + this.storeFeaturePreview = savedFeaturePreviewOverrides.setFn!; + savedFeaturePreviewOverrides = undefined; + + // Fire a change for all feature previews + for (const feature of featurePreviews) { + this._onDidChangeFeaturePreview.fire(this.getStoredFeaturePreview(feature)); + } + } + }, overrideSession: (session: AuthenticationSession | null | undefined) => { - restore ??= { session: this._session }; + savedSession ??= { session: this._session }; - setSession.call(this, session); + setSession(session); }, restoreSession: () => { - if (restore == null) return; + if (savedSession == null) return; - const { session } = restore; - restore = undefined; + const { session } = savedSession; + savedSession = undefined; setSession.call(this, session); }, @@ -250,6 +356,10 @@ export class SubscriptionService implements Disposable { registerCommand(Commands.PlusRestore, (src?: Source) => this.setProFeaturesVisibility(true, src)), registerCommand(Commands.PlusValidate, (src?: Source) => this.validate({ force: true }, src)), + + registerCommand(Commands.PlusContinueFeaturePreview, ({ feature }: { feature: FeaturePreviews }) => + this.continueFeaturePreview(feature), + ), ]; } @@ -265,6 +375,56 @@ export class SubscriptionService implements Disposable { return this._subscription; } + @gate() + @log() + async continueFeaturePreview(feature: FeaturePreviews) { + const featurePreview = this.getStoredFeaturePreview(feature); + // If the current iteration is still active, don't do anything + if (isFeaturePreviewActive(featurePreview)) return; + if (isFeaturePreviewExpired(featurePreview)) { + void window.showInformationMessage( + `Your ${proFeaturePreviewUsages}-day preview of the ${getFeaturePreviewLabel(feature)} has expired.`, + ); + return; + } + + const now = new Date(); + const usages = [ + ...featurePreview.usages, + { + startedOn: now.toISOString(), + expiresOn: createFromDateDelta(now, { + days: proFeaturePreviewUsageDurationInDays, + }).toISOString(), + }, + ]; + + await this.storeFeaturePreview(feature, usages); + + this._onDidChangeFeaturePreview.fire({ feature: feature, usages: usages }); + + if (this.container.telemetry.enabled) { + const days: FeaturePreviewActionsDayEventData = Object.fromEntries( + usages.map>((d, i) => [ + `day.${i + 1}.startedOn`, + d.startedOn, + ]), + ); + const data: FeaturePreviewActionEventData = { + action: `start-preview-trial:${feature}`, + startedOn: usages[0].startedOn, + day: usages.length, + ...days, + }; + + this.container.telemetry.sendEvent('subscription/action', data, { source: feature }); + } + } + + getFeaturePreview(feature: FeaturePreviews): FeaturePreview { + return this.getStoredFeaturePreview(feature); + } + @debug() async learnAboutPro(source: Source, originalSource: Source | undefined): Promise { if (originalSource != null) { @@ -315,46 +475,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, + `You are now on the ${actual.name} plan and have full access to all GitLens Pro features.`, + { modal: true }, confirm, + learn, ); if (result === learn) { @@ -363,10 +504,10 @@ 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 ${ + `Welcome to your ${effective.name} Trial.\n\nYou now have full access to all GitLens Pro features for ${ days < 1 ? '<1 more day' : pluralize('day', days, { infix: ' more ' }) }.`, { @@ -382,13 +523,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 all Pro features, please upgrade to GitLens Pro.', }, upgrade, learn, @@ -684,11 +825,11 @@ export class SubscriptionService implements Disposable { void this.showAccountView(); if (plan.effective.id === SubscriptionPlanId.Community) { - const signUp: MessageItem = { title: 'Start Pro Trial' }; + const signUp: MessageItem = { title: 'Try GitLens Pro' }; const signIn: MessageItem = { title: 'Sign In' }; const cancel: MessageItem = { title: 'Cancel', isCloseAffordance: true }; const result = await window.showInformationMessage( - `Do you want to start your free ${proTrialLengthInDays}-day Pro trial for full access to Pro features?`, + `Do you want to start your free ${proTrialLengthInDays}-day Pro trial for full access to all GitLens Pro features?`, { modal: true }, signUp, signIn, @@ -712,11 +853,11 @@ export class SubscriptionService implements Disposable { setTimeout(async () => { const confirm: MessageItem = { title: 'Continue' }; - const learn: MessageItem = { title: 'See Pro Features' }; + const learn: MessageItem = { title: 'Learn More' }; const result = await window.showInformationMessage( `You can now preview local Pro features 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.`, + }, or for full access to all GitLens Pro features, [start your free ${proTrialLengthInDays}-day Pro trial](command:gitlens.plus.signUp "Try GitLens Pro") — no credit card required.`, confirm, learn, ); @@ -1288,6 +1429,17 @@ export class SubscriptionService implements Disposable { (subscription.plan.effective as Mutable).name = getSubscriptionPlanName( subscription.plan.effective.id, ); + // Deprecate (expire) the preview trial + if ( + subscription.previewTrial?.expiresOn == null || + new Date(subscription.previewTrial.expiresOn) >= new Date() + ) { + subscription.previewTrial = { + startedOn: subscription.previewTrial?.startedOn ?? new Date(0).toISOString(), + ...subscription.previewTrial, + expiresOn: new Date(0).toISOString(), + }; + } } return subscription; @@ -1300,6 +1452,17 @@ export class SubscriptionService implements Disposable { }); } + private getStoredFeaturePreview(feature: FeaturePreviews): FeaturePreview { + return { + feature: feature, + usages: this.container.storage.get(`plus:preview:${feature}:usages`, []), + }; + } + + private storeFeaturePreview(feature: FeaturePreviews, usages: StoredFeaturePreviewUsagePeriod[]): Promise { + return this.container.storage.store(`plus:preview:${feature}:usages`, usages); + } + private _cancellationSource: CancellationTokenSource | undefined; private _updateAccessContextDebounced: Deferrable | undefined; @@ -1411,7 +1574,7 @@ export class SubscriptionService implements Disposable { infix: ' more ', })} in your **${effective.name}** trial.` : `You have ${pluralize('day', remaining ?? 0)} remaining in your **${effective.name}** trial.` - } Once your trial ends, you'll need a paid plan for full access to [Pro features](command:gitlens.openWalkthrough?%7B%22step%22%3A%22pro-trial%22,%22source%22%3A%22prompt%22%7D).\n\nYour trial also includes access to the [GitKraken DevEx platform](${ + } Once your trial ends, you'll need [GitLens Pro](command:gitlens.openWalkthrough?%7B%22step%22%3A%22pro-trial%22,%22source%22%3A%22prompt%22%7D) for full access to Pro features.\n\nYour trial also includes access to the [GitKraken DevEx platform](${ urls.platform }), unleashing powerful Git visualization & productivity capabilities everywhere you work: IDE, desktop, browser, and terminal.`, true, diff --git a/src/plus/utils.ts b/src/plus/utils.ts index f1fd1fffafbc2..d4bb44e5b4aba 100644 --- a/src/plus/utils.ts +++ b/src/plus/utils.ts @@ -52,11 +52,11 @@ export async function ensurePaidPlan( void container.subscription.startPreviewTrial(source); break; } else if (subscription.account == null) { - const signUp = { title: 'Start Pro Trial' }; + const signUp = { title: 'Try GitLens Pro' }; const signIn = { title: 'Sign In' }; const cancel = { title: 'Cancel', isCloseAffordance: true }; const result = await window.showWarningMessage( - `${title}\n\nDo you want to start your free ${proTrialLengthInDays}-day Pro trial for full access to Pro features?`, + `${title}\n\nDo you want to start your free ${proTrialLengthInDays}-day Pro trial for full access to all GitLens Pro features?`, { modal: true }, signUp, signIn, @@ -72,7 +72,7 @@ export async function ensurePaidPlan( const upgrade = { title: 'Upgrade to Pro' }; const cancel = { title: 'Cancel', isCloseAffordance: true }; const result = await window.showWarningMessage( - `${title}\n\nDo you want to upgrade for full access to Pro features?`, + `${title}\n\nDo you want to upgrade for full access to all GitLens Pro features?`, { modal: true }, upgrade, cancel, diff --git a/src/plus/webviews/graph/graphWebview.ts b/src/plus/webviews/graph/graphWebview.ts index 4a69e82a51d0d..635a8570e47ba 100644 --- a/src/plus/webviews/graph/graphWebview.ts +++ b/src/plus/webviews/graph/graphWebview.ts @@ -24,7 +24,8 @@ import type { GraphShownTelemetryContext, GraphTelemetryContext, TelemetryEvents import type { Container } from '../../../container'; import { CancellationError } from '../../../errors'; import type { CommitSelectedEvent } from '../../../eventBus'; -import { PlusFeatures } from '../../../features'; +import type { FeaturePreview } from '../../../features'; +import { isFeaturePreviewActive, PlusFeatures } from '../../../features'; import { executeGitCommand } from '../../../git/actions'; import * as BranchActions from '../../../git/actions/branch'; import { @@ -126,7 +127,7 @@ import type { IpcCallMessageType, IpcMessage, IpcNotification } from '../../../w import type { WebviewHost, WebviewProvider, WebviewShowingArgs } from '../../../webviews/webviewProvider'; import type { WebviewPanelShowCommandArgs, WebviewShowOptions } from '../../../webviews/webviewsController'; import { isSerializedState } from '../../../webviews/webviewsController'; -import type { SubscriptionChangeEvent } from '../../gk/account/subscriptionService'; +import type { FeaturePreviewChangeEvent, SubscriptionChangeEvent } from '../../gk/account/subscriptionService'; import type { ConnectionStateChangeEvent } from '../../integrations/integrationService'; import { remoteProviderIdToIntegrationId } from '../../integrations/integrationService'; import { getPullRequestBranchDeepLink } from '../../launchpad/launchpadProvider'; @@ -206,6 +207,7 @@ import { DidChangeWorkingTreeNotification, DidFetchNotification, DidSearchNotification, + DidStartFeaturePreviewNotification, DoubleClickedCommandType, EnsureRowRequest, GetCountsRequest, @@ -294,6 +296,7 @@ export class GraphWebviewProvider implements WebviewProvider | null | undefined; private _search: GitSearch | undefined; @@ -317,6 +320,7 @@ export class GraphWebviewProvider implements WebviewProvider { if (this._etag !== this.container.git.etag) { if (this._discovering != null) { @@ -916,6 +920,17 @@ export class GraphWebviewProvider implements WebviewProvider({ args: { 0: e => e.toString() } }) private onRepositoryChanged(e: RepositoryChangeEvent) { if ( @@ -1874,6 +1889,21 @@ export class GraphWebviewProvider implements WebviewProvider>[0] | undefined, + featurePreview: FeaturePreview, + ) { + return (access?.allowed ?? false) !== false || isFeaturePreviewActive(featurePreview); + } + private getGraphItemContext(context: unknown): unknown | undefined { const item = typeof context === 'string' ? JSON.parse(context) : context; // Add the `webview` prop to the context if its missing (e.g. when this context doesn't come through via the context menus) @@ -2516,8 +2553,11 @@ export class GraphWebviewProvider implements WebviewProvider(scope, 'didFetch'); +export interface DidStartFeaturePreviewParams { + featurePreview: FeaturePreview; + allowed: boolean; +} +export const DidStartFeaturePreviewNotification = new IpcNotification( + scope, + 'featurePreview/didStart', +); + export interface ShowInCommitGraphCommandArgs { ref: GitReference; preserveFocus?: boolean; diff --git a/src/plus/workspaces/workspacesService.ts b/src/plus/workspaces/workspacesService.ts index d22de86f38a82..7b88a3d586c50 100644 --- a/src/plus/workspaces/workspacesService.ts +++ b/src/plus/workspaces/workspacesService.ts @@ -1,7 +1,6 @@ import { getSupportedWorkspacesPathMappingProvider } from '@env/providers'; import type { CancellationToken, Event, MessageItem, QuickPickItem } from 'vscode'; import { Disposable, EventEmitter, ProgressLocation, Uri, window, workspace } from 'vscode'; -import { SubscriptionState } from '../../constants.subscription'; import type { Container } from '../../container'; import type { GitRemote } from '../../git/models/remote'; import { RemoteResourceType } from '../../git/models/remoteResource'; @@ -11,6 +10,7 @@ import { log } from '../../system/decorators/log'; import { normalizePath } from '../../system/path'; import type { OpenWorkspaceLocation } from '../../system/vscode/utils'; import { openWorkspace } from '../../system/vscode/utils'; +import { isSubscriptionStatePaidOrTrial } from '../gk/account/subscription'; import type { SubscriptionChangeEvent } from '../gk/account/subscriptionService'; import type { ServerConnection } from '../gk/serverConnection'; import type { @@ -115,11 +115,7 @@ export class WorkspacesService implements Disposable { } let filteredSharedWorkspaceCount = 0; - const isPlusEnabled = - subscription.state === SubscriptionState.ProPreview || - subscription.state === SubscriptionState.ProTrial || - subscription.state === SubscriptionState.Paid; - + const isPlusEnabled = isSubscriptionStatePaidOrTrial(subscription.state); if (workspaces?.length) { for (const workspace of workspaces) { const localPath = await this._workspacesPathProvider.getCloudWorkspaceCodeWorkspacePath(workspace.id); diff --git a/src/quickpicks/items/directive.ts b/src/quickpicks/items/directive.ts index ba5b010f0dc62..74fd024935288 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 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/system/vscode/storage.ts b/src/system/vscode/storage.ts index 6e6d686f92edb..10f249c984a89 100644 --- a/src/system/vscode/storage.ts +++ b/src/system/vscode/storage.ts @@ -69,10 +69,7 @@ export class Storage implements Disposable { return this.deleteWithPrefixCore(prefix); } - async deleteWithPrefixCore( - prefix?: ExtractPrefixes, - exclude?: GlobalStorageKeys[], - ): Promise { + async deleteWithPrefixCore(prefix?: ExtractPrefixes, exclude?: RegExp): Promise { const qualifiedKeyPrefix = `${extensionPrefix}:`; for (const qualifiedKey of this.context.globalState.keys() as `${typeof extensionPrefix}:${GlobalStorageKeys}`[]) { @@ -80,7 +77,7 @@ export class Storage implements Disposable { const key = qualifiedKey.substring(qualifiedKeyPrefix.length) as GlobalStorageKeys; if (prefix == null || key === prefix || key.startsWith(`${prefix}:`)) { - if (exclude?.includes(key)) continue; + if (exclude?.test(key)) continue; await this.context.globalState.update(qualifiedKey, undefined); this._onDidChange.fire({ key: key, workspace: false }); @@ -90,7 +87,7 @@ export class Storage implements Disposable { @debug({ logThreshold: 250 }) async reset(): Promise { - return this.deleteWithPrefixCore(undefined, ['premium:subscription']); + return this.deleteWithPrefixCore(undefined, /^(premium:subscription|plus:preview:.*)$/); } @debug({ args: { 1: false }, logThreshold: 250 }) diff --git a/src/system/webview.ts b/src/system/webview.ts index 7687fc69ea2ec..4cbcc27b8e50f 100644 --- a/src/system/webview.ts +++ b/src/system/webview.ts @@ -1,7 +1,7 @@ import type { WebviewIds, WebviewViewIds } from '../constants.views'; export function createWebviewCommandLink( - command: `${WebviewIds | WebviewViewIds}.${string}`, + command: `${WebviewIds | WebviewViewIds}.${string}` | `gitlens.plus.${string}`, webviewId: WebviewIds | WebviewViewIds, webviewInstanceId: string | undefined, args?: T, diff --git a/src/uris/deepLinks/deepLinkService.ts b/src/uris/deepLinks/deepLinkService.ts index 4b1124f2f1a1c..e073d3d2316d7 100644 --- a/src/uris/deepLinks/deepLinkService.ts +++ b/src/uris/deepLinks/deepLinkService.ts @@ -647,7 +647,7 @@ export class DeepLinkService implements Disposable { )) ) { action = DeepLinkServiceAction.DeepLinkErrored; - message = 'Paid plan required to open link'; + message = 'GitLens Pro is required to open link'; break; } diff --git a/src/views/nodes/worktreesNode.ts b/src/views/nodes/worktreesNode.ts index 3d7772e4b81d0..46c6d0b8f105f 100644 --- a/src/views/nodes/worktreesNode.ts +++ b/src/views/nodes/worktreesNode.ts @@ -76,7 +76,7 @@ export class WorktreesNode extends CacheableChildrenViewNode<'worktrees', ViewsW item.contextValue = ContextValues.Worktrees; item.description = access.allowed ? undefined - : ` ${GlyphChars.Warning} Requires a trial or paid plan for use on privately-hosted repos`; + : ` ${GlyphChars.Warning} Use on privately-hosted repos requires GitLens Pro`; // TODO@eamodio `folder` icon won't work here for some reason item.iconPath = new ThemeIcon('folder-opened'); return item; 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..3bdc69196bc47 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, + DidStartFeaturePreviewNotification, } 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 [featurePreview, setFeaturePreview] = useState(state.featurePreview); + // search state const searchEl = useRef(null); const [searchQuery, setSearchQuery] = useState(undefined); @@ -318,6 +321,10 @@ export function GraphWrapper({ setStyleProps(state.theming); } break; + case DidStartFeaturePreviewNotification: + setFeaturePreview(state.featurePreview); + setAllowed(state.allowed ?? false); + break; case DidChangeAvatarsNotification: setAvatars(state.avatars); break; @@ -420,6 +427,7 @@ export function GraphWrapper({ setRepo(repos.find(item => item.path === state.selectedRepository)); // setGraphDateFormatter(getGraphDateFormatter(config)); setSubscription(state.subscription); + setFeaturePreview(state.featurePreview); const { results, resultsError } = getSearchResultModel(state); setSearchResultsError(resultsError); @@ -1530,10 +1538,22 @@ export function GraphWrapper({

diff --git a/src/webviews/apps/plus/graph/graph.scss b/src/webviews/apps/plus/graph/graph.scss index 2791dea4325e0..4b1a08300d297 100644 --- a/src/webviews/apps/plus/graph/graph.scss +++ b/src/webviews/apps/plus/graph/graph.scss @@ -979,8 +979,8 @@ gl-feature-gate gl-feature-badge { } &__gate { - // top: 34px; /* height of the header bar */ - padding-top: 34px; + // top: 40px; /* height of the header bar */ + padding-top: 40px; } &__header { diff --git a/src/webviews/apps/plus/graph/graph.tsx b/src/webviews/apps/plus/graph/graph.tsx index f215c20b40da0..6e9aa518f819b 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, + DidStartFeaturePreviewNotification, 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 DidStartFeaturePreviewNotification.is(msg): + this.state.featurePreview = msg.params.featurePreview; + this.state.allowed = msg.params.allowed; + this.setState(this.state, DidStartFeaturePreviewNotification); + 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..f79de285e5a4e 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,15 @@ 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 { + proFeaturePreviewUsages, + proTrialLengthInDays, + SubscriptionState, +} from '../../../../../constants.subscription'; import type { Source } from '../../../../../constants.telemetry'; +import type { FeaturePreview } from '../../../../../features'; +import { isFeaturePreviewExpired } from '../../../../../features'; import type { Promo } from '../../../../../plus/gk/account/promos'; import { getApplicablePromo } from '../../../../../plus/gk/account/promos'; import { pluralize } from '../../../../../system/string'; @@ -35,7 +42,7 @@ export class GlFeatureGatePlusState extends LitElement { } @container (max-width: 600px) { - :host([appearance='welcome']) gl-button { + :host([appearance='welcome']) gl-button:not(.inline) { display: block; margin-left: auto; margin-right: auto; @@ -60,6 +67,14 @@ export class GlFeatureGatePlusState extends LitElement { text-align: center; } + .actions-row { + display: flex; + gap: 0.6em; + align-items: baseline; + justify-content: center; + white-space: nowrap; + } + .hint { border-bottom: 1px dashed currentColor; } @@ -69,6 +84,12 @@ export class GlFeatureGatePlusState extends LitElement { @query('gl-button') private readonly button!: GlButton; + @property({ type: Object }) + featurePreview?: FeaturePreview; + + @property({ type: String }) + featurePreviewCommandLink?: string; + @property({ type: String }) appearance?: 'alert' | 'welcome'; @@ -81,6 +102,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()); @@ -100,6 +124,7 @@ export class GlFeatureGatePlusState extends LitElement { switch (this.state) { case SubscriptionState.VerificationRequired: return html` +

You must verify your email before you can continue.

`; + // 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.Community: - return html` - Continue + case SubscriptionState.ProPreviewExpired: + if (this.featurePreview && !isFeaturePreviewExpired(this.featurePreview)) { + return html`${this.renderFeaturePreview(this.featurePreview)}`; + } + + return html`

- 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 trialGitLens Pro. +

+

+  Try GitLens Pro or + sign in - or - sign in.

- `; - - case SubscriptionState.ProPreviewExpired: - return html` - Start Pro Trial

- Start your free ${proTrialLengthInDays}-day Pro trial to try - ${this.featureWithArticleIfNeeded ? `${this.featureWithArticleIfNeeded} and other ` : ''}Pro - features, or - sign in. -

- `; + Get ${pluralize('day', proTrialLengthInDays)} of + GitLens Pro for free — no credit card required. +

`; case SubscriptionState.ProTrialExpired: - return html` 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. -

`; + return html` +

Use on privately-hosted repos requires GitLens Pro.

+

+ Upgrade to Proor + sign in +

+

${this.renderPromo(promo)}

`; case SubscriptionState.ProTrialReactivationEligible: - return html` - Continue + return html` +

+ Continueor + sign in +

- Reactivate your Pro trial and experience + Reactivate your GitLens Pro trial and experience ${this.featureWithArticleIfNeeded ? `${this.featureWithArticleIfNeeded} and ` : ''}all the new Pro features — free for another ${pluralize('day', proTrialLengthInDays)}! -

- `; +

`; } return undefined; } + private renderFeaturePreview(featurePreview: FeaturePreview) { + const appearance = (this.appearance ?? 'alert') === 'alert' ? 'alert' : nothing; + const used = featurePreview.usages.length; + + if (used === 0) { + return html` + Continue +

+ Continue to preview + ${this.featureWithArticleIfNeeded ? `${this.featureWithArticleIfNeeded} on` : ''} privately-hosted + repos, or + sign in.
+ ${appearance !== 'alert' ? html`
` : ''} For full access to all GitLens Pro features, + start your free ${proTrialLengthInDays}-day Pro trial + — no credit card required. +

`; + } + + const left = proFeaturePreviewUsages - used; + + return html` + ${this.renderFeaturePreviewStep(featurePreview, used)} +

+ Continue Previewor + sign in +

+

+ After continuing, you will have ${pluralize('day', left, { infix: ' more ' })} to preview + ${this.featureWithArticleIfNeeded ? `${this.featureWithArticleIfNeeded} on` : ''} privately-hosted + repos.
+ ${appearance !== 'alert' ? html`
` : ''} For full access to all GitLens Pro features, + start your free ${proTrialLengthInDays}-day Pro trial + — no credit card required. +

+ `; + } + + private renderFeaturePreviewStep(featurePreview: FeaturePreview, used: number) { + switch (featurePreview.feature) { + case 'graph': + switch (used) { + case 1: + return 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 +

`; + + case 2: + return 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 +

+ `; + + default: + return html``; + } + + default: + return html``; + } + } + private renderPromo(promo: Promo | undefined) { return html``; } 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 cb7233a5f0dbb..28d57e1fc0544 100644 --- a/src/webviews/apps/plus/shared/components/home-account-content.ts +++ b/src/webviews/apps/plus/shared/components/home-account-content.ts @@ -316,10 +316,10 @@ export class GLHomeAccountContent extends LitElement { >

- Your ${getSubscriptionPlanName(this.planId)} plan provides full access to all Pro features - and the GitKraken DevEx platform, unleashing powerful Git - visualization & productivity capabilities everywhere you work: IDE, desktop, browser, - and terminal. + Your ${getSubscriptionPlanName(this.planId)} plan provides full access to all GitLens Pro + features and the GitKraken DevEx platform, unleashing + powerful Git visualization & productivity capabilities everywhere you work: IDE, + desktop, browser, and terminal.

`; @@ -367,11 +367,14 @@ export class GLHomeAccountContent extends LitElement { case SubscriptionState.ProTrialExpired: return html` `; @@ -401,15 +404,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..cd4f1d1babad1 100644 --- a/src/webviews/apps/plus/shared/components/vscode.css.ts +++ b/src/webviews/apps/plus/shared/components/vscode.css.ts @@ -2,11 +2,18 @@ import { css } from 'lit'; export const linkStyles = css` a { + border: 0; color: var(--link-foreground); + font-weight: 400; + outline: none; text-decoration: var(--link-decoration-default, none); } - a:focus { + a:focus, + a:focus-visible { outline-color: var(--focus-border); + outline-style: solid; + outline-width: 1px; + border-radius: 0.2rem; } a:hover { color: var(--link-foreground-active); diff --git a/src/webviews/apps/shared/components/feature-badge.ts b/src/webviews/apps/shared/components/feature-badge.ts index da5981a592614..59e6260d4f40b 100644 --- a/src/webviews/apps/shared/components/feature-badge.ts +++ b/src/webviews/apps/shared/components/feature-badge.ts @@ -176,13 +176,11 @@ export class GlFeatureBadge extends LitElement { } } - return this.cloud ? html`${text}☁️` : text; + return text; } private renderPopoverHeader() { - const text = html`${this.preview ? 'Preview feature' : 'Pro feature'}${this.cloud ? ' ☁️' : ''}`; + const text = html`${this.preview ? 'Preview feature' : 'Pro feature'}`; if (this.state === SubscriptionState.Paid) { return html``; @@ -192,24 +190,24 @@ export class GlFeatureBadge extends LitElement { if (this.preview) { return html``; } return html``; } if (this.preview) { return html``; } return html``; } @@ -265,7 +263,9 @@ export class GlFeatureBadge extends LitElement { content = html`

Your Pro trial has ended. You can now only use Pro features on publicly-hosted repos.

- ${this.renderUpgradeActions(html`

Please upgrade for full access to Pro features:

`)}`; + ${this.renderUpgradeActions( + html`

Please upgrade for full access to all GitLens Pro features:

`, + )}`; break; case SubscriptionState.ProTrialReactivationEligible: diff --git a/src/webviews/apps/shared/components/feature-gate.ts b/src/webviews/apps/shared/components/feature-gate.ts index 9bba2a5095de6..8dbc31bc4f3ad 100644 --- a/src/webviews/apps/shared/components/feature-gate.ts +++ b/src/webviews/apps/shared/components/feature-gate.ts @@ -2,8 +2,10 @@ 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 { FeaturePreview } from '../../../../features'; import { isSubscriptionStatePaidOrTrial } from '../../../../plus/gk/account/subscription'; import '../../plus/shared/components/feature-gate-plus-state'; +import { linkStyles } from '../../plus/shared/components/vscode.css'; declare global { interface HTMLElementTagNameMap { @@ -15,84 +17,94 @@ declare global { @customElement('gl-feature-gate') export class GlFeatureGate extends LitElement { - static override styles = css` - :host { - --background: var(--vscode-sideBar-background); - --foreground: var(--vscode-sideBar-foreground); - --link-foreground: var(--vscode-textLink-foreground); - --link-foreground-active: var(--vscode-textLink-activeForeground); - - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - overflow: auto; - z-index: 100; - - box-sizing: border-box; - } - - :host-context(body[data-placement='editor']), - :host([appearance='alert']) { - --background: transparent; - --foreground: var(--vscode-editor-foreground); - - backdrop-filter: blur(3px) saturate(0.8); - padding: 0 2rem; - } - - ::slotted(p) { - margin: revert !important; - } - - ::slotted(p:first-child) { - margin-top: 0 !important; - } - - section { - --section-foreground: var(--foreground); - --section-background: var(--background); - --section-border-color: transparent; - - display: flex; - flex-direction: column; - padding: 0 2rem 1.3rem 2rem; - background: var(--section-background); - color: var(--section-foreground); - border: 1px solid var(--section-border-color); - - height: min-content; - } - - :host-context(body[data-placement='editor']) section, - :host([appearance='alert']) section { - --section-foreground: var(--color-alert-foreground); - --section-background: var(--color-alert-infoBackground); - --section-border-color: var(--color-alert-infoBorder); - - --link-decoration-default: underline; - --link-foreground: var(--vscode-foreground); - --link-foreground-active: var(--vscode-foreground); - - border-radius: 0.3rem; - max-width: 600px; - max-height: min-content; - margin: 0.2rem auto; - padding: 1.3rem; - } - - :host-context(body[data-placement='editor']) section ::slotted(gl-button), - :host([appearance='alert']) section ::slotted(gl-button) { - display: block; - margin-left: auto; - margin-right: auto; - } - `; + static override styles = [ + linkStyles, + css` + :host { + --background: var(--vscode-sideBar-background); + --foreground: var(--vscode-sideBar-foreground); + + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + overflow: auto; + z-index: 100; + + box-sizing: border-box; + } + + :host-context(body[data-placement='editor']), + :host([appearance='alert']) { + --background: transparent; + --foreground: var(--vscode-editor-foreground); + + backdrop-filter: blur(3px) saturate(0.8); + padding: 0 2rem; + } + + ::slotted(p) { + margin: revert !important; + } + + ::slotted(p:first-child) { + margin-top: 0 !important; + } + + section { + --section-foreground: var(--foreground); + --section-background: var(--background); + --section-border-color: transparent; + + display: flex; + flex-direction: column; + padding: 0 2rem 1.3rem 2rem; + background: var(--section-background); + color: var(--section-foreground); + border: 1px solid var(--section-border-color); + + height: min-content; + } + + :host-context(body[data-placement='editor']) section, + :host([appearance='alert']) section { + --section-foreground: var(--color-alert-foreground); + --section-background: var(--color-alert-infoBackground); + --section-border-color: var(--color-alert-infoBorder); + + --link-decoration-default: underline; + --link-foreground: var(--vscode-foreground); + /* --link-foreground-active: var(--vscode-foreground); */ + + /* --link-foreground: var(--vscode-textLink-foreground); */ + --link-foreground-active: var(--vscode-textLink-activeForeground); + + border-radius: 0.3rem; + max-width: 600px; + max-height: min-content; + margin: 0.2rem auto; + padding: 1.3rem; + } + + :host-context(body[data-placement='editor']) section ::slotted(gl-button), + :host([appearance='alert']) section ::slotted(gl-button) { + display: block; + margin-left: auto; + margin-right: auto; + } + `, + ]; @property({ reflect: true }) appearance?: 'alert' | 'welcome'; + @property({ type: Object }) + featurePreview?: FeaturePreview; + + @property({ type: String }) + featurePreviewCommandLink?: string; + @property() featureWithArticleIfNeeded?: string; @@ -105,6 +117,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,16 +132,21 @@ export class GlFeatureGate extends LitElement { : 'welcome'; this.hidden = false; + return html`
- + .webroot=${this.webroot} + > + +
`; } diff --git a/walkthroughs/welcome/get-started-community.md b/walkthroughs/welcome/get-started-community.md index f0548b34acf5e..465a1051d777a 100644 --- a/walkthroughs/welcome/get-started-community.md +++ b/walkthroughs/welcome/get-started-community.md @@ -4,4 +4,4 @@ Accelerate PR reviews, gain actionable code insights with visualizations, and streamline collaboration to supercharge your Git and VS Code experience. Leverage powerful workflows with GitLens Pro that will increase productivity for you and your team. -[Get started with GitLens Pro](command:gitlens.walkthrough.plus.signUp) free for 14 days—no credit card required. +[Get started with GitLens Pro](command:gitlens.walkthrough.plus.signUp) free for 14 days — no credit card required. diff --git a/walkthroughs/welcome/welcome-in-trial-expired-eligible.md b/walkthroughs/welcome/welcome-in-trial-expired-eligible.md index c5d9de3762515..1f61654453639 100644 --- a/walkthroughs/welcome/welcome-in-trial-expired-eligible.md +++ b/walkthroughs/welcome/welcome-in-trial-expired-eligible.md @@ -6,4 +6,4 @@ Access features that accelerate PR reviews, provide actionable code visuals, and streamline collaboration to supercharge Git and VS Code. There are workflows in the walkthrough that utilize Pro features designed to further increase developer productivity for professional developers and teams. -[Get started with GitLens Pro](command:gitlens.walkthrough.plus.signUp) by creating for an account to receive 14 days free. No Credit Card Required. +[Get started with GitLens Pro](command:gitlens.walkthrough.plus.signUp) free for 14 days — no credit card required.