diff --git a/src/plus/gk/subscription/checkin.ts b/src/plus/gk/subscription/checkin.ts new file mode 100644 index 0000000..c8b0006 --- /dev/null +++ b/src/plus/gk/subscription/checkin.ts @@ -0,0 +1,189 @@ +import type { Subscription } from './subscription'; +import { getSubscriptionPlan, getSubscriptionPlanPriority, SubscriptionPlanId } from './subscription'; + +export interface GKCheckInResponse { + readonly user: GKUser; + readonly licenses: { + readonly paidLicenses: Record; + readonly effectiveLicenses: Record; + }; + readonly orgIds?: string[]; +} + +export interface GKUser { + readonly id: string; + readonly name: string; + readonly email: string; + readonly status: 'activated' | 'pending'; + readonly createdDate: string; + readonly firstGitLensCheckIn?: string; +} + +export interface GKLicense { + readonly latestStatus: 'active' | 'canceled' | 'cancelled' | 'expired' | 'in_trial' | 'non_renewing' | 'trial'; + readonly latestStartDate: string; + readonly latestEndDate: string; + readonly organizationId: string | undefined; + readonly reactivationCount?: number; +} + +export type GKLicenseType = + | 'gitlens-pro' + | 'gitlens-teams' + | 'gitlens-hosted-enterprise' + | 'gitlens-self-hosted-enterprise' + | 'gitlens-standalone-enterprise' + | 'bundle-pro' + | 'bundle-teams' + | 'bundle-hosted-enterprise' + | 'bundle-self-hosted-enterprise' + | 'bundle-standalone-enterprise' + | 'gitkraken_v1-pro' + | 'gitkraken_v1-teams' + | 'gitkraken_v1-hosted-enterprise' + | 'gitkraken_v1-self-hosted-enterprise' + | 'gitkraken_v1-standalone-enterprise'; + +export function getSubscriptionFromCheckIn(data: GKCheckInResponse): Partial { + const account: Subscription['account'] = { + id: data.user.id, + name: data.user.name, + email: data.user.email, + verified: data.user.status === 'activated', + createdOn: data.user.createdDate, + organizationIds: data.orgIds ?? [], + }; + + const effectiveLicenses = Object.entries(data.licenses.effectiveLicenses) as [GKLicenseType, GKLicense][]; + const paidLicenses = Object.entries(data.licenses.paidLicenses) as [GKLicenseType, GKLicense][]; + + let actual: Subscription['plan']['actual'] | undefined; + if (paidLicenses.length > 0) { + if (paidLicenses.length > 1) { + paidLicenses.sort( + (a, b) => + getSubscriptionPlanPriority(convertLicenseTypeToPlanId(b[0])) + + licenseStatusPriority(b[1].latestStatus) - + (getSubscriptionPlanPriority(convertLicenseTypeToPlanId(a[0])) + + licenseStatusPriority(a[1].latestStatus)), + ); + } + + const [licenseType, license] = paidLicenses[0]; + actual = getSubscriptionPlan( + convertLicenseTypeToPlanId(licenseType), + isBundleLicenseType(licenseType), + license.reactivationCount ?? 0, + license.organizationId, + new Date(license.latestStartDate), + new Date(license.latestEndDate), + license.latestStatus === 'cancelled', + ); + } + + if (actual == null) { + actual = getSubscriptionPlan( + SubscriptionPlanId.FreePlus, + false, + 0, + undefined, + data.user.firstGitLensCheckIn != null + ? new Date(data.user.firstGitLensCheckIn) + : data.user.createdDate != null + ? new Date(data.user.createdDate) + : undefined, + ); + } + + let effective: Subscription['plan']['effective'] | undefined; + if (effectiveLicenses.length > 0) { + if (effectiveLicenses.length > 1) { + effectiveLicenses.sort( + (a, b) => + getSubscriptionPlanPriority(convertLicenseTypeToPlanId(b[0])) + + licenseStatusPriority(b[1].latestStatus) - + (getSubscriptionPlanPriority(convertLicenseTypeToPlanId(a[0])) + + licenseStatusPriority(a[1].latestStatus)), + ); + } + + const [licenseType, license] = effectiveLicenses[0]; + effective = getSubscriptionPlan( + convertLicenseTypeToPlanId(licenseType), + isBundleLicenseType(licenseType), + license.reactivationCount ?? 0, + license.organizationId, + new Date(license.latestStartDate), + new Date(license.latestEndDate), + license.latestStatus === 'cancelled', + ); + } + + if (effective == null || getSubscriptionPlanPriority(actual.id) >= getSubscriptionPlanPriority(effective.id)) { + effective = { ...actual }; + } + + return { + plan: { + actual: actual, + effective: effective, + }, + account: account, + }; +} + +function convertLicenseTypeToPlanId(licenseType: GKLicenseType): SubscriptionPlanId { + switch (licenseType) { + case 'gitlens-pro': + case 'bundle-pro': + case 'gitkraken_v1-pro': + return SubscriptionPlanId.Pro; + case 'gitlens-teams': + case 'bundle-teams': + case 'gitkraken_v1-teams': + return SubscriptionPlanId.Teams; + case 'gitlens-hosted-enterprise': + case 'gitlens-self-hosted-enterprise': + case 'gitlens-standalone-enterprise': + case 'bundle-hosted-enterprise': + case 'bundle-self-hosted-enterprise': + case 'bundle-standalone-enterprise': + case 'gitkraken_v1-hosted-enterprise': + case 'gitkraken_v1-self-hosted-enterprise': + case 'gitkraken_v1-standalone-enterprise': + return SubscriptionPlanId.Enterprise; + default: + return SubscriptionPlanId.FreePlus; + } +} + +function isBundleLicenseType(licenseType: GKLicenseType): boolean { + switch (licenseType) { + case 'bundle-pro': + case 'bundle-teams': + case 'bundle-hosted-enterprise': + case 'bundle-self-hosted-enterprise': + case 'bundle-standalone-enterprise': + return true; + default: + return false; + } +} + +function licenseStatusPriority(status: GKLicense['latestStatus']): number { + switch (status) { + case 'active': + return 100; + case 'expired': + case 'cancelled': + return -100; + case 'in_trial': + case 'trial': + return 1; + case 'canceled': + case 'non_renewing': + return 0; + default: + return -200; + } +} diff --git a/src/plus/gk/subscription/subscription.ts b/src/plus/gk/subscription/subscription.ts index a5dcd8c..06aa94a 100644 --- a/src/plus/gk/subscription/subscription.ts +++ b/src/plus/gk/subscription/subscription.ts @@ -251,3 +251,7 @@ export function hasAccountFromSubscriptionState(state: SubscriptionState | undef state !== SubscriptionState.FreeInPreviewTrial ); } + +export function assertSubscriptionState( + subscription: Optional, +): asserts subscription is Subscription {} diff --git a/src/plus/gk/subscription/subscriptionService.ts b/src/plus/gk/subscription/subscriptionService.ts index eb1e6f6..cf6306a 100644 --- a/src/plus/gk/subscription/subscriptionService.ts +++ b/src/plus/gk/subscription/subscriptionService.ts @@ -42,12 +42,14 @@ import { satisfies } from '../../../system/version'; import { authenticationProviderId, authenticationProviderScopes } from '../authenticationProvider'; import type { ServerConnection } from '../serverConnection'; import { ensurePlusFeaturesEnabled } from '../utils'; +import type { GKCheckInResponse } from './checkin'; +import { getSubscriptionFromCheckIn } from './checkin'; import type { Subscription } from './subscription'; import { + assertSubscriptionState, computeSubscriptionState, getSubscriptionPlan, getSubscriptionPlanName, - getSubscriptionPlanPriority, getSubscriptionTimeRemaining, getTimeRemaining, isSubscriptionExpired, @@ -575,8 +577,8 @@ export class SubscriptionService implements Disposable { throw new AccountValidationError('Unable to validate account', undefined, rsp.status, rsp.statusText); } - const data: GKLicenseInfo = await rsp.json(); - this.validateSubscription(data); + const data: GKCheckInResponse = await rsp.json(); + this.validateAndUpdateSubscription(data); } catch (ex) { Logger.error(ex, scope); debugger; @@ -605,94 +607,14 @@ export class SubscriptionService implements Disposable { } @debug() - private validateSubscription(data: GKLicenseInfo) { - const account: Subscription['account'] = { - id: data.user.id, - name: data.user.name, - email: data.user.email, - verified: data.user.status === 'activated', - createdOn: data.user.createdDate, - organizationIds: data.orgIds ?? [], - }; - - const effectiveLicenses = Object.entries(data.licenses.effectiveLicenses) as [GKLicenseType, GKLicense][]; - const paidLicenses = Object.entries(data.licenses.paidLicenses) as [GKLicenseType, GKLicense][]; - - let actual: Subscription['plan']['actual'] | undefined; - if (paidLicenses.length > 0) { - if (paidLicenses.length > 1) { - paidLicenses.sort( - (a, b) => - getSubscriptionPlanPriority(convertLicenseTypeToPlanId(b[0])) + - licenseStatusPriority(b[1].latestStatus) - - (getSubscriptionPlanPriority(convertLicenseTypeToPlanId(a[0])) + - licenseStatusPriority(a[1].latestStatus)), - ); - } - - const [licenseType, license] = paidLicenses[0]; - actual = getSubscriptionPlan( - convertLicenseTypeToPlanId(licenseType), - isBundleLicenseType(licenseType), - license.reactivationCount ?? 0, - license.organizationId, - new Date(license.latestStartDate), - new Date(license.latestEndDate), - license.latestStatus === 'cancelled', - ); - } - - if (actual == null) { - actual = getSubscriptionPlan( - SubscriptionPlanId.FreePlus, - false, - 0, - undefined, - data.user.firstGitLensCheckIn != null - ? new Date(data.user.firstGitLensCheckIn) - : data.user.createdDate != null - ? new Date(data.user.createdDate) - : undefined, - ); - } - - let effective: Subscription['plan']['effective'] | undefined; - if (effectiveLicenses.length > 0) { - if (effectiveLicenses.length > 1) { - effectiveLicenses.sort( - (a, b) => - getSubscriptionPlanPriority(convertLicenseTypeToPlanId(b[0])) + - licenseStatusPriority(b[1].latestStatus) - - (getSubscriptionPlanPriority(convertLicenseTypeToPlanId(a[0])) + - licenseStatusPriority(a[1].latestStatus)), - ); - } - - const [licenseType, license] = effectiveLicenses[0]; - effective = getSubscriptionPlan( - convertLicenseTypeToPlanId(licenseType), - isBundleLicenseType(licenseType), - license.reactivationCount ?? 0, - license.organizationId, - new Date(license.latestStartDate), - new Date(license.latestEndDate), - license.latestStatus === 'cancelled', - ); - } - - if (effective == null || getSubscriptionPlanPriority(actual.id) >= getSubscriptionPlanPriority(effective.id)) { - effective = { ...actual }; - } + private validateAndUpdateSubscription(data: GKCheckInResponse) { + const subscription = getSubscriptionFromCheckIn(data); this._lastValidatedDate = new Date(); this.changeSubscription( { ...this._subscription, - plan: { - actual: actual, - effective: effective, - }, - account: account, + ...subscription, }, { store: true }, ); @@ -1094,104 +1016,3 @@ function flattenSubscription(subscription: Optional | und 'subscription.state': subscription.state, }; } - -function assertSubscriptionState(subscription: Optional): asserts subscription is Subscription {} - -interface GKLicenseInfo { - readonly user: GKUser; - readonly licenses: { - readonly paidLicenses: Record; - readonly effectiveLicenses: Record; - }; - readonly orgIds?: string[]; -} - -interface GKLicense { - readonly latestStatus: 'active' | 'canceled' | 'cancelled' | 'expired' | 'in_trial' | 'non_renewing' | 'trial'; - readonly latestStartDate: string; - readonly latestEndDate: string; - readonly organizationId: string | undefined; - readonly reactivationCount?: number; -} - -type GKLicenseType = - | 'gitlens-pro' - | 'gitlens-teams' - | 'gitlens-hosted-enterprise' - | 'gitlens-self-hosted-enterprise' - | 'gitlens-standalone-enterprise' - | 'bundle-pro' - | 'bundle-teams' - | 'bundle-hosted-enterprise' - | 'bundle-self-hosted-enterprise' - | 'bundle-standalone-enterprise' - | 'gitkraken_v1-pro' - | 'gitkraken_v1-teams' - | 'gitkraken_v1-hosted-enterprise' - | 'gitkraken_v1-self-hosted-enterprise' - | 'gitkraken_v1-standalone-enterprise'; - -interface GKUser { - readonly id: string; - readonly name: string; - readonly email: string; - readonly status: 'activated' | 'pending'; - readonly createdDate: string; - readonly firstGitLensCheckIn?: string; -} - -function convertLicenseTypeToPlanId(licenseType: GKLicenseType): SubscriptionPlanId { - switch (licenseType) { - case 'gitlens-pro': - case 'bundle-pro': - case 'gitkraken_v1-pro': - return SubscriptionPlanId.Pro; - case 'gitlens-teams': - case 'bundle-teams': - case 'gitkraken_v1-teams': - return SubscriptionPlanId.Teams; - case 'gitlens-hosted-enterprise': - case 'gitlens-self-hosted-enterprise': - case 'gitlens-standalone-enterprise': - case 'bundle-hosted-enterprise': - case 'bundle-self-hosted-enterprise': - case 'bundle-standalone-enterprise': - case 'gitkraken_v1-hosted-enterprise': - case 'gitkraken_v1-self-hosted-enterprise': - case 'gitkraken_v1-standalone-enterprise': - return SubscriptionPlanId.Enterprise; - default: - return SubscriptionPlanId.FreePlus; - } -} - -function isBundleLicenseType(licenseType: GKLicenseType): boolean { - switch (licenseType) { - case 'bundle-pro': - case 'bundle-teams': - case 'bundle-hosted-enterprise': - case 'bundle-self-hosted-enterprise': - case 'bundle-standalone-enterprise': - return true; - default: - return false; - } -} - -function licenseStatusPriority(status: GKLicense['latestStatus']): number { - switch (status) { - case 'active': - return 100; - case 'expired': - case 'cancelled': - return -100; - case 'in_trial': - case 'trial': - return 1; - case 'canceled': - case 'non_renewing': - return 0; - default: - return -200; - } -}