Skip to content

Commit 110a794

Browse files
committed
Fix biometric unlock state handling
1 parent e138b6b commit 110a794

22 files changed

Lines changed: 461 additions & 123 deletions

apps/easypid/src/app/authenticate.tsx

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import { TypedArrayEncoder } from '@credo-ts/core'
2-
import { useBiometricsType } from '@easypid/hooks/useBiometricsType'
32
import { useLingui } from '@lingui/react/macro'
43
import { PinDotsInput, type PinDotsInputRef } from '@package/app'
54
import { commonMessages } from '@package/translations'
65
import { FlexPage, Heading, HeroIcons, IconContainer, useDeviceMedia, useToastController, YStack } from '@package/ui'
76
import {
87
ParadymWalletAuthenticationInvalidPinError,
98
ParadymWalletBiometricAuthenticationError,
10-
useCanUseBiometryBackedWalletKey,
11-
useIsBiometricsEnabled,
9+
useBiometricUnlockState,
1210
useParadym,
1311
} from '@paradym/wallet-sdk'
1412
import { Redirect, useLocalSearchParams } from 'expo-router'
@@ -27,41 +25,63 @@ export default function Authenticate() {
2725

2826
const { redirectAfterUnlock } = useLocalSearchParams<{ redirectAfterUnlock?: string }>()
2927
const toast = useToastController()
30-
const biometricsType = useBiometricsType()
3128
const pinInputRef = useRef<PinDotsInputRef>(null)
29+
const hasAttemptedAutoBiometricsRef = useRef(false)
3230
const { additionalPadding, noBottomSafeArea } = useDeviceMedia()
3331
const [isInitializingAgent, setIsInitializingAgent] = useState(false)
34-
const [isAllowedToUnlockWithFaceId, setIsAllowedToUnlockWithFaceId] = useState(false)
35-
const [isBiometricsEnabled] = useIsBiometricsEnabled()
36-
const canUseBiometryBackedWalletKey = useCanUseBiometryBackedWalletKey()
32+
const [isAllowedToAutoPromptBiometrics, setIsAllowedToAutoPromptBiometrics] = useState(false)
33+
const biometricUnlockState = useBiometricUnlockState()
3734
const [shouldPromptBiometrics, setShouldPromptBiometrics] = useState(true)
38-
39-
const isLoading = paradym.state === 'locked' && paradym.isUnlocking
35+
const biometricsType =
36+
biometricUnlockState.data?.biometryType?.toLowerCase().includes('face') ||
37+
biometricUnlockState.data?.biometryType?.toLowerCase().includes('optic')
38+
? 'face'
39+
: 'fingerprint'
40+
const showBiometricUnlockAction =
41+
biometricUnlockState.data?.canUnlockNow === true &&
42+
(paradym.state === 'locked' || (paradym.state === 'acquired-wallet-key' && paradym.unlockMethod === 'biometrics'))
43+
const canAutoPromptBiometricUnlock =
44+
biometricUnlockState.data?.canUnlockNow === true &&
45+
paradym.state === 'locked' &&
46+
paradym.canTryUnlockingUsingBiometrics
47+
48+
const isLoading =
49+
paradym.state === 'acquired-wallet-key' ||
50+
(paradym.state === 'locked' && paradym.isUnlocking) ||
51+
isInitializingAgent
4052

4153
useEffect(() => {
4254
if (paradym.state === 'unlocked' && redirectAfterUnlock) {
4355
paradym.lock()
4456
}
4557
}, [])
4658

47-
// After resetting the wallet, we want to avoid prompting for face id immediately
59+
// After resetting the wallet, we want to avoid prompting biometrics immediately
4860
// So we add an artificial delay
4961
useEffect(() => {
50-
const timer = setTimeout(() => setIsAllowedToUnlockWithFaceId(true), 500)
62+
const timer = setTimeout(() => setIsAllowedToAutoPromptBiometrics(true), 500)
5163

5264
return () => clearTimeout(timer)
5365
}, [])
5466

5567
useEffect(() => {
68+
if (paradym.state !== 'locked') {
69+
hasAttemptedAutoBiometricsRef.current = false
70+
return
71+
}
72+
5673
if (
57-
paradym.state === 'locked' &&
58-
paradym.canTryUnlockingUsingBiometrics &&
59-
isAllowedToUnlockWithFaceId &&
60-
shouldPromptBiometrics
74+
!canAutoPromptBiometricUnlock ||
75+
!isAllowedToAutoPromptBiometrics ||
76+
!shouldPromptBiometrics ||
77+
hasAttemptedAutoBiometricsRef.current
6178
) {
62-
paradym.tryUnlockingUsingBiometrics()
79+
return
6380
}
64-
}, [paradym.state, isAllowedToUnlockWithFaceId])
81+
82+
hasAttemptedAutoBiometricsRef.current = true
83+
void paradym.tryUnlockingUsingBiometrics()
84+
}, [canAutoPromptBiometricUnlock, isAllowedToAutoPromptBiometrics, paradym, paradym.state, shouldPromptBiometrics])
6585

