diff --git a/web/__test__/store/server.test.ts b/web/__test__/store/server.test.ts index 4d9841a233..dfe728c7bf 100644 --- a/web/__test__/store/server.test.ts +++ b/web/__test__/store/server.test.ts @@ -764,4 +764,323 @@ describe('useServerStore', () => { expect(store.cloudError).toBeDefined(); expect((store.cloudError as { message: string })?.message).toBe('Test error'); }); + + describe('trial extension features', () => { + it('should determine trial extension eligibility correctly', () => { + const store = getStore(); + + // Add trialExtensionEligible property to the store + Object.defineProperty(store, 'trialExtensionEligible', { + get: () => !store.regGen || store.regGen < 2, + }); + + // Eligible - no regGen + store.setServer({ regGen: 0 }); + expect(store.trialExtensionEligible).toBe(true); + + // Eligible - regGen = 1 + store.setServer({ regGen: 1 }); + expect(store.trialExtensionEligible).toBe(true); + + // Not eligible - regGen = 2 + store.setServer({ regGen: 2 }); + expect(store.trialExtensionEligible).toBe(false); + + // Not eligible - regGen > 2 + store.setServer({ regGen: 3 }); + expect(store.trialExtensionEligible).toBe(false); + }); + + it('should calculate trial within 5 days of expiration correctly', () => { + const store = getStore(); + + // Add properties to the store + Object.defineProperty(store, 'expireTime', { value: 0, writable: true }); + Object.defineProperty(store, 'trialWithin5DaysOfExpiration', { + get: () => { + if (!store.expireTime || store.state !== 'TRIAL') { + return false; + } + const today = dayjs(); + const expirationDate = dayjs(store.expireTime); + const daysUntilExpiration = expirationDate.diff(today, 'day'); + return daysUntilExpiration <= 5 && daysUntilExpiration >= 0; + }, + }); + + // Not a trial + store.setServer({ state: 'PRO' as ServerState, expireTime: dayjs().add(3, 'day').unix() * 1000 }); + expect(store.trialWithin5DaysOfExpiration).toBe(false); + + // Trial but no expireTime + store.setServer({ state: 'TRIAL' as ServerState, expireTime: 0 }); + expect(store.trialWithin5DaysOfExpiration).toBe(false); + + // Trial expiring in 3 days + store.setServer({ state: 'TRIAL' as ServerState, expireTime: dayjs().add(3, 'day').unix() * 1000 }); + expect(store.trialWithin5DaysOfExpiration).toBe(true); + + // Trial expiring in exactly 5 days + store.setServer({ state: 'TRIAL' as ServerState, expireTime: dayjs().add(5, 'day').unix() * 1000 }); + expect(store.trialWithin5DaysOfExpiration).toBe(true); + + // Trial expiring in 7 days (to ensure it's clearly outside the 5-day window) + store.setServer({ state: 'TRIAL' as ServerState, expireTime: dayjs().add(7, 'day').unix() * 1000 }); + expect(store.trialWithin5DaysOfExpiration).toBe(false); + + // Trial already expired + store.setServer({ state: 'TRIAL' as ServerState, expireTime: dayjs().subtract(1, 'day').unix() * 1000 }); + expect(store.trialWithin5DaysOfExpiration).toBe(false); + }); + + it('should calculate trial extension renewal window conditions correctly', () => { + const store = getStore(); + + // Add all necessary properties + Object.defineProperty(store, 'expireTime', { value: 0, writable: true }); + Object.defineProperty(store, 'trialExtensionEligible', { + get: () => !store.regGen || store.regGen < 2, + }); + Object.defineProperty(store, 'trialWithin5DaysOfExpiration', { + get: () => { + if (!store.expireTime || store.state !== 'TRIAL') { + return false; + } + const today = dayjs(); + const expirationDate = dayjs(store.expireTime); + const daysUntilExpiration = expirationDate.diff(today, 'day'); + return daysUntilExpiration <= 5 && daysUntilExpiration >= 0; + }, + }); + Object.defineProperty(store, 'trialExtensionEligibleInsideRenewalWindow', { + get: () => store.trialExtensionEligible && store.trialWithin5DaysOfExpiration, + }); + Object.defineProperty(store, 'trialExtensionEligibleOutsideRenewalWindow', { + get: () => store.trialExtensionEligible && !store.trialWithin5DaysOfExpiration, + }); + Object.defineProperty(store, 'trialExtensionIneligibleInsideRenewalWindow', { + get: () => !store.trialExtensionEligible && store.trialWithin5DaysOfExpiration, + }); + + // Eligible inside renewal window + store.setServer({ + state: 'TRIAL' as ServerState, + regGen: 1, + expireTime: dayjs().add(3, 'day').unix() * 1000, + }); + expect(store.trialExtensionEligibleInsideRenewalWindow).toBe(true); + expect(store.trialExtensionEligibleOutsideRenewalWindow).toBe(false); + expect(store.trialExtensionIneligibleInsideRenewalWindow).toBe(false); + + // Eligible outside renewal window + store.setServer({ + state: 'TRIAL' as ServerState, + regGen: 1, + expireTime: dayjs().add(10, 'day').unix() * 1000, + }); + expect(store.trialExtensionEligibleInsideRenewalWindow).toBe(false); + expect(store.trialExtensionEligibleOutsideRenewalWindow).toBe(true); + expect(store.trialExtensionIneligibleInsideRenewalWindow).toBe(false); + + // Ineligible inside renewal window + store.setServer({ + state: 'TRIAL' as ServerState, + regGen: 2, + expireTime: dayjs().add(3, 'day').unix() * 1000, + }); + expect(store.trialExtensionEligibleInsideRenewalWindow).toBe(false); + expect(store.trialExtensionEligibleOutsideRenewalWindow).toBe(false); + expect(store.trialExtensionIneligibleInsideRenewalWindow).toBe(true); + + // Ineligible outside renewal window + store.setServer({ + state: 'TRIAL' as ServerState, + regGen: 2, + expireTime: dayjs().add(10, 'day').unix() * 1000, + }); + expect(store.trialExtensionEligibleInsideRenewalWindow).toBe(false); + expect(store.trialExtensionEligibleOutsideRenewalWindow).toBe(false); + expect(store.trialExtensionIneligibleInsideRenewalWindow).toBe(false); + }); + + it('should display correct trial messages based on extension eligibility and renewal window', () => { + const store = getStore(); + + // Add all necessary properties + Object.defineProperty(store, 'expireTime', { value: 0, writable: true }); + Object.defineProperty(store, 'trialExtensionEligible', { + get: () => !store.regGen || store.regGen < 2, + }); + Object.defineProperty(store, 'trialWithin5DaysOfExpiration', { + get: () => { + if (!store.expireTime || store.state !== 'TRIAL') { + return false; + } + const today = dayjs(); + const expirationDate = dayjs(store.expireTime); + const daysUntilExpiration = expirationDate.diff(today, 'day'); + return daysUntilExpiration <= 5 && daysUntilExpiration >= 0; + }, + }); + Object.defineProperty(store, 'trialExtensionEligibleInsideRenewalWindow', { + get: () => store.trialExtensionEligible && store.trialWithin5DaysOfExpiration, + }); + Object.defineProperty(store, 'trialExtensionEligibleOutsideRenewalWindow', { + get: () => store.trialExtensionEligible && !store.trialWithin5DaysOfExpiration, + }); + Object.defineProperty(store, 'trialExtensionIneligibleInsideRenewalWindow', { + get: () => !store.trialExtensionEligible && store.trialWithin5DaysOfExpiration, + }); + + // Mock stateData getter to include trial message logic + Object.defineProperty(store, 'stateData', { + get: () => { + if (store.state !== 'TRIAL') { + return { + humanReadable: '', + heading: '', + message: '', + actions: [], + }; + } + + let trialMessage = ''; + if (store.trialExtensionEligibleInsideRenewalWindow) { + trialMessage = '

Your Trial key includes all the functionality and device support of an Unleashed key.

Your trial is expiring soon. When it expires, the array will stop. You may extend your trial now, purchase a license key, or wait until expiration to take action.

'; + } else if (store.trialExtensionIneligibleInsideRenewalWindow) { + trialMessage = '

Your Trial key includes all the functionality and device support of an Unleashed key.

Your trial is expiring soon and you have used all available extensions. When it expires, the array will stop. To continue using Unraid OS, you must purchase a license key.

'; + } else if (store.trialExtensionEligibleOutsideRenewalWindow) { + trialMessage = '

Your Trial key includes all the functionality and device support of an Unleashed key.

When your Trial expires, the array will stop. At that point you may either purchase a license key or request a Trial extension.

'; + } else { + trialMessage = '

Your Trial key includes all the functionality and device support of an Unleashed key.

You have used all available trial extensions. When your Trial expires, the array will stop. To continue using Unraid OS after expiration, you must purchase a license key.

'; + } + + return { + humanReadable: 'Trial', + heading: 'Thank you for choosing Unraid OS!', + message: trialMessage, + actions: [], + }; + }, + }); + + // Test case 1: Eligible inside renewal window + store.setServer({ + state: 'TRIAL' as ServerState, + regGen: 1, + expireTime: dayjs().add(3, 'day').unix() * 1000, + }); + expect(store.stateData.message).toContain('Your trial is expiring soon'); + expect(store.stateData.message).toContain('You may extend your trial now'); + + // Test case 2: Ineligible inside renewal window + store.setServer({ + state: 'TRIAL' as ServerState, + regGen: 2, + expireTime: dayjs().add(3, 'day').unix() * 1000, + }); + expect(store.stateData.message).toContain('Your trial is expiring soon and you have used all available extensions'); + expect(store.stateData.message).toContain('To continue using Unraid OS, you must purchase a license key'); + + // Test case 3: Eligible outside renewal window + store.setServer({ + state: 'TRIAL' as ServerState, + regGen: 0, + expireTime: dayjs().add(10, 'day').unix() * 1000, + }); + expect(store.stateData.message).toContain('At that point you may either purchase a license key or request a Trial extension'); + + // Test case 4: Ineligible outside renewal window + store.setServer({ + state: 'TRIAL' as ServerState, + regGen: 2, + expireTime: dayjs().add(10, 'day').unix() * 1000, + }); + expect(store.stateData.message).toContain('You have used all available trial extensions'); + expect(store.stateData.message).toContain('To continue using Unraid OS after expiration, you must purchase a license key'); + }); + + it('should include trial extend action only when eligible inside renewal window', () => { + const store = getStore(); + + // Add necessary properties + Object.defineProperty(store, 'expireTime', { value: 0, writable: true }); + Object.defineProperty(store, 'trialExtensionEligible', { + get: () => !store.regGen || store.regGen < 2, + }); + Object.defineProperty(store, 'trialWithin5DaysOfExpiration', { + get: () => { + if (!store.expireTime || store.state !== 'TRIAL') { + return false; + } + const today = dayjs(); + const expirationDate = dayjs(store.expireTime); + const daysUntilExpiration = expirationDate.diff(today, 'day'); + return daysUntilExpiration <= 5 && daysUntilExpiration >= 0; + }, + }); + Object.defineProperty(store, 'trialExtensionEligibleInsideRenewalWindow', { + get: () => store.trialExtensionEligible && store.trialWithin5DaysOfExpiration, + }); + + // Mock the trialExtendAction + const trialExtendAction = { name: 'trialExtend', text: 'Extend Trial' }; + + // Mock stateData getter to include actions logic + Object.defineProperty(store, 'stateData', { + get: () => { + if (store.state !== 'TRIAL') { + return { + humanReadable: '', + heading: '', + message: '', + actions: [], + }; + } + + const actions = []; + if (store.trialExtensionEligibleInsideRenewalWindow) { + actions.push(trialExtendAction); + } + + return { + humanReadable: 'Trial', + heading: 'Thank you for choosing Unraid OS!', + message: '', + actions, + }; + }, + }); + + // Test case 1: Eligible inside renewal window - should include trialExtend action + store.setServer({ + state: 'TRIAL' as ServerState, + regGen: 1, + expireTime: dayjs().add(3, 'day').unix() * 1000, + registered: true, + connectPluginInstalled: 'true' as ServerconnectPluginInstalled, + }); + expect(store.stateData.actions?.some((action: { name: string }) => action.name === 'trialExtend')).toBe(true); + + // Test case 2: Not eligible inside renewal window - should NOT include trialExtend action + store.setServer({ + state: 'TRIAL' as ServerState, + regGen: 2, + expireTime: dayjs().add(3, 'day').unix() * 1000, + registered: true, + connectPluginInstalled: 'true' as ServerconnectPluginInstalled, + }); + expect(store.stateData.actions?.some((action: { name: string }) => action.name === 'trialExtend')).toBe(false); + + // Test case 3: Eligible outside renewal window - should NOT include trialExtend action + store.setServer({ + state: 'TRIAL' as ServerState, + regGen: 1, + expireTime: dayjs().add(10, 'day').unix() * 1000, + registered: true, + connectPluginInstalled: 'true' as ServerconnectPluginInstalled, + }); + expect(store.stateData.actions?.some((action: { name: string }) => action.name === 'trialExtend')).toBe(false); + }); + }); }); diff --git a/web/locales/en_US.json b/web/locales/en_US.json index 807c7dfaec..0bd2ccf220 100644 --- a/web/locales/en_US.json +++ b/web/locales/en_US.json @@ -23,6 +23,10 @@ "

To support more storage devices as your server grows, click Upgrade Key.

": "

To support more storage devices as your server grows, click Upgrade Key.

", "

You have used all your Trial extensions. To continue using Unraid OS you may purchase a license key.

": "

You have used all your Trial extensions. To continue using Unraid OS you may purchase a license key.

", "

Your Trial key includes all the functionality and device support of an Unleashed key.

After your Trial has reached expiration, your server still functions normally until the next time you Stop the array or reboot your server.

At that point you may either purchase a license key or request a Trial extension.

": "

Your Trial key includes all the functionality and device support of an Unleashed key.

After your Trial has reached expiration, your server still functions normally until the next time you Stop the array or reboot your server.

At that point you may either purchase a license key or request a Trial extension.

", + "

Your Trial key includes all the functionality and device support of an Unleashed key.

When your Trial expires, the array will stop. At that point you may either purchase a license key or request a Trial extension.

": "

Your Trial key includes all the functionality and device support of an Unleashed key.

When your Trial expires, the array will stop. At that point you may either purchase a license key or request a Trial extension.

", + "

Your Trial key includes all the functionality and device support of an Unleashed key.

Your trial is expiring soon. When it expires, the array will stop. You may extend your trial now, purchase a license key, or wait until expiration to take action.

": "

Your Trial key includes all the functionality and device support of an Unleashed key.

Your trial is expiring soon. When it expires, the array will stop. You may extend your trial now, purchase a license key, or wait until expiration to take action.

", + "

Your Trial key includes all the functionality and device support of an Unleashed key.

Your trial is expiring soon and you have used all available extensions. When it expires, the array will stop. To continue using Unraid OS, you must purchase a license key.

": "

Your Trial key includes all the functionality and device support of an Unleashed key.

Your trial is expiring soon and you have used all available extensions. When it expires, the array will stop. To continue using Unraid OS, you must purchase a license key.

", + "

Your Trial key includes all the functionality and device support of an Unleashed key.

You have used all available trial extensions. When your Trial expires, the array will stop. To continue using Unraid OS after expiration, you must purchase a license key.

": "

Your Trial key includes all the functionality and device support of an Unleashed key.

You have used all available trial extensions. When your Trial expires, the array will stop. To continue using Unraid OS after expiration, you must purchase a license key.

", "

Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.

If you do not have a backup copy of your license key file you may attempt to recover your key.

If this was an expired Trial installation, you may purchase a license key.

": "

Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.

If you do not have a backup copy of your license key file you may attempt to recover your key.

If this was an expired Trial installation, you may purchase a license key.

", "

Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.

You may attempt to recover your key with your Unraid.net account.

If this was an expired Trial installation, you may purchase a license key.

": "

Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.

You may attempt to recover your key with your Unraid.net account.

If this was an expired Trial installation, you may purchase a license key.

", "

Your server will not be usable until you purchase a Registration key or install a free 30 day Trial key. A Trial key provides all the functionality of an Unleashed Registration key.

Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device at least 1GB in size.

Note: USB memory card readers are generally not supported because most do not present unique serial numbers.

Important:

": "

Your server will not be usable until you purchase a Registration key or install a free 30 day Trial key. A Trial key provides all the functionality of an Unleashed Registration key.

Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device at least 1GB in size.

Note: USB memory card readers are generally not supported because most do not present unique serial numbers.

Important:

", diff --git a/web/store/server.ts b/web/store/server.ts index 313be3061d..17d834a981 100644 --- a/web/store/server.ts +++ b/web/store/server.ts @@ -495,6 +495,7 @@ export const useServerStore = defineStore('server', () => { }); let messageEGUID = ''; + let trialMessage = ''; const stateData = computed((): ServerStateData => { switch (state.value) { case 'ENOKEYFILE': @@ -510,16 +511,26 @@ export const useServerStore = defineStore('server', () => { '

Choose an option below, then use our Getting Started Guide to configure your array in less than 15 minutes.

', }; case 'TRIAL': + if (trialExtensionEligibleInsideRenewalWindow.value) { + trialMessage = '

Your Trial key includes all the functionality and device support of an Unleashed key.

Your trial is expiring soon. When it expires, the array will stop. You may extend your trial now, purchase a license key, or wait until expiration to take action.

'; + } else if (trialExtensionIneligibleInsideRenewalWindow.value) { + trialMessage = '

Your Trial key includes all the functionality and device support of an Unleashed key.

Your trial is expiring soon and you have used all available extensions. When it expires, the array will stop. To continue using Unraid OS, you must purchase a license key.

'; + } else if (trialExtensionEligibleOutsideRenewalWindow.value) { + trialMessage = '

Your Trial key includes all the functionality and device support of an Unleashed key.

When your Trial expires, the array will stop. At that point you may either purchase a license key or request a Trial extension.

'; + } else { // would be trialExtensionIneligibleOutsideRenewalWindow if it wasn't an else conditionally + trialMessage = '

Your Trial key includes all the functionality and device support of an Unleashed key.

You have used all available trial extensions. When your Trial expires, the array will stop. To continue using Unraid OS after expiration, you must purchase a license key.

'; + } + return { actions: [ ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []), ...[purchaseAction.value, redeemAction.value], + ...(trialExtensionEligibleInsideRenewalWindow.value ? [trialExtendAction.value] : []), ...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []), ], humanReadable: 'Trial', heading: 'Thank you for choosing Unraid OS!', - message: - '

Your Trial key includes all the functionality and device support of an Unleashed key.

After your Trial has reached expiration, your server still functions normally until the next time you Stop the array or reboot your server.

At that point you may either purchase a license key or request a Trial extension.

', + message: trialMessage, }; case 'EEXPIRED': return { @@ -773,6 +784,18 @@ export const useServerStore = defineStore('server', () => { return stateData.value.actions.filter((action) => !authActionsNames.includes(action.name)); }); const trialExtensionEligible = computed(() => !regGen.value || regGen.value < 2); + const trialWithin5DaysOfExpiration = computed(() => { + if (!expireTime.value || state.value !== 'TRIAL') { + return false; + } + const today = dayjs(); + const expirationDate = dayjs(expireTime.value); + const daysUntilExpiration = expirationDate.diff(today, 'day'); + return daysUntilExpiration <= 5 && daysUntilExpiration >= 0; + }); + const trialExtensionEligibleInsideRenewalWindow = computed(() => trialExtensionEligible.value && trialWithin5DaysOfExpiration.value); + const trialExtensionEligibleOutsideRenewalWindow = computed(() => trialExtensionEligible.value && !trialWithin5DaysOfExpiration.value); + const trialExtensionIneligibleInsideRenewalWindow = computed(() => !trialExtensionEligible.value && trialWithin5DaysOfExpiration.value); const serverConfigError = computed((): Error | undefined => { if (!config.value?.valid && config.value?.error) {