<commit_before>import {
	authentication,
	AuthenticationSession,
	AuthenticationSessionsChangeEvent,
	version as codeVersion,
	commands,
	Disposable,
	env,
	Event,
	EventEmitter,
	MarkdownString,
	StatusBarAlignment,
	StatusBarItem,
	ThemeColor,
	Uri,
	window,
} from 'vscode';
import { fetch } from '@env/fetch';
import { getPlatform } from '@env/platform';
import { Commands, ContextKeys } from '../../constants';
import type { Container } from '../../container';
import { setContext } from '../../context';
import { AccountValidationError } from '../../errors';
import { RepositoriesChangeEvent } from '../../git/gitProviderService';
import { Logger } from '../../logger';
import { StorageKeys, WorkspaceStorageKeys } from '../../storage';
import {
	computeSubscriptionState,
	getSubscriptionPlan,
	getSubscriptionPlanPriority,
	getSubscriptionTimeRemaining,
	getTimeRemaining,
	isSubscriptionExpired,
	isSubscriptionPaidPlan,
	isSubscriptionTrial,
	Subscription,
	SubscriptionPlanId,
	SubscriptionState,
} from '../../subscription';
import { executeCommand } from '../../system/command';
import { createFromDateDelta } from '../../system/date';
import { gate } from '../../system/decorators/gate';
import { debug, log } from '../../system/decorators/log';
import { memoize } from '../../system/decorators/memoize';
import { once } from '../../system/function';
import { pluralize } from '../../system/string';
import { openWalkthrough } from '../../system/utils';

// TODO: What user-agent should we use?
const userAgent = 'Visual-Studio-Code-GitLens';

export interface SubscriptionChangeEvent {
	readonly current: Subscription;
	readonly previous: Subscription;
	readonly etag: number;
}

export class SubscriptionService implements Disposable {
	private static authenticationProviderId = 'gitkraken';
	private static authenticationScopes = ['gitlens'];

	private _onDidChange = new EventEmitter<SubscriptionChangeEvent>();
	get onDidChange(): Event<SubscriptionChangeEvent> {
		return this._onDidChange.event;
	}

	private _disposable: Disposable;
	private _subscription!: Subscription;
	private _statusBarSubscription: StatusBarItem | undefined;

	constructor(private readonly container: Container) {
		this._disposable = Disposable.from(
			once(container.onReady)(this.onReady, this),
			authentication.onDidChangeSessions(this.onAuthenticationChanged, this),
		);

		this.changeSubscription(this.getStoredSubscription(), true);
		setTimeout(() => void this.ensureSession(false), 10000);
	}

	dispose(): void {
		this._statusBarSubscription?.dispose();

		this._disposable.dispose();
	}

	private onAuthenticationChanged(e: AuthenticationSessionsChangeEvent): void {
		if (e.provider.id !== SubscriptionService.authenticationProviderId) return;

		void this.ensureSession(false, true);
	}

	@memoize()
	private get baseApiUri(): Uri {
		const { env } = this.container;
		if (env === 'staging') {
			return Uri.parse('https://stagingapi.gitkraken.com');
		}

		if (env === 'dev' || this.container.debugging) {
			return Uri.parse('https://devapi.gitkraken.com');
		}

		return Uri.parse('https://api.gitkraken.com');
	}

	@memoize()
	private get baseAccountUri(): Uri {
		const { env } = this.container;
		if (env === 'staging') {
			return Uri.parse('https://stagingaccount.gitkraken.com');
		}

		if (env === 'dev' || this.container.debugging) {
			return Uri.parse('https://devaccount.gitkraken.com');
		}

		return Uri.parse('https://account.gitkraken.com');
	}

	@memoize()
	private get baseSiteUri(): Uri {
		const { env } = this.container;
		if (env === 'staging') {
			return Uri.parse('https://staging.gitkraken.com');
		}

		if (env === 'dev' || this.container.debugging) {
			return Uri.parse('https://dev.gitkraken.com');
		}

		return Uri.parse('https://gitkraken.com');
	}

	private get connectedKey(): `${WorkspaceStorageKeys.ConnectedPrefix}${string}` {
		return `${WorkspaceStorageKeys.ConnectedPrefix}gitkraken`;
	}

