diff --git a/example/capacitor/src/pages/Home.tsx b/example/capacitor/src/pages/Home.tsx index f7df9155..5910e960 100644 --- a/example/capacitor/src/pages/Home.tsx +++ b/example/capacitor/src/pages/Home.tsx @@ -44,7 +44,7 @@ import authgearCapacitor, { BiometricOptions, BiometricAccessConstraintIOS, BiometricLAPolicy, - BiometricAccessConstraintAndroid, + BiometricAuthenticatorAndroid, WebKitWebViewUIImplementation, DeviceBrowserUIImplementation, } from "@authgear/capacitor"; @@ -76,6 +76,7 @@ const REDIRECT_URI_CAPACITOR = "com.authgear.exampleapp.capacitor://host/path"; const biometricOptions: BiometricOptions = { ios: { localizedReason: "Use biometric to authenticate", + localizedCancelTitle: "Customized Cancel", constraint: BiometricAccessConstraintIOS.BiometryCurrentSet, policy: BiometricLAPolicy.deviceOwnerAuthenticationWithBiometrics, }, @@ -83,8 +84,14 @@ const biometricOptions: BiometricOptions = { title: "Biometric Authentication", subtitle: "Biometric authentication", description: "Use biometric to authenticate", - negativeButtonText: "Cancel", - constraint: [BiometricAccessConstraintAndroid.BiometricStrong], + negativeButtonText: "Customized Cancel", + allowedAuthenticatorsOnEnable: [ + BiometricAuthenticatorAndroid.BiometricStrong, + ], + allowedAuthenticatorsOnAuthenticate: [ + BiometricAuthenticatorAndroid.BiometricStrong, + BiometricAuthenticatorAndroid.DeviceCredential, + ], invalidatedByBiometricEnrollment: true, }, }; diff --git a/example/reactnative/src/screens/MainScreen.tsx b/example/reactnative/src/screens/MainScreen.tsx index 16d83723..d7554c9a 100644 --- a/example/reactnative/src/screens/MainScreen.tsx +++ b/example/reactnative/src/screens/MainScreen.tsx @@ -30,7 +30,7 @@ import authgear, { BiometricOptions, BiometricAccessConstraintIOS, BiometricLAPolicy, - BiometricAccessConstraintAndroid, + BiometricAuthenticatorAndroid, SessionState, WebKitWebViewUIImplementation, } from '@authgear/react-native'; @@ -132,6 +132,7 @@ const wechatRedirectURI = Platform.select({ const biometricOptions: BiometricOptions = { ios: { localizedReason: 'Use biometric to authenticate', + localizedCancelTitle: 'Customized Cancel', constraint: BiometricAccessConstraintIOS.BiometryCurrentSet, policy: BiometricLAPolicy.deviceOwnerAuthenticationWithBiometrics, }, @@ -139,8 +140,14 @@ const biometricOptions: BiometricOptions = { title: 'Biometric Authentication', subtitle: 'Biometric authentication', description: 'Use biometric to authenticate', - negativeButtonText: 'Cancel', - constraint: [BiometricAccessConstraintAndroid.BiometricStrong], + negativeButtonText: 'Customized Cancel', + allowedAuthenticatorsOnEnable: [ + BiometricAuthenticatorAndroid.BiometricStrong, + ], + allowedAuthenticatorsOnAuthenticate: [ + BiometricAuthenticatorAndroid.BiometricStrong, + BiometricAuthenticatorAndroid.DeviceCredential, + ], invalidatedByBiometricEnrollment: true, }, }; diff --git a/packages/authgear-capacitor/android/src/main/java/com/authgear/capacitor/Authgear.java b/packages/authgear-capacitor/android/src/main/java/com/authgear/capacitor/Authgear.java index af007ef0..76d4fa21 100644 --- a/packages/authgear-capacitor/android/src/main/java/com/authgear/capacitor/Authgear.java +++ b/packages/authgear-capacitor/android/src/main/java/com/authgear/capacitor/Authgear.java @@ -237,19 +237,19 @@ JSONObject getDeviceInfo(Context ctx) throws Exception { return rootMap; } - int checkBiometricSupported(Context ctx, int flags) throws Exception { + int checkBiometricSupported(Context ctx, int allowedAuthenticatorsOnEnableFlags, int allowedAuthenticatorsOnAuthenticateFlags) throws Exception { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { throw this.makeBiometricMinimumAPILevelException(); } BiometricManager manager = BiometricManager.from(ctx); - int result = manager.canAuthenticate(flags); + int result = manager.canAuthenticate(allowedAuthenticatorsOnEnableFlags); if (result == BiometricManager.BIOMETRIC_SUCCESS) { // Further test if the key pair generator can be initialized. // https://issuetracker.google.com/issues/147374428#comment9 try { - this.createKeyPairGenerator(this.makeGenerateKeyPairSpec("__test__", true, flags, true)); + this.createKeyPairGenerator(this.makeGenerateKeyPairSpec("__test__", true, allowedAuthenticatorsOnAuthenticateFlags, true)); } catch (Exception e) { // This branch is reachable only when there is a weak face and no strong fingerprints. // So we treat this situation as BIOMETRIC_ERROR_NONE_ENROLLED. @@ -266,11 +266,11 @@ void createBiometricPrivateKey(AppCompatActivity activity, BiometricOptions opti throw this.makeBiometricMinimumAPILevelException(); } - BiometricPrompt.PromptInfo promptInfo = this.buildPromptInfo(options); + BiometricPrompt.PromptInfo promptInfo = this.buildPromptInfo(options, options.allowedAuthenticatorsOnEnableFlags); KeyGenParameterSpec spec = this.makeGenerateKeyPairSpec( options.alias, true, - this.authenticatorTypesToKeyProperties(options.flags), + this.authenticatorTypesToKeyProperties(options.allowedAuthenticatorsOnAuthenticateFlags), options.invalidatedByBiometricEnrollment ); KeyPair keyPair = this.createKeyPair(spec); @@ -289,7 +289,7 @@ void signWithBiometricPrivateKey(AppCompatActivity activity, BiometricOptions op throw this.makeBiometricMinimumAPILevelException(); } - BiometricPrompt.PromptInfo promptInfo = this.buildPromptInfo(options); + BiometricPrompt.PromptInfo promptInfo = this.buildPromptInfo(options, options.allowedAuthenticatorsOnAuthenticateFlags); KeyPair keyPair = this.getPrivateKey(options.alias); this.signBiometricJWT( activity, @@ -435,14 +435,15 @@ private KeyPair getPrivateKey(String alias) throws Exception { } private BiometricPrompt.PromptInfo buildPromptInfo( - BiometricOptions options + BiometricOptions options, + int flags ) { BiometricPrompt.PromptInfo.Builder builder = new BiometricPrompt.PromptInfo.Builder(); builder.setTitle(options.title); builder.setSubtitle(options.subtitle); builder.setDescription(options.description); - builder.setAllowedAuthenticators(options.flags); - if ((options.flags & BiometricManager.Authenticators.DEVICE_CREDENTIAL) == 0) { + builder.setAllowedAuthenticators(flags); + if ((flags & BiometricManager.Authenticators.DEVICE_CREDENTIAL) == 0) { builder.setNegativeButtonText(options.negativeButtonText); } return builder.build(); diff --git a/packages/authgear-capacitor/android/src/main/java/com/authgear/capacitor/AuthgearPlugin.java b/packages/authgear-capacitor/android/src/main/java/com/authgear/capacitor/AuthgearPlugin.java index 1d507584..ea55b280 100644 --- a/packages/authgear-capacitor/android/src/main/java/com/authgear/capacitor/AuthgearPlugin.java +++ b/packages/authgear-capacitor/android/src/main/java/com/authgear/capacitor/AuthgearPlugin.java @@ -189,12 +189,14 @@ private void handleOpenAuthorizeURLWithWebView(PluginCall call, ActivityResult a @PluginMethod public void checkBiometricSupported(PluginCall call) { JSObject android = call.getObject("android"); - JSONArray constraint = this.jsObjectGetArray(android, "constraint"); - int flags = this.constraintToFlag(constraint); + JSONArray allowedAuthenticatorsOnEnable = this.jsObjectGetArray(android, "allowedAuthenticatorsOnEnable"); + JSONArray allowedAuthenticatorsOnAuthenticate = this.jsObjectGetArray(android, "allowedAuthenticatorsOnAuthenticate"); + int allowedAuthenticatorsOnEnableFlags = this.constraintToFlag(allowedAuthenticatorsOnEnable); + int allowedAuthenticatorsOnAuthenticateFlags = this.constraintToFlag(allowedAuthenticatorsOnAuthenticate); Context ctx = this.getContext(); try { - int result = this.implementation.checkBiometricSupported(ctx, flags); + int result = this.implementation.checkBiometricSupported(ctx, allowedAuthenticatorsOnEnableFlags, allowedAuthenticatorsOnAuthenticateFlags); if (result == BiometricManager.BIOMETRIC_SUCCESS) { call.resolve(); } else { @@ -214,19 +216,23 @@ public void createBiometricPrivateKey(PluginCall call) { String kid = call.getString("kid"); String alias = "com.authgear.keys.biometric." + kid; JSObject android = call.getObject("android"); - JSONArray constraint = this.jsObjectGetArray(android, "constraint"); boolean invalidatedByBiometricEnrollment = android.getBool("invalidatedByBiometricEnrollment"); - int flags = this.constraintToFlag(constraint); String title = android.getString("title"); String subtitle = android.getString("subtitle"); String description = android.getString("description"); String negativeButtonText = android.getString("negativeButtonText"); + JSONArray allowedAuthenticatorsOnEnable = this.jsObjectGetArray(android, "allowedAuthenticatorsOnEnable"); + JSONArray allowedAuthenticatorsOnAuthenticate = this.jsObjectGetArray(android, "allowedAuthenticatorsOnAuthenticate"); + int allowedAuthenticatorsOnEnableFlags = this.constraintToFlag(allowedAuthenticatorsOnEnable); + int allowedAuthenticatorsOnAuthenticateFlags = this.constraintToFlag(allowedAuthenticatorsOnAuthenticate); + BiometricOptions options = new BiometricOptions(); options.payload = payload; options.kid = kid; options.alias = alias; - options.flags = flags; + options.allowedAuthenticatorsOnEnableFlags = allowedAuthenticatorsOnEnableFlags; + options.allowedAuthenticatorsOnAuthenticateFlags = allowedAuthenticatorsOnAuthenticateFlags; options.invalidatedByBiometricEnrollment = invalidatedByBiometricEnrollment; options.title = title; options.subtitle = subtitle; @@ -269,19 +275,23 @@ public void signWithBiometricPrivateKey(PluginCall call) { String kid = call.getString("kid"); String alias = "com.authgear.keys.biometric." + kid; JSObject android = call.getObject("android"); - JSONArray constraint = this.jsObjectGetArray(android, "constraint"); boolean invalidatedByBiometricEnrollment = android.getBool("invalidatedByBiometricEnrollment"); - int flags = this.constraintToFlag(constraint); String title = android.getString("title"); String subtitle = android.getString("subtitle"); String description = android.getString("description"); String negativeButtonText = android.getString("negativeButtonText"); + JSONArray allowedAuthenticatorsOnEnable = this.jsObjectGetArray(android, "allowedAuthenticatorsOnEnable"); + JSONArray allowedAuthenticatorsOnAuthenticate = this.jsObjectGetArray(android, "allowedAuthenticatorsOnAuthenticate"); + int allowedAuthenticatorsOnEnableFlags = this.constraintToFlag(allowedAuthenticatorsOnEnable); + int allowedAuthenticatorsOnAuthenticateFlags = this.constraintToFlag(allowedAuthenticatorsOnAuthenticate); + BiometricOptions options = new BiometricOptions(); options.payload = payload; options.kid = kid; options.alias = alias; - options.flags = flags; + options.allowedAuthenticatorsOnEnableFlags = allowedAuthenticatorsOnEnableFlags; + options.allowedAuthenticatorsOnAuthenticateFlags = allowedAuthenticatorsOnAuthenticateFlags; options.invalidatedByBiometricEnrollment = invalidatedByBiometricEnrollment; options.title = title; options.subtitle = subtitle; diff --git a/packages/authgear-capacitor/android/src/main/java/com/authgear/capacitor/BiometricOptions.java b/packages/authgear-capacitor/android/src/main/java/com/authgear/capacitor/BiometricOptions.java index 21bfeb03..c140b1f9 100644 --- a/packages/authgear-capacitor/android/src/main/java/com/authgear/capacitor/BiometricOptions.java +++ b/packages/authgear-capacitor/android/src/main/java/com/authgear/capacitor/BiometricOptions.java @@ -6,7 +6,8 @@ class BiometricOptions { JSObject payload; String kid; String alias; - int flags; + int allowedAuthenticatorsOnEnableFlags; + int allowedAuthenticatorsOnAuthenticateFlags; boolean invalidatedByBiometricEnrollment; String title; String subtitle; diff --git a/packages/authgear-capacitor/ios/Plugin/AuthgearPlugin.swift b/packages/authgear-capacitor/ios/Plugin/AuthgearPlugin.swift index 5756f676..b3a6730f 100644 --- a/packages/authgear-capacitor/ios/Plugin/AuthgearPlugin.swift +++ b/packages/authgear-capacitor/ios/Plugin/AuthgearPlugin.swift @@ -136,9 +136,10 @@ public class AuthgearPlugin: CAPPlugin { @objc func checkBiometricSupported(_ call: CAPPluginCall) { let ios = call.getObject("ios")! let policyString = ios["policy"] as! String + let localizedCancelTitle = ios["localizedCancelTitle"] as? String DispatchQueue.main.async { do { - try self.impl.checkBiometricSupported(policyString: policyString) + try self.impl.checkBiometricSupported(policyString: policyString, localizedCancelTitle: localizedCancelTitle) call.resolve() } catch { error.reject(call) @@ -153,12 +154,14 @@ public class AuthgearPlugin: CAPPlugin { let constraint = ios["constraint"] as! String let policyString = ios["policy"] as! String let localizedReason = ios["localizedReason"] as! String + let localizedCancelTitle = ios["localizedCancelTitle"] as? String let tag = "com.authgear.keys.biometric.\(kid)" DispatchQueue.main.async { self.impl.createBiometricPrivateKey( policyString: policyString, localizedReason: localizedReason, + localizedCancelTitle: localizedCancelTitle, constraint: constraint, kid: kid, tag: tag, @@ -182,12 +185,14 @@ public class AuthgearPlugin: CAPPlugin { let ios = call.getObject("ios")! let policyString = ios["policy"] as! String let localizedReason = ios["localizedReason"] as! String + let localizedCancelTitle = ios["localizedCancelTitle"] as? String let tag = "com.authgear.keys.biometric.\(kid)" DispatchQueue.main.async { self.impl.signWithBiometricPrivateKey( policyString: policyString, localizedReason: localizedReason, + localizedCancelTitle: localizedCancelTitle, kid: kid, tag: tag, payload: payload diff --git a/packages/authgear-capacitor/ios/Plugin/AuthgearPluginImpl.swift b/packages/authgear-capacitor/ios/Plugin/AuthgearPluginImpl.swift index dc9e796b..9b33f218 100644 --- a/packages/authgear-capacitor/ios/Plugin/AuthgearPluginImpl.swift +++ b/packages/authgear-capacitor/ios/Plugin/AuthgearPluginImpl.swift @@ -250,10 +250,10 @@ import Capacitor controller?.start() } - @objc func checkBiometricSupported(policyString: String) throws { + @objc func checkBiometricSupported(policyString: String, localizedCancelTitle: String?) throws { if #available(iOS 11.3, *) { let policy = LAPolicy.from(string: policyString)! - let laContext = self.makeLAContext(policy: policy) + let laContext = self.makeLAContext(policy: policy, localizedCancelTitle: localizedCancelTitle) var error: NSError? laContext.canEvaluatePolicy(policy, error: &error) if let error = error { @@ -267,6 +267,7 @@ import Capacitor @objc func createBiometricPrivateKey( policyString: String, localizedReason: String, + localizedCancelTitle: String?, constraint: String, kid: String, tag: String, @@ -274,7 +275,7 @@ import Capacitor completion: @escaping (String?, Error?) -> Void ) { let policy = LAPolicy.from(string: policyString)! - let ctx = makeLAContext(policy: policy) + let ctx = makeLAContext(policy: policy, localizedCancelTitle: localizedCancelTitle) ctx.evaluatePolicy(policy, localizedReason: localizedReason) { ok, error in if let error = error { completion(nil, error) @@ -296,13 +297,14 @@ import Capacitor @objc func signWithBiometricPrivateKey( policyString: String, localizedReason: String, + localizedCancelTitle: String?, kid: String, tag: String, payload: [String: Any], completion: @escaping (String?, Error?) -> Void ) { let policy = LAPolicy.from(string: policyString)! - let ctx = makeLAContext(policy: policy) + let ctx = makeLAContext(policy: policy, localizedCancelTitle: localizedCancelTitle) ctx.evaluatePolicy(policy, localizedReason: localizedReason) { ok, error in if let error = error { completion(nil, error) @@ -393,11 +395,12 @@ import Capacitor return window } - private func makeLAContext(policy: LAPolicy) -> LAContext { + private func makeLAContext(policy: LAPolicy, localizedCancelTitle: String?) -> LAContext { let ctx = LAContext() if policy == LAPolicy.deviceOwnerAuthenticationWithBiometrics { ctx.localizedFallbackTitle = ""; } + ctx.localizedCancelTitle = localizedCancelTitle return ctx } diff --git a/packages/authgear-capacitor/src/types.ts b/packages/authgear-capacitor/src/types.ts index 6298d8d6..36488b36 100644 --- a/packages/authgear-capacitor/src/types.ts +++ b/packages/authgear-capacitor/src/types.ts @@ -277,11 +277,17 @@ export enum BiometricAccessConstraintIOS { */ export interface BiometricOptionsIOS { /** - * See https://developer.apple.com/documentation/localauthentication/lacontext/1514176-evaluatepolicy#parameters + * See https://developer.apple.com/documentation/localauthentication/lacontext/localizedreason * * @public */ localizedReason: string; + /** + * See https://developer.apple.com/documentation/localauthentication/lacontext/localizedcanceltitle + * + * @public + */ + localizedCancelTitle?: string; constraint: BiometricAccessConstraintIOS; policy: BiometricLAPolicy; } @@ -291,7 +297,7 @@ export interface BiometricOptionsIOS { * * @public */ -export enum BiometricAccessConstraintAndroid { +export enum BiometricAuthenticatorAndroid { /** * The user can use Class 3 biometric to authenticate. * @@ -340,7 +346,28 @@ export interface BiometricOptionsAndroid { * @public */ negativeButtonText: string; - constraint: BiometricAccessConstraintAndroid[]; + /** + * Set the allowed authenticators when the user enables biometric. + * This must be a subset of allowedAuthenticatorsOnAuthenticate because + * you normally want to ensure the user performed at least once biometric authentication during setup, + * but allow the user to fallback to passcode for subsequent biometric authentication. + * + * See {@link BiometricAuthenticatorAndroid} + * + * @public + */ + allowedAuthenticatorsOnEnable: BiometricAuthenticatorAndroid[]; + /** + * Set the allowed authenticators when the user performs biometric authentication. + * This must be a superset of allowedAuthenticatorsOnEnable because + * you normally want to ensure the user performed at least once biometric authentication during setup, + * but allow the user to fallback to passcode for subsequent biometric authentication. + * + * See {@link BiometricAuthenticatorAndroid} + * + * @public + */ + allowedAuthenticatorsOnAuthenticate: BiometricAuthenticatorAndroid[]; /** * The user needs to set up biometric again when a new biometric is enrolled or all enrolled biometrics are removed. * diff --git a/packages/authgear-react-native/android/src/main/java/com/authgear/reactnative/AuthgearReactNativeModuleImpl.java b/packages/authgear-react-native/android/src/main/java/com/authgear/reactnative/AuthgearReactNativeModuleImpl.java index 7e5dcbbe..d32514da 100644 --- a/packages/authgear-react-native/android/src/main/java/com/authgear/reactnative/AuthgearReactNativeModuleImpl.java +++ b/packages/authgear-react-native/android/src/main/java/com/authgear/reactnative/AuthgearReactNativeModuleImpl.java @@ -389,16 +389,18 @@ public void checkBiometricSupported(ReadableMap options, Promise promise) { return; } ReadableMap android = options.getMap("android"); - ReadableArray constraint = android.getArray("constraint"); + ReadableArray allowedAuthenticatorsOnEnable = android.getArray("allowedAuthenticatorsOnEnable"); + ReadableArray allowedAuthenticatorsOnAuthenticate = android.getArray("allowedAuthenticatorsOnAuthenticate"); BiometricManager manager = BiometricManager.from(this.getReactApplicationContext()); - int flags = constraintToFlag(constraint); - int result = manager.canAuthenticate(flags); + int allowedAuthenticatorsOnEnableFlags = constraintToFlag(allowedAuthenticatorsOnEnable); + int allowedAuthenticatorsOnAuthenticateFlags = constraintToFlag(allowedAuthenticatorsOnAuthenticate); + int result = manager.canAuthenticate(allowedAuthenticatorsOnEnableFlags); if (result == BiometricManager.BIOMETRIC_SUCCESS) { // Further test if the key pair generator can be initialized. // https://issuetracker.google.com/issues/147374428#comment9 try { - this.createKeyPairGenerator(this.makeGenerateKeyPairSpec("__test__", flags, true)); + this.createKeyPairGenerator(this.makeGenerateKeyPairSpec("__test__", allowedAuthenticatorsOnAuthenticateFlags, true)); promise.resolve(null); return; } catch (Exception e) { @@ -665,13 +667,15 @@ public void createBiometricPrivateKey(ReadableMap options, Promise promise) { String kid = options.getString("kid"); ReadableMap payload = options.getMap("payload"); ReadableMap android = options.getMap("android"); - ReadableArray constraint = android.getArray("constraint"); + ReadableArray allowedAuthenticatorsOnEnable = android.getArray("allowedAuthenticatorsOnEnable"); + ReadableArray allowedAuthenticatorsOnAuthenticate = android.getArray("allowedAuthenticatorsOnAuthenticate"); boolean invalidatedByBiometricEnrollment = android.getBoolean("invalidatedByBiometricEnrollment"); - int flags = constraintToFlag(constraint); + int allowedAuthenticatorsOnEnableFlags = constraintToFlag(allowedAuthenticatorsOnEnable); + int allowedAuthenticatorsOnAuthenticateFlags = constraintToFlag(allowedAuthenticatorsOnAuthenticate); String alias = "com.authgear.keys.biometric." + kid; - BiometricPrompt.PromptInfo promptInfo = this.buildPromptInfo(android, flags); - KeyGenParameterSpec spec = this.makeGenerateKeyPairSpec(alias, authenticatorTypesToKeyProperties(flags), invalidatedByBiometricEnrollment); + BiometricPrompt.PromptInfo promptInfo = this.buildPromptInfo(android, allowedAuthenticatorsOnEnableFlags); + KeyGenParameterSpec spec = this.makeGenerateKeyPairSpec(alias, authenticatorTypesToKeyProperties(allowedAuthenticatorsOnAuthenticateFlags), invalidatedByBiometricEnrollment); try { KeyPair keyPair = this.createKeyPair(spec); @@ -707,11 +711,11 @@ public void signWithBiometricPrivateKey(ReadableMap options, Promise promise) { String kid = options.getString("kid"); ReadableMap payload = options.getMap("payload"); ReadableMap android = options.getMap("android"); - ReadableArray constraint = android.getArray("constraint"); - int flags = constraintToFlag(constraint); + ReadableArray allowedAuthenticatorsOnAuthenticate = android.getArray("allowedAuthenticatorsOnAuthenticate"); + int allowedAuthenticatorsOnAuthenticateFlags = constraintToFlag(allowedAuthenticatorsOnAuthenticate); String alias = "com.authgear.keys.biometric." + kid; - BiometricPrompt.PromptInfo promptInfo = this.buildPromptInfo(android, flags); + BiometricPrompt.PromptInfo promptInfo = this.buildPromptInfo(android, allowedAuthenticatorsOnAuthenticateFlags); try { KeyPair keyPair = this.getPrivateKey(alias); diff --git a/packages/authgear-react-native/ios/AGAuthgearReactNative.mm b/packages/authgear-react-native/ios/AGAuthgearReactNative.mm index b1b1c8ed..cf15d510 100644 --- a/packages/authgear-react-native/ios/AGAuthgearReactNative.mm +++ b/packages/authgear-react-native/ios/AGAuthgearReactNative.mm @@ -370,8 +370,9 @@ + (BOOL)application:(UIApplication *)application if (@available(iOS 11.3, *)) { NSDictionary *iosDict = options[@"ios"]; NSString *policyString = iosDict[@"policy"]; + NSString *localizedCancelTitle = iosDict[@"localizedCancelTitle"]; LAPolicy policy = [self laPolicyFromString:policyString]; - LAContext *context = [self laContextFromPolicy:policy]; + LAContext *context = [self laContextFromPolicy:policy localizedCancelTitle:localizedCancelTitle]; NSError *error = NULL; [context canEvaluatePolicy:policy error:&error]; if (error) { @@ -410,10 +411,11 @@ + (BOOL)application:(UIApplication *)application NSDictionary *iosDict = options[@"ios"]; NSString *constraint = iosDict[@"constraint"]; NSString *localizedReason = iosDict[@"localizedReason"]; + NSString *localizedCancelTitle = iosDict[@"localizedCancelTitle"]; NSString *policyString = iosDict[@"policy"]; NSString *tag = [NSString stringWithFormat:@"com.authgear.keys.biometric.%@", kid]; LAPolicy policy = [self laPolicyFromString:policyString]; - LAContext *context = [self laContextFromPolicy:policy]; + LAContext *context = [self laContextFromPolicy:policy localizedCancelTitle:localizedCancelTitle]; [context evaluatePolicy:policy localizedReason:localizedReason reply:^(BOOL success, NSError * _Nullable laError) { dispatch_async(dispatch_get_main_queue(), ^{ @@ -456,10 +458,11 @@ + (BOOL)application:(UIApplication *)application NSDictionary *payload = options[@"payload"]; NSDictionary *iosDict = options[@"ios"]; NSString *localizedReason = iosDict[@"localizedReason"]; + NSString *localizedCancelTitle = iosDict[@"localizedCancelTitle"]; NSString *policyString = iosDict[@"policy"]; NSString *tag = [NSString stringWithFormat:@"com.authgear.keys.biometric.%@", kid]; LAPolicy policy = [self laPolicyFromString:policyString]; - LAContext *context = [self laContextFromPolicy:policy]; + LAContext *context = [self laContextFromPolicy:policy localizedCancelTitle:localizedCancelTitle]; [context evaluatePolicy:policy localizedReason:localizedReason reply:^(BOOL success, NSError * _Nullable laError) { dispatch_async(dispatch_get_main_queue(), ^{ @@ -1044,12 +1047,13 @@ -(LAPolicy)laPolicyFromString:(NSString *)str return LAPolicyDeviceOwnerAuthentication; } --(LAContext *)laContextFromPolicy:(LAPolicy)policy +-(LAContext *)laContextFromPolicy:(LAPolicy)policy localizedCancelTitle:(NSString *)localizedCancelTitle { LAContext *context = [[LAContext alloc] init]; if (policy == LAPolicyDeviceOwnerAuthenticationWithBiometrics) { context.localizedFallbackTitle = @""; } + context.localizedCancelTitle = localizedCancelTitle; return context; } diff --git a/packages/authgear-react-native/src/types.ts b/packages/authgear-react-native/src/types.ts index 10b183f6..adff859e 100644 --- a/packages/authgear-react-native/src/types.ts +++ b/packages/authgear-react-native/src/types.ts @@ -334,11 +334,18 @@ export enum BiometricAccessConstraintIOS { */ export interface BiometricOptionsIOS { /** - * See https://developer.apple.com/documentation/localauthentication/lacontext/1514176-evaluatepolicy#parameters + * See https://developer.apple.com/documentation/localauthentication/lacontext/localizedreason * * @public */ localizedReason: string; + + /** + * See https://developer.apple.com/documentation/localauthentication/lacontext/localizedcanceltitle + * + * @public + */ + localizedCancelTitle?: string; /** * Set the contraint for the authenticator to be used for biometric authentication. * @@ -362,7 +369,7 @@ export interface BiometricOptionsIOS { * * @public */ -export enum BiometricAccessConstraintAndroid { +export enum BiometricAuthenticatorAndroid { /** * The user can use Class 3 biometric to authenticate. * @@ -411,14 +418,29 @@ export interface BiometricOptionsAndroid { * @public */ negativeButtonText: string; + /** - * Set the contraint for the authenticator to be used for biometric authentication. + * Set the allowed authenticators when the user enables biometric. + * This must be a subset of allowedAuthenticatorsOnAuthenticate because + * you normally want to ensure the user performed at least once biometric authentication during setup, + * but allow the user to fallback to passcode for subsequent biometric authentication. + * + * See {@link BiometricAuthenticatorAndroid} + * + * @public + */ + allowedAuthenticatorsOnEnable: BiometricAuthenticatorAndroid[]; + /** + * Set the allowed authenticators when the user performs biometric authentication. + * This must be a superset of allowedAuthenticatorsOnEnable because + * you normally want to ensure the user performed at least once biometric authentication during setup, + * but allow the user to fallback to passcode for subsequent biometric authentication. * - * See {@link BiometricAccessConstraintAndroid} + * See {@link BiometricAuthenticatorAndroid} * * @public */ - constraint: BiometricAccessConstraintAndroid[]; + allowedAuthenticatorsOnAuthenticate: BiometricAuthenticatorAndroid[]; /** * The user needs to set up biometric again when a new biometric is enrolled or all enrolled biometrics are removed. *