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

some fixes and updates #253

Merged
merged 7 commits into from
Dec 4, 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
4 changes: 3 additions & 1 deletion apps/easypid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,6 @@ The following standards and specifications were implemented.

## Known Bugs

- Entering incorrect PIN during presentation sharing will get stuck on the PIN loading screen (it does show correct PIN invalid toast).
- You have to force close the app when you use BLE for the first time after enabling location permission because the permission popup does not go away.
- Installing the latest version of the app if you had a previous version of the application can cause you to get stuck in a broken state, even if the application is removed and reinstalled.

Expand All @@ -385,6 +384,9 @@ The following standards and specifications were implemented.
#### 04-12-2024

**Wallet**
- Added a development mode that shows internal error messages for easier debugging by LSPs [commit](https://github.com/animo/paradym-wallet/commit/a1aaf26655456082d15863d6f88edecfecaca598)
- Fixed an issue where the PIN screen would get stuck in a loading state when an incorrect PIN was entered [commit](https://github.com/animo/paradym-wallet/commit/0f65ef98f5f26c3afc0968e4f848bf538a86cfd7)
- Fixed an issue with redirect based auth flow if the authorization flow left the in-app browser (e.g. when requiring authentication using the native AusweisApp with the eID card) [commit](https://github.com/animo/paradym-wallet/commit/eb333b81fe5662cc2f010e1ee9bbdc83a7e19aa3)
- Fixed an issue where the PID setup would get stuck if you skipped it during onboarding [commit](https://github.com/animo/openid4vc-playground-funke/commit/65178e776bc421b9ca413542ea0e86db4ad1ead4)

#### 28-11-2024
Expand Down
16 changes: 16 additions & 0 deletions apps/easypid/src/app/+native-intent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ export async function redirectSystemPath({ path, initial }: { path: string; init
if (!isRecognizedDeeplink) return path

try {
// For the bdr mDL issuer we use authorized code flow, but they also
// redirect to the ausweis app. From the ausweis app we are then redirected
// back to the easypid wallet.
const parsedPath = new URL(path)
const credentialAuthorizationCode = parsedPath.searchParams.get('code')
if (
parsedPath.protocol === 'id.animo.ausweis:' &&
parsedPath.pathname === '/wallet/redirect' &&
credentialAuthorizationCode
) {
// We just set the credentialAuthorizationCode, which should be handled by the browser
// auth session code in the credential screen that is open.
router.setParams({ credentialAuthorizationCode })
return null
}

const parseResult = await parseInvitationUrl(path)
if (!parseResult.success) {
return '/'
Expand Down
23 changes: 21 additions & 2 deletions apps/easypid/src/features/menu/FunkeSettingsScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { Button, FlexPage, Heading, HeroIcons, Paragraph, ScrollView, Stack, YStack } from '@package/ui'
import { Button, FlexPage, Heading, HeroIcons, Paragraph, ScrollView, Stack, XStack, YStack } from '@package/ui'
import React from 'react'
import { useRouter } from 'solito/router'
import { Label, Switch } from 'tamagui'

import { useScrollViewPosition } from '@package/app/src/hooks'
import { useDevelopmentMode } from '../../hooks/useDevelopmentMode'

export function FunkeSettingsScreen() {
const { handleScroll, isScrolledByOffset, scrollEventThrottle } = useScrollViewPosition()
const router = useRouter()
const [isDevelpomentModeEnabled, setIsDevelopmentModeEnabled] = useDevelopmentMode()

return (
<FlexPage gap="$0" paddingHorizontal="$0">
Expand All @@ -24,7 +27,23 @@ export function FunkeSettingsScreen() {
contentContainerStyle={{ minHeight: '85%' }}
>
<YStack fg={1} px="$4" jc="space-between">
<Paragraph color="$grey-700">This page is under construction.</Paragraph>
<YStack>
<Paragraph color="$grey-700" py="$4">
This page is under construction. More options will be added.
</Paragraph>
<XStack jc="space-between" ai="center">
<Label>Development Mode</Label>
<Switch
size="$5"
checked={isDevelpomentModeEnabled}
onCheckedChange={setIsDevelopmentModeEnabled}
animation="quick"
backgroundColor={isDevelpomentModeEnabled ? '$primary-500' : '$primary-300'}
>
<Switch.Thumb animation="quick" backgroundColor="$grey-200" />
</Switch>
</XStack>
</YStack>
<Button.Text color="$primary-500" fontWeight="$semiBold" fontSize="$4" onPress={() => router.back()}>
<HeroIcons.ArrowLeft mr={-4} color="$primary-500" size={20} /> Back
</Button.Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import { useAppAgent } from '@easypid/agent'

import { InvalidPinError } from '@easypid/crypto/error'
import { useDevelopmentMode } from '@easypid/hooks'
import { SlideWizard, usePushToWallet } from '@package/app'
import { useToastController } from '@package/ui'
import { useCallback, useEffect, useState } from 'react'
Expand Down Expand Up @@ -63,6 +64,7 @@ export function FunkeCredentialNotificationScreen() {

const [errorReason, setErrorReason] = useState<string>()
const [isCompleted, setIsCompleted] = useState(false)
const [isDevelopmentModeEnabled] = useDevelopmentMode()

const [resolvedCredentialOffer, setResolvedCredentialOffer] = useState<OpenId4VciResolvedCredentialOffer>()
const [resolvedAuthorizationRequest, setResolvedAuthorizationRequest] =
Expand Down Expand Up @@ -96,6 +98,17 @@ export function FunkeCredentialNotificationScreen() {
: {}
)

const setErrorReasonWithError = useCallback(
(baseMessage: string, error: unknown) => {
if (isDevelopmentModeEnabled && error instanceof Error) {
setErrorReason(`${baseMessage}\n\nDevelopment mode error:\n${error.message}`)
} else {
setErrorReason(baseMessage)
}
},
[isDevelopmentModeEnabled]
)

const shouldUsePinForPresentation = useShouldUsePinForSubmission(credentialsForRequest)
const preAuthGrant =
resolvedCredentialOffer?.credentialOfferPayload.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']
Expand All @@ -110,7 +123,17 @@ export function FunkeCredentialNotificationScreen() {
// )

useEffect(() => {
resolveOpenId4VciOffer({ agent, offer: params, authorization })
resolveOpenId4VciOffer({
agent,
offer: {
// NOTE: the params can contain more than data and uri
// so it's important we only use these params, so the use
// effect doesn't run again the data nd uri
data: params.data,
uri: params.uri,
},
authorization,
})
.then(({ resolvedAuthorizationRequest, resolvedCredentialOffer }) => {
setResolvedCredentialOffer(resolvedCredentialOffer)
setResolvedAuthorizationRequest(resolvedAuthorizationRequest)
Expand All @@ -119,9 +142,9 @@ export function FunkeCredentialNotificationScreen() {
agent.config.logger.error(`Couldn't resolve OpenID4VCI offer`, {
error,
})
setErrorReason('Credential information could not be extracted')
setErrorReasonWithError('Credential information could not be extracted', error)
})
}, [params, agent])
}, [params.data, params.uri, agent, setErrorReasonWithError])

const retrieveCredentials = useCallback(
async (
Expand Down Expand Up @@ -192,10 +215,17 @@ export function FunkeCredentialNotificationScreen() {
agent.config.logger.error(`Couldn't receive credential from OpenID4VCI offer`, {
error,
})
setErrorReason('Error while retrieving credentials')
setErrorReasonWithError('Error while retrieving credentials', error)
}
},
[resolvedCredentialOffer, resolvedAuthorizationRequest, retrieveCredentials, agent, configurationId]
[
resolvedCredentialOffer,
resolvedAuthorizationRequest,
retrieveCredentials,
agent,
configurationId,
setErrorReasonWithError,
]
)

const acquireCredentialsPreAuth = useCallback(
Expand All @@ -216,10 +246,10 @@ export function FunkeCredentialNotificationScreen() {
agent.config.logger.error(`Couldn't receive credential from OpenID4VCI offer`, {
error,
})
setErrorReason('Error while retrieving credentials')
setErrorReasonWithError('Error while retrieving credentials', error)
}
},
[resolvedCredentialOffer, agent, retrieveCredentials, configurationId]
[resolvedCredentialOffer, agent, retrieveCredentials, configurationId, setErrorReasonWithError]
)

const parsePresentationRequestUrl = useCallback(
Expand All @@ -230,12 +260,12 @@ export function FunkeCredentialNotificationScreen() {
})
.then(setCredentialsForRequest)
.catch((error) => {
setErrorReason('Presentation information could not be extracted.')
setErrorReasonWithError('Presentation information could not be extracted.', error)
agent.config.logger.error('Error getting credentials for request', {
error,
})
}),
[agent]
[agent, setErrorReasonWithError]
)

const onCheckCardContinue = useCallback(async () => {
Expand Down Expand Up @@ -269,22 +299,21 @@ export function FunkeCredentialNotificationScreen() {
setIsSharingPresentation(true)

if (shouldUsePinForPresentation) {
// TODO: we should handle invalid pin
if (!pin) {
setErrorReason('Presentation information could not be extracted.')
return
}
// TODO: maybe provide to shareProof method?
try {
await setWalletServiceProviderPin(pin.split('').map(Number))
} catch (e) {
if (e instanceof InvalidPinError) {
toast.show(e.message, { customData: { preset: 'danger' } })
} catch (error) {
if (error instanceof InvalidPinError) {
toast.show('Invalid PIN entered', { customData: { preset: 'danger' } })
setIsSharingPresentation(false)
return { status: 'error', result: { title: e.message }, redirectToWallet: false }
return { status: 'error', result: { title: error.message }, redirectToWallet: false }
}

setErrorReason('Presentation information could not be extracted.')
setErrorReasonWithError('Presentation information could not be extracted', error)
return
}
}
Expand Down Expand Up @@ -316,7 +345,7 @@ export function FunkeCredentialNotificationScreen() {
agent.config.logger.error('Error accepting presentation', {
error,
})
setErrorReason('Presentation could not be shared.')
setErrorReasonWithError('Presentation could not be shared.', error)
}
},
[
Expand All @@ -327,6 +356,7 @@ export function FunkeCredentialNotificationScreen() {
resolvedCredentialOffer,
shouldUsePinForPresentation,
toast.show,
setErrorReasonWithError,
]
)

Expand All @@ -343,6 +373,10 @@ export function FunkeCredentialNotificationScreen() {
resolvedCredentialOffer &&
resolvedAuthorizationRequest?.authorizationFlow === OpenId4VciAuthorizationFlow.Oauth2Redirect

// These are callbacks to not change on every render
const onCancelAuthorization = useCallback(() => setErrorReason('Authorization cancelled'), [])
const onErrorAuthorization = useCallback(() => setErrorReason('Authorization failed'), [])

return (
<SlideWizard
steps={[
Expand Down Expand Up @@ -384,12 +418,8 @@ export function FunkeCredentialNotificationScreen() {
domain: resolvedCredentialOffer.metadata.credentialIssuer.credential_issuer,
}}
onAuthFlowCallback={acquireCredentialsAuth}
onCancel={() => {
setErrorReason('Authorization cancelled')
}}
onError={() => {
setErrorReason('Authorization failed')
}}
onCancel={onCancelAuthorization}
onError={onErrorAuthorization}
/>
),
}
Expand Down
59 changes: 45 additions & 14 deletions apps/easypid/src/features/receive/slides/AuthCodeFlowSlide.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useHasInternetConnection, useWizard } from '@package/app'
import { DualResponseButtons } from '@package/app/src/components/DualResponseButtons'
import { Heading, MiniCardRowItem, Paragraph, YStack, useToastController } from '@package/ui'
import { useGlobalSearchParams } from 'expo-router'
import * as WebBrowser from 'expo-web-browser'
import type { CredentialDisplay } from 'packages/agent/src'
import { useEffect, useState } from 'react'

export type AuthCodeFlowDetails = {
domain: string
Expand All @@ -28,26 +30,55 @@ export const AuthCodeFlowSlide = ({
const toast = useToastController()
const { onNext, onCancel: wizardOnCancel } = useWizard()
const hasInternet = useHasInternetConnection()
const { credentialAuthorizationCode } = useGlobalSearchParams<{ credentialAuthorizationCode?: string }>()
const [browserResult, setBrowserResult] = useState<WebBrowser.WebBrowserAuthSessionResult>()
const [hasHandledResult, setHasHandledResult] = useState(false)

const onPressContinue = async () => {
const result = await WebBrowser.openAuthSessionAsync(authCodeFlowDetails.openUrl, authCodeFlowDetails.redirectUri)
useEffect(() => {
if (hasHandledResult) return

if (result.type !== 'success') {
toast.show('Authorization failed', { customData: { preset: 'warning' } })
// NOTE: credentialAuthorizationCode is set in +native-intent
// after an external browser or app redirects back to us. In some
// cases the in-app browser is exited (e.g. when authenticating from
// a native app) and thus we need to manually dimiss the auth session
// and instead use the auth code from there.
if (credentialAuthorizationCode) {
WebBrowser.dismissAuthSession()
setHasHandledResult(true)
onNext()
onAuthFlowCallback(credentialAuthorizationCode)
} else if (browserResult) {
if (browserResult.type !== 'success') {
toast.show('Authorization failed', { customData: { preset: 'warning' } })

result.type === 'cancel' || result.type === 'dismiss' ? onCancel() : onError()
return
}
browserResult.type === 'cancel' || browserResult.type === 'dismiss' ? onCancel() : onError()
return
}

const authorizationCode = new URL(result.url).searchParams.get('code')
if (!authorizationCode) {
toast.show('Authorization failed', { customData: { preset: 'warning' } })
onError()
return
const authorizationCode = new URL(browserResult.url).searchParams.get('code')
if (!authorizationCode) {
toast.show('Authorization failed', { customData: { preset: 'warning' } })
onError()
return
}

onNext()
onAuthFlowCallback(authorizationCode)
}
}, [
browserResult,
hasHandledResult,
credentialAuthorizationCode,
onAuthFlowCallback,
toast.show,
onCancel,
onError,
onNext,
])

onNext()
onAuthFlowCallback(authorizationCode)
const onPressContinue = async () => {
const result = await WebBrowser.openAuthSessionAsync(authCodeFlowDetails.openUrl, authCodeFlowDetails.redirectUri)
setBrowserResult(result)
}

return (
Expand Down
Loading