diff --git a/frontend/src/views/auth/RegisterView.vue b/frontend/src/views/auth/RegisterView.vue
index b4a7a60096..4b89779142 100644
--- a/frontend/src/views/auth/RegisterView.vue
+++ b/frontend/src/views/auth/RegisterView.vue
@@ -134,6 +134,27 @@
+
+
+
+
+
+
-
+
(true)
const emailVerifyEnabled = ref(false)
const promoCodeEnabled = ref(true)
const invitationCodeEnabled = ref(false)
+const affiliateEnabled = ref(false)
const turnstileEnabled = ref(false)
const turnstileSiteKey = ref('')
const siteName = ref('Sub2API')
@@ -459,6 +481,7 @@ onMounted(async () => {
emailVerifyEnabled.value = settings.email_verify_enabled
promoCodeEnabled.value = settings.promo_code_enabled
invitationCodeEnabled.value = settings.invitation_code_enabled
+ affiliateEnabled.value = settings.affiliate_enabled
turnstileEnabled.value = settings.turnstile_enabled
turnstileSiteKey.value = settings.turnstile_site_key || ''
siteName.value = settings.site_name || 'Sub2API'
diff --git a/frontend/src/views/auth/__tests__/RegisterView.spec.ts b/frontend/src/views/auth/__tests__/RegisterView.spec.ts
new file mode 100644
index 0000000000..20f63018a6
--- /dev/null
+++ b/frontend/src/views/auth/__tests__/RegisterView.spec.ts
@@ -0,0 +1,112 @@
+import { flushPromises, mount } from '@vue/test-utils'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import RegisterView from '@/views/auth/RegisterView.vue'
+
+const { getPublicSettingsMock } = vi.hoisted(() => ({
+ getPublicSettingsMock: vi.fn()
+}))
+
+const publicSettings = {
+ registration_enabled: true,
+ email_verify_enabled: false,
+ promo_code_enabled: false,
+ invitation_code_enabled: false,
+ affiliate_enabled: true,
+ turnstile_enabled: true,
+ turnstile_site_key: 'site-key',
+ site_name: 'Sub2API',
+ registration_email_suffix_whitelist: [],
+ linuxdo_oauth_enabled: false,
+ wechat_oauth_enabled: false,
+ oidc_oauth_enabled: false,
+ github_oauth_enabled: false,
+ google_oauth_enabled: false
+}
+
+vi.mock('vue-router', () => ({
+ useRouter: () => ({ push: vi.fn() }),
+ useRoute: () => ({ query: {} })
+}))
+
+vi.mock('vue-i18n', () => ({
+ createI18n: () => ({
+ global: {
+ t: (key: string) => key
+ }
+ }),
+ useI18n: () => ({
+ t: (key: string) => key,
+ locale: { value: 'en' }
+ })
+}))
+
+vi.mock('@/stores', () => ({
+ useAuthStore: () => ({ register: vi.fn() }),
+ useAppStore: () => ({
+ showError: vi.fn(),
+ showSuccess: vi.fn(),
+ showWarning: vi.fn()
+ })
+}))
+
+vi.mock('@/api/auth', async () => {
+ const actual = await vi.importActual('@/api/auth')
+ return {
+ ...actual,
+ getPublicSettings: (...args: unknown[]) => getPublicSettingsMock(...args)
+ }
+})
+
+function mountRegister() {
+ return mount(RegisterView, {
+ global: {
+ stubs: {
+ AuthLayout: { template: '
' },
+ Icon: true,
+ TurnstileWidget: { template: '' },
+ LoginAgreementPrompt: true,
+ EmailOAuthButtons: true,
+ LinuxDoOAuthSection: true,
+ WechatOAuthSection: true,
+ OidcOAuthSection: true,
+ RouterLink: true,
+ transition: false
+ }
+ }
+ })
+}
+
+describe('RegisterView invitation layout', () => {
+ beforeEach(() => {
+ getPublicSettingsMock.mockReset()
+ getPublicSettingsMock.mockResolvedValue(publicSettings)
+ })
+
+ it('keeps the optional affiliate invitation field before Turnstile', async () => {
+ const wrapper = mountRegister()
+ await flushPromises()
+
+ const invitationField = wrapper.get('[data-testid="affiliate-invitation-field"]')
+ const turnstile = wrapper.get('[data-testid="registration-turnstile"]')
+
+ expect(invitationField.get('input').attributes('id')).toBe('affiliate_code')
+ expect(invitationField.text()).toContain('common.optional')
+ expect(
+ invitationField.element.compareDocumentPosition(turnstile.element) &
+ Node.DOCUMENT_POSITION_FOLLOWING
+ ).toBeTruthy()
+ })
+
+ it('uses the mandatory invitation field without duplicating the affiliate field', async () => {
+ getPublicSettingsMock.mockResolvedValueOnce({
+ ...publicSettings,
+ invitation_code_enabled: true
+ })
+
+ const wrapper = mountRegister()
+ await flushPromises()
+
+ expect(wrapper.find('[data-testid="affiliate-invitation-field"]').exists()).toBe(false)
+ expect(wrapper.get('#invitation_code').exists()).toBe(true)
+ })
+})