@exodus/react-native-biometrics 0.3.0 → 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,10 +50,42 @@ public final class RNBiometricsModule extends ReactContextBaseJavaModule {
40
50
  }
41
51
 
42
52
  @ReactMethod
43
- public void authenticate(String title, String subtitle, String cancelButtonText, boolean fallbackToPasscode, 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() {
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);
85
+ }
86
+ }
87
+ };
88
+
47
89
  try {
48
90
  int authenticators = fallbackToPasscode
49
91
  ? BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL
@@ -54,33 +96,74 @@ public final class RNBiometricsModule extends ReactContextBaseJavaModule {
54
96
  .setTitle(title)
55
97
  .setSubtitle(subtitle);
56
98
 
57
- // Negative button text cannot be set when DEVICE_CREDENTIAL is allowed.
58
99
  if (!fallbackToPasscode) {
59
100
  promptInfoBuilder.setNegativeButtonText(cancelButtonText);
60
101
  }
61
102
 
62
- new BiometricPrompt(
63
- (FragmentActivity) context.getCurrentActivity(),
103
+ BiometricPrompt prompt = new BiometricPrompt(
104
+ fragmentActivity,
64
105
  ContextCompat.getMainExecutor(context),
65
- new BiometricPrompt.AuthenticationCallback() {
66
- @Override
67
- public void onAuthenticationError(int errorCode, CharSequence errString) {
68
- super.onAuthenticationError(errorCode, errString);
69
- promise.resolve(false);
70
- }
71
-
72
- @Override
73
- public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
74
- super.onAuthenticationSucceeded(result);
75
- promise.resolve(true);
76
- }
77
- }
78
- ).authenticate(promptInfoBuilder.build());
106
+ callback
107
+ );
108
+
109
+ activePrompt = prompt;
110
+ activePromise = promise;
111
+ activeSettled = settled;
79
112
 
113
+ prompt.authenticate(promptInfoBuilder.build());
80
114
  } catch (Exception e) {
81
- promise.reject(e);
115
+ if (settled.compareAndSet(false, true)) {
116
+ clearSlotIfMatches(promise);
117
+ promise.reject(e);
118
+ }
82
119
  }
83
120
  }
84
121
  });
85
122
  }
86
- }
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.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "React Native Biometrics",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -8,5 +8,10 @@ export default {
8
8
  },
9
9
  authenticate: async ({ title, subtitle, cancelButtonText, fallbackToPasscode = false }) => {
10
10
  return AndroidBiometrics.authenticate(title, subtitle, cancelButtonText, fallbackToPasscode);
11
- }
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
  }