	private _etag: number = 0;
	get etag(): number {
		return this._etag;
	}

	private onReady() {
		this._disposable = Disposable.from(
			this._disposable,
			this.container.git.onDidChangeRepositories(this.onRepositoriesChanged, this),
			...this.registerCommands(),
		);
		this.updateContext();
	}

	private onRepositoriesChanged(_e: RepositoriesChangeEvent): void {
		this.updateContext();
	}

	private registerCommands(): Disposable[] {
		void this.container.viewCommands;

		return [
			commands.registerCommand(Commands.PremiumLearn, () => this.learn()),
			commands.registerCommand(Commands.PremiumLogin, () => this.loginOrSignUp()),
			commands.registerCommand(Commands.PremiumLoginOrSignUp, () => this.loginOrSignUp()),
			commands.registerCommand(Commands.PremiumSignUp, () => this.loginOrSignUp()),
			commands.registerCommand(Commands.PremiumLogout, () => this.logout()),

			commands.registerCommand(Commands.PremiumStartPreviewTrial, () => this.startPreviewTrial()),
			commands.registerCommand(Commands.PremiumPurchase, () => this.purchase()),

			commands.registerCommand(Commands.PremiumResendVerification, () => this.resendVerification()),
			commands.registerCommand(Commands.PremiumValidate, () => this.validate()),

			commands.registerCommand(Commands.PremiumShowPlans, () => this.showPlans()),

			commands.registerCommand('gitlens.premium.reset', () => this.logout(true)),
		];
	}

	async getSubscription(): Promise<Subscription> {
		void (await this.ensureSession(false));
		return this._subscription;
	}

	@debug()
	learn(): void {
		void openWalkthrough(this.container.context.extension.id, 'gitlens.premium');
	}

	@gate()
	@log()
	async loginOrSignUp(): Promise<boolean> {
		void this.showHomeView();

		await this.container.storage.deleteWorkspace(this.connectedKey);

		const session = await this.ensureSession(true);
		const loggedIn = Boolean(session);
		if (loggedIn) {
			const {
				account,
				plan: { actual, effective },
			} = this._subscription;

			if (account?.verified === false) {
				const confirm = { title: 'Resend Verification' };
				const cancel = { title: 'Cancel' };
				const result = await window.showInformationMessage(
					`Before you can access your ${actual.name} account, you must verify your email address.`,
					confirm,
					cancel,
				);

				if (result === confirm) {
					void this.resendVerification();
				}
			} else if (isSubscriptionTrial(this._subscription)) {
				const remaining = getSubscriptionTimeRemaining(this._subscription, 'days');
				void window.showInformationMessage(
					`You are now signed in to your ${actual.name} account. There are ${pluralize(
						'day',
						remaining ?? 0,
					)} remaining in your ${
						effective.name
					} trial which grants you access to premium features for private repos.`,
					'OK',
				);
			} else {
				void window.showInformationMessage(`You are now signed in to your ${actual.name} account.`, 'OK');
			}
		}
		return loggedIn;
	}

	@gate()
	@log()
	logout(reset: boolean = false): void {
		this._sessionPromise = undefined;
		this._session = undefined;
		void this.container.storage.storeWorkspace(this.connectedKey, false);

		if (reset && this.container.debugging) {
			this.changeSubscription(undefined);

			return;
		}

		this.changeSubscription({
			...this._subscription,
			plan: {
				actual: getSubscriptionPlan(SubscriptionPlanId.Free),
				effective: getSubscriptionPlan(SubscriptionPlanId.Free),
			},
			account: undefined,
		});
	}

	@log()
	async purchase(): Promise<void> {
		if (this._subscription.account == null) {
			void this.showPlans();
		} else {
			void env.openExternal(
				Uri.joinPath(this.baseAccountUri, 'create-organization').with({ query: 'product=gitlens' }),
			);
		}
		await this.showHomeView();
	}

