mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 19:28:26 +00:00
feat: enable device PIN fallback for authentication
This commit is contained in:
committed by
minibits-cash
parent
3909660529
commit
14cfb5302b
+74
-21
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import FlashMessage from "react-native-flash-message"
|
import FlashMessage from "react-native-flash-message"
|
||||||
import {
|
import {
|
||||||
SafeAreaProvider,
|
SafeAreaProvider,
|
||||||
@@ -14,9 +14,8 @@ import {KeyChain, WalletKeys} from './services'
|
|||||||
import {ErrorBoundary} from './screens/ErrorScreen/ErrorBoundary'
|
import {ErrorBoundary} from './screens/ErrorScreen/ErrorBoundary'
|
||||||
import Config from './config'
|
import Config from './config'
|
||||||
import {log} from './services'
|
import {log} from './services'
|
||||||
import AppError, { Err } from './utils/AppError'
|
import { Image, Pressable, Text as RNText, TextStyle, View } from 'react-native'
|
||||||
import { Image, TextStyle, View } from 'react-native'
|
import { colors, spacing, typography } from './theme'
|
||||||
import { spacing, typography } from './theme'
|
|
||||||
import { displayName } from '../app.json'
|
import { displayName } from '../app.json'
|
||||||
import { Text } from './components/Text'
|
import { Text } from './components/Text'
|
||||||
import useIsInternetReachable from './utils/useIsInternetReachable'
|
import useIsInternetReachable from './utils/useIsInternetReachable'
|
||||||
@@ -35,6 +34,8 @@ function App() {
|
|||||||
|
|
||||||
const [isUserAuthenticated, setIsUserAuthenticated] = useState(false)
|
const [isUserAuthenticated, setIsUserAuthenticated] = useState(false)
|
||||||
const [isDeviceAuthenticated, setIsDeviceAuthenticated] = useState(false)
|
const [isDeviceAuthenticated, setIsDeviceAuthenticated] = useState(false)
|
||||||
|
const [isAuthLocked, setIsAuthLocked] = useState(false)
|
||||||
|
const isAuthInProgressRef = useRef(false)
|
||||||
|
|
||||||
const isInternetReachable = useIsInternetReachable() // boolean | null
|
const isInternetReachable = useIsInternetReachable() // boolean | null
|
||||||
const [isNetworkChecked, setIsNetworkChecked] = useState(false)
|
const [isNetworkChecked, setIsNetworkChecked] = useState(false)
|
||||||
@@ -46,20 +47,51 @@ function App() {
|
|||||||
}
|
}
|
||||||
}, [isInternetReachable])
|
}, [isInternetReachable])
|
||||||
|
|
||||||
|
const attemptAuth = useCallback(async () => {
|
||||||
|
if (isAuthInProgressRef.current) {
|
||||||
|
log.trace('[App] attemptAuth skipped, auth already in progress')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isAuthInProgressRef.current = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const isAuthEnabled = userSettingsStore.isAuthOn
|
||||||
|
log.trace('[App] attemptAuth called')
|
||||||
|
|
||||||
|
const result = await KeyChain.authenticateOnAppStart(isAuthEnabled)
|
||||||
|
log.trace('[App] attemptAuth result:', { success: result.success, shouldExitApp: result.shouldExitApp })
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setIsUserAuthenticated(true)
|
||||||
|
setIsAuthLocked(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.shouldExitApp) {
|
||||||
|
RNExitApp.exitApp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.trace('[App] attemptAuth failed, locking app')
|
||||||
|
setIsAuthLocked(true)
|
||||||
|
} catch (e: any) {
|
||||||
|
log.error('[App] attemptAuth caught error', { message: e.message })
|
||||||
|
setIsAuthLocked(true)
|
||||||
|
} finally {
|
||||||
|
isAuthInProgressRef.current = false
|
||||||
|
}
|
||||||
|
}, [userSettingsStore])
|
||||||
|
|
||||||
// === Only safe, always-run startup logic inside useInitialRootStore ===
|
// === Only safe, always-run startup logic inside useInitialRootStore ===
|
||||||
const { rehydrated } = useInitialRootStore(() => {
|
const { rehydrated } = useInitialRootStore(() => {
|
||||||
log.trace('[App]', 'Root store rehydrated')
|
log.trace('[App]', 'Root store rehydrated')
|
||||||
|
|
||||||
// User authentication (biometrics / PIN)
|
// User authentication (biometrics / PIN)
|
||||||
if (userSettingsStore.isAuthOn) {
|
if (userSettingsStore.isAuthOn) {
|
||||||
KeyChain.authenticateOnAppStart(userSettingsStore.isAuthOn)
|
// Keep UI in locked mode until authentication succeeds.
|
||||||
.then((result) => {
|
setIsAuthLocked(true)
|
||||||
if (result.success) {
|
attemptAuth()
|
||||||
setIsUserAuthenticated(true)
|
|
||||||
} else if (result.shouldExitApp) {
|
|
||||||
RNExitApp.exitApp()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
setIsUserAuthenticated(true)
|
setIsUserAuthenticated(true)
|
||||||
}
|
}
|
||||||
@@ -73,7 +105,6 @@ function App() {
|
|||||||
relaysStore.resetStatuses()
|
relaysStore.resetStatuses()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
isInternetReachable === null ||
|
isInternetReachable === null ||
|
||||||
@@ -126,19 +157,41 @@ function App() {
|
|||||||
rehydrated
|
rehydrated
|
||||||
])
|
])
|
||||||
|
|
||||||
// Show splash screen until fully ready
|
// Splash / Locked screen
|
||||||
if (
|
if (
|
||||||
!rehydrated ||
|
!rehydrated ||
|
||||||
!isUserAuthenticated ||
|
!isUserAuthenticated ||
|
||||||
!isDeviceAuthenticated
|
!isDeviceAuthenticated ||
|
||||||
|
isAuthLocked
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary catchErrors={Config.catchErrors}>
|
<View style={{flex: 1}}>
|
||||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
<ErrorBoundary catchErrors={Config.catchErrors}>
|
||||||
<Text text={displayName} style={$title} />
|
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||||
<Image source={require('../android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png')} />
|
<Text text={displayName} style={$title} />
|
||||||
</View>
|
<Image source={require('../android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png')} />
|
||||||
</ErrorBoundary>
|
</View>
|
||||||
|
</ErrorBoundary>
|
||||||
|
{isAuthLocked && (
|
||||||
|
<View style={{alignItems: 'center', paddingHorizontal: spacing.large, paddingBottom: 60}}>
|
||||||
|
<RNText style={{color: colors.palette.neutral400, fontSize: 14, textAlign: 'center', marginBottom: spacing.large}}>
|
||||||
|
Minibits is locked. Authentication is required to continue.
|
||||||
|
</RNText>
|
||||||
|
<Pressable
|
||||||
|
style={{backgroundColor: colors.palette.accent400, paddingVertical: spacing.small, paddingHorizontal: spacing.extraLarge, borderRadius: spacing.small, marginBottom: spacing.medium, minWidth: 200, alignItems: 'center'}}
|
||||||
|
onPress={attemptAuth}
|
||||||
|
>
|
||||||
|
<RNText style={{color: 'white', fontSize: 16, fontWeight: '600'}}>Authenticate</RNText>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable
|
||||||
|
style={{paddingVertical: spacing.small, paddingHorizontal: spacing.extraLarge, minWidth: 200, alignItems: 'center'}}
|
||||||
|
onPress={() => RNExitApp.exitApp()}
|
||||||
|
>
|
||||||
|
<RNText style={{color: colors.palette.neutral400, fontSize: 16}}>Close app</RNText>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -473,13 +473,13 @@
|
|||||||
"saveContactPrivateDesc": "Save %{nip05} to your Private contacts so you can pay faster",
|
"saveContactPrivateDesc": "Save %{nip05} to your Private contacts so you can pay faster",
|
||||||
"scanReceiveExtractFail": "Could not extract ecash token nor LNURL withdraw link to receive.",
|
"scanReceiveExtractFail": "Could not extract ecash token nor LNURL withdraw link to receive.",
|
||||||
"scanScreen_onPasteEmptyClipboard": "First copy ecash token, invoice, LNURL link or lightning address. Then paste.",
|
"scanScreen_onPasteEmptyClipboard": "First copy ecash token, invoice, LNURL link or lightning address. Then paste.",
|
||||||
"securityScreen_biometricAuth": "Biometric authentication",
|
"securityScreen_biometricAuth": "Authentication",
|
||||||
"securityScreen_biometricAuthDescription": "Require biometric authentication for Minibits to start.",
|
"securityScreen_biometricAuthDescription": "Require authentication for Minibits to start.",
|
||||||
"securityScreen_biometry": "Biometric authentication",
|
"securityScreen_biometry": "Authentication",
|
||||||
"securityScreen_biometryAvailable": "Biometric authentication is now required for Minibits to start.",
|
"securityScreen_biometryAvailable": "Authentication is now required for Minibits to start.",
|
||||||
"securityScreen_biometryNone": "You have not setup biometric authentication or your device does not support it.",
|
"securityScreen_biometryNone": "Your device does not support authentication.",
|
||||||
"securityScreen_posAuth": "POS mode authentication",
|
"securityScreen_posAuth": "POS mode authentication",
|
||||||
"securityScreen_posAuthDescription": "Require biometric authentication when leaving wallet POS mode.",
|
"securityScreen_posAuthDescription": "Require authentication when leaving wallet POS mode.",
|
||||||
"securityScreenTitle": "Security",
|
"securityScreenTitle": "Security",
|
||||||
"seedRecoveryMints": "Mints",
|
"seedRecoveryMints": "Mints",
|
||||||
"seedRecoveryScreenTitle": "Wallet recovery",
|
"seedRecoveryScreenTitle": "Wallet recovery",
|
||||||
|
|||||||
@@ -472,11 +472,13 @@
|
|||||||
"saveContactPrivateDesc": "Guarda %{nip05} en tus contactos privados para que puedas pagar más rápido",
|
"saveContactPrivateDesc": "Guarda %{nip05} en tus contactos privados para que puedas pagar más rápido",
|
||||||
"scanReceiveExtractFail": "No se pudo extraer el token ecash ni el enlace de retiro de LNURL para recibirlo.",
|
"scanReceiveExtractFail": "No se pudo extraer el token ecash ni el enlace de retiro de LNURL para recibirlo.",
|
||||||
"scanScreen_onPasteEmptyClipboard": "Primero, copia el token de ecash, la factura, el enlace LNURL o la dirección Lightning. Luego, pégalo.",
|
"scanScreen_onPasteEmptyClipboard": "Primero, copia el token de ecash, la factura, el enlace LNURL o la dirección Lightning. Luego, pégalo.",
|
||||||
"securityScreen_biometricAuth": "Autenticación biométrica",
|
"securityScreen_biometricAuth": "Autenticación",
|
||||||
"securityScreen_biometricAuthDescription": "Requerir autenticación biométrica para que Minibits comience.",
|
"securityScreen_biometricAuthDescription": "Requerir autenticación para que Minibits comience.",
|
||||||
"securityScreen_biometry": "Autenticación biométrica",
|
"securityScreen_biometry": "Autenticación",
|
||||||
"securityScreen_biometryAvailable": "Ahora se requiere autenticación biométrica para iniciar Minibits.",
|
"securityScreen_biometryAvailable": "Ahora se requiere autenticación para iniciar Minibits.",
|
||||||
"securityScreen_biometryNone": "No ha configurado la autenticación biométrica o su dispositivo no la admite.",
|
"securityScreen_biometryNone": "Su dispositivo no admite autenticación.",
|
||||||
|
"securityScreen_posAuth": "Autenticación de modo POS",
|
||||||
|
"securityScreen_posAuthDescription": "Requerir autenticación al salir del modo POS.",
|
||||||
"securityScreenTitle": "Seguridad",
|
"securityScreenTitle": "Seguridad",
|
||||||
"seedRecoveryMints": "Mentas",
|
"seedRecoveryMints": "Mentas",
|
||||||
"seedRecoveryScreenTitle": "Recuperación de billetera",
|
"seedRecoveryScreenTitle": "Recuperación de billetera",
|
||||||
|
|||||||
@@ -473,11 +473,13 @@
|
|||||||
"saveContactPrivateDesc": "Salve %{nip05} em seus contatos privados para pagar mais rápido",
|
"saveContactPrivateDesc": "Salve %{nip05} em seus contatos privados para pagar mais rápido",
|
||||||
"scanReceiveExtractFail": "Não foi possível extrair token ecash nem link de saque LNURL para receber.",
|
"scanReceiveExtractFail": "Não foi possível extrair token ecash nem link de saque LNURL para receber.",
|
||||||
"scanScreen_onPasteEmptyClipboard": "Primeiro copie token ecash, invoice, link LNURL ou endereço Lightning. Depois cole.",
|
"scanScreen_onPasteEmptyClipboard": "Primeiro copie token ecash, invoice, link LNURL ou endereço Lightning. Depois cole.",
|
||||||
"securityScreen_biometricAuth": "Autenticação biométrica",
|
"securityScreen_biometricAuth": "Autenticação",
|
||||||
"securityScreen_biometricAuthDescription": "Exija autenticação biométrica para iniciar Minibits.",
|
"securityScreen_biometricAuthDescription": "Exija autenticação para iniciar Minibits.",
|
||||||
"securityScreen_biometry": "Autenticação biométrica",
|
"securityScreen_biometry": "Autenticação",
|
||||||
"securityScreen_biometryAvailable": "Autenticação biométrica agora é necessária para iniciar Minibits.",
|
"securityScreen_biometryAvailable": "Autenticação agora é necessária para iniciar Minibits.",
|
||||||
"securityScreen_biometryNone": "Você não configurou autenticação biométrica ou seu dispositivo não suporta.",
|
"securityScreen_biometryNone": "Seu dispositivo não suporta autenticação.",
|
||||||
|
"securityScreen_posAuth": "Autenticação do modo POS",
|
||||||
|
"securityScreen_posAuthDescription": "Exigir autenticação ao sair do modo POS.",
|
||||||
"securityScreenTitle": "Segurança",
|
"securityScreenTitle": "Segurança",
|
||||||
"seedRecoveryMints": "Mints",
|
"seedRecoveryMints": "Mints",
|
||||||
"seedRecoveryScreenTitle": "Recuperação de carteira",
|
"seedRecoveryScreenTitle": "Recuperação de carteira",
|
||||||
|
|||||||
@@ -473,11 +473,13 @@
|
|||||||
"saveContactPrivateDesc": "Ulož %{nip05} do svojich súkromných kontaktov a plať rýchlejšie.",
|
"saveContactPrivateDesc": "Ulož %{nip05} do svojich súkromných kontaktov a plať rýchlejšie.",
|
||||||
"scanReceiveExtractFail": "Nenašiel sa ecash token ani LNURL výber na prijatie.",
|
"scanReceiveExtractFail": "Nenašiel sa ecash token ani LNURL výber na prijatie.",
|
||||||
"scanScreen_onPasteEmptyClipboard": "Najprv skopíruj ecash token, invoice, LNURL kód, alebo lightning adresu. Potom vlož.",
|
"scanScreen_onPasteEmptyClipboard": "Najprv skopíruj ecash token, invoice, LNURL kód, alebo lightning adresu. Potom vlož.",
|
||||||
"securityScreen_biometricAuth": "Biometrická autentifikácia",
|
"securityScreen_biometricAuth": "Autentifikácia",
|
||||||
"securityScreen_biometricAuthDescription": "Vyžaduj biometrickú autentifikáciu na vstup do Minibits.",
|
"securityScreen_biometricAuthDescription": "Vyžaduj autentifikáciu na vstup do Minibits.",
|
||||||
"securityScreen_biometry": "Biometrická autentifikácia",
|
"securityScreen_biometry": "Autentifikácia",
|
||||||
"securityScreen_biometryAvailable": "Biometrická autentifikácia je potrebná na vstup do Minibits.",
|
"securityScreen_biometryAvailable": "Autentifikácia je potrebná na vstup do Minibits.",
|
||||||
"securityScreen_biometryNone": "Nenastavil si biometrickú autentifikáciu, alebo ju tvoj mobil nepodporuje.",
|
"securityScreen_biometryNone": "Tvoj mobil nepodporuje autentifikáciu.",
|
||||||
|
"securityScreen_posAuth": "POS autentifikácia",
|
||||||
|
"securityScreen_posAuthDescription": "Vyžaduj autentifikáciu pri opúšťaní POS režimu.",
|
||||||
"securityScreenTitle": "Bezpečnosť",
|
"securityScreenTitle": "Bezpečnosť",
|
||||||
"seedRecoveryMints": "Minty",
|
"seedRecoveryMints": "Minty",
|
||||||
"seedRecoveryScreenTitle": "Obnova peňaženky",
|
"seedRecoveryScreenTitle": "Obnova peňaženky",
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import React, {FC, useEffect, useState} from 'react'
|
import React, {useState} from 'react'
|
||||||
import {ScrollView, Switch, TextStyle, View, ViewStyle} from 'react-native'
|
import {ScrollView, Switch, TextStyle, View, ViewStyle} from 'react-native'
|
||||||
import {colors, spacing, useThemeColor} from '../theme'
|
import {colors, spacing, useThemeColor} from '../theme'
|
||||||
import {
|
import {
|
||||||
Icon,
|
|
||||||
ListItem,
|
ListItem,
|
||||||
Screen,
|
Screen,
|
||||||
Text,
|
Text,
|
||||||
Card,
|
Card,
|
||||||
Loading,
|
Loading,
|
||||||
ErrorModal,
|
ErrorModal,
|
||||||
InfoModal,
|
|
||||||
BottomModal,
|
BottomModal,
|
||||||
PosIcon,
|
PosIcon,
|
||||||
} from '../components'
|
} from '../components'
|
||||||
@@ -19,9 +17,6 @@ import {useHeader} from '../utils/useHeader'
|
|||||||
import {useStores} from '../models'
|
import {useStores} from '../models'
|
||||||
import AppError from '../utils/AppError'
|
import AppError from '../utils/AppError'
|
||||||
import {ResultModalInfo} from './Wallet/ResultModalInfo'
|
import {ResultModalInfo} from './Wallet/ResultModalInfo'
|
||||||
import { KeyChain } from '../services'
|
|
||||||
import { BIOMETRY_TYPE } from 'react-native-keychain'
|
|
||||||
import { log } from '../services/logService'
|
|
||||||
import { StaticScreenProps, useNavigation } from '@react-navigation/native'
|
import { StaticScreenProps, useNavigation } from '@react-navigation/native'
|
||||||
|
|
||||||
type Props = StaticScreenProps<undefined>
|
type Props = StaticScreenProps<undefined>
|
||||||
@@ -34,41 +29,18 @@ export const SecurityScreen = observer(function SecurityScreen({ route }: Props)
|
|||||||
})
|
})
|
||||||
|
|
||||||
const {userSettingsStore} = useStores()
|
const {userSettingsStore} = useStores()
|
||||||
const [info, setInfo] = useState('')
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [isBiometricAuthOn, setIsBiometricAuthOn] = useState<boolean>(
|
const [isBiometricAuthOn, setIsBiometricAuthOn] = useState<boolean>(
|
||||||
userSettingsStore.isAuthOn,
|
userSettingsStore.isAuthOn,
|
||||||
)
|
)
|
||||||
const [biometryType, setBiometryType] = useState<BIOMETRY_TYPE | null>(null)
|
|
||||||
const [error, setError] = useState<AppError | undefined>()
|
const [error, setError] = useState<AppError | undefined>()
|
||||||
const [isAuthModalVisible, setIsAuthModalVisible] = useState<boolean>(false)
|
const [isAuthModalVisible, setIsAuthModalVisible] = useState<boolean>(false)
|
||||||
const [resultMessage, setResultMessage] = useState<string>()
|
const [resultMessage, setResultMessage] = useState<string>()
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const getBiometry = async () => {
|
|
||||||
const biometry: BIOMETRY_TYPE | null = await KeyChain.getSupportedBiometryType()
|
|
||||||
log.trace('[getBiometry]', {biometry, isAuthOn: userSettingsStore.isAuthOn})
|
|
||||||
setBiometryType(biometry)
|
|
||||||
}
|
|
||||||
|
|
||||||
getBiometry()
|
|
||||||
return () => {}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
|
|
||||||
const toggleBiometricAuthSwitch = async () => {
|
const toggleBiometricAuthSwitch = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
// check device has biometric support - disabled for testing
|
|
||||||
if(!isBiometricAuthOn) {
|
|
||||||
const biometryType = await KeyChain.getSupportedBiometryType()
|
|
||||||
|
|
||||||
if(!biometryType) {
|
|
||||||
setInfo('Your device does not support any biometric authentication method.')
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await userSettingsStore.setIsAuthOn(
|
const result = await userSettingsStore.setIsAuthOn(
|
||||||
!isBiometricAuthOn,
|
!isBiometricAuthOn,
|
||||||
@@ -78,13 +50,13 @@ export const SecurityScreen = observer(function SecurityScreen({ route }: Props)
|
|||||||
|
|
||||||
if (result === true) {
|
if (result === true) {
|
||||||
setResultMessage(
|
setResultMessage(
|
||||||
'Biometric authentication to access the wallet has been turned on.',
|
'Authentication to access the wallet has been turned on.',
|
||||||
)
|
)
|
||||||
toggleAuthModal()
|
toggleAuthModal()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setResultMessage('Biometric authentication has been disabled.')
|
setResultMessage('Authentication has been disabled.')
|
||||||
toggleAuthModal()
|
toggleAuthModal()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
handleError(e)
|
handleError(e)
|
||||||
@@ -233,7 +205,6 @@ export const SecurityScreen = observer(function SecurityScreen({ route }: Props)
|
|||||||
onBackdropPress={toggleAuthModal}
|
onBackdropPress={toggleAuthModal}
|
||||||
/>
|
/>
|
||||||
{error && <ErrorModal error={error} />}
|
{error && <ErrorModal error={error} />}
|
||||||
{info && <InfoModal message={info} />}
|
|
||||||
</Screen>
|
</Screen>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
+63
-29
@@ -344,7 +344,8 @@ const saveAuthToken = async function (
|
|||||||
const getAuthToken = async function (isAuthOn: boolean): Promise<string | undefined> {
|
const getAuthToken = async function (isAuthOn: boolean): Promise<string | undefined> {
|
||||||
try {
|
try {
|
||||||
const result = await _Keychain.getGenericPassword({
|
const result = await _Keychain.getGenericPassword({
|
||||||
service: KeyChainServiceName.BIOMETRIC_AUTH,
|
service: KeyChainServiceName.BIOMETRIC_AUTH,
|
||||||
|
accessControl: isAuthOn ? _Keychain.ACCESS_CONTROL.BIOMETRY_ANY_OR_DEVICE_PASSCODE : undefined,
|
||||||
authenticationPrompt: isAuthOn ? {
|
authenticationPrompt: isAuthOn ? {
|
||||||
title: 'Please authenticate',
|
title: 'Please authenticate',
|
||||||
subtitle: '',
|
subtitle: '',
|
||||||
@@ -391,6 +392,39 @@ export type AuthResult = {
|
|||||||
shouldExitApp: boolean
|
shouldExitApp: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getAuthErrorContext = (e: any) => {
|
||||||
|
const codeCandidates = [
|
||||||
|
e?.code,
|
||||||
|
e?.params?.code,
|
||||||
|
e?.params?.error?.code,
|
||||||
|
e?.params?.cause?.code,
|
||||||
|
]
|
||||||
|
.filter((value) => value !== undefined && value !== null)
|
||||||
|
.map((value) => String(value))
|
||||||
|
|
||||||
|
const serialized = `${e?.message ?? ''} ${JSON.stringify(e)}`
|
||||||
|
|
||||||
|
const isBackPressed =
|
||||||
|
codeCandidates.includes('10') ||
|
||||||
|
/"code"\s*:\s*10|code:\s*10/.test(serialized)
|
||||||
|
|
||||||
|
const isCancelPressed =
|
||||||
|
codeCandidates.includes('13') ||
|
||||||
|
/"code"\s*:\s*13|code:\s*13/.test(serialized)
|
||||||
|
|
||||||
|
const isIOSCancel = codeCandidates.includes('-128')
|
||||||
|
const isCryptoFailed = serialized.includes('E_CRYPTO_FAILED')
|
||||||
|
|
||||||
|
return {
|
||||||
|
codeCandidates,
|
||||||
|
serialized,
|
||||||
|
isBackPressed,
|
||||||
|
isCancelPressed,
|
||||||
|
isIOSCancel,
|
||||||
|
isCryptoFailed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticate user on app start
|
* Authenticate user on app start
|
||||||
@@ -407,28 +441,32 @@ const authenticateOnAppStart = async function (isAuthOn: boolean): Promise<AuthR
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.trace('[authenticateOnAppStart]', 'No auth token found')
|
log.trace('[authenticateOnAppStart]', 'No auth token found')
|
||||||
return { success: false, shouldExitApp: false }
|
return { success: false, shouldExitApp: true }
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
// Handle specific error codes for user actions
|
// Handle specific error codes for user actions
|
||||||
if (e && typeof e === 'object') {
|
const authErrorContext = getAuthErrorContext(e)
|
||||||
const errString = JSON.stringify(e)
|
|
||||||
const isBackPressed = errString.includes('code: 10')
|
|
||||||
const isCancelPressed = errString.includes('code: 13')
|
|
||||||
const isIOSCancel = 'code' in e && String(e.code) === '-128'
|
|
||||||
|
|
||||||
if (isBackPressed) {
|
log.warn('[authenticateOnAppStart]', 'Auth error caught', {
|
||||||
log.trace('[authenticateOnAppStart]', 'User pressed back button')
|
message: e.message,
|
||||||
return { success: false, shouldExitApp: true }
|
code: e.code,
|
||||||
}
|
name: e.name,
|
||||||
|
params: e.params,
|
||||||
|
stringified: authErrorContext.serialized,
|
||||||
|
codeCandidates: authErrorContext.codeCandidates,
|
||||||
|
})
|
||||||
|
|
||||||
if (isCancelPressed || isIOSCancel) {
|
if (authErrorContext.isBackPressed) {
|
||||||
log.trace('[authenticateOnAppStart]', 'User cancelled authentication')
|
log.trace('[authenticateOnAppStart]', 'User pressed back button')
|
||||||
return { success: false, shouldExitApp: true }
|
return { success: false, shouldExitApp: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authErrorContext.isCancelPressed || authErrorContext.isIOSCancel || authErrorContext.isCryptoFailed) {
|
||||||
|
log.trace('[authenticateOnAppStart]', 'User cancelled authentication')
|
||||||
|
return { success: false, shouldExitApp: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
log.warn('[authenticateOnAppStart]', 'Authentication failed', { message: e.message })
|
log.warn('[authenticateOnAppStart]', 'Authentication failed', { message: e.message })
|
||||||
return { success: false, shouldExitApp: false }
|
return { success: false, shouldExitApp: true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,6 +495,7 @@ const authenticatePOSMode = async function (): Promise<boolean> {
|
|||||||
try {
|
try {
|
||||||
const result = await _Keychain.getGenericPassword({
|
const result = await _Keychain.getGenericPassword({
|
||||||
service: KeyChainServiceName.BIOMETRIC_AUTH,
|
service: KeyChainServiceName.BIOMETRIC_AUTH,
|
||||||
|
accessControl: _Keychain.ACCESS_CONTROL.BIOMETRY_ANY_OR_DEVICE_PASSCODE,
|
||||||
authenticationPrompt: {
|
authenticationPrompt: {
|
||||||
title: 'Authentication required',
|
title: 'Authentication required',
|
||||||
subtitle: '',
|
subtitle: '',
|
||||||
@@ -474,21 +513,16 @@ const authenticatePOSMode = async function (): Promise<boolean> {
|
|||||||
return false
|
return false
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
// Handle specific error codes for user actions
|
// Handle specific error codes for user actions
|
||||||
if (e && typeof e === 'object') {
|
const authErrorContext = getAuthErrorContext(e)
|
||||||
const errString = JSON.stringify(e)
|
|
||||||
const isBackPressed = errString.includes('code: 10')
|
|
||||||
const isCancelPressed = errString.includes('code: 13')
|
|
||||||
const isIOSCancel = 'code' in e && String(e.code) === '-128'
|
|
||||||
|
|
||||||
if (isBackPressed) {
|
if (authErrorContext.isBackPressed) {
|
||||||
log.trace('[authenticatePOSMode]', 'User pressed back button')
|
log.trace('[authenticatePOSMode]', 'User pressed back button')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCancelPressed || isIOSCancel) {
|
if (authErrorContext.isCancelPressed || authErrorContext.isIOSCancel || authErrorContext.isCryptoFailed) {
|
||||||
log.trace('[authenticatePOSMode]', 'User cancelled authentication')
|
log.trace('[authenticatePOSMode]', 'User cancelled authentication')
|
||||||
return false
|
return false
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.trace('[authenticatePOSMode]', 'Authentication failed', { message: e.message })
|
log.trace('[authenticatePOSMode]', 'Authentication failed', { message: e.message })
|
||||||
|
|||||||
Reference in New Issue
Block a user