diff --git a/apps/easypid/README.md b/apps/easypid/README.md index ba73c80d..39401d04 100644 --- a/apps/easypid/README.md +++ b/apps/easypid/README.md @@ -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. @@ -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 diff --git a/apps/easypid/src/app/+native-intent.tsx b/apps/easypid/src/app/+native-intent.tsx index ad6ad2c2..8e55b004 100644 --- a/apps/easypid/src/app/+native-intent.tsx +++ b/apps/easypid/src/app/+native-intent.tsx @@ -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 '/' diff --git a/apps/easypid/src/features/menu/FunkeSettingsScreen.tsx b/apps/easypid/src/features/menu/FunkeSettingsScreen.tsx index 720a5a9a..09b1ae7e 100644 --- a/apps/easypid/src/features/menu/FunkeSettingsScreen.tsx +++ b/apps/easypid/src/features/menu/FunkeSettingsScreen.tsx @@ -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 ( @@ -24,7 +27,23 @@ export function FunkeSettingsScreen() { contentContainerStyle={{ minHeight: '85%' }} > - This page is under construction. + + + This page is under construction. More options will be added. + + + + + + + + router.back()}> Back diff --git a/apps/easypid/src/features/receive/FunkeCredentialNotificationScreen.tsx b/apps/easypid/src/features/receive/FunkeCredentialNotificationScreen.tsx index 7c2f8e6b..068dec6c 100644 --- a/apps/easypid/src/features/receive/FunkeCredentialNotificationScreen.tsx +++ b/apps/easypid/src/features/receive/FunkeCredentialNotificationScreen.tsx @@ -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' @@ -63,6 +64,7 @@ export function FunkeCredentialNotificationScreen() { const [errorReason, setErrorReason] = useState() const [isCompleted, setIsCompleted] = useState(false) + const [isDevelopmentModeEnabled] = useDevelopmentMode() const [resolvedCredentialOffer, setResolvedCredentialOffer] = useState() const [resolvedAuthorizationRequest, setResolvedAuthorizationRequest] = @@ -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'] @@ -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) @@ -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 ( @@ -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( @@ -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( @@ -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 () => { @@ -269,7 +299,6 @@ export function FunkeCredentialNotificationScreen() { setIsSharingPresentation(true) if (shouldUsePinForPresentation) { - // TODO: we should handle invalid pin if (!pin) { setErrorReason('Presentation information could not be extracted.') return @@ -277,14 +306,14 @@ export function FunkeCredentialNotificationScreen() { // 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 } } @@ -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) } }, [ @@ -327,6 +356,7 @@ export function FunkeCredentialNotificationScreen() { resolvedCredentialOffer, shouldUsePinForPresentation, toast.show, + setErrorReasonWithError, ] ) @@ -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 ( { - setErrorReason('Authorization cancelled') - }} - onError={() => { - setErrorReason('Authorization failed') - }} + onCancel={onCancelAuthorization} + onError={onErrorAuthorization} /> ), } diff --git a/apps/easypid/src/features/receive/slides/AuthCodeFlowSlide.tsx b/apps/easypid/src/features/receive/slides/AuthCodeFlowSlide.tsx index bff3840c..7a98ca51 100644 --- a/apps/easypid/src/features/receive/slides/AuthCodeFlowSlide.tsx +++ b/apps/easypid/src/features/receive/slides/AuthCodeFlowSlide.tsx @@ -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 @@ -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() + 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 ( diff --git a/apps/easypid/src/features/share/FunkeOpenIdPresentationNotificationScreen.tsx b/apps/easypid/src/features/share/FunkeOpenIdPresentationNotificationScreen.tsx index e48c4c08..553cd1e3 100644 --- a/apps/easypid/src/features/share/FunkeOpenIdPresentationNotificationScreen.tsx +++ b/apps/easypid/src/features/share/FunkeOpenIdPresentationNotificationScreen.tsx @@ -12,6 +12,7 @@ import React, { useEffect, useState, useCallback } from 'react' import { useAppAgent } from '@easypid/agent' import { InvalidPinError } from '@easypid/crypto/error' +import { useDevelopmentMode } from '@easypid/hooks' import { analyzeVerification } from '@easypid/use-cases/ValidateVerification' import type { VerificationAnalysisResponse } from '@easypid/use-cases/ValidateVerification' import { usePushToWallet } from '@package/app/src/hooks/usePushToWallet' @@ -28,6 +29,7 @@ export function FunkeOpenIdPresentationNotificationScreen() { const params = useLocalSearchParams() const pushToWallet = usePushToWallet() const { agent } = useAppAgent() + const [isDevelopmentModeEnabled] = useDevelopmentMode() const [credentialsForRequest, setCredentialsForRequest] = useState() const [isSharing, setIsSharing] = useState(false) @@ -48,6 +50,8 @@ export function FunkeOpenIdPresentationNotificationScreen() { .then(setCredentialsForRequest) .catch((error) => { toast.show('Presentation information could not be extracted.', { + message: + error instanceof Error && isDevelopmentModeEnabled ? `Development mode error: ${error.message}` : undefined, customData: { preset: 'danger' }, }) agent.config.logger.error('Error getting credentials for request', { @@ -56,7 +60,7 @@ export function FunkeOpenIdPresentationNotificationScreen() { pushToWallet() }) - }, [credentialsForRequest, params.data, params.uri, toast.show, agent, pushToWallet, toast]) + }, [credentialsForRequest, params.data, params.uri, toast.show, agent, pushToWallet, toast, isDevelopmentModeEnabled]) const [verificationAnalysis, setVerificationAnalysis] = useState<{ isLoading: boolean @@ -105,24 +109,26 @@ export function FunkeOpenIdPresentationNotificationScreen() { setIsSharing(true) if (shouldUsePin) { - // TODO: we should handle invalid pin if (!pin) { + setIsSharing(false) return { status: 'error', result: { title: 'Authentication failed', }, + redirectToWallet: true, } } // TODO: maybe provide to shareProof method? try { await setWalletServiceProviderPin(pin.split('').map(Number)) } catch (e) { + setIsSharing(false) if (e instanceof InvalidPinError) { return { status: 'error', result: { - title: 'Authentication Failed', + title: 'Invalid PIN entered', }, } } @@ -131,7 +137,10 @@ export function FunkeOpenIdPresentationNotificationScreen() { status: 'error', result: { title: 'Authentication failed', + message: + e instanceof Error && isDevelopmentModeEnabled ? `Development mode error: ${e.message}` : undefined, }, + redirectToWallet: true, } } } @@ -173,11 +182,15 @@ export function FunkeOpenIdPresentationNotificationScreen() { redirectToWallet: true, result: { title: 'Presentation could not be shared.', + message: + error instanceof Error && isDevelopmentModeEnabled + ? `Development mode error: ${error.message}` + : undefined, }, } } }, - [credentialsForRequest, agent, shouldUsePin] + [credentialsForRequest, agent, shouldUsePin, isDevelopmentModeEnabled] ) const onProofDecline = async () => { diff --git a/apps/easypid/src/features/share/slides/PinSlide.tsx b/apps/easypid/src/features/share/slides/PinSlide.tsx index 8be78ee2..adda4a67 100644 --- a/apps/easypid/src/features/share/slides/PinSlide.tsx +++ b/apps/easypid/src/features/share/slides/PinSlide.tsx @@ -24,7 +24,7 @@ export const PinSlide = ({ onPinComplete, isLoading }: PinSlideProps) => { toast.show(r.result.title, { message: r.result.message, - customData: { preset: r.redirectToWallet ? 'danger' : 'warning' }, + customData: { preset: 'danger' }, }) pinRef.current?.shake() @@ -39,7 +39,7 @@ export const PinSlide = ({ onPinComplete, isLoading }: PinSlideProps) => { Send data with your PIN code - Use your security PIN to confirm the request. + Use your app PIN code to confirm the request. - +