Skip to content

Commit

Permalink
feat: pid flow in wallet (#208)
Browse files Browse the repository at this point in the history
Signed-off-by: Jan <[email protected]>
  • Loading branch information
janrtvld authored Nov 19, 2024
1 parent 4b5f277 commit 670ee81
Show file tree
Hide file tree
Showing 20 changed files with 863 additions and 192 deletions.
1 change: 1 addition & 0 deletions apps/easypid/src/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export default function AppLayout() {
<Stack.Screen name="pinConfirmation" options={headerNormalOptions} />
<Stack.Screen name="pinLocked" options={headerNormalOptions} />
<Stack.Screen name="issuer" options={headerNormalOptions} />
<Stack.Screen name="pidSetup" />
</Stack>
</DeeplinkHandler>
</WalletJsonStoreProvider>
Expand Down
5 changes: 5 additions & 0 deletions apps/easypid/src/app/(app)/pidSetup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { FunkePidSetupScreen } from '@easypid/features/pid/FunkePidSetupScreen'

export default function PidSetup() {
return <FunkePidSetupScreen />
}
15 changes: 13 additions & 2 deletions apps/easypid/src/app/authenticate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,26 @@ export default function Authenticate() {
const biometricsType = useBiometricsType()
const pinInputRef = useRef<PinDotsInputRef>(null)
const [isInitializingAgent, setIsInitializingAgent] = useState(false)
const [isAllowedToUnlockWithFaceId, setIsAllowedToUnlockWithFaceId] = useState(false)
const isLoading =
secureUnlock.state === 'acquired-wallet-key' || (secureUnlock.state === 'locked' && secureUnlock.isUnlocking)

// 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)
}, [])

// biome-ignore lint/correctness/useExhaustiveDependencies: canTryUnlockingUsingBiometrics not needed
useEffect(() => {
if (secureUnlock.state === 'locked' && secureUnlock.canTryUnlockingUsingBiometrics) {
if (secureUnlock.state === 'locked' && secureUnlock.canTryUnlockingUsingBiometrics && isAllowedToUnlockWithFaceId) {
secureUnlock.tryUnlockingUsingBiometrics()
}
}, [secureUnlock.state])
}, [secureUnlock.state, isAllowedToUnlockWithFaceId])

