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

feat: working for android #141

Merged
merged 7 commits into from
Aug 19, 2024
Merged
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
6 changes: 3 additions & 3 deletions apps/ausweis/app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@ const config = {
icon: './assets/icon.png',
userInterfaceStyle: 'light',
androidStatusBar: {
backgroundColor: '#F2F4F6',
backgroundColor: '#FFFFFF',
},
androidNavigationBar: {
backgroundColor: '#F2F4F6',
backgroundColor: '#FFFFFF',
},
splash: {
image: './assets/splash.png',
resizeMode: 'contain',
backgroundColor: '#F2F4F6',
backgroundColor: '#FFFFFF',
},
updates: {
fallbackToCacheTimeout: 0,
Expand Down
2 changes: 1 addition & 1 deletion apps/ausweis/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
},
"devDependencies": {
"@babel/core": "^7.24.4",
"@tamagui/babel-plugin": "^1.108.0",
"@tamagui/babel-plugin": "1.108.0",
"expo-build-properties": "^0.12.5",
"typescript": "*"
}
Expand Down
2 changes: 1 addition & 1 deletion apps/ausweis/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ MIICeTCCAiCgAwIBAgIUB5E9QVZtmUYcDtCjKB/H3VQv72gwCgYIKoZIzj0EAwIwgYgxCzAJBgNVBAYT

// https://funke.animo.id
const animoFunkeRelyingPartyCertificate =
'MIIBAzCBq6ADAgECAhAcowXbm2aJXPP7Am0/DqMGMAoGCCqGSM49BAMCMAAwIBcNNzAwMTAxMDAwMDAwWhgPMjI4NjExMjAxNzQ2NDBaMAAwOTATBgcqhkjOPQIBBggqhkjOPQMBBwMiAALcD1XzKepFxWMAOqV+ln1fybBt7DRO5CV0f9A6mRp2xaMlMCMwIQYDVR0RBBowGIYWaHR0cHM6Ly9mdW5rZS5hbmltby5pZDAKBggqhkjOPQQDAgNHADBEAiBd0afYRMKslUz1DooFge3Vk2ggZcwlSkTF750rfa7MjAIgGFPM9J6PvAXkvsxUdC1xCcyCQBBrOsLdwqCT4wzTmko='
'MIIBBTCBq6ADAgECAhAar4TMiACrMSPwZ04DngKaMAoGCCqGSM49BAMCMAAwIBcNNzAwMTAxMDAwMDAwWhgPMjI4NjExMjAxNzQ2NDBaMAAwOTATBgcqhkjOPQIBBggqhkjOPQMBBwMiAALcD1XzKepFxWMAOqV+ln1fybBt7DRO5CV0f9A6mRp2xaMlMCMwIQYDVR0RBBowGIYWaHR0cHM6Ly9mdW5rZS5hbmltby5pZDAKBggqhkjOPQQDAgNJADBGAiEA1pSuByfgYpXmUjs+OGIJvqeHXCtTXiMxZDKe7bH7ySUCIQDyLR+EfdKPgj83FGUB9sERjfrCH9sH6QwI0TPAJSGicA=='

export const trustedX509Certificates = [bdrPidIssuerCertificate, animoFunkeRelyingPartyCertificate]

Expand Down
193 changes: 150 additions & 43 deletions apps/ausweis/src/features/onboarding/onboardingContext.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { sendCommand } from '@animo-id/expo-ausweis-sdk'
import { type AppAgent, initializeAppAgent, useSecureUnlock } from '@ausweis/agent'
import {
type CardScanningErrorDetails,
ReceivePidUseCase,
type ReceivePidUseCaseOptions,
type ReceivePidUseCaseState,
} from '@ausweis/use-cases/ReceivePidUseCase'
import type { SdJwtVcHeader } from '@credo-ts/core'
import { storeCredential } from '@package/agent'
import { useToastController } from '@package/ui'
import { capitalizeFirstLetter } from '@package/utils'
import { capitalizeFirstLetter, sleep } from '@package/utils'
import { useRouter } from 'expo-router'
import type React from 'react'
import { type PropsWithChildren, createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'
import { Platform } from 'react-native'
import { OnboardingBiometrics } from './screens/biometrics'
import { OnboardingIdCardFetch } from './screens/id-card-fetch'
import { OnboardingIdCardPinEnter } from './screens/id-card-pin'
Expand Down Expand Up @@ -98,7 +100,10 @@ const onboardingStepsCFlow = [
progress: 66,
page: {
type: 'content',
title: 'Place your eID card at the top of you phone.',
title:
Platform.OS === 'android'
? 'Place your eID card at the back of your phone'
: 'Place your eID card at the top of you phone.',
animationKey: 'id-card-scan',
},
Screen: OnboardingIdCardStartScan,
Expand Down Expand Up @@ -171,6 +176,17 @@ 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'
}>({
isCardAttached: undefined,
progress: 0,
state: 'readyToScan',
showScanModal: true,
})

const currentStep = onboardingStepsCFlow.find((step) => step.step === currentStepName)
if (!currentStep) throw new Error(`Invalid step ${currentStepName}`)
Expand Down Expand Up @@ -248,25 +264,56 @@ export function OnboardingContextProvider({
(options) => {
if (!idCardPin) {
// We need to hide the NFC modal on iOS, as we first need to ask the user for the pin again
sendCommand({ cmd: 'INTERRUPT' })
if (Platform.OS === 'ios') sendCommand({ cmd: 'INTERRUPT' })

setIdCardScanningState((state) => ({
...state,
progress: 0,
state: 'error',
showScanModal: true,
isCardAttached: undefined,
}))

// Ask user for PIN:
return new Promise<string>((resolve) => {
setOnIdCardPinReEnter(() => {
return async (idCardPin: string) => {
setIdCardScanningState((state) => ({
...state,
showScanModal: true,
}))
setCurrentStepName('id-card-scan')
// UI blocks if we immediately resolve the PIN, we first want to make sure we navigate to the id-card-scan page again
setTimeout(() => resolve(idCardPin), 100)
setOnIdCardPinReEnter(undefined)
}
})
// If we don't wait for a bit, it will render the keyboard and the nfc modal at the same time...
setTimeout(() => {

let promise: Promise<void>
// On android we have a custom modal, so we can keep the timeout shorten, but we do want to show the error modal for a bit.
if (Platform.OS === 'android') {
promise = sleep(1000).then(async () => {
setIdCardScanningState((state) => ({
...state,
state: 'readyToScan',
showScanModal: false,
}))

await sleep(500)
})
}
// on iOS we need to wait 3 seconds for the NFC modal to close, as otherwise it will render the keyboard and the nfc modal at the same time...
else {
promise = sleep(3000)
}

// Navigate to the id-card-pin and show a toast
promise.then(() => {
setCurrentStepName('id-card-pin')
toast.show('Invalid PIN entered for eID Card. Please try again', {
customData: { preset: 'danger' },
})
setCurrentStepName('id-card-pin')
}, 3000)
})
})
}

Expand Down Expand Up @@ -298,6 +345,13 @@ export function OnboardingContextProvider({
return ReceivePidUseCase.initialize({
agent: secureUnlock.context.agent,
onStateChange: setReceivePidUseCaseState,
onCardAttachedChanged: ({ isCardAttached }) =>
setIdCardScanningState((state) => ({
...state,
isCardAttached,
state: state.state === 'readyToScan' && isCardAttached ? 'scanning' : state.state,
})),
onStatusProgress: ({ progress }) => setIdCardScanningState((state) => ({ ...state, progress })),
onEnterPin: (options) => onEnterPinRef.current.onEnterPin(options),
})
.then((receivePidUseCase) => {
Expand All @@ -317,9 +371,11 @@ export function OnboardingContextProvider({
const reset = ({
resetToStep = 'welcome',
error,
showToast = true,
}: {
error?: unknown
resetToStep: OnboardingStep['step']
showToast?: boolean
}) => {
if (error) console.error(error)

Expand All @@ -335,28 +391,35 @@ export function OnboardingContextProvider({

// Reset eID Card state
if (stepsToCompleteAfterReset.includes('id-card-pin')) {
// TODO: we need to be able to re-initialize the expo ausweis sdk
setIdCardPin(undefined)
setReceivePidUseCaseState(undefined)
setReceivePidUseCase(undefined)
setIdCardScanningState({
progress: 0,
state: 'readyToScan',
isCardAttached: undefined,
showScanModal: true,
})
setOnIdCardPinReEnter(undefined)
}
if (stepsToCompleteAfterReset.includes('id-card-fetch')) {
setUserName(undefined)
}
if (stepsToCompleteAfterReset.includes('id-card-pin')) {
setIdCardPin(undefined)
}

// TODO: if we already have the agent, we should either remove the wallet and start again,
// or we need to start from the id card flow
setCurrentStepName(resetToStep)

toast.show('Error occurred during onboarding', {
message: 'Please try again.',
customData: {
preset: 'danger',
},
})
if (showToast) {
toast.show('Error occurred during onboarding', {
message: 'Please try again.',
customData: {
preset: 'danger',
},
})
}
}

const onStartScanning = async () => {
Expand All @@ -377,37 +440,69 @@ export function OnboardingContextProvider({
return
}

goToNextStep()

// Authenticate
try {
goToNextStep()
// Authenticate
await receivePidUseCase.authenticateUsingIdCard()
} catch (error) {
setIdCardScanningState((state) => ({
...state,
state: 'error',
}))
await sleep(500)
setIdCardScanningState((state) => ({
...state,
showScanModal: false,
}))
await sleep(500)

const reason = (error as CardScanningErrorDetails).reason
if (reason === 'user_cancelled' || reason === 'cancelled') {
reset({
resetToStep: 'id-card-pin',
error,
showToast: false,
})
toast.show('eID card scanning cancelled', {
message: 'Please try again.',
customData: {
preset: 'danger',
},
})
} else {
reset({ resetToStep: 'id-card-pin', error })
}

// The modal on iOS is so slooooooow.
// TODO: we probably don't need this on Android
setTimeout(async () => {
try {
setCurrentStepName('id-card-fetch')

// Acquire access token
await receivePidUseCase.acquireAccessToken()

// Retrieve Credential
const credential = await receivePidUseCase.retrieveCredential()
await storeCredential(secureUnlock.context.agent, credential)
const parsed = secureUnlock.context.agent.sdJwtVc.fromCompact<
SdJwtVcHeader,
{ given_name: string; family_name: string }
>(credential.compactSdJwtVc)
setUserName(
`${capitalizeFirstLetter(parsed.prettyClaims.given_name.toLowerCase())} ${capitalizeFirstLetter(
parsed.prettyClaims.family_name.toLowerCase()
)}`
)
setCurrentStepName('id-card-complete')
} catch (error) {
reset({ resetToStep: 'id-card-pin', error })
}
}, 2000)
return
}

try {
setIdCardScanningState((state) => ({ ...state, state: 'complete', progress: 100 }))

// on iOS it takes around two seconds for the modal to close. On Android we wait 1 second
// and then close the modal
await new Promise((resolve) => setTimeout(resolve, 1000))
setIdCardScanningState((state) => ({ ...state, showScanModal: false }))
await new Promise((resolve) => setTimeout(resolve, Platform.OS === 'android' ? 500 : 1000))
setCurrentStepName('id-card-fetch')

// Acquire access token
await receivePidUseCase.acquireAccessToken()

// Retrieve Credential
const credential = await receivePidUseCase.retrieveCredential()
await storeCredential(secureUnlock.context.agent, credential)
const parsed = secureUnlock.context.agent.sdJwtVc.fromCompact<
SdJwtVcHeader,
{ given_name: string; family_name: string }
>(credential.compactSdJwtVc)
setUserName(
`${capitalizeFirstLetter(parsed.prettyClaims.given_name.toLowerCase())} ${capitalizeFirstLetter(
parsed.prettyClaims.family_name.toLowerCase()
)}`
)
setCurrentStepName('id-card-complete')
} catch (error) {
reset({ resetToStep: 'id-card-pin', error })
}
Expand All @@ -422,6 +517,18 @@ export function OnboardingContextProvider({
screen = <currentStep.Screen goToNextStep={onStartScanning} />
} else if (currentStep.step === 'id-card-complete') {
screen = <currentStep.Screen goToNextStep={goToNextStep} userName={userName} />
} else if (currentStep.step === 'id-card-scan') {
screen = (
<currentStep.Screen
progress={idCardScanningState.progress}
scanningState={idCardScanningState.state}
isCardAttached={idCardScanningState.isCardAttached}
onCancel={() => {
receivePidUseCase?.cancelIdCardScanning()
}}
showScanModal={idCardScanningState.showScanModal ?? true}
/>
)
} else {
screen = <currentStep.Screen goToNextStep={goToNextStep} />
}
Expand Down
5 changes: 4 additions & 1 deletion apps/ausweis/src/features/onboarding/screens/id-card-pin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ export const OnboardingIdCardPinEnter = forwardRef(({ goToNextStep }: Onboarding
<Input
ref={inputRef}
value={pin}
borderWidth={0}
// borderWidth={0}
// Setting borderWidth to 0 makes it not work on Android (maybe it needs to be 'visible'?)
// So we set it to white, the same as the background
borderColor="white"
zIndex={-10000}
position="absolute"
onBlur={() => inputRef.current?.focus()}
Expand Down
41 changes: 33 additions & 8 deletions apps/ausweis/src/features/onboarding/screens/id-card-scan.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,40 @@
import { Stack } from '@package/ui'
import { NfcScannerModalAndroid, Stack } from '@package/ui'

import { NfcCardScanningPlacementImage } from '@package/ui/src/images/NfcScanningCardPlacementImage'
import { Platform } from 'react-native'

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

export function OnboardingIdCardScan({
progress,
scanningState,
isCardAttached,
onCancel,
showScanModal,
}: OnboardingIdCardScanProps) {
return (
<Stack flex-1>
<Stack flex={3}>
<NfcCardScanningPlacementImage height="80%" />
<>
<Stack flex-1>
<Stack flex={3}>
<NfcCardScanningPlacementImage height="80%" />
</Stack>
{/* This is here to have the same layout as id-card-start-scan */}
<Stack flex-1 />
</Stack>
{/* This is here to have the same layout as id-card-start-scan */}
<Stack flex-1 />
</Stack>
{Platform.OS === 'android' && (
<NfcScannerModalAndroid
onCancel={onCancel}
open={showScanModal}
progress={progress}
scanningState={scanningState === 'scanning' && !isCardAttached ? 'readyToScan' : scanningState}
/>
)}
</>
)
}
Loading