From 66df4f531ad19a68856cdd8425ebb88b38dcd6b4 Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Wed, 24 Nov 2021 20:51:54 -0600 Subject: [PATCH 01/24] Add the yaml keyword back in --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 0da17ef4..895288dd 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ "cicd", "CI/CD", "CircleCI", - "continuous integration" + "continuous integration", + "YAML" ], "activationEvents": [ "localCiJobs.selectRepo", From c2e3c98b143c958b998657971431c77e4587febc Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Thu, 25 Nov 2021 12:07:36 -0600 Subject: [PATCH 02/24] Update the links to preview and paid to point to /pricing --- README.md | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8e01f6b4..765a492c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![2 day free preview](https://img.shields.io/badge/trial-2%20day-orange)](https://getlocalci.com) -[![Buy license key](https://img.shields.io/badge/%24-paid-orange)](https://getlocalci.com) +[![2 day free preview](https://img.shields.io/badge/trial-2%20day-orange)](https://getlocalci.com/pricing) +[![Buy license key](https://img.shields.io/badge/%24-paid-orange)](https://getlocalci.com/pricing) [![Platform: macOS](https://img.shields.io/badge/platform-macOS-yellow)](https://en.wikipedia.org/wiki/MacOS) [![Requires CircleCI®](https://img.shields.io/badge/requires-CirlcleCI%C2%AE-yellow)](https://circleci.com) diff --git a/package.json b/package.json index 895288dd..514e93f9 100644 --- a/package.json +++ b/package.json @@ -223,7 +223,7 @@ "badges": [ { "url": "https://img.shields.io/badge/trial-2%20day-orange", - "href": "https://getlocalci.com", + "href": "https://getlocalci.com/pricing", "description": "2 day free preview" }, { From a15dd9d93cd8aebcd0c16b2720ac70b5c6b5518e Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Thu, 25 Nov 2021 12:17:03 -0600 Subject: [PATCH 03/24] Add a link to get started with CircleCI --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 765a492c..70d85440 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ But first you'll get a free 2-day preview, no sign-up or credit card needed. [CircleCI®](https://circleci.com/), [macOS](https://en.wikipedia.org/wiki/MacOS), [Docker](https://www.docker.com/) -If you don't have a CircleCI® account, please [sign up here](https://circleci.com/docs/2.0/first-steps/). +If you don’t have a CircleCI® account, you can [get started](https://circleci.com/docs/2.0/first-steps/) with CircleCI® for free. A `.circleci/config.yml` file should be somewhere in the VS Code workspace, using the [2.x configuration](https://circleci.com/docs/2.0/configuration-reference/). From 900311ed4582956711e0e72aac2c18ad4588b37e Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Thu, 25 Nov 2021 12:18:12 -0600 Subject: [PATCH 04/24] Change the backtick to an apostrophe --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 70d85440..db8e5397 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ But first you'll get a free 2-day preview, no sign-up or credit card needed. [CircleCI®](https://circleci.com/), [macOS](https://en.wikipedia.org/wiki/MacOS), [Docker](https://www.docker.com/) -If you don’t have a CircleCI® account, you can [get started](https://circleci.com/docs/2.0/first-steps/) with CircleCI® for free. +If you don't have a CircleCI® account, you can [get started](https://circleci.com/docs/2.0/first-steps/) with CircleCI® for free. A `.circleci/config.yml` file should be somewhere in the VS Code workspace, using the [2.x configuration](https://circleci.com/docs/2.0/configuration-reference/). From dc69b534db5fdab9f16a5afa4ae5a22b3ded3bcf Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Mon, 29 Nov 2021 23:34:16 -0600 Subject: [PATCH 05/24] Change the remaining time in the preview to account for days Setting the stage to add time to the trial for filling out the survey. --- src/classes/LicenseProvider.ts | 7 ++- src/constants/index.ts | 1 + .../getDaysAndHoursRemainingInTrial.test.ts | 55 +++++++++++++++++++ ...> getMillisecondsRemainingInTrial.test.ts} | 16 +++--- src/utils/getDaysAndHoursRemainingInTrial.ts | 49 +++++++++++++++++ src/utils/getLicenseInformation.ts | 30 ++++------ ....ts => getMillisecondsRemainingInTrial.ts} | 7 +-- src/utils/isTrialExpired.ts | 7 ++- webview/index.js | 24 ++++---- 9 files changed, 152 insertions(+), 44 deletions(-) create mode 100644 src/test/suite/utils/getDaysAndHoursRemainingInTrial.test.ts rename src/test/suite/utils/{getHoursRemainingInTrial.test.ts => getMillisecondsRemainingInTrial.test.ts} (51%) create mode 100644 src/utils/getDaysAndHoursRemainingInTrial.ts rename src/utils/{getHoursRemainingInTrial.ts => getMillisecondsRemainingInTrial.ts} (58%) diff --git a/src/classes/LicenseProvider.ts b/src/classes/LicenseProvider.ts index cd6bbd78..fea88751 100644 --- a/src/classes/LicenseProvider.ts +++ b/src/classes/LicenseProvider.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { LICENSE_ERROR } from '../constants'; +import { LICENSE_ERROR, SURVEY_URL } from '../constants'; import getLicenseErrorMessage from '../utils/getLicenseErrorMessage'; import getLicenseInformation from '../utils/getLicenseInformation'; import isLicenseValid from '../utils/isLicenseValid'; @@ -66,6 +66,11 @@ export default class LicenseProvider implements vscode.WebviewViewProvider { }); } } + + if (data.type === 'takeSurvey') { + // @todo: Set globalState to extend the preview length. + vscode.env.openExternal(vscode.Uri.parse(SURVEY_URL)); + } }); } diff --git a/src/constants/index.ts b/src/constants/index.ts index 80195499..4d182d80 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -50,3 +50,4 @@ export const LOCAL_VOLUME_DIRECTORY = `${HOST_TMP_DIRECTORY}/volume`; export const RUN_JOB_COMMAND = 'local-ci.job.run'; export const SUPPRESS_UNCOMMITTED_FILE_WARNING = 'local-ci.suppress-warning.uncommitted'; +export const SURVEY_URL = 'https://example.com'; diff --git a/src/test/suite/utils/getDaysAndHoursRemainingInTrial.test.ts b/src/test/suite/utils/getDaysAndHoursRemainingInTrial.test.ts new file mode 100644 index 00000000..c61bee9d --- /dev/null +++ b/src/test/suite/utils/getDaysAndHoursRemainingInTrial.test.ts @@ -0,0 +1,55 @@ +import * as assert from 'assert'; +import getDaysAndHoursRemainingInTrial from '../../../utils/getDaysAndHoursRemainingInTrial'; + +const hourInMilliseconds = 3600000; +suite('getDaysAndHoursRemainingInTrial', () => { + test('no trial started timestamp', () => { + const time = new Date().getTime(); + assert.strictEqual(getDaysAndHoursRemainingInTrial(time, null), 'No time'); + }); + + test('entire trial remaining', () => { + const time = new Date().getTime(); + assert.strictEqual(getDaysAndHoursRemainingInTrial(time, time), '2 days'); + }); + + test('entire trial remaining', () => { + const time = new Date().getTime(); + assert.strictEqual( + getDaysAndHoursRemainingInTrial(time, time - 10 * hourInMilliseconds), + '1 day, 14 hours' + ); + }); + + test('one day remaining', () => { + const time = new Date().getTime(); + assert.strictEqual( + getDaysAndHoursRemainingInTrial(time, time - 24 * hourInMilliseconds), + '1 day' + ); + }); + + test('five hours remaining', () => { + const time = new Date().getTime(); + assert.strictEqual( + getDaysAndHoursRemainingInTrial(time, time - 43 * hourInMilliseconds), + '5 hours' + ); + }); + + test('one hour remaining', () => { + const time = new Date().getTime(); + assert.strictEqual( + getDaysAndHoursRemainingInTrial(time, time - 47 * hourInMilliseconds), + '1 hour' + ); + }); + + test('no time remaining', () => { + const time = new Date().getTime(); + assert.strictEqual( + getDaysAndHoursRemainingInTrial(time, time - 48 * hourInMilliseconds), + 'No time' + ); + }); +}); diff --git a/src/test/suite/utils/getHoursRemainingInTrial.test.ts b/src/test/suite/utils/getMillisecondsRemainingInTrial.test.ts similarity index 51% rename from src/test/suite/utils/getHoursRemainingInTrial.test.ts rename to src/test/suite/utils/getMillisecondsRemainingInTrial.test.ts index 0c95132d..6524e21c 100644 --- a/src/test/suite/utils/getHoursRemainingInTrial.test.ts +++ b/src/test/suite/utils/getMillisecondsRemainingInTrial.test.ts @@ -1,33 +1,33 @@ import * as assert from 'assert'; -import getHoursRemainingInTrial from '../../../utils/getHoursRemainingInTrial'; +import getMillisecondsRemainingInTrial from '../../../utils/getMillisecondsRemainingInTrial'; const hourInMilliseconds = 3600000; -suite('getHoursRemainingInTrial', () => { +suite('getMillisecondsRemainingInTrial', () => { test('entire trial remaining', () => { const time = new Date().getTime(); - assert.strictEqual(getHoursRemainingInTrial(time, time), 48); + assert.strictEqual(getMillisecondsRemainingInTrial(time, time), 172800000); }); test('one day remaining', () => { const time = new Date().getTime(); assert.strictEqual( - getHoursRemainingInTrial(time, time - 24 * hourInMilliseconds), - 24 + getMillisecondsRemainingInTrial(time, time - 24 * hourInMilliseconds), + 86400000 ); }); test('one hour remaining', () => { const time = new Date().getTime(); assert.strictEqual( - getHoursRemainingInTrial(time, time - 47 * hourInMilliseconds), - 1 + getMillisecondsRemainingInTrial(time, time - 47 * hourInMilliseconds), + 3600000 ); }); test('no time remaining', () => { const time = new Date().getTime(); assert.strictEqual( - getHoursRemainingInTrial(time, time - 48 * hourInMilliseconds), + getMillisecondsRemainingInTrial(time, time - 48 * hourInMilliseconds), 0 ); }); diff --git a/src/utils/getDaysAndHoursRemainingInTrial.ts b/src/utils/getDaysAndHoursRemainingInTrial.ts new file mode 100644 index 00000000..01daa439 --- /dev/null +++ b/src/utils/getDaysAndHoursRemainingInTrial.ts @@ -0,0 +1,49 @@ +import getMillisecondsRemainingInTrial from './getMillisecondsRemainingInTrial'; +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 getDaysAndHoursRemainingInTrial( + currentTimeStamp: number, + trialStartedTimeStamp: number | unknown +): string { + const defaultTime = 'No time'; + if (!trialStartedTimeStamp) { + return defaultTime; + } + + const millisecondsRemainingInTrial = getMillisecondsRemainingInTrial( + currentTimeStamp, + trialStartedTimeStamp + ); + const daysRemaining = Math.floor( + millisecondsRemainingInTrial / dayInMilliseconds + ); + const hoursRemaining = Math.ceil( + (millisecondsRemainingInTrial % dayInMilliseconds) / hourInMilliseconds + ); + + return millisecondsRemainingInTrial > 0 + ? [ + getTextForNumber( + `${daysRemaining} day`, + `${daysRemaining} days`, + daysRemaining + ), + getTextForNumber( + `${hoursRemaining} hour`, + `${hoursRemaining} hours`, + hoursRemaining + ), + ] + .filter((text) => text) + .join(', ') + : defaultTime; +} diff --git a/src/utils/getLicenseInformation.ts b/src/utils/getLicenseInformation.ts index 7da6679b..30dd1aee 100644 --- a/src/utils/getLicenseInformation.ts +++ b/src/utils/getLicenseInformation.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import getHoursRemainingInTrial from './getHoursRemainingInTrial'; +import getDaysAndHoursRemainingInTrial from './getDaysAndHoursRemainingInTrial'; import getLicenseErrorMessage from './getLicenseErrorMessage'; import isLicenseValid from './isLicenseValid'; import isTrialExpired from './isTrialExpired'; @@ -11,10 +11,6 @@ import { 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 { @@ -27,9 +23,10 @@ export default async function getLicenseInformation( const enterLicenseButton = ``; const changeLicenseButton = ``; const retryValidationButton = ``; + const takeSurveyButton = ``; const isValid = await isLicenseValid(context); - const previewExpired = isTrialExpired(previewStartedTimeStamp); + const isPreviewExpired = isTrialExpired(previewStartedTimeStamp); if (isValid) { return `

Your Local CI license key is valid!

@@ -40,11 +37,12 @@ export default async function getLicenseInformation( 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.

+

${takeSurveyButton}

${getLicenseLink}

${enterLicenseButton}

`; } - 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)) @@ -54,24 +52,20 @@ export default async function getLicenseInformation(

${retryValidationButton}

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

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

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

+

${takeSurveyButton}

${getLicenseLink}

${enterLicenseButton}

`; } - 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 - )}

