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: paradym wallet merge #262

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions apps/easypid/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
EXPO_PUBLIC_WALLET_SERVICE_PROVIDER_URL=
EXPO_PUBLIC_APP_TYPE=PARADYM_WALLET # or FUNKE_WALLET
11 changes: 8 additions & 3 deletions apps/easypid/app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ const variants = {
development: {
bundle: '.dev',
name: ' (Dev)',
mediatorDid: 'did:web:mediator.dev.paradym.id',
},
preview: {
bundle: '.preview',
name: ' (Preview)',
mediatorDid: 'did:web:mediator.paradym.id',
},
production: {
bundle: '',
name: '',
mediatorDid: 'did:web:mediator.paradym.id',
},
}

Expand Down Expand Up @@ -115,9 +118,10 @@ const config = {
supportsTablet: false,
bundleIdentifier: `id.animo.ausweis${variant.bundle}`,
infoPlist: {
NSPhotoLibraryUsageDescription: 'EasyPID uses the camera to initiate receiving and sharing of credentials.',
NSCameraUsageDescription: 'EasyPID uses the camera to initiate receiving and sharing of credentials.',
NSFaceIDUsageDescription: 'EasyPID uses FaceID to securely unlock the wallet and share credentials.',
NSPhotoLibraryUsageDescription:
'Paradym Wallet uses the camera to initiate receiving and sharing of credentials.',
NSCameraUsageDescription: 'Paradym Wallet uses the camera to initiate receiving and sharing of credentials.',
NSFaceIDUsageDescription: 'Paradym Wallet uses FaceID to securely unlock the wallet and share credentials.',
ITSAppUsesNonExemptEncryption: false,
// Add schemes for deep linking
CFBundleURLTypes: [
Expand Down Expand Up @@ -169,6 +173,7 @@ const config = {
eas: {
projectId: '28b058bb-3c4b-4347-8e72-41dfc1dd99eb',
},
mediatorDid: variant.mediatorDid,
},
}

Expand Down
4 changes: 3 additions & 1 deletion apps/easypid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@react-native-masked-view/masked-view": "0.3.1",
"@react-navigation/native": "^6.1.6",
"babel-plugin-module-resolver": "^4.1.0",
"buffer": "6.0.3",
"burnt": "^0.12.2",
"expo": "~51.0.39",
"expo-blur": "^13.0.2",
Expand All @@ -47,6 +48,7 @@
"expo-router": "~3.5.24",
"expo-secure-store": "~13.0.1",
"expo-splash-screen": "~0.27.7",
"expo-standard-web-crypto": "catalog:",
"expo-status-bar": "~1.12.1",
"expo-system-ui": "~3.0.6",
"expo-updates": "~0.25.16",
Expand All @@ -70,7 +72,7 @@
},
"devDependencies": {
"@babel/core": "^7.24.4",
"@tamagui/babel-plugin": "1.122.6",
"@tamagui/babel-plugin": "1.122.7",
"babel-plugin-syntax-hermes-parser": "^0.25.1",
"expo-build-properties": "^0.12.5",
"typescript": "catalog:"
Expand Down
41 changes: 37 additions & 4 deletions apps/easypid/src/agent/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,43 @@
import { type EasyPIDAppAgent, useAgent } from '@package/agent'
import { useAgent } from '@package/agent'
import {
type EasyPIDAppAgent,
type EitherAgent,
type ParadymAppAgent,
isEasyPIDAgent,
isParadymAgent,
} from '@package/agent/src/agent'
import { useSecureUnlock as _useSecureUnlock } from '@package/secure-store/secureUnlock'

export { initializeAppAgent } from './initialize'

export const useAppAgent = useAgent<EasyPIDAppAgent>
export type AppAgent = EasyPIDAppAgent
export type SecureUnlockContext = { agent: AppAgent }
export const useAppAgent = useAgent<EitherAgent>
export type AppAgent = EitherAgent
export type SecureUnlockContext = { agent: EitherAgent }

export const useEasyPIDAgent = () => {
const agent = useAppAgent()
if (!isEasyPIDAgent(agent.agent)) {
throw new Error('Expected EasyPID agent')
}

const { agent: originalAgent, ...rest } = agent
return {
agent: originalAgent as EasyPIDAppAgent,
...rest,
}
}

export const useParadymAgent = () => {
const agent = useAppAgent()
if (!isParadymAgent(agent.agent)) {
throw new Error('Expected Paradym agent')
}

const { agent: originalAgent, ...rest } = agent
return {
agent: originalAgent as ParadymAppAgent,
...rest,
}
}

export const useSecureUnlock = () => _useSecureUnlock<SecureUnlockContext>()
48 changes: 30 additions & 18 deletions apps/easypid/src/agent/initialize.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,51 @@
import { setFallbackSecureEnvironment, shouldUseFallbackSecureEnvironment } from '@animo-id/expo-secure-environment'
import { trustedX509Certificates } from '@easypid/constants'
import { WalletServiceProviderClient } from '@easypid/crypto/WalletServiceProviderClient'
import { isFunkeWallet } from '@easypid/hooks/useFeatureFlag'
import { initializeEasyPIDAgent } from '@package/agent'
import { initializeParadymAgent, isEasyPIDAgent } from '@package/agent/src/agent'
import { getShouldUseCloudHsm } from '../features/onboarding/useShouldUseCloudHsm'

export async function initializeAppAgent({
walletKey,
walletKeyVersion,
registerWallet,
}: { walletKey: string; walletKeyVersion: number; registerWallet?: boolean }) {
const agent = await initializeEasyPIDAgent({
keyDerivation: 'raw',
walletId: `easypid-wallet-${walletKeyVersion}`,
walletKey,
walletLabel: 'EasyPID Wallet',
trustedX509Certificates,
})
const agent = isFunkeWallet()
? await initializeEasyPIDAgent({
keyDerivation: 'raw',
walletId: `easypid-wallet-${walletKeyVersion}`,
walletKey,
walletLabel: 'EasyPID Wallet',
trustedX509Certificates,
})
: await initializeParadymAgent({
keyDerivation: 'raw',
walletId: 'paradym-wallet-secure',
walletKey,
walletLabel: 'Paradym Wallet',
trustedX509Certificates,
})

/**
*
* Setup specific for the Wallet Service provider
*
*/
const wsp = new WalletServiceProviderClient(
process.env.EXPO_PUBLIC_WALLET_SERVICE_PROVIDER_URL ?? 'https://wsp.funke.animo.id',
agent
)
if (registerWallet) {
await wsp.createSalt()
await wsp.register()
}
if (isEasyPIDAgent(agent)) {
const wsp = new WalletServiceProviderClient(
process.env.EXPO_PUBLIC_WALLET_SERVICE_PROVIDER_URL ?? 'https://wsp.funke.animo.id',
agent
)
if (registerWallet) {
await wsp.createSalt()
await wsp.register()
}

const shouldUseCloudHsm = getShouldUseCloudHsm()
if (shouldUseCloudHsm) shouldUseFallbackSecureEnvironment(true)
setFallbackSecureEnvironment(wsp)
const shouldUseCloudHsm = getShouldUseCloudHsm()
if (shouldUseCloudHsm) shouldUseFallbackSecureEnvironment(true)
setFallbackSecureEnvironment(wsp)
}

return agent
}
35 changes: 32 additions & 3 deletions apps/easypid/src/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,30 @@ import { Redirect, Stack, useGlobalSearchParams, usePathname, useRouter } from '

import { TypedArrayEncoder } from '@credo-ts/core'
import { useSecureUnlock } from '@easypid/agent'
import { mediatorDid } from '@easypid/constants'
import { activityStorage } from '@easypid/features/activity/activityRecord'
import { useHasFinishedOnboarding } from '@easypid/features/onboarding'
import { useFeatureFlag } from '@easypid/hooks/useFeatureFlag'
import { resetWallet, useResetWalletDevMenu } from '@easypid/utils/resetWallet'
import { AgentProvider, WalletJsonStoreProvider } from '@package/agent'
import { type CredentialDataHandlerOptions, DeeplinkHandler, useHaptics } from '@package/app'
import { AgentProvider, type InvitationType, WalletJsonStoreProvider, useMediatorSetup } from '@package/agent'
import { isParadymAgent } from '@package/agent/src/agent'
import { type CredentialDataHandlerOptions, useHaptics, useHasInternetConnection } from '@package/app'
import { HeroIcons, IconContainer } from '@package/ui'
import { useEffect, useState } from 'react'
import { useTheme } from 'tamagui'

const jsonRecordIds = [activityStorage.recordId]

const isDIDCommEnabled = useFeatureFlag('DIDCOMM')

// When deeplink routing we want to push
export const credentialDataHandlerOptions = {
routeMethod: 'push',
allowedInvitationTypes: [
'openid-credential-offer',
'openid-authorization-request',
...(isDIDCommEnabled ? (['didcomm'] as InvitationType[]) : []),
],
} satisfies CredentialDataHandlerOptions

export default function AppLayout() {
Expand All @@ -27,6 +37,7 @@ export default function AppLayout() {
const [redirectAfterUnlocked, setRedirectAfterUnlocked] = useState<string>()
const pathname = usePathname()
const params = useGlobalSearchParams()
const hasInternetConnection = useHasInternetConnection()

// It could be that the onboarding is cut of mid-process, and e.g. the user closes the app
// if this is the case we will redo the onboarding
Expand All @@ -36,6 +47,16 @@ export default function AppLayout() {
secureUnlock.state !== 'not-configured' && secureUnlock.state !== 'initializing' && !hasFinishedOnboarding
const isWalletLocked = secureUnlock.state === 'locked' || secureUnlock.state === 'acquired-wallet-key'

// Only setup mediation if the agent is a paradym agent
useMediatorSetup({
agent:
secureUnlock.state === 'unlocked' && isParadymAgent(secureUnlock.context.agent) && isDIDCommEnabled
? secureUnlock.context.agent
: undefined,
hasInternetConnection,
mediatorDid,
})

useEffect(() => {
// Reset state
if (hasResetWallet && !shouldResetWallet) {
Expand All @@ -48,7 +69,7 @@ export default function AppLayout() {
resetWallet(secureUnlock)
}, [secureUnlock, hasResetWallet, shouldResetWallet])

// If we are intializing and the wallet was opened using a deeplinkg we will be redirected
// If we are initializing and the wallet was opened using a deeplink we will be redirected
// to the authentication screen. We first save the redirection url and use that when navigation
// to the auth screen
if (
Expand Down Expand Up @@ -132,6 +153,14 @@ export default function AppLayout() {
gestureEnabled: false,
}}
/>

<Stack.Screen
name="notifications/didcomm"
options={{
gestureEnabled: false,
}}
/>

<Stack.Screen name="credentials/index" options={headerNormalOptions} />
<Stack.Screen name="credentials/[id]/index" options={headerNormalOptions} />
<Stack.Screen name="credentials/[id]/attributes" options={headerNormalOptions} />
Expand Down
5 changes: 5 additions & 0 deletions apps/easypid/src/app/(app)/notifications/didComm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { DidCommNotificationScreen } from '@easypid/features/didcomm/DidCommNotificationScreen'

export default function Screen() {
return <DidCommNotificationScreen />
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FunkeCredentialNotificationScreen } from '@easypid/features/receive/FunkeCredentialNotificationScreen'
import { FunkeCredentialNotificationScreen } from '@easypid/features/receive/FunkeOpenIdCredentialNotificationScreen'

export default function Screen() {
return <FunkeCredentialNotificationScreen />
Expand Down
2 changes: 1 addition & 1 deletion apps/easypid/src/app/+native-intent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export async function redirectSystemPath({ path, initial }: { path: string; init
redirectPath = `/(app)/notifications/openIdPresentation?${invitationData.format === 'url' ? 'uri' : 'data'}=${encodeURIComponent(invitationData.format === 'parsed' ? JSON.stringify(invitationData.data) : (invitationData.data as string))}`
}
if (invitationData.type === 'didcomm') {
redirectPath = `/notifications/didcomm?${invitationData.format === 'url' ? 'invitationUrl' : 'invitation'}=${encodeURIComponent(invitationData.format === 'parsed' ? JSON.stringify(invitationData.data) : (invitationData.data as string))}`
redirectPath = `/(app)/notifications/didcomm?${invitationData.format === 'url' ? 'invitationUrl' : 'invitation'}=${encodeURIComponent(invitationData.format === 'parsed' ? JSON.stringify(invitationData.data) : (invitationData.data as string))}`
}

if (redirectPath) {
Expand Down
36 changes: 36 additions & 0 deletions apps/easypid/src/config/appType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { APP_CONFIGS } from './features'

export type AppType = keyof typeof APP_CONFIGS
export const appTypes = Object.keys(APP_CONFIGS)

const getAppType = (): AppType => {
let appType = process.env.EXPO_PUBLIC_APP_TYPE as AppType

if (!appType) {
console.warn('⚠️ EXPO_PUBLIC_APP_TYPE not set, falling back to PARADYM_WALLET')
appType = 'PARADYM_WALLET'
}

if (!appTypes.includes(appType)) {
console.warn(`⚠️ EXPO_PUBLIC_APP_TYPE is not a valid app type: ${appType}. Falling back to PARADYM_WALLET.`)
appType = 'PARADYM_WALLET'
}

const features = APP_CONFIGS[appType]
const sortedFeatures = Object.entries(features).sort(([, a], [, b]) => (b ? 1 : 0) - (a ? 1 : 0))

console.log(`
🔧 App Configuration
━━━━━━━━━━━━━━━━━━━━
📱 App Type: ${appType}
⚙️ Features:${sortedFeatures
.map(
([key, value]) => `
- ${key}: ${value ? '✅' : '❌'}`
)
.join('')}`)

return appType as AppType
}

export const CURRENT_APP_TYPE = getAppType()
22 changes: 22 additions & 0 deletions apps/easypid/src/config/copy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { type AppType, CURRENT_APP_TYPE } from './appType'

export const copy = {
FUNKE_WALLET: {
about: {
description: `This app was created by Animo Solutions in the context of the SPRIN-D Funke ‘EUDI Wallet Prototypes’. It
serves as a prototype for future wallet providers. All code is available under Apache 2.0.`,
emailHeader: 'Reach out from Funke EUDI Wallet',
},
},
PARADYM_WALLET: {
about: {
description:
'This app was created by Animo Solutions as a companion app for Paradym. All code is available under Apache 2.0.',
emailHeader: 'Reach out from Paradym Wallet',
},
},
} satisfies Record<AppType, Record<string, unknown>>

export function useAppCopy() {
return copy[CURRENT_APP_TYPE]
}
20 changes: 20 additions & 0 deletions apps/easypid/src/config/features.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export const APP_CONFIGS = {
FUNKE_WALLET: {
EID_CARD: true,
AI_ANALYSIS: true,
DIDCOMM: false,
},
PARADYM_WALLET: {
EID_CARD: false,
AI_ANALYSIS: false,
DIDCOMM: true,
},
} satisfies Record<string, Features>

export interface Features {
EID_CARD: boolean
AI_ANALYSIS: boolean
DIDCOMM: boolean
}

export type FeatureKey = keyof Features
10 changes: 10 additions & 0 deletions apps/easypid/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
import ExpoConstants from 'expo-constants'

const MEDIATOR_DID = ExpoConstants.expoConfig?.extra?.mediatorDid as string | undefined

if (!MEDIATOR_DID || typeof MEDIATOR_DID !== 'string') {
throw new Error('Mediator DID not found in expo config')
}

export const mediatorDid = MEDIATOR_DID

export const EASYPID_WALLET_PID_PIN_KEY_ID = 'EASYPID_WALLET_PID_PIN_KEY_ID_NO_BIOMETRICS'
export const EASYPID_WALLET_INSTANCE_LONG_TERM_AES_KEY_ID = 'EASYPID_WALLET_INSTANCE_LONG_TERM_AES_KEY_ID'

Expand Down
Loading