Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: improve biometrics permission prompting. #1301

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ariesbifold">

<uses-permission android:name="android.permission.USE_BIOMETRICS" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ const ToggleButton: React.FC<ToggleButtonProps> = ({
testID={testID}
accessibilityLabel="Toggle Button"
accessibilityRole="switch"
accessibilityState={{
checked: isEnabled
}}
onPress={isAvailable && !disabled ? toggleAction : undefined} // Prevent onPress if not available or disabled
disabled={!isAvailable || disabled}
>
Expand Down
7 changes: 6 additions & 1 deletion packages/legacy/core/App/localization/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,12 @@ const translation = {
"Warning": "Ensure only you have access to your wallet.",
"UseToUnlock": "Use biometrics to unlock wallet?",
"UnlockPromptTitle": "Wallet Unlock",
"UnlockPromptDescription": "Use biometrics to unlock your wallet"
"UnlockPromptDescription": "Use biometrics to unlock your wallet",
"AllowBiometricsTitle": "Enable biometrics",
"AllowBiometricsDesc": "To unlock BC Wallet with your biometrics, please allow biometrics use within your device's settings.",
"SetupBiometricsTitle": "Biometrics is not enabled",
"SetupBiometricsDesc": "To unlock BC Wallet with your biometrics, please set up your biometrics in your device's settings.",
"OpenSettings": "Open settings"
},
"ActivityHistory": {
"Header": "Activity history",
Expand Down
7 changes: 6 additions & 1 deletion packages/legacy/core/App/localization/fr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,12 @@ const translation = {
"Warning": "\n\nAssurez-vous que vous seul avez accès à votre portefeuille.",
"UseToUnlock": "Utiliser la biométrie pour déverrouiller le portefeuille ?",
"UnlockPromptTitle": "Déverrouillage du portefeuille",
"UnlockPromptDescription": "Utilisez la biométrie pour déverrouiller votre portefeuille"
"UnlockPromptDescription": "Utilisez la biométrie pour déverrouiller votre portefeuille",
"AllowBiometricsTitle": "Activer la biométrie",
"AllowBiometricsDesc": "Pour déverrouiller BC Wallet avec votre biométrie, permettrez la biométrie dans les paramètres de votre appareil.",
"SetupBiometricsTitle": "La biométrie n'est pas activée",
"SetupBiometricsDesc": "Pour déverrouiller BC Wallet avec votre biométrie, configurez votre biométrie dans les paramètres de votre appareil.",
"OpenSettings": "Ouvrir les paramètres"
},
"ActivityHistory": {
"Header": "Activity history(fr)",
Expand Down
101 changes: 93 additions & 8 deletions packages/legacy/core/App/screens/UseBiometry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { CommonActions, useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import React, { useState, useEffect, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, Text, View, Modal, ScrollView, DeviceEventEmitter } from 'react-native'
import { StyleSheet, Text, View, Modal, ScrollView, DeviceEventEmitter, Linking, Platform } from 'react-native'
import { PERMISSIONS, RESULTS, request, check, PermissionStatus } from 'react-native-permissions'
import { SafeAreaView } from 'react-native-safe-area-context'

import Button, { ButtonType } from '../components/buttons/Button'
Expand All @@ -15,6 +16,7 @@ import { useStore } from '../contexts/store'
import { useTheme } from '../contexts/theme'
import { OnboardingStackParams, Screens } from '../types/navigators'
import { testIdWithKey } from '../utils/testable'
import DismissiblePopupModal from '../components/modals/DismissiblePopupModal'

import PINEnter, { PINEntryUsage } from './PINEnter'
import { TOKENS, useServices } from '../container-api'
Expand All @@ -32,6 +34,7 @@ const UseBiometry: React.FC = () => {
const [biometryAvailable, setBiometryAvailable] = useState(false)
const [biometryEnabled, setBiometryEnabled] = useState(store.preferences.useBiometry)
const [continueEnabled, setContinueEnabled] = useState(true)
const [settingsPopupConfig, setSettingsPopupConfig] = useState<null | {title: string, description: string}>(null)
const [canSeeCheckPIN, setCanSeeCheckPIN] = useState<boolean>(false)
const { ColorPallet, TextTheme, Assets } = useTheme()
const { ButtonLoading } = useAnimatedComponents()
Expand All @@ -40,6 +43,8 @@ const UseBiometry: React.FC = () => {
return store.onboarding.didCompleteOnboarding ? UseBiometryUsage.ToggleOnOff : UseBiometryUsage.InitialSetup
}, [store.onboarding.didCompleteOnboarding])

const BIOMETRY_PERMISSION = PERMISSIONS.IOS.FACE_ID;

const styles = StyleSheet.create({
container: {
height: '100%',
Expand Down Expand Up @@ -102,17 +107,88 @@ const UseBiometry: React.FC = () => {
}
}, [biometryEnabled, commitPIN, dispatch, enablePushNotifications, navigation])

const toggleSwitch = useCallback(() => {
// If the user is toggling biometrics on/off they need
// to first authenticate before this action is accepted
const onOpenSettingsTouched = async () => {
await Linking.openSettings()
onOpenSettingsDismissed()
}

const onOpenSettingsDismissed = () => {
setSettingsPopupConfig(null)
}

const onSwitchToggleAllowed = useCallback((newValue: boolean) => {
if (screenUsage === UseBiometryUsage.ToggleOnOff) {
setCanSeeCheckPIN(true)
DeviceEventEmitter.emit(EventTypes.BIOMETRY_UPDATE, true)
} else {
setBiometryEnabled(newValue)
}
}, [screenUsage])

const onRequestSystemBiometrics = useCallback(async (newToggleValue: boolean) => {
const permissionResult: PermissionStatus = await request(BIOMETRY_PERMISSION)
switch (permissionResult) {
case RESULTS.GRANTED:
case RESULTS.LIMITED:
// Granted
onSwitchToggleAllowed(newToggleValue)
break
default:
break
}
}, [onSwitchToggleAllowed, BIOMETRY_PERMISSION])

const onCheckSystemBiometrics = useCallback(async (): Promise<PermissionStatus> => {
if (Platform.OS === 'android') {
// Android doesn't need to prompt biometric permission
// for an app to use it.
return biometryAvailable ? RESULTS.GRANTED : RESULTS.UNAVAILABLE
} else if (Platform.OS === 'ios') {
return await check(BIOMETRY_PERMISSION)
}

return RESULTS.UNAVAILABLE
}, [biometryAvailable, BIOMETRY_PERMISSION])

const toggleSwitch = useCallback(async () => {
const newValue = !biometryEnabled

if (!newValue) {
// Turning off doesn't require OS'es biometrics enabled
onSwitchToggleAllowed(newValue)
return
}

setBiometryEnabled((previousState) => !previousState)
}, [screenUsage])
// If the user is turning it on, they need
// to first authenticate the OS'es biometrics before this action is accepted
const permissionResult: PermissionStatus = await onCheckSystemBiometrics()
switch (permissionResult) {
case RESULTS.GRANTED:
case RESULTS.LIMITED:
// Already granted
onSwitchToggleAllowed(newValue)
break
case RESULTS.UNAVAILABLE:
setSettingsPopupConfig({
title: t('Biometry.SetupBiometricsTitle'),
description: t('Biometry.SetupBiometricsDesc')
})
break
case RESULTS.BLOCKED:
// Previously denied
setSettingsPopupConfig({
title: t('Biometry.AllowBiometricsTitle'),
description: t('Biometry.AllowBiometricsDesc')
})
break
case RESULTS.DENIED:
// Has not been requested
await onRequestSystemBiometrics(newValue)
break
default:
break
}
}, [onSwitchToggleAllowed, onRequestSystemBiometrics, onCheckSystemBiometrics, biometryEnabled, t])

const onAuthenticationComplete = useCallback((status: boolean) => {
// If successfully authenticated the toggle may proceed.
Expand All @@ -125,6 +201,15 @@ const UseBiometry: React.FC = () => {

return (
<SafeAreaView edges={['left', 'right', 'bottom']}>
{settingsPopupConfig && (
<DismissiblePopupModal
title={settingsPopupConfig.title}
description={settingsPopupConfig.description}
onCallToActionLabel={t('Biometry.OpenSettings')}
onCallToActionPressed={onOpenSettingsTouched}
onDismissPressed={onOpenSettingsDismissed}
/>
)}
<ScrollView style={styles.container}>
<View style={{ alignItems: 'center' }}>
<Assets.svg.biometrics style={styles.image} />
Expand Down Expand Up @@ -153,9 +238,9 @@ const UseBiometry: React.FC = () => {
<ToggleButton
testID={testIdWithKey("ToggleBiometrics")}
isEnabled={biometryEnabled}
isAvailable={biometryAvailable}
isAvailable={true}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it ok that these are hard coded values? the diff shows biometryAvailable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it was changed so that the biometric switch is now always enabled for interaction, it will shows a popup with messages when biometryAvailable == false; instead of being disabled from interacting entirely before this change.

toggleAction={toggleSwitch}
disabled={!biometryAvailable}
disabled={false}
enabledIcon="check"
disabledIcon="close"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ exports[`ToggleButton Component renders correctly when disabled 1`] = `
accessibilityState={
Object {
"busy": undefined,
"checked": undefined,
"checked": false,
"disabled": false,
"expanded": undefined,
"selected": undefined,
Expand Down Expand Up @@ -100,7 +100,7 @@ exports[`ToggleButton Component renders correctly when enabled 1`] = `
accessibilityState={
Object {
"busy": undefined,
"checked": undefined,
"checked": true,
"disabled": false,
"expanded": undefined,
"selected": undefined,
Expand Down Expand Up @@ -193,7 +193,7 @@ exports[`ToggleButton Component renders correctly when not available 1`] = `
accessibilityState={
Object {
"busy": undefined,
"checked": undefined,
"checked": false,
"disabled": true,
"expanded": undefined,
"selected": undefined,
Expand Down
Loading
Loading