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: authenticated screens and pid receiving #130

Merged
merged 7 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
9 changes: 9 additions & 0 deletions apps/funke/agent/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { type FunkeAppAgent, useAgent } from '@package/agent'
import { useSecureUnlock as _useSecureUnlock } from '@package/secure-store/secureUnlock'

export { initializeAppAgent } from './initialize'

export const useAppAgent = useAgent<FunkeAppAgent>
export type AppAgent = FunkeAppAgent

export const useSecureUnlock = () => _useSecureUnlock<{ agent?: AppAgent }>()
12 changes: 12 additions & 0 deletions apps/funke/agent/initialize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { initializeFunkeAgent } from '@package/agent'
import { trustedCertificates } from '../constants'

export function initializeAppAgent({ walletKey }: { walletKey: string }) {
return initializeFunkeAgent({
keyDerivation: 'raw',
walletId: 'funke-wallet',
walletKey: walletKey,
TimoGlastra marked this conversation as resolved.
Show resolved Hide resolved
walletLabel: 'Funke Wallet',
trustedX509Certificates: trustedCertificates,
TimoGlastra marked this conversation as resolved.
Show resolved Hide resolved
})
}
16 changes: 5 additions & 11 deletions apps/funke/app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,14 @@ const variants = {
development: {
bundle: '.dev',
name: ' (Dev)',
trustedCertificates: [
Copy link
Member

Choose a reason for hiding this comment

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

Ill likely encounter it later, but why was this removed?

Copy link
Member Author

Choose a reason for hiding this comment

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

I just couldn't get it to work. The value was not availalbe in the app. So after spending an hour on it, I just moved it to constants file

// https://funke.animo.id
'MIIBAzCBq6ADAgECAhArxq0w60RTDK4WY9HzgcvBMAoGCCqGSM49BAMCMAAwIBcNNzAwMTAxMDAwMDAwWhgPMjI4NjExMjAxNzQ2NDBaMAAwOTATBgcqhkjOPQIBBggqhkjOPQMBBwMiAALcD1XzKepFxWMAOqV+ln1fybBt7DRO5CV0f9A6mRp2xaMlMCMwIQYDVR0RBBowGIYWaHR0cHM6Ly9mdW5rZS5hbmltby5pZDAKBggqhkjOPQQDAgNHADBEAiAfvGG6sqrvzIMWYpJB5VLloo9f51loYXSkKxJIOztlNwIgLLSvEl0Dmp5vtj2buZ2nXQ2RBKxiLbc5eYGeMeoUnjk=',
],
},
preview: {
bundle: '.preview',
name: ' (Preview)',
trustedCertificates: [
// https://funke.animo.id
'MIIBAzCBq6ADAgECAhArxq0w60RTDK4WY9HzgcvBMAoGCCqGSM49BAMCMAAwIBcNNzAwMTAxMDAwMDAwWhgPMjI4NjExMjAxNzQ2NDBaMAAwOTATBgcqhkjOPQIBBggqhkjOPQMBBwMiAALcD1XzKepFxWMAOqV+ln1fybBt7DRO5CV0f9A6mRp2xaMlMCMwIQYDVR0RBBowGIYWaHR0cHM6Ly9mdW5rZS5hbmltby5pZDAKBggqhkjOPQQDAgNHADBEAiAfvGG6sqrvzIMWYpJB5VLloo9f51loYXSkKxJIOztlNwIgLLSvEl0Dmp5vtj2buZ2nXQ2RBKxiLbc5eYGeMeoUnjk=',
],
},
production: {
bundle: '',
name: '',
trustedCertificates: [],
},
}

Expand Down Expand Up @@ -62,6 +53,7 @@ const config = {
fallbackToCacheTimeout: 0,
},
plugins: [
'@animo-id/expo-ausweis-sdk',
[
'expo-font',
{
Expand Down Expand Up @@ -111,11 +103,13 @@ const config = {
})),
],
},
experiments: {
tsconfigPaths: true,
},
extra: {
eas: {
projectId: 'b5f457fa-bcab-4c6e-8092-8cdf1239027a',
projectId: 'todo',
},
trustedCertificates: variant.trustedCertificates,
},
}

Expand Down
File renamed without changes.
37 changes: 37 additions & 0 deletions apps/funke/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Redirect, Stack } from 'expo-router'
import { Text } from 'react-native'

