Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions apps/easypid/src/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,7 @@ export default function AppLayout() {
<Stack.Screen name="menu/about" options={headerNormalOptions} />
<Stack.Screen name="activity/index" options={headerNormalOptions} />
<Stack.Screen name="activity/[id]" options={headerNormalOptions} />
<Stack.Screen name="pinConfirmation" options={headerNormalOptions} />
<Stack.Screen name="pinLocked" options={headerNormalOptions} />
<Stack.Screen name="trust" options={headerNormalOptions} />
<Stack.Screen name="pidSetup" />
<Stack.Screen name="inbox" options={headerNormalOptions} />
</Stack>
</ParadymWalletSdk.AppProvider>
Expand Down
46 changes: 0 additions & 46 deletions apps/easypid/src/app/(app)/pinConfirmation.tsx

This file was deleted.

16 changes: 0 additions & 16 deletions apps/easypid/src/app/(app)/pinLocked.tsx

This file was deleted.

139 changes: 78 additions & 61 deletions apps/easypid/src/app/authenticate.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { TypedArrayEncoder } from '@credo-ts/core'
import { useBiometricsType } from '@easypid/hooks/useBiometricsType'
import {
WalletPinPromptHeader,
WalletPinPromptInput,
WalletUnlockPromptInput,
} from '@easypid/components/WalletPinPrompt'
import { setupWalletServiceProvider, setWalletServiceProviderPin } from '@easypid/crypto/WalletServiceProviderClient'
import { useShouldUseCloudHsm } from '@easypid/features/onboarding/useShouldUseCloudHsm'
import {
clearWalletFlowAuthorization,
getRedirectedWalletFlowAuthorizationRoute,
isWalletAuthPromptError,
setWalletFlowAuthorizationSession,
} from '@easypid/utils/authorizeWalletFlow'
import { useLingui } from '@lingui/react/macro'
import { PinDotsInput, type PinDotsInputRef } from '@package/app'
import type { PinDotsInputRef } from '@package/app'
import { commonMessages } from '@package/translations'
import { FlexPage, Heading, HeroIcons, IconContainer, useDeviceMedia, useToastController, YStack } from '@package/ui'
import {
ParadymWalletAuthenticationInvalidPinError,
ParadymWalletBiometricAuthenticationError,
useCanUseBiometryBackedWalletKey,
useIsBiometricsEnabled,
useParadym,
} from '@paradym/wallet-sdk'
import { FlexPage, HeroIcons, IconContainer, useDeviceMedia, useToastController, YStack } from '@package/ui'
import { useBiometricUnlockState, useParadym } from '@paradym/wallet-sdk'
import { Redirect, useLocalSearchParams } from 'expo-router'
import * as SplashScreen from 'expo-splash-screen'
import { useEffect, useRef, useState } from 'react'
Expand All @@ -27,69 +33,67 @@ export default function Authenticate() {

const { redirectAfterUnlock } = useLocalSearchParams<{ redirectAfterUnlock?: string }>()
const toast = useToastController()
const biometricsType = useBiometricsType()
const pinInputRef = useRef<PinDotsInputRef>(null)
const redirectedFlowPinRef = useRef<string | undefined>(undefined)
const { additionalPadding, noBottomSafeArea } = useDeviceMedia()
const [isInitializingAgent, setIsInitializingAgent] = useState(false)
const [isAllowedToUnlockWithFaceId, setIsAllowedToUnlockWithFaceId] = useState(false)
const [isBiometricsEnabled] = useIsBiometricsEnabled()
const canUseBiometryBackedWalletKey = useCanUseBiometryBackedWalletKey()
const [shouldPromptBiometrics, setShouldPromptBiometrics] = useState(true)
const [shouldUseCloudHsmValue] = useShouldUseCloudHsm()
const biometricUnlockState = useBiometricUnlockState()
const redirectAfterUnlockUrl = redirectAfterUnlock
? TypedArrayEncoder.toUtf8String(TypedArrayEncoder.fromBase64(redirectAfterUnlock))
: undefined
const redirectedFlowAuthorizationRoute = getRedirectedWalletFlowAuthorizationRoute(
redirectAfterUnlockUrl,
shouldUseCloudHsmValue === true
)
const shouldUsePinOnlyRedirectedFlowAuth = redirectedFlowAuthorizationRoute !== undefined
const showBiometricUnlockAction =
!shouldUsePinOnlyRedirectedFlowAuth &&
biometricUnlockState.data?.canUnlockNow === true &&
(paradym.state === 'locked' || (paradym.state === 'acquired-wallet-key' && paradym.unlockMethod === 'biometrics'))

const isLoading = paradym.state === 'locked' && paradym.isUnlocking
const isLoading =
paradym.state === 'acquired-wallet-key' ||
(paradym.state === 'locked' && paradym.isUnlocking) ||
isInitializingAgent

useEffect(() => {
if (paradym.state === 'unlocked' && redirectAfterUnlock) {
paradym.lock()
}
}, [])

// After resetting the wallet, we want to avoid prompting for face id immediately
// So we add an artificial delay
useEffect(() => {
const timer = setTimeout(() => setIsAllowedToUnlockWithFaceId(true), 500)

return () => clearTimeout(timer)
}, [])

useEffect(() => {
if (
paradym.state === 'locked' &&
paradym.canTryUnlockingUsingBiometrics &&
isAllowedToUnlockWithFaceId &&
shouldPromptBiometrics
) {
paradym.tryUnlockingUsingBiometrics()
}
}, [paradym.state, isAllowedToUnlockWithFaceId])

useEffect(() => {
if (isInitializingAgent || paradym.state !== 'acquired-wallet-key') return

setIsInitializingAgent(true)
paradym
.unlock()
.then(async (sdk) => {
await setupWalletServiceProvider(sdk)

if (!redirectedFlowAuthorizationRoute || !redirectedFlowPinRef.current) {
return
}

await setWalletServiceProviderPin(redirectedFlowPinRef.current, false)
setWalletFlowAuthorizationSession(redirectedFlowAuthorizationRoute)
})
.catch((error) => {
if (
error instanceof ParadymWalletAuthenticationInvalidPinError ||
error instanceof ParadymWalletBiometricAuthenticationError
) {
redirectedFlowPinRef.current = undefined
clearWalletFlowAuthorization()

if (isWalletAuthPromptError(error)) {
pinInputRef.current?.clear()
pinInputRef.current?.shake()
}
if (error instanceof ParadymWalletAuthenticationInvalidPinError) {
// We do not want to prompt biometrics directly after an incorrect pin input
setShouldPromptBiometrics(false)
}
})
.finally(() => setIsInitializingAgent(false))
}, [paradym, isInitializingAgent])
}, [paradym, isInitializingAgent, redirectedFlowAuthorizationRoute])

if (paradym.state === 'unlocked') {
// Expo and urls as query params don't go well together, so we encoded the url as base64
const redirect = redirectAfterUnlock
? TypedArrayEncoder.toUtf8String(TypedArrayEncoder.fromBase64(redirectAfterUnlock))
: '/'
const redirect = redirectAfterUnlockUrl ?? '/'

return <Redirect href={redirect} />
}
Expand All @@ -114,27 +118,40 @@ export default function Authenticate() {

const unlockUsingPin = async (pin: string) => {
if (paradym.state !== 'locked') return
await paradym.unlockUsingPin(pin)

try {
if (shouldUsePinOnlyRedirectedFlowAuth) redirectedFlowPinRef.current = pin
await paradym.unlockUsingPin(pin)
} catch (error) {
redirectedFlowPinRef.current = undefined
throw error
}
}

return (
<FlexPage flex-1 alignItems="center">
<YStack fg={1} gap="$6" mb={noBottomSafeArea ? -additionalPadding : undefined}>
<YStack flex-1 alignItems="center" justifyContent="flex-end" gap="$4">
<IconContainer h="$4" w="$4" ai="center" jc="center" icon={<HeroIcons.LockClosedFilled />} />
<Heading heading="h2" fontWeight="$semiBold">
{t(commonMessages.enterPin)}
</Heading>
<WalletPinPromptHeader
title={t(commonMessages.enterPin)}
centerHeader
headerIcon={<IconContainer h="$4" w="$4" ai="center" jc="center" icon={<HeroIcons.LockClosedFilled />} />}
titleHeading="h2"
titleFontWeight="$semiBold"
/>
</YStack>
<PinDotsInput
isLoading={isLoading}
ref={pinInputRef}
pinLength={6}
onPinComplete={unlockUsingPin}
onBiometricsTap={isBiometricsEnabled && canUseBiometryBackedWalletKey ? unlockUsingBiometrics : undefined}
useNativeKeyboard={false}
biometricsType={biometricsType ?? 'fingerprint'}
/>
{shouldUsePinOnlyRedirectedFlowAuth ? (
<WalletPinPromptInput isLoading={isLoading} inputRef={pinInputRef} onPinComplete={unlockUsingPin} />
) : (
<WalletUnlockPromptInput
isLoading={isLoading}
inputRef={pinInputRef}
onPinComplete={unlockUsingPin}
onBiometricsTap={unlockUsingBiometrics}
showBiometricUnlockAction={showBiometricUnlockAction}
autoPromptBiometrics={paradym.state === 'locked' && paradym.canTryUnlockingUsingBiometrics}
/>
)}
</YStack>
</FlexPage>
)
Expand Down
52 changes: 52 additions & 0 deletions apps/easypid/src/components/WalletFlowAuthPrompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { SubmissionAuthorizationMode } from '@easypid/hooks/useSubmissionAuthorizationMode'
import { useLingui } from '@lingui/react/macro'
import type { PinDotsInputRef } from '@package/app'
import { commonMessages } from '@package/translations'
import { useRef, useState } from 'react'
import { YStack } from 'tamagui'
import { WalletPinPromptHeader, WalletPinPromptInput, WalletUnlockPromptInput } from './WalletPinPrompt'

export type OnWalletAuthSubmitProps = { pin?: string; onAuthorized?: () => void; onAuthorizationError?: () => void }

export interface WalletFlowAuthPromptProps {
authMode: Exclude<SubmissionAuthorizationMode, 'none'>
onSubmit: ({ pin, onAuthorized, onAuthorizationError }: OnWalletAuthSubmitProps) => Promise<void>
isLoading: boolean
annotation?: string
}

export function WalletFlowAuthPrompt({ authMode, onSubmit, isLoading, annotation }: WalletFlowAuthPromptProps) {
const { t } = useLingui()
const [isSubmitting, setIsSubmitting] = useState(false)
const pinRef = useRef<PinDotsInputRef>(null)

const onAuthorizationError = () => {
pinRef.current?.shake()
pinRef.current?.clear()
}

const submit = (pin?: string) => {
setIsSubmitting(true)
void onSubmit({ pin, onAuthorizationError }).finally(() => setIsSubmitting(false))
}

return (
<YStack fg={1} jc="space-between">
<YStack gap="$4">
<WalletPinPromptHeader title={t(commonMessages.enterPinToShareData)} annotation={annotation} />
</YStack>
<YStack fg={1} mt="$10">
{authMode === 'pin-only' ? (
<WalletPinPromptInput onPinComplete={submit} isLoading={isLoading || isSubmitting} inputRef={pinRef} />
) : (
<WalletUnlockPromptInput
onPinComplete={submit}
onBiometricsTap={() => submit()}
isLoading={isLoading || isSubmitting}
inputRef={pinRef}
/>
)}
</YStack>
</YStack>
)
}
Loading