[INJIMO-3192]: Integrate swift error response enhancement (#2001)

* [INJIMOB-3192]: Native module integration for OVP error handling

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-3192]: Fixing iOS build failure

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-3192]: Refactoring code to move useEffect from SendVPScreenController to UI component

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-3192]: Updating useOvpErrorModal while resolving merge conflicts

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-3192]: Update iOs package dependencies to develop for OVP library

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-3192]: Update history error messages

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-3192]: Update the user declined history message

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-3192]: Remove moduleClassName and use existing method

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-3192]: Fix for retry scenario error messages

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-3192]: Fix for sendErrorToVerifier crash

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-3192]: Add Log for decline scenario

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-3192]: Fix for decline crash issue

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-3192]: Common method for reject Exceptions

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-3192]: Modify general exception for retry scenario

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-3192]: Remove comments

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-3192]: Update package resolved

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

---------

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>
This commit is contained in:
balachandarg-tw
2025-07-22 15:42:05 +05:30
committed by GitHub
parent 3beca4e7f8
commit 0fe6915bb8
17 changed files with 401 additions and 295 deletions

View File

@@ -29,16 +29,27 @@ import {VerifyIdentityOverlay} from '../VerifyIdentityOverlay';
import {VCShareFlowType} from '../../shared/Utils';
import {APP_EVENTS} from '../../machines/app';
import {GlobalContext} from '../../shared/GlobalContext';
import {useOvpErrorModal} from '../../shared/hooks/useOvpErrorModal';
export const ScanScreen: React.FC = () => {
const {t} = useTranslation('ScanScreen');
const scanScreenController = useScanScreen();
const sendVcScreenController = useSendVcScreen();
const sendVPScreenController = useSendVPScreen();
const [errorModal] = useOvpErrorModal({
error: sendVPScreenController.error,
noCredentialsMatchingVPRequest:
sendVPScreenController.noCredentialsMatchingVPRequest,
requestedClaimsByVerifier: sendVPScreenController.requestedClaimsByVerifier,
getAdditionalMessage: sendVPScreenController.getAdditionalMessage,
generateAndStoreLogMessage:
sendVPScreenController.generateAndStoreLogMessage,
t,
});
const [isBluetoothOn, setIsBluetoothOn] = useState(false);
const showErrorModal =
sendVPScreenController.scanScreenError ||
(sendVPScreenController.errorModal.show &&
(errorModal.show &&
(sendVPScreenController.flowType ===
VCShareFlowType.MINI_VIEW_SHARE_OPENID4VP ||
sendVPScreenController.flowType ===
@@ -277,7 +288,7 @@ export const ScanScreen: React.FC = () => {
const getPrimaryButtonText = () => {
if (
sendVPScreenController.errorModal.showRetryButton &&
errorModal.showRetryButton &&
sendVPScreenController.openID4VPRetryCount < 3
) {
return t('ScanScreen:status.retry');
@@ -366,8 +377,8 @@ export const ScanScreen: React.FC = () => {
alignActionsOnEnd
showClose={false}
isVisible={showErrorModal}
title={sendVPScreenController.errorModal.title}
message={sendVPScreenController.errorModal.message}
title={errorModal.title}
message={errorModal.message}
image={SvgImage.PermissionDenied()}
primaryButtonTestID={'retry'}
primaryButtonText={getPrimaryButtonText()}

View File

@@ -32,19 +32,29 @@ import OpenID4VP from '../../shared/openID4VP/OpenID4VP';
import {GlobalContext} from '../../shared/GlobalContext';
import {APP_EVENTS} from '../../machines/app';
import {useScanScreen} from './ScanScreenController';
import {useOvpErrorModal} from '../../shared/hooks/useOvpErrorModal';
export const SendVPScreen: React.FC<ScanLayoutProps> = props => {
const {t} = useTranslation('SendVPScreen');
const controller = useSendVPScreen();
const scanScreenController = useScanScreen();
const [errorModal, resetErrorModal] = useOvpErrorModal({
error: controller.error,
noCredentialsMatchingVPRequest: controller.noCredentialsMatchingVPRequest,
requestedClaimsByVerifier: controller.requestedClaimsByVerifier,
getAdditionalMessage: controller.getAdditionalMessage,
generateAndStoreLogMessage: controller.generateAndStoreLogMessage,
t,
});
const vcsMatchingAuthRequest = controller.vcsMatchingAuthRequest;
const {appService} = useContext(GlobalContext);
const [triggerExitFlow, setTriggerExitFlow] = useState(false);
useEffect(() => {
if (controller.errorModal.show && controller.isOVPViaDeepLink) {
if (errorModal.show && controller.isOVPViaDeepLink) {
const timeout = setTimeout(
() => {
OpenID4VP.sendErrorToVerifier(
@@ -58,11 +68,11 @@ export const SendVPScreen: React.FC<ScanLayoutProps> = props => {
return () => clearTimeout(timeout);
}
}, [controller.errorModal.show, controller.isOVPViaDeepLink]);
}, [errorModal.show, controller.isOVPViaDeepLink]);
useEffect(() => {
if (triggerExitFlow) {
controller.RESET_LOGGED_ERROR();
RESET_LOGGED_ERROR();
controller.GO_TO_HOME();
controller.RESET_RETRY_COUNT();
appService.send(APP_EVENTS.RESET_AUTHORIZATION_REQUEST());
@@ -106,11 +116,16 @@ export const SendVPScreen: React.FC<ScanLayoutProps> = props => {
}
});
const RESET_LOGGED_ERROR = () => {
resetErrorModal();
};
const handleDismiss = () => {
OpenID4VP.sendErrorToVerifier(
OVP_ERROR_MESSAGES.DECLINED,
OVP_ERROR_CODE.DECLINED,
);
controller.generateAndStoreLogMessage('USER_DECLINED_CONSENT');
if (controller.isOVPViaDeepLink) {
controller.GO_TO_HOME();
BackHandler.exitApp();
@@ -124,6 +139,7 @@ export const SendVPScreen: React.FC<ScanLayoutProps> = props => {
OVP_ERROR_MESSAGES.DECLINED,
OVP_ERROR_CODE.DECLINED,
);
controller.generateAndStoreLogMessage('USER_DECLINED_CONSENT');
if (controller.isOVPViaDeepLink) {
controller.GO_TO_HOME();
BackHandler.exitApp();
@@ -135,12 +151,9 @@ export const SendVPScreen: React.FC<ScanLayoutProps> = props => {
const getAdditionalMessage = () => {
if (
controller.isOVPViaDeepLink &&
!(
controller.errorModal.showRetryButton &&
controller.openID4VPRetryCount < 3
)
!(errorModal.showRetryButton && controller.openID4VPRetryCount < 3)
) {
return controller.errorModal.additionalMessage;
return errorModal.additionalMessage;
}
return undefined;
};
@@ -221,8 +234,7 @@ export const SendVPScreen: React.FC<ScanLayoutProps> = props => {
};
const getPrimaryButtonText = () => {
return controller.errorModal.showRetryButton &&
controller.openID4VPRetryCount < 3
return errorModal.showRetryButton && controller.openID4VPRetryCount < 3
? t('ScanScreen:status.retry')
: undefined;
};
@@ -417,14 +429,14 @@ export const SendVPScreen: React.FC<ScanLayoutProps> = props => {
/>
</>
)}
{controller.errorModal.show && (
{errorModal.show && (
<Error
isModal
alignActionsOnEnd
showClose={false}
isVisible={controller.errorModal.show}
title={controller.errorModal.title}
message={controller.errorModal.message}
isVisible={errorModal.show}
title={errorModal.title}
message={errorModal.message}
additionalMessage={getAdditionalMessage()}
image={SvgImage.PermissionDenied()}
primaryButtonTestID={'retry'}

View File

@@ -1,6 +1,6 @@
import {NavigationProp, useNavigation} from '@react-navigation/native';
import {useSelector} from '@xstate/react';
import {useContext, useEffect, useRef, useState} from 'react';
import {useCallback, useContext, useRef, useState} from 'react';
import {useTranslation} from 'react-i18next';
import {ActorRefFrom} from 'xstate';
import {Theme} from '../../components/ui/styleUtils';
@@ -151,208 +151,30 @@ export function useSendVPScreen() {
selectIsOVPViaDeeplink,
);
const getAdditionalMessage = () => {
const getAdditionalMessage = useCallback(() => {
return isOVPViaDeepLink && isIOS() ? t('errors.additionalMessage') : '';
};
}, [isOVPViaDeepLink, t]);
const generateAndStoreLogMessage = useCallback(
(logType: string, errorInfo?: string) => {
activityLogService.send(
ActivityLogEvents.LOG_ACTIVITY(
VPShareActivityLog.getLogFromObject({
timestamp: Date.now(),
type: logType,
info: errorInfo,
}),
),
);
},
[activityLogService],
);
function generateAndStoreLogMessage(logType: string, errorInfo?: string) {
activityLogService.send(
ActivityLogEvents.LOG_ACTIVITY(
VPShareActivityLog.getLogFromObject({
timestamp: Date.now(),
type: logType,
info: errorInfo,
}),
),
);
}
const requestedClaimsByVerifier = useSelector(
openID4VPService,
selectRequestedClaimsByVerifier,
);
const [errorModal, setErrorModalData] = useState({
show: false,
title: '',
message: '',
additionalMessage: '',
showRetryButton: false,
});
const isClaimsEmpty =
!requestedClaimsByVerifier || requestedClaimsByVerifier.trim() === '';
if (noCredentialsMatchingVPRequest) {
errorModal.title = isClaimsEmpty
? t('errors.noMatchingCredentialsWithMissingClaims.title')
: t('errors.noMatchingCredentials.title');
errorModal.message = isClaimsEmpty
? t('errors.noMatchingCredentialsWithMissingClaims.message')
: t('errors.noMatchingCredentials.message', { claims: requestedClaimsByVerifier });
generateAndStoreLogMessage(
'NO_CREDENTIAL_MATCHING_REQUEST',
requestedClaimsByVerifier,
);
} else if (
error.includes('Verifier authentication was unsuccessful') ||
error.startsWith('api error')
) {
errorModal.title = t('errors.invalidVerifier.title');
errorModal.message = t('errors.invalidVerifier.message');
generateAndStoreLogMessage('VERIFIER_AUTHENTICATION_FAILED');
} else if (error.includes('credential mismatch detected')) {
errorModal.title = t('errors.credentialsMismatch.title');
errorModal.message = t('errors.credentialsMismatch.message', {
claims: requestedClaimsByVerifier,
});
generateAndStoreLogMessage(
'CREDENTIAL_MISMATCH_FROM_KEBAB',
requestedClaimsByVerifier,
);
} else if (error.includes('none of the selected VC has image')) {
errorModal.title = t('errors.noImage.title');
errorModal.message = t('errors.noImage.message');
generateAndStoreLogMessage('NO_SELECTED_VC_HAS_IMAGE');
} else if (error.includes('invalid_request_uri_method')) {
errorModal.title = t('errors.invalidRequestURI.title');
errorModal.message = t('errors.invalidRequestURI.message');
generateAndStoreLogMessage('INVALID_REQUEST_URI_METHOD');
} else if (error.includes('invalid_request')) {
errorModal.title = t('errors.invalidQrCode.title');
errorModal.message = t('errors.invalidQrCode.message');
generateAndStoreLogMessage('INVALID_AUTH_REQUEST');
} else if (error.includes('vp_formats_not_supported')) {
errorModal.title = t('errors.vpFormatsNotSupported.title');
errorModal.message = t('errors.vpFormatsNotSupported.message');
generateAndStoreLogMessage('VP_FORMATS_NOT_SUPPORTED');
} else if (error.includes('invalid_presentation_definition_uri')) {
errorModal.title = t('errors.invalidPresentationDefinitionURI.title');
errorModal.message = t('errors.invalidPresentationDefinitionURI.message');
generateAndStoreLogMessage('INVALID_PRESENTATION_DEFINITION_URI');
} else if (error.includes('invalid_presentation_definition_reference')) {
errorModal.title = t('errors.invalidPresentationDefinitionRef.title');
errorModal.message = t('errors.invalidPresentationDefinitionRef.message');
generateAndStoreLogMessage('INVALID_PRESENTATION_DEFINITION_REFERENCE');
} else if (error.startsWith('send vp')) {
errorModal.title = t('errors.sendVPError.title');
errorModal.message = t('errors.sendVPError.message');
errorModal.showRetryButton = true;
} else if (error !== '') {
errorModal.title = t('errors.genericError.title');
errorModal.message = t('errors.genericError.message');
generateAndStoreLogMessage('TECHNICAL_ERROR');
}
useEffect(() => {
if (noCredentialsMatchingVPRequest && !hasLoggedErrorRef.current) {
const isClaimsEmpty =
!requestedClaimsByVerifier || requestedClaimsByVerifier.trim() === '';
setErrorModalData({
show: true,
title: isClaimsEmpty
? t('errors.noMatchingCredentialsWithMissingClaims.title')
: t('errors.noMatchingCredentials.title'),
message: isClaimsEmpty
? t('errors.noMatchingCredentialsWithMissingClaims.message')
: t('errors.noMatchingCredentials.message', {
claims: requestedClaimsByVerifier,
}),
additionalMessage: getAdditionalMessage(),
showRetryButton: false,
});
generateAndStoreLogMessage(
'NO_CREDENTIAL_MATCHING_REQUEST',
requestedClaimsByVerifier,
);
hasLoggedErrorRef.current = true;
} else if (
(error.includes('Verifier authentication was unsuccessful') ||
error.startsWith('api error')) &&
!hasLoggedErrorRef.current
) {
setErrorModalData({
show: true,
title: t('errors.invalidVerifier.title'),
message: t('errors.invalidVerifier.message'),
additionalMessage: getAdditionalMessage(),
showRetryButton: false,
});
generateAndStoreLogMessage('VERIFIER_AUTHENTICATION_FAILED');
hasLoggedErrorRef.current = true;
} else if (
error.includes('credential mismatch detected') &&
!hasLoggedErrorRef.current
) {
setErrorModalData({
show: true,
title: t('errors.credentialsMismatch.title'),
message: t('errors.credentialsMismatch.message', {
claims: requestedClaimsByVerifier,
}),
additionalMessage: getAdditionalMessage(),
showRetryButton: false,
});
generateAndStoreLogMessage(
'CREDENTIAL_MISMATCH_FROM_KEBAB',
requestedClaimsByVerifier,
);
hasLoggedErrorRef.current = true;
} else if (
error.includes('none of the selected VC has image') &&
!hasLoggedErrorRef.current
) {
setErrorModalData({
show: true,
title: t('errors.noImage.title'),
message: t('errors.noImage.message'),
additionalMessage: getAdditionalMessage(),
showRetryButton: false,
});
generateAndStoreLogMessage('NO_SELECTED_VC_HAS_IMAGE');
hasLoggedErrorRef.current = true;
} else if (
error.startsWith('vc validation') &&
!hasLoggedErrorRef.current
) {
setErrorModalData({
show: true,
title: t('errors.invalidQrCode.title'),
message: t('errors.invalidQrCode.message'),
additionalMessage: getAdditionalMessage(),
showRetryButton: false,
});
generateAndStoreLogMessage('INVALID_AUTH_REQUEST');
hasLoggedErrorRef.current = true;
} else if (error.startsWith('send vp') && !hasLoggedErrorRef.current) {
setErrorModalData({
show: true,
title: t('errors.genericError.title'),
message: t('errors.genericError.message'),
additionalMessage: getAdditionalMessage(),
showRetryButton: true,
});
hasLoggedErrorRef.current = true;
} else if (error !== '' && !hasLoggedErrorRef.current) {
setErrorModalData({
show: true,
title: t('errors.genericError.title'),
message: t('errors.genericError.message'),
additionalMessage: getAdditionalMessage(),
showRetryButton: false,
});
generateAndStoreLogMessage('TECHNICAL_ERROR');
hasLoggedErrorRef.current = true;
} else if (error === '') {
setErrorModalData({
show: false,
title: '',
message: '',
additionalMessage: '',
showRetryButton: false,
});
hasLoggedErrorRef.current = false;
}
}, [error, noCredentialsMatchingVPRequest]);
let overlayDetails: Omit<VPShareOverlayProps, 'isVisible'> | null = null;
let vpVerifierName = useSelector(
openID4VPService,
@@ -401,18 +223,12 @@ export function useSendVPScreen() {
checkIfAnyVCHasImage,
checkIfAllVCsHasImage,
getSelectedVCs,
errorModal,
error,
noCredentialsMatchingVPRequest,
requestedClaimsByVerifier,
getAdditionalMessage,
overlayDetails,
RESET_LOGGED_ERROR: () => {
hasLoggedErrorRef.current = false;
setErrorModalData({
show: false,
title: '',
message: '',
additionalMessage: '',
showRetryButton: false,
});
},
generateAndStoreLogMessage,
scanScreenError: useSelector(scanService, selectIsSendingVPError),
vcsMatchingAuthRequest,
userSelectedVCs: useSelector(openID4VPService, selectSelectedVCs),