	@gate()
	@log()
	async resendVerification(): Promise<void> {
		if (this._subscription.account?.verified) return;

		const cc = Logger.getCorrelationContext();

		void this.showHomeView();

		const session = await this.ensureSession(false);
		if (session == null) return;

		try {
			const rsp = await fetch(Uri.joinPath(this.baseApiUri, 'resend-email').toString(), {
				method: 'POST',
				headers: {
					Authorization: `Bearer ${session.accessToken}`,
					'User-Agent': userAgent,
					'Content-Type': 'application/json',
				},
				body: JSON.stringify({ id: session.account.id }),
			});

			if (!rsp.ok) {
				debugger;
				Logger.error('', cc, `Unable to resend verification email; status=(${rsp.status}): ${rsp.statusText}`);

				void window.showErrorMessage(`Unable to resend verification email; Status: ${rsp.statusText}`, 'OK');

				return;
			}

			const confirm = { title: 'Recheck' };
			const cancel = { title: 'Cancel' };
			const result = await window.showInformationMessage(
				"Once you have verified your email address, click 'Recheck'.",
				confirm,
				cancel,
			);
			if (result === confirm) {
				await this.validate();
			}
		} catch (ex) {
			Logger.error(ex, cc);
			debugger;

			void window.showErrorMessage('Unable to resend verification email', 'OK');
		}
	}

	@log()
	async showHomeView(): Promise<void> {
		if (!this.container.homeView.visible) {
			await executeCommand(Commands.ShowHomeView);
		}
	}

	private showPlans(): void {
		void env.openExternal(Uri.joinPath(this.baseSiteUri, 'gitlens/pricing'));
	}

