diff --git a/package.json b/package.json index 06f6249194bc8..524c9d2d8f065 100644 --- a/package.json +++ b/package.json @@ -17548,7 +17548,7 @@ { "view": "gitlens.views.worktrees", "contents": "[Upgrade to Pro](command:gitlens.plus.upgrade?%7B%22source%22%3A%22worktrees%22%7D)", - "when": "gitlens:plus:required && gitlens:plus:state == 4" + "when": "gitlens:plus:required && (gitlens:plus:state == 4 || gitlens:plus:state == 7)" }, { "view": "gitlens.views.worktrees", @@ -17558,12 +17558,12 @@ { "view": "gitlens.views.worktrees", "contents": "Launchpad sale: Save 75% or more on GitLens Pro", - "when": "gitlens:plus:required && gitlens:plus:state == 4 && gitlens:promo == launchpad-extended" + "when": "gitlens:plus:required && (gitlens:plus:state == 4 || gitlens:plus:state == 7) && gitlens:promo == launchpad-extended" }, { "view": "gitlens.views.worktrees", "contents": "Limited-time sale: Save up to 80% on GitLen Pro", - "when": "gitlens:plus:required && gitlens:plus:state == 4 && gitlens:promo == devexdays" + "when": "gitlens:plus:required && (gitlens:plus:state == 4 ||  gitlens:plus:state == 7) && gitlens:promo == devexdays" }, { "view": "gitlens.views.worktrees", @@ -17845,6 +17845,15 @@ }, "when": "gitlens:plus:state == 6" }, + { + "id": "pro-expired-reactivate", + "title": "Reactivate Pro Power-up", + "description": "Reactivate your Pro account and experience all the new [Pro features](https://gitkraken.com/gitlens/pro-features?utm_source=gitlens-extension&utm_medium=in-app-links) and the full [GitKraken DevEx platform](https://gitkraken.com/devex?utm_source=gitlens-extension&utm_medium=in-app-links).\n\n[Reactivate Pro](command:gitlens.plus.upgrade?%7B%22source%22%3A%22walkthrough%22%7D)\n\n**Pro Features**\n$(gitlens-graph)  [Commit Graph](command:gitlens.openWalkthrough?%7B%22step%22%3A%22visualize%22,%22source%22%3A%22walkthrough%22%7D) — visualize your repository and keep track of all work in progress\n$(rocket)  [Launchpad](command:gitlens.openWalkthrough?%7B%22step%22%3A%22launchpad%22,%22source%22%3A%22walkthrough%22%7D) — stay focused and keep your team unblocked\n$(gitlens-code-suggestion)  [Code Suggest](command:gitlens.openWalkthrough?%7B%22step%22%3A%22code-collab%22,%22source%22%3A%22walkthrough%22%7D) — free your code reviews from unnecessary restrictions\n$(gitlens-cloud-patch)  [Cloud Patches](command:gitlens.openWalkthrough?%7B%22step%22%3A%22code-collab%22,%22source%22%3A%22walkthrough%22%7D) — easily and securely share code with your teammates\n$(gitlens-worktrees-view)  **Worktrees** — work on multiple branches simultaneously\n$(gitlens-workspaces-view)  **Workspaces** — group and manage multiple repositories together\n$(graph-scatter)  [Visual File History](command:gitlens.openWalkthrough?%7B%22step%22%3A%22visualize%22,%22source%22%3A%22walkthrough%22%7D) — visualize the evolution of a file and quickly identify when the most impactful changes were made and by whom", + "media": { + "markdown": "walkthroughs/welcome/pro-reactivate.md" + }, + "when": "gitlens:plus:state == 7" + }, { "id": "visualize", "title": "Visualize with Commit Graph & Visual File History", diff --git a/src/constants.subscription.ts b/src/constants.subscription.ts index 9e8e644a2b18c..794f54f44ba6a 100644 --- a/src/constants.subscription.ts +++ b/src/constants.subscription.ts @@ -30,4 +30,6 @@ export const enum SubscriptionState { ProTrialReactivationEligible = 5, /** Indicates a Pro/Teams/Enterprise paid user */ Paid = 6, + /** Indicates a Paid user who's license has expired */ + PaidExpired = 7, } diff --git a/src/constants.telemetry.ts b/src/constants.telemetry.ts index f0e7b91a4a212..9ef9b1d5e5366 100644 --- a/src/constants.telemetry.ts +++ b/src/constants.telemetry.ts @@ -395,6 +395,7 @@ export type TelemetryEvents = { | 'pro-trial' | 'pro-upgrade' | 'pro-reactivate' + | 'pro-expired-reactivate' | 'pro-paid' | 'visualize' | 'launchpad' diff --git a/src/constants.ts b/src/constants.ts index 727e2689dbea9..67a8bb88f693b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -179,6 +179,7 @@ export type WalkthroughSteps = | 'pro-trial' | 'pro-upgrade' | 'pro-reactivate' + | 'pro-expired-reactivate' | 'pro-paid' | 'visualize' | 'launchpad' diff --git a/src/git/gitProviderService.ts b/src/git/gitProviderService.ts index 29a22a7fe9672..88e9891779852 100644 --- a/src/git/gitProviderService.ts +++ b/src/git/gitProviderService.ts @@ -20,7 +20,7 @@ import type { Container } from '../container'; import { AccessDeniedError, CancellationError, ProviderNotFoundError, ProviderNotSupportedError } from '../errors'; import type { FeatureAccess, Features, PlusFeatures, RepoFeatureAccess } from '../features'; import type { Subscription } from '../plus/gk/account/subscription'; -import { isSubscriptionPaidPlan } from '../plus/gk/account/subscription'; +import { isSubscriptionExpired, isSubscriptionPaidPlan } from '../plus/gk/account/subscription'; import type { SubscriptionChangeEvent } from '../plus/gk/account/subscriptionService'; import type { HostingIntegration } from '../plus/integrations/integration'; import type { RepoComparisonKey } from '../repositories'; @@ -773,7 +773,10 @@ export class GitProviderService implements Disposable { const plan = subscription.plan.effective.id; if (isSubscriptionPaidPlan(plan)) { - return { allowed: subscription.account?.verified !== false, subscription: { current: subscription } }; + return { + allowed: subscription.account?.verified !== false && !isSubscriptionExpired(subscription), + subscription: { current: subscription }, + }; } if (feature === 'launchpad') { diff --git a/src/plus/gk/account/promos.ts b/src/plus/gk/account/promos.ts index a6e0d9ecdfb2e..4bb897788de3d 100644 --- a/src/plus/gk/account/promos.ts +++ b/src/plus/gk/account/promos.ts @@ -30,6 +30,7 @@ const promos: Promo[] = [ SubscriptionState.ProTrial, SubscriptionState.ProTrialExpired, SubscriptionState.ProTrialReactivationEligible, + SubscriptionState.PaidExpired, ], startsOn: new Date('2024-09-27T06:59:00.000Z').getTime(), expiresOn: new Date('2024-10-14T06:59:00.000Z').getTime(), @@ -48,6 +49,7 @@ const promos: Promo[] = [ SubscriptionState.ProTrial, SubscriptionState.ProTrialExpired, SubscriptionState.ProTrialReactivationEligible, + SubscriptionState.PaidExpired, ], startsOn: new Date('2024-10-13T06:59:00.000Z').getTime(), expiresOn: new Date('2024-11-05T06:59:00.000Z').getTime(), @@ -66,6 +68,7 @@ const promos: Promo[] = [ SubscriptionState.ProTrial, SubscriptionState.ProTrialExpired, SubscriptionState.ProTrialReactivationEligible, + SubscriptionState.PaidExpired, ], command: { tooltip: 'Limited-Time sale: Save 33% or more on your 1st seat of Pro. See your special price' }, quickpick: { diff --git a/src/plus/gk/account/subscription.ts b/src/plus/gk/account/subscription.ts index bf5c009ad2d36..8def7ea0e77a6 100644 --- a/src/plus/gk/account/subscription.ts +++ b/src/plus/gk/account/subscription.ts @@ -40,6 +40,7 @@ export interface SubscriptionPlan { readonly startedOn: string; readonly expiresOn?: string | undefined; readonly organizationId: string | undefined; + readonly isTrial: boolean; } export interface SubscriptionAccount { @@ -93,6 +94,8 @@ export function getSubscriptionStateString(state: SubscriptionState | undefined) return 'trial-reactivation-eligible'; case SubscriptionState.Paid: return 'paid'; + case SubscriptionState.PaidExpired: + return 'paid-expired'; default: return 'unknown'; } @@ -123,6 +126,10 @@ export function computeSubscriptionState(subscription: Optional): boolean { - return subscription.plan.actual.id !== subscription.plan.effective.id; + return subscription.plan.effective.isTrial; } export function isSubscriptionInProTrial(subscription: Optional): boolean { diff --git a/src/plus/gk/account/subscriptionService.ts b/src/plus/gk/account/subscriptionService.ts index 903a3fd621113..1933d0db110a7 100644 --- a/src/plus/gk/account/subscriptionService.ts +++ b/src/plus/gk/account/subscriptionService.ts @@ -311,6 +311,12 @@ export class SubscriptionService implements Disposable { step: 'pro-paid', }); break; + case SubscriptionState.PaidExpired: + void executeCommand(Commands.OpenWalkthrough, { + ...source, + step: 'pro-expired-reactivate', + }); + break; } } @@ -716,7 +722,6 @@ export class SubscriptionService implements Disposable { const days = proPreviewLengthInDays; const subscription = getPreviewSubscription(days, this._subscription); this.changeSubscription(subscription); - setTimeout(async () => { const confirm: MessageItem = { title: 'Continue' }; const learn: MessageItem = { title: 'See Pro Features' }; @@ -1229,6 +1234,7 @@ export class SubscriptionService implements Disposable { undefined, new Date(subscription.previewTrial.startedOn), new Date(subscription.previewTrial.expiresOn), + true, ), }, }; diff --git a/src/plus/gk/checkin.ts b/src/plus/gk/checkin.ts index 9d4bbf031c03f..3a04ee38ade42 100644 --- a/src/plus/gk/checkin.ts +++ b/src/plus/gk/checkin.ts @@ -1,7 +1,7 @@ import { SubscriptionPlanId } from '../../constants.subscription'; import type { Organization } from './account/organization'; import type { Subscription } from './account/subscription'; -import { getSubscriptionPlan, getSubscriptionPlanPriority } from './account/subscription'; +import { getSubscriptionPlan, getSubscriptionPlanPriority, getTimeRemaining } from './account/subscription'; export type GKLicenses = Partial>; @@ -69,9 +69,6 @@ export function getSubscriptionFromCheckIn( let effectiveLicenses = Object.entries(data.licenses.effectiveLicenses) as [GKLicenseType, GKLicense][]; let paidLicenses = Object.entries(data.licenses.paidLicenses) as [GKLicenseType, GKLicense][]; - paidLicenses = paidLicenses.filter( - license => license[1].latestStatus !== 'expired' && license[1].latestStatus !== 'cancelled', - ); if (paidLicenses.length > 1) { paidLicenses.sort( (a, b) => @@ -81,6 +78,7 @@ export function getSubscriptionFromCheckIn( licenseStatusPriority(a[1].latestStatus)), ); } + if (effectiveLicenses.length > 1) { effectiveLicenses.sort( (a, b) => @@ -133,13 +131,21 @@ export function getSubscriptionFromCheckIn( organizationId != null ? paidLicensesByOrganizationId.get(organizationId) ?? bestPaidLicense : bestPaidLicense; if (chosenPaidLicense != null) { const [licenseType, license] = chosenPaidLicense; + const latestStartDate = new Date(license.latestStartDate); + const latestEndDate = new Date(license.latestEndDate); + const today = new Date(); actual = getSubscriptionPlan( convertLicenseTypeToPlanId(licenseType), isBundleLicenseType(licenseType), license.reactivationCount ?? 0, license.organizationId, - new Date(license.latestStartDate), - new Date(license.latestEndDate), + latestStartDate, + latestEndDate, + undefined, + undefined, + (license.latestStatus === 'in_trial' || license.latestStatus === 'trial') && + latestEndDate > today && + today > latestStartDate, ); } @@ -157,6 +163,7 @@ export function getSubscriptionFromCheckIn( undefined, undefined, data.nextOptInDate, + false, ); } @@ -167,6 +174,9 @@ export function getSubscriptionFromCheckIn( : bestEffectiveLicense; if (chosenEffectiveLicense != null) { const [licenseType, license] = chosenEffectiveLicense; + const latestStartDate = new Date(license.latestStartDate); + const latestEndDate = new Date(license.latestEndDate); + const today = new Date(); effective = getSubscriptionPlan( convertLicenseTypeToPlanId(licenseType), isBundleLicenseType(licenseType), @@ -176,10 +186,19 @@ export function getSubscriptionFromCheckIn( new Date(license.latestEndDate), license.latestStatus === 'cancelled', license.nextOptInDate ?? data.nextOptInDate, + (license.latestStatus === 'in_trial' || license.latestStatus === 'trial') && + latestEndDate > today && + today > latestStartDate, ); } - if (effective == null || getSubscriptionPlanPriority(actual.id) >= getSubscriptionPlanPriority(effective.id)) { + const remainingTime = getTimeRemaining(actual.expiresOn); + if ( + effective == null || + (getSubscriptionPlanPriority(actual.id) >= getSubscriptionPlanPriority(effective.id) && + remainingTime != null && + remainingTime > 0) + ) { effective = { ...actual }; } 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..8ba91b2efc4f2 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 @@ -165,6 +165,19 @@ export class GlFeatureGatePlusState extends LitElement { features.

`; + case SubscriptionState.PaidExpired: + return html` Upgrade to Pro + ${this.renderPromo(promo)} +

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

`; + case SubscriptionState.ProTrialReactivationEligible: return html` Please upgrade for full access to Pro features:

`)}`; break; + case SubscriptionState.PaidExpired: + content = html`

+ Your Pro license has expired. You can now only use Pro features on publicly-hosted repos. +

+ ${this.renderUpgradeActions(html`

Please upgrade for full access to Pro features:

`)}`; + break; + case SubscriptionState.ProTrialReactivationEligible: content = html`

Reactivate your Pro trial and experience all the new Pro features — free for another