import { useSecureUnlock } from '@/agent'
import { AgentProvider } from '@package/agent'

export default function AppLayout() {
const secureUnlock = useSecureUnlock()

// Wallet is not configured yet. Redirect to onboarding
if (secureUnlock.state === 'not-configured') {
Copy link
Member

Choose a reason for hiding this comment

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

enum would be nicer, can be done in the future as in improvement.

Copy link
Member

Choose a reason for hiding this comment

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

same as with the href routing.

return <Redirect href="/onboarding" />
}

// Wallet is locked. Redirect to authentication screen
if (secureUnlock.state === 'locked') {
return <Redirect href="/authenticate" />
}

// This should show the splash screen
if (secureUnlock.state === 'initializing') {
return <Text>Loading... initializing</Text>
}

if (!secureUnlock.context.agent) {
TimoGlastra marked this conversation as resolved.
Show resolved Hide resolved
// TODO: what to do? Should be set by authentication or onboarding
// Probably authenticate again?
return <Text>Loading... missing agent</Text>
}

// Render the normal wallet, which is everything inside (app)
return (
<AgentProvider agent={secureUnlock.context.agent}>
<Stack />
</AgentProvider>
)
}
70 changes: 70 additions & 0 deletions apps/funke/app/(app)/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { useAppAgent } from '@/agent'
import { addMessageListener } from '@animo-id/expo-ausweis-sdk'
import { Button, Paragraph, XStack, YStack } from '@package/ui'
import { Stack } from 'expo-router'
import { useEffect, useState } from 'react'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { ReceivePidUseCase, type ReceivePidUseCaseState } from '../../use-cases/ReceivePidUseCase'

export default function Screen() {
const { top } = useSafeAreaInsets()
// FIXME: should be useReceivePidUseCase as the state is now not updated....
const [receivePidUseCase, setReceivePidUseCase] = useState<ReceivePidUseCase>()
const [state, setState] = useState<ReceivePidUseCaseState | 'not-initialized'>('not-initialized')
const [credential, setCredential] = useState<string>()
const { agent } = useAppAgent()

useEffect(() => {
const { remove } = addMessageListener((message) => {
TimoGlastra marked this conversation as resolved.
Show resolved Hide resolved
console.log('receive message', JSON.stringify(message, null, 2))
})

return () => remove()
}, [])

useEffect(() => {
ReceivePidUseCase.initialize({ agent, onStateChange: setState }).then((pidUseCase) => {
setReceivePidUseCase(pidUseCase)
})
}, [agent])

const nextStep = async () => {
if (!receivePidUseCase) return
if (state === 'error') return

TimoGlastra marked this conversation as resolved.
Show resolved Hide resolved
if (state === 'acquire-access-token') return
Copy link
Member

Choose a reason for hiding this comment

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

states should also be an enum.

if (state === 'id-card-auth') {
console.log('authenticate using id card')
TimoGlastra marked this conversation as resolved.
Show resolved Hide resolved
await receivePidUseCase.authenticateUsingIdCard()
return
}
if (state === 'retrieve-credential') {
const credential = await receivePidUseCase.retrieveCredential()
setCredential(credential.compactSdJwtVc)
}
}

return (
<>
<Stack.Screen
options={{
headerShown: true,
title: 'Home',
header: () => {
return <XStack h={top} bg="$background" />
},
}}
/>
<YStack>
<Paragraph>State: {state}</Paragraph>
<Button.Solid disabled={state !== 'id-card-auth'} onPress={nextStep}>
ID Card Auth
</Button.Solid>
<Button.Solid disabled={state !== 'retrieve-credential'} onPress={nextStep}>
Retrieve credential
</Button.Solid>
<Paragraph>credential: {credential}</Paragraph>
</YStack>
</>
)
}
154 changes: 12 additions & 142 deletions apps/funke/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,160 +1,30 @@
import type { OpenId4VcHolderAppAgent } from '@package/agent'

import { AgentProvider } from '@package/agent'
import {
DeeplinkHandler,
NoInternetToastProvider,
Provider,
isAndroid,
useTransparentNavigationBar,
} from '@package/app'
import { getLegacySecureWalletKey } from '@package/secure-store/legacyUnlock'
import { Heading, Page, Paragraph, XStack, YStack, config, useToastController } from '@package/ui'
import { NoInternetToastProvider, Provider, useTransparentNavigationBar } from '@package/app'
import { SecureUnlockProvider } from '@package/secure-store/secureUnlock'
import { DefaultTheme, ThemeProvider } from '@react-navigation/native'
import { Stack } from 'expo-router'
import { Slot } from 'expo-router'
import * as SplashScreen from 'expo-splash-screen'
import { useEffect, useState } from 'react'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { useSafeAreaInsets } from 'react-native-safe-area-context'

