-
-
Notifications
You must be signed in to change notification settings - Fork 191
Open
Labels
Description
Bug report
Description
- I use this plugin in a decoupled and concurrent way
- The
isAvailable
response was sent into aloadBiometricSecret
response, beingsecret
a'biometrics'
literal. I suspect this happens fromFingerprint.java#executeIsAvailable
. - I expect not to mix the responses across the different methods
Environment
- Plugin version: 5.0.1
- Android 13
Logs
// pseudocode
isAvailable();
isAvailable().then(() => loadBiometricSecret()).then(console.log) // sometimes logs the secret, sometimes logs 'biometric'. Maybe such 'biometric' literal comes from the previously `isAvailable` request, that resolved after this second call.

Proposal
To have different channels to resolve each type of response.
Mitigation
Having a lock for each kind of request, aiming to:
- Debounce the requests of the same method
- Queue the requests of different methods
- It should ideally differentiate also the arguments, that's not in this proposal
This is what I have, if it helps:
type IsAvailableOptions = Readonly<{
allowBackup: boolean;
}>;
type ShowOptions = Readonly<{
title?: string;
subtitle?: string;
description?: string;
fallbackButtonTitle?: string;
disableBackup?: boolean;
cancelButtonTitle?: string;
confirmationRequired?: boolean;
}>;
type RegisterBiometricSecretOptions = Readonly<{
title?: string;
subtitle?: string;
description?: string;
fallbackButtonTitle?: string;
disableBackup?: boolean;
cancelButtonTitle?: string;
confirmationRequired?: boolean;
secret: string;
invalidateOnEnrollment?: boolean;
}>;
type LoadBiometricSecretOptions = Readonly<{
title?: string;
subtitle?: string;
description?: string;
fallbackButtonTitle?: string;
disableBackup?: boolean;
cancelButtonTitle?: string;
confirmationRequired?: boolean;
}>;
type Fingerprint = Readonly<{
isAvailable: (
onSuccess: (available: boolean) => void,
onError?: (error: unknown) => void,
options?: IsAvailableOptions
) => void;
show: (
options: ShowOptions,
onSuccess: () => void,
onError?: (error: unknown) => void
) => void;
registerBiometricSecret: (
options: RegisterBiometricSecretOptions,
onSuccess: () => void,
onError?: (error: unknown) => void
) => void;
loadBiometricSecret: (
options: LoadBiometricSecretOptions,
onSuccess: (secret: string) => void,
onError?: (error: unknown) => void
) => void;
BIOMETRIC_UNKNOWN_ERROR: -100;
BIOMETRIC_UNAVAILABLE: -101;
BIOMETRIC_AUTHENTICATION_FAILED: -102;
BIOMETRIC_SDK_NOT_SUPPORTED: -103;
BIOMETRIC_HARDWARE_NOT_SUPPORTED: -104;
BIOMETRIC_PERMISSION_NOT_GRANTED: -105;
BIOMETRIC_NOT_ENROLLED: -106;
BIOMETRIC_INTERNAL_PLUGIN_ERROR: -107;
BIOMETRIC_DISMISSED: -108;
BIOMETRIC_PIN_OR_PATTERN_DISMISSED: -109;
BIOMETRIC_SCREEN_GUARD_UNSECURED: -110;
BIOMETRIC_LOCKED_OUT: -111;
BIOMETRIC_LOCKED_OUT_PERMANENT: -112;
BIOMETRIC_NO_SECRET_FOUND: -113;
}>;
declare global {
interface Window {
readonly Fingerprint?: Fingerprint;
}
}
let isAvailablePromise: Promise<boolean>;
let registerPromise: Promise<void>;
let loadPromise: Promise<string | null>;
const waitForOngoingPromise = () =>
new Promise<void>(resolve =>
(
isAvailablePromise ??
registerPromise ??
loadPromise ??
Promise.resolve()
).finally(resolve)
);
export const isAvailable = async (options?: IsAvailableOptions) => {
await waitForOngoingPromise();
if (!isAvailablePromise) {
isAvailablePromise = new Promise<boolean>(resolve => {
if (!window.Fingerprint) {
resolve(false);
return;
}
window.Fingerprint.isAvailable(
resolve,
() => resolve(false),
options
);
});
}
return isAvailablePromise;
};
const getNotAvailableError = () =>
new Error('Fingerprint service not available');
const isDismissedError = (error: any) =>
window.Fingerprint &&
error?.code === window.Fingerprint.BIOMETRIC_DISMISSED;
export const registerBiometricSecret = async (
options: RegisterBiometricSecretOptions
) => {
await waitForOngoingPromise();
if (!registerPromise) {
registerPromise = new Promise<void>((resolve, reject) => {
if (!window.Fingerprint) {
reject(getNotAvailableError());
return;
}
window.Fingerprint.registerBiometricSecret(
options,
resolve,
error => (isDismissedError(error) ? resolve() : reject(error))
);
});
}
return registerPromise;
};
export const loadBiometricSecret = async (
options?: LoadBiometricSecretOptions
) => {
await waitForOngoingPromise();
if (!loadPromise) {
loadPromise = new Promise<string | null>((resolve, reject) => {
if (!window.Fingerprint) {
reject(getNotAvailableError());
return;
}
window.Fingerprint.loadBiometricSecret(
options ?? {},
resolve,
error =>
isDismissedError(error) ? resolve(null) : reject(error)
);
});
}
return loadPromise;
};