|
4 | 4 | import SolveCaptcha from "$lib/components/views/SolveCaptcha.svelte";
|
5 | 5 | import SetupOrUseExistingPasskey from "$lib/components/views/SetupOrUseExistingPasskey.svelte";
|
6 | 6 | import CreatePasskey from "$lib/components/views/CreatePasskey.svelte";
|
7 |
| - import { |
8 |
| - AuthenticationV2Events, |
9 |
| - authenticationV2Funnel, |
10 |
| - } from "$lib/utils/analytics/authenticationV2Funnel"; |
11 |
| - import { |
12 |
| - authenticateWithJWT, |
13 |
| - authenticateWithPasskey, |
14 |
| - authenticateWithSession, |
15 |
| - } from "$lib/utils/authentication"; |
16 |
| - import { canisterConfig, canisterId } from "$lib/globals"; |
17 |
| - import { |
18 |
| - authenticatedStore, |
19 |
| - authenticationStore, |
20 |
| - } from "$lib/stores/authentication.store"; |
21 |
| - import { lastUsedIdentitiesStore } from "$lib/stores/last-used-identities.store"; |
22 | 7 | import { handleError } from "$lib/components/utils/error";
|
23 |
| - import { DiscoverablePasskeyIdentity } from "$lib/utils/discoverablePasskeyIdentity"; |
24 |
| - import { inferPasskeyAlias, loadUAParser } from "$lib/flows/register"; |
25 |
| - import { passkeyAuthnMethodData } from "$lib/utils/authnMethodData"; |
26 |
| - import { isCanisterError, throwCanisterError } from "$lib/utils/utils"; |
27 | 8 | import { toaster } from "$lib/components/utils/toaster";
|
28 |
| - import type { |
29 |
| - CheckCaptchaError, |
30 |
| - IdRegFinishError, |
31 |
| - IdRegStartError, |
32 |
| - OpenIdDelegationError, |
33 |
| - } from "$lib/generated/internet_identity_types"; |
34 |
| - import { createGoogleRequestConfig, requestJWT } from "$lib/utils/openID"; |
35 |
| - import { sessionStore } from "$lib/stores/session.store"; |
36 | 9 | import SystemOverlayBackdrop from "$lib/components/utils/SystemOverlayBackdrop.svelte";
|
37 |
| - import { onMount } from "svelte"; |
38 |
| - import { features } from "$lib/legacy/features"; |
39 |
| - import { DiscoverableDummyIdentity } from "$lib/utils/discoverableDummyIdentity"; |
| 10 | + import { AuthFlow } from "$lib/flows/authFlow.svelte"; |
40 | 11 |
|
41 | 12 | interface Props {
|
42 |
| - onCancel: () => void; |
43 | 13 | onSuccess: (identityNumber: bigint) => void;
|
| 14 | + onCancel: () => void; |
44 | 15 | }
|
45 | 16 |
|
46 | 17 | const { onCancel, onSuccess }: Props = $props();
|
47 | 18 |
|
48 |
| - let view = $state< |
49 |
| - "chooseMethod" | "setupOrUseExistingPasskey" | "setupNewPasskey" |
50 |
| - >("chooseMethod"); |
51 |
| - let captcha = $state<{ |
52 |
| - image: string; |
53 |
| - attempt: number; |
54 |
| - solve: (solution: string) => void; |
55 |
| - }>(); |
56 |
| - let systemOverlay = $state(false); |
57 |
| -
|
58 |
| - const setupOrUseExistingPasskey = async () => { |
59 |
| - authenticationV2Funnel.trigger( |
60 |
| - AuthenticationV2Events.ContinueWithPasskeyScreen, |
61 |
| - ); |
62 |
| - view = "setupOrUseExistingPasskey"; |
63 |
| - }; |
64 |
| -
|
65 |
| - const continueWithExistingPasskey = async () => { |
66 |
| - try { |
67 |
| - authenticationV2Funnel.trigger(AuthenticationV2Events.UseExistingPasskey); |
68 |
| - const { identity, identityNumber, credentialId } = |
69 |
| - await authenticateWithPasskey({ |
70 |
| - canisterId, |
71 |
| - session: $sessionStore, |
72 |
| - }); |
73 |
| - authenticationStore.set({ identity, identityNumber }); |
74 |
| - const info = |
75 |
| - await $authenticatedStore.actor.get_anchor_info(identityNumber); |
76 |
| - lastUsedIdentitiesStore.addLastUsedIdentity({ |
77 |
| - identityNumber, |
78 |
| - name: info.name[0], |
79 |
| - authMethod: { passkey: { credentialId } }, |
80 |
| - }); |
81 |
| - onSuccess(identityNumber); |
82 |
| - } catch (error) { |
83 |
| - handleError(error); |
84 |
| - onCancel(); |
85 |
| - } |
86 |
| - }; |
87 |
| -
|
88 |
| - const setupNewPasskey = () => { |
89 |
| - authenticationV2Funnel.trigger(AuthenticationV2Events.EnterNameScreen); |
90 |
| - view = "setupNewPasskey"; |
91 |
| - }; |
92 |
| -
|
93 |
| - const createPasskey = async (name: string) => { |
94 |
| - authenticationV2Funnel.trigger( |
95 |
| - AuthenticationV2Events.StartWebauthnCreation, |
96 |
| - ); |
97 |
| - try { |
98 |
| - const passkeyIdentity = |
99 |
| - features.DUMMY_AUTH || nonNullish(canisterConfig.dummy_auth[0]?.[0]) |
100 |
| - ? await DiscoverableDummyIdentity.createNew(name) |
101 |
| - : await DiscoverablePasskeyIdentity.createNew(name); |
102 |
| - await startRegistration(); |
103 |
| - await registerWithPasskey(passkeyIdentity); |
104 |
| - } catch (error) { |
105 |
| - handleError(error); |
106 |
| - onCancel(); |
107 |
| - } |
108 |
| - }; |
109 |
| -
|
110 |
| - const registerWithPasskey = async ( |
111 |
| - passkeyIdentity: DiscoverablePasskeyIdentity, |
112 |
| - attempts = 0, |
113 |
| - ) => { |
114 |
| - authenticationV2Funnel.trigger(AuthenticationV2Events.RegisterWithPasskey); |
115 |
| - const uaParser = loadUAParser(); |
116 |
| - const alias = await inferPasskeyAlias({ |
117 |
| - authenticatorType: passkeyIdentity.getAuthenticatorAttachment(), |
118 |
| - userAgent: navigator.userAgent, |
119 |
| - uaParser, |
120 |
| - aaguid: passkeyIdentity.getAaguid(), |
121 |
| - }); |
122 |
| - const authnMethod = passkeyAuthnMethodData({ |
123 |
| - alias, |
124 |
| - pubKey: passkeyIdentity.getPublicKey().toDer(), |
125 |
| - credentialId: passkeyIdentity.getCredentialId()!, |
126 |
| - authenticatorAttachment: passkeyIdentity.getAuthenticatorAttachment(), |
127 |
| - origin: window.location.origin, |
128 |
| - }); |
129 |
| - const name = passkeyIdentity.getName(); |
130 |
| - try { |
131 |
| - const { identity_number: identityNumber } = await $sessionStore.actor |
132 |
| - .identity_registration_finish({ |
133 |
| - name: nonNullish(name) ? [name] : [], |
134 |
| - authn_method: authnMethod, |
135 |
| - }) |
136 |
| - .then(throwCanisterError); |
137 |
| - authenticationV2Funnel.trigger( |
138 |
| - AuthenticationV2Events.SuccessfulPasskeyRegistration, |
139 |
| - ); |
140 |
| - const credentialId = new Uint8Array(passkeyIdentity.getCredentialId()!); |
141 |
| - const identity = await authenticateWithSession({ |
142 |
| - session: $sessionStore, |
143 |
| - }); |
144 |
| - authenticationStore.set({ identity, identityNumber }); |
145 |
| - lastUsedIdentitiesStore.addLastUsedIdentity({ |
146 |
| - identityNumber, |
147 |
| - name: passkeyIdentity.getName(), |
148 |
| - authMethod: { passkey: { credentialId } }, |
149 |
| - }); |
| 19 | + const authFlow = new AuthFlow({ |
| 20 | + onSignIn: onSuccess, |
| 21 | + onSignUp: (identityNumber) => { |
150 | 22 | toaster.success({
|
151 | 23 | title: "You're all set. Your identity has been created.",
|
152 | 24 | duration: 4000,
|
153 | 25 | closable: false,
|
154 | 26 | });
|
155 | 27 | onSuccess(identityNumber);
|
156 |
| - } catch (error) { |
157 |
| - if (isCanisterError<IdRegFinishError>(error)) { |
158 |
| - switch (error.type) { |
159 |
| - case "UnexpectedCall": |
160 |
| - const nextStep = error.value(error.type).next_step; |
161 |
| - if ("CheckCaptcha" in nextStep) { |
162 |
| - if (attempts < 3) { |
163 |
| - await solveCaptcha( |
164 |
| - `data:image/png;base64,${nextStep.CheckCaptcha.captcha_png_base64}`, |
165 |
| - ); |
166 |
| - return registerWithPasskey(passkeyIdentity, attempts + 1); |
167 |
| - } |
168 |
| - } |
169 |
| - break; |
170 |
| - case "NoRegistrationFlow": |
171 |
| - if (attempts < 3) { |
172 |
| - // Apparently the flow has been cleaned up, try again. |
173 |
| - await startRegistration(); |
174 |
| - return await registerWithPasskey(passkeyIdentity, attempts + 1); |
175 |
| - } |
176 |
| - break; |
177 |
| - } |
178 |
| - } |
| 28 | + }, |
| 29 | + onError: (error) => { |
179 | 30 | handleError(error);
|
180 | 31 | onCancel();
|
181 |
| - } |
182 |
| - }; |
183 |
| -
|
184 |
| - const continueWithGoogle = async () => { |
185 |
| - let jwt: string | undefined; |
186 |
| - try { |
187 |
| - authenticationV2Funnel.trigger(AuthenticationV2Events.ContinueWithGoogle); |
188 |
| - const clientId = canisterConfig.openid_google?.[0]?.[0]?.client_id!; |
189 |
| - const requestConfig = createGoogleRequestConfig(clientId); |
190 |
| - systemOverlay = true; |
191 |
| - jwt = await requestJWT(requestConfig, { |
192 |
| - nonce: $sessionStore.nonce, |
193 |
| - mediation: "required", |
194 |
| - }); |
195 |
| - systemOverlay = false; |
196 |
| - const { identity, identityNumber, iss, sub } = await authenticateWithJWT({ |
197 |
| - canisterId, |
198 |
| - session: $sessionStore, |
199 |
| - jwt, |
200 |
| - }); |
201 |
| - // If the previous call succeeds, it means the Google user already exists in II. |
202 |
| - // Therefore, they are logging in. |
203 |
| - // If the call fails, it means the Google user does not exist in II. |
204 |
| - // In that case, we register them. |
205 |
| - authenticationV2Funnel.trigger(AuthenticationV2Events.LoginWithGoogle); |
206 |
| - authenticationStore.set({ identity, identityNumber }); |
207 |
| - const info = |
208 |
| - await $authenticatedStore.actor.get_anchor_info(identityNumber); |
209 |
| - lastUsedIdentitiesStore.addLastUsedIdentity({ |
210 |
| - identityNumber, |
211 |
| - name: info.name[0], |
212 |
| - authMethod: { openid: { iss, sub } }, |
213 |
| - }); |
214 |
| - onSuccess(identityNumber); |
215 |
| - } catch (error) { |
216 |
| - systemOverlay = false; |
217 |
| - if ( |
218 |
| - isCanisterError<OpenIdDelegationError>(error) && |
219 |
| - error.type === "NoSuchAnchor" && |
220 |
| - nonNullish(jwt) |
221 |
| - ) { |
222 |
| - authenticationV2Funnel.trigger( |
223 |
| - AuthenticationV2Events.RegisterWithGoogle, |
224 |
| - ); |
225 |
| - await startRegistration(); |
226 |
| - return registerWithGoogle(jwt); |
227 |
| - } |
228 |
| - handleError(error); |
229 |
| - onCancel(); |
230 |
| - } |
231 |
| - }; |
232 |
| -
|
233 |
| - const startRegistration = async (): Promise<void> => { |
234 |
| - try { |
235 |
| - const { next_step } = await $sessionStore.actor |
236 |
| - .identity_registration_start() |
237 |
| - .then(throwCanisterError); |
238 |
| - if ("CheckCaptcha" in next_step) { |
239 |
| - await solveCaptcha( |
240 |
| - `data:image/png;base64,${next_step.CheckCaptcha.captcha_png_base64}`, |
241 |
| - ); |
242 |
| - } |
243 |
| - } catch (error) { |
244 |
| - if ( |
245 |
| - isCanisterError<IdRegStartError>(error) && |
246 |
| - error.type === "AlreadyInProgress" |
247 |
| - ) { |
248 |
| - // Ignore since it means we can continue with an existing registration |
249 |
| - return; |
250 |
| - } |
251 |
| - handleError(error); |
252 |
| - onCancel(); |
253 |
| - } |
254 |
| - }; |
255 |
| -
|
256 |
| - const solveCaptcha = async (image: string, attempt = 0): Promise<void> => |
257 |
| - new Promise((resolve) => { |
258 |
| - captcha = { |
259 |
| - image, |
260 |
| - attempt, |
261 |
| - solve: async (solution) => { |
262 |
| - try { |
263 |
| - await $sessionStore.actor |
264 |
| - .check_captcha({ solution }) |
265 |
| - .then(throwCanisterError); |
266 |
| - resolve(); |
267 |
| - } catch (error) { |
268 |
| - if ( |
269 |
| - isCanisterError<CheckCaptchaError>(error) && |
270 |
| - error.type === "WrongSolution" |
271 |
| - ) { |
272 |
| - const nextImage = `data:image/png;base64,${error.value(error.type).new_captcha_png_base64}`; |
273 |
| - await solveCaptcha(nextImage, attempt + 1); |
274 |
| - resolve(); |
275 |
| - return; |
276 |
| - } |
277 |
| - handleError(error); |
278 |
| - onCancel(); |
279 |
| - } |
280 |
| - }, |
281 |
| - }; |
282 |
| - }); |
283 |
| -
|
284 |
| - const registerWithGoogle = async (jwt: string) => { |
285 |
| - try { |
286 |
| - await $sessionStore.actor |
287 |
| - .openid_identity_registration_finish({ |
288 |
| - jwt, |
289 |
| - salt: $sessionStore.salt, |
290 |
| - }) |
291 |
| - .then(throwCanisterError); |
292 |
| - const { identity, identityNumber, iss, sub } = await authenticateWithJWT({ |
293 |
| - canisterId, |
294 |
| - session: $sessionStore, |
295 |
| - jwt, |
296 |
| - }); |
297 |
| - authenticationV2Funnel.trigger( |
298 |
| - AuthenticationV2Events.SuccessfulGoogleRegistration, |
299 |
| - ); |
300 |
| - authenticationStore.set({ identity, identityNumber }); |
301 |
| - const info = |
302 |
| - await $authenticatedStore.actor.get_anchor_info(identityNumber); |
303 |
| - lastUsedIdentitiesStore.addLastUsedIdentity({ |
304 |
| - identityNumber, |
305 |
| - name: info.name[0], |
306 |
| - authMethod: { openid: { iss, sub } }, |
307 |
| - }); |
308 |
| - toaster.success({ |
309 |
| - title: "You're all set. Your identity has been created.", |
310 |
| - duration: 4000, |
311 |
| - closable: false, |
312 |
| - }); |
313 |
| - onSuccess(identityNumber); |
314 |
| - } catch (error) { |
315 |
| - if ( |
316 |
| - isCanisterError<IdRegFinishError>(error) && |
317 |
| - error.type === "UnexpectedCall" |
318 |
| - ) { |
319 |
| - const nextStep = error.value(error.type).next_step; |
320 |
| - if ("CheckCaptcha" in nextStep) { |
321 |
| - await solveCaptcha( |
322 |
| - `data:image/png;base64,${nextStep.CheckCaptcha.captcha_png_base64}`, |
323 |
| - ); |
324 |
| - return registerWithGoogle(jwt); |
325 |
| - } |
326 |
| - } |
327 |
| - handleError(error); |
328 |
| - onCancel(); |
329 |
| - } |
330 |
| - }; |
331 |
| -
|
332 |
| - onMount(() => { |
333 |
| - authenticationV2Funnel.trigger(AuthenticationV2Events.SelectMethodScreen); |
| 32 | + }, |
334 | 33 | });
|
335 | 34 | </script>
|
336 | 35 |
|
337 |
| -{#if nonNullish(captcha)} |
338 |
| - <SolveCaptcha {...captcha} /> |
339 |
| -{:else if view === "chooseMethod"} |
| 36 | +{#if nonNullish(authFlow.captcha)} |
| 37 | + <SolveCaptcha {...authFlow.captcha} /> |
| 38 | +{:else if authFlow.view === "chooseMethod"} |
340 | 39 | <h1 class="text-text-primary mb-2 text-2xl font-medium">
|
341 | 40 | Use another identity
|
342 | 41 | </h1>
|
343 | 42 | <p class="text-text-secondary mb-6 self-start text-sm">Choose method</p>
|
344 |
| - <PickAuthenticationMethod {setupOrUseExistingPasskey} {continueWithGoogle} /> |
345 |
| -{:else if view === "setupOrUseExistingPasskey"} |
| 43 | + <PickAuthenticationMethod |
| 44 | + setupOrUseExistingPasskey={authFlow.setupOrUseExistingPasskey} |
| 45 | + continueWithGoogle={authFlow.continueWithGoogle} |
| 46 | + /> |
| 47 | +{:else if authFlow.view === "setupOrUseExistingPasskey"} |
346 | 48 | <SetupOrUseExistingPasskey
|
347 |
| - setupNew={setupNewPasskey} |
348 |
| - useExisting={continueWithExistingPasskey} |
| 49 | + setupNew={authFlow.setupNewPasskey} |
| 50 | + useExisting={authFlow.continueWithExistingPasskey} |
349 | 51 | />
|
350 |
| -{:else if view === "setupNewPasskey"} |
351 |
| - <CreatePasskey create={createPasskey} /> |
| 52 | +{:else if authFlow.view === "setupNewPasskey"} |
| 53 | + <CreatePasskey create={authFlow.createPasskey} /> |
352 | 54 | {/if}
|
353 | 55 |
|
354 |
| -{#if systemOverlay} |
| 56 | +{#if authFlow.systemOverlay} |
355 | 57 | <SystemOverlayBackdrop />
|
356 | 58 | {/if}
|
0 commit comments