6686
useEffect(() => {
6787
if (isInitializingAgent || paradym.state !== 'acquired-wallet-key') return
@@ -102,6 +122,8 @@ export default function Authenticate() {
102122

103123
const unlockUsingBiometrics = async () => {
104124
if (paradym.state === 'locked') {
125+
hasAttemptedAutoBiometricsRef.current = true
126+
setShouldPromptBiometrics(false)
105127
await paradym.tryUnlockingUsingBiometrics()
106128
} else {
107129
toast.show(t({ id: 'authenticate.pinRequiredToast', message: 'Your PIN is required to unlock the app' }), {
@@ -131,7 +153,7 @@ export default function Authenticate() {
131153
ref={pinInputRef}
132154
pinLength={6}
133155
onPinComplete={unlockUsingPin}
134-
onBiometricsTap={isBiometricsEnabled && canUseBiometryBackedWalletKey ? unlockUsingBiometrics : undefined}
156+
onBiometricsTap={showBiometricUnlockAction ? unlockUsingBiometrics : undefined}
135157
useNativeKeyboard={false}
136158
biometricsType={biometricsType ?? 'fingerprint'}
137159
/>

apps/easypid/src/features/menu/FunkeSettingsScreen.tsx

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,13 @@ import {
2323
ParadymWalletBiometricAuthenticationCancelledError,
2424
ParadymWalletBiometricAuthenticationNotEnabledError,
2525
ParadymWalletSdkConsoleLogger,
26-
useCanUseBiometryBackedWalletKey,
27-
useIsBiometricsEnabled,
26+
useBiometricUnlockState,
2827
useParadym,
2928
} from '@paradym/wallet-sdk'
3029
import { Picker } from '@react-native-picker/picker'
3130
import { useState } from 'react'
3231
import { ScrollView, Share } from 'react-native'
3332
import { Label } from 'tamagui'
34-
import { useBiometricsType } from '../../hooks/useBiometricsType'
3533
import { useDevelopmentMode } from '../../hooks/useDevelopmentMode'
3634
import { useStoredLocale } from '../../hooks/useStoredLocale'
3735

@@ -89,8 +87,32 @@ export function FunkeSettingsScreen() {
8987
const toast = useToastController()
9088
const { handleScroll, isScrolledByOffset, scrollEventThrottle } = useScrollViewPosition()
9189
const [isDevelopmentModeEnabled, setIsDevelopmentModeEnabled] = useDevelopmentMode()
92-
93-
const [isBiometricsEnabled] = useIsBiometricsEnabled()
90+
const biometricUnlockState = useBiometricUnlockState()
91+
const isBiometricsConfigured = biometricUnlockState.data?.configured ?? false
92+
const isBiometricsCapable = biometricUnlockState.data?.capable ?? false
93+
const biometricsType =
94+
biometricUnlockState.data?.biometryType?.toLowerCase().includes('face') ||
95+
biometricUnlockState.data?.biometryType?.toLowerCase().includes('optic')
96+
? 'face'
97+
: 'fingerprint'
98+
const biometricsDescription =
99+
biometricUnlockState.data == null
100+
? undefined
101+
: !isBiometricsConfigured && !isBiometricsCapable
102+
? t({
103+
id: 'settings.biometricsNotSupportedDescription',
104+
message: 'Biometric authentication is disabled or not supported on this device.',
105+
comment: 'Description that the biometric unlock feature is not supported on this device',
106+
})
107+
: isBiometricsConfigured && !isBiometricsCapable
108+
? t({
109+
id: 'settings.biometricsCurrentlyUnavailableDescription',
110+
message:
111+
'Biometric unlock is configured for this wallet, but currently unavailable on this device. You can turn it off here and set it up again later.',
112+
comment:
113+
'Description shown when biometric unlock is configured, but the device can no longer use it right now.',
114+
})
115+
: undefined
94116

95117
async function enableBiometrics() {
96118
try {
@@ -144,9 +166,6 @@ export function FunkeSettingsScreen() {
144166
}
145167
}
146168

147-
const canUseBiometryBackedWalletKey = useCanUseBiometryBackedWalletKey()
148-
const biometricsType = useBiometricsType()
149-
150169
return (
151170
<FlexPage gap="$0" paddingHorizontal="$0">
152171
<HeaderContainer
@@ -172,18 +191,10 @@ export function FunkeSettingsScreen() {
172191
comment: 'Label for the toggle to enable biometric unlock',
173192
})}
174193
icon={biometricsType === 'face' ? <CustomIcons.FaceId /> : <HeroIcons.FingerPrint />}
175-
disabled={canUseBiometryBackedWalletKey === false}
176-
description={
177-
canUseBiometryBackedWalletKey === false
178-
? t({
179-
id: 'settings.biometricsNotSupportedDescription',
180-
message: 'Biometric authentication is disabled or not supported on this device.',
181-
comment: 'Description that the biometric unlock feature is not supported on this device',
182-
})
183-
: undefined
184-
}
185-
value={isBiometricsEnabled}
186-
onChange={isBiometricsEnabled ? disableBiometrics : enableBiometrics}
194+
disabled={biometricUnlockState.data == null || (!isBiometricsConfigured && !isBiometricsCapable)}
195+
description={biometricsDescription}
196+
value={isBiometricsConfigured}
197+
onChange={isBiometricsConfigured ? disableBiometrics : enableBiometrics}
187198
/>
188199
<Switch
189200
id="development-mode"

apps/easypid/src/features/onboarding/onboardingContext.tsx

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useHaptics } from '@package/app'
66
import {
77
ParadymWalletBiometricAuthenticationCancelledError,
88
ParadymWalletBiometricAuthenticationNotEnabledError,
9+
secureWalletKey,
910
useParadym,
1011
} from '@package/sdk'
1112
import { commonMessages } from '@package/translations'
@@ -93,7 +94,7 @@ export function OnboardingContextProvider({
9394

9495
const onPinEnter = async (pin: string) => {
9596
setWalletPin(pin)
96-
goToNextStep()
97+
await goToNextStep()
9798
}
9899

99100
const onPinReEnter = async (pin: string) => {
@@ -128,7 +129,13 @@ export function OnboardingContextProvider({
128129
try {
129130
await paradym.setPin(walletPin as string)
130131
await setWalletServiceProviderPin((walletPin as string).split('').map(Number), false)
131-
goToNextStep()
132+
const biometricUnlockState = await secureWalletKey.getBiometricUnlockState(secureWalletKey.getWalletKeyVersion())
133+
134+
if (biometricUnlockState.capable) {
135+
await goToNextStep()
136+
} else {
137+
setCurrentStepName('data-protection')
138+
}
132139
} catch (e) {
133140
reset({ error: e, resetToStep: 'welcome' })
134141
throw e
@@ -139,7 +146,12 @@ export function OnboardingContextProvider({
139146
return Linking.openSettings().then(() => setCurrentStepName('biometrics'))
140147
}
141148

142-
const onEnableBiometrics = async () => {
149+
const onEnableBiometrics = async (enableBiometrics: boolean) => {
150+
if (!enableBiometrics) {
151+
await goToNextStep()
152+
return
153+
}
154+
143155
if (paradym.state !== 'acquired-wallet-key' && paradym.state !== 'unlocked') {
144156
await reset({
145157
resetToStep: 'pin',
@@ -148,12 +160,29 @@ export function OnboardingContextProvider({
148160
}
149161

150162
try {
163+
let sdk = paradym.state === 'unlocked' ? paradym.paradym : undefined
164+
151165
if (paradym.state === 'acquired-wallet-key') {
152-
const sdk = await paradym.unlock({ enableBiometrics: true })
153-
await setupWalletServiceProvider(sdk, true)
166+
sdk = await paradym.unlock({ enableBiometrics: true })
154167
}
155168

156-
goToNextStep()
169+
if (paradym.state === 'unlocked') {
170+
const biometricUnlockState = await secureWalletKey.getBiometricUnlockState(
171+
secureWalletKey.getWalletKeyVersion()
172+
)
173+
174+
if (!biometricUnlockState.configured) {
175+
await paradym.enableBiometricUnlock()
176+
}
177+
}
178+
179+
if (!sdk) {
180+
throw new Error('Wallet SDK is not available during onboarding biometric setup')
181+
}
182+
183+
await setupWalletServiceProvider(sdk, true)
184+
185+
await goToNextStep()
157186
} catch (error) {
158187
// We can recover from this, and will show an error on the screen
159188
if (error instanceof ParadymWalletBiometricAuthenticationCancelledError) {
@@ -166,9 +195,10 @@ export function OnboardingContextProvider({
166195
throw error
167196
}
168197

169-
await reset({
170-
resetToStep: 'pin',
171-
error,
198+
toast.show(t(commonMessages.errorChangingBiometrics), {
199+
customData: {
200+
preset: 'danger',
201+
},
172202
})
173203
throw error
174204
}

apps/easypid/src/locales/al/messages.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3627,5 +3627,30 @@
36273627
"comments": [],
36283628
"origin": [["src/app/authenticate.tsx", 104]],
36293629
"translation": "PIN-i yt kërkohet për të hapur aplikacionin"
3630+
},
3631+
"browserAuthFlow.authorizationCancelled": {
3632+
"message": "Authorization cancelled",
3633+
"placeholders": {},
3634+
"comments": [],
3635+
"origin": [
3636+
["src/features/receive/FunkeOpenIdCredentialNotificationScreen.tsx", 157]
3637+
],
3638+
"translation": "Autorizimi u anulua"
3639+
},
3640+
"common.invitationResolvedParameterMissing": {
3641+
"message": "Resolved parameter is missing, but required for accepting an invitation.",
3642+
"placeholders": {},
3643+
"comments": [],
3644+
"origin": [["../../packages/translations/src/commonMessages.ts", 225]],
3645+
"translation": "Parametri \"resolved\" mungon, por kërkohet për të pranuar një ftesë."
3646+
},
3647+
"settings.biometricsCurrentlyUnavailableDescription": {
3648+
"message": "Biometric unlock is configured for this wallet, but currently unavailable on this device. You can turn it off here and set it up again later.",
3649+
"placeholders": {},
3650+
"comments": [
3651+
"Description shown when biometric unlock is configured, but the device can no longer use it right now."
3652+
],
3653+
"origin": [["src/features/menu/FunkeSettingsScreen.tsx", 109]],
3654+
"translation": "Zhbllokimi biometrik është konfiguruar për këtë portofol, por aktualisht nuk është i disponueshëm në këtë pajisje. Mund ta çaktivizosh këtu dhe ta konfigurosh sërish më vonë."
36303655
}
36313656
}

apps/easypid/src/locales/al/messages.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apps/easypid/src/locales/de/messages.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3627,5 +3627,30 @@
36273627
"comments": [],
36283628
"origin": [["src/app/authenticate.tsx", 104]],
36293629
"translation": "Deine PIN ist erforderlich, um die App zu entsperren"
3630+
},
3631+
"browserAuthFlow.authorizationCancelled": {
3632+
"message": "Authorization cancelled",
3633+
"placeholders": {},
3634+
"comments": [],
3635+
"origin": [
3636+
["src/features/receive/FunkeOpenIdCredentialNotificationScreen.tsx", 157]
3637+
],
3638+
"translation": "Autorisierung abgebrochen"
3639+
},
3640+
"common.invitationResolvedParameterMissing": {
3641+
"message": "Resolved parameter is missing, but required for accepting an invitation.",
3642+
"placeholders": {},
3643+
"comments": [],
3644+
"origin": [["../../packages/translations/src/commonMessages.ts", 225]],
3645+
"translation": "Der Parameter „resolved“ fehlt, ist aber erforderlich, um eine Einladung anzunehmen."
3646+
},
3647+
"settings.biometricsCurrentlyUnavailableDescription": {
3648+
"message": "Biometric unlock is configured for this wallet, but currently unavailable on this device. You can turn it off here and set it up again later.",
3649+
"placeholders": {},
3650+
"comments": [
3651+
"Description shown when biometric unlock is configured, but the device can no longer use it right now."
3652+
],
3653+
"origin": [["src/features/menu/FunkeSettingsScreen.tsx", 109]],
3654+
"translation": "Die biometrische Entsperrung ist für dieses Wallet eingerichtet, ist auf diesem Gerät derzeit jedoch nicht verfügbar. Du kannst sie hier deaktivieren und später erneut einrichten."
36303655
}
36313656
}

apps/easypid/src/locales/de/messages.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apps/easypid/src/locales/en/messages.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3627,5 +3627,30 @@
36273627
"comments": [],
36283628
"origin": [["src/app/authenticate.tsx", 104]],
36293629
"translation": "Your PIN is required to unlock the app"
3630+
},
3631+
"browserAuthFlow.authorizationCancelled": {
3632+
"message": "Authorization cancelled",
3633+
"placeholders": {},
3634+
"comments": [],
3635+
"origin": [
3636+
["src/features/receive/FunkeOpenIdCredentialNotificationScreen.tsx", 157]
3637+
],
3638+
"translation": "Authorization cancelled"
3639+
},
3640+
"common.invitationResolvedParameterMissing": {
3641+
"message": "Resolved parameter is missing, but required for accepting an invitation.",
3642+
"placeholders": {},
3643+
"comments": [],
3644+
"origin": [["../../packages/translations/src/commonMessages.ts", 225]],
3645+
"translation": "Resolved parameter is missing, but required for accepting an invitation."
3646+
},
3647+
"settings.biometricsCurrentlyUnavailableDescription": {
3648+
"message": "Biometric unlock is configured for this wallet, but currently unavailable on this device. You can turn it off here and set it up again later.",
3649+
"placeholders": {},
3650+
"comments": [
3651+
"Description shown when biometric unlock is configured, but the device can no longer use it right now."
3652+
],
3653+
"origin": [["src/features/menu/FunkeSettingsScreen.tsx", 109]],
3654+
"translation": "Biometric unlock is configured for this wallet, but currently unavailable on this device. You can turn it off here and set it up again later."
36303655
}
36313656
}

apps/easypid/src/locales/en/messages.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)