import { initializeAppAgent } from '.'
import tamaguiConfig from '../tamagui.config'

void SplashScreen.preventAutoHideAsync()

export const unstable_settings = {
// Ensure any route can link back to `/`
initialRouteName: 'index',
initialRouteName: '(app)/index',
}

export default function HomeLayout() {
const [agent, setAgent] = useState<OpenId4VcHolderAppAgent>()

const [agentInitializationFailed, setAgentInitializationFailed] = useState(false)
const toast = useToastController()
const { top } = useSafeAreaInsets()
export default function RootLayout() {
useTransparentNavigationBar()

// Initialize agent
useEffect(() => {
if (agent) return

const startAgent = async () => {
const walletKey = await getLegacySecureWalletKey().catch(() => {
toast.show('Could not load wallet key from secure storage.')
setAgentInitializationFailed(true)
})
if (!walletKey) return

try {
const agent = await initializeAppAgent({
...walletKey,
walletId: 'funke-wallet-secure',
walletLabel: 'funke-wallet',
})

setAgent(agent)
} catch {
setAgentInitializationFailed(true)
toast.show('Could not initialize agent.')
}
if (!agent) return
}

void startAgent()
}, [toast, agent])

// Hide splash screen when agent and fonts are loaded or agent could not be initialized
useEffect(() => {
if (agent || agentInitializationFailed) {
void SplashScreen.hideAsync()
}
}, [agent, agentInitializationFailed])

// Show error screen if agent could not be initialized
if (agentInitializationFailed) {
return (
<Provider config={tamaguiConfig}>
<Page jc="center" ai="center" g="md">
<YStack>
<Heading variant="h1">Error</Heading>
<Paragraph>Could not establish a secure environment. The current device could be not supported.</Paragraph>
</YStack>
</Page>
</Provider>
)
}

// The splash screen will be rendered on top of this
if (!agent) {
return null
}

// On Android, we push down the screen content when the presentation is a Modal
// This is because Android phones render Modals as full screen pages.
const headerModalOptions = isAndroid() && {
headerShown: true,
header: () => {
// Header is translucent by default. See configuration in app.json
return <XStack bg="$background" h={top} />
},
}

return (
<Provider config={tamaguiConfig}>
<GestureHandlerRootView style={{ flex: 1 }}>
<AgentProvider agent={agent}>
<ThemeProvider value={DefaultTheme}>
<NoInternetToastProvider>
<DeeplinkHandler>
<Stack initialRouteName="index" screenOptions={{ headerShown: false }}>
<Stack.Screen
options={{
presentation: 'modal',
// Extra modal options not needed for QR Scanner
}}
name="(home)/scan"
/>
<Stack.Screen
options={{ presentation: 'modal', ...headerModalOptions }}
name="notifications/openIdCredential"
/>
<Stack.Screen
options={{ presentation: 'modal', ...headerModalOptions }}
name="notifications/openIdPresentation"
/>
<Stack.Screen
options={{
headerShown: true,
headerStyle: {
backgroundColor: config.tokens.color.background.val,
},
headerShadowVisible: false,
headerTintColor: config.tokens.color['primary-500'].val,
headerTitle: 'Inbox',
headerTitleAlign: 'center',
headerTitleStyle: {
fontWeight: isAndroid() ? '700' : '500', // Match font weight on android to native back button style
fontSize: 18,
},
}}
name="notifications/inbox"
/>
<Stack.Screen
options={{
headerShown: true,
headerTransparent: true,
headerTintColor: config.tokens.color['primary-500'].val,
headerTitle: '',
}}
name="credentials/[id]"
/>
</Stack>
</DeeplinkHandler>
</NoInternetToastProvider>
</ThemeProvider>
</AgentProvider>
</GestureHandlerRootView>
<SecureUnlockProvider>
<ThemeProvider value={DefaultTheme}>
<NoInternetToastProvider>
<Slot />
</NoInternetToastProvider>
</ThemeProvider>
</SecureUnlockProvider>
</Provider>
)
}
Loading