@exodus/react-native-biometrics 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,33 @@
1
1
  package org.exodusmovement.rn.biometrics;
2
2
 
3
- import java.util.concurrent.Executor;
4
- import com.facebook.react.bridge.Promise;
5
- import com.facebook.react.bridge.ReactMethod;
6
- import com.facebook.react.bridge.ReactApplicationContext;
7
- import com.facebook.react.bridge.ReactContextBaseJavaModule;
8
- import com.facebook.react.module.annotations.ReactModule;
3
+ import java.util.concurrent.atomic.AtomicBoolean;
4
+
5
+ import android.app.Activity;
6
+ import android.os.Handler;
7
+ import android.os.Looper;
9
8
 
9
+ import androidx.annotation.Nullable;
10
10
  import androidx.biometric.BiometricPrompt;
11
11
  import androidx.biometric.BiometricManager;
12
12
  import androidx.core.content.ContextCompat;
13
13
  import androidx.fragment.app.FragmentActivity;
14
14
 
15
+ import com.facebook.react.bridge.Promise;
16
+ import com.facebook.react.bridge.ReactMethod;
17
+ import com.facebook.react.bridge.ReactApplicationContext;
18
+ import com.facebook.react.bridge.ReactContextBaseJavaModule;
19
+ import com.facebook.react.module.annotations.ReactModule;
20
+
15
21
  @ReactModule(name = RNBiometricsModule.NAME)
