Skip to content

Commit c2c0425

Browse files
authored
feat: registration and trial actions use Account app (#1928)
### Summary - Replace Registration replace/link status widgets with a single `Manage License` action and helper text. - Remove legacy trial-start UI/store path and modal wiring from UPC. - Keep trial callback installs in the shared key-install pipeline (`trialStart`/`trialExtend` treated as key actions). - Update keyserver typing/response handling and related registration/account/server behaviors. - Clean up obsolete tests tied to removed registration/trial paths. ### Testing - `pnpm test __test__/components/Modals.test.ts __test__/components/Registration.test.ts` - `pnpm test __test__/store/replaceRenew.test.ts` - `pnpm test __test__/store/account.test.ts __test__/store/server.test.ts __test__/components/Registration.test.ts` - `pnpm test __test__/store/callbackActions.helpers.test.ts __test__/store/callbackActions.test.ts __test__/store/installKey.test.ts` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Removed Features** * Trial modal and trial start/extend actions removed from the UI * Legacy license linking and replace-check UI removed * **New Features** * Added a Manage License action/button with helper text to open account license management * **Improvements** * Streamlined registration UI and simplified license management flows * Fewer renewal/replace checks during registration, reducing complexity <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent bb6f241 commit c2c0425

23 files changed

+140
-1444
lines changed

web/__test__/components/Modals.test.ts

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import type { Pinia } from 'pinia';
99

1010
import Modals from '~/components/Modals.standalone.vue';
1111
import { useCallbackActionsStore } from '~/store/callbackActions';
12-
import { useTrialStore } from '~/store/trial';
1312
import { useUpdateOsStore } from '~/store/updateOs';
1413

1514
// Mock child components
@@ -45,14 +44,6 @@ vi.mock('~/components/UserProfile/CallbackFeedback.vue', () => ({
4544
},
4645
}));
4746

