Skip to content

Commit 5331d1c

Browse files
Sahil2004coodoscoderabbitai[bot]
authored
feat: added the recover your evault link when entering wrong PIN. Not… (#995)
* feat: added the recover your evault link when entering wrong PIN. Not based on the attempts. * Update infrastructure/eid-wallet/src/routes/(auth)/login/+page.svelte Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: Merul Dhiman <69296233+coodos@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent ec3fb2c commit 5331d1c

4 files changed

Lines changed: 139 additions & 130 deletions

File tree

infrastructure/eid-wallet/src-tauri/gen/android/.idea/deploymentTargetSelector.xml

Lines changed: 0 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

infrastructure/eid-wallet/src-tauri/gen/android/.idea/gradle.xml

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

infrastructure/eid-wallet/src-tauri/gen/android/.idea/misc.xml

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

infrastructure/eid-wallet/src/routes/(auth)/login/+page.svelte

Lines changed: 134 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,129 +1,137 @@
11
<script lang="ts">
2-
import { goto } from "$app/navigation";
3-
import { keyboardInset } from "$lib/actions/keyboardInset";
4-
import type { GlobalState } from "$lib/global";
5-
import { LoadingSheet, PinDots } from "$lib/ui";
6-
import * as Button from "$lib/ui/Button";
7-
import { continueAfterSuccessfulAuth } from "$lib/utils/postLogin";
8-
import {
9-
type AuthOptions,
10-
authenticate,
11-
checkStatus,
12-
} from "@tauri-apps/plugin-biometric";
13-
import { getContext, onMount } from "svelte";
14-
import StepHeader from "../onboarding/steps/StepHeader.svelte";
15-
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-
20-
let pin = $state("");
21-
let isError = $state(false);
22-
let isPostAuthLoading = $state(false);
23-
let 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-
}
35-
36-
const getGlobalState = getContext<() => GlobalState | undefined>("globalState");
37-
let globalState: GlobalState | undefined = $state(undefined);
38-
39-
const authOpts: AuthOptions = {
40-
allowDeviceCredential: false,
41-
cancelTitle: "Cancel",
42-
// iOS
43-
fallbackTitle: "Please enter your PIN",
44-
// Android
45-
title: "Login",
46-
subtitle: "Please authenticate to continue",
47-
confirmationRequired: true,
48-
};
49-
50-
async function clearPin() {
51-
if (isPostAuthLoading) return;
52-
pin = "";
53-
isError = false;
54-
}
55-
56-
async function verifyAndAdvance(currentPin: string) {
57-
if (isPostAuthLoading) return;
58-
if (!globalState) return;
59-
if (currentPin.length !== 4) return;
60-
61-
isError = false;
62-
isPostAuthLoading = true;
63-
64-
const ok = await globalState.securityController.verifyPin(currentPin);
65-
if (!ok) {
66-
isError = true;
67-
pin = "";
68-
isPostAuthLoading = false;
69-
return;
2+
import { goto } from "$app/navigation";
3+
import { keyboardInset } from "$lib/actions/keyboardInset";
4+
import type { GlobalState } from "$lib/global";
5+
import { LoadingSheet, PinDots } from "$lib/ui";
6+
import * as Button from "$lib/ui/Button";
7+
import { continueAfterSuccessfulAuth } from "$lib/utils/postLogin";
8+
import {
9+
type AuthOptions,
10+
authenticate,
11+
checkStatus,
12+
} from "@tauri-apps/plugin-biometric";
13+
import { getContext, onMount } from "svelte";
14+
import StepHeader from "../onboarding/steps/StepHeader.svelte";
15+
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+
20+
let pin = $state("");
21+
let isError = $state(false);
22+
let isPostAuthLoading = $state(false);
23+
let 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 });
7034
}
7135
72-
await continueAfterSuccessfulAuth(globalState);
73-
}
74-
75-
$effect(() => {
76-
if (pin.length === 4) verifyAndAdvance(pin);
77-
});
78-
79-
onMount(async () => {
80-
// Root +layout creates globalState in its own onMount (which runs after
81-
// children). Poll until it's available — same pattern as (app)/+layout.
82-
let gs = getGlobalState();
83-
let retries = 0;
84-
while (!gs && retries < 50) {
85-
await new Promise((r) => setTimeout(r, 100));
86-
gs = getGlobalState();
87-
retries++;
88-
}
89-
if (!gs) {
90-
console.error("Global state never became available");
91-
await goto("/");
92-
return;
93-
}
94-
globalState = gs;
95-
96-
const pendingDeepLink = sessionStorage.getItem("pendingDeepLink");
97-
hasPendingDeepLink = !!pendingDeepLink;
98-
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;
36+
const getGlobalState =
37+
getContext<() => GlobalState | undefined>("globalState");
38+
let globalState: GlobalState | undefined = $state(undefined);
39+
40+
const authOpts: AuthOptions = {
41+
allowDeviceCredential: false,
42+
cancelTitle: "Cancel",
43+
// iOS
44+
fallbackTitle: "Please enter your PIN",
45+
// Android
46+
title: "Login",
47+
subtitle: "Please authenticate to continue",
48+
confirmationRequired: true,
49+
};
50+
51+
async function clearPin() {
52+
if (isPostAuthLoading) return;
53+
pin = "";
54+
isError = false;
10755
}
10856
109-
// Try biometric first if available.
110-
if (
111-
(await gs.securityController.biometricSupport) &&
112-
(await checkStatus()).isAvailable
113-
) {
57+
async function verifyAndAdvance(currentPin: string) {
58+
if (isPostAuthLoading) return;
59+
if (!globalState) return;
60+
if (currentPin.length !== 4) return;
61+
62+
isError = false;
63+
isPostAuthLoading = true;
64+
11465
try {
115-
await authenticate(
116-
"You must authenticate with PIN first",
117-
authOpts,
118-
);
119-
isPostAuthLoading = true;
120-
await continueAfterSuccessfulAuth(gs);
66+
const ok = await globalState.securityController.verifyPin(currentPin);
67+
if (!ok) {
68+
isError = true;
69+
pin = "";
70+
return;
71+
}
72+
73+
await continueAfterSuccessfulAuth(globalState);
12174
} catch (e) {
122-
console.error("Biometric authentication failed", e);
75+
console.error("PIN verification failed", e);
76+
isError = true;
77+
pin = "";
78+
} finally {
12379
isPostAuthLoading = false;
12480
}
12581
}
126-
});
82+
83+
$effect(() => {
84+
if (pin.length === 4) verifyAndAdvance(pin);
85+
});
86+
87+
onMount(async () => {
88+
// Root +layout creates globalState in its own onMount (which runs after
89+
// children). Poll until it's available — same pattern as (app)/+layout.
90+
let gs = getGlobalState();
91+
let retries = 0;
92+
while (!gs && retries < 50) {
93+
await new Promise((r) => setTimeout(r, 100));
94+
gs = getGlobalState();
95+
retries++;
96+
}
97+
if (!gs) {
98+
console.error("Global state never became available");
99+
await goto("/");
100+
return;
101+
}
102+
globalState = gs;
103+
104+
const pendingDeepLink = sessionStorage.getItem("pendingDeepLink");
105+
hasPendingDeepLink = !!pendingDeepLink;
106+
107+
// If the splash already prompted biometric over its own screen, skip the
108+
// retry here and let the user enter their PIN. The flag survives the
109+
// route transition but is single-use.
110+
const biometricHandledBySplash =
111+
sessionStorage.getItem(BIOMETRIC_ATTEMPTED_KEY) === "true";
112+
if (biometricHandledBySplash) {
113+
sessionStorage.removeItem(BIOMETRIC_ATTEMPTED_KEY);
114+
return;
115+
}
116+
117+
// Try biometric first if available.
118+
if (
119+
(await gs.securityController.biometricSupport) &&
120+
(await checkStatus()).isAvailable
121+
) {
122+
try {
123+
await authenticate(
124+
"You must authenticate with PIN first",
125+
authOpts,
126+
);
127+
isPostAuthLoading = true;
128+
await continueAfterSuccessfulAuth(gs);
129+
} catch (e) {
130+
console.error("Biometric authentication failed", e);
131+
isPostAuthLoading = false;
132+
}
133+
}
134+
});
127135
</script>
128136

129137
<!-- The PIN input is the only meaningful interaction here; keyboard users
@@ -154,9 +162,16 @@ onMount(async () => {
154162
<PinDots bind:pin bind:inputEl={pinInput} />
155163

156164
{#if isError}
157-
<p class="text-danger text-sm font-medium" role="alert">
158-
Your PIN does not match, try again.
159-
</p>
165+
<article class="flex flex-col items-center justify-center gap-2">
166+
<p class="text-danger text-sm font-medium" role="alert">
167+
Your PIN does not match, try again.
168+
</p>
169+
<p class="text-black-700 opacity-50 text-sm font-medium">
170+
Forgot your pin? <a href="/recover"
171+
><u>Recover your eVault.</u></a
172+
>
173+
</p>
174+
</article>
160175
{/if}
161176
</section>
162177

0 commit comments

Comments
 (0)