From 8564d356828c3952a4466cbf1203593f6cc00171 Mon Sep 17 00:00:00 2001 From: James18232 <180368042+James18232@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:56:18 +1000 Subject: [PATCH] feat: login code input boxes (#1545) Co-authored-by: james Co-authored-by: Elias Schneider --- frontend/messages/en.json | 1 + .../src/lib/components/ui/input-otp/index.ts | 15 +++++ .../ui/input-otp/input-otp-group.svelte | 23 +++++++ .../ui/input-otp/input-otp-separator.svelte | 27 ++++++++ .../ui/input-otp/input-otp-slot.svelte | 33 ++++++++++ .../components/ui/input-otp/input-otp.svelte | 27 ++++++++ .../login/alternative/code/+page.svelte | 65 +++++++++++++++---- tests/specs/one-time-access-token.spec.ts | 6 +- 8 files changed, 184 insertions(+), 13 deletions(-) create mode 100644 frontend/src/lib/components/ui/input-otp/index.ts create mode 100644 frontend/src/lib/components/ui/input-otp/input-otp-group.svelte create mode 100644 frontend/src/lib/components/ui/input-otp/input-otp-separator.svelte create mode 100644 frontend/src/lib/components/ui/input-otp/input-otp-slot.svelte create mode 100644 frontend/src/lib/components/ui/input-otp/input-otp.svelte diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 076658d1..ea1c369b 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -531,6 +531,7 @@ "user_has_no_passkeys_yet": "This user has no passkeys yet.", "replay_protection": "Replay Protection", "replay_protection_description": "If enabled the provided token can only be used once. If your provider uses the same token multiple times, you may need to disable this option.", + "i_have_a_longer_code": "I have a longer code", "pkce_supported_client_title": "This client supports PKCE", "pkce_supported_client_description": "This client supports Proof Key for Code Exchange (PKCE). PKCE is a security feature that helps protect against certain attacks during the OAuth 2.0 authorization process. It's recommended to enable it." } diff --git a/frontend/src/lib/components/ui/input-otp/index.ts b/frontend/src/lib/components/ui/input-otp/index.ts new file mode 100644 index 00000000..e9ae273e --- /dev/null +++ b/frontend/src/lib/components/ui/input-otp/index.ts @@ -0,0 +1,15 @@ +import Root from "./input-otp.svelte"; +import Group from "./input-otp-group.svelte"; +import Slot from "./input-otp-slot.svelte"; +import Separator from "./input-otp-separator.svelte"; + +export { + Root, + Group, + Slot, + Separator, + Root as InputOTP, + Group as InputOTPGroup, + Slot as InputOTPSlot, + Separator as InputOTPSeparator, +}; diff --git a/frontend/src/lib/components/ui/input-otp/input-otp-group.svelte b/frontend/src/lib/components/ui/input-otp/input-otp-group.svelte new file mode 100644 index 00000000..084577f7 --- /dev/null +++ b/frontend/src/lib/components/ui/input-otp/input-otp-group.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/input-otp/input-otp-separator.svelte b/frontend/src/lib/components/ui/input-otp/input-otp-separator.svelte new file mode 100644 index 00000000..e6025bb0 --- /dev/null +++ b/frontend/src/lib/components/ui/input-otp/input-otp-separator.svelte @@ -0,0 +1,27 @@ + + + diff --git a/frontend/src/lib/components/ui/input-otp/input-otp-slot.svelte b/frontend/src/lib/components/ui/input-otp/input-otp-slot.svelte new file mode 100644 index 00000000..8fe0a189 --- /dev/null +++ b/frontend/src/lib/components/ui/input-otp/input-otp-slot.svelte @@ -0,0 +1,33 @@ + + + + {cell.char} + {#if cell.hasFakeCaret} +
+ +
+ {/if} +
diff --git a/frontend/src/lib/components/ui/input-otp/input-otp.svelte b/frontend/src/lib/components/ui/input-otp/input-otp.svelte new file mode 100644 index 00000000..f80a2f2d --- /dev/null +++ b/frontend/src/lib/components/ui/input-otp/input-otp.svelte @@ -0,0 +1,27 @@ + + + diff --git a/frontend/src/routes/login/alternative/code/+page.svelte b/frontend/src/routes/login/alternative/code/+page.svelte index c7c4b40e..8799644d 100644 --- a/frontend/src/routes/login/alternative/code/+page.svelte +++ b/frontend/src/routes/login/alternative/code/+page.svelte @@ -2,6 +2,7 @@ import { afterNavigate, goto } from '$app/navigation'; import SignInWrapper from '$lib/components/login-wrapper.svelte'; import { Button } from '$lib/components/ui/button'; + import * as InputOTP from '$lib/components/ui/input-otp/index.js'; import Input from '$lib/components/ui/input/input.svelte'; import { m } from '$lib/paraglide/messages'; import UserService from '$lib/services/user-service'; @@ -17,6 +18,10 @@ let error: string | undefined = $state(); let backHref = $state('/login/alternative'); + let longCodeRequested = $state(code.length > 6); + let showLongCodeOption = $state(true); + let codeComplete = $derived(longCodeRequested ? code.length === 16 : code.length === 6); + const userService = new UserService(); // If the previous page is a Pocket ID page, go back there instead of the generic alternative login page @@ -28,6 +33,7 @@ async function authenticate() { if (!code?.trim()) return; + if (!codeComplete) return; isLoading = true; try { const user = await userService.exchangeOneTimeAccessToken(code); @@ -49,6 +55,10 @@ if (code) { authenticate(); } + + if (data.redirect.startsWith('/interaction')) { + showLongCodeOption = false; + } }); @@ -68,18 +78,49 @@ {:else}

{m.enter_the_code_you_received_to_sign_in()}

{/if} -
- -
- - + +
+ {#if longCodeRequested} + + {:else} + + {#snippet children({ cells })} + + {#each cells as cell} + + {/each} + + {/snippet} + + {/if} + {#if !longCodeRequested && showLongCodeOption} +
+ +
+ {/if} +
+
+ +
diff --git a/tests/specs/one-time-access-token.spec.ts b/tests/specs/one-time-access-token.spec.ts index d388a4b5..25506aad 100644 --- a/tests/specs/one-time-access-token.spec.ts +++ b/tests/specs/one-time-access-token.spec.ts @@ -17,7 +17,9 @@ test('Sign in with login code', async ({ page }) => { test('Sign in with login code entered manually', async ({ page }) => { const token = oneTimeAccessTokens.filter((t) => !t.expired)[0]; await page.goto('/lc'); - + + await page.getByText('I have a longer code').click(); + await page.getByPlaceholder('Code').first().fill(token.token); await page.getByText('Submit').first().click(); @@ -38,6 +40,8 @@ test('Sign in with login code entered manually fails', async ({ page }) => { const token = oneTimeAccessTokens.filter((t) => t.expired)[0]; await page.goto('/lc'); + await page.getByText('I have a longer code').click(); + await page.getByPlaceholder('Code').first().fill(token.token); await page.getByText('Submit').first().click();