16
22
  public final class RNBiometricsModule extends ReactContextBaseJavaModule {
17
23
  public static final String NAME = "Biometrics";
18
24
 
19
25
  private final ReactApplicationContext context;
20
26
 
27
+ @Nullable private BiometricPrompt activePrompt;
28
+ @Nullable private Promise activePromise;
29
+ @Nullable private AtomicBoolean activeSettled;
30
+
21
31
  public RNBiometricsModule(ReactApplicationContext context) {
22
32
  super(context);
23
33
  this.context = context;
@@ -40,40 +50,120 @@ public final class RNBiometricsModule extends ReactContextBaseJavaModule {
40
50
  }
41
51
 
42
52
  @ReactMethod
43
- public void authenticate(String title, String subtitle, String cancelButtonText, Promise promise) {
44
- context.getCurrentActivity().runOnUiThread(new Runnable() {
53
+ public void authenticate(final String title, final String subtitle, final String cancelButtonText, final boolean fallbackToPasscode, final Promise promise) {
54
+ final Activity activity = context.getCurrentActivity();
55
+ if (!(activity instanceof FragmentActivity)) {
56
+ promise.reject("E_NO_ACTIVITY", "No FragmentActivity available to host BiometricPrompt");
57
+ return;
58
+ }
59
+
60
+ final FragmentActivity fragmentActivity = (FragmentActivity) activity;
61
+
62
+ fragmentActivity.runOnUiThread(new Runnable() {
45
63
  @Override
46
64
  public void run() {
47
- try {
48
- new BiometricPrompt(
49
- (FragmentActivity) context.getCurrentActivity(),
50
- ContextCompat.getMainExecutor(context),
51
- new BiometricPrompt.AuthenticationCallback() {
52
- @Override
53
- public void onAuthenticationError(int errorCode, CharSequence errString) {
54
- super.onAuthenticationError(errorCode, errString);
55
- promise.resolve(false);
56
- }
57
-
58
- @Override
59
- public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
60
- super.onAuthenticationSucceeded(result);
61
- promise.resolve(true);
62
- }
65
+ cancelInternal();
66
+
67
+ final AtomicBoolean settled = new AtomicBoolean(false);
68
+
69
+ BiometricPrompt.AuthenticationCallback callback = new BiometricPrompt.AuthenticationCallback() {
70
+ @Override
71
+ public void onAuthenticationError(int errorCode, CharSequence errString) {
72
+ super.onAuthenticationError(errorCode, errString);
73
+ if (settled.compareAndSet(false, true)) {
74
+ clearSlotIfMatches(promise);
75
+ promise.resolve(false);
76
+ }
77
+ }
78
+
79
+ @Override
80
+ public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
81
+ super.onAuthenticationSucceeded(result);
82
+ if (settled.compareAndSet(false, true)) {
83
+ clearSlotIfMatches(promise);
84
+ promise.resolve(true);
63
85
  }
64
- ).authenticate(
65
- new BiometricPrompt.PromptInfo.Builder()
66
- .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
86
+ }
87
+ };
88
+
89
+ try {
90
+ int authenticators = fallbackToPasscode
91
+ ? BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL
92
+ : BiometricManager.Authenticators.BIOMETRIC_STRONG;
93
+
94
+ BiometricPrompt.PromptInfo.Builder promptInfoBuilder = new BiometricPrompt.PromptInfo.Builder()
95
+ .setAllowedAuthenticators(authenticators)
67
96
  .setTitle(title)
68
- .setSubtitle(subtitle)
69
- .setNegativeButtonText(cancelButtonText)
70
- .build()
97
+ .setSubtitle(subtitle);
98
+
99
+ if (!fallbackToPasscode) {
100
+ promptInfoBuilder.setNegativeButtonText(cancelButtonText);
101
+ }
102
+
103
+ BiometricPrompt prompt = new BiometricPrompt(
104
+ fragmentActivity,
105
+ ContextCompat.getMainExecutor(context),
106
+ callback
71
107
  );
72
108
 
109
+ activePrompt = prompt;
110
+ activePromise = promise;
111
+ activeSettled = settled;
112
+
113
+ prompt.authenticate(promptInfoBuilder.build());
73
114
  } catch (Exception e) {
74
- promise.reject(e);
115
+ if (settled.compareAndSet(false, true)) {
116
+ clearSlotIfMatches(promise);
117
+ promise.reject(e);
118
+ }
75
119
  }
76
120
  }
77
121
  });
78
122
  }
79
- }
123
+
124
+ @ReactMethod
125
+ public void cancel() {
126
+ final Runnable runnable = new Runnable() {
127
+ @Override
128
+ public void run() {
129
+ cancelInternal();
130
+ }
131
+ };
132
+
133
+ final Activity activity = context.getCurrentActivity();
134
+ if (activity != null) {
135
+ activity.runOnUiThread(runnable);
136
+ } else {
137
+ new Handler(Looper.getMainLooper()).post(runnable);
138
+ }
139
+ }
140
+
141
+ private void cancelInternal() {
142
+ BiometricPrompt prompt = activePrompt;
143
+ Promise promise = activePromise;
144
+ AtomicBoolean settled = activeSettled;
145
+
146
+ activePrompt = null;
147
+ activePromise = null;
148
+ activeSettled = null;
149
+
150
+ if (prompt != null) {
151
+ try {
152
+ prompt.cancelAuthentication();
153
+ } catch (Exception ignored) {
154
+ }
155
+ }
156
+
157
+ if (promise != null && settled != null && settled.compareAndSet(false, true)) {
158
+ promise.resolve(false);
159
+ }
160
+ }
161
+
162
+ private void clearSlotIfMatches(Promise promise) {
163
+ if (activePromise == promise) {
164
+ activePrompt = null;
165
+ activePromise = null;
166
+ activeSettled = null;
167
+ }
168
+ }
169
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/react-native-biometrics",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "React Native Biometrics",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -6,7 +6,12 @@ export default {
6
6
  isSupported: async () => {
7
7
  return AndroidBiometrics.isSupported();
8
8
  },
9
- authenticate: async ({ title, subtitle, cancelButtonText }) => {
10
- return AndroidBiometrics.authenticate(title, subtitle, cancelButtonText);
11
- }
9
+ authenticate: async ({ title, subtitle, cancelButtonText, fallbackToPasscode = false }) => {
10
+ return AndroidBiometrics.authenticate(title, subtitle, cancelButtonText, fallbackToPasscode);
11
+ },
12
+ cancel: () => {
13
+ if (typeof AndroidBiometrics?.cancel === 'function') {
14
+ AndroidBiometrics.cancel();
15
+ }
16
+ },
12
17
  }
@@ -4,5 +4,6 @@ export default {
4
4
  },
5
5
  authenticate: async () => {
6
6
  return Promise.reject('Biometrics is not supported on this platform');
7
- }
7
+ },
8
+ cancel: () => {},
8
9
  }