diff --git a/.nvmrc b/.nvmrc index 64f5a0a6..b6a7d89c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -node +16 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index c53bbbfd..ab1fbf45 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,4 @@ { - // See http://go.microsoft.com/fwlink/?LinkId=827846 - // for the documentation about the extensions.json format "recommendations": [ "dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher" diff --git a/CHANGELOG.md b/CHANGELOG.md index 77b3f64d..e84b0b50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## 1.0.2 - 5 December 2021 + +### Added +- Extend the free preview for filling out a 2-minute survey. [#49](https://github.com/getlocalci/local-ci/pull/49/) + ## 1.0.1 - 24 November 2021 ### Added diff --git a/package-lock.json b/package-lock.json index 370ef098..dbcdbc9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "local-ci", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "local-ci", - "version": "1.0.1", + "version": "1.0.2", "hasInstallScript": true, "license": "GPL-2.0-or-later", "os": [ @@ -43,12 +43,12 @@ "typescript": "^4.3.2", "util": "^0.12.4", "vsce": "^2.5.0", - "vscode-test": "^1.5.2", + "vscode-test": "^1.6.1", "webpack": "^5.38.1", "webpack-cli": "^4.7.0" }, "engines": { - "node": ">=16", + "node": "16", "vscode": "^1.59.0" } }, @@ -4884,9 +4884,9 @@ } }, "node_modules/vscode-test": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.5.2.tgz", - "integrity": "sha512-x9PVfKxF6EInH9iSFGQi0V8H5zIW1fC7RAer6yNQR6sy3WyOwlWkuT3I+wf75xW/cO53hxMi1aj/EvqQfDFOAg==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.6.1.tgz", + "integrity": "sha512-086q88T2ca1k95mUzffvbzb7esqQNvJgiwY4h29ukPhFo8u+vXOOmelUoU5EQUHs3Of8+JuQ3oGdbVCqaxuTXA==", "dev": true, "dependencies": { "http-proxy-agent": "^4.0.1", @@ -9168,9 +9168,9 @@ } }, "vscode-test": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.5.2.tgz", - "integrity": "sha512-x9PVfKxF6EInH9iSFGQi0V8H5zIW1fC7RAer6yNQR6sy3WyOwlWkuT3I+wf75xW/cO53hxMi1aj/EvqQfDFOAg==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.6.1.tgz", + "integrity": "sha512-086q88T2ca1k95mUzffvbzb7esqQNvJgiwY4h29ukPhFo8u+vXOOmelUoU5EQUHs3Of8+JuQ3oGdbVCqaxuTXA==", "dev": true, "requires": { "http-proxy-agent": "^4.0.1", diff --git a/package.json b/package.json index 514e93f9..1d587177 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "local-ci", "displayName": "Local CI", "description": "Debug CircleCI® workflows locally, with Bash access during and after. Free preview, then paid.", - "version": "1.0.1", + "version": "1.0.2", "publisher": "LocalCI", "contributors": [ "Ryan Kienstra" @@ -21,7 +21,7 @@ "qna": "https://github.com/getlocalci/local-ci/discussions", "engines": { "vscode": "^1.59.0", - "node": ">=16" + "node": "16" }, "os": [ "!win32" @@ -287,7 +287,7 @@ "typescript": "^4.3.2", "util": "^0.12.4", "vsce": "^2.5.0", - "vscode-test": "^1.5.2", + "vscode-test": "^1.6.1", "webpack": "^5.38.1", "webpack-cli": "^4.7.0" }, diff --git a/src/classes/JobProvider.ts b/src/classes/JobProvider.ts index 9230c867..6105c101 100644 --- a/src/classes/JobProvider.ts +++ b/src/classes/JobProvider.ts @@ -15,6 +15,7 @@ import getAllConfigFilePaths from '../utils/getAllConfigFilePaths'; import getConfigFilePath from '../utils/getConfigFilePath'; import getDockerError from '../utils/getDockerError'; import getProcessFilePath from '../utils/getProcessFilePath'; +import getTrialLength from '../utils/getTrialLength'; import isDockerRunning from '../utils/isDockerRunning'; import isLicenseValid from '../utils/isLicenseValid'; import isTrialExpired from '../utils/isTrialExpired'; @@ -73,7 +74,10 @@ export default class JobProvider const shouldEnableExtension = (await isLicenseValid(this.context)) || - !isTrialExpired(this.context.globalState.get(TRIAL_STARTED_TIMESTAMP)); + !isTrialExpired( + this.context.globalState.get(TRIAL_STARTED_TIMESTAMP), + getTrialLength(this.context) + ); const dockerRunning = isDockerRunning(); if (shouldEnableExtension && dockerRunning) { diff --git a/src/classes/LicenseProvider.ts b/src/classes/LicenseProvider.ts index cd6bbd78..2acc6fd6 100644 --- a/src/classes/LicenseProvider.ts +++ b/src/classes/LicenseProvider.ts @@ -1,7 +1,14 @@ import * as vscode from 'vscode'; -import { LICENSE_ERROR } from '../constants'; +import { + EXTENDED_TRIAL_LENGTH_IN_MILLISECONDS, + HAS_EXTENDED_TRIAL, + LICENSE_ERROR, + SURVEY_URL, + TRIAL_STARTED_TIMESTAMP, +} from '../constants'; import getLicenseErrorMessage from '../utils/getLicenseErrorMessage'; import getLicenseInformation from '../utils/getLicenseInformation'; +import getPrettyPrintedTimeRemaining from '../utils/getPrettyPrintedTimeRemaining'; import isLicenseValid from '../utils/isLicenseValid'; import showLicenseInput from '../utils/showLicenseInput'; @@ -37,11 +44,11 @@ export default class LicenseProvider implements vscode.WebviewViewProvider { localResourceRoots: [this.extensionUri], }; - await this.load(); + this.load(); webviewView.webview.onDidReceiveMessage(async (data) => { if (data.type === 'enterLicense') { - await showLicenseInput( + showLicenseInput( this.context, () => this.load(), () => this.licenseSuccessCallback() @@ -66,6 +73,25 @@ export default class LicenseProvider implements vscode.WebviewViewProvider { }); } } + + if (data.type === 'takeSurvey') { + if (this.context.globalState.get(HAS_EXTENDED_TRIAL)) { + return; + } + + this.load(); + this.context.globalState.update(HAS_EXTENDED_TRIAL, true); + this.context.globalState.update( + TRIAL_STARTED_TIMESTAMP, + new Date().getTime() + ); + vscode.env.openExternal(vscode.Uri.parse(SURVEY_URL)); + vscode.window.showInformationMessage( + `Thanks, your free preview is now ${getPrettyPrintedTimeRemaining( + EXTENDED_TRIAL_LENGTH_IN_MILLISECONDS + )} longer` + ); + } }); } diff --git a/src/constants/index.ts b/src/constants/index.ts index 80195499..089c0eeb 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -41,6 +41,8 @@ export const LICENSE_VALIDITY = 'local-ci.license.validity'; export const LICENSE_VALIDITY_CACHE_EXPIRATION = 'local-ci.license.cache.expiration'; export const TRIAL_LENGTH_IN_MILLISECONDS = 172800000; // 2 days. +export const EXTENDED_TRIAL_LENGTH_IN_MILLISECONDS = 1296000000; // 15 days. +export const HAS_EXTENDED_TRIAL = 'local-ci.license.trial-extended.survey'; export const TRIAL_STARTED_TIMESTAMP = 'local-ci.license.trial-started.timestamp'; export const CONTAINER_STORAGE_DIRECTORY = '/tmp/local-ci'; @@ -48,5 +50,8 @@ export const HOST_TMP_DIRECTORY = '/tmp/local-ci'; // Also hard-coded in node/un export const PROCESS_FILE_DIRECTORY = `${HOST_TMP_DIRECTORY}/process`; export const LOCAL_VOLUME_DIRECTORY = `${HOST_TMP_DIRECTORY}/volume`; export const RUN_JOB_COMMAND = 'local-ci.job.run'; +export const SCHEDULE_INTERVIEW_URL = + 'https://tidycal.com/localci/30-minute-meeting'; export const SUPPRESS_UNCOMMITTED_FILE_WARNING = 'local-ci.suppress-warning.uncommitted'; +export const SURVEY_URL = 'https://www.surveymonkey.com/r/localci'; diff --git a/src/extension.ts b/src/extension.ts index ff0e1501..b9575a35 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -13,6 +13,7 @@ import { JOB_TREE_VIEW_ID, RUN_JOB_COMMAND, SELECTED_CONFIG_PATH, + TRIAL_STARTED_TIMESTAMP, } from './constants'; import cleanUpCommittedImages from './utils/cleanUpCommittedImages'; import disposeTerminalsForJob from './utils/disposeTerminalsForJob'; @@ -22,7 +23,6 @@ import getConfig from './utils/getConfig'; import getConfigFilePath from './utils/getConfigFilePath'; import getDebuggingTerminalName from './utils/getDebuggingTerminalName'; import getFinalTerminalName from './utils/getFinalTerminalName'; -import getLicenseInformation from './utils/getLicenseInformation'; import getProcessedConfig from './utils/getProcessedConfig'; import getProcessFilePath from './utils/getProcessFilePath'; import getRepoBasename from './utils/getRepoBasename'; @@ -31,6 +31,9 @@ import showLicenseInput from './utils/showLicenseInput'; import writeProcessFile from './utils/writeProcessFile'; export function activate(context: vscode.ExtensionContext): void { + if (!context.globalState.get(TRIAL_STARTED_TIMESTAMP)) { + context.globalState.update(TRIAL_STARTED_TIMESTAMP, new Date().getTime()); + } const jobProvider = new JobProvider(context); vscode.window.registerTreeDataProvider(JOB_TREE_VIEW_ID, jobProvider); @@ -255,6 +258,4 @@ export function activate(context: vscode.ExtensionContext): void { } }, }); - - getLicenseInformation(context); } diff --git a/src/test/suite/utils/getHoursRemainingInTrial.test.ts b/src/test/suite/utils/getHoursRemainingInTrial.test.ts deleted file mode 100644 index 0c95132d..00000000 --- a/src/test/suite/utils/getHoursRemainingInTrial.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as assert from 'assert'; -import getHoursRemainingInTrial from '../../../utils/getHoursRemainingInTrial'; - -const hourInMilliseconds = 3600000; -suite('getHoursRemainingInTrial', () => { - test('entire trial remaining', () => { - const time = new Date().getTime(); - assert.strictEqual(getHoursRemainingInTrial(time, time), 48); - }); - - test('one day remaining', () => { - const time = new Date().getTime(); - assert.strictEqual( - getHoursRemainingInTrial(time, time - 24 * hourInMilliseconds), - 24 - ); - }); - - test('one hour remaining', () => { - const time = new Date().getTime(); - assert.strictEqual( - getHoursRemainingInTrial(time, time - 47 * hourInMilliseconds), - 1 - ); - }); - - test('no time remaining', () => { - const time = new Date().getTime(); - assert.strictEqual( - getHoursRemainingInTrial(time, time - 48 * hourInMilliseconds), - 0 - ); - }); -}); diff --git a/src/test/suite/utils/getMillisecondsRemainingInTrial.test.ts b/src/test/suite/utils/getMillisecondsRemainingInTrial.test.ts new file mode 100644 index 00000000..973bf3eb --- /dev/null +++ b/src/test/suite/utils/getMillisecondsRemainingInTrial.test.ts @@ -0,0 +1,50 @@ +import * as assert from 'assert'; +import { TRIAL_LENGTH_IN_MILLISECONDS } from '../../../constants'; +import getMillisecondsRemainingInTrial from '../../../utils/getMillisecondsRemainingInTrial'; + +const hourInMilliseconds = 3600000; +suite('getMillisecondsRemainingInTrial', () => { + test('entire trial remaining', () => { + const time = new Date().getTime(); + assert.strictEqual( + getMillisecondsRemainingInTrial(time, time, TRIAL_LENGTH_IN_MILLISECONDS), + 172800000 + ); + }); + + test('1 day remaining', () => { + const time = new Date().getTime(); + assert.strictEqual( + getMillisecondsRemainingInTrial( + time, + time - 24 * hourInMilliseconds, + TRIAL_LENGTH_IN_MILLISECONDS + ), + 86400000 + ); + }); + + test('1 hour remaining', () => { + const time = new Date().getTime(); + assert.strictEqual( + getMillisecondsRemainingInTrial( + time, + time - 47 * hourInMilliseconds, + TRIAL_LENGTH_IN_MILLISECONDS + ), + 3600000 + ); + }); + + test('no time remaining', () => { + const time = new Date().getTime(); + assert.strictEqual( + getMillisecondsRemainingInTrial( + time, + time - 48 * hourInMilliseconds, + TRIAL_LENGTH_IN_MILLISECONDS + ), + 0 + ); + }); +}); diff --git a/src/test/suite/utils/getTimeRemainingInTrial.test.ts b/src/test/suite/utils/getTimeRemainingInTrial.test.ts new file mode 100644 index 00000000..207b48f1 --- /dev/null +++ b/src/test/suite/utils/getTimeRemainingInTrial.test.ts @@ -0,0 +1,141 @@ +import * as assert from 'assert'; +import { TRIAL_LENGTH_IN_MILLISECONDS } from '../../../constants'; +import getTimeRemainingInTrial from '../../../utils/getTimeRemainingInTrial'; + +const minuteInMilliseconds = 60000; +const hourInMilliseconds = 3600000; +const dayInMilliseconds = 86400000; + +suite('getTimeRemainingInTrial', () => { + const time = new Date().getTime(); + test('no trial started timestamp', () => { + assert.strictEqual( + getTimeRemainingInTrial(time, null, TRIAL_LENGTH_IN_MILLISECONDS), + 'No time' + ); + }); + + test('14 days and 13 hours remaining', () => { + assert.strictEqual( + getTimeRemainingInTrial( + time, + time, + 14 * dayInMilliseconds + 13 * hourInMilliseconds + ), + '15 days' + ); + }); + + test('14 days and 11 hours remaining', () => { + assert.strictEqual( + getTimeRemainingInTrial( + time, + time, + 14 * dayInMilliseconds + 11 * hourInMilliseconds + ), + '14 days, 11 hours' + ); + }); + + test('14 days remaining', () => { + assert.strictEqual( + getTimeRemainingInTrial(time, time, 14 * dayInMilliseconds), + '14 days' + ); + }); + + test('2 hours remaining', () => { + assert.strictEqual( + getTimeRemainingInTrial(time, time, dayInMilliseconds * 2), + '2 days' + ); + }); + + test('1 day, 14 hours remaining', () => { + assert.strictEqual( + getTimeRemainingInTrial( + time, + time - 10 * hourInMilliseconds, + dayInMilliseconds * 2 + ), + '2 days' + ); + }); + + test('exactly 1 day remaining', () => { + assert.strictEqual( + getTimeRemainingInTrial( + time, + time - 24 * hourInMilliseconds, + dayInMilliseconds * 2 + ), + '1 day' + ); + }); + + test('five hours remaining', () => { + assert.strictEqual( + getTimeRemainingInTrial( + time, + time - 43 * hourInMilliseconds, + dayInMilliseconds * 2 + ), + '5 hours' + ); + }); + + test('one hour remaining', () => { + assert.strictEqual( + getTimeRemainingInTrial( + time, + time - 47 * hourInMilliseconds, + dayInMilliseconds * 2 + ), + '1 hour' + ); + }); + + test('23 minutes remaining', () => { + assert.strictEqual( + getTimeRemainingInTrial( + time, + time - 47 * hourInMilliseconds - 37 * minuteInMilliseconds, + dayInMilliseconds * 2 + ), + '23 minutes' + ); + }); + + test('1 minute remaining', () => { + assert.strictEqual( + getTimeRemainingInTrial( + time, + time - 47 * hourInMilliseconds - 59 * minuteInMilliseconds, + dayInMilliseconds * 2 + ), + '1 minute' + ); + }); + + test('30 seconds remaining', () => { + assert.strictEqual( + getTimeRemainingInTrial( + time, + time - 47 * hourInMilliseconds - 59 * minuteInMilliseconds - 30000, + dayInMilliseconds * 2 + ), + 'No time' + ); + }); + + test('no time remaining', () => { + assert.strictEqual( + getTimeRemainingInTrial( + time, + time - 48 * hourInMilliseconds, + dayInMilliseconds * 2 + ), + 'No time' + ); + }); +}); diff --git a/src/test/suite/utils/isTrialExpired.test.ts b/src/test/suite/utils/isTrialExpired.test.ts index a23ab8a4..0a7fa627 100644 --- a/src/test/suite/utils/isTrialExpired.test.ts +++ b/src/test/suite/utils/isTrialExpired.test.ts @@ -1,22 +1,72 @@ import * as assert from 'assert'; import * as mocha from 'mocha'; import * as sinon from 'sinon'; +import { + EXTENDED_TRIAL_LENGTH_IN_MILLISECONDS, + TRIAL_LENGTH_IN_MILLISECONDS, +} from '../../../constants'; import isTrialExpired from '../../../utils/isTrialExpired'; mocha.afterEach(() => { sinon.restore(); }); +const extendedTrial = + TRIAL_LENGTH_IN_MILLISECONDS + EXTENDED_TRIAL_LENGTH_IN_MILLISECONDS; + suite('isTrialExpired', () => { test('preview just began', () => { - assert.strictEqual(isTrialExpired(new Date().getTime()), false); + assert.strictEqual( + isTrialExpired(new Date().getTime(), TRIAL_LENGTH_IN_MILLISECONDS), + false + ); }); test('preview began 2 days and 1 millisecond ago', () => { - assert.strictEqual(isTrialExpired(new Date().getTime() - 172800001), true); + assert.strictEqual( + isTrialExpired( + new Date().getTime() - 172800001, + TRIAL_LENGTH_IN_MILLISECONDS + ), + true + ); }); test('preview began a week ago', () => { - assert.strictEqual(isTrialExpired(new Date().getTime() - 604800000), true); + assert.strictEqual( + isTrialExpired( + new Date().getTime() - 604800000, + TRIAL_LENGTH_IN_MILLISECONDS + ), + true + ); + }); + + test('preview just began and was extended', () => { + assert.strictEqual( + isTrialExpired(new Date().getTime(), TRIAL_LENGTH_IN_MILLISECONDS), + false + ); + }); + + test('preview began 2 days and 10 milliseconds ago and was extended', () => { + assert.strictEqual( + isTrialExpired(new Date().getTime() - 172800010, extendedTrial), + false + ); + }); + + test('preview began a week ago and was extended', () => { + assert.strictEqual( + isTrialExpired(new Date().getTime() - 604800000, extendedTrial), + false + ); + }); + + test('preview began 17 days and 1 millisecond ago and was extended', () => { + assert.strictEqual( + isTrialExpired(new Date().getTime() - 1468800001, extendedTrial), + true + ); }); }); diff --git a/src/utils/getHoursRemainingInTrial.ts b/src/utils/getHoursRemainingInTrial.ts deleted file mode 100644 index 499352b2..00000000 --- a/src/utils/getHoursRemainingInTrial.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { TRIAL_LENGTH_IN_MILLISECONDS } from '../constants'; -const hourInMilliseconds = 3600000; - -export default function getHoursRemainingInTrial( - currentTimeStamp: number, - trialStartedTimeStamp: number | unknown -): number { - const previewTimeElapsed = currentTimeStamp - Number(trialStartedTimeStamp); - - return trialStartedTimeStamp - ? Math.ceil( - (TRIAL_LENGTH_IN_MILLISECONDS - previewTimeElapsed) / hourInMilliseconds - ) - : 0; -} diff --git a/src/utils/getLicenseInformation.ts b/src/utils/getLicenseInformation.ts index 7da6679b..4ed93004 100644 --- a/src/utils/getLicenseInformation.ts +++ b/src/utils/getLicenseInformation.ts @@ -1,20 +1,20 @@ import * as vscode from 'vscode'; -import getHoursRemainingInTrial from './getHoursRemainingInTrial'; import getLicenseErrorMessage from './getLicenseErrorMessage'; +import getTimeRemainingInTrial from './getTimeRemainingInTrial'; +import getTrialLength from './getTrialLength'; import isLicenseValid from './isLicenseValid'; import isTrialExpired from './isTrialExpired'; import { + EXTENDED_TRIAL_LENGTH_IN_MILLISECONDS, GET_LICENSE_KEY_URL, + HAS_EXTENDED_TRIAL, LICENSE_ERROR, LICENSE_KEY, LICENSE_VALIDITY, + SCHEDULE_INTERVIEW_URL, TRIAL_STARTED_TIMESTAMP, } from '../constants'; -function getTextForNumber(singular: string, plural: string, count: number) { - return count === 1 ? singular : plural; -} - export default async function getLicenseInformation( context: vscode.ExtensionContext ): Promise { @@ -22,56 +22,73 @@ export default async function getLicenseInformation( const previewStartedTimeStamp = context.globalState.get( TRIAL_STARTED_TIMESTAMP ); + const dayInMilliseconds = 86400000; const licenseKey = await context.secrets.get(LICENSE_KEY); - const getLicenseLink = `Buy license`; + const getLicenseLink = `Buy license`; const enterLicenseButton = ``; const changeLicenseButton = ``; const retryValidationButton = ``; + const takeSurveyButton = ``; + const scheduleInterviewLink = `Get a free lifetime license by doing a 30-minute Zoom user research interview`; + const complainUri = 'mailto:ryan@getlocalci.com'; + const complainLink = `Complain to me`; const isValid = await isLicenseValid(context); - const previewExpired = isTrialExpired(previewStartedTimeStamp); + const hasExtendedTrial = !!context.globalState.get(HAS_EXTENDED_TRIAL); + const trialLengthInMilliseconds = getTrialLength(context); + const isPreviewExpired = isTrialExpired( + previewStartedTimeStamp, + trialLengthInMilliseconds + ); + + const isPreviewExpiredByOneDay = isTrialExpired( + previewStartedTimeStamp, + trialLengthInMilliseconds + dayInMilliseconds + ); if (isValid) { return `

Your Local CI license key is valid!

- ${changeLicenseButton}`; - } - - if (!previewStartedTimeStamp && !licenseKey) { - context.globalState.update(TRIAL_STARTED_TIMESTAMP, new Date().getTime()); - return `

Thanks for previewing Local CI!

-

This free trial will last for 2 days, then it will require a purchased license key.

-

${getLicenseLink}

-

${enterLicenseButton}

`; +

${changeLicenseButton}

+

${complainLink}

`; } - if (previewExpired && !!licenseKey && !isValid) { + if (isPreviewExpired && !!licenseKey && !isValid) { return `

There was an error validating the license key.

${getLicenseErrorMessage( String(await context.secrets.get(LICENSE_ERROR)) )}

${getLicenseLink}

${enterLicenseButton}

-

${retryValidationButton}

`; + ${hasExtendedTrial ? '' : `

${takeSurveyButton}

`} +

${retryValidationButton}

+

${complainLink}

`; } - if (previewExpired) { + if (isPreviewExpiredByOneDay) { + return `

${scheduleInterviewLink}

+

No sales pitch, I have nothing to sell you after giving you the free lifetime license.

+

${enterLicenseButton}

`; + } + + if (isPreviewExpired) { return `

Thanks for previewing Local CI! The free preview is over.

Please enter a Local CI license key to keep using this.

+ ${hasExtendedTrial ? '' : `

${takeSurveyButton}

`}

${getLicenseLink}

-

${enterLicenseButton}

`; +

${enterLicenseButton}

+

${complainLink}

`; } - const timeRemaining = getHoursRemainingInTrial( - new Date().getTime(), - previewStartedTimeStamp - ); - return `

Thanks for previewing Local CI!

-

${getTextForNumber( - `Your free preview has ${timeRemaining} hour left.`, - `Your free preview has ${timeRemaining} hours left.`, - timeRemaining - )}

+

${getTimeRemainingInTrial( + new Date().getTime(), + context.globalState.get(TRIAL_STARTED_TIMESTAMP), + trialLengthInMilliseconds + )} left in this free preview.

+ ${hasExtendedTrial ? '' : `

${takeSurveyButton}

`}

${getLicenseLink}

-

${enterLicenseButton}

`; +

${enterLicenseButton}

+

${complainLink}

`; } diff --git a/src/utils/getMillisecondsRemainingInTrial.ts b/src/utils/getMillisecondsRemainingInTrial.ts new file mode 100644 index 00000000..4fbaf2ca --- /dev/null +++ b/src/utils/getMillisecondsRemainingInTrial.ts @@ -0,0 +1,11 @@ +export default function getMillisecondsRemainingInTrial( + currentTimeStamp: number, + trialStartedTimeStamp: number | unknown, + trialLengthInMilliseconds: number +): number { + const previewTimeElapsed = currentTimeStamp - Number(trialStartedTimeStamp); + + return trialStartedTimeStamp + ? trialLengthInMilliseconds - previewTimeElapsed + : 0; +} diff --git a/src/utils/getPrettyPrintedTimeRemaining.ts b/src/utils/getPrettyPrintedTimeRemaining.ts new file mode 100644 index 00000000..2f4e1531 --- /dev/null +++ b/src/utils/getPrettyPrintedTimeRemaining.ts @@ -0,0 +1,55 @@ +const minuteInMilliseconds = 60000; +const hourInMilliseconds = 3600000; +const dayInMilliseconds = 86400000; + +function getTextForNumber(singular: string, plural: string, count: number) { + if (!count) { + return ''; + } + + return count === 1 ? singular : plural; +} + +export default function getPrettyPrintedTimeRemaining( + millisecondsRemaining: number +): string { + if (millisecondsRemaining < minuteInMilliseconds) { + return 'No time'; + } + + if (millisecondsRemaining < hourInMilliseconds) { + const minutesRemaining = Math.floor( + millisecondsRemaining / minuteInMilliseconds + ); + + return getTextForNumber( + `${minutesRemaining} minute`, + `${minutesRemaining} minutes`, + minutesRemaining + ); + } + + const daysRemaining = Math.round(millisecondsRemaining / dayInMilliseconds); + const hoursRemaining = Math.floor( + Math.max( + (millisecondsRemaining - daysRemaining * dayInMilliseconds) / + hourInMilliseconds, + 0 + ) + ); + + return [ + getTextForNumber( + `${daysRemaining} day`, + `${daysRemaining} days`, + daysRemaining + ), + getTextForNumber( + `${hoursRemaining} hour`, + `${hoursRemaining} hours`, + hoursRemaining + ), + ] + .filter((timeRemaining) => timeRemaining) + .join(', '); +} diff --git a/src/utils/getTimeRemainingInTrial.ts b/src/utils/getTimeRemainingInTrial.ts new file mode 100644 index 00000000..4c7f7bc7 --- /dev/null +++ b/src/utils/getTimeRemainingInTrial.ts @@ -0,0 +1,16 @@ +import getMillisecondsRemainingInTrial from './getMillisecondsRemainingInTrial'; +import getPrettyPrintedTimeRemaining from './getPrettyPrintedTimeRemaining'; + +export default function getTimeRemainingInTrial( + currentTimeStamp: number, + trialStartedTimeStamp: number | unknown, + trialLengthInMilliseconds: number +): string { + return getPrettyPrintedTimeRemaining( + getMillisecondsRemainingInTrial( + currentTimeStamp, + trialStartedTimeStamp, + trialLengthInMilliseconds + ) + ); +} diff --git a/src/utils/getTrialLength.ts b/src/utils/getTrialLength.ts new file mode 100644 index 00000000..2e0d2337 --- /dev/null +++ b/src/utils/getTrialLength.ts @@ -0,0 +1,14 @@ +import * as vscode from 'vscode'; +import { + EXTENDED_TRIAL_LENGTH_IN_MILLISECONDS, + HAS_EXTENDED_TRIAL, + TRIAL_LENGTH_IN_MILLISECONDS, +} from '../constants'; + +export default function getTrialLength( + context: vscode.ExtensionContext +): number { + return context.globalState.get(HAS_EXTENDED_TRIAL) + ? EXTENDED_TRIAL_LENGTH_IN_MILLISECONDS + TRIAL_LENGTH_IN_MILLISECONDS + : TRIAL_LENGTH_IN_MILLISECONDS; +} diff --git a/src/utils/isTrialExpired.ts b/src/utils/isTrialExpired.ts index cef2319f..eb804000 100644 --- a/src/utils/isTrialExpired.ts +++ b/src/utils/isTrialExpired.ts @@ -1,11 +1,15 @@ -import { TRIAL_LENGTH_IN_MILLISECONDS } from '../constants'; +import getMillisecondsRemainingInTrial from './getMillisecondsRemainingInTrial'; export default function isTrialExpired( - trialStartedTimeStamp: number | unknown + trialStartedTimeStamp: number | unknown, + trialLengthInMilliseconds: number ): boolean { return ( !trialStartedTimeStamp || - new Date().getTime() - Number(trialStartedTimeStamp) > - TRIAL_LENGTH_IN_MILLISECONDS + getMillisecondsRemainingInTrial( + new Date().getTime(), + trialStartedTimeStamp, + trialLengthInMilliseconds + ) <= 0 ); } diff --git a/webview/index.js b/webview/index.js index a7cd1a8a..7dffb96b 100644 --- a/webview/index.js +++ b/webview/index.js @@ -3,17 +3,20 @@ (function () { function addLicenseHandlers() { const vscode = acquireVsCodeApi(); - document - .getElementById('enter-license') - .addEventListener('click', () => - vscode.postMessage({ type: 'enterLicense' }) - ); + const listenerElements = { + 'take-survey': 'takeSurvey', + 'enter-license': 'enterLicense', + 'retry-license-validation': 'retryLicenseValidation', + }; - document - .getElementById('retry-license-validation') - .addEventListener('click', () => - vscode.postMessage({ type: 'retryLicenseValidation' }) - ); + Object.keys(listenerElements).forEach((elementId) => { + const element = document.getElementById(elementId) + if (element) { + element.addEventListener('click', () => + vscode.postMessage({ type: listenerElements[elementId] }) + ); + } + }); } // Mainly copied from @wordpress/dom-ready https://github.com/WordPress/gutenberg/blob/3da717b8d0ac7d7821fc6d0475695ccf3ae2829f/packages/dom-ready/src/index.js#L31 diff --git a/webview/vscode.css b/webview/vscode.css index 82d8e4e8..71a43791 100644 --- a/webview/vscode.css +++ b/webview/vscode.css @@ -34,10 +34,26 @@ a.button { text-align: center; outline: 1px solid transparent; outline-offset: 2px !important; + text-decoration: none; +} + +button.primary, +a.primary { color: var(--vscode-button-foreground); background: var(--vscode-button-background); } +button.secondary, +a.secondary { + color: var(--vscode-button-secondaryForeground); + background: var(--vscode-button-secondaryBackground); +} + +button.secondary:hover +a.button.secondary:hover { + background: var(--vscode-button-secondaryHoverBackground); +} + button:hover, a.button:hover { cursor: pointer; @@ -48,12 +64,3 @@ button:focus, a.button:focus { outline-color: var(--vscode-focusBorder); } - -button.secondary { - color: var(--vscode-button-secondaryForeground); - background: var(--vscode-button-secondaryBackground); -} - -button.secondary:hover { - background: var(--vscode-button-secondaryHoverBackground); -}