@@ -114,11 +114,29 @@ function serverCredential(overrides: Record<string, unknown> = {}) {
114114
115115function apiError ( code : string , paramName ?: string ) {
116116 return new ClerkAPIResponseError ( code , {
117- data : [ { code, message : code , long_message : code , meta : paramName ? { param_name : paramName } : { } } ] ,
117+ data : [
118+ {
119+ code,
120+ message : `${ code } message` ,
121+ long_message : `${ code } long message` ,
122+ meta : paramName ? { param_name : paramName } : { } ,
123+ } ,
124+ ] ,
118125 status : 422 ,
119126 } ) ;
120127}
121128
129+ async function expectBiometricApiError ( operation : Promise < unknown > , cause : ClerkAPIResponseError ) {
130+ const error = await operation . then (
131+ ( ) => expect . unreachable ( 'Expected the operation to reject' ) ,
132+ ( rejection : unknown ) => rejection ,
133+ ) ;
134+ const code = cause . errors [ 0 ] . code ;
135+ expect ( isBiometricCredentialError ( error ) ) . toBe ( true ) ;
136+ expect ( error ) . not . toBeInstanceOf ( ClerkAPIResponseError ) ;
137+ expect ( error ) . toMatchObject ( { code, message : `${ code } long message` , cause } ) ;
138+ }
139+
122140function createClerk ( ) {
123141 const user = {
124142 id : 'user_1' ,
@@ -386,7 +404,7 @@ describe('getAvailability', () => {
386404 const error = apiError ( 'session_reverification_required' ) ;
387405 clerk . user . __experimental_getBiometricCredentials . mockRejectedValue ( error ) ;
388406
389- await expect ( renderBiometricCredentials ( ) . getAvailability ( ) ) . rejects . toBe ( error ) ;
407+ await expectBiometricApiError ( renderBiometricCredentials ( ) . getAvailability ( ) , error ) ;
390408 expect ( biometrics . store . records ) . toHaveLength ( 1 ) ;
391409 } ) ;
392410 } ) ;
@@ -567,7 +585,7 @@ describe('enroll', () => {
567585 const error = apiError ( 'session_reverification_required' ) ;
568586 clerk . user . __experimental_prepareBiometricCredential . mockRejectedValue ( error ) ;
569587
570- await expect ( renderBiometricCredentials ( ) . enroll ( ) ) . rejects . toBe ( error ) ;
588+ await expectBiometricApiError ( renderBiometricCredentials ( ) . enroll ( ) , error ) ;
571589 expect ( biometrics . deleteKey ) . toHaveBeenCalledWith ( 'key_new' ) ;
572590 expect ( biometrics . sign ) . not . toHaveBeenCalled ( ) ;
573591 expect ( biometrics . saveRecord ) . not . toHaveBeenCalled ( ) ;
@@ -587,7 +605,7 @@ describe('enroll', () => {
587605 const error = apiError ( 'form_param_invalid' , 'signature' ) ;
588606 clerk . user . __experimental_attemptBiometricCredential . mockRejectedValue ( error ) ;
589607
590- await expect ( renderBiometricCredentials ( ) . enroll ( ) ) . rejects . toBe ( error ) ;
608+ await expectBiometricApiError ( renderBiometricCredentials ( ) . enroll ( ) , error ) ;
591609 expect ( biometrics . deleteKey ) . toHaveBeenCalledWith ( 'key_new' ) ;
592610 expect ( biometrics . saveRecord ) . not . toHaveBeenCalled ( ) ;
593611 expect ( clerk . user . __experimental_revokeBiometricCredential ) . not . toHaveBeenCalled ( ) ;
@@ -637,7 +655,7 @@ describe('revoke', () => {
637655 const error = apiError ( 'session_reverification_required' ) ;
638656 clerk . user . __experimental_revokeBiometricCredential . mockRejectedValue ( error ) ;
639657
640- await expect ( renderBiometricCredentials ( ) . revoke ( 'td_1' ) ) . rejects . toBe ( error ) ;
658+ await expectBiometricApiError ( renderBiometricCredentials ( ) . revoke ( 'td_1' ) , error ) ;
641659 expect ( biometrics . deleteRecord ) . not . toHaveBeenCalled ( ) ;
642660 } ) ;
643661
@@ -744,7 +762,7 @@ describe('signIn', () => {
744762 const error = apiError ( code , 'trusted_device_id' ) ;
745763 clerk . clientSignIn . create . mockRejectedValue ( error ) ;
746764
747- await expect ( renderBiometricCredentials ( ) . signIn ( ) ) . rejects . toBe ( error ) ;
765+ await expectBiometricApiError ( renderBiometricCredentials ( ) . signIn ( ) , error ) ;
748766 expect ( biometrics . deleteRecord ) . toHaveBeenCalledWith ( 'key_1' ) ;
749767 expect ( biometrics . store . records ) . toEqual ( [ ] ) ;
750768 } ,
@@ -755,7 +773,7 @@ describe('signIn', () => {
755773 const error = apiError ( 'trusted_device_not_registered' , 'trusted_device_id' ) ;
756774 clerk . createdSignIn . attemptFirstFactor . mockRejectedValue ( error ) ;
757775
758- await expect ( renderBiometricCredentials ( ) . signIn ( ) ) . rejects . toBe ( error ) ;
776+ await expectBiometricApiError ( renderBiometricCredentials ( ) . signIn ( ) , error ) ;
759777 expect ( biometrics . store . records ) . toEqual ( [ ] ) ;
760778 } ) ;
761779
@@ -764,7 +782,7 @@ describe('signIn', () => {
764782 const error = apiError ( 'form_resource_not_found' , 'sign_in_id' ) ;
765783 clerk . createdSignIn . attemptFirstFactor . mockRejectedValue ( error ) ;
766784
767- await expect ( renderBiometricCredentials ( ) . signIn ( ) ) . rejects . toBe ( error ) ;
785+ await expectBiometricApiError ( renderBiometricCredentials ( ) . signIn ( ) , error ) ;
768786 expect ( biometrics . deleteRecord ) . not . toHaveBeenCalled ( ) ;
769787 } ) ;
770788
@@ -806,6 +824,51 @@ describe('signIn', () => {
806824 } ) ;
807825} ) ;
808826
827+ describe ( 'API errors' , ( ) => {
828+ test ( 'surface a reverification-required error with its API code' , async ( ) => {
829+ signInClerkUser ( ) ;
830+ clerk . user . __experimental_prepareBiometricCredential . mockRejectedValue ( apiError ( 'session_reverification_required' ) ) ;
831+
832+ const operation = renderBiometricCredentials ( ) . enroll ( ) ;
833+
834+ await expect ( operation ) . rejects . toMatchObject ( { code : 'session_reverification_required' } ) ;
835+ await operation . catch ( error => expect ( isBiometricCredentialError ( error ) ) . toBe ( true ) ) ;
836+ } ) ;
837+
838+ test ( 'surface a trusted_device_not_registered error with its API code' , async ( ) => {
839+ addLocalCredential ( ) ;
840+ clerk . clientSignIn . create . mockRejectedValue ( apiError ( 'trusted_device_not_registered' , 'trusted_device_id' ) ) ;
841+
842+ const operation = renderBiometricCredentials ( ) . signIn ( ) ;
843+
844+ await expect ( operation ) . rejects . toMatchObject ( {
845+ code : 'trusted_device_not_registered' ,
846+ message : 'trusted_device_not_registered long message' ,
847+ } ) ;
848+ await operation . catch ( error => expect ( isBiometricCredentialError ( error ) ) . toBe ( true ) ) ;
849+ } ) ;
850+
851+ test ( 'fall back to the short message and the operation code' , async ( ) => {
852+ signInClerkUser ( ) ;
853+ const withoutLongMessage = new ClerkAPIResponseError ( 'native_api_disabled' , {
854+ data : [ { code : 'native_api_disabled' , message : 'Native API is disabled' , long_message : undefined as never } ] ,
855+ status : 403 ,
856+ } ) ;
857+ clerk . user . __experimental_getBiometricCredentials . mockRejectedValueOnce ( withoutLongMessage ) ;
858+ clerk . user . __experimental_getBiometricCredentials . mockRejectedValueOnce ( new Error ( 'Network down' ) ) ;
859+ const biometricCredentials = renderBiometricCredentials ( ) ;
860+
861+ await expect ( biometricCredentials . list ( ) ) . rejects . toMatchObject ( {
862+ code : 'native_api_disabled' ,
863+ message : 'Native API is disabled' ,
864+ } ) ;
865+ await expect ( biometricCredentials . list ( ) ) . rejects . toMatchObject ( {
866+ code : 'E_TRUSTED_DEVICE_LIST_FAILED' ,
867+ message : 'Network down' ,
868+ } ) ;
869+ } ) ;
870+ } ) ;
871+
809872describe ( 'missing @clerk/expo-biometrics' , ( ) => {
810873 test . each ( [ 'getAvailability' , 'list' , 'enroll' , 'revoke' , 'signIn' ] as const ) (
811874 '%s explains how to install the package' ,
0 commit comments