useEffect(() => {
if (secureUnlock.state !== 'acquired-wallet-key') return
Expand Down
2 changes: 1 addition & 1 deletion apps/easypid/src/features/menu/FunkeMenuScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function FunkeMenuScreen() {
<MenuItem
key="id"
item={{
href: '/',
href: '/pidSetup',
icon: HeroIcons.IdentificationFilled,
title: 'Setup digital ID',
}}
Expand Down
159 changes: 15 additions & 144 deletions apps/easypid/src/features/onboarding/onboardingContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ import type {
ReceivePidUseCaseState,
} from '@easypid/use-cases/ReceivePidUseCaseFlow'
import { resetWallet } from '@easypid/utils/resetWallet'
import {
type CardScanningState,
type OnboardingPage,
type OnboardingStep,
type PidFlowTypes,
SIMULATOR_PIN,
pidSetupSteps,
} from '@easypid/utils/sharedPidSetup'
import {
BiometricAuthenticationCancelledError,
BiometricAuthenticationNotEnabledError,
Expand All @@ -26,31 +34,11 @@ import { type PidSdJwtVcAttributes, usePidDisplay } from '../../hooks'
import { addReceivedActivity } from '../activity/activityRecord'
import { useHasFinishedOnboarding } from './hasFinishedOnboarding'
import { OnboardingBiometrics } from './screens/biometrics'
import { OnboardingIdCardBiometricsDisabled } from './screens/id-card-biometrics-disabled'
import { OnboardingIdCardFetch } from './screens/id-card-fetch'
import { OnboardingIdCardPinEnter } from './screens/id-card-pin'
import { OnboardingIdCardRequestedAttributes } from './screens/id-card-requested-attributes'
import { OnboardingIdCardScan } from './screens/id-card-scan'
import { OnboardingIdCardStart } from './screens/id-card-start'
import { OnboardingIdCardVerify } from './screens/id-card-verify'
import { OnboardingIntroductionSteps } from './screens/introduction-steps'
import OnboardingPinEnter from './screens/pin'
import OnboardingWelcome from './screens/welcome'

type Page =
| { type: 'fullscreen' }
| {
type: 'content'
title: string
animation?: 'default' | 'delayed'
subtitle?: string
caption?: string
animationKey?: string
}

// Same animation key means the content won't fade out and then in again. So if the two screens have most content in common
// this looks nicer.
const onboardingSteps = [
export const onboardingSteps = [
{
step: 'welcome',
alternativeFlow: false,
Expand Down Expand Up @@ -121,125 +109,13 @@ const onboardingSteps = [
},
Screen: OnboardingBiometrics,
},
{
step: 'id-card-start',
alternativeFlow: false,
progress: 49.5,
page: {
type: 'content',
title: 'Scan your eID card to retrieve your data',
subtitle: 'Add your personal details once using your eID card and its PIN.',
caption: 'Your eID PIN was issued to you when you received your eID card.',
},
Screen: OnboardingIdCardStart,
},
{
step: 'id-card-requested-attributes',
alternativeFlow: false,
progress: 49.5,
page: {
type: 'content',
title: 'Review the request',
},
Screen: OnboardingIdCardRequestedAttributes,
},
{
step: 'id-card-pin',
alternativeFlow: false,
progress: 49.5,
page: {
type: 'content',
title: 'Enter your eID card PIN',
},
Screen: OnboardingIdCardPinEnter,
},
{
step: 'id-card-start-scan',
alternativeFlow: false,
progress: 66,
page: {
type: 'content',
title: 'Scan your eID card',
subtitle: 'Place your device on top of your eID card to scan it.',
animationKey: 'id-card-scan',
},
Screen: OnboardingIdCardScan,
},
{
step: 'id-card-scan',
alternativeFlow: false,
progress: 66,
page: {
type: 'content',
title: 'Scan your eID card',
subtitle: 'Place your device on top of your eID card to scan it.',
animationKey: 'id-card-scan',
},
Screen: OnboardingIdCardScan,
},
{
step: 'id-card-fetch',
alternativeFlow: false,
progress: 82.5,
page: {
type: 'content',
title: 'Fetching information',
},
Screen: OnboardingIdCardFetch,
},
{
step: 'id-card-verify',
progress: 82.5,
alternativeFlow: true,
page: {
type: 'content',
title: 'We need to verify it’s you',
subtitle: 'Your biometrics are required to verify your identity.',
animationKey: 'id-card',
},
Screen: OnboardingIdCardVerify,
},
{
step: 'id-card-biometrics-disabled',
progress: 82.5,
alternativeFlow: true,
page: {
type: 'content',
title: 'You need to enable biometrics',
subtitle:
'To continue, make sure your device has biometric protection enabled, and that EasyPID is allowed to use biometrics.',
},
Screen: OnboardingIdCardBiometricsDisabled,
},
{
step: 'id-card-complete',
progress: 100,
alternativeFlow: false,
page: {
type: 'content',
title: 'Success!',
subtitle: 'Your information has been retrieved from your eID card.',
animationKey: 'id-card-success',
},
Screen: OnboardingIdCardFetch,
},
] as const satisfies Array<{
step: string
progress: number
page: Page
// if true will not be navigated to by goToNextStep
alternativeFlow: boolean
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
Screen: React.FunctionComponent<any>
}>

export type OnboardingSteps = typeof onboardingSteps
export type OnboardingStep = OnboardingSteps[number]
...pidSetupSteps,
] as const satisfies Array<OnboardingStep>

export type OnboardingContext = {
currentStep: OnboardingStep['step']
progress: number
page: Page
page: OnboardingPage
screen: React.JSX.Element
reset: () => void
}
Expand All @@ -259,7 +135,7 @@ export function OnboardingContextProvider({
const [, setHasFinishedOnboarding] = useHasFinishedOnboarding()
const pidDisplay = usePidDisplay()

const [selectedFlow, setSelectedFlow] = useState<'c' | 'bprime'>('c')
const [selectedFlow, setSelectedFlow] = useState<PidFlowTypes>('c')
const [receivePidUseCase, setReceivePidUseCase] = useState<ReceivePidUseCaseCFlow | ReceivePidUseCaseBPrimeFlow>()
const [receivePidUseCaseState, setReceivePidUseCaseState] = useState<ReceivePidUseCaseState | 'initializing'>()
const [allowSimulatorCard, setAllowSimulatorCard] = useState(false)
Expand All @@ -268,12 +144,7 @@ export function OnboardingContextProvider({
const [idCardPin, setIdCardPin] = useState<string>()
const [userName, setUserName] = useState<string>()
const [agent, setAgent] = useState<AppAgent>()
const [idCardScanningState, setIdCardScanningState] = useState<{
showScanModal: boolean
isCardAttached?: boolean
progress: number
state: 'readyToScan' | 'scanning' | 'complete' | 'error'
}>({
const [idCardScanningState, setIdCardScanningState] = useState<CardScanningState>({
isCardAttached: undefined,
progress: 0,
state: 'readyToScan',
Expand Down Expand Up @@ -343,7 +214,7 @@ export function OnboardingContextProvider({
const onPinReEnter = async (pin: string) => {
// Spells BROKEN on the pin pad (with letters)
// Allows bypassing the eID card and use a simulator card
const isSimulatorPinCode = pin === '276536'
const isSimulatorPinCode = pin === SIMULATOR_PIN

if (isSimulatorPinCode) {
setAllowSimulatorCard(true)
Expand Down
4 changes: 2 additions & 2 deletions apps/easypid/src/features/onboarding/screens/id-card-scan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { AnimatedNfcScan, Button, NfcScannerModalAndroid, Stack, YStack } from '

import { Platform } from 'react-native'

interface OnboardingIdCardScanProps {
export interface OnboardingIdCardScanProps {
isCardAttached?: boolean
scanningState: 'readyToScan' | 'scanning' | 'complete' | 'error'
progress: number
showScanModal: boolean
onCancel: () => void
onStartScanning?: () => void
onStartScanning?: () => Promise<void>
}

export function OnboardingIdCardScan({
Expand Down
12 changes: 7 additions & 5 deletions apps/easypid/src/features/onboarding/screens/id-card-start.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { useState } from 'react'

interface OnboardingIdCardStartScanProps {
goToNextStep: () => Promise<void>
onSkipCardSetup: () => void
onSkipCardSetup?: () => void
}

export function OnboardingIdCardStart({ goToNextStep, onSkipCardSetup }: OnboardingIdCardStartScanProps) {
const [isLoading, setIsLoading] = useState(false)

const onSetupLater = () => {
if (isLoading) return
if (isLoading || !onSkipCardSetup) return

setIsLoading(true)
onSkipCardSetup()
Expand All @@ -38,9 +38,11 @@ export function OnboardingIdCardStart({ goToNextStep, onSkipCardSetup }: Onboard
</ScrollView>
</YStack>
<YStack gap="$4" alignItems="center">
<Button.Text disabled={isLoading} icon={HeroIcons.ArrowRight} scaleOnPress onPress={onSetupLater}>
Set up later
</Button.Text>
{onSkipCardSetup && (
<Button.Text disabled={isLoading} icon={HeroIcons.ArrowRight} scaleOnPress onPress={onSetupLater}>
Set up later
</Button.Text>
)}
<Button.Solid scaleOnPress disabled={isLoading} onPress={onContinue}>
{isLoading ? <Spinner variant="dark" /> : 'Continue'}
</Button.Solid>
Expand Down
Loading

0 comments on commit 670ee81

Please sign in to comment.