Skip to content

Commit d5f3614

Browse files
committed
fix(dc-api): show errors before closing sheet
1 parent cbbbf5e commit d5f3614

2 files changed

Lines changed: 136 additions & 64 deletions

File tree

apps/easypid/src/features/receive/slides/InteractionErrorSlide.tsx

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
import { useLingui } from '@lingui/react/macro'
22
import { commonMessages } from '@package/translations'
33
import { Button, Heading, HeroIcons, Paragraph, ScrollView, Stack, XStack, YStack } from '@package/ui'
4+
import type { ReactNode } from 'react'
45
import { useState } from 'react'
6+
import { useWindowDimensions } from 'react-native'
57

68
interface InteractionErrorSlideProps {
79
reason?: string
810
flowType: 'issue' | 'verify' | 'connect' | 'sign'
911
onCancel: () => void
12+
layout?: 'full-screen' | 'content'
13+
buttonLabel?: ReactNode
1014
}
1115

12-
export const InteractionErrorSlide = ({ reason, onCancel, flowType }: InteractionErrorSlideProps) => {
16+
export const InteractionErrorSlide = ({
17+
reason,
18+
onCancel,
19+
flowType,
20+
layout = 'full-screen',
21+
buttonLabel,
22+
}: InteractionErrorSlideProps) => {
1323
const { t } = useLingui()
24+
const { height } = useWindowDimensions()
1425
const [scrollViewHeight, setScrollViewHeight] = useState(0)
26+
const isContentLayout = layout === 'content'
1527

1628
const message =
1729
flowType === 'connect'
@@ -42,9 +54,21 @@ export const InteractionErrorSlide = ({ reason, onCancel, flowType }: Interactio
4254
})
4355

4456
return (
45-
<YStack fg={1} jc="space-between">
46-
<YStack gap="$6" fg={1} onLayout={(event) => setScrollViewHeight(event.nativeEvent.layout.height)}>
47-
<ScrollView fg={1} maxHeight={scrollViewHeight} contentContainerStyle={{ gap: '$4' }}>
57+
<YStack
58+
fg={isContentLayout ? undefined : 1}
59+
jc={isContentLayout ? undefined : 'space-between'}
60+
gap={isContentLayout ? '$6' : undefined}
61+
>
62+
<YStack
63+
gap="$6"
64+
fg={isContentLayout ? undefined : 1}
65+
onLayout={(event) => setScrollViewHeight(event.nativeEvent.layout.height)}
66+
>
67+
<ScrollView
68+
fg={isContentLayout ? undefined : 1}
69+
maxHeight={isContentLayout ? height * 0.55 : scrollViewHeight}
70+
contentContainerStyle={{ gap: '$4' }}
71+
>
4872
<YStack gap="$4">
4973
<Heading>{t(commonMessages.somethingWentWrong)}</Heading>
5074
<Stack alignSelf="flex-start">
@@ -55,7 +79,7 @@ export const InteractionErrorSlide = ({ reason, onCancel, flowType }: Interactio
5579
<Paragraph>{message}</Paragraph>
5680
</YStack>
5781

58-
{reason && scrollViewHeight !== 0 && (
82+
{reason && (scrollViewHeight !== 0 || isContentLayout) && (
5983
<YStack>
6084
<Paragraph variant="sub">
6185
<Paragraph variant="caption">
@@ -74,7 +98,7 @@ export const InteractionErrorSlide = ({ reason, onCancel, flowType }: Interactio
7498

7599
<Stack borderTopWidth="$0.5" borderColor="$grey-200" py="$4" mx="$-4" px="$4">
76100
<Button.Solid scaleOnPress onPress={onCancel}>
77-
{t(commonMessages.goToWallet)} <HeroIcons.ArrowRight size={20} color="$white" />
101+
{buttonLabel ?? t(commonMessages.goToWallet)} <HeroIcons.ArrowRight size={20} color="$white" />
78102
</Button.Solid>
79103
</Stack>
80104
</YStack>
Lines changed: 106 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
1+
import { sendErrorResponse } from '@animo-id/expo-digital-credentials-api'
12
import { type OnWalletAuthSubmitProps, WalletFlowAuthPrompt } from '@easypid/components/WalletFlowAuthPrompt'
23
import { paradymWalletSdkOptions } from '@easypid/config/paradym'
34
import { setupWalletServiceProvider, setWalletServiceProviderPin } from '@easypid/crypto/WalletServiceProviderClient'
45
import { useShouldUseCloudHsm } from '@easypid/features/onboarding/useShouldUseCloudHsm'
6+
import { useDevelopmentMode } from '@easypid/hooks'
57
import type { SubmissionAuthorizationMode } from '@easypid/hooks/useSubmissionAuthorizationMode'
68
import {
79
authorizeWalletFlow,
810
clearWalletFlowAuthorization,
911
isWalletAuthPromptError,
1012
} from '@easypid/utils/authorizeWalletFlow'
11-
import { TranslationProvider } from '@package/translations'
13+
import { useLingui } from '@lingui/react/macro'
14+
import { commonMessages, TranslationProvider } from '@package/translations'
1215
import { Stack, TamaguiProvider, YStack } from '@package/ui'
1316
import { type DigitalCredentialsRequest, ParadymWalletSdk, useParadym } from '@paradym/wallet-sdk'
1417
import { useEffect, useRef, useState } from 'react'
1518
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context'
1619
import tamaguiConfig from '../../../tamagui.config'
1720
import { useStoredLocale } from '../../hooks/useStoredLocale'
21+
import { InteractionErrorSlide } from '../receive/slides/InteractionErrorSlide'
1822

1923
type DcApiSharingScreenProps = {
2024
request: DigitalCredentialsRequest
@@ -39,11 +43,15 @@ export function DcApiSharingScreen({ request }: DcApiSharingScreenProps) {
3943
}
4044

4145
export function DcApiSharingScreenWithContext({ request }: DcApiSharingScreenProps) {
46+
const { t } = useLingui()
4247
const [isProcessing, setIsProcessing] = useState(false)
48+
const [errorReason, setErrorReason] = useState<string>()
4349
const cloudHsmPinRef = useRef<string | undefined>(undefined)
4450
const onAuthorizationErrorRef = useRef<(() => void) | undefined>(undefined)
51+
const errorResponseMessageRef = useRef('Unable to share credentials')
4552
const insets = useSafeAreaInsets()
4653
const paradym = useParadym()
54+
const [isDevelopmentModeEnabled] = useDevelopmentMode()
4755
const [shouldUseCloudHsmValue] = useShouldUseCloudHsm()
4856
const shouldUseCloudHsm = shouldUseCloudHsmValue === true
4957
const authorizationMode: Exclude<SubmissionAuthorizationMode, 'none'> = shouldUseCloudHsm
@@ -52,40 +60,62 @@ export function DcApiSharingScreenWithContext({ request }: DcApiSharingScreenPro
5260
const isAuthorizing =
5361
isProcessing || paradym.state === 'acquired-wallet-key' || (paradym.state === 'locked' && paradym.isUnlocking)
5462

63+
const setFlowError = ({
64+
reason,
65+
error,
66+
responseMessage,
67+
}: {
68+
reason: string
69+
error: unknown
70+
responseMessage: string
71+
}) => {
72+
errorResponseMessageRef.current = responseMessage
73+
const errorMessage =
74+
error instanceof Error && isDevelopmentModeEnabled ? `Development mode error: ${error.message}` : undefined
75+
76+
setErrorReason(errorMessage ? `${reason}\n${errorMessage}` : reason)
77+
}
78+
5579
const onShareResponse = async (sdk: ParadymWalletSdk) => {
56-
const resolvedRequest = await sdk.dcApi
57-
.resolveRequest({ request })
58-
.then((resolvedRequest) => {
59-
// We can't share multiple documents at the moment
60-
if (resolvedRequest.formattedSubmission.entries.length > 1) {
61-
throw new Error('Multiple cards requested, but only one card can be shared with the digital credentials api.')
62-
}
80+
let resolvedRequest: Awaited<ReturnType<typeof sdk.dcApi.resolveRequest>>
6381

64-
return resolvedRequest
65-
})
66-
.catch((error) => {
67-
sdk.logger.error('Error getting credentials for dc api request', {
68-
error,
69-
})
82+
try {
83+
resolvedRequest = await sdk.dcApi.resolveRequest({ request })
7084

71-
// Not shown to the user
72-
sdk.dcApi.sendErrorResponse('Presentation information could not be extracted')
85+
// We can't share multiple documents at the moment
86+
if (resolvedRequest.formattedSubmission.entries.length > 1) {
87+
throw new Error('Multiple cards requested, but only one card can be shared with the digital credentials api.')
88+
}
89+
} catch (error) {
90+
sdk.logger.error('Error getting credentials for dc api request', {
91+
error,
7392
})
7493

75-
if (!resolvedRequest) return
94+
setFlowError({
95+
reason: t(commonMessages.presentationInformationCouldNotBeExtracted),
96+
error,
97+
responseMessage: 'Presentation information could not be extracted',
98+
})
99+
return false
100+
}
76101

77102
// Once this returns we just assume it's successful
78103
try {
79104
await sdk.dcApi.sendResponse({
80105
dcRequest: request,
81106
resolvedRequest,
82107
})
108+
109+
return true
83110
} catch (error) {
84111
sdk.logger.error('Could not share response', { error })
85112

86-
// Not shown to the user
87-
sdk.dcApi.sendErrorResponse('Unable to share credentials')
88-
return
113+
setFlowError({
114+
reason: t(commonMessages.presentationCouldNotBeShared),
115+
error,
116+
responseMessage: 'Unable to share credentials',
117+
})
118+
return false
89119
}
90120
}
91121

@@ -110,7 +140,11 @@ export function DcApiSharingScreenWithContext({ request }: DcApiSharingScreenPro
110140
return
111141
}
112142

113-
throw error
143+
setFlowError({
144+
reason: t(commonMessages.presentationCouldNotBeShared),
145+
error,
146+
responseMessage: 'Unable to share credentials',
147+
})
114148
})
115149
.finally(() => {
116150
cloudHsmPinRef.current = undefined
@@ -123,25 +157,25 @@ export function DcApiSharingScreenWithContext({ request }: DcApiSharingScreenPro
123157
const onAuthorize = async ({ pin, onAuthorized, onAuthorizationError }: OnWalletAuthSubmitProps = {}) => {
124158
onAuthorizationErrorRef.current = onAuthorizationError
125159

126-
if (paradym.state === 'locked') {
127-
if (shouldUseCloudHsm) {
128-
if (!pin) throw new Error('PIN is required to use Cloud HSM')
129-
cloudHsmPinRef.current = pin
130-
}
160+
try {
161+
if (paradym.state === 'locked') {
162+
if (shouldUseCloudHsm) {
163+
if (!pin) throw new Error('PIN is required to use Cloud HSM')
164+
cloudHsmPinRef.current = pin
165+
}
131166

132-
if (pin) {
133-
await paradym.unlockUsingPin(pin)
134-
} else {
135-
await paradym.tryUnlockingUsingBiometrics()
136-
}
167+
if (pin) {
168+
await paradym.unlockUsingPin(pin)
169+
} else {
170+
await paradym.tryUnlockingUsingBiometrics()
171+
}
137172

138-
onAuthorized?.()
139-
return
140-
}
173+
onAuthorized?.()
174+
return
175+
}
141176

142-
if (paradym.state === 'unlocked') {
143-
setIsProcessing(true)
144-
try {
177+
if (paradym.state === 'unlocked') {
178+
setIsProcessing(true)
145179
await authorizeWalletFlow({
146180
mode: authorizationMode,
147181
pin,
@@ -151,23 +185,27 @@ export function DcApiSharingScreenWithContext({ request }: DcApiSharingScreenPro
151185
await setupWalletServiceProvider(paradym.paradym)
152186
}
153187

154-
await onShareResponse(paradym.paradym)
155-
onAuthorized?.()
156-
} catch (error) {
157-
if (isWalletAuthPromptError(error)) {
158-
onAuthorizationError?.()
159-
return
160-
}
188+
const didShare = await onShareResponse(paradym.paradym)
189+
if (didShare) onAuthorized?.()
190+
return
191+
}
161192

162-
throw error
163-
} finally {
164-
clearWalletFlowAuthorization()
165-
setIsProcessing(false)
193+
throw new Error(`Invalid state. Received: '${paradym.state}'`)
194+
} catch (error) {
195+
if (isWalletAuthPromptError(error)) {
196+
onAuthorizationError?.()
197+
return
166198
}
167-
return
168-
}
169199

170-
throw new Error(`Invalid state. Received: '${paradym.state}'`)
200+
setFlowError({
201+
reason: t(commonMessages.presentationCouldNotBeShared),
202+
error,
203+
responseMessage: 'Unable to share credentials',
204+
})
205+
} finally {
206+
clearWalletFlowAuthorization()
207+
setIsProcessing(false)
208+
}
171209
}
172210

173211
return (
@@ -179,14 +217,24 @@ export function DcApiSharingScreenWithContext({ request }: DcApiSharingScreenPro
179217
p="$4"
180218
paddingBottom={insets.bottom ?? '$6'}
181219
>
182-
<Stack pt="$5">
183-
<WalletFlowAuthPrompt
184-
authMode={authorizationMode}
185-
onSubmit={onAuthorize}
186-
isLoading={isAuthorizing}
187-
annotation={request.origin}
220+
{errorReason ? (
221+
<InteractionErrorSlide
222+
flowType="verify"
223+
reason={errorReason}
224+
layout="content"
225+
buttonLabel={t(commonMessages.close)}
226+
onCancel={() => sendErrorResponse({ errorMessage: errorResponseMessageRef.current })}
188227
/>
189-
</Stack>
228+
) : (
229+
<Stack pt="$5">
230+
<WalletFlowAuthPrompt
231+
authMode={authorizationMode}
232+
onSubmit={onAuthorize}
233+
isLoading={isAuthorizing}
234+
annotation={request.origin}
235+
/>
236+
</Stack>
237+
)}
190238
</YStack>
191239
)
192240
}

0 commit comments

Comments
 (0)