diff --git a/ios/ReactNativePasskeysModule.swift b/ios/ReactNativePasskeysModule.swift index 06e9aa9..e666f80 100644 --- a/ios/ReactNativePasskeysModule.swift +++ b/ios/ReactNativePasskeysModule.swift @@ -26,11 +26,11 @@ final public class ReactNativePasskeysModule: Module, PasskeyResultHandler { } AsyncFunction("get") { - (request: PublicKeyCredentialRequestOptions, promise: Promise) throws in + (request: PublicKeyCredentialRequestOptions, requireBiometrics: Bool, promise: Promise) throws in do { // - all the throws are already in the helper `isAvailable` so we don't need to do anything // ? this seems like a code smell ... what is the best way to do this - let _ = try isAvailable() + let _ = try isAvailable(requireBiometrics: requireBiometrics) } catch let error { throw error } @@ -54,11 +54,11 @@ final public class ReactNativePasskeysModule: Module, PasskeyResultHandler { }.runOnQueue(.main) AsyncFunction("create") { - (request: PublicKeyCredentialCreationOptions, promise: Promise) throws in + (request: PublicKeyCredentialCreationOptions, requireBiometrics: Bool, promise: Promise) throws in do { // - all the throws are already in the helper `isAvailable` so we don't need to do anything // ? this seems like a code smell ... what is the best way to do this - let _ = try isAvailable() + let _ = try isAvailable(requireBiometrics: requireBiometrics) } catch let error { throw error } @@ -112,7 +112,7 @@ final public class ReactNativePasskeysModule: Module, PasskeyResultHandler { } - private func isAvailable() throws -> Bool { + private func isAvailable(requireBiometrics: Bool = true) throws -> Bool { if #unavailable(iOS 15.0) { throw NotSupportedException() } @@ -121,7 +121,15 @@ final public class ReactNativePasskeysModule: Module, PasskeyResultHandler { throw PendingPasskeyRequestException() } - if LAContext().biometricType == .none { + let context = LAContext() + + // Check the local authentication policy can be evaluated + let policy: LAPolicy = requireBiometrics ? .deviceOwnerAuthenticationWithBiometrics : .deviceOwnerAuthentication + guard context.canEvaluatePolicy(policy, error: nil) else { + throw BiometricException() + } + + if requireBiometrics && context.biometricType == .none { throw BiometricException() } @@ -457,11 +465,6 @@ extension LAContext { var biometricType: BiometricType { var error: NSError? - guard self.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { - // Capture these recoverable error thru Crashlytics - return .none - } - if #available(iOS 11.0, *) { switch self.biometryType { case .none: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b21f96c..379dfe9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,13 +9,13 @@ importers: .: dependencies: expo: - specifier: '*' + specifier: '>=48.0.0' version: 52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(react-native@0.79.1(@babel/core@7.26.10)(@types/react@18.3.20)(react@19.1.0))(react@19.1.0) react: specifier: '*' version: 19.1.0 react-native: - specifier: '*' + specifier: '>=0.71.0' version: 0.79.1(@babel/core@7.26.10)(@types/react@18.3.20)(react@19.1.0) devDependencies: '@arethetypeswrong/cli': @@ -35,7 +35,7 @@ importers: version: 18.3.20 expo-module-scripts: specifier: ^3.4.1 - version: 3.5.4(@babel/core@7.26.10)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(eslint@8.57.1)(jest@29.7.0(@types/node@22.15.2))(prettier@2.8.8)(react-test-renderer@18.2.0(react@19.1.0))(react@19.1.0) + version: 3.5.4(@babel/core@7.26.10)(@jest/types@29.6.3)(@types/eslint@9.6.1)(babel-jest@29.7.0(@babel/core@7.26.10))(eslint@8.57.1)(jest@29.7.0(@types/node@22.15.2))(prettier@2.8.8)(react-dom@19.2.0(react@19.1.0))(react-test-renderer@19.0.0(react@19.1.0))(react@19.1.0) expo-modules-core: specifier: ^1.11.9 version: 1.12.26 @@ -947,8 +947,8 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@eslint-community/eslint-utils@4.6.1': - resolution: {integrity: sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==} + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -1371,6 +1371,12 @@ packages: '@types/babel__traverse@7.20.7': resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -1410,10 +1416,10 @@ packages: '@types/prop-types@15.7.14': resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} - '@types/react-dom@19.1.2': - resolution: {integrity: sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==} + '@types/react-dom@19.2.2': + resolution: {integrity: sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==} peerDependencies: - '@types/react': ^19.0.0 + '@types/react': ^19.2.0 '@types/react-test-renderer@19.1.0': resolution: {integrity: sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==} @@ -1421,8 +1427,8 @@ packages: '@types/react@18.3.20': resolution: {integrity: sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==} - '@types/semver@7.7.0': - resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -1546,6 +1552,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -2336,8 +2347,8 @@ packages: engines: {node: '>=6.0'} hasBin: true - eslint-config-prettier@8.10.0: - resolution: {integrity: sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==} + eslint-config-prettier@8.10.2: + resolution: {integrity: sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==} hasBin: true peerDependencies: eslint: '>=7.0.0' @@ -4124,6 +4135,11 @@ packages: react-devtools-core@6.1.1: resolution: {integrity: sha512-TFo1MEnkqE6hzAbaztnyR5uLTMoz6wnEWwWBsCUzNt+sVXJycuRJdDqvL078M4/h65BI/YO5XWTaxZDWVsW0fw==} + react-dom@19.2.0: + resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} + peerDependencies: + react: ^19.2.0 + react-error-boundary@3.1.4: resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==} engines: {node: '>=10', npm: '>=6'} @@ -4136,6 +4152,9 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-is@19.2.0: + resolution: {integrity: sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==} + react-native@0.79.1: resolution: {integrity: sha512-MZQFEKyKPjqvyjuMUvH02elnmRQFzbS0yf46YOe9ktJWTZGwklsbJkRgaXJx9KA3SK6v1/QXVeCqZmrzho+1qw==} engines: {node: '>=18'} @@ -4161,6 +4180,11 @@ packages: peerDependencies: react: ^18.2.0 + react-test-renderer@19.0.0: + resolution: {integrity: sha512-oX5u9rOQlHzqrE/64CNr0HB0uWxkCQmZNSfozlYvwE71TLVgeZxVf0IjouGEr1v7r1kcDifdAJBeOhdhxsG/DA==} + peerDependencies: + react: ^19.0.0 + react@19.1.0: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} @@ -4321,6 +4345,9 @@ packages: scheduler@0.25.0: resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + selfsigned@2.4.1: resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} engines: {node: '>=10'} @@ -4598,6 +4625,7 @@ packages: sudo-prompt@9.1.1: resolution: {integrity: sha512-es33J1g2HjMpyAhz8lOR+ICmXXAqTuKbuXuUWLhOLew20oN9oUCgCJx615U/v7aioZg7IX5lIh9x34vwneu4pA==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} @@ -5096,8 +5124,8 @@ packages: peerDependencies: zod: ^3.18.0 - zod@3.24.3: - resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} snapshots: @@ -6272,7 +6300,7 @@ snapshots: '@colors/colors@1.5.0': optional: true - '@eslint-community/eslint-utils@4.6.1(eslint@8.57.1)': + '@eslint-community/eslint-utils@4.9.0(eslint@8.57.1)': dependencies: eslint: 8.57.1 eslint-visitor-keys: 3.4.3 @@ -7147,16 +7175,17 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@testing-library/react-hooks@7.0.2(react-test-renderer@18.2.0(react@19.1.0))(react@19.1.0)': + '@testing-library/react-hooks@7.0.2(react-dom@19.2.0(react@19.1.0))(react-test-renderer@19.0.0(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.27.0 '@types/react': 18.3.20 - '@types/react-dom': 19.1.2(@types/react@18.3.20) + '@types/react-dom': 19.2.2(@types/react@18.3.20) '@types/react-test-renderer': 19.1.0 react: 19.1.0 react-error-boundary: 3.1.4(react@19.1.0) optionalDependencies: - react-test-renderer: 18.2.0(react@19.1.0) + react-dom: 19.2.0(react@19.1.0) + react-test-renderer: 19.0.0(react@19.1.0) '@tootallnate/once@2.0.0': {} @@ -7183,6 +7212,15 @@ snapshots: dependencies: '@babel/types': 7.27.0 + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + optional: true + + '@types/estree@1.0.8': + optional: true + '@types/graceful-fs@4.1.9': dependencies: '@types/node': 22.15.2 @@ -7229,7 +7267,7 @@ snapshots: '@types/prop-types@15.7.14': {} - '@types/react-dom@19.1.2(@types/react@18.3.20)': + '@types/react-dom@19.2.2(@types/react@18.3.20)': dependencies: '@types/react': 18.3.20 @@ -7242,7 +7280,7 @@ snapshots: '@types/prop-types': 15.7.14 csstype: 3.1.3 - '@types/semver@7.7.0': {} + '@types/semver@7.7.1': {} '@types/stack-utils@2.0.3': {} @@ -7327,9 +7365,9 @@ snapshots: '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.6.1(eslint@8.57.1) + '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) '@types/json-schema': 7.0.15 - '@types/semver': 7.7.0 + '@types/semver': 7.7.1 '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) @@ -7375,19 +7413,21 @@ snapshots: acorn-globals@7.0.1: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 acorn-walk: 8.3.4 - acorn-jsx@5.3.2(acorn@8.14.1): + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: - acorn: 8.14.1 + acorn: 8.15.0 acorn-walk@8.3.4: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 acorn@8.14.1: {} + acorn@8.15.0: {} + agent-base@6.0.2: dependencies: debug: 4.4.0 @@ -7607,8 +7647,8 @@ snapshots: chalk: 4.1.2 invariant: 2.2.4 pretty-format: 24.9.0 - zod: 3.24.3 - zod-validation-error: 2.1.0(zod@3.24.3) + zod: 3.25.76 + zod-validation-error: 2.1.0(zod@3.25.76) babel-plugin-react-native-web@0.19.13: {} @@ -8323,19 +8363,19 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-prettier@8.10.0(eslint@8.57.1): + eslint-config-prettier@8.10.2(eslint@8.57.1): dependencies: eslint: 8.57.1 - eslint-config-universe@12.1.0(eslint@8.57.1)(prettier@2.8.8)(typescript@5.8.3): + eslint-config-universe@12.1.0(@types/eslint@9.6.1)(eslint@8.57.1)(prettier@2.8.8)(typescript@5.8.3): dependencies: '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.3) eslint: 8.57.1 - eslint-config-prettier: 8.10.0(eslint@8.57.1) + eslint-config-prettier: 8.10.2(eslint@8.57.1) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1) eslint-plugin-node: 11.1.0(eslint@8.57.1) - eslint-plugin-prettier: 5.2.6(eslint-config-prettier@8.10.0(eslint@8.57.1))(eslint@8.57.1)(prettier@2.8.8) + eslint-plugin-prettier: 5.2.6(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@8.57.1))(eslint@8.57.1)(prettier@2.8.8) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) optionalDependencies: @@ -8410,14 +8450,15 @@ snapshots: resolve: 1.22.10 semver: 6.3.1 - eslint-plugin-prettier@5.2.6(eslint-config-prettier@8.10.0(eslint@8.57.1))(eslint@8.57.1)(prettier@2.8.8): + eslint-plugin-prettier@5.2.6(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@8.57.1))(eslint@8.57.1)(prettier@2.8.8): dependencies: eslint: 8.57.1 prettier: 2.8.8 prettier-linter-helpers: 1.0.0 synckit: 0.11.4 optionalDependencies: - eslint-config-prettier: 8.10.0(eslint@8.57.1) + '@types/eslint': 9.6.1 + eslint-config-prettier: 8.10.2(eslint@8.57.1) eslint-plugin-react-hooks@4.6.2(eslint@8.57.1): dependencies: @@ -8460,7 +8501,7 @@ snapshots: eslint@8.57.1: dependencies: - '@eslint-community/eslint-utils': 4.6.1(eslint@8.57.1) + '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) '@eslint-community/regexpp': 4.12.1 '@eslint/eslintrc': 2.1.4 '@eslint/js': 8.57.1 @@ -8503,8 +8544,8 @@ snapshots: espree@9.6.1: dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 3.4.3 esprima@4.0.1: {} @@ -8597,20 +8638,20 @@ snapshots: expo: 52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(react-native@0.79.1(@babel/core@7.26.10)(@types/react@18.3.20)(react@19.1.0))(react@19.1.0) react: 19.1.0 - expo-module-scripts@3.5.4(@babel/core@7.26.10)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(eslint@8.57.1)(jest@29.7.0(@types/node@22.15.2))(prettier@2.8.8)(react-test-renderer@18.2.0(react@19.1.0))(react@19.1.0): + expo-module-scripts@3.5.4(@babel/core@7.26.10)(@jest/types@29.6.3)(@types/eslint@9.6.1)(babel-jest@29.7.0(@babel/core@7.26.10))(eslint@8.57.1)(jest@29.7.0(@types/node@22.15.2))(prettier@2.8.8)(react-dom@19.2.0(react@19.1.0))(react-test-renderer@19.0.0(react@19.1.0))(react@19.1.0): dependencies: '@babel/cli': 7.27.0(@babel/core@7.26.10) '@babel/plugin-transform-export-namespace-from': 7.25.9(@babel/core@7.26.10) '@babel/preset-env': 7.26.9(@babel/core@7.26.10) '@babel/preset-typescript': 7.27.0(@babel/core@7.26.10) '@expo/npm-proofread': 1.0.1 - '@testing-library/react-hooks': 7.0.2(react-test-renderer@18.2.0(react@19.1.0))(react@19.1.0) + '@testing-library/react-hooks': 7.0.2(react-dom@19.2.0(react@19.1.0))(react-test-renderer@19.0.0(react@19.1.0))(react@19.1.0) '@tsconfig/node18': 18.2.4 '@types/jest': 29.5.14 babel-plugin-dynamic-import-node: 2.3.3 babel-preset-expo: 11.0.15(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10)) commander: 2.20.3 - eslint-config-universe: 12.1.0(eslint@8.57.1)(prettier@2.8.8)(typescript@5.8.3) + eslint-config-universe: 12.1.0(@types/eslint@9.6.1)(eslint@8.57.1)(prettier@2.8.8)(typescript@5.8.3) find-yarn-workspace-root: 2.0.0 glob: 7.2.3 jest-expo: 51.0.4(@babel/core@7.26.10)(jest@29.7.0(@types/node@22.15.2))(react@19.1.0) @@ -9745,7 +9786,7 @@ snapshots: jsdom@20.0.3: dependencies: abab: 2.0.6 - acorn: 8.14.1 + acorn: 8.15.0 acorn-globals: 7.0.1 cssom: 0.5.0 cssstyle: 2.3.0 @@ -10596,6 +10637,12 @@ snapshots: - bufferutil - utf-8-validate + react-dom@19.2.0(react@19.1.0): + dependencies: + react: 19.1.0 + scheduler: 0.27.0 + optional: true + react-error-boundary@3.1.4(react@19.1.0): dependencies: '@babel/runtime': 7.27.0 @@ -10605,6 +10652,9 @@ snapshots: react-is@18.3.1: {} + react-is@19.2.0: + optional: true + react-native@0.79.1(@babel/core@7.26.10)(@types/react@18.3.20)(react@19.1.0): dependencies: '@jest/create-cache-key-function': 29.7.0 @@ -10668,6 +10718,13 @@ snapshots: react-shallow-renderer: 16.15.0(react@19.1.0) scheduler: 0.23.2 + react-test-renderer@19.0.0(react@19.1.0): + dependencies: + react: 19.1.0 + react-is: 19.2.0 + scheduler: 0.25.0 + optional: true + react@19.1.0: {} read-yaml-file@1.1.0: @@ -10838,6 +10895,9 @@ snapshots: scheduler@0.25.0: {} + scheduler@0.27.0: + optional: true + selfsigned@2.4.1: dependencies: '@types/node-forge': 1.3.11 @@ -11631,8 +11691,8 @@ snapshots: yocto-queue@0.1.0: {} - zod-validation-error@2.1.0(zod@3.24.3): + zod-validation-error@2.1.0(zod@3.25.76): dependencies: - zod: 3.24.3 + zod: 3.25.76 - zod@3.24.3: {} + zod@3.25.76: {} diff --git a/src/ReactNativePasskeysModule.ts b/src/ReactNativePasskeysModule.ts index c14413b..8ce17ce 100644 --- a/src/ReactNativePasskeysModule.ts +++ b/src/ReactNativePasskeysModule.ts @@ -1,9 +1,11 @@ -import { requireNativeModule } from "expo-modules-core"; +import { Platform, requireNativeModule } from "expo-modules-core"; import { NotSupportedError } from "./errors"; import type { PublicKeyCredentialCreationOptionsJSON, + PublicKeyCredentialRequestOptionsJSON, CreationResponse, + AuthenticationResponseJSON, } from "./ReactNativePasskeys.types"; // It loads the native module object from the JSI or falls back to @@ -13,10 +15,17 @@ const passkeys = requireNativeModule("ReactNativePasskeys"); export default { ...passkeys, - async create(request: PublicKeyCredentialCreationOptionsJSON): Promise { + async create( + request: PublicKeyCredentialCreationOptionsJSON, + requireBiometrics: boolean, + ): Promise { if (!this.isSupported) throw new NotSupportedError(); - const credential = await passkeys.create(request); + const credential = + Platform.OS === "ios" + ? await passkeys.create(request, requireBiometrics) + : await passkeys.create(request); + return { ...credential, response: { @@ -27,4 +36,13 @@ export default { }, }; }, + + async get( + request: PublicKeyCredentialRequestOptionsJSON, + requireBiometrics: boolean, + ): Promise { + return Platform.OS === "ios" + ? await passkeys.get(request, requireBiometrics) + : await passkeys.get(request); + }, }; diff --git a/src/ReactNativePasskeysModule.web.ts b/src/ReactNativePasskeysModule.web.ts index 5bee97c..2ca2a76 100644 --- a/src/ReactNativePasskeysModule.web.ts +++ b/src/ReactNativePasskeysModule.web.ts @@ -2,15 +2,15 @@ import { NotSupportedError } from "./errors"; import { base64URLStringToBuffer, bufferToBase64URLString } from "./utils/base64"; import type { - AuthenticationCredential, - AuthenticationExtensionsClientInputs, - AuthenticationExtensionsClientOutputs, - AuthenticationExtensionsClientOutputsJSON, - AuthenticationResponseJSON, - PublicKeyCredentialCreationOptionsJSON, - PublicKeyCredentialRequestOptionsJSON, - RegistrationCredential, - CreationResponse, + AuthenticationCredential, + AuthenticationExtensionsClientInputs, + AuthenticationExtensionsClientOutputs, + AuthenticationExtensionsClientOutputsJSON, + AuthenticationResponseJSON, + CreationResponse, + PublicKeyCredentialCreationOptionsJSON, + PublicKeyCredentialRequestOptionsJSON, + RegistrationCredential, } from "./ReactNativePasskeys.types"; import { normalizePRFInputs } from "./utils/prf"; @@ -193,7 +193,7 @@ const warnUserOfMissingWebauthnExtensions = ( ) => { if (clientExtensionResults) { for (const key in requestedExtensions) { - if (typeof clientExtensionResults[key] === "undefined") { + if (typeof (clientExtensionResults)[key] === "undefined") { alert( `Webauthn extension ${key} is undefined -- your browser probably doesn't know about it`, ); diff --git a/src/index.ts b/src/index.ts index 933dc78..68aa419 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,26 @@ export function isAutoFillAvalilable(): boolean { return ReactNativePasskeysModule.isAutoFillAvalilable(); } +export interface PasskeysConfig { + /** + * Options and configuration specific to the iOS platform. + */ + ios?: { + /** + * Defines the [local authentication policy](https://developer.apple.com/documentation/localauthentication/lapolicy) to use: + * - `true`: Use the `deviceOwnerAuthenticationWithBiometrics` policy. + * - `false`: Use the `deviceOwnerAuthentication` policy. + * Defaults to `true`. + * + * @see {@linkcode https://developer.apple.com/documentation/localauthentication/lapolicy/deviceownerauthenticationwithbiometrics|LAPolicy.deviceOwnerAuthenticationWithBiometrics} + * @see {@linkcode https://developer.apple.com/documentation/localauthentication/lapolicy/deviceownerauthentication|LAPolicy.deviceOwnerAuthentication} + */ + requireBiometrics?: boolean; + }; +} + +export interface PasskeysCreateOptions extends PasskeysConfig {} + export async function create( request: Omit & { // Platform support: @@ -32,10 +52,13 @@ export async function create( credProps?: boolean; }; } & Pick, + options?: PasskeysCreateOptions, ): Promise { - return await ReactNativePasskeysModule.create(request); + return await ReactNativePasskeysModule.create(request, options?.ios?.requireBiometrics ?? true); } +export interface PasskeysGetOptions extends PasskeysConfig {} + export async function get( request: Omit & { // Platform support: @@ -47,6 +70,7 @@ export async function get( prf?: AuthenticationExtensionsPRFInputs; }; }, + options?: PasskeysGetOptions, ): Promise { - return await ReactNativePasskeysModule.get(request); + return await ReactNativePasskeysModule.get(request, options?.ios?.requireBiometrics ?? true); } diff --git a/src/utils/warn-user-of-missing-webauthn-extensions.ts b/src/utils/warn-user-of-missing-webauthn-extensions.ts index c887a54..b21827c 100644 --- a/src/utils/warn-user-of-missing-webauthn-extensions.ts +++ b/src/utils/warn-user-of-missing-webauthn-extensions.ts @@ -12,7 +12,7 @@ export const warnUserOfMissingWebauthnExtensions = ( ) => { if (clientExtensionResults) { for (const key in requestedExtensions) { - if (typeof clientExtensionResults[key] === "undefined") { + if (typeof (clientExtensionResults)[key] === "undefined") { alert( `Webauthn extension ${key} is undefined -- your browser probably doesn't know about it`, );