Skip to content

Commit 8f8ae36

Browse files
committed
fix: unify passkey-autofill with navigator polyfill setup
1 parent 639d494 commit 8f8ae36

File tree

7 files changed

+134
-95
lines changed

7 files changed

+134
-95
lines changed

app/_layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const provider = new ReactNativeProvider(
5454
}
5555
)
5656

57-
setupNavigatorPolyfill(provider)
57+
setupNavigatorPolyfill()
5858

5959
async function bootstrap() {
6060
try {

extensions/passkeys-keystore/extension.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,16 @@ export const WithPasskeysKeystore: Extension<PasskeysKeystoreExtension> = (
3939
const keyStore: Store<KeyStoreState> = options.keystore.store;
4040
const { autoPopulate = true } = options.passkeys.keystore ?? {};
4141

42+
const toUrlSafe = (id: string) => id.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
43+
4244
// Hook into passkey removal to also remove from keystore
4345
provider.passkey.store.hooks.before("remove", async ({ id }) => {
44-
const keyExists = (keyStore.state.keys as Key[]).some((k) => k.id === id);
45-
if (keyExists) {
46+
const foundKey = (keyStore.state.keys as Key[]).find((k) => toUrlSafe(k.id) === id);
47+
if (foundKey) {
4648
try {
47-
await provider.key.store.remove(id);
49+
await provider.key.store.remove(foundKey.id);
4850
} catch (error) {
49-
console.error(`Failed to remove key ${id} from keystore:`, error);
51+
console.error(`Failed to remove key ${foundKey.id} from keystore:`, error);
5052
}
5153
}
5254
});
@@ -61,14 +63,15 @@ export const WithPasskeysKeystore: Extension<PasskeysKeystoreExtension> = (
6163
throw new Error(`Key ${key.id} is missing public key`);
6264
}
6365
return {
64-
id: key.id,
66+
id: toUrlSafe(key.id),
6567
name: key.metadata.origin || "Unnamed Passkey",
6668
publicKey: key.publicKey,
6769
algorithm: key.algorithm || "ES256",
68-
createdAt: Date.now(),
70+
createdAt: key.metadata.createdAt || Date.now(),
6971
metadata: {
7072
...key.metadata,
7173
keyId: key.id,
74+
registered: key.metadata.registered ?? false,
7275
},
7376
};
7477
};
@@ -96,7 +99,13 @@ export const WithPasskeysKeystore: Extension<PasskeysKeystoreExtension> = (
9699
(existingKey) => !newKeys.some((newKey) => newKey.id === existingKey.id),
97100
);
98101

99-
if (addedKeys.length === 0 && removedKeys.length === 0) {
102+
// Find updated keys
103+
const updatedKeys = newKeys.filter((nk) => {
104+
const existing = keys.find(k => k.id === nk.id);
105+
return existing && JSON.stringify(existing.metadata) !== JSON.stringify(nk.metadata);
106+
});
107+
108+
if (addedKeys.length === 0 && removedKeys.length === 0 && updatedKeys.length === 0) {
100109
isProcessing = false;
101110

102111
return;
@@ -109,7 +118,7 @@ export const WithPasskeysKeystore: Extension<PasskeysKeystoreExtension> = (
109118
// Remove passkeys for removed keys
110119
removedKeys.forEach((k) => {
111120
if (k.type === "hd-derived-passkey" || k.type === "xhd-derived-p256" || k.type === "hd-derived-p256") {
112-
provider.passkey.store.removePasskey(k.id);
121+
provider.passkey.store.removePasskey(toUrlSafe(k.id));
113122
}
114123
});
115124

@@ -122,17 +131,29 @@ export const WithPasskeysKeystore: Extension<PasskeysKeystoreExtension> = (
122131
}
123132
}
124133

134+
// Refresh passkeys for updated keys
135+
for (const k of updatedKeys) {
136+
if (k.type === "hd-derived-passkey" || k.type === "xhd-derived-p256" || k.type === "hd-derived-p256") {
137+
provider.passkey.store.addPasskey(
138+
createPasskeyFromKey(k as XHDDomainP256KeyData),
139+
);
140+
}
141+
}
142+
125143
isProcessing = false;
126144

145+
if (nextKeys) {
146+
const k = nextKeys;
147+
nextKeys = null;
148+
processUpdates(k);
149+
}
127150
};
128151

129152
processUpdates(keyStore.state.keys as unknown as Key[]);
130153

131154
keyStore.subscribe((state) => {
132155
if (state.status !== 'ready' && state.status !== 'idle') return;
133-
setTimeout(() => {
134-
processUpdates(state.keys as unknown as Key[]);
135-
}, 0);
156+
processUpdates(state.keys as unknown as Key[]);
136157
});
137158
}
138159

extensions/passkeys/extension.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
addPasskey,
66
clearPasskeys,
77
getPasskey,
8+
getPasskeys,
89
removePasskey,
910
} from "./store";
1011
import type {
@@ -62,6 +63,11 @@ export const WithPasskeyStore: Extension<PasskeyStoreExtension> = (
6263
id,
6364
});
6465
},
66+
getPasskeys: async () => {
67+
return passkeyHooks("list", getPasskeys, {
68+
store: passkeyStore,
69+
});
70+
},
6571
clear: async () => {
6672
return passkeyHooks("clear", clearPasskeys, {
6773
store: passkeyStore,

extensions/passkeys/store.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,26 @@ export function getPasskey({
8181
return store.state.passkeys.find((passkey) => passkey.id === id);
8282
}
8383

84+
/**
85+
* Retrieves all passkeys from the store.
86+
*
87+
* @param params - The retrieval parameters.
88+
* @param params.store - The TanStack store instance for {@link PasskeyStoreState}.
89+
* @returns An array of all {@link Passkey}s.
90+
*
91+
* @example
92+
* ```typescript
93+
* getPasskeys({ store });
94+
* ```
95+
*/
96+
export function getPasskeys({
97+
store,
98+
}: {
99+
store: Store<PasskeyStoreState>;
100+
}): Passkey[] {
101+
return store.state.passkeys;
102+
}
103+
84104
/**
85105
* Clears all passkeys from the store.
86106
*

extensions/passkeys/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ export interface PasskeyStoreApi {
9494
* @returns The passkey if found, otherwise undefined.
9595
*/
9696
getPasskey: (id: string) => Promise<Passkey | undefined>;
97+
/**
98+
* Retrieves all passkeys from the store.
99+
*
100+
* @returns A promise that resolves to an array of all passkeys.
101+
*/
102+
getPasskeys: () => Promise<Passkey[]>;
97103
/**
98104
* Clears all passkeys from the store.
99105
*

hooks/useConnection.ts

Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ import {
1111
encodeCredential,
1212
} from "@algorandfoundation/liquid-client";
1313
import { encodeAddress } from "@algorandfoundation/keystore";
14+
import type { KeyData, KeyStoreState } from "@algorandfoundation/keystore";
15+
import { fetchSecret, getMasterKey, commit } from '@algorandfoundation/react-native-keystore';
16+
import { keyStore } from '@/stores/keystore';
17+
import { accountsStore } from '@/stores/accounts';
18+
import { passkeysStore } from '@/stores/passkeys';
1419
import { useProvider } from '@/hooks/useProvider';
1520
import { addMessage } from '@/stores/messages';
1621
import { sessionsStore, addSession, updateSessionStatus, updateSessionActivity, Session } from '@/stores/sessions';
@@ -29,7 +34,7 @@ interface UseConnectionResult {
2934

3035
export function useConnection(origin: string, requestId: string): UseConnectionResult {
3136
const router = useRouter();
32-
const { accounts, keys, key, passkeys, sessions } = useProvider();
37+
const { accounts, keys, key, passkey, sessions } = useProvider();
3338

3439
const [isConnected, setIsConnected] = useState(false);
3540
const [address, setAddress] = useState<string | null>(null);
@@ -46,18 +51,7 @@ export function useConnection(origin: string, requestId: string): UseConnectionR
4651
const dataChannelRef = useRef<RTCDataChannel | null>(null);
4752
const clientRef = useRef<SignalClient | null>(null);
4853
const lastUserActivityRef = useRef<number>(Date.now());
49-
50-
useEffect(() => {
51-
if (accounts.length > 0 && keys.length > 0 && !address) {
52-
let foundKey = keys.find((k) => k.id === accounts[0]?.metadata?.keyId);
53-
if (!foundKey && keys.length > 0) {
54-
foundKey = keys[0];
55-
}
56-
if (foundKey?.publicKey) {
57-
setAddress(encodeAddress(foundKey.publicKey));
58-
}
59-
}
60-
}, [accounts, keys, address]);
54+
const authFlowInProgressRef = useRef<boolean>(false);
6155

6256
const session = useStore(sessionsStore, (state) =>
6357
state.sessions.find(s => s.id === requestId && s.origin === origin)
@@ -130,13 +124,19 @@ export function useConnection(origin: string, requestId: string): UseConnectionR
130124
let active = true;
131125

132126
async function setupConnection() {
127+
const toUrlSafe = (id: string) => id.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
133128
if (!origin || !requestId) {
134129
console.error("Missing origin or requestId");
135130
setIsLoading(false);
136131
return;
137132
}
138133

139-
if (accounts.length === 0 || keys.length === 0) {
134+
if (authFlowInProgressRef.current) {
135+
console.log("Auth flow already in progress, skipping duplicate setup");
136+
return;
137+
}
138+
139+
if (accountsStore.state.accounts.length === 0 || keyStore.state.keys.length === 0) {
140140
console.log("Waiting for accounts and keys to load...");
141141
// If it's been loading for more than a few seconds, it might really be empty
142142
// but typically it's better to wait for them to be non-empty.
@@ -152,23 +152,27 @@ export function useConnection(origin: string, requestId: string): UseConnectionR
152152
setError(null);
153153

154154
try {
155-
const existingSession = sessions.find(s => s.id === requestId && s.origin === origin);
155+
const currentSessions = sessionsStore.state.sessions;
156+
const currentKeys = keyStore.state.keys;
157+
const currentAccounts = accountsStore.state.accounts;
158+
159+
const existingSession = currentSessions.find(s => s.id === requestId && s.origin === origin);
156160
if (!existingSession) {
157161
addSession({ id: requestId, origin, status: 'active', ttl: 60000 });
158162
} else if (existingSession.status !== 'active') {
159163
updateSessionStatus(requestId, origin, 'active');
160164
}
161165

162166
// Try to find the key associated with the first account, but fall back to the first available key
163-
let foundKey = keys.find((k) => k.id === accounts[0]?.metadata?.keyId);
164-
if (!foundKey && keys.length > 0) {
165-
foundKey = keys[0];
167+
let foundKey = currentKeys.find((k) => k.id === currentAccounts[0]?.metadata?.keyId);
168+
if (!foundKey && currentKeys.length > 0) {
169+
foundKey = currentKeys[0];
166170
console.log("Falling back to the first available key for attestation");
167171
}
168172

169173
if (!foundKey || !foundKey.publicKey) {
170-
console.error("No key found for attestation. Keys:", JSON.stringify(keys.map(k => ({id: k.id, type: k.type})), null, 2));
171-
console.error("Accounts:", JSON.stringify(accounts.map(a => ({address: a.address, keyId: a.metadata?.keyId})), null, 2));
174+
console.error("No key found for attestation. Keys:", JSON.stringify(currentKeys.map(k => ({id: k.id, type: k.type})), null, 2));
175+
console.error("Accounts:", JSON.stringify(currentAccounts.map(a => ({address: a.address, keyId: a.metadata?.keyId})), null, 2));
172176
throw new Error("No key found for attestation");
173177
}
174178

@@ -177,8 +181,10 @@ export function useConnection(origin: string, requestId: string): UseConnectionR
177181
const sessionCheck = await fetch(`${origin}/auth/session`);
178182
if (!active) return;
179183
console.log("Initial session status:", sessionCheck.ok);
184+
authFlowInProgressRef.current = true;
180185

181-
const relevantPasskeys = passkeys.filter(p => {
186+
const currentPasskeys = await passkey.store.getPasskeys();
187+
const relevantPasskeys = currentPasskeys.filter(p => {
182188
const storedOrigin = p.metadata?.origin;
183189
if (!storedOrigin) return false;
184190
try {
@@ -256,7 +262,7 @@ export function useConnection(origin: string, requestId: string): UseConnectionR
256262
}
257263

258264
if (!selectedAddress) {
259-
const matchedPasskey = relevantPasskeys.find(p => p.id === credential.id) || passkeys.find(p => p.id === credential.id);
265+
const matchedPasskey = relevantPasskeys.find(p => p.id === credential.id) || currentPasskeys.find(p => p.id === credential.id);
260266
const userHandle = matchedPasskey?.metadata?.userHandle;
261267
if (userHandle) {
262268
try {
@@ -280,7 +286,7 @@ export function useConnection(origin: string, requestId: string): UseConnectionR
280286

281287
// Re-sign the challenge if the address changed to match the selected passkey
282288
const selectedPublicKey = decodeAddress(selectedAddress);
283-
const selectedKey = keys.find(k =>
289+
const selectedKey = keyStore.state.keys.find(k =>
284290
k.publicKey && k.publicKey.length === selectedPublicKey.length &&
285291
k.publicKey.every((v, i) => v === selectedPublicKey[i])
286292
);
@@ -310,6 +316,24 @@ export function useConnection(origin: string, requestId: string): UseConnectionR
310316
if (!submitResponse.ok) {
311317
throw new Error(`Failed to submit assertion response: ${submitResponse.status} ${submitResponse.statusText}`);
312318
}
319+
320+
const currentPasskeys = await passkey.store.getPasskeys();
321+
const matchedPasskey = currentPasskeys.find(p => p.id === credential.id);
322+
const matchedKey = keyStore.state.keys.find(k => k.id === matchedPasskey?.metadata?.keyId) ||
323+
keyStore.state.keys.find((k) => toUrlSafe(k.id) === credential.id);
324+
325+
if (matchedKey) {
326+
try {
327+
const masterKey = await getMasterKey();
328+
const keyData = await fetchSecret<KeyData>({ keyId: matchedKey.id, masterKey });
329+
if (keyData) {
330+
keyData.metadata = { ...keyData.metadata, registered: true };
331+
await commit({ store: keyStore as any, keyData });
332+
}
333+
} catch (error) {
334+
console.error('Failed to update key metadata after assertion:', error);
335+
}
336+
}
313337
} else {
314338
console.log("No existing passkey for origin, using attestation");
315339

@@ -400,6 +424,24 @@ export function useConnection(origin: string, requestId: string): UseConnectionR
400424
if (!submitResponse.ok) {
401425
throw new Error(`Failed to submit attestation response: ${submitResponse.status} ${submitResponse.statusText}`);
402426
}
427+
428+
const currentPasskeys = await passkey.store.getPasskeys();
429+
const matchedPasskey = currentPasskeys.find(p => p.id === credential.id);
430+
const matchedKey = keyStore.state.keys.find(k => k.id === matchedPasskey?.metadata?.keyId) ||
431+
keyStore.state.keys.find((k) => toUrlSafe(k.id) === credential.id);
432+
433+
if (matchedKey) {
434+
try {
435+
const masterKey = await getMasterKey();
436+
const keyData = await fetchSecret<KeyData>({ keyId: matchedKey.id, masterKey });
437+
if (keyData) {
438+
keyData.metadata = { ...keyData.metadata, registered: true };
439+
await commit({ store: keyStore as any, keyData });
440+
}
441+
} catch (error) {
442+
console.error('Failed to update key metadata after attestation:', error);
443+
}
444+
}
403445
}
404446

405447
// Final validation of the session before connecting
@@ -517,6 +559,8 @@ export function useConnection(origin: string, requestId: string): UseConnectionR
517559
[{ text: "OK", onPress: () => router.back() }]
518560
);
519561
}
562+
} finally {
563+
authFlowInProgressRef.current = false;
520564
}
521565
}
522566

@@ -533,7 +577,7 @@ export function useConnection(origin: string, requestId: string): UseConnectionR
533577
clientRef.current = null;
534578
}
535579
};
536-
}, [origin, requestId, accounts, keys, key.store, router]);
580+
}, [origin, requestId, router, key, passkey, accounts.length > 0, keys.length > 0]);
537581

538582
return {
539583
session,

0 commit comments

Comments
 (0)