+

${getDaysAndHoursRemainingInTrial( + new Date().getTime(), + previewStartedTimeStamp + )} left in the preview

+

${takeSurveyButton}

${getLicenseLink}

${enterLicenseButton}

`; } diff --git a/src/utils/getHoursRemainingInTrial.ts b/src/utils/getMillisecondsRemainingInTrial.ts similarity index 58% rename from src/utils/getHoursRemainingInTrial.ts rename to src/utils/getMillisecondsRemainingInTrial.ts index 499352b2..125e73f4 100644 --- a/src/utils/getHoursRemainingInTrial.ts +++ b/src/utils/getMillisecondsRemainingInTrial.ts @@ -1,15 +1,12 @@ import { TRIAL_LENGTH_IN_MILLISECONDS } from '../constants'; -const hourInMilliseconds = 3600000; -export default function getHoursRemainingInTrial( +export default function getMillisecondsRemainingInTrial( currentTimeStamp: number, trialStartedTimeStamp: number | unknown ): number { const previewTimeElapsed = currentTimeStamp - Number(trialStartedTimeStamp); return trialStartedTimeStamp - ? Math.ceil( - (TRIAL_LENGTH_IN_MILLISECONDS - previewTimeElapsed) / hourInMilliseconds - ) + ? TRIAL_LENGTH_IN_MILLISECONDS - previewTimeElapsed : 0; } diff --git a/src/utils/isTrialExpired.ts b/src/utils/isTrialExpired.ts index cef2319f..21d071cd 100644 --- a/src/utils/isTrialExpired.ts +++ b/src/utils/isTrialExpired.ts @@ -1,11 +1,14 @@ import { TRIAL_LENGTH_IN_MILLISECONDS } from '../constants'; +import getMillisecondsRemainingInTrial from './getMillisecondsRemainingInTrial'; export default function isTrialExpired( trialStartedTimeStamp: number | unknown ): boolean { return ( !trialStartedTimeStamp || - new Date().getTime() - Number(trialStartedTimeStamp) > - TRIAL_LENGTH_IN_MILLISECONDS + getMillisecondsRemainingInTrial( + new Date().getTime(), + trialStartedTimeStamp + ) < TRIAL_LENGTH_IN_MILLISECONDS ); } diff --git a/webview/index.js b/webview/index.js index a7cd1a8a..5945e58e 100644 --- a/webview/index.js +++ b/webview/index.js @@ -3,17 +3,21 @@ (function () { function addLicenseHandlers() { const vscode = acquireVsCodeApi(); - document - .getElementById('enter-license') - .addEventListener('click', () => - vscode.postMessage({ type: 'enterLicense' }) - ); + const elementListeners = { + 'enter-license': 'enterLicense', + 'retry-license-validation': 'retryLicenseValidation', + 'take-survey': 'takeSurvey', + }; - document - .getElementById('retry-license-validation') - .addEventListener('click', () => - vscode.postMessage({ type: 'retryLicenseValidation' }) - ); + Object.keys(elementListeners).forEach( + (elementId) => { + document + .getElementById(elementId) + .addEventListener('click', () => + vscode.postMessage({ type: elementListeners[elementId] }) + ); + } + ); } // Mainly copied from @wordpress/dom-ready https://github.com/WordPress/gutenberg/blob/3da717b8d0ac7d7821fc6d0475695ccf3ae2829f/packages/dom-ready/src/index.js#L31 From e853d3851a7471df69ed57fe9fec7d15faa74dc0 Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Wed, 1 Dec 2021 21:55:05 -0600 Subject: [PATCH 06/24] Extend the trial by 15 days on filling out the survey --- .nvmrc | 2 +- package-lock.json | 2 +- package.json | 2 +- src/classes/JobProvider.ts | 6 +- src/classes/LicenseProvider.ts | 27 ++++++++- src/constants/index.ts | 2 + src/extension.ts | 3 - .../getDaysAndHoursRemainingInTrial.test.ts | 55 +++++++++++++++--- .../getMillisecondsRemainingInTrial.test.ts | 24 ++++++-- src/test/suite/utils/isTrialExpired.test.ts | 56 ++++++++++++++++++- src/utils/getDaysAndHoursRemainingInTrial.ts | 51 +++-------------- src/utils/getLicenseInformation.ts | 33 +++++++---- src/utils/getMillisecondsRemainingInTrial.ts | 7 +-- src/utils/getPrettyPrintedTimeRemaining.ts | 52 +++++++++++++++++ src/utils/getTrialLength.ts | 14 +++++ src/utils/isTrialExpired.ts | 9 +-- webview/index.js | 31 +++++----- webview/vscode.css | 24 +++++--- 18 files changed, 291 insertions(+), 109 deletions(-) create mode 100644 src/utils/getPrettyPrintedTimeRemaining.ts create mode 100644 src/utils/getTrialLength.ts diff --git a/.nvmrc b/.nvmrc index 64f5a0a6..b6a7d89c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -node +16 diff --git a/package-lock.json b/package-lock.json index 370ef098..b0245aeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,7 @@ "webpack-cli": "^4.7.0" }, "engines": { - "node": ">=16", + "node": "=16", "vscode": "^1.59.0" } }, diff --git a/package.json b/package.json index 514e93f9..0e956823 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "qna": "https://github.com/getlocalci/local-ci/discussions", "engines": { "vscode": "^1.59.0", - "node": ">=16" + "node": "16" }, "os": [ "!win32" 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 fea88751..2a143fe5 100644 --- a/src/classes/LicenseProvider.ts +++ b/src/classes/LicenseProvider.ts @@ -1,8 +1,15 @@ import * as vscode from 'vscode'; -import { LICENSE_ERROR, SURVEY_URL } from '../constants'; +import { + HAS_EXTENDED_TRIAL, + LICENSE_ERROR, + SURVEY_URL, + TRIAL_STARTED_TIMESTAMP, + EXTENDED_TRIAL_LENGTH_IN_MILLISECONDS, +} from '../constants'; import getLicenseErrorMessage from '../utils/getLicenseErrorMessage'; import getLicenseInformation from '../utils/getLicenseInformation'; import isLicenseValid from '../utils/isLicenseValid'; +import getPrettyPrintedTimeRemaining from '../utils/getPrettyPrintedTimeRemaining'; import showLicenseInput from '../utils/showLicenseInput'; function getNonce() { @@ -37,7 +44,7 @@ export default class LicenseProvider implements vscode.WebviewViewProvider { localResourceRoots: [this.extensionUri], }; - await this.load(); + this.load(); webviewView.webview.onDidReceiveMessage(async (data) => { if (data.type === 'enterLicense') { @@ -68,8 +75,22 @@ export default class LicenseProvider implements vscode.WebviewViewProvider { } if (data.type === 'takeSurvey') { - // @todo: Set globalState to extend the preview length. + 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 4d182d80..4bb40880 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'; diff --git a/src/extension.ts b/src/extension.ts index ff0e1501..72be54da 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -22,7 +22,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'; @@ -255,6 +254,4 @@ export function activate(context: vscode.ExtensionContext): void { } }, }); - - getLicenseInformation(context); } diff --git a/src/test/suite/utils/getDaysAndHoursRemainingInTrial.test.ts b/src/test/suite/utils/getDaysAndHoursRemainingInTrial.test.ts index c61bee9d..e38c835b 100644 --- a/src/test/suite/utils/getDaysAndHoursRemainingInTrial.test.ts +++ b/src/test/suite/utils/getDaysAndHoursRemainingInTrial.test.ts @@ -1,22 +1,35 @@ import * as assert from 'assert'; +import { TRIAL_LENGTH_IN_MILLISECONDS } from '../../../constants'; import getDaysAndHoursRemainingInTrial from '../../../utils/getDaysAndHoursRemainingInTrial'; +const minuteInMilliseconds = 60000; const hourInMilliseconds = 3600000; + suite('getDaysAndHoursRemainingInTrial', () => { test('no trial started timestamp', () => { const time = new Date().getTime(); - assert.strictEqual(getDaysAndHoursRemainingInTrial(time, null), 'No time'); + assert.strictEqual( + getDaysAndHoursRemainingInTrial(time, null, TRIAL_LENGTH_IN_MILLISECONDS), + 'No time' + ); }); test('entire trial remaining', () => { const time = new Date().getTime(); - assert.strictEqual(getDaysAndHoursRemainingInTrial(time, time), '2 days'); + assert.strictEqual( + getDaysAndHoursRemainingInTrial(time, time, TRIAL_LENGTH_IN_MILLISECONDS), + '2 days' + ); }); test('entire trial remaining', () => { const time = new Date().getTime(); assert.strictEqual( - getDaysAndHoursRemainingInTrial(time, time - 10 * hourInMilliseconds), + getDaysAndHoursRemainingInTrial( + time, + time - 10 * hourInMilliseconds, + TRIAL_LENGTH_IN_MILLISECONDS + ), '1 day, 14 hours' ); }); @@ -24,7 +37,11 @@ suite('getDaysAndHoursRemainingInTrial', () => { test('one day remaining', () => { const time = new Date().getTime(); assert.strictEqual( - getDaysAndHoursRemainingInTrial(time, time - 24 * hourInMilliseconds), + getDaysAndHoursRemainingInTrial( + time, + time - 24 * hourInMilliseconds, + TRIAL_LENGTH_IN_MILLISECONDS + ), '1 day' ); }); @@ -32,7 +49,11 @@ suite('getDaysAndHoursRemainingInTrial', () => { test('five hours remaining', () => { const time = new Date().getTime(); assert.strictEqual( - getDaysAndHoursRemainingInTrial(time, time - 43 * hourInMilliseconds), + getDaysAndHoursRemainingInTrial( + time, + time - 43 * hourInMilliseconds, + TRIAL_LENGTH_IN_MILLISECONDS + ), '5 hours' ); }); @@ -40,15 +61,35 @@ suite('getDaysAndHoursRemainingInTrial', () => { test('one hour remaining', () => { const time = new Date().getTime(); assert.strictEqual( - getDaysAndHoursRemainingInTrial(time, time - 47 * hourInMilliseconds), + getDaysAndHoursRemainingInTrial( + time, + time - 47 * hourInMilliseconds, + TRIAL_LENGTH_IN_MILLISECONDS + ), '1 hour' ); }); + test('one minute remaining', () => { + const time = new Date().getTime(); + assert.strictEqual( + getDaysAndHoursRemainingInTrial( + time, + time - 47 * hourInMilliseconds - 59 * minuteInMilliseconds, + TRIAL_LENGTH_IN_MILLISECONDS + ), + '1 minute' + ); + }); + test('no time remaining', () => { const time = new Date().getTime(); assert.strictEqual( - getDaysAndHoursRemainingInTrial(time, time - 48 * hourInMilliseconds), + getDaysAndHoursRemainingInTrial( + time, + time - 48 * hourInMilliseconds, + TRIAL_LENGTH_IN_MILLISECONDS + ), 'No time' ); }); diff --git a/src/test/suite/utils/getMillisecondsRemainingInTrial.test.ts b/src/test/suite/utils/getMillisecondsRemainingInTrial.test.ts index 6524e21c..86d8df21 100644 --- a/src/test/suite/utils/getMillisecondsRemainingInTrial.test.ts +++ b/src/test/suite/utils/getMillisecondsRemainingInTrial.test.ts @@ -1,17 +1,25 @@ 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), 172800000); + assert.strictEqual( + getMillisecondsRemainingInTrial(time, time, TRIAL_LENGTH_IN_MILLISECONDS), + 172800000 + ); }); test('one day remaining', () => { const time = new Date().getTime(); assert.strictEqual( - getMillisecondsRemainingInTrial(time, time - 24 * hourInMilliseconds), + getMillisecondsRemainingInTrial( + time, + time - 24 * hourInMilliseconds, + TRIAL_LENGTH_IN_MILLISECONDS + ), 86400000 ); }); @@ -19,7 +27,11 @@ suite('getMillisecondsRemainingInTrial', () => { test('one hour remaining', () => { const time = new Date().getTime(); assert.strictEqual( - getMillisecondsRemainingInTrial(time, time - 47 * hourInMilliseconds), + getMillisecondsRemainingInTrial( + time, + time - 47 * hourInMilliseconds, + TRIAL_LENGTH_IN_MILLISECONDS + ), 3600000 ); }); @@ -27,7 +39,11 @@ suite('getMillisecondsRemainingInTrial', () => { test('no time remaining', () => { const time = new Date().getTime(); assert.strictEqual( - getMillisecondsRemainingInTrial(time, time - 48 * hourInMilliseconds), + getMillisecondsRemainingInTrial( + time, + time - 48 * hourInMilliseconds, + TRIAL_LENGTH_IN_MILLISECONDS + ), 0 ); }); diff --git a/src/test/suite/utils/isTrialExpired.test.ts b/src/test/suite/utils/isTrialExpired.test.ts index a23ab8a4..0cc8d79c 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 a 17 days and 1 millisecond ago and was extended', () => { + assert.strictEqual( + isTrialExpired(new Date().getTime() - 1468800001, extendedTrial), + true + ); }); }); diff --git a/src/utils/getDaysAndHoursRemainingInTrial.ts b/src/utils/getDaysAndHoursRemainingInTrial.ts index 01daa439..81b5be15 100644 --- a/src/utils/getDaysAndHoursRemainingInTrial.ts +++ b/src/utils/getDaysAndHoursRemainingInTrial.ts @@ -1,49 +1,16 @@ import getMillisecondsRemainingInTrial from './getMillisecondsRemainingInTrial'; -const hourInMilliseconds = 3600000; -const dayInMilliseconds = 86400000; - -function getTextForNumber(singular: string, plural: string, count: number) { - if (!count) { - return ''; - } - - return count === 1 ? singular : plural; -} +import getPrettyPrintedTimeRemaining from './getPrettyPrintedTimeRemaining'; export default function getDaysAndHoursRemainingInTrial( currentTimeStamp: number, - trialStartedTimeStamp: number | unknown + trialStartedTimeStamp: number | unknown, + trialLengthInMilliseconds: number ): string { - const defaultTime = 'No time'; - if (!trialStartedTimeStamp) { - return defaultTime; - } - - const millisecondsRemainingInTrial = getMillisecondsRemainingInTrial( - currentTimeStamp, - trialStartedTimeStamp + return getPrettyPrintedTimeRemaining( + getMillisecondsRemainingInTrial( + currentTimeStamp, + trialStartedTimeStamp, + trialLengthInMilliseconds + ) ); - const daysRemaining = Math.floor( - millisecondsRemainingInTrial / dayInMilliseconds - ); - const hoursRemaining = Math.ceil( - (millisecondsRemainingInTrial % dayInMilliseconds) / hourInMilliseconds - ); - - return millisecondsRemainingInTrial > 0 - ? [ - getTextForNumber( - `${daysRemaining} day`, - `${daysRemaining} days`, - daysRemaining - ), - getTextForNumber( - `${hoursRemaining} hour`, - `${hoursRemaining} hours`, - hoursRemaining - ), - ] - .filter((text) => text) - .join(', ') - : defaultTime; } diff --git a/src/utils/getLicenseInformation.ts b/src/utils/getLicenseInformation.ts index 30dd1aee..3f6678ea 100644 --- a/src/utils/getLicenseInformation.ts +++ b/src/utils/getLicenseInformation.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; -import getDaysAndHoursRemainingInTrial from './getDaysAndHoursRemainingInTrial'; import getLicenseErrorMessage from './getLicenseErrorMessage'; +import getPrettyPrintedTimeRemaining from './getPrettyPrintedTimeRemaining'; +import getTrialLength from './getTrialLength'; import isLicenseValid from './isLicenseValid'; import isTrialExpired from './isTrialExpired'; import { @@ -9,6 +10,7 @@ import { LICENSE_KEY, LICENSE_VALIDITY, TRIAL_STARTED_TIMESTAMP, + HAS_EXTENDED_TRIAL, } from '../constants'; export default async function getLicenseInformation( @@ -19,25 +21,34 @@ export default async function getLicenseInformation( TRIAL_STARTED_TIMESTAMP ); 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 takeSurveyButton = `Get 15 more free days by taking a 2-minute anonymous survey`; const isValid = await isLicenseValid(context); - const isPreviewExpired = isTrialExpired(previewStartedTimeStamp); + const hasExtendedTrial = !!context.globalState.get(HAS_EXTENDED_TRIAL); + const trialLengthInMilliseconds = getTrialLength(context); + const isPreviewExpired = isTrialExpired( + previewStartedTimeStamp, + trialLengthInMilliseconds + ); if (isValid) { return `