	@gate()
	@log()
	async startPreviewTrial(): Promise<void> {
		let { plan, previewTrial } = this._subscription;
		if (previewTrial != null || plan.effective.id !== SubscriptionPlanId.Free) {
			if (plan.effective.id === SubscriptionPlanId.Free) {
				const confirm = { title: 'Extend Trial' };
				const cancel = { title: 'Cancel' };
				const result = await window.showInformationMessage(
					'Your premium feature trial has expired. Please create a free account to extend your trial.',
					confirm,
					cancel,
				);

				if (result === confirm) {<commit_msg>Shows the home view on "start" trial<commit_after>import {
	authentication,
	AuthenticationSession,
	AuthenticationSessionsChangeEvent,
	version as codeVersion,
	commands,
	Disposable,
	env,
	Event,
	EventEmitter,
	MarkdownString,
	StatusBarAlignment,
	StatusBarItem,
	ThemeColor,
	Uri,
	window,
} from 'vscode';
import { fetch } from '@env/fetch';
import { getPlatform } from '@env/platform';
import { Commands, ContextKeys } from '../../constants';
import type { Container } from '../../container';
import { setContext } from '../../context';
import { AccountValidationError } from '../../errors';
import { RepositoriesChangeEvent } from '../../git/gitProviderService';
import { Logger } from '../../logger';
import { StorageKeys, WorkspaceStorageKeys } from '../../storage';
import {
	computeSubscriptionState,
	getSubscriptionPlan,
	getSubscriptionPlanPriority,
	getSubscriptionTimeRemaining,
	getTimeRemaining,
	isSubscriptionExpired,
	isSubscriptionPaidPlan,
	isSubscriptionTrial,
	Subscription,
	SubscriptionPlanId,
	SubscriptionState,
} from '../../subscription';
import { executeCommand } from '../../system/command';
import { createFromDateDelta } from '../../system/date';
import { gate } from '../../system/decorators/gate';
import { debug, log } from '../../system/decorators/log';
import { memoize } from '../../system/decorators/memoize';
import { once } from '../../system/function';
import { pluralize } from '../../system/string';
import { openWalkthrough } from '../../system/utils';

// TODO: What user-agent should we use?
const userAgent = 'Visual-Studio-Code-GitLens';

export interface SubscriptionChangeEvent {
	readonly current: Subscription;
	readonly previous: Subscription;
	readonly etag: number;
}

export class SubscriptionService implements Disposable {
	private static authenticationProviderId = 'gitkraken';
	private static authenticationScopes = ['gitlens'];

	private _onDidChange = new EventEmitter<SubscriptionChangeEvent>();
	get onDidChange(): Event<SubscriptionChangeEvent> {
		return this._onDidChange.event;
	}

	private _disposable: Disposable;
	private _subscription!: Subscription;
	private _statusBarSubscription: StatusBarItem | undefined;

	constructor(private readonly container: Container) {
		this._disposable = Disposable.from(
			once(container.onReady)(this.onReady, this),
			authentication.onDidChangeSessions(this.onAuthenticationChanged, this),
		);

		this.changeSubscription(this.getStoredSubscription(), true);
		setTimeout(() => void this.ensureSession(false), 10000);
	}

	dispose(): void {
		this._statusBarSubscription?.dispose();

		this._disposable.dispose();
	}

	private onAuthenticationChanged(e: AuthenticationSessionsChangeEvent): void {
		if (e.provider.id !== SubscriptionService.authenticationProviderId) return;

		void this.ensureSession(false, true);
	}

	@memoize()
	private get baseApiUri(): Uri {
		const { env } = this.container;
		if (env === 'staging') {
			return Uri.parse('https://stagingapi.gitkraken.com');
		}

		if (env === 'dev' || this.container.debugging) {
			return Uri.parse('https://devapi.gitkraken.com');
		}

		return Uri.parse('https://api.gitkraken.com');
	}

	@memoize()
	private get baseAccountUri(): Uri {
		const { env } = this.container;
		if (env === 'staging') {
			return Uri.parse('https://stagingaccount.gitkraken.com');
		}

		if (env === 'dev' || this.container.debugging) {
			return Uri.parse('https://devaccount.gitkraken.com');
		}

		return Uri.parse('https://account.gitkraken.com');
	}

	@memoize()
	private get baseSiteUri(): Uri {
		const { env } = this.container;
		if (env === 'staging') {
			return Uri.parse('https://staging.gitkraken.com');
		}

		if (env === 'dev' || this.container.debugging) {
			return Uri.parse('https://dev.gitkraken.com');
		}

		return Uri.parse('https://gitkraken.com');
	}

	private get connectedKey(): `${WorkspaceStorageKeys.ConnectedPrefix}${string}` {
		return `${WorkspaceStorageKeys.ConnectedPrefix}gitkraken`;
	}

	private _etag: number = 0;
	get etag(): number {
		return this._etag;
	}

	private onReady() {
		this._disposable = Disposable.from(
			this._disposable,
			this.container.git.onDidChangeRepositories(this.onRepositoriesChanged, this),
			...this.registerCommands(),
		);
		this.updateContext();
	}

	private onRepositoriesChanged(_e: RepositoriesChangeEvent): void {
		this.updateContext();
	}

	private registerCommands(): Disposable[] {
		void this.container.viewCommands;

		return [
			commands.registerCommand(Commands.PremiumLearn, () => this.learn()),
			commands.registerCommand(Commands.PremiumLogin, () => this.loginOrSignUp()),
			commands.registerCommand(Commands.PremiumLoginOrSignUp, () => this.loginOrSignUp()),
			commands.registerCommand(Commands.PremiumSignUp, () => this.loginOrSignUp()),
			commands.registerCommand(Commands.PremiumLogout, () => this.logout()),

			commands.registerCommand(Commands.PremiumStartPreviewTrial, () => this.startPreviewTrial()),
			commands.registerCommand(Commands.PremiumPurchase, () => this.purchase()),

			commands.registerCommand(Commands.PremiumResendVerification, () => this.resendVerification()),
			commands.registerCommand(Commands.PremiumValidate, () => this.validate()),

			commands.registerCommand(Commands.PremiumShowPlans, () => this.showPlans()),

			commands.registerCommand('gitlens.premium.reset', () => this.logout(true)),
		];
	}

	async getSubscription(): Promise<Subscription> {
		void (await this.ensureSession(false));
		return this._subscription;
	}

	@debug()
	learn(): void {
		void openWalkthrough(this.container.context.extension.id, 'gitlens.premium');
	}

	@gate()
	@log()
	async loginOrSignUp(): Promise<boolean> {
		void this.showHomeView();

		await this.container.storage.deleteWorkspace(this.connectedKey);

		const session = await this.ensureSession(true);
		const loggedIn = Boolean(session);
		if (loggedIn) {
			const {
				account,
				plan: { actual, effective },
			} = this._subscription;

			if (account?.verified === false) {
				const confirm = { title: 'Resend Verification' };
				const cancel = { title: 'Cancel' };
				const result = await window.showInformationMessage(
					`Before you can access your ${actual.name} account, you must verify your email address.`,
					confirm,
					cancel,
				);

				if (result === confirm) {
					void this.resendVerification();
				}
			} else if (isSubscriptionTrial(this._subscription)) {
				const remaining = getSubscriptionTimeRemaining(this._subscription, 'days');
				void window.showInformationMessage(
					`You are now signed in to your ${actual.name} account. There are ${pluralize(
						'day',
						remaining ?? 0,
					)} remaining in your ${
						effective.name
					} trial which grants you access to premium features for private repos.`,
					'OK',
				);
			} else {
				void window.showInformationMessage(`You are now signed in to your ${actual.name} account.`, 'OK');
			}
		}
		return loggedIn;
	}

	@gate()
	@log()
	logout(reset: boolean = false): void {
		this._sessionPromise = undefined;
		this._session = undefined;
		void this.container.storage.storeWorkspace(this.connectedKey, false);

		if (reset && this.container.debugging) {
			this.changeSubscription(undefined);

			return;
		}

		this.changeSubscription({
			...this._subscription,
			plan: {
				actual: getSubscriptionPlan(SubscriptionPlanId.Free),
				effective: getSubscriptionPlan(SubscriptionPlanId.Free),
			},
			account: undefined,
		});
	}

	@log()
	async purchase(): Promise<void> {
		if (this._subscription.account == null) {
			void this.showPlans();
		} else {
			void env.openExternal(
				Uri.joinPath(this.baseAccountUri, 'create-organization').with({ query: 'product=gitlens' }),
			);
		}
		await this.showHomeView();
	}

	@gate()
	@log()
	async resendVerification(): Promise<void> {
		if (this._subscription.account?.verified) return;

		const cc = Logger.getCorrelationContext();

		void this.showHomeView();

		const session = await this.ensureSession(false);
		if (session == null) return;

		try {
			const rsp = await fetch(Uri.joinPath(this.baseApiUri, 'resend-email').toString(), {
				method: 'POST',
				headers: {
					Authorization: `Bearer ${session.accessToken}`,
					'User-Agent': userAgent,
					'Content-Type': 'application/json',
				},
				body: JSON.stringify({ id: session.account.id }),
			});

			if (!rsp.ok) {
				debugger;
				Logger.error('', cc, `Unable to resend verification email; status=(${rsp.status}): ${rsp.statusText}`);

				void window.showErrorMessage(`Unable to resend verification email; Status: ${rsp.statusText}`, 'OK');

				return;
			}

			const confirm = { title: 'Recheck' };
			const cancel = { title: 'Cancel' };
			const result = await window.showInformationMessage(
				"Once you have verified your email address, click 'Recheck'.",
				confirm,
				cancel,
			);
			if (result === confirm) {
				await this.validate();
			}
		} catch (ex) {
			Logger.error(ex, cc);
			debugger;

			void window.showErrorMessage('Unable to resend verification email', 'OK');
		}
	}

	@log()
	async showHomeView(): Promise<void> {
		if (!this.container.homeView.visible) {
			await executeCommand(Commands.ShowHomeView);
		}
	}

	private showPlans(): void {
		void env.openExternal(Uri.joinPath(this.baseSiteUri, 'gitlens/pricing'));
	}

	@gate()
	@log()
	async startPreviewTrial(): Promise<void> {
		let { plan, previewTrial } = this._subscription;
		if (previewTrial != null || plan.effective.id !== SubscriptionPlanId.Free) {
			void this.showHomeView();

			if (plan.effective.id === SubscriptionPlanId.Free) {
				const confirm = { title: 'Extend Trial' };
				const cancel = { title: 'Cancel' };
				const result = await window.showInformationMessage(
					'Your premium features trial has expired. Please create a free account to extend your trial.',
					confirm,
					cancel,
				);

				if (result === confirm) {
{'fraction_non_alphanumeric': 0.14560940985601298, 'fraction_numerical': 0.000709795173392821, 'mean_word_length': 4.190789473684211, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 43, 'https://': 18, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3947134', 'n_tokens_mistral': 6716, 'n_tokens_neox': 5996, 'n_words': 1308}