Skip to content

Commit 8f03326

Browse files
committedApr 17, 2025
ContinueAs working
1 parent 11f7d3e commit 8f03326

File tree

4 files changed

+133
-14
lines changed

4 files changed

+133
-14
lines changed
 

‎src/frontend/src/lib/stores/writable.store.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { browser } from "$app/environment";
2-
import { jsonReplacer, jsonReviver, nonNullish } from "@dfinity/utils";
2+
import { nonNullish } from "@dfinity/utils";
3+
import { jsonReplacer, jsonReviver } from "$lib/utils/json.utils";
34
import { writable, type Unsubscriber, type Writable } from "svelte/store";
45
import { type StoreLocalStorageKey } from "$lib/constants/store.constants";
56

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { Principal } from "@dfinity/principal";
2+
import { nonNullish } from "@dfinity/utils";
3+
4+
const JSON_KEY_BIGINT = "__bigint__";
5+
const JSON_KEY_PRINCIPAL = "__principal__";
6+
const JSON_KEY_UINT8ARRAY = "__uint8array__";
7+
const JSON_KEY_ARRAYBUFFER = "__arraybuffer__";
8+
9+
// Helper function to convert ArrayBuffer to Base64
10+
const arrayBufferToBase64 = (buffer: ArrayBuffer): string => {
11+
let binary = "";
12+
const bytes = new Uint8Array(buffer);
13+
const len = bytes.byteLength;
14+
for (let i = 0; i < len; i++) {
15+
binary += String.fromCharCode(bytes[i]);
16+
}
17+
return btoa(binary);
18+
};
19+
20+
// Helper function to convert Base64 to ArrayBuffer
21+
const base64ToArrayBuffer = (base64: string): ArrayBuffer => {
22+
const binary_string = atob(base64);
23+
const len = binary_string.length;
24+
const bytes = new Uint8Array(len);
25+
for (let i = 0; i < len; i++) {
26+
bytes[i] = binary_string.charCodeAt(i);
27+
}
28+
return bytes.buffer;
29+
};
30+
31+
/**
32+
* A custom replacer for `JSON.stringify` that converts specific types not natively supported
33+
* by the API into JSON-compatible formats.
34+
*
35+
* Supported conversions:
36+
* - `BigInt` → `{ "__bigint__": string }`
37+
* - `Principal` → `{ "__principal__": string }`
38+
* - `Uint8Array` → `{ "__uint8array__": number[] }`
39+
* - `ArrayBuffer` → `{ "__arraybuffer__": string }` (base64 encoded)
40+
*
41+
* @param {string} _key - Ignored. Only provided for API compatibility.
42+
* @param {unknown} value - The value to transform before stringification.
43+
* @returns {unknown} The transformed value if it matches a known type, otherwise the original value.
44+
*/
45+
export const jsonReplacer = (_key: string, value: unknown): unknown => {
46+
if (typeof value === "bigint") {
47+
return { [JSON_KEY_BIGINT]: `${value}` };
48+
}
49+
50+
if (nonNullish(value) && value instanceof Principal) {
51+
return { [JSON_KEY_PRINCIPAL]: value.toText() };
52+
}
53+
54+
if (nonNullish(value) && value instanceof Uint8Array) {
55+
return { [JSON_KEY_UINT8ARRAY]: Array.from(value) };
56+
}
57+
58+
if (nonNullish(value) && value instanceof ArrayBuffer) {
59+
return { [JSON_KEY_ARRAYBUFFER]: arrayBufferToBase64(value) };
60+
}
61+
62+
return value;
63+
};
64+
65+
/**
66+
* A custom reviver for `JSON.parse` that reconstructs specific types from their JSON-encoded representations.
67+
*
68+
* This reverses the transformations applied by `jsonReplacer`, restoring the original types.
69+
*
70+
* Supported conversions:
71+
* - `{ "__bigint__": string }` → `BigInt`
72+
* - `{ "__principal__": string }` → `Principal`
73+
* - `{ "__uint8array__": number[] }` → `Uint8Array`
74+
* - `{ "__arraybuffer__": string }` → `ArrayBuffer` (from base64)
75+
*
76+
* @param {string} _key - Ignored but provided for API compatibility.
77+
* @param {unknown} value - The parsed value to transform.
78+
* @returns {unknown} The reconstructed value if it matches a known type, otherwise the original value.
79+
*/
80+
export const jsonReviver = (_key: string, value: unknown): unknown => {
81+
const mapValue = <T>(key: string): T => (value as Record<string, T>)[key];
82+
83+
if (
84+
nonNullish(value) &&
85+
typeof value === "object" &&
86+
JSON_KEY_BIGINT in value
87+
) {
88+
return BigInt(mapValue(JSON_KEY_BIGINT));
89+
}
90+
91+
if (
92+
nonNullish(value) &&
93+
typeof value === "object" &&
94+
JSON_KEY_PRINCIPAL in value
95+
) {
96+
return Principal.fromText(mapValue(JSON_KEY_PRINCIPAL));
97+
}
98+
99+
if (
100+
nonNullish(value) &&
101+
typeof value === "object" &&
102+
JSON_KEY_UINT8ARRAY in value
103+
) {
104+
return Uint8Array.from(mapValue(JSON_KEY_UINT8ARRAY));
105+
}
106+
107+
if (
108+
nonNullish(value) &&
109+
typeof value === "object" &&
110+
JSON_KEY_ARRAYBUFFER in value
111+
) {
112+
return base64ToArrayBuffer(mapValue(JSON_KEY_ARRAYBUFFER));
113+
}
114+
115+
return value;
116+
};