Your Local CI license key is valid!

${changeLicenseButton}`; } + const daysAndHoursRemainingInTrial = getPrettyPrintedTimeRemaining( + trialLengthInMilliseconds + ); + 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.

-

${takeSurveyButton}

+

This free trial will last for ${daysAndHoursRemainingInTrial}.

+ ${hasExtendedTrial ? '' : `

${takeSurveyButton}

`}

${getLicenseLink}

${enterLicenseButton}

`; } @@ -49,23 +60,21 @@ export default async function getLicenseInformation( )}

${getLicenseLink}

${enterLicenseButton}

+ ${hasExtendedTrial ? '' : `

${takeSurveyButton}

`}

${retryValidationButton}

`; } if (isPreviewExpired) { return `

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

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

-

${takeSurveyButton}

+ ${hasExtendedTrial ? '' : `

${takeSurveyButton}

`}

${getLicenseLink}

${enterLicenseButton}

`; } return `

Thanks for previewing Local CI!

-

${getDaysAndHoursRemainingInTrial( - new Date().getTime(), - previewStartedTimeStamp - )} left in the preview

-

${takeSurveyButton}

+

${daysAndHoursRemainingInTrial} left in the free preview.

+ ${hasExtendedTrial ? '' : `

${takeSurveyButton}

`}

