From de3420fe80c353adba3a992ae06e363e028fda0e Mon Sep 17 00:00:00 2001 From: Cole Gentry Date: Mon, 28 Sep 2026 16:29:58 -0400 Subject: [PATCH 1/2] fix(membership): return to /membership/youtube without a query string Supabase matches linkIdentity's redirectTo against the Redirect URLs allowlist as a whole address, so '?linked=1' failed the exact entry and the user landed on the Site URL with an unused code. The returned code/error_code already mark the return. --- app/pages/membership/youtube.vue | 8 ++++++-- tests/unit/pages/membership-youtube.test.ts | 13 +++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/pages/membership/youtube.vue b/app/pages/membership/youtube.vue index 5fbb4bec..dcf89077 100644 --- a/app/pages/membership/youtube.vue +++ b/app/pages/membership/youtube.vue @@ -13,7 +13,7 @@ * `youtube-bridge-sync` Edge Function (via POST /api/membership/youtube-sync) * then reads the role and attaches the membership. * - * Redirect: `linkIdentity` comes back HERE (`?linked=1&code=…`), not to + * Redirect: `linkIdentity` comes back HERE (`?code=…`), not to * /auth/callback. The callback has no redirect parameter (it uses the /login * localStorage stash), it records a `login_success` and can divert a user to * /welcome, and on a GoTrue error redirect it drops the `error_code` that tells @@ -168,7 +168,11 @@ try { const { error } = await supabase.auth.linkIdentity({ provider: 'discord', - options: { redirectTo: `${window.location.origin}${SELF_PATH}?linked=1` }, + // No query string: Supabase matches redirectTo against the Redirect URLs + // allowlist as a whole address, so `?linked=1` failed the exact entry and + // GoTrue fell back to the Site URL (seen 2026-09-28). The returned + // `code` / `error_code` already mark the return. + options: { redirectTo: `${window.location.origin}${SELF_PATH}` }, }); if (error) { outcome.value = outcomeForLinkError((error as { code?: string }).code, error.message); diff --git a/tests/unit/pages/membership-youtube.test.ts b/tests/unit/pages/membership-youtube.test.ts index 301f0a52..f085dd6f 100644 --- a/tests/unit/pages/membership-youtube.test.ts +++ b/tests/unit/pages/membership-youtube.test.ts @@ -6,9 +6,10 @@ * Covers: * - signed out → sign-in card that returns here * - no Discord identity → Link Discord calls linkIdentity('discord') with a - * redirect back to /membership/youtube?linked=1 + * redirect back to /membership/youtube (no query string: the allowlist + * matches the whole address) * - Discord already linked → "Check my YouTube membership" instead - * - ?linked=1&code= → PKCE exchange, URL cleaned, sync runs at once + * - ?code= → PKCE exchange, URL cleaned, sync runs at once * - every sync status and error status renders its own outcome * - linkIdentity errors: identity already linked elsewhere / linking disabled * @@ -132,7 +133,7 @@ describe('no Discord identity yet', () => { await flushPromises(); expect(supabase.auth.linkIdentity).toHaveBeenCalledWith({ provider: 'discord', - options: { redirectTo: `${window.location.origin}/membership/youtube?linked=1` }, + options: { redirectTo: `${window.location.origin}/membership/youtube` }, }); }); @@ -189,11 +190,11 @@ describe('Discord already linked', () => { }); }); -describe('return from Discord (?linked=1)', () => { +describe('return from Discord (?code=)', () => { it('exchanges the code, cleans the URL and syncs at once', async () => { const { supabase } = stubEnvironment({ supabase: makeSupabaseStub({ providers: ['discord'] }), - query: { linked: '1', code: 'abc' }, + query: { code: 'abc' }, fetchImpl: () => Promise.resolve({ status: 'linked', plan: 'pro' }), }); const wrapper = await mountPage(); @@ -207,7 +208,7 @@ describe('return from Discord (?linked=1)', () => { const supabase = makeSupabaseStub({ providers: ['discord'] }); supabase.auth.exchangeCodeForSession.mockResolvedValue({ data: { session: null }, error: { message: 'x' } } as any); vi.spyOn(console, 'error').mockImplementation(() => {}); - stubEnvironment({ supabase, query: { linked: '1', code: 'abc' } }); + stubEnvironment({ supabase, query: { code: 'abc' } }); await mountPage(); expect(fetchMock).toHaveBeenCalledTimes(1); }); From b1a3d34454fbbaeaf8065bd2d40c4443e491a44c Mon Sep 17 00:00:00 2001 From: Cole Gentry Date: Mon, 28 Sep 2026 16:31:25 -0400 Subject: [PATCH 2/2] fix(membership): start the membership check on a returned code alone Without ?linked=1 the return carries only ?code=; the sync ran only on 'linked', so the page linked Discord but never checked the level. --- app/pages/membership/youtube.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/pages/membership/youtube.vue b/app/pages/membership/youtube.vue index dcf89077..21324a95 100644 --- a/app/pages/membership/youtube.vue +++ b/app/pages/membership/youtube.vue @@ -286,7 +286,8 @@ return; } - if (back.linked) { + // Back from Discord: the code (or the legacy ?linked=1) marks the return. + if (back.linked || back.code) { await checkMembership(); return; }