‎src/frontend/src/routes/(new-styling)/new-authorize/+page.svelte

+14-12
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
5353
let onAuthenticate: (
5454
authenticatedConnection: AuthenticatedConnection,
55+
credentialId: ArrayBuffer | undefined,
5556
) => void;
5657
const connection = new Connection(readCanisterId(), readCanisterConfig());
5758
@@ -90,16 +91,19 @@
9091
() => userNumber,
9192
passkeyIdentity,
9293
);
93-
onAuthenticate(result.connection);
94+
onAuthenticate(result.connection, passkeyIdentity.getCredentialId());
9495
} catch {
9596
// If error or cancelled, go back to method selection
9697
pickAuthenticationMethod();
9798
}
9899
};
99100
100-
const authenticateWithPasskey = async (credentialId: ArrayBuffer) => {
101+
const authenticateWithPasskey = async (credentialId: ArrayBuffer | undefined) => {
101102
currentState = { state: "loading" };
102103
try {
104+
if (!credentialId) {
105+
throw new Error("Credential ID is required");
106+
}
103107
const lookupResult = await connection.lookupDeviceKey(
104108
new Uint8Array(credentialId),
105109
);
@@ -119,9 +123,10 @@
119123
[credentialData]
120124
);
121125
if (result.kind === "loginSuccess") {
122-
onAuthenticate(result.connection);
126+
onAuthenticate(result.connection, credentialId);
127+
} else {
128+
throw new Error("Failed to login");
123129
}
124-
throw new Error("Failed to login");
125130
} catch {
126131
// If error or cancelled, go back to method selection
127132
pickAuthenticationMethod();
@@ -182,7 +187,7 @@
182187
() => identity_number,
183188
data.session.identity,
184189
);
185-
onAuthenticate(result.connection);
190+
onAuthenticate(result.connection, passkey.getCredentialId());
186191
} catch (error) {
187192
if (
188193
isCanisterError<IdRegFinishError>(error) &&
@@ -224,7 +229,7 @@
224229
anchorNumber,
225230
identity,
226231
);
227-
onAuthenticate(result.connection);
232+
onAuthenticate(result.connection, undefined);
228233
} catch (error) {
229234
if (
230235
isCanisterError<OpenIdDelegationError>(error) &&
@@ -317,7 +322,7 @@
317322
anchorNumber,
318323
identity,
319324
);
320-
onAuthenticate(result.connection);
325+
onAuthenticate(result.connection, undefined);
321326
} catch (error) {
322327
if (
323328
isCanisterError<IdRegFinishError>(error) &&
@@ -341,7 +346,7 @@
341346
authenticate: (context) => {
342347
authContext = context;
343348
return new Promise((resolve) => {
344-
onAuthenticate = async (authenticatedConnection) => {
349+
onAuthenticate = async (authenticatedConnection, credentialId: ArrayBuffer | undefined) => {
345350
const derivationOrigin =
346351
context.authRequest.derivationOrigin ?? context.requestOrigin;
347352
const [result, anchorInfo] = await Promise.all([
@@ -356,10 +361,6 @@
356361
if ("error" in result) {
357362
return;
358363
}
359-
let credentialId: ArrayBuffer | undefined;
360-
if (authenticatedConnection.identity instanceof DiscoverablePasskeyIdentity || authenticatedConnection.identity instanceof MultiWebAuthnIdentity || authenticatedConnection.identity instanceof WebAuthnIdentity) {
361-
credentialId = authenticatedConnection.identity.getCredentialId();
362-
}
363364
const [userKey, parsed_signed_delegation] = result;
364365
lastUsedIdentitiesStore.addLatestUsed({
365366
identityNumber: authenticatedConnection.userNumber,
@@ -378,6 +379,7 @@
378379
state: "continueAs",
379380
number: data.lastUsedIdentity.identityNumber,
380381
name: data.lastUsedIdentity.name,
382+
credentialId: data.lastUsedIdentity.credentialId,
381383
continue: authenticateWithPasskey,
382384
useAnother: pickAuthenticationMethod,
383385
}

‎src/frontend/src/routes/(new-styling)/new-authorize/components/ContinueAs.svelte

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
</script>
99

1010
<div class="flex flex-col items-stretch gap-4">
11-
<Button onclick={() => continueFn(credentialId)} class="px-6 py-4 text-left" variant="primary"
11+
<Button onclick={() => continueFn($state.snapshot(credentialId))} class="px-6 py-4 text-left" variant="primary"
1212
>Continue as {name ?? number}</Button
1313
>
1414
<Button onclick={useAnother} class="px-6 py-4 text-left" variant="dashed"

0 commit comments

Comments
 (0)