diff --git a/frontend/src/views/auth/RegisterView.vue b/frontend/src/views/auth/RegisterView.vue index ea43b8965f..a3f9c0e4a5 100644 --- a/frontend/src/views/auth/RegisterView.vue +++ b/frontend/src/views/auth/RegisterView.vue @@ -422,7 +422,13 @@ const confirmPassword = ref('') // Public settings const registrationEnabled = ref(true) const emailVerifyEnabled = ref(false) -const promoCodeEnabled = ref(true) +// Public settings are injected into the app store before Vue mounts. Use that +// value for the first render so a disabled promo-code field never flashes +// while the async settings request is still in flight. If injection is +// unavailable, fail closed until the request explicitly enables the field. +const promoCodeEnabled = ref( + appStore.cachedPublicSettings?.promo_code_enabled === true +) const invitationCodeEnabled = ref(false) const affiliateEnabled = ref(false) const turnstileEnabled = ref(false) diff --git a/frontend/src/views/auth/__tests__/RegisterView.spec.ts b/frontend/src/views/auth/__tests__/RegisterView.spec.ts index 9626a03b4a..8a9ba3a309 100644 --- a/frontend/src/views/auth/__tests__/RegisterView.spec.ts +++ b/frontend/src/views/auth/__tests__/RegisterView.spec.ts @@ -2,12 +2,25 @@ import { flushPromises, mount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' import RegisterView from '@/views/auth/RegisterView.vue' -const { getPublicSettingsMock, registerMock, showErrorMock, pushMock, verifyActionMock } = vi.hoisted(() => ({ +const { + getPublicSettingsMock, + registerMock, + showErrorMock, + pushMock, + verifyActionMock, + appStoreMock +} = vi.hoisted(() => ({ getPublicSettingsMock: vi.fn(), registerMock: vi.fn(), showErrorMock: vi.fn(), pushMock: vi.fn(), - verifyActionMock: vi.fn() + verifyActionMock: vi.fn(), + appStoreMock: { + cachedPublicSettings: null as { promo_code_enabled?: boolean } | null, + showError: (...args: unknown[]) => showErrorMock(...args), + showSuccess: vi.fn(), + showWarning: vi.fn() + } })) const publicSettings = { @@ -49,11 +62,7 @@ vi.mock('vue-i18n', () => ({ vi.mock('@/stores', () => ({ useAuthStore: () => ({ register: (...args: unknown[]) => registerMock(...args) }), - useAppStore: () => ({ - showError: (...args: unknown[]) => showErrorMock(...args), - showSuccess: vi.fn(), - showWarning: vi.fn() - }) + useAppStore: () => appStoreMock })) vi.mock('@/api/auth', async () => { @@ -93,12 +102,40 @@ describe('RegisterView', () => { showErrorMock.mockReset() pushMock.mockReset() verifyActionMock.mockReset() + appStoreMock.cachedPublicSettings = null sessionStorage.removeItem('register_data') verifyActionMock.mockResolvedValue({ token: 'ticket', randstr: 'randstr' }) getPublicSettingsMock.mockResolvedValue(publicSettings) registerMock.mockResolvedValue({}) }) + it('does not flash the promo-code field before disabled settings finish loading', async () => { + let resolveSettings!: (settings: typeof publicSettings) => void + getPublicSettingsMock.mockReturnValueOnce( + new Promise((resolve) => { + resolveSettings = resolve + }) + ) + + const wrapper = mountRegister() + + expect(wrapper.find('#promo_code').exists()).toBe(false) + + resolveSettings(publicSettings) + await flushPromises() + + expect(wrapper.find('#promo_code').exists()).toBe(false) + }) + + it('uses injected public settings to show an enabled promo-code field on first render', () => { + appStoreMock.cachedPublicSettings = { promo_code_enabled: true } + getPublicSettingsMock.mockReturnValueOnce(new Promise(() => {})) + + const wrapper = mountRegister() + + expect(wrapper.find('#promo_code').exists()).toBe(true) + }) + it.each([ ['', 'auth.confirmPasswordRequired'], ['different-password', 'auth.passwordsDoNotMatch']