${getLicenseLink}

${enterLicenseButton}

`; } diff --git a/src/utils/getMillisecondsRemainingInTrial.ts b/src/utils/getMillisecondsRemainingInTrial.ts index 125e73f4..4fbaf2ca 100644 --- a/src/utils/getMillisecondsRemainingInTrial.ts +++ b/src/utils/getMillisecondsRemainingInTrial.ts @@ -1,12 +1,11 @@ -import { TRIAL_LENGTH_IN_MILLISECONDS } from '../constants'; - export default function getMillisecondsRemainingInTrial( currentTimeStamp: number, - trialStartedTimeStamp: number | unknown + trialStartedTimeStamp: number | unknown, + trialLengthInMilliseconds: number ): number { const previewTimeElapsed = currentTimeStamp - Number(trialStartedTimeStamp); return trialStartedTimeStamp - ? TRIAL_LENGTH_IN_MILLISECONDS - previewTimeElapsed + ? trialLengthInMilliseconds - previewTimeElapsed : 0; } diff --git a/src/utils/getPrettyPrintedTimeRemaining.ts b/src/utils/getPrettyPrintedTimeRemaining.ts new file mode 100644 index 00000000..65c8df51 --- /dev/null +++ b/src/utils/getPrettyPrintedTimeRemaining.ts @@ -0,0 +1,52 @@ +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 { + const defaultTime = 'No time'; + if (millisecondsRemaining <= 0) { + return defaultTime; + } + + if (millisecondsRemaining < hourInMilliseconds) { + const minutesRemaining = Math.floor( + millisecondsRemaining / minuteInMilliseconds + ); + + return getTextForNumber( + `${minutesRemaining} minute`, + `${minutesRemaining} minutes`, + minutesRemaining + ); + } + + const daysRemaining = Math.floor(millisecondsRemaining / dayInMilliseconds); + const hoursRemaining = Math.floor( + (millisecondsRemaining % dayInMilliseconds) / hourInMilliseconds + ); + + return [ + getTextForNumber( + `${daysRemaining} day`, + `${daysRemaining} days`, + daysRemaining + ), + getTextForNumber( + `${hoursRemaining} hour`, + `${hoursRemaining} hours`, + hoursRemaining + ), + ] + .filter((text) => text) + .join(', '); +} 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 21d071cd..eb804000 100644 --- a/src/utils/isTrialExpired.ts +++ b/src/utils/isTrialExpired.ts @@ -1,14 +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 || getMillisecondsRemainingInTrial( new Date().getTime(), - trialStartedTimeStamp - ) < TRIAL_LENGTH_IN_MILLISECONDS + trialStartedTimeStamp, + trialLengthInMilliseconds + ) <= 0 ); } diff --git a/webview/index.js b/webview/index.js index 5945e58e..70db278c 100644 --- a/webview/index.js +++ b/webview/index.js @@ -3,21 +3,24 @@ (function () { function addLicenseHandlers() { const vscode = acquireVsCodeApi(); - const elementListeners = { - 'enter-license': 'enterLicense', - 'retry-license-validation': 'retryLicenseValidation', - 'take-survey': 'takeSurvey', - }; - Object.keys(elementListeners).forEach( - (elementId) => { - document - .getElementById(elementId) - .addEventListener('click', () => - vscode.postMessage({ type: elementListeners[elementId] }) - ); - } - ); + document + .getElementById('take-survey') + .addEventListener('click', () => + vscode.postMessage({ type: 'takeSurvey' }) + ); + + document + .getElementById('enter-license') + .addEventListener('click', () => + vscode.postMessage({ type: 'enterLicense' }) + ); + + document + .getElementById('retry-license-validation') + .addEventListener('click', () => + vscode.postMessage({ type: 'retryLicenseValidation' }) + ); } // 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..316ebcfb 100644 --- a/webview/vscode.css +++ b/webview/vscode.css @@ -34,10 +34,25 @@ a.button { text-align: center; outline: 1px solid transparent; outline-offset: 2px !important; +} + +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 +63,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); -} From 9aefcd22146ebe8a201c8d091fc105cadb7264c1 Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Wed, 1 Dec 2021 21:59:16 -0600 Subject: [PATCH 07/24] Remove = before Node version Also, remove await before showLicenseInput(). --- package-lock.json | 2 +- src/classes/LicenseProvider.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index b0245aeb..bd417712 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,7 @@ "webpack-cli": "^4.7.0" }, "engines": { - "node": "=16", + "node": "16", "vscode": "^1.59.0" } }, diff --git a/src/classes/LicenseProvider.ts b/src/classes/LicenseProvider.ts index 2a143fe5..c15f538b 100644 --- a/src/classes/LicenseProvider.ts +++ b/src/classes/LicenseProvider.ts @@ -48,7 +48,7 @@ export default class LicenseProvider implements vscode.WebviewViewProvider { webviewView.webview.onDidReceiveMessage(async (data) => { if (data.type === 'enterLicense') { - await showLicenseInput( + showLicenseInput( this.context, () => this.load(), () => this.licenseSuccessCallback() From a2abc588c1c909ebf62d771465c54a5cf26a62a6 Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Wed, 1 Dec 2021 22:33:08 -0600 Subject: [PATCH 08/24] Bump vscode-test to the latest --- package-lock.json | 14 ++++---- package.json | 2 +- ...est.ts => getTimeRemainingInTrial.test.ts} | 26 +++++++------- src/utils/getLicenseInformation.ts | 15 ++++---- src/utils/getPrettyPrintedTimeRemaining.ts | 34 ++++++++----------- ...gInTrial.ts => getTimeRemainingInTrial.ts} | 2 +- 6 files changed, 46 insertions(+), 47 deletions(-) rename src/test/suite/utils/{getDaysAndHoursRemainingInTrial.test.ts => getTimeRemainingInTrial.test.ts} (73%) rename src/utils/{getDaysAndHoursRemainingInTrial.ts => getTimeRemainingInTrial.ts} (88%) diff --git a/package-lock.json b/package-lock.json index bd417712..aa313e98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,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" }, @@ -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 0e956823..46fc0d29 100644 --- a/package.json +++ b/package.json @@ -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/test/suite/utils/getDaysAndHoursRemainingInTrial.test.ts b/src/test/suite/utils/getTimeRemainingInTrial.test.ts similarity index 73% rename from src/test/suite/utils/getDaysAndHoursRemainingInTrial.test.ts rename to src/test/suite/utils/getTimeRemainingInTrial.test.ts index e38c835b..cae2c292 100644 --- a/src/test/suite/utils/getDaysAndHoursRemainingInTrial.test.ts +++ b/src/test/suite/utils/getTimeRemainingInTrial.test.ts @@ -1,15 +1,15 @@ import * as assert from 'assert'; import { TRIAL_LENGTH_IN_MILLISECONDS } from '../../../constants'; -import getDaysAndHoursRemainingInTrial from '../../../utils/getDaysAndHoursRemainingInTrial'; +import getTimeRemainingInTrial from '../../../utils/getTimeRemainingInTrial'; const minuteInMilliseconds = 60000; const hourInMilliseconds = 3600000; -suite('getDaysAndHoursRemainingInTrial', () => { +suite('getTimeRemainingInTrial', () => { test('no trial started timestamp', () => { const time = new Date().getTime(); assert.strictEqual( - getDaysAndHoursRemainingInTrial(time, null, TRIAL_LENGTH_IN_MILLISECONDS), + getTimeRemainingInTrial(time, null, TRIAL_LENGTH_IN_MILLISECONDS), 'No time' ); }); @@ -17,27 +17,27 @@ suite('getDaysAndHoursRemainingInTrial', () => { test('entire trial remaining', () => { const time = new Date().getTime(); assert.strictEqual( - getDaysAndHoursRemainingInTrial(time, time, TRIAL_LENGTH_IN_MILLISECONDS), + getTimeRemainingInTrial(time, time, TRIAL_LENGTH_IN_MILLISECONDS), '2 days' ); }); - test('entire trial remaining', () => { + test('1 day, 14 hours remaining', () => { const time = new Date().getTime(); assert.strictEqual( - getDaysAndHoursRemainingInTrial( + getTimeRemainingInTrial( time, time - 10 * hourInMilliseconds, TRIAL_LENGTH_IN_MILLISECONDS ), - '1 day, 14 hours' + '1 day' ); }); - test('one day remaining', () => { + test('exactly one day remaining', () => { const time = new Date().getTime(); assert.strictEqual( - getDaysAndHoursRemainingInTrial( + getTimeRemainingInTrial( time, time - 24 * hourInMilliseconds, TRIAL_LENGTH_IN_MILLISECONDS @@ -49,7 +49,7 @@ suite('getDaysAndHoursRemainingInTrial', () => { test('five hours remaining', () => { const time = new Date().getTime(); assert.strictEqual( - getDaysAndHoursRemainingInTrial( + getTimeRemainingInTrial( time, time - 43 * hourInMilliseconds, TRIAL_LENGTH_IN_MILLISECONDS @@ -61,7 +61,7 @@ suite('getDaysAndHoursRemainingInTrial', () => { test('one hour remaining', () => { const time = new Date().getTime(); assert.strictEqual( - getDaysAndHoursRemainingInTrial( + getTimeRemainingInTrial( time, time - 47 * hourInMilliseconds, TRIAL_LENGTH_IN_MILLISECONDS @@ -73,7 +73,7 @@ suite('getDaysAndHoursRemainingInTrial', () => { test('one minute remaining', () => { const time = new Date().getTime(); assert.strictEqual( - getDaysAndHoursRemainingInTrial( + getTimeRemainingInTrial( time, time - 47 * hourInMilliseconds - 59 * minuteInMilliseconds, TRIAL_LENGTH_IN_MILLISECONDS @@ -85,7 +85,7 @@ suite('getDaysAndHoursRemainingInTrial', () => { test('no time remaining', () => { const time = new Date().getTime(); assert.strictEqual( - getDaysAndHoursRemainingInTrial( + getTimeRemainingInTrial( time, time - 48 * hourInMilliseconds, TRIAL_LENGTH_IN_MILLISECONDS diff --git a/src/utils/getLicenseInformation.ts b/src/utils/getLicenseInformation.ts index 3f6678ea..64eecbfd 100644 --- a/src/utils/getLicenseInformation.ts +++ b/src/utils/getLicenseInformation.ts @@ -12,6 +12,7 @@ import { TRIAL_STARTED_TIMESTAMP, HAS_EXTENDED_TRIAL, } from '../constants'; +import getTimeRemainingInTrial from './getTimeRemainingInTrial'; export default async function getLicenseInformation( context: vscode.ExtensionContext @@ -40,14 +41,12 @@ export default async function getLicenseInformation( ${changeLicenseButton}`; } - const daysAndHoursRemainingInTrial = getPrettyPrintedTimeRemaining( - trialLengthInMilliseconds - ); - if (!previewStartedTimeStamp && !licenseKey) { context.globalState.update(TRIAL_STARTED_TIMESTAMP, new Date().getTime()); return `

Thanks for previewing Local CI!

-

This free trial will last for ${daysAndHoursRemainingInTrial}.

+

This free trial will last for ${getPrettyPrintedTimeRemaining( + trialLengthInMilliseconds + )}.

${hasExtendedTrial ? '' : `

${takeSurveyButton}

`}

${getLicenseLink}

${enterLicenseButton}

`; @@ -73,7 +72,11 @@ export default async function getLicenseInformation( } return `

Thanks for previewing Local CI!

-

${daysAndHoursRemainingInTrial} left in the free preview.

+

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

${hasExtendedTrial ? '' : `

${takeSurveyButton}

`}

${getLicenseLink}

${enterLicenseButton}

`; diff --git a/src/utils/getPrettyPrintedTimeRemaining.ts b/src/utils/getPrettyPrintedTimeRemaining.ts index 65c8df51..802d0199 100644 --- a/src/utils/getPrettyPrintedTimeRemaining.ts +++ b/src/utils/getPrettyPrintedTimeRemaining.ts @@ -3,10 +3,6 @@ const hourInMilliseconds = 3600000; const dayInMilliseconds = 86400000; function getTextForNumber(singular: string, plural: string, count: number) { - if (!count) { - return ''; - } - return count === 1 ? singular : plural; } @@ -30,23 +26,23 @@ export default function getPrettyPrintedTimeRemaining( ); } - const daysRemaining = Math.floor(millisecondsRemaining / dayInMilliseconds); - const hoursRemaining = Math.floor( - (millisecondsRemaining % dayInMilliseconds) / hourInMilliseconds - ); + if (millisecondsRemaining < dayInMilliseconds) { + const hoursRemaining = Math.floor( + millisecondsRemaining / hourInMilliseconds + ); - return [ - getTextForNumber( - `${daysRemaining} day`, - `${daysRemaining} days`, - daysRemaining - ), - getTextForNumber( + return getTextForNumber( `${hoursRemaining} hour`, `${hoursRemaining} hours`, hoursRemaining - ), - ] - .filter((text) => text) - .join(', '); + ); + } + + const daysRemaining = Math.floor(millisecondsRemaining / dayInMilliseconds); + + return getTextForNumber( + `${daysRemaining} day`, + `${daysRemaining} days`, + daysRemaining + ); } diff --git a/src/utils/getDaysAndHoursRemainingInTrial.ts b/src/utils/getTimeRemainingInTrial.ts similarity index 88% rename from src/utils/getDaysAndHoursRemainingInTrial.ts rename to src/utils/getTimeRemainingInTrial.ts index 81b5be15..4c7f7bc7 100644 --- a/src/utils/getDaysAndHoursRemainingInTrial.ts +++ b/src/utils/getTimeRemainingInTrial.ts @@ -1,7 +1,7 @@ import getMillisecondsRemainingInTrial from './getMillisecondsRemainingInTrial'; import getPrettyPrintedTimeRemaining from './getPrettyPrintedTimeRemaining'; -export default function getDaysAndHoursRemainingInTrial( +export default function getTimeRemainingInTrial( currentTimeStamp: number, trialStartedTimeStamp: number | unknown, trialLengthInMilliseconds: number From 731fd366b815ea0e38f595a6b03bd00db0b34dc1 Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Wed, 1 Dec 2021 23:53:18 -0600 Subject: [PATCH 09/24] Have an hour count at less than 2 days --- src/classes/LicenseProvider.ts | 4 +- .../utils/getTimeRemainingInTrial.test.ts | 59 +++++++++++++------ src/utils/getPrettyPrintedTimeRemaining.ts | 28 ++++++--- 3 files changed, 64 insertions(+), 27 deletions(-) diff --git a/src/classes/LicenseProvider.ts b/src/classes/LicenseProvider.ts index c15f538b..2acc6fd6 100644 --- a/src/classes/LicenseProvider.ts +++ b/src/classes/LicenseProvider.ts @@ -1,15 +1,15 @@ import * as vscode from 'vscode'; import { + EXTENDED_TRIAL_LENGTH_IN_MILLISECONDS, HAS_EXTENDED_TRIAL, LICENSE_ERROR, SURVEY_URL, TRIAL_STARTED_TIMESTAMP, - EXTENDED_TRIAL_LENGTH_IN_MILLISECONDS, } from '../constants'; import getLicenseErrorMessage from '../utils/getLicenseErrorMessage'; import getLicenseInformation from '../utils/getLicenseInformation'; -import isLicenseValid from '../utils/isLicenseValid'; import getPrettyPrintedTimeRemaining from '../utils/getPrettyPrintedTimeRemaining'; +import isLicenseValid from '../utils/isLicenseValid'; import showLicenseInput from '../utils/showLicenseInput'; function getNonce() { diff --git a/src/test/suite/utils/getTimeRemainingInTrial.test.ts b/src/test/suite/utils/getTimeRemainingInTrial.test.ts index cae2c292..bc506802 100644 --- a/src/test/suite/utils/getTimeRemainingInTrial.test.ts +++ b/src/test/suite/utils/getTimeRemainingInTrial.test.ts @@ -4,91 +4,114 @@ 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', () => { - const time = new Date().getTime(); assert.strictEqual( getTimeRemainingInTrial(time, null, TRIAL_LENGTH_IN_MILLISECONDS), 'No time' ); }); - test('entire trial remaining', () => { - const time = new Date().getTime(); + test('14 days and 1 hour remaining', () => { assert.strictEqual( - getTimeRemainingInTrial(time, time, TRIAL_LENGTH_IN_MILLISECONDS), + getTimeRemainingInTrial( + time, + time, + 14 * dayInMilliseconds + hourInMilliseconds + ), + '14 days' + ); + }); + + 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', () => { - const time = new Date().getTime(); assert.strictEqual( getTimeRemainingInTrial( time, time - 10 * hourInMilliseconds, - TRIAL_LENGTH_IN_MILLISECONDS + dayInMilliseconds * 2 ), - '1 day' + '1 day, 14 hours' ); }); test('exactly one day remaining', () => { - const time = new Date().getTime(); assert.strictEqual( getTimeRemainingInTrial( time, time - 24 * hourInMilliseconds, - TRIAL_LENGTH_IN_MILLISECONDS + dayInMilliseconds * 2 ), '1 day' ); }); test('five hours remaining', () => { - const time = new Date().getTime(); assert.strictEqual( getTimeRemainingInTrial( time, time - 43 * hourInMilliseconds, - TRIAL_LENGTH_IN_MILLISECONDS + dayInMilliseconds * 2 ), '5 hours' ); }); test('one hour remaining', () => { - const time = new Date().getTime(); assert.strictEqual( getTimeRemainingInTrial( time, time - 47 * hourInMilliseconds, - TRIAL_LENGTH_IN_MILLISECONDS + dayInMilliseconds * 2 ), '1 hour' ); }); - test('one minute remaining', () => { - const time = new Date().getTime(); + 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, - TRIAL_LENGTH_IN_MILLISECONDS + dayInMilliseconds * 2 ), '1 minute' ); }); test('no time remaining', () => { - const time = new Date().getTime(); assert.strictEqual( getTimeRemainingInTrial( time, time - 48 * hourInMilliseconds, - TRIAL_LENGTH_IN_MILLISECONDS + dayInMilliseconds * 2 ), 'No time' ); diff --git a/src/utils/getPrettyPrintedTimeRemaining.ts b/src/utils/getPrettyPrintedTimeRemaining.ts index 802d0199..740cd3c6 100644 --- a/src/utils/getPrettyPrintedTimeRemaining.ts +++ b/src/utils/getPrettyPrintedTimeRemaining.ts @@ -3,6 +3,10 @@ const hourInMilliseconds = 3600000; const dayInMilliseconds = 86400000; function getTextForNumber(singular: string, plural: string, count: number) { + if (!count) { + return ''; + } + return count === 1 ? singular : plural; } @@ -26,16 +30,26 @@ export default function getPrettyPrintedTimeRemaining( ); } - if (millisecondsRemaining < dayInMilliseconds) { + if (millisecondsRemaining < 2 * dayInMilliseconds) { + const daysRemaining = Math.floor(millisecondsRemaining / dayInMilliseconds); const hoursRemaining = Math.floor( - millisecondsRemaining / hourInMilliseconds + (millisecondsRemaining % dayInMilliseconds) / hourInMilliseconds ); - return getTextForNumber( - `${hoursRemaining} hour`, - `${hoursRemaining} hours`, - hoursRemaining - ); + return [ + getTextForNumber( + `${daysRemaining} day`, + `${daysRemaining} days`, + daysRemaining + ), + getTextForNumber( + `${hoursRemaining} hour`, + `${hoursRemaining} hours`, + hoursRemaining + ), + ] + .filter((timeRemaining) => timeRemaining) + .join(', '); } const daysRemaining = Math.floor(millisecondsRemaining / dayInMilliseconds); From 8bcf36bbb9a20c0bb71fef0b77a0cd545e2a26a4 Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Thu, 2 Dec 2021 00:09:16 -0600 Subject: [PATCH 10/24] If there's less than a minute remaining, show 'No time' --- .../utils/getMillisecondsRemainingInTrial.test.ts | 4 ++-- .../suite/utils/getTimeRemainingInTrial.test.ts | 13 ++++++++++++- src/test/suite/utils/isTrialExpired.test.ts | 2 +- src/utils/getPrettyPrintedTimeRemaining.ts | 2 +- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/test/suite/utils/getMillisecondsRemainingInTrial.test.ts b/src/test/suite/utils/getMillisecondsRemainingInTrial.test.ts index 86d8df21..973bf3eb 100644 --- a/src/test/suite/utils/getMillisecondsRemainingInTrial.test.ts +++ b/src/test/suite/utils/getMillisecondsRemainingInTrial.test.ts @@ -12,7 +12,7 @@ suite('getMillisecondsRemainingInTrial', () => { ); }); - test('one day remaining', () => { + test('1 day remaining', () => { const time = new Date().getTime(); assert.strictEqual( getMillisecondsRemainingInTrial( @@ -24,7 +24,7 @@ suite('getMillisecondsRemainingInTrial', () => { ); }); - test('one hour remaining', () => { + test('1 hour remaining', () => { const time = new Date().getTime(); assert.strictEqual( getMillisecondsRemainingInTrial( diff --git a/src/test/suite/utils/getTimeRemainingInTrial.test.ts b/src/test/suite/utils/getTimeRemainingInTrial.test.ts index bc506802..aac12a93 100644 --- a/src/test/suite/utils/getTimeRemainingInTrial.test.ts +++ b/src/test/suite/utils/getTimeRemainingInTrial.test.ts @@ -51,7 +51,7 @@ suite('getTimeRemainingInTrial', () => { ); }); - test('exactly one day remaining', () => { + test('exactly 1 day remaining', () => { assert.strictEqual( getTimeRemainingInTrial( time, @@ -106,6 +106,17 @@ suite('getTimeRemainingInTrial', () => { ); }); + 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( diff --git a/src/test/suite/utils/isTrialExpired.test.ts b/src/test/suite/utils/isTrialExpired.test.ts index 0cc8d79c..0a7fa627 100644 --- a/src/test/suite/utils/isTrialExpired.test.ts +++ b/src/test/suite/utils/isTrialExpired.test.ts @@ -63,7 +63,7 @@ suite('isTrialExpired', () => { ); }); - test('preview began a 17 days and 1 millisecond ago and was extended', () => { + 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/getPrettyPrintedTimeRemaining.ts b/src/utils/getPrettyPrintedTimeRemaining.ts index 740cd3c6..48a65821 100644 --- a/src/utils/getPrettyPrintedTimeRemaining.ts +++ b/src/utils/getPrettyPrintedTimeRemaining.ts @@ -14,7 +14,7 @@ export default function getPrettyPrintedTimeRemaining( millisecondsRemaining: number ): string { const defaultTime = 'No time'; - if (millisecondsRemaining <= 0) { + if (millisecondsRemaining < minuteInMilliseconds) { return defaultTime; } From a6053dbfd8734637d565a47c491d9c62e32c8815 Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Thu, 2 Dec 2021 00:16:38 -0600 Subject: [PATCH 11/24] Prevent a JS error in addLicenseHandlers() Before, there was an error when the button wasn't in the DOM. --- webview/index.js | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/webview/index.js b/webview/index.js index 70db278c..7dffb96b 100644 --- a/webview/index.js +++ b/webview/index.js @@ -3,24 +3,20 @@ (function () { function addLicenseHandlers() { const vscode = acquireVsCodeApi(); + const listenerElements = { + 'take-survey': 'takeSurvey', + 'enter-license': 'enterLicense', + 'retry-license-validation': 'retryLicenseValidation', + }; - document - .getElementById('take-survey') - .addEventListener('click', () => - vscode.postMessage({ type: 'takeSurvey' }) - ); - - document - .getElementById('enter-license') - .addEventListener('click', () => - vscode.postMessage({ type: 'enterLicense' }) - ); - - 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 From fdddfc394194ad5bfe1f0202cbfba90d4d1fd96c Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Thu, 2 Dec 2021 00:22:06 -0600 Subject: [PATCH 12/24] Always show the hours if there are any after days Otherwise, 17 days immediately goes down to 16 days, as it's Math.floor(). --- .../utils/getTimeRemainingInTrial.test.ts | 2 +- src/utils/getPrettyPrintedTimeRemaining.ts | 44 +++++++------------ 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/src/test/suite/utils/getTimeRemainingInTrial.test.ts b/src/test/suite/utils/getTimeRemainingInTrial.test.ts index aac12a93..6d6914d0 100644 --- a/src/test/suite/utils/getTimeRemainingInTrial.test.ts +++ b/src/test/suite/utils/getTimeRemainingInTrial.test.ts @@ -22,7 +22,7 @@ suite('getTimeRemainingInTrial', () => { time, 14 * dayInMilliseconds + hourInMilliseconds ), - '14 days' + '14 days, 1 hour' ); }); diff --git a/src/utils/getPrettyPrintedTimeRemaining.ts b/src/utils/getPrettyPrintedTimeRemaining.ts index 48a65821..4af2479e 100644 --- a/src/utils/getPrettyPrintedTimeRemaining.ts +++ b/src/utils/getPrettyPrintedTimeRemaining.ts @@ -30,33 +30,23 @@ export default function getPrettyPrintedTimeRemaining( ); } - if (millisecondsRemaining < 2 * dayInMilliseconds) { - const daysRemaining = Math.floor(millisecondsRemaining / dayInMilliseconds); - const hoursRemaining = Math.floor( - (millisecondsRemaining % dayInMilliseconds) / hourInMilliseconds - ); - - return [ - getTextForNumber( - `${daysRemaining} day`, - `${daysRemaining} days`, - daysRemaining - ), - getTextForNumber( - `${hoursRemaining} hour`, - `${hoursRemaining} hours`, - hoursRemaining - ), - ] - .filter((timeRemaining) => timeRemaining) - .join(', '); - } - const daysRemaining = Math.floor(millisecondsRemaining / dayInMilliseconds); - - return getTextForNumber( - `${daysRemaining} day`, - `${daysRemaining} days`, - daysRemaining + const hoursRemaining = Math.floor( + (millisecondsRemaining % dayInMilliseconds) / hourInMilliseconds ); + + return [ + getTextForNumber( + `${daysRemaining} day`, + `${daysRemaining} days`, + daysRemaining + ), + getTextForNumber( + `${hoursRemaining} hour`, + `${hoursRemaining} hours`, + hoursRemaining + ), + ] + .filter((timeRemaining) => timeRemaining) + .join(', '); } From 53ce8ade6acc278625887e37af084def79535ebf Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Thu, 2 Dec 2021 00:40:33 -0600 Subject: [PATCH 13/24] Close the

tag, add a dynamic number of days --- src/utils/getLicenseInformation.ts | 16 ++++++++++------ webview/vscode.css | 1 + 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/utils/getLicenseInformation.ts b/src/utils/getLicenseInformation.ts index 64eecbfd..9022c5bc 100644 --- a/src/utils/getLicenseInformation.ts +++ b/src/utils/getLicenseInformation.ts @@ -11,6 +11,7 @@ import { LICENSE_VALIDITY, TRIAL_STARTED_TIMESTAMP, HAS_EXTENDED_TRIAL, + EXTENDED_TRIAL_LENGTH_IN_MILLISECONDS, } from '../constants'; import getTimeRemainingInTrial from './getTimeRemainingInTrial'; @@ -21,12 +22,15 @@ export default async function getLicenseInformation( const previewStartedTimeStamp = context.globalState.get( TRIAL_STARTED_TIMESTAMP ); + const daysInMilliseconds = 86400000; const licenseKey = await context.secrets.get(LICENSE_KEY); const getLicenseLink = `Buy license`; const enterLicenseButton = ``; const changeLicenseButton = ``; const retryValidationButton = ``; - const takeSurveyButton = `Get 15 more free days by taking a 2-minute anonymous survey`; + const takeSurveyButton = `Get ${ + EXTENDED_TRIAL_LENGTH_IN_MILLISECONDS / daysInMilliseconds + } more free days by taking a 2-minute anonymous survey`; const isValid = await isLicenseValid(context); const hasExtendedTrial = !!context.globalState.get(HAS_EXTENDED_TRIAL); @@ -47,7 +51,7 @@ export default async function getLicenseInformation(

This free trial will last for ${getPrettyPrintedTimeRemaining( trialLengthInMilliseconds )}.

- ${hasExtendedTrial ? '' : `

${takeSurveyButton}

`} + ${hasExtendedTrial ? '' : `

${takeSurveyButton}

`}

${getLicenseLink}

${enterLicenseButton}

`; } @@ -59,14 +63,14 @@ export default async function getLicenseInformation( )}

${getLicenseLink}

${enterLicenseButton}

- ${hasExtendedTrial ? '' : `

${takeSurveyButton}

`} + ${hasExtendedTrial ? '' : `

${takeSurveyButton}

`}

${retryValidationButton}

`; } 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}

`} + ${hasExtendedTrial ? '' : `

${takeSurveyButton}

`}

${getLicenseLink}

${enterLicenseButton}

`; } @@ -76,8 +80,8 @@ export default async function getLicenseInformation( new Date().getTime(), context.globalState.get(TRIAL_STARTED_TIMESTAMP), trialLengthInMilliseconds - )} left in the free preview.

- ${hasExtendedTrial ? '' : `

${takeSurveyButton}

`} + )} left in this free preview.

+ ${hasExtendedTrial ? '' : `

${takeSurveyButton}

`}

${getLicenseLink}

${enterLicenseButton}

`; } diff --git a/webview/vscode.css b/webview/vscode.css index 316ebcfb..71a43791 100644 --- a/webview/vscode.css +++ b/webview/vscode.css @@ -34,6 +34,7 @@ a.button { text-align: center; outline: 1px solid transparent; outline-offset: 2px !important; + text-decoration: none; } button.primary, From 3f49b250dc3a1028af01e5cfb5e18337bc21094d Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Thu, 2 Dec 2021 00:55:35 -0600 Subject: [PATCH 14/24] Change the to a `; const changeLicenseButton = ``; const retryValidationButton = ``; - const takeSurveyButton = `Get ${ + const takeSurveyButton = ``; const isValid = await isLicenseValid(context); const hasExtendedTrial = !!context.globalState.get(HAS_EXTENDED_TRIAL); From 9b4023c8981bbdca78829f179aad539576f31be4 Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Thu, 2 Dec 2021 01:28:20 -0600 Subject: [PATCH 15/24] Round the hours up, so it doesn't show 1 day, 23 hours right away It's confusing when it says there's a 2-day trial, but it shows 1 day, 23 hours. --- src/extension.ts | 4 ++++ .../utils/getTimeRemainingInTrial.test.ts | 19 +++++++++++++++---- src/utils/getLicenseInformation.ts | 12 ------------ src/utils/getPrettyPrintedTimeRemaining.ts | 8 +++++--- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 72be54da..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'; @@ -30,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); diff --git a/src/test/suite/utils/getTimeRemainingInTrial.test.ts b/src/test/suite/utils/getTimeRemainingInTrial.test.ts index 6d6914d0..207b48f1 100644 --- a/src/test/suite/utils/getTimeRemainingInTrial.test.ts +++ b/src/test/suite/utils/getTimeRemainingInTrial.test.ts @@ -15,14 +15,25 @@ suite('getTimeRemainingInTrial', () => { ); }); - test('14 days and 1 hour remaining', () => { + test('14 days and 13 hours remaining', () => { assert.strictEqual( getTimeRemainingInTrial( time, time, - 14 * dayInMilliseconds + hourInMilliseconds + 14 * dayInMilliseconds + 13 * hourInMilliseconds ), - '14 days, 1 hour' + '15 days' + ); + }); + + test('14 days and 11 hours remaining', () => { + assert.strictEqual( + getTimeRemainingInTrial( + time, + time, + 14 * dayInMilliseconds + 11 * hourInMilliseconds + ), + '14 days, 11 hours' ); }); @@ -47,7 +58,7 @@ suite('getTimeRemainingInTrial', () => { time - 10 * hourInMilliseconds, dayInMilliseconds * 2 ), - '1 day, 14 hours' + '2 days' ); }); diff --git a/src/utils/getLicenseInformation.ts b/src/utils/getLicenseInformation.ts index 113492bd..94494ace 100644 --- a/src/utils/getLicenseInformation.ts +++ b/src/utils/getLicenseInformation.ts @@ -1,6 +1,5 @@ import * as vscode from 'vscode'; import getLicenseErrorMessage from './getLicenseErrorMessage'; -import getPrettyPrintedTimeRemaining from './getPrettyPrintedTimeRemaining'; import getTimeRemainingInTrial from './getTimeRemainingInTrial'; import getTrialLength from './getTrialLength'; import isLicenseValid from './isLicenseValid'; @@ -45,17 +44,6 @@ export default async function getLicenseInformation( ${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 ${getPrettyPrintedTimeRemaining( - trialLengthInMilliseconds - )}.

- ${hasExtendedTrial ? '' : `

${takeSurveyButton}

`} -

${getLicenseLink}

-

${enterLicenseButton}

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

There was an error validating the license key.

${getLicenseErrorMessage( diff --git a/src/utils/getPrettyPrintedTimeRemaining.ts b/src/utils/getPrettyPrintedTimeRemaining.ts index 4af2479e..f9fbb667 100644 --- a/src/utils/getPrettyPrintedTimeRemaining.ts +++ b/src/utils/getPrettyPrintedTimeRemaining.ts @@ -30,9 +30,11 @@ export default function getPrettyPrintedTimeRemaining( ); } - const daysRemaining = Math.floor(millisecondsRemaining / dayInMilliseconds); - const hoursRemaining = Math.floor( - (millisecondsRemaining % dayInMilliseconds) / hourInMilliseconds + const daysRemaining = Math.round(millisecondsRemaining / dayInMilliseconds); + const hoursRemaining = Math.max( + (millisecondsRemaining - daysRemaining * dayInMilliseconds) / + hourInMilliseconds, + 0 ); return [ From d622b72dc805595de45b24c0dd93bc1c6b69c7f2 Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Thu, 2 Dec 2021 01:30:43 -0600 Subject: [PATCH 16/24] Rename daysInMilliseconds to be singular --- src/utils/getLicenseInformation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/getLicenseInformation.ts b/src/utils/getLicenseInformation.ts index 94494ace..fbe6dfd5 100644 --- a/src/utils/getLicenseInformation.ts +++ b/src/utils/getLicenseInformation.ts @@ -21,14 +21,14 @@ export default async function getLicenseInformation( const previewStartedTimeStamp = context.globalState.get( TRIAL_STARTED_TIMESTAMP ); - const daysInMilliseconds = 86400000; + const dayInMilliseconds = 86400000; const licenseKey = await context.secrets.get(LICENSE_KEY); const getLicenseLink = `Buy license`; const enterLicenseButton = ``; const changeLicenseButton = ``; const retryValidationButton = ``; const takeSurveyButton = ``; const isValid = await isLicenseValid(context); From dd9d47abcf17d6b206fe9f0dc44fb9f45966a974 Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Fri, 3 Dec 2021 23:28:31 -0600 Subject: [PATCH 17/24] Prevent a decimal hours count This shouldn't be a decimal like 1.4232 hours. --- .vscode/extensions.json | 2 -- src/utils/getPrettyPrintedTimeRemaining.ts | 10 ++++++---- 2 files changed, 6 insertions(+), 6 deletions(-) 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/src/utils/getPrettyPrintedTimeRemaining.ts b/src/utils/getPrettyPrintedTimeRemaining.ts index f9fbb667..bf934331 100644 --- a/src/utils/getPrettyPrintedTimeRemaining.ts +++ b/src/utils/getPrettyPrintedTimeRemaining.ts @@ -31,10 +31,12 @@ export default function getPrettyPrintedTimeRemaining( } const daysRemaining = Math.round(millisecondsRemaining / dayInMilliseconds); - const hoursRemaining = Math.max( - (millisecondsRemaining - daysRemaining * dayInMilliseconds) / - hourInMilliseconds, - 0 + const hoursRemaining = Math.floor( + Math.max( + (millisecondsRemaining - daysRemaining * dayInMilliseconds) / + hourInMilliseconds, + 0 + ) ); return [ From cecbf8ca57f016ee4ace934074a777f33dc74284 Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Sat, 4 Dec 2021 00:02:43 -0600 Subject: [PATCH 18/24] Add a link for a research interview --- src/constants/index.ts | 1 + src/utils/getLicenseInformation.ts | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/constants/index.ts b/src/constants/index.ts index 4bb40880..b7693ed5 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -50,6 +50,7 @@ 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://example.com'; export const SUPPRESS_UNCOMMITTED_FILE_WARNING = 'local-ci.suppress-warning.uncommitted'; export const SURVEY_URL = 'https://example.com'; diff --git a/src/utils/getLicenseInformation.ts b/src/utils/getLicenseInformation.ts index fbe6dfd5..fda0a251 100644 --- a/src/utils/getLicenseInformation.ts +++ b/src/utils/getLicenseInformation.ts @@ -5,13 +5,14 @@ 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, TRIAL_STARTED_TIMESTAMP, - HAS_EXTENDED_TRIAL, - EXTENDED_TRIAL_LENGTH_IN_MILLISECONDS, + SCHEDULE_INTERVIEW_URL, } from '../constants'; export default async function getLicenseInformation( @@ -23,13 +24,14 @@ export default async function getLicenseInformation( ); 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 interview about why you didn't buy`; const isValid = await isLicenseValid(context); const hasExtendedTrial = !!context.globalState.get(HAS_EXTENDED_TRIAL); @@ -39,6 +41,11 @@ export default async function getLicenseInformation( trialLengthInMilliseconds ); + const isPreviewExpiredByOneDay = isTrialExpired( + previewStartedTimeStamp, + trialLengthInMilliseconds + dayInMilliseconds + ); + if (isValid) { return `

Your Local CI license key is valid!

${changeLicenseButton}`; @@ -55,6 +62,12 @@ export default async function getLicenseInformation(

${retryValidationButton}

`; } + 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.

From efa66d5df9803e2f7b5fa50121fc36569d04be7a Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Sat, 4 Dec 2021 00:22:56 -0600 Subject: [PATCH 19/24] Add a 'Complain' link, thanks to Jason Cohen's idea Following: https://blog.asmartbear.com/more-sales-customer-feedback.html --- src/utils/getLicenseInformation.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/utils/getLicenseInformation.ts b/src/utils/getLicenseInformation.ts index fda0a251..1712e01a 100644 --- a/src/utils/getLicenseInformation.ts +++ b/src/utils/getLicenseInformation.ts @@ -32,6 +32,8 @@ export default async function getLicenseInformation( EXTENDED_TRIAL_LENGTH_IN_MILLISECONDS / dayInMilliseconds } more free days by taking a 2-minute anonymous survey`; const scheduleInterviewLink = `
Get a free lifetime license by doing a 30-minute Zoom interview about why you didn't buy`; + const complainUrl = 'mailto:ryan@getlocalci.com'; + const complainLink = `Complain to me`; const isValid = await isLicenseValid(context); const hasExtendedTrial = !!context.globalState.get(HAS_EXTENDED_TRIAL); @@ -48,7 +50,8 @@ export default async function getLicenseInformation( if (isValid) { return `

Your Local CI license key is valid!

- ${changeLicenseButton}`; +

${changeLicenseButton}

+

${complainLink}

`; } if (isPreviewExpired && !!licenseKey && !isValid) { @@ -59,7 +62,8 @@ export default async function getLicenseInformation(

${getLicenseLink}

${enterLicenseButton}

${hasExtendedTrial ? '' : `

${takeSurveyButton}

`} -

${retryValidationButton}

`; +

${retryValidationButton}

+

${complainLink}

`; } if (isPreviewExpiredByOneDay) { @@ -73,7 +77,8 @@ export default async function getLicenseInformation(

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

${hasExtendedTrial ? '' : `

${takeSurveyButton}

`}

${getLicenseLink}

-

${enterLicenseButton}

`; +

${enterLicenseButton}

+

${complainLink}

`; } return `

Thanks for previewing Local CI!

@@ -84,5 +89,6 @@ export default async function getLicenseInformation( )} left in this free preview.

${hasExtendedTrial ? '' : `

${takeSurveyButton}

`}

${getLicenseLink}

-

${enterLicenseButton}

`; +

${enterLicenseButton}

+

${complainLink}

`; } From 676979e9143ebf45cbb9848d05006b0c39e43ebb Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Sat, 4 Dec 2021 21:15:22 -0600 Subject: [PATCH 20/24] Remove needless const, rearrange destructuring --- src/utils/getLicenseInformation.ts | 8 ++++---- src/utils/getPrettyPrintedTimeRemaining.ts | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/utils/getLicenseInformation.ts b/src/utils/getLicenseInformation.ts index 1712e01a..55267338 100644 --- a/src/utils/getLicenseInformation.ts +++ b/src/utils/getLicenseInformation.ts @@ -11,8 +11,8 @@ import { LICENSE_ERROR, LICENSE_KEY, LICENSE_VALIDITY, - TRIAL_STARTED_TIMESTAMP, SCHEDULE_INTERVIEW_URL, + TRIAL_STARTED_TIMESTAMP, } from '../constants'; export default async function getLicenseInformation( @@ -31,9 +31,9 @@ export default async function getLicenseInformation( const takeSurveyButton = ``; - const scheduleInterviewLink = `Get a free lifetime license by doing a 30-minute Zoom interview about why you didn't buy`; - const complainUrl = 'mailto:ryan@getlocalci.com'; - const complainLink = `Complain to me`; + 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 hasExtendedTrial = !!context.globalState.get(HAS_EXTENDED_TRIAL); diff --git a/src/utils/getPrettyPrintedTimeRemaining.ts b/src/utils/getPrettyPrintedTimeRemaining.ts index bf934331..2f4e1531 100644 --- a/src/utils/getPrettyPrintedTimeRemaining.ts +++ b/src/utils/getPrettyPrintedTimeRemaining.ts @@ -13,9 +13,8 @@ function getTextForNumber(singular: string, plural: string, count: number) { export default function getPrettyPrintedTimeRemaining( millisecondsRemaining: number ): string { - const defaultTime = 'No time'; if (millisecondsRemaining < minuteInMilliseconds) { - return defaultTime; + return 'No time'; } if (millisecondsRemaining < hourInMilliseconds) { From 13cc31254fd834a688efeb3901cb76aaf674f042 Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Sat, 4 Dec 2021 22:15:57 -0600 Subject: [PATCH 21/24] Change the URL to book a meeting --- src/constants/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/constants/index.ts b/src/constants/index.ts index b7693ed5..c48b7987 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -50,7 +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://example.com'; +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://example.com'; From 38ce98c01a99c7ec061dfbf16cd3cb3f28e54d47 Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Sat, 4 Dec 2021 23:46:21 -0600 Subject: [PATCH 22/24] Add the survey URL --- src/constants/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/index.ts b/src/constants/index.ts index c48b7987..089c0eeb 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -54,4 +54,4 @@ 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://example.com'; +export const SURVEY_URL = 'https://www.surveymonkey.com/r/localci'; From 93e015cdb8e46638e171589b38c984ff11883938 Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Sat, 4 Dec 2021 23:47:24 -0600 Subject: [PATCH 23/24] Bump the version to 1.0.2 --- CHANGELOG.md | 5 +++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) 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 aa313e98..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": [ diff --git a/package.json b/package.json index 46fc0d29..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" From 7fb93fac8f2d16e216a215177ef2750c5a72525c Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Sat, 4 Dec 2021 23:51:53 -0600 Subject: [PATCH 24/24] Fix the link to do the interview Its closing tag should be a , not a . --- src/utils/getLicenseInformation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/getLicenseInformation.ts b/src/utils/getLicenseInformation.ts index 55267338..4ed93004 100644 --- a/src/utils/getLicenseInformation.ts +++ b/src/utils/getLicenseInformation.ts @@ -31,7 +31,7 @@ export default async function getLicenseInformation( const takeSurveyButton = ``; - const scheduleInterviewLink = `Get a free lifetime license by doing a 30-minute Zoom user research interview`; + 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`;