@@ -2,9 +2,113 @@ import { captureException } from "@sentry/react-native";
22
33export default function reportError ( error : unknown , hint ?: Parameters < typeof captureException > [ 1 ] ) {
44 console . error ( error ) ; // eslint-disable-line no-console
5+ const classification = classify ( parseError ( error ) ) ;
6+ if ( classification . expected ) return ;
57 try {
6- return captureException ( error , hint ) ;
8+ if ( hint ) return captureException ( error , hint ) ;
9+ return captureException (
10+ error ,
11+ classification . fingerprint ? { fingerprint : classification . fingerprint } : undefined ,
12+ ) ;
713 } catch ( sentryError ) {
814 console . error ( sentryError ) ; // eslint-disable-line no-console
915 }
1016}
17+
18+ const passkeyCancelledMessages = new Set ( [
19+ "The operation couldn’t be completed. (com.apple.AuthenticationServices.AuthorizationError error 1001.)" ,
20+ "The operation couldn’t be completed. (com.apple.AuthenticationServices.AuthorizationError error 1004.)" ,
21+ "The operation couldn’t be completed. Device must be unlocked to perform request." ,
22+ "UserCancelled" ,
23+ ] ) ;
24+ const passkeyExpectedMessages = new Set ( [
25+ ...passkeyCancelledMessages ,
26+ "The operation couldn’t be completed. Stolen Device Protection is enabled and biometry is required." ,
27+ ] ) ;
28+ const authPrefixes = [ "androidx.credentials.exceptions.domerrors.NotAllowedError" ] ;
29+ const networkMessages = new Set ( [
30+ "Unknown error: A TLS error caused the secure connection to fail." ,
31+ "Unknown error: The Internet connection appears to be offline." ,
32+ "Unknown error: The request timed out." ,
33+ ] ) ;
34+ type ParsedError = ReturnType < typeof parseError > ;
35+
36+ export function isPasskeyExpected ( error : unknown ) {
37+ return classify ( parseError ( error ) ) . passkeyExpected ;
38+ }
39+
40+ export function isPasskeyCancelled ( error : unknown ) {
41+ return classify ( parseError ( error ) ) . passkeyCancelled ;
42+ }
43+
44+ export function isAuthExpected ( error : unknown ) {
45+ return classify ( parseError ( error ) ) . authExpected ;
46+ }
47+
48+ export function isExpected ( error : unknown ) {
49+ return classify ( parseError ( error ) ) . expected ;
50+ }
51+
52+ export function fingerprint ( error : unknown ) {
53+ return classify ( parseError ( error ) ) . fingerprint ;
54+ }
55+
56+ function parseError ( error : unknown ) {
57+ const code =
58+ typeof error === "object" &&
59+ error !== null &&
60+ "code" in error &&
61+ typeof error . code === "string" &&
62+ error . code . length > 0
63+ ? error . code
64+ : undefined ;
65+ const name =
66+ typeof error === "object" &&
67+ error !== null &&
68+ "name" in error &&
69+ typeof error . name === "string" &&
70+ error . name . length > 0
71+ ? error . name
72+ : undefined ;
73+ const message =
74+ error instanceof Error
75+ ? normalizeMessage ( error . message )
76+ : typeof error === "string"
77+ ? normalizeMessage ( error )
78+ : undefined ;
79+ return { code, name, message } ;
80+ }
81+
82+ function classify ( { code, message, name } : ParsedError ) {
83+ const passkeyCancelled = message !== undefined && passkeyCancelledMessages . has ( message ) ;
84+ const unknown = message === "UnknownError" || message ?. startsWith ( "Unknown error:" ) === true ;
85+ const passkeyUnknown = code === "ERR_UNKNOWN" && ( unknown || name === "NotAllowedError" ) ;
86+ const passkeyExpected =
87+ passkeyCancelled ||
88+ passkeyUnknown ||
89+ name === "NotAllowedError" ||
90+ ( message !== undefined &&
91+ ( passkeyExpectedMessages . has ( message ) ||
92+ message . includes ( "There is already a pending passkey request" ) ||
93+ authPrefixes . some ( ( prefix ) => message . startsWith ( prefix ) ) ) ) ;
94+ const authExpected = passkeyExpected || message === "invalid operation" ;
95+ const expected = authExpected || ( message !== undefined && networkMessages . has ( message ) ) ;
96+ const fingerprintMessage =
97+ message ?. startsWith ( "Calling the 'get' function has failed" ) ||
98+ message ?. startsWith ( "The operation couldn’t be completed." ) ||
99+ ( passkeyUnknown && unknown )
100+ ? message
101+ : undefined ;
102+ const value =
103+ code !== undefined && code !== "ERR_UNKNOWN"
104+ ? [ "{{ default }}" , code ]
105+ : fingerprintMessage === undefined
106+ ? undefined
107+ : [ "{{ default }}" , fingerprintMessage ] ;
108+ return { passkeyExpected, passkeyCancelled, authExpected, expected, fingerprint : value } ;
109+ }
110+
111+ function normalizeMessage ( message : string ) {
112+ const value = message . startsWith ( "Error: " ) ? message . slice ( "Error: " . length ) : message ;
113+ return value . trim ( ) ;
114+ }
0 commit comments