48-
vi.mock('~/components/UserProfile/Trial.vue', () => ({
49-
default: {
50-
name: 'UpcTrial',
51-
props: ['open'],
52-
template: '<div v-if="open">Trial</div>',
53-
},
54-
}));
55-
5647
vi.mock('vue-i18n', () => ({
5748
useI18n: () => ({
5849
t: (key: string) => key,
@@ -88,7 +79,6 @@ describe('Modals.standalone.vue', () => {
8879

8980
it('should render all modal components', () => {
9081
expect(wrapper.findComponent({ name: 'UpcCallbackFeedback' }).exists()).toBe(true);
91-
expect(wrapper.findComponent({ name: 'UpcTrial' }).exists()).toBe(true);
9282
expect(wrapper.findComponent({ name: 'UpdateOsCheckUpdateResponseModal' }).exists()).toBe(true);
9383
expect(wrapper.findComponent({ name: 'UpdateOsChangelogModal' }).exists()).toBe(true);
9484
expect(wrapper.findComponent({ name: 'OnboardingModal' }).exists()).toBe(true);
@@ -108,21 +98,6 @@ describe('Modals.standalone.vue', () => {
10898
expect(callbackFeedback.props('open')).toBe(false);
10999
});
110100

111-
it('should pass correct props to Trial modal based on store state', async () => {
112-
const trialStore = useTrialStore();
113-
// trialModalVisible is computed based on trialStatus
114-
trialStore.trialStatus = 'trialStart';
115-
116-
await nextTick();
117-
118-
const trialModal = wrapper.findComponent({ name: 'UpcTrial' });
119-
expect(trialModal.props('open')).toBe(true);
120-
121-
trialStore.trialStatus = 'ready';
122-
await nextTick();
123-
expect(trialModal.props('open')).toBe(false);
124-
});
125-
126101
it('should pass correct props to UpdateOs modal based on store state', async () => {
127102
const updateOsStore = useUpdateOsStore();
128103
updateOsStore.setModalOpen(true);
@@ -163,7 +138,6 @@ describe('Modals.standalone.vue', () => {
163138
it('should render all modal components without t props (using useI18n)', () => {
164139
const components = [
165140
'UpcCallbackFeedback',
166-
'UpcTrial',
167141
'UpdateOsCheckUpdateResponseModal',
168142
'UpdateOsChangelogModal',
169143
];
@@ -179,20 +153,17 @@ describe('Modals.standalone.vue', () => {
179153
it('should use computed properties for reactive store access', async () => {
180154
// Test that computed properties react to store changes
181155
const callbackStore = useCallbackActionsStore();
182-
const trialStore = useTrialStore();
183156
const updateOsStore = useUpdateOsStore();
184157

185158
// Initially all should be closed/default
186159
expect(wrapper.findComponent({ name: 'UpcCallbackFeedback' }).props('open')).toBe(false);
187-
expect(wrapper.findComponent({ name: 'UpcTrial' }).props('open')).toBe(false);
188160
expect(wrapper.findComponent({ name: 'UpdateOsCheckUpdateResponseModal' }).props('open')).toBe(
189161
false
190162
);
191163
expect(wrapper.findComponent({ name: 'UpdateOsChangelogModal' }).props('open')).toBe(false);
192164

193165
// Update all stores using proper methods
194166
callbackStore.callbackStatus = 'loading';
195-
trialStore.trialStatus = 'trialStart';
196167
updateOsStore.setModalOpen(true);
197168
updateOsStore.setReleaseForUpdate({
198169
version: '6.13.0',
@@ -208,19 +179,16 @@ describe('Modals.standalone.vue', () => {
208179

209180
// All should be open now
210181
expect(wrapper.findComponent({ name: 'UpcCallbackFeedback' }).props('open')).toBe(true);
211-
expect(wrapper.findComponent({ name: 'UpcTrial' }).props('open')).toBe(true);
212182
expect(wrapper.findComponent({ name: 'UpdateOsCheckUpdateResponseModal' }).props('open')).toBe(true);
213183
expect(wrapper.findComponent({ name: 'UpdateOsChangelogModal' }).props('open')).toBe(true);
214184
});
215185

216186
it('should render modals container even when all modals are closed', () => {
217187
const callbackStore = useCallbackActionsStore();
218-
const trialStore = useTrialStore();
219188
const updateOsStore = useUpdateOsStore();
220189

221190
// Set all modals to closed state
222191
callbackStore.callbackStatus = 'ready';
223-
trialStore.trialStatus = 'ready';
224192
updateOsStore.setModalOpen(false);
225193
updateOsStore.setReleaseForUpdate(null);
226194

web/__test__/components/Registration.test.ts

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import type { Pinia } from 'pinia';
1515
import Registration from '~/components/Registration.standalone.vue';
1616
import { useAccountStore } from '~/store/account';
1717
import { usePurchaseStore } from '~/store/purchase';
18-
import { useReplaceRenewStore } from '~/store/replaceRenew';
1918
import { useServerStore } from '~/store/server';
2019
import { createTestI18n, testTranslate } from '../utils/i18n';
2120

@@ -103,14 +102,6 @@ vi.mock('~/components/KeyActions.vue', () => ({
103102
default: { template: '<div data-testid="key-actions"><slot/></div>', props: ['t', 'filterOut'] },
104103
}));
105104

106-
vi.mock('~/components/Registration/KeyLinkedStatus.vue', () => ({
107-
default: { template: '<div data-testid="key-linked-status"></div>', props: ['t'] },
108-
}));
109-
110-
vi.mock('~/components/Registration/ReplaceCheck.vue', () => ({
111-
default: { template: '<div data-testid="replace-check"></div>', props: ['t'] },
112-
}));
113-
114105
vi.mock('~/components/Registration/UpdateExpirationAction.vue', () => ({
115106
default: { template: '<div data-testid="update-expiration"></div>', props: ['t'] },
116107
}));
@@ -158,7 +149,6 @@ describe('Registration.standalone.vue', () => {
158149
let pinia: Pinia;
159150
let accountStore: ReturnType<typeof useAccountStore>;
160151
let serverStore: ReturnType<typeof useServerStore>;
161-
let replaceRenewStore: ReturnType<typeof useReplaceRenewStore>;
162152
let purchaseStore: ReturnType<typeof usePurchaseStore>;
163153

164154
const mountComponent = () =>
@@ -203,13 +193,10 @@ describe('Registration.standalone.vue', () => {
203193

204194
accountStore = useAccountStore();
205195
serverStore = useServerStore();
206-
replaceRenewStore = useReplaceRenewStore();
207196
purchaseStore = usePurchaseStore();
208197

209198
serverStore.deprecatedUnraidSSL = undefined;
210199

211-
replaceRenewStore.check = vi.fn();
212-
213200
vi.clearAllMocks();
214201

215202
activationCodeStateHolder.current!.value = null;
@@ -231,8 +218,7 @@ describe('Registration.standalone.vue', () => {
231218
expect(findItemByLabel(t('License key type'))).toBeUndefined();
232219
expect(findItemByLabel(t('Device GUID'))).toBeUndefined();
233220
expect(wrapper.find('[data-testid="key-actions"]').exists()).toBe(true);
234-
expect(wrapper.find('[data-testid="replace-check"]').exists()).toBe(false);
235-
expect(wrapper.find('[data-testid="key-linked-status"]').exists()).toBe(false);
221+
expect(wrapper.find('[data-testid="manage-license-button"]').exists()).toBe(false);
236222
});
237223

238224
it('does not show a connect sign-in action on the registration page', async () => {
@@ -315,6 +301,32 @@ describe('Registration.standalone.vue', () => {
315301
expect(attachedStorageDevicesItem?.props('text')).toBe('8 out of unlimited devices');
316302
});
317303

304+
it('shows a Manage License action with helper text for eligible key states', async () => {
305+
serverStore.state = 'PRO';
306+
serverStore.guid = 'FLASH-GUID-123';
307+
serverStore.keyfile = 'test-keyfile.key';
308+
309+
await wrapper.vm.$nextTick();
310+
311+
const manageButton = wrapper.find('[data-testid="manage-license-button"]');
312+
const manageHelperText = wrapper.find('[data-testid="manage-license-helper-text"]');
313+
314+
expect(manageButton.exists()).toBe(true);
315+
expect(manageButton.text()).toContain('Manage License');
316+
expect(manageHelperText.exists()).toBe(true);
317+
});
318+
319+
it('opens Manage License route when Manage License button is clicked', async () => {
320+
serverStore.state = 'PRO';
321+
serverStore.guid = 'FLASH-GUID-123';
322+
serverStore.keyfile = 'test-keyfile.key';
323+
324+
await wrapper.vm.$nextTick();
325+
await wrapper.find('[data-testid="manage-license-button"]').trigger('click');
326+
327+
expect(accountStore.myKeys).toHaveBeenCalled();
328+
});
329+
318330
it('shows Move License to TPM when TPM licensing is available', async () => {
319331
serverStore.state = 'PRO';
320332
serverStore.guid = '058F-6387-0000-0000F1F1E1C6';
@@ -428,6 +440,6 @@ describe('Registration.standalone.vue', () => {
428440
await wrapper.vm.$nextTick();
429441

430442
const actionNames = serverStore.keyActions?.map((action) => action.name);
431-
expect(actionNames).toEqual(['activate', 'recover', 'trialStart']);
443+
expect(actionNames).toEqual(['activate', 'recover']);
432444
});
433445
});

web/__test__/store/account.test.ts

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ const mockUseMutation = vi.fn(() => {
3939
});
4040

4141
const mockSend = vi.fn();
42-
const mockPurge = vi.fn();
4342
const mockSetError = vi.fn();
4443

4544
vi.mock('~/store/callbackActions', () => ({
@@ -55,12 +54,6 @@ vi.mock('~/store/errors', () => ({
5554
}),
5655
}));
5756

58-
vi.mock('~/store/replaceRenew', () => ({
59-
useReplaceRenewStore: () => ({
60-
purgeValidationResponse: mockPurge,
61-
}),
62-
}));
63-
6457
vi.mock('~/store/server', () => ({
6558
useServerStore: () => ({
6659
serverAccountPayload: {
@@ -109,10 +102,8 @@ describe('Account Store', () => {
109102
);
110103
});
111104

112-
it('should call myKeys action correctly', async () => {
113-
await store.myKeys();
114-
115-
expect(mockPurge).toHaveBeenCalledTimes(1);
105+
it('should call myKeys action correctly', () => {
106+
store.myKeys();
116107
expect(mockSend).toHaveBeenCalledTimes(1);
117108
expect(mockSend).toHaveBeenCalledWith(
118109
ACCOUNT_CALLBACK.toString(),
@@ -122,19 +113,6 @@ describe('Account Store', () => {
122113
);
123114
});
124115

125-
it('should call linkKey action correctly', async () => {
126-
await store.linkKey();
127-
128-
expect(mockPurge).toHaveBeenCalledTimes(1);
129-
expect(mockSend).toHaveBeenCalledTimes(1);
130-
expect(mockSend).toHaveBeenCalledWith(
131-
ACCOUNT_CALLBACK.toString(),
132-
[{ server: { guid: 'test-guid', name: 'test-server' }, type: 'linkKey' }],
133-
undefined,
134-
'post'
135-
);
136-
});
137-
138116
it('should call recover action correctly', () => {
139117
store.recover();
140118
expect(mockSend).toHaveBeenCalledTimes(1);
@@ -250,18 +228,6 @@ describe('Account Store', () => {
250228
'post'
251229
);
252230
});
253-
254-
it('should call trialStart action correctly', () => {
255-
store.trialStart();
256-
257-
expect(mockSend).toHaveBeenCalledTimes(1);
258-
expect(mockSend).toHaveBeenCalledWith(
259-
ACCOUNT_CALLBACK.toString(),
260-
[{ server: { guid: 'test-guid', name: 'test-server' }, type: 'trialStart' }],
261-
undefined,
262-
'post'
263-
);
264-
});
265231
});
266232

267233
describe('State Management', () => {

web/__test__/store/callbackActions.helpers.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const signOutAction = (): ExternalActions => ({
3434
});
3535

3636
const keyAction = (
37-
type: 'activate' | 'purchase' | 'recover' | 'replace' = 'purchase'
37+
type: 'activate' | 'purchase' | 'recover' | 'replace' | 'trialExtend' | 'trialStart' = 'purchase'
3838
): ExternalActions => ({
3939
type,
4040
keyUrl: 'https://example.com/test.key',
@@ -86,6 +86,8 @@ describe('callbackActions.helpers', () => {
8686
it('classifies key actions', () => {
8787
expect(isKeyAction(keyAction())).toBe(true);
8888
expect(isKeyAction(keyAction('activate'))).toBe(true);
89+
expect(isKeyAction(keyAction('trialStart'))).toBe(true);
90+
expect(isKeyAction(keyAction('trialExtend'))).toBe(true);
8991
expect(isKeyAction(signInAction())).toBe(false);
9092
});
9193

web/__test__/store/callbackActions.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,50 @@ describe('Callback Actions Store', () => {
532532
expect(store.callbackStatus).toBe('success');
533533
});
534534

535+
it('should handle trialStart as a key install action', async () => {
536+
const mockData: QueryPayloads = {
537+
type: 'forUpc',
538+
actions: [
539+
{
540+
type: 'trialStart',
541+
keyUrl: 'https://example.com/trial.key',
542+
},
543+
],
544+
sender: 'test',
545+
};
546+
547+
await store.saveCallbackData(mockData);
548+
549+
expect(mockInstall).toHaveBeenCalledWith(mockData.actions[0]);
550+
expect(mockRefreshServerState).toHaveBeenCalledWith({
551+
poll: false,
552+
delayMs: keyActionRefreshDelayMs,
553+
});
554+
expect(store.callbackStatus).toBe('success');
555+
});
556+
557+
it('should handle trialExtend as a key install action', async () => {
558+
const mockData: QueryPayloads = {
559+
type: 'forUpc',
560+
actions: [
561+
{
562+
type: 'trialExtend',
563+
keyUrl: 'https://example.com/trial-extend.key',
564+
},
565+
],
566+
sender: 'test',
567+
};
568+
569+
await store.saveCallbackData(mockData);
570+
571+
expect(mockInstall).toHaveBeenCalledWith(mockData.actions[0]);
572+
expect(mockRefreshServerState).toHaveBeenCalledWith({
573+
poll: false,
574+
delayMs: keyActionRefreshDelayMs,
575+
});
576+
expect(store.callbackStatus).toBe('success');
577+
});
578+
535579
it('should keep key install callbacks successful even when refresh state is not done yet', async () => {
536580
mockRefreshServerStateStatus.value = 'timeout';
537581

web/__test__/store/installKey.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,25 @@ describe('InstallKey Store', () => {
106106
expect(store.keyType).toBe('license');
107107
});
108108

109+
it('should install trialStart keys using the same install flow', async () => {
110+
mockGetFn.mockResolvedValueOnce({ success: true });
111+
const action = createTestAction({
112+
type: 'trialStart',
113+
keyUrl: 'https://example.com/trial.key',
114+
});
115+
116+
await store.install(action);
117+
118+
const { WebguiInstallKey } = await import('~/composables/services/webgui');
119+
120+
expect(WebguiInstallKey.query).toHaveBeenCalledWith({ url: action.keyUrl });
121+
expect(mockGetFn).toHaveBeenCalled();
122+
expect(store.keyInstallStatus).toBe('success');
123+
expect(store.keyActionType).toBe('trialStart');
124+
expect(store.keyUrl).toBe('https://example.com/trial.key');
125+
expect(store.keyType).toBe('trial');
126+
});
127+
109128
it('should extract key type from .key URL', async () => {
110129
mockGetFn.mockResolvedValueOnce({ success: true });
111130

0 commit comments

Comments
 (0)