Skip to content

Commit 73f515f

Browse files
feat: passkey for Svelte (#176)
Signed-off-by: David Dal Busco <david.dalbusco@outlook.com>
1 parent 870a1d6 commit 73f515f

7 files changed

Lines changed: 295 additions & 2 deletions

File tree

templates/sveltekit-example/src/lib/components/Auth.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { userSignedIn } from '$lib/derived/user.derived';
66
import Logout from '$lib/components/Logout.svelte';
77
import Login from '$lib/components/Login.svelte';
8+
import Passkey from '$lib/components/passkey/Passkey.svelte';
89
910
interface Props {
1011
children: Snippet;
@@ -30,5 +31,9 @@
3031
<Logout />
3132
</div>
3233
{:else}
33-
<Login />
34+
<div class="gap flex flex-col">
35+
<Passkey />
36+
37+
<Login />
38+
</div>
3439
{/if}

templates/sveltekit-example/src/lib/components/Login.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
const signInWithII = async () => {
66
await signIn({
7-
internet_identity: {},
7+
internet_identity: {}
88
});
99
};
1010
</script>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<script lang="ts">
2+
import {
3+
type SignProgress,
4+
type SignProgressFn,
5+
signUp,
6+
WebAuthnSignUpProgressStep
7+
} from '@junobuild/core';
8+
import type { PasskeyProps } from '$lib/types/passkey';
9+
import Button from '$lib/components/Button.svelte';
10+
import Progress from '$lib/components/passkey/Progress.svelte';
11+
12+
type ProgressSignUp =
13+
| { state: 'init' | 'setup' | 'hidden' }
14+
| { state: 'progress'; detail: SignProgress<WebAuthnSignUpProgressStep> };
15+
16+
let { progress: wizardProgress, onProgress: wizardOnProgress }: PasskeyProps = $props();
17+
18+
let inputText = $state('');
19+
20+
const progress = $derived(
21+
wizardProgress === undefined
22+
? { state: 'init' }
23+
: 'signUp' in wizardProgress
24+
? { state: 'progress', detail: wizardProgress.signUp }
25+
: 'setup' in wizardProgress
26+
? { state: 'setup' }
27+
: { state: 'hidden' }
28+
);
29+
30+
const goToSetup = async () => {
31+
wizardOnProgress({ setup: null });
32+
};
33+
34+
const onProgress: SignProgressFn<WebAuthnSignUpProgressStep> = (progress) =>
35+
wizardOnProgress({ signUp: progress });
36+
37+
const doSignUp = async () => {
38+
try {
39+
await signUp({
40+
webauthn: {
41+
options: {
42+
onProgress,
43+
...(inputText !== '' && {
44+
passkey: { user: { displayName: inputText } }
45+
})
46+
}
47+
}
48+
});
49+
} catch (error: unknown) {
50+
wizardOnProgress(undefined);
51+
52+
throw error;
53+
}
54+
};
55+
</script>
56+
57+
{#if progress.state === 'init'}
58+
<p>First time here? Use your device (Face ID, Windows Hello, or screen lock) to get in.</p>
59+
60+
<Button onclick={goToSetup}>Create a new passkey</Button>
61+
{/if}
62+
63+
{#if progress.state === 'setup'}
64+
<p>Want to give it a nickname so you'll spot it easily later?</p>
65+
66+
<input
67+
class="mx-0 mt-2 mb-6 block w-full resize-none rounded-sm border-[3px] border-black bg-white px-3 py-1.5 text-base font-normal shadow-[5px_5px_0px_rgba(0,0,0,1)] focus:outline-hidden"
68+
placeholder="An optional nickname"
69+
bind:value={inputText}
70+
/>
71+
72+
<Button onclick={doSignUp}>Create now</Button>
73+
{/if}
74+
75+
{#if progress.state === 'progress'}
76+
<Progress>
77+
{#if progress.detail.step === WebAuthnSignUpProgressStep.CreatingUserCredential}
78+
<span>Creating user credential...</span>
79+
{:else if progress.detail.step === WebAuthnSignUpProgressStep.ValidatingUserCredential}
80+
<span>Validating user credential...</span>
81+
{:else if progress.detail.step === WebAuthnSignUpProgressStep.FinalizingCredential}
82+
<span>Finalizing credential...</span>
83+
{:else if progress.detail.step === WebAuthnSignUpProgressStep.Signing}
84+
<span>Signing request...</span>
85+
{:else if progress.detail.step === WebAuthnSignUpProgressStep.FinalizingSession}
86+
<span>Finalizing session...</span>
87+
{:else if progress.detail.step === WebAuthnSignUpProgressStep.RegisteringUser}
88+
<span>Registering user...</span>
89+
{/if}
90+
</Progress>
91+
{/if}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<script lang="ts">
2+
import { isWebAuthnAvailable } from '@junobuild/core';
3+
import type { PasskeyProgress } from '$lib/types/passkey';
4+
import { onMount } from 'svelte';
5+
import Button from '$lib/components/Button.svelte';
6+
import CreatePasskey from '$lib/components/passkey/CreatePasskey.svelte';
7+
import UsePasskey from '$lib/components/passkey/UsePasskey.svelte';
8+
import Backdrop from '$lib/components/Backdrop.svelte';
9+
10+
let passkeySupported = $state(true);
11+
let showModal = $state(false);
12+
let progress = $state<PasskeyProgress | undefined>(undefined);
13+
14+
onMount(async () => {
15+
passkeySupported = await isWebAuthnAvailable();
16+
});
17+
18+
const start = async () => {
19+
progress = undefined;
20+
showModal = true;
21+
};
22+
23+
const close = async () => {
24+
showModal = false;
25+
progress = undefined;
26+
};
27+
28+
const onProgress = (p: PasskeyProgress | undefined) => {
29+
progress = p;
30+
};
31+
</script>
32+
33+
{#if passkeySupported}
34+
<Button onclick={start}>Continue with Passkey</Button>
35+
{/if}
36+
37+
{#if showModal}
38+
<div
39+
class="animate-fade fixed inset-0 z-50 p-16 md:px-24 md:py-44"
40+
role="dialog"
41+
aria-modal="true"
42+
aria-labelledby="modalTitle"
43+
>
44+
<div
45+
class="w-full max-w-md rounded-sm border-[3px] border-black bg-white px-4 py-3 shadow-[5px_5px_0px_rgba(0,0,0,1)]"
46+
>
47+
{#if progress === undefined || 'setup' in progress}
48+
<div class="flex items-start justify-between">
49+
<h2 id="modalTitle" class="text-xl font-bold text-gray-900 sm:text-2xl">Hey 👋</h2>
50+
51+
<button
52+
type="button"
53+
class="-me-4 -mt-4 rounded-full p-2 text-gray-400 transition-colors hover:bg-gray-50 hover:text-gray-600 focus:outline-none"
54+
aria-label="Close"
55+
onclick={close}
56+
>
57+
<svg
58+
xmlns="http://www.w3.org/2000/svg"
59+
class="size-5"
60+
fill="none"
61+
viewBox="0 0 24 24"
62+
stroke="currentColor"
63+
>
64+
<path
65+
stroke-linecap="round"
66+
stroke-linejoin="round"
67+
stroke-width="2"
68+
d="M6 18L18 6M6 6l12 12"
69+
/>
70+
</svg>
71+
</button>
72+
</div>
73+
{/if}
74+
75+
<div class="mt-4">
76+
<CreatePasskey {progress} {onProgress} />
77+
<UsePasskey {progress} {onProgress} />
78+
</div>
79+
</div>
80+
</div>
81+
82+
<Backdrop />
83+
{/if}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<script lang="ts">
2+
import type { Snippet } from 'svelte';
3+
4+
interface Props {
5+
children: Snippet;
6+
}
7+
8+
let { children }: Props = $props();
9+
</script>
10+
11+
<div class="flex flex-col items-center justify-center gap-2 pt-6">
12+
<svg
13+
class="animate-spin"
14+
viewBox="0 0 64 64"
15+
fill="none"
16+
xmlns="http://www.w3.org/2000/svg"
17+
width="48"
18+
height="48"
19+
>
20+
<path
21+
d="M32 3C35.8083 3 39.5794 3.75011 43.0978 5.20749C46.6163 6.66488 49.8132 8.80101 52.5061 11.4939C55.199 14.1868 57.3351 17.3837 58.7925 20.9022C60.2499 24.4206 61 28.1917 61 32C61 35.8083 60.2499 39.5794 58.7925 43.0978C57.3351 46.6163 55.199 49.8132 52.5061 52.5061C49.8132 55.199 46.6163 57.3351 43.0978 58.7925C39.5794 60.2499 35.8083 61 32 61C28.1917 61 24.4206 60.2499 20.9022 58.7925C17.3837 57.3351 14.1868 55.199 11.4939 52.5061C8.801 49.8132 6.66487 46.6163 5.20749 43.0978C3.7501 39.5794 3 35.8083 3 32C3 28.1917 3.75011 24.4206 5.2075 20.9022C6.66489 17.3837 8.80101 14.1868 11.4939 11.4939C14.1868 8.80099 17.3838 6.66487 20.9022 5.20749C24.4206 3.7501 28.1917 3 32 3L32 3Z"
22+
stroke="currentColor"
23+
stroke-width="5"
24+
stroke-linecap="round"
25+
stroke-linejoin="round"
26+
></path>
27+
<path
28+
d="M32 3C36.5778 3 41.0906 4.08374 45.1692 6.16256C49.2477 8.24138 52.7762 11.2562 55.466 14.9605C58.1558 18.6647 59.9304 22.9531 60.6448 27.4748C61.3591 31.9965 60.9928 36.6232 59.5759 40.9762"
29+
stroke="currentColor"
30+
stroke-width="5"
31+
stroke-linecap="round"
32+
stroke-linejoin="round"
33+
class="text-lavender-blue-500"
34+
></path>
35+
</svg>
36+
37+
<p class="max-w-1/2 pb-6 text-center">{@render children()}</p>
38+
</div>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<script lang="ts">
2+
import {
3+
signIn,
4+
type SignProgress,
5+
type SignProgressFn,
6+
WebAuthnSignInProgressStep
7+
} from '@junobuild/core';
8+
import type { PasskeyProps } from '$lib/types/passkey';
9+
import Button from '$lib/components/Button.svelte';
10+
import Progress from '$lib/components/passkey/Progress.svelte';
11+
12+
let { progress: wizardProgress, onProgress: wizardOnProgress }: PasskeyProps = $props();
13+
14+
const progress = $derived(
15+
wizardProgress === undefined
16+
? undefined
17+
: 'signIn' in wizardProgress
18+
? wizardProgress.signIn
19+
: null
20+
);
21+
22+
const onProgress: SignProgressFn<WebAuthnSignInProgressStep> = (p) => {
23+
wizardOnProgress({ signIn: p });
24+
};
25+
26+
const doSignIn = async () => {
27+
try {
28+
await signIn({
29+
webauthn: { options: { onProgress } }
30+
});
31+
} catch (error: unknown) {
32+
wizardOnProgress(undefined);
33+
34+
throw error;
35+
}
36+
};
37+
</script>
38+
39+
{#if progress === undefined}
40+
<p class="pt-6">Already got one set-up?</p>
41+
42+
<Button onclick={doSignIn}>Use your passkey</Button>
43+
{:else if progress !== null}
44+
<Progress>
45+
{#if progress.step === WebAuthnSignInProgressStep.RequestingUserCredential}
46+
<span>Requesting user credential...</span>
47+
{:else if progress.step === WebAuthnSignInProgressStep.FinalizingCredential}
48+
<span>Finalizing credential...</span>
49+
{:else if progress.step === WebAuthnSignInProgressStep.Signing}
50+
<span>Signing request...</span>
51+
{:else if progress.step === WebAuthnSignInProgressStep.FinalizingSession}
52+
<span>Finalizing session...</span>
53+
{:else if progress.step === WebAuthnSignInProgressStep.RetrievingUser}
54+
<span>Loading user...</span>
55+
{/if}
56+
</Progress>
57+
{/if}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {
2+
WebAuthnSignInProgressStep,
3+
type SignProgress,
4+
WebAuthnSignUpProgressStep
5+
} from '@junobuild/core';
6+
7+
export type PasskeyProgress =
8+
| {
9+
signUp: SignProgress<WebAuthnSignUpProgressStep>;
10+
}
11+
| {
12+
signIn: SignProgress<WebAuthnSignInProgressStep>;
13+
}
14+
| { setup: null };
15+
16+
export interface PasskeyProps {
17+
progress: PasskeyProgress | undefined;
18+
onProgress: (progress: PasskeyProgress | undefined) => void;
19+
}

0 commit comments

Comments
 (0)