Skip to content

Commit

Permalink
some fixes and updates (#253)
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra authored Dec 4, 2024
1 parent 5aaad86 commit 50a1f2f
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 46 deletions.
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

0 comments on commit 50a1f2f

Please sign in to comment.