@@ -4,6 +4,7 @@ import { keyboardInset } from "$lib/actions/keyboardInset";
44import type { GlobalState } from " $lib/global" ;
55import { LoadingSheet , PinDots } from " $lib/ui" ;
66import * as Button from " $lib/ui/Button" ;
7+ import { continueAfterSuccessfulAuth } from " $lib/utils/postLogin" ;
78import {
89 type AuthOptions ,
910 authenticate ,
@@ -12,10 +13,25 @@ import {
1213import { getContext , onMount } from " svelte" ;
1314import StepHeader from " ../onboarding/steps/StepHeader.svelte" ;
1415
16+ // Splash sets this when it has already tried biometric over its own screen.
17+ // /login then skips re-prompting and just shows the PIN UI.
18+ const BIOMETRIC_ATTEMPTED_KEY = " biometricAttemptedOnSplash" ;
19+
1520let pin = $state (" " );
1621let isError = $state (false );
1722let isPostAuthLoading = $state (false );
1823let hasPendingDeepLink = $state (false );
24+ let pinInput = $state <HTMLInputElement | undefined >(undefined );
25+
26+ // Refocus the hidden PIN input on background taps so the user doesn't have
27+ // to aim for the dots themselves to summon the keyboard. We skip taps that
28+ // land on interactive elements so their own click handlers (Clear PIN, back
29+ // chevron, etc.) still behave normally.
30+ function handleBackgroundClick(e : MouseEvent ) {
31+ const target = e .target as HTMLElement | null ;
32+ if (target ?.closest (' button, a, [role="button"]' )) return ;
33+ pinInput ?.focus ({ preventScroll: true });
34+ }
1935
2036const getGlobalState = getContext <() => GlobalState | undefined >(" globalState" );
2137let globalState: GlobalState | undefined = $state (undefined );
@@ -37,61 +53,6 @@ async function clearPin() {
3753 isError = false ;
3854}
3955
40- async function continueAfterSuccessfulAuth(gs : GlobalState ) {
41- // Fire-and-forget post-login chores. These hit the network with no client
42- // timeout, so awaiting them here can strand the user on the "Logging you
43- // in…" spinner indefinitely. The app pages will retry as needed.
44- try {
45- const vault = await gs .vaultController .vault ;
46- if (vault ?.ename ) {
47- const ename = vault .ename ;
48- void gs .vaultController
49- .checkHealth (ename )
50- .then ((health ) => {
51- if (! health .healthy ) {
52- console .warn (
53- " eVault health check failed:" ,
54- health .error ,
55- );
56- }
57- })
58- .catch ((error ) =>
59- console .error (" eVault health check error:" , error ),
60- );
61- void gs .vaultController
62- .syncPublicKey (ename )
63- .catch ((error ) =>
64- console .error (" Error syncing public key:" , error ),
65- );
66- void gs .notificationService
67- .registerDevice (ename )
68- .catch ((error ) =>
69- console .error (
70- " Error registering device for notifications:" ,
71- error ,
72- ),
73- );
74- }
75- } catch (error ) {
76- console .error (" Error reading vault during login:" , error );
77- }
78-
79- const pendingDeepLink = sessionStorage .getItem (" pendingDeepLink" );
80- if (pendingDeepLink ) {
81- try {
82- sessionStorage .setItem (" deepLinkData" , pendingDeepLink );
83- sessionStorage .removeItem (" pendingDeepLink" );
84- await goto (" /scan-qr" );
85- return ;
86- } catch (error ) {
87- console .error (" Error processing pending deep link:" , error );
88- sessionStorage .removeItem (" pendingDeepLink" );
89- }
90- }
91-
92- await goto (" /main" );
93- }
94-
9556async function verifyAndAdvance(currentPin : string ) {
9657 if (isPostAuthLoading ) return ;
9758 if (! globalState ) return ;
@@ -135,6 +96,16 @@ onMount(async () => {
13596 const pendingDeepLink = sessionStorage .getItem (" pendingDeepLink" );
13697 hasPendingDeepLink = !! pendingDeepLink ;
13798
99+ // If the splash already prompted biometric over its own screen, skip the
100+ // retry here and let the user enter their PIN. The flag survives the
101+ // route transition but is single-use.
102+ const biometricHandledBySplash =
103+ sessionStorage .getItem (BIOMETRIC_ATTEMPTED_KEY ) === " true" ;
104+ if (biometricHandledBySplash ) {
105+ sessionStorage .removeItem (BIOMETRIC_ATTEMPTED_KEY );
106+ return ;
107+ }
108+
138109 // Try biometric first if available.
139110 if (
140111 (await gs .securityController .biometricSupport ) &&
@@ -155,8 +126,15 @@ onMount(async () => {
155126});
156127 </script >
157128
129+ <!-- The PIN input is the only meaningful interaction here; keyboard users
130+ focus it directly via tab. The pointer-only listener just refocuses the
131+ same input when sighted users tap the background, so there's no
132+ keyboard interaction worth mirroring. -->
133+ <!-- svelte-ignore a11y_click_events_have_key_events -->
134+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
158135<main
159136 use:keyboardInset
137+ onclick ={handleBackgroundClick }
160138 class =" h-dvh overflow-hidden px-[5vw] flex flex-col bg-white"
161139 style =" padding-top: max(2svh, env(safe-area-inset-top)); padding-bottom: calc(max(16px, env(safe-area-inset-bottom)) + var(--kb-inset, 0px));"
162140>
@@ -173,7 +151,7 @@ onMount(async () => {
173151 {/if }
174152
175153 <section class =" flex-1 flex flex-col items-center justify-center gap-6" >
176- <PinDots bind:pin />
154+ <PinDots bind:pin bind:inputEl ={ pinInput } />
177155
178156 {#if isError }
179157 <p class =" text-danger text-sm font-medium" role =" alert" >
0 commit comments