From 01e9db5d512371b53c4f3e5214914146f4a57bcb Mon Sep 17 00:00:00 2001 From: Zack Spear Date: Tue, 8 Jul 2025 16:33:07 -0700 Subject: [PATCH 1/5] feat: trial extension allowed within 5 days of expiration --- web/locales/en_US.json | 4 ++++ web/store/server.ts | 37 +++++++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) 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..dd6f8be871 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,32 @@ 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': + trialMessage = '

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

'; + + if (trialWithin5DaysOfExpiration.value) { + if (trialExtensionEligible.value) { + trialMessage += '

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 { + trialMessage += '

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 (trialExtensionEligible.value) { + trialMessage += '

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 += '

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], + ...(trialExtensionEligible.value && trialWithin5DaysOfExpiration.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 +790,22 @@ 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; + }); + + watchEffect(() => { + console.log('expireTime', expireTime.value); + console.log('state', state.value); + console.log('trialExtensionEligible', trialExtensionEligible.value); + console.log('trialWithin5DaysOfExpiration', trialWithin5DaysOfExpiration.value); + }); const serverConfigError = computed((): Error | undefined => { if (!config.value?.valid && config.value?.error) { From 43cff73b84dddf7f53c01b1e507721eed6db4df3 Mon Sep 17 00:00:00 2001 From: Zack Spear Date: Tue, 8 Jul 2025 17:18:52 -0700 Subject: [PATCH 2/5] chore: remove console logs from server store --- web/store/server.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/web/store/server.ts b/web/store/server.ts index dd6f8be871..8dcb265a30 100644 --- a/web/store/server.ts +++ b/web/store/server.ts @@ -800,13 +800,6 @@ export const useServerStore = defineStore('server', () => { return daysUntilExpiration <= 5 && daysUntilExpiration >= 0; }); - watchEffect(() => { - console.log('expireTime', expireTime.value); - console.log('state', state.value); - console.log('trialExtensionEligible', trialExtensionEligible.value); - console.log('trialWithin5DaysOfExpiration', trialWithin5DaysOfExpiration.value); - }); - const serverConfigError = computed((): Error | undefined => { if (!config.value?.valid && config.value?.error) { switch (config.value?.error) { From 82eaca400ffe584b0fcd2f1f6f24f796d3d92a7e Mon Sep 17 00:00:00 2001 From: Zack Spear Date: Wed, 9 Jul 2025 12:02:27 -0700 Subject: [PATCH 3/5] refactor: streamline trial message logic in server store Consolidated trial message handling to reduce redundancy and improve readability. The logic now checks conditions for trial expiration and extension eligibility more efficiently, ensuring users receive accurate information based on their trial status. --- web/store/server.ts | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/web/store/server.ts b/web/store/server.ts index 8dcb265a30..c490aa5e2e 100644 --- a/web/store/server.ts +++ b/web/store/server.ts @@ -511,20 +511,14 @@ 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': - trialMessage = '

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

'; - - if (trialWithin5DaysOfExpiration.value) { - if (trialExtensionEligible.value) { - trialMessage += '

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 { - trialMessage += '

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.

'; - } + if (trialWithin5DaysOfExpiration.value && trialExtensionEligible.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 (trialWithin5DaysOfExpiration.value && !trialExtensionEligible.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 (!trialWithin5DaysOfExpiration.value && trialExtensionEligible.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 { - if (trialExtensionEligible.value) { - trialMessage += '

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 += '

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.

'; - } + 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 { From 287b3d25c3df990348e3ee5ed19d93fd9d9b6f9b Mon Sep 17 00:00:00 2001 From: Zack Spear Date: Thu, 10 Jul 2025 11:31:45 -0700 Subject: [PATCH 4/5] refactor: trial message logic to utilize new computed properties for better clarity and maintainability --- web/store/server.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/web/store/server.ts b/web/store/server.ts index c490aa5e2e..17d834a981 100644 --- a/web/store/server.ts +++ b/web/store/server.ts @@ -511,13 +511,13 @@ 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 (trialWithin5DaysOfExpiration.value && trialExtensionEligible.value) { + 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 (trialWithin5DaysOfExpiration.value && !trialExtensionEligible.value) { + } 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 (!trialWithin5DaysOfExpiration.value && trialExtensionEligible.value) { + } 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 { + } 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.

'; } @@ -525,7 +525,7 @@ export const useServerStore = defineStore('server', () => { actions: [ ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []), ...[purchaseAction.value, redeemAction.value], - ...(trialExtensionEligible.value && trialWithin5DaysOfExpiration.value ? [trialExtendAction.value] : []), + ...(trialExtensionEligibleInsideRenewalWindow.value ? [trialExtendAction.value] : []), ...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []), ], humanReadable: 'Trial', @@ -793,6 +793,9 @@ export const useServerStore = defineStore('server', () => { 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) { From dbf2887f030e516d69c2ce9811893b66bda7be48 Mon Sep 17 00:00:00 2001 From: Zack Spear Date: Thu, 10 Jul 2025 17:18:07 -0700 Subject: [PATCH 5/5] test: add tests for sever store trial extension eligibility and messaging Implemented new tests in `server.test.ts` to validate trial extension eligibility, expiration conditions, and corresponding user messages. This includes checks for eligibility based on `regGen` values and expiration timing, ensuring accurate feedback for users regarding their trial status. --- web/__test__/store/server.test.ts | 319 ++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) 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); + }); + }); });