From 99fdaddd455f75c8b5669a09132d6fe8c0aac9d6 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 4 Nov 2024 12:39:50 +0100 Subject: [PATCH 1/9] feat: button for faceid Signed-off-by: Jan --- apps/easypid/src/app/_layout.tsx | 18 +++++++-- apps/easypid/src/app/authenticate.tsx | 29 ++++++++++++-- .../src/provider/BackgroundLockProvider.tsx | 39 +++++++++++++++++++ packages/app/src/provider/index.ts | 1 + packages/app/src/utils/DeeplinkHandler.tsx | 2 +- packages/ui/src/components/PinDotsInput.tsx | 6 ++- packages/ui/src/components/PinPad.tsx | 14 ++++--- 7 files changed, 93 insertions(+), 16 deletions(-) create mode 100644 packages/app/src/provider/BackgroundLockProvider.tsx diff --git a/apps/easypid/src/app/_layout.tsx b/apps/easypid/src/app/_layout.tsx index cf853e46..e22c4269 100644 --- a/apps/easypid/src/app/_layout.tsx +++ b/apps/easypid/src/app/_layout.tsx @@ -1,4 +1,10 @@ -import { NoInternetToastProvider, Provider, useTransparentNavigationBar } from '@package/app' +import { + BackgroundLockProvider, + DeeplinkHandler, + NoInternetToastProvider, + Provider, + useTransparentNavigationBar, +} from '@package/app' import { SecureUnlockProvider } from '@package/secure-store/secureUnlock' import { DefaultTheme, ThemeProvider } from '@react-navigation/native' import { Slot } from 'expo-router' @@ -28,9 +34,13 @@ export default function RootLayout() { }, }} > - - - + + + + + + + diff --git a/apps/easypid/src/app/authenticate.tsx b/apps/easypid/src/app/authenticate.tsx index a98c91a7..2005cfc7 100644 --- a/apps/easypid/src/app/authenticate.tsx +++ b/apps/easypid/src/app/authenticate.tsx @@ -3,9 +3,17 @@ import { Redirect } from 'expo-router' import { WalletInvalidKeyError } from '@credo-ts/core' import { initializeAppAgent, useSecureUnlock } from '@easypid/agent' import { secureWalletKey } from '@package/secure-store/secureUnlock' -import { FlexPage, Heading, HeroIcons, PinDotsInput, type PinDotsInputRef, YStack } from '@package/ui' +import { + FlexPage, + Heading, + HeroIcons, + PinDotsInput, + type PinDotsInputRef, + YStack, + useToastController, +} from '@package/ui' import * as SplashScreen from 'expo-splash-screen' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { Circle } from 'tamagui' import { useResetWalletDevMenu } from '../utils/resetWallet' @@ -15,17 +23,19 @@ import { useResetWalletDevMenu } from '../utils/resetWallet' export default function Authenticate() { useResetWalletDevMenu() + const toast = useToastController() const secureUnlock = useSecureUnlock() const pinInputRef = useRef(null) const [isInitializingAgent, setIsInitializingAgent] = useState(false) const isLoading = secureUnlock.state === 'acquired-wallet-key' || (secureUnlock.state === 'locked' && secureUnlock.isUnlocking) + // biome-ignore lint/correctness/useExhaustiveDependencies: canTryUnlockingUsingBiometrics not needed useEffect(() => { if (secureUnlock.state === 'locked' && secureUnlock.canTryUnlockingUsingBiometrics) { secureUnlock.tryUnlockingUsingBiometrics() } - }, [secureUnlock]) + }, []) useEffect(() => { if (secureUnlock.state !== 'acquired-wallet-key') return @@ -62,6 +72,18 @@ export default function Authenticate() { void SplashScreen.hideAsync() + const unlockUsingBiometrics = async () => { + if (secureUnlock.state === 'locked') { + secureUnlock.tryUnlockingUsingBiometrics() + } else { + toast.show('You PIN is required to unlock the app', { + customData: { + preset: 'danger', + }, + }) + } + } + const unlockUsingPin = async (pin: string) => { if (secureUnlock.state !== 'locked') return await secureUnlock.unlockUsingPin(pin) @@ -82,6 +104,7 @@ export default function Authenticate() { ref={pinInputRef} pinLength={6} onPinComplete={unlockUsingPin} + onBiometricsTap={unlockUsingBiometrics} useNativeKeyboard={false} /> diff --git a/packages/app/src/provider/BackgroundLockProvider.tsx b/packages/app/src/provider/BackgroundLockProvider.tsx new file mode 100644 index 00000000..7736d3db --- /dev/null +++ b/packages/app/src/provider/BackgroundLockProvider.tsx @@ -0,0 +1,39 @@ +import type { PropsWithChildren } from 'react' + +import { useSecureUnlock } from '@package/secure-store/secure-wallet-key/SecureUnlockProvider' +import { useEffect, useRef } from 'react' +import React from 'react' +import { AppState, type AppStateStatus } from 'react-native' + +const BACKGROUND_TIME_THRESHOLD = 3000 // FIXME + +export function BackgroundLockProvider({ children }: PropsWithChildren) { + const secureUnlock = useSecureUnlock() + const backgroundTimeRef = useRef(null) + + useEffect(() => { + const handleAppStateChange = (nextAppState: AppStateStatus) => { + if (nextAppState === 'background' || nextAppState === 'inactive') { + backgroundTimeRef.current = new Date() + } else if (nextAppState === 'active') { + if (backgroundTimeRef.current) { + const timeInBackground = new Date().getTime() - backgroundTimeRef.current.getTime() + + if (timeInBackground > BACKGROUND_TIME_THRESHOLD && secureUnlock.state === 'unlocked') { + console.log('App was in background for more than 30 seconds, locking') + secureUnlock.lock() + } + backgroundTimeRef.current = null + } + } + } + + const subscription = AppState.addEventListener('change', handleAppStateChange) + + return () => { + subscription.remove() + } + }, [secureUnlock]) + + return <>{children} +} diff --git a/packages/app/src/provider/index.ts b/packages/app/src/provider/index.ts index d4893858..116890b6 100644 --- a/packages/app/src/provider/index.ts +++ b/packages/app/src/provider/index.ts @@ -2,3 +2,4 @@ export * from './Provider' export * from './ToastViewport' export * from './NoInternetToastProvider' export * from './ModalProvider' +export * from './BackgroundLockProvider' diff --git a/packages/app/src/utils/DeeplinkHandler.tsx b/packages/app/src/utils/DeeplinkHandler.tsx index f16b25f7..6521dda9 100644 --- a/packages/app/src/utils/DeeplinkHandler.tsx +++ b/packages/app/src/utils/DeeplinkHandler.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react' import type { ReactNode } from 'react' -import { InvitationQrTypes, type InvitationType } from '@package/agent' +import { InvitationQrTypes } from '@package/agent' import { useToastController } from '@package/ui' import { CommonActions } from '@react-navigation/native' import * as Linking from 'expo-linking' diff --git a/packages/ui/src/components/PinDotsInput.tsx b/packages/ui/src/components/PinDotsInput.tsx index f9e79234..adf7b26e 100644 --- a/packages/ui/src/components/PinDotsInput.tsx +++ b/packages/ui/src/components/PinDotsInput.tsx @@ -17,6 +17,7 @@ interface PinDotsInputProps { onPinComplete: (pin: string) => void isLoading?: boolean useNativeKeyboard?: boolean + onBiometricsTap?: () => void } export interface PinDotsInputRef { @@ -28,7 +29,7 @@ export interface PinDotsInputRef { export const PinDotsInput = forwardRef( ( - { onPinComplete, pinLength, isLoading, useNativeKeyboard = true }: PinDotsInputProps, + { onPinComplete, pinLength, isLoading, useNativeKeyboard = true, onBiometricsTap }: PinDotsInputProps, ref: ForwardedRef ) => { const [pin, setPin] = useState('') @@ -91,7 +92,8 @@ export const PinDotsInput = forwardRef( return } - if (character === PinValues.Empty) { + if (character === PinValues.Biometrics) { + onBiometricsTap?.() return } diff --git a/packages/ui/src/components/PinPad.tsx b/packages/ui/src/components/PinPad.tsx index de4558ca..59adce9e 100644 --- a/packages/ui/src/components/PinPad.tsx +++ b/packages/ui/src/components/PinPad.tsx @@ -1,4 +1,4 @@ -import { Text, useTheme } from 'tamagui' +import { Text } from 'tamagui' import { Stack, XStack, YStack } from '../base' import { HeroIcons } from '../content' @@ -12,10 +12,11 @@ export enum PinValues { Seven = '7', Eight = '8', Nine = '9', - Empty = '', Zero = '0', Backspace = 'backspace', + Biometrics = 'biometrics', } + const letterMap: Record = { [PinValues.One]: '', [PinValues.Two]: 'abc', @@ -27,7 +28,7 @@ const letterMap: Record = { [PinValues.Eight]: 'tuv', [PinValues.Nine]: 'wxyz', [PinValues.Zero]: '', - [PinValues.Empty]: '', + [PinValues.Biometrics]: '', [PinValues.Backspace]: '', } @@ -41,9 +42,8 @@ const PinNumber = ({ character, onPressPinNumber, disabled }: PinNumberProps) => fg={1} jc="center" ai="center" - backgroundColor={character === PinValues.Backspace ? '$grey-200' : '$white'} + backgroundColor={[PinValues.Backspace, PinValues.Biometrics].includes(character) ? '$grey-200' : '$white'} pressStyle={{ opacity: 0.5, backgroundColor: '$grey-100' }} - opacity={character === PinValues.Empty ? 0 : 1} onPress={() => onPressPinNumber(character)} disabled={disabled} h="$6" @@ -53,6 +53,8 @@ const PinNumber = ({ character, onPressPinNumber, disabled }: PinNumberProps) => > {character === PinValues.Backspace ? ( + ) : character === PinValues.Biometrics ? ( + ) : ( {/* NOTE: using fontSize $ values will crash on android due to an issue with react-native-reanimated (it seems the string value is sent to the native side, which shouldn't happen) */} @@ -79,7 +81,7 @@ export const PinPad = ({ onPressPinNumber, disabled }: PinPadProps) => { [PinValues.One, PinValues.Two, PinValues.Three], [PinValues.Four, PinValues.Five, PinValues.Six], [PinValues.Seven, PinValues.Eight, PinValues.Nine], - [PinValues.Empty, PinValues.Zero, PinValues.Backspace], + [PinValues.Biometrics, PinValues.Zero, PinValues.Backspace], ] const pinNumbers = pinValues.map((rowItems, rowIndex) => ( From dc36d5153d31c172a12a979e9ef1fe0c7936dde8 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 4 Nov 2024 12:47:56 +0100 Subject: [PATCH 2/9] fix: show empty when no biometrics --- apps/easypid/src/app/authenticate.tsx | 2 +- packages/ui/src/components/PinDotsInput.tsx | 14 +++++++++++--- packages/ui/src/components/PinPad.tsx | 11 ++++++++--- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/apps/easypid/src/app/authenticate.tsx b/apps/easypid/src/app/authenticate.tsx index 2005cfc7..fc65b39d 100644 --- a/apps/easypid/src/app/authenticate.tsx +++ b/apps/easypid/src/app/authenticate.tsx @@ -13,7 +13,7 @@ import { useToastController, } from '@package/ui' import * as SplashScreen from 'expo-splash-screen' -import { useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { Circle } from 'tamagui' import { useResetWalletDevMenu } from '../utils/resetWallet' diff --git a/packages/ui/src/components/PinDotsInput.tsx b/packages/ui/src/components/PinDotsInput.tsx index adf7b26e..28110b61 100644 --- a/packages/ui/src/components/PinDotsInput.tsx +++ b/packages/ui/src/components/PinDotsInput.tsx @@ -92,8 +92,12 @@ export const PinDotsInput = forwardRef( return } - if (character === PinValues.Biometrics) { - onBiometricsTap?.() + if (character === PinValues.Empty) { + return + } + + if (character === PinValues.Biometrics && onBiometricsTap) { + onBiometricsTap() return } @@ -158,7 +162,11 @@ export const PinDotsInput = forwardRef( secureTextEntry /> ) : ( - + )} ) diff --git a/packages/ui/src/components/PinPad.tsx b/packages/ui/src/components/PinPad.tsx index 59adce9e..a6dfca84 100644 --- a/packages/ui/src/components/PinPad.tsx +++ b/packages/ui/src/components/PinPad.tsx @@ -14,6 +14,7 @@ export enum PinValues { Nine = '9', Zero = '0', Backspace = 'backspace', + Empty = '', Biometrics = 'biometrics', } @@ -29,6 +30,7 @@ const letterMap: Record = { [PinValues.Nine]: 'wxyz', [PinValues.Zero]: '', [PinValues.Biometrics]: '', + [PinValues.Empty]: '', [PinValues.Backspace]: '', } @@ -42,7 +44,9 @@ const PinNumber = ({ character, onPressPinNumber, disabled }: PinNumberProps) => fg={1} jc="center" ai="center" - backgroundColor={[PinValues.Backspace, PinValues.Biometrics].includes(character) ? '$grey-200' : '$white'} + backgroundColor={ + [PinValues.Backspace, PinValues.Biometrics, PinValues.Empty].includes(character) ? '$grey-200' : '$white' + } pressStyle={{ opacity: 0.5, backgroundColor: '$grey-100' }} onPress={() => onPressPinNumber(character)} disabled={disabled} @@ -73,15 +77,16 @@ const PinNumber = ({ character, onPressPinNumber, disabled }: PinNumberProps) => export interface PinPadProps { onPressPinNumber: (character: PinValues) => void + useBiometricsPad?: boolean disabled?: boolean } -export const PinPad = ({ onPressPinNumber, disabled }: PinPadProps) => { +export const PinPad = ({ onPressPinNumber, useBiometricsPad, disabled }: PinPadProps) => { const pinValues = [ [PinValues.One, PinValues.Two, PinValues.Three], [PinValues.Four, PinValues.Five, PinValues.Six], [PinValues.Seven, PinValues.Eight, PinValues.Nine], - [PinValues.Biometrics, PinValues.Zero, PinValues.Backspace], + [useBiometricsPad ? PinValues.Biometrics : PinValues.Empty, PinValues.Zero, PinValues.Backspace], ] const pinNumbers = pinValues.map((rowItems, rowIndex) => ( From f4c8d89b632f975668e307d51cb121b17ac3b7c8 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 4 Nov 2024 13:36:23 +0100 Subject: [PATCH 3/9] fix: default floating sheet to padding value --- packages/ui/src/panels/FloatingSheet.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/panels/FloatingSheet.tsx b/packages/ui/src/panels/FloatingSheet.tsx index 83e2dc41..6fb18350 100644 --- a/packages/ui/src/panels/FloatingSheet.tsx +++ b/packages/ui/src/panels/FloatingSheet.tsx @@ -34,7 +34,7 @@ export function FloatingSheet({ children, isOpen, setIsOpen, ...props }: Floatin enterStyle={{ opacity: 0 }} exitStyle={{ opacity: 0 }} /> - + {children} From 78cb300d35db064353920708c5c3ae5eef0e647c Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 4 Nov 2024 14:00:01 +0100 Subject: [PATCH 4/9] chore: icon type --- apps/easypid/src/app/authenticate.tsx | 3 ++ apps/easypid/src/hooks/useBiometricsType.tsx | 23 ++++++++++ .../src/provider/BackgroundLockProvider.tsx | 2 +- packages/ui/assets/FaceId.tsx | 46 +++++++++++++++++++ packages/ui/src/components/PinDotsInput.tsx | 13 +++++- packages/ui/src/components/PinPad.tsx | 25 +++++++--- packages/ui/src/content/Icon.tsx | 2 + 7 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 apps/easypid/src/hooks/useBiometricsType.tsx create mode 100644 packages/ui/assets/FaceId.tsx diff --git a/apps/easypid/src/app/authenticate.tsx b/apps/easypid/src/app/authenticate.tsx index fc65b39d..d55267ff 100644 --- a/apps/easypid/src/app/authenticate.tsx +++ b/apps/easypid/src/app/authenticate.tsx @@ -2,6 +2,7 @@ import { Redirect } from 'expo-router' import { WalletInvalidKeyError } from '@credo-ts/core' import { initializeAppAgent, useSecureUnlock } from '@easypid/agent' +import { useBiometricsType } from '@easypid/hooks/useBiometricsType' import { secureWalletKey } from '@package/secure-store/secureUnlock' import { FlexPage, @@ -25,6 +26,7 @@ export default function Authenticate() { const toast = useToastController() const secureUnlock = useSecureUnlock() + const biometricsType = useBiometricsType() const pinInputRef = useRef(null) const [isInitializingAgent, setIsInitializingAgent] = useState(false) const isLoading = @@ -106,6 +108,7 @@ export default function Authenticate() { onPinComplete={unlockUsingPin} onBiometricsTap={unlockUsingBiometrics} useNativeKeyboard={false} + biometricsType={biometricsType ?? 'fingerprint'} /> ) diff --git a/apps/easypid/src/hooks/useBiometricsType.tsx b/apps/easypid/src/hooks/useBiometricsType.tsx new file mode 100644 index 00000000..9a5454bb --- /dev/null +++ b/apps/easypid/src/hooks/useBiometricsType.tsx @@ -0,0 +1,23 @@ +import { useEffect, useState } from 'react' +import { Platform } from 'react-native' +import * as Keychain from 'react-native-keychain' + +export function useBiometricsType() { + // Set initial state based on platform (iOS has higher probability for face id) + const [biometryType, setBiometryType] = useState<'face' | 'fingerprint'>( + Platform.OS === 'ios' ? 'face' : 'fingerprint' + ) + + useEffect(() => { + async function checkBiometryType() { + const supportedBiometryType = await Keychain.getSupportedBiometryType() + if (supportedBiometryType) { + setBiometryType(supportedBiometryType?.toLowerCase().includes('face') ? 'face' : 'fingerprint') + } + } + + checkBiometryType() + }, []) + + return biometryType +} diff --git a/packages/app/src/provider/BackgroundLockProvider.tsx b/packages/app/src/provider/BackgroundLockProvider.tsx index 7736d3db..3a5677c7 100644 --- a/packages/app/src/provider/BackgroundLockProvider.tsx +++ b/packages/app/src/provider/BackgroundLockProvider.tsx @@ -5,7 +5,7 @@ import { useEffect, useRef } from 'react' import React from 'react' import { AppState, type AppStateStatus } from 'react-native' -const BACKGROUND_TIME_THRESHOLD = 3000 // FIXME +const BACKGROUND_TIME_THRESHOLD = 30000 // 30 seconds export function BackgroundLockProvider({ children }: PropsWithChildren) { const secureUnlock = useSecureUnlock() diff --git a/packages/ui/assets/FaceId.tsx b/packages/ui/assets/FaceId.tsx new file mode 100644 index 00000000..92882dbd --- /dev/null +++ b/packages/ui/assets/FaceId.tsx @@ -0,0 +1,46 @@ +import Svg, { Path, type SvgProps } from 'react-native-svg' + +export const FaceIdIcon = ({ width = 24, height = 24, color = 'black', ...props }: SvgProps) => { + return ( + + + + + + + + + + + ) +} diff --git a/packages/ui/src/components/PinDotsInput.tsx b/packages/ui/src/components/PinDotsInput.tsx index 28110b61..168cb8cd 100644 --- a/packages/ui/src/components/PinDotsInput.tsx +++ b/packages/ui/src/components/PinDotsInput.tsx @@ -18,6 +18,7 @@ interface PinDotsInputProps { isLoading?: boolean useNativeKeyboard?: boolean onBiometricsTap?: () => void + biometricsType?: 'face' | 'fingerprint' } export interface PinDotsInputRef { @@ -29,7 +30,14 @@ export interface PinDotsInputRef { export const PinDotsInput = forwardRef( ( - { onPinComplete, pinLength, isLoading, useNativeKeyboard = true, onBiometricsTap }: PinDotsInputProps, + { + onPinComplete, + pinLength, + isLoading, + useNativeKeyboard = true, + onBiometricsTap, + biometricsType, + }: PinDotsInputProps, ref: ForwardedRef ) => { const [pin, setPin] = useState('') @@ -96,7 +104,7 @@ export const PinDotsInput = forwardRef( return } - if (character === PinValues.Biometrics && onBiometricsTap) { + if ([PinValues.Fingerprint, PinValues.FaceId].includes(character) && onBiometricsTap) { onBiometricsTap() return } @@ -166,6 +174,7 @@ export const PinDotsInput = forwardRef( onPressPinNumber={onPressPinNumber} disabled={isInLoadingState} useBiometricsPad={!!onBiometricsTap} + biometricsType={biometricsType} /> )} diff --git a/packages/ui/src/components/PinPad.tsx b/packages/ui/src/components/PinPad.tsx index a6dfca84..3e09de75 100644 --- a/packages/ui/src/components/PinPad.tsx +++ b/packages/ui/src/components/PinPad.tsx @@ -1,6 +1,6 @@ import { Text } from 'tamagui' import { Stack, XStack, YStack } from '../base' -import { HeroIcons } from '../content' +import { CustomIcons, HeroIcons } from '../content' export enum PinValues { One = '1', @@ -15,7 +15,8 @@ export enum PinValues { Zero = '0', Backspace = 'backspace', Empty = '', - Biometrics = 'biometrics', + Fingerprint = 'fingerprint', + FaceId = 'faceid', } const letterMap: Record = { @@ -29,7 +30,8 @@ const letterMap: Record = { [PinValues.Eight]: 'tuv', [PinValues.Nine]: 'wxyz', [PinValues.Zero]: '', - [PinValues.Biometrics]: '', + [PinValues.Fingerprint]: '', + [PinValues.FaceId]: '', [PinValues.Empty]: '', [PinValues.Backspace]: '', } @@ -45,7 +47,9 @@ const PinNumber = ({ character, onPressPinNumber, disabled }: PinNumberProps) => jc="center" ai="center" backgroundColor={ - [PinValues.Backspace, PinValues.Biometrics, PinValues.Empty].includes(character) ? '$grey-200' : '$white' + [PinValues.Backspace, PinValues.Fingerprint, PinValues.FaceId, PinValues.Empty].includes(character) + ? '$grey-200' + : '$white' } pressStyle={{ opacity: 0.5, backgroundColor: '$grey-100' }} onPress={() => onPressPinNumber(character)} @@ -57,8 +61,10 @@ const PinNumber = ({ character, onPressPinNumber, disabled }: PinNumberProps) => > {character === PinValues.Backspace ? ( - ) : character === PinValues.Biometrics ? ( + ) : character === PinValues.Fingerprint ? ( + ) : character === PinValues.FaceId ? ( + ) : ( {/* NOTE: using fontSize $ values will crash on android due to an issue with react-native-reanimated (it seems the string value is sent to the native side, which shouldn't happen) */} @@ -78,15 +84,20 @@ const PinNumber = ({ character, onPressPinNumber, disabled }: PinNumberProps) => export interface PinPadProps { onPressPinNumber: (character: PinValues) => void useBiometricsPad?: boolean + biometricsType?: 'face' | 'fingerprint' disabled?: boolean } -export const PinPad = ({ onPressPinNumber, useBiometricsPad, disabled }: PinPadProps) => { +export const PinPad = ({ onPressPinNumber, useBiometricsPad, disabled, biometricsType }: PinPadProps) => { const pinValues = [ [PinValues.One, PinValues.Two, PinValues.Three], [PinValues.Four, PinValues.Five, PinValues.Six], [PinValues.Seven, PinValues.Eight, PinValues.Nine], - [useBiometricsPad ? PinValues.Biometrics : PinValues.Empty, PinValues.Zero, PinValues.Backspace], + [ + useBiometricsPad ? (biometricsType === 'face' ? PinValues.FaceId : PinValues.Fingerprint) : PinValues.Empty, + PinValues.Zero, + PinValues.Backspace, + ], ] const pinNumbers = pinValues.map((rowItems, rowIndex) => ( diff --git a/packages/ui/src/content/Icon.tsx b/packages/ui/src/content/Icon.tsx index 0a66a4c2..3d1772a7 100644 --- a/packages/ui/src/content/Icon.tsx +++ b/packages/ui/src/content/Icon.tsx @@ -73,6 +73,7 @@ import { } from 'react-native-heroicons/solid' import { ExclamationIcon } from '../../assets/Exclamation' +import { FaceIdIcon } from '../../assets/FaceId' import { styled } from 'tamagui' @@ -167,6 +168,7 @@ export const HeroIcons = { export const CustomIcons = { Exclamation: wrapLocalSvg(ExclamationIcon as React.ComponentType), + FaceId: wrapLocalSvg(FaceIdIcon as React.ComponentType), } export type CustomIconProps = SvgProps & { From d02acd99150c379062033cd734baf88ddbab9649 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 4 Nov 2024 14:24:00 +0100 Subject: [PATCH 5/9] chore: better haptic --- apps/easypid/src/app/authenticate.tsx | 11 ++--------- .../src/features/onboarding/screens/id-card-pin.tsx | 6 ++++-- .../{ui => app}/src/components/PinDotsInput.tsx | 13 ++++++++----- packages/app/src/components/index.ts | 1 + packages/app/src/hooks/useHaptics.tsx | 12 +++++------- packages/ui/src/components/index.tsx | 1 - 6 files changed, 20 insertions(+), 24 deletions(-) rename packages/{ui => app}/src/components/PinDotsInput.tsx (93%) diff --git a/apps/easypid/src/app/authenticate.tsx b/apps/easypid/src/app/authenticate.tsx index d55267ff..6b36eb83 100644 --- a/apps/easypid/src/app/authenticate.tsx +++ b/apps/easypid/src/app/authenticate.tsx @@ -4,16 +4,9 @@ import { WalletInvalidKeyError } from '@credo-ts/core' import { initializeAppAgent, useSecureUnlock } from '@easypid/agent' import { useBiometricsType } from '@easypid/hooks/useBiometricsType' import { secureWalletKey } from '@package/secure-store/secureUnlock' -import { - FlexPage, - Heading, - HeroIcons, - PinDotsInput, - type PinDotsInputRef, - YStack, - useToastController, -} from '@package/ui' +import { FlexPage, Heading, HeroIcons, YStack, useToastController } from '@package/ui' import * as SplashScreen from 'expo-splash-screen' +import { PinDotsInput, type PinDotsInputRef } from 'packages/app/src' import { useEffect, useRef, useState } from 'react' import { Circle } from 'tamagui' import { useResetWalletDevMenu } from '../utils/resetWallet' diff --git a/apps/easypid/src/features/onboarding/screens/id-card-pin.tsx b/apps/easypid/src/features/onboarding/screens/id-card-pin.tsx index 47243984..1e1fd133 100644 --- a/apps/easypid/src/features/onboarding/screens/id-card-pin.tsx +++ b/apps/easypid/src/features/onboarding/screens/id-card-pin.tsx @@ -2,6 +2,7 @@ import { IdCard, Paragraph, PinPad, PinValues, Stack, XStack, YStack } from '@pa import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react' import type { TextInput } from 'react-native' +import { useHaptics } from 'packages/app/src/hooks/useHaptics' import germanIssuerImage from '../../../../assets/german-issuer-image.png' import pidBackgroundImage from '../../../../assets/pid-background.png' @@ -14,6 +15,7 @@ const pinLength = 6 export const OnboardingIdCardPinEnter = forwardRef(({ goToNextStep }: OnboardingIdCardPinEnterProps, ref) => { const [pin, setPin] = useState('') const inputRef = useRef(null) + const { withHaptics } = useHaptics() useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus(), @@ -35,7 +37,7 @@ export const OnboardingIdCardPinEnter = forwardRef(({ goToNextStep }: Onboarding } } - const onPressPinNumber = (character: PinValues) => { + const onPressPinNumber = withHaptics((character: PinValues) => { if (character === PinValues.Backspace) { setPin((pin) => pin.slice(0, pin.length - 1)) return @@ -55,7 +57,7 @@ export const OnboardingIdCardPinEnter = forwardRef(({ goToNextStep }: Onboarding return newPin }) - } + }) return ( diff --git a/packages/ui/src/components/PinDotsInput.tsx b/packages/app/src/components/PinDotsInput.tsx similarity index 93% rename from packages/ui/src/components/PinDotsInput.tsx rename to packages/app/src/components/PinDotsInput.tsx index 168cb8cd..668e3774 100644 --- a/packages/ui/src/components/PinDotsInput.tsx +++ b/packages/app/src/components/PinDotsInput.tsx @@ -9,8 +9,9 @@ import Animated, { Easing, } from 'react-native-reanimated' import { Circle, Input } from 'tamagui' -import { XStack, YStack } from '../base' -import { PinPad, PinValues } from './PinPad' +import { XStack, YStack } from '../../../ui/src/base' +import { PinPad, PinValues } from '../../../ui/src/components/PinPad' +import { useHaptics } from '../hooks' interface PinDotsInputProps { pinLength: number @@ -40,6 +41,7 @@ export const PinDotsInput = forwardRef( }: PinDotsInputProps, ref: ForwardedRef ) => { + const { withHaptics, errorHaptic } = useHaptics() const [pin, setPin] = useState('') const inputRef = useRef(null) @@ -52,12 +54,13 @@ export const PinDotsInput = forwardRef( // Shake animation const startShakeAnimation = useCallback(() => { + errorHaptic() shakeAnimation.value = withRepeat( withSequence(...[10, -7.5, 5, -2.5, 0].map((toValue) => withTiming(toValue, { duration: 75 }))), 1, true ) - }, [shakeAnimation]) + }, [shakeAnimation, errorHaptic]) useEffect(() => { translationAnimations.forEach((animation, index) => { @@ -94,7 +97,7 @@ export const PinDotsInput = forwardRef( [startShakeAnimation] ) - const onPressPinNumber = (character: PinValues) => { + const onPressPinNumber = withHaptics((character: PinValues) => { if (character === PinValues.Backspace) { setPin((pin) => pin.slice(0, pin.length - 1)) return @@ -119,7 +122,7 @@ export const PinDotsInput = forwardRef( return newPin }) - } + }) const onChangePin = (newPin: string) => { if (isLoading) return diff --git a/packages/app/src/components/index.ts b/packages/app/src/components/index.ts index de43b5a1..636e703a 100644 --- a/packages/app/src/components/index.ts +++ b/packages/app/src/components/index.ts @@ -15,3 +15,4 @@ export * from './FunkeCredentialCard' export * from './DeleteCredentialSheet' export * from './CardInfoLifecycle' export * from './CardWithAttributes' +export * from './PinDotsInput' diff --git a/packages/app/src/hooks/useHaptics.tsx b/packages/app/src/hooks/useHaptics.tsx index 8cba1e76..f96882a2 100644 --- a/packages/app/src/hooks/useHaptics.tsx +++ b/packages/app/src/hooks/useHaptics.tsx @@ -22,11 +22,9 @@ export function useHaptics() { }, []) const withHaptics = useCallback( - unknown>( - callback: T, - hapticType: HapticType = 'light' - ): ((...args: Parameters) => ReturnType) => { - return (...args) => { + // biome-ignore lint/suspicious/noExplicitAny: should work no matter what the callback returns + any>(callback: T, hapticType: HapticType = 'light'): T => { + return ((...args) => { switch (hapticType) { case 'heavy': heavyHaptic() @@ -40,8 +38,8 @@ export function useHaptics() { default: lightHaptic() } - return callback(...args) as ReturnType - } + return callback(...args) + }) as T }, [lightHaptic, heavyHaptic, successHaptic, errorHaptic] ) diff --git a/packages/ui/src/components/index.tsx b/packages/ui/src/components/index.tsx index 192f4c97..486e06c4 100644 --- a/packages/ui/src/components/index.tsx +++ b/packages/ui/src/components/index.tsx @@ -1,4 +1,3 @@ -export * from './PinDotsInput' export * from './ProgressHeader' export * from './OnboardingStepItem' export * from './IdCard' From 9747a2f09623aa1ca03e34099aa59dd1898bfbb1 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 4 Nov 2024 18:59:03 +0530 Subject: [PATCH 6/9] allow issuance deeplinks Signed-off-by: Timo Glastra --- apps/easypid/src/app/(app)/_layout.tsx | 1 - apps/easypid/src/app/_layout.tsx | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/easypid/src/app/(app)/_layout.tsx b/apps/easypid/src/app/(app)/_layout.tsx index b49d2e4b..75ad832c 100644 --- a/apps/easypid/src/app/(app)/_layout.tsx +++ b/apps/easypid/src/app/(app)/_layout.tsx @@ -15,7 +15,6 @@ const jsonRecordIds = [seedCredentialStorage.recordId, activityStorage.recordId] // When deeplink routing we want to push export const credentialDataHandlerOptions = { - allowedInvitationTypes: ['openid-authorization-request'], routeMethod: 'push', } satisfies CredentialDataHandlerOptions diff --git a/apps/easypid/src/app/_layout.tsx b/apps/easypid/src/app/_layout.tsx index e22c4269..76ffc821 100644 --- a/apps/easypid/src/app/_layout.tsx +++ b/apps/easypid/src/app/_layout.tsx @@ -36,9 +36,7 @@ export default function RootLayout() { > - - - + From 37b601603761e11b8841b6a046224c89fc8a9df1 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 5 Nov 2024 09:28:05 +0100 Subject: [PATCH 7/9] chore: types --- apps/easypid/src/app/(app)/pinConfirmation.tsx | 3 ++- apps/easypid/src/features/onboarding/screens/pin.tsx | 4 +++- apps/easypid/src/features/share/slides/PinSlide.tsx | 4 ++-- .../src/features/wallet/FunkePidConfirmationScreen.tsx | 4 +++- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/easypid/src/app/(app)/pinConfirmation.tsx b/apps/easypid/src/app/(app)/pinConfirmation.tsx index cd07d00d..18ebe67a 100644 --- a/apps/easypid/src/app/(app)/pinConfirmation.tsx +++ b/apps/easypid/src/app/(app)/pinConfirmation.tsx @@ -1,6 +1,7 @@ import { FunkePidConfirmationScreen } from '@easypid/features/wallet/FunkePidConfirmationScreen' -import { HeroIcons, type PinDotsInputRef, XStack } from '@package/ui' +import { HeroIcons, XStack } from '@package/ui' import { useGlobalSearchParams, useNavigation, useRouter } from 'expo-router' +import type { PinDotsInputRef } from 'packages/app/src' import { useEffect, useRef, useState } from 'react' export default function Screen() { diff --git a/apps/easypid/src/features/onboarding/screens/pin.tsx b/apps/easypid/src/features/onboarding/screens/pin.tsx index 4ba5ce19..411ea951 100644 --- a/apps/easypid/src/features/onboarding/screens/pin.tsx +++ b/apps/easypid/src/features/onboarding/screens/pin.tsx @@ -1,4 +1,6 @@ -import { PinDotsInput, type PinDotsInputRef, YStack } from '@package/ui' +import { YStack } from '@package/ui' +import { PinDotsInput } from 'packages/app/src' +import type { PinDotsInputRef } from 'packages/app/src' import React, { useRef, useState } from 'react' export interface OnboardingPinEnterProps { diff --git a/apps/easypid/src/features/share/slides/PinSlide.tsx b/apps/easypid/src/features/share/slides/PinSlide.tsx index 0018bfa7..663947d4 100644 --- a/apps/easypid/src/features/share/slides/PinSlide.tsx +++ b/apps/easypid/src/features/share/slides/PinSlide.tsx @@ -1,5 +1,5 @@ -import { usePushToWallet, useWizard } from '@package/app' -import { Heading, Paragraph, PinDotsInput, type PinDotsInputRef, YStack, useToastController } from '@package/ui' +import { PinDotsInput, type PinDotsInputRef, usePushToWallet, useWizard } from '@package/app' +import { Heading, Paragraph, YStack, useToastController } from '@package/ui' import { useRef, useState } from 'react' import type { PresentationRequestResult } from '../FunkeOpenIdPresentationNotificationScreen' diff --git a/apps/easypid/src/features/wallet/FunkePidConfirmationScreen.tsx b/apps/easypid/src/features/wallet/FunkePidConfirmationScreen.tsx index 93394696..6806c71d 100644 --- a/apps/easypid/src/features/wallet/FunkePidConfirmationScreen.tsx +++ b/apps/easypid/src/features/wallet/FunkePidConfirmationScreen.tsx @@ -1,4 +1,6 @@ -import { FlexPage, Heading, HeroIcons, PinDotsInput, type PinDotsInputRef, YStack } from '@package/ui' +import { FlexPage, Heading, HeroIcons, YStack } from '@package/ui' +import { PinDotsInput } from 'packages/app/src' +import type { PinDotsInputRef } from 'packages/app/src' import React, { forwardRef } from 'react' import { Circle } from 'tamagui' From e357675240277d91e810117f2c7ff033f57c7968 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 5 Nov 2024 09:34:25 +0100 Subject: [PATCH 8/9] chore: state --- apps/easypid/src/app/authenticate.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/easypid/src/app/authenticate.tsx b/apps/easypid/src/app/authenticate.tsx index 6b36eb83..8978a71f 100644 --- a/apps/easypid/src/app/authenticate.tsx +++ b/apps/easypid/src/app/authenticate.tsx @@ -30,7 +30,7 @@ export default function Authenticate() { if (secureUnlock.state === 'locked' && secureUnlock.canTryUnlockingUsingBiometrics) { secureUnlock.tryUnlockingUsingBiometrics() } - }, []) + }, [secureUnlock.state]) useEffect(() => { if (secureUnlock.state !== 'acquired-wallet-key') return From 5eb7b9d865ec9461dd4f493df70be9684b1dc019 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 5 Nov 2024 09:41:41 +0100 Subject: [PATCH 9/9] chore: unused import --- apps/easypid/src/app/_layout.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/easypid/src/app/_layout.tsx b/apps/easypid/src/app/_layout.tsx index 76ffc821..68169ce7 100644 --- a/apps/easypid/src/app/_layout.tsx +++ b/apps/easypid/src/app/_layout.tsx @@ -1,10 +1,4 @@ -import { - BackgroundLockProvider, - DeeplinkHandler, - NoInternetToastProvider, - Provider, - useTransparentNavigationBar, -} from '@package/app' +import { BackgroundLockProvider, NoInternetToastProvider, Provider, useTransparentNavigationBar } from '@package/app' import { SecureUnlockProvider } from '@package/secure-store/secureUnlock' import { DefaultTheme, ThemeProvider } from '@react-navigation/native' import { Slot } from 'expo-router'