Skip to content

Commit c837ba2

Browse files
committed
🥅 app: handle localized native auth errors
1 parent 2559ad3 commit c837ba2

3 files changed

Lines changed: 72 additions & 54 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@exactly/mobile": patch
3+
---
4+
5+
🥅 handle localized native auth errors by matching expo codes

‎src/components/add-funds/Bridge.tsx‎

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ import View from "../shared/View";
5757
import TokenInput from "../swaps/TokenInput";
5858

5959
import type { Chain, Token } from "@lifi/sdk";
60-
import type { TFunction } from "i18next";
6160

6261
export default function Bridge() {
6362
const router = useRouter();
@@ -375,41 +374,41 @@ export default function Bridge() {
375374
await switchChain(senderConfig, { chainId: source.chain });
376375
}
377376
setBridgeStatus(t("Submitting bridge transaction..."));
378-
let id: string | undefined;
379377
try {
380-
const result = await sendCallsTx({
381-
chainId: source.chain,
382-
calls: [
383-
...(approval ? [{ to: getAddress(source.address), data: approval }] : []),
384-
{ to: from.to, data: from.data, value: from.value },
385-
],
386-
});
387-
id = result.id;
388-
} catch (error) {
389-
if (isExaSender) throw error;
390-
if (classifyError(error).authKnown) throw error;
391-
reportError(error, {
392-
level: "warning",
393-
extra: error instanceof Error ? { cause: error.cause } : undefined,
394-
});
395-
await switchChain(senderConfig, { chainId: source.chain });
378+
let id: string | undefined;
396379
try {
380+
const result = await sendCallsTx({
381+
chainId: source.chain,
382+
calls: [
383+
...(approval ? [{ to: getAddress(source.address), data: approval }] : []),
384+
{ to: from.to, data: from.data, value: from.value },
385+
],
386+
});
387+
id = result.id;
388+
} catch (error) {
389+
if (isExaSender) throw error;
390+
if (classifyError(error).authKnown) throw error;
391+
reportError(error, {
392+
level: "warning",
393+
extra: error instanceof Error ? { cause: error.cause } : undefined,
394+
});
395+
await switchChain(senderConfig, { chainId: source.chain });
397396
if (approval) {
398397
const hash = await sendTx({ chainId: source.chain, to: getAddress(source.address), data: approval });
399398
await waitForTransactionReceipt(senderConfig, { hash, chainId: source.chain });
400399
}
401400
const hash = await sendTx({ chainId: source.chain, to: from.to, data: from.data, value: from.value });
402401
await waitForTransactionReceipt(senderConfig, { hash, chainId: source.chain });
403-
} finally {
404-
await switchChain(senderConfig, { chainId: chain.id }).catch(reportError);
402+
setBridgeStatus(t("Bridge transaction submitted"));
403+
return;
405404
}
405+
if (!id) throw new Error("missing sendCalls id");
406+
const { status } = await waitForCallsStatus(senderConfig, { id });
407+
if (status === "failure") throw new Error("failed to submit bridge transaction");
406408
setBridgeStatus(t("Bridge transaction submitted"));
407-
return;
409+
} finally {
410+
if (!isExaSender) await switchChain(senderConfig, { chainId: chain.id }).catch(reportError);
408411
}
409-
if (!id) throw new Error("missing sendCalls id");
410-
const { status } = await waitForCallsStatus(senderConfig, { id });
411-
if (status === "failure") throw new Error("failed to submit bridge transaction");
412-
setBridgeStatus(t("Bridge transaction submitted"));
413412
},
414413
onSuccess: async () => {
415414
toast.show(t("Bridge transaction submitted"), {
@@ -419,8 +418,17 @@ export default function Bridge() {
419418
});
420419
await queryClient.invalidateQueries({ queryKey: ["bridge", "sources"] });
421420
},
422-
onError: (error: unknown) => {
423-
handleError(error, toast, t);
421+
onError(error) {
422+
if (classifyError(error).authKnown) {
423+
setBridgePreview(undefined);
424+
resetBridgeMutation();
425+
return;
426+
}
427+
toast.show(t("Bridge failed. Please try again."), {
428+
native: true,
429+
duration: 1000,
430+
burntOptions: { haptic: "error", preset: "error" },
431+
});
424432
},
425433
onSettled: () => {
426434
setBridgeStatus(undefined);
@@ -464,8 +472,17 @@ export default function Bridge() {
464472
});
465473
await queryClient.invalidateQueries({ queryKey: ["bridge", "sources"] });
466474
},
467-
onError: (error: unknown) => {
468-
handleError(error, toast, t, true);
475+
onError(error) {
476+
if (classifyError(error).authKnown) {
477+
setBridgePreview(undefined);
478+
resetTransferMutation();
479+
return;
480+
}
481+
toast.show(t("Transfer failed. Please try again."), {
482+
native: true,
483+
duration: 1000,
484+
burntOptions: { haptic: "error", preset: "error" },
485+
});
469486
},
470487
onSettled: () => {
471488
setBridgeStatus(undefined);
@@ -1041,12 +1058,3 @@ export default function Bridge() {
10411058
</SafeView>
10421059
);
10431060
}
1044-
1045-
function handleError(error: unknown, toast: ReturnType<typeof useToastController>, t: TFunction, isTransfer?: boolean) {
1046-
if (!reportError(error).authKnown)
1047-
toast.show(isTransfer ? t("Transfer failed. Please try again.") : t("Bridge failed. Please try again."), {
1048-
native: true,
1049-
duration: 1000,
1050-
burntOptions: { haptic: "error", preset: "error" },
1051-
});
1052-
}

‎src/utils/reportError.ts‎

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,20 @@ export default function reportError(error: unknown, hint?: Parameters<typeof cap
4444
return classification;
4545
}
4646

47-
const passkeyCancelledMessages = new Set([
48-
"The operation couldn’t be completed. (com.apple.AuthenticationServices.AuthorizationError error 1001.)",
49-
"The operation couldn’t be completed. (com.apple.AuthenticationServices.AuthorizationError error 1004.)",
50-
"The operation couldn’t be completed. Device must be unlocked to perform request.",
51-
"UserCancelled",
52-
]);
53-
const passkeyKnownMessages = new Set([
54-
...passkeyCancelledMessages,
55-
"The operation couldn’t be completed. Stolen Device Protection is enabled and biometry is required.",
56-
]);
47+
const passkeyCancelledCodes = new Set(["ERR_USER_CANCELLED", "ERR_PASSKEY_REQUEST_FAILED"]);
48+
const passkeyCancelledPatterns = [
49+
/com\.apple\.AuthenticationServices\.AuthorizationError\D+\b1001\b/i,
50+
/com\.apple\.AuthenticationServices\.AuthorizationError\D+\b1004\b/i,
51+
/^UserCancelled$/,
52+
];
53+
const passkeyKnownCodes = new Set([...passkeyCancelledCodes, "ERR_PENDING_PASSKEY_REQUEST"]);
54+
const passkeyKnownPatterns = [
55+
...passkeyCancelledPatterns,
56+
/com\.apple\.AuthenticationServices\.AuthorizationError/i,
57+
/Biometrics must be enabled/,
58+
/Device must be unlocked/,
59+
/There is already a pending passkey request/,
60+
];
5761
const authPrefixes = ["androidx.credentials.exceptions.domerrors.NotAllowedError"];
5862
const networkTypes = [
5963
["ConnectionLost", /network connection was lost/i],
@@ -109,13 +113,14 @@ function parseError(error: unknown) {
109113
function classify({ code, message, name, reason, revert, status }: ParsedError) {
110114
const passkeyNotAllowed =
111115
name === "NotAllowedError" || (message !== undefined && authPrefixes.some((prefix) => message.startsWith(prefix)));
112-
const passkeyCancelled = message !== undefined && passkeyCancelledMessages.has(message);
116+
const passkeyCancelled =
117+
(code !== undefined && passkeyCancelledCodes.has(code)) ||
118+
(message !== undefined && passkeyCancelledPatterns.some((pattern) => pattern.test(message)));
113119
const passkeyKnown =
114-
message !== undefined &&
115-
(passkeyKnownMessages.has(message) ||
116-
message.includes("Biometrics must be enabled") ||
117-
message.includes("There is already a pending passkey request") ||
118-
authPrefixes.some((prefix) => message.startsWith(prefix)));
120+
(code !== undefined && passkeyKnownCodes.has(code)) ||
121+
(message !== undefined &&
122+
(passkeyKnownPatterns.some((pattern) => pattern.test(message)) ||
123+
authPrefixes.some((prefix) => message.startsWith(prefix))));
119124
const passkeyWarning = passkeyKnown && !passkeyCancelled && !passkeyNotAllowed;
120125
const biometric = code === "ERR_BIOMETRIC";
121126
const walletRejected = status === "4001" || status === "5000";

0 commit comments

Comments
 (0)