diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 6e04cf34c102d..b86ae89c647b2 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -511,6 +511,10 @@ const ROUTES = { route: 'settings/wallet/card/:cardID/activate', getRoute: (cardID: string) => `settings/wallet/card/${cardID}/activate` as const, }, + SETTINGS_WALLET_CARD_CHANGE_PIN_REQUIREMENT: { + route: 'settings/wallet/card/:cardID/change-pin-requirement', + getRoute: (cardID: string) => `settings/wallet/card/${cardID}/change-pin-requirement` as const, + }, SETTINGS_WALLET_TRAVEL_CVV: 'settings/wallet/travel-cvv', SETTINGS_WALLET_TRAVEL_CVV_VERIFY_ACCOUNT: `settings/wallet/travel-cvv/${VERIFY_ACCOUNT}`, SETTINGS_RULES: 'settings/rules', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 27e79174c3c2a..e57de7551e9b9 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -191,6 +191,7 @@ const SCREENS = { TRANSACTIONS_IMPORTED: 'Settings_Wallet_Transactions_Imported', ENABLE_PAYMENTS: 'Settings_Wallet_EnablePayments', CARD_ACTIVATE: 'Settings_Wallet_Card_Activate', + CARD_CHANGE_PIN_REQUIREMENT: 'Settings_Wallet_Card_Change_PIN_Requirement', REPORT_VIRTUAL_CARD_FRAUD: 'Settings_Wallet_ReportVirtualCardFraud', REPORT_VIRTUAL_CARD_FRAUD_CONFIRM_MAGIC_CODE: 'Settings_Wallet_ReportVirtualCardFraud_ConfirmMagicCode', REPORT_VIRTUAL_CARD_FRAUD_CONFIRMATION: 'Settings_Wallet_ReportVirtualCardFraudConfirmation', diff --git a/src/components/MultifactorAuthentication/config/scenarios/UnblockCardPIN.tsx b/src/components/MultifactorAuthentication/config/scenarios/UnblockCardPIN.tsx new file mode 100644 index 0000000000000..1b80b4ca47e90 --- /dev/null +++ b/src/components/MultifactorAuthentication/config/scenarios/UnblockCardPIN.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import createScreenWithDefaults from '@components/MultifactorAuthentication/components/OutcomeScreen/createScreenWithDefaults'; +import {DefaultClientFailureScreen} from '@components/MultifactorAuthentication/components/OutcomeScreen/FailureScreen/defaultScreens'; +import DefaultSuccessScreen from '@components/MultifactorAuthentication/components/OutcomeScreen/SuccessScreen/defaultScreens'; +import type { + MultifactorAuthenticationScenario, + MultifactorAuthenticationScenarioAdditionalParams, + MultifactorAuthenticationScenarioCustomConfig, +} from '@components/MultifactorAuthentication/config/types'; +import {unblockCardPIN} from '@libs/actions/MultifactorAuthentication'; +import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +/** + * Payload type for the UNBLOCK_CARD_PIN scenario. + * Contains cardID and isOfflinePINMarket flag to determine success experience. + */ +type Payload = { + cardID: string; + isOfflinePINMarket: boolean; +}; + +/** + * Type guard to verify the payload is an UnblockCardPIN payload. + */ +function isUnblockCardPINPayload(payload: MultifactorAuthenticationScenarioAdditionalParams | undefined): payload is Payload { + return !!payload && 'cardID' in payload; +} + +const CardUnlockedSuccessScreen = createScreenWithDefaults( + DefaultSuccessScreen, + { + title: 'multifactorAuthentication.unblockCardPIN.cardUnlocked', + subtitle: 'multifactorAuthentication.unblockCardPIN.cardUnlockedSubtitle', + }, + 'CardUnlockedSuccessScreen', +); + +const AuthenticationCanceledFailureScreen = createScreenWithDefaults( + DefaultClientFailureScreen, + { + subtitle: 'multifactorAuthentication.unblockCardPIN.didNotUnlockCard', + }, + 'AuthenticationCanceledFailureScreen', +); + +/** + * Configuration for the UNBLOCK_CARD_PIN multifactor authentication scenario. + * This scenario is used when a UK/EU cardholder needs to unlock their PIN-blocked card. + * + * Callback behavior per design doc: + * - Online market success: Return SHOW_OUTCOME_SCREEN → shows Card Unlocked success screen + * - Offline market success: Return SKIP_OUTCOME_SCREEN → navigate to ChangePINRequirementPage + * - Authentication failure: Return SHOW_OUTCOME_SCREEN to show failure screen + */ +export default { + allowedAuthenticationMethods: [CONST.MULTIFACTOR_AUTHENTICATION.TYPE.BIOMETRICS], + action: unblockCardPIN, + + callback: async (isSuccessful, _callbackInput, payload) => { + if (isSuccessful && isUnblockCardPINPayload(payload)) { + if (payload.isOfflinePINMarket) { + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_CHANGE_PIN_REQUIREMENT.getRoute(payload.cardID)); + return CONST.MULTIFACTOR_AUTHENTICATION.CALLBACK_RESPONSE.SKIP_OUTCOME_SCREEN; + } + + return CONST.MULTIFACTOR_AUTHENTICATION.CALLBACK_RESPONSE.SHOW_OUTCOME_SCREEN; + } + + return CONST.MULTIFACTOR_AUTHENTICATION.CALLBACK_RESPONSE.SHOW_OUTCOME_SCREEN; + }, + + successScreen: , + + failureScreens: { + [CONST.MULTIFACTOR_AUTHENTICATION.REASON.EXPO.CANCELED]: , + }, +} as const satisfies MultifactorAuthenticationScenarioCustomConfig; + +export type {Payload}; diff --git a/src/components/MultifactorAuthentication/config/scenarios/index.ts b/src/components/MultifactorAuthentication/config/scenarios/index.ts index 8163a5b2a3999..9f291f9660f31 100644 --- a/src/components/MultifactorAuthentication/config/scenarios/index.ts +++ b/src/components/MultifactorAuthentication/config/scenarios/index.ts @@ -7,6 +7,8 @@ import BiometricsTest from './BiometricsTest'; import {customConfig} from './DefaultUserInterface'; import type {Payload as SetPinOrderCardPayload} from './SetPinOrderCard'; import SetPinOrderCard from './SetPinOrderCard'; +import type {Payload as UnblockCardPINPayload} from './UnblockCardPIN'; +import UnblockCardPIN from './UnblockCardPIN'; /** * Payload types for multifactor authentication scenarios. @@ -16,6 +18,7 @@ type Payloads = { [CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.BIOMETRICS_TEST]: EmptyObject; [CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.SET_PIN_ORDER_CARD]: SetPinOrderCardPayload; [CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.AUTHORIZE_TRANSACTION]: AuthorizeTransactionPayload; + [CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.UNBLOCK_CARD_PIN]: UnblockCardPINPayload; }; /** @@ -25,6 +28,7 @@ const Configs = { [CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.BIOMETRICS_TEST]: customConfig(BiometricsTest), [CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.SET_PIN_ORDER_CARD]: customConfig(SetPinOrderCard), [CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.AUTHORIZE_TRANSACTION]: customConfig(AuthorizeTransaction), + [CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.UNBLOCK_CARD_PIN]: customConfig(UnblockCardPIN), } as const satisfies MultifactorAuthenticationScenarioConfigRecord; export default Configs; diff --git a/src/components/MultifactorAuthentication/config/scenarios/names.ts b/src/components/MultifactorAuthentication/config/scenarios/names.ts index 16f361cc11f62..b4cc545ab489e 100644 --- a/src/components/MultifactorAuthentication/config/scenarios/names.ts +++ b/src/components/MultifactorAuthentication/config/scenarios/names.ts @@ -10,6 +10,7 @@ const SCENARIO_NAMES = { BIOMETRICS_TEST: 'BIOMETRICS-TEST', SET_PIN_ORDER_CARD: 'SET-PIN-ORDER-CARD', AUTHORIZE_TRANSACTION: 'AUTHORIZE-TRANSACTION', + UNBLOCK_CARD_PIN: 'UNBLOCK-CARD-PIN', } as const; /** diff --git a/src/languages/en.ts b/src/languages/en.ts index 5060d081a79e1..3dd993f588151 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -742,6 +742,11 @@ const translations = { setPin: { didNotShipCard: "We didn't ship your card. Please try again.", }, + unblockCardPIN: { + cardUnlocked: 'Card unlocked!', + cardUnlockedSubtitle: 'Your card has been unlocked and is ready to use.', + didNotUnlockCard: "We didn't unlock your card. Please try again.", + }, }, validateCodeModal: { successfulSignInTitle: dedent(` @@ -2390,6 +2395,16 @@ const translations = { unfreezeCard: 'Unfreeze card', freezeDescription: 'A frozen card cannot be used for purchases and transactions. You can unfreeze it at any time.', unfreezeDescription: "Unfreezing this card will start allowing purchases and transactions again. Only proceed if you're sure the card is safe to use.", + pinBlocked: { + suspendedError: 'Your card is locked due to too many incorrect PIN attempts. Tap Unlock Card to regain access.', + openError: 'You need to change your PIN at any ATM to finish unlocking your card.', + unlockCard: 'Unlock card', + }, + changePINRequirement: { + title: 'Change PIN', + heading: 'Change your PIN at any ATM', + description: 'Your card has been unlocked, but you need to change your PIN at any ATM to finish the process.', + }, frozen: 'Frozen', youFroze: ({date}: {date: string}) => `You froze this card on ${date}.`, frozenBy: ({person, date}: {person: string; date: string}) => `${person} froze this card on ${date}.`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 473af9bd21833..2377e83945e48 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -630,6 +630,11 @@ const translations: TranslationDeepObject = { setPin: { didNotShipCard: 'No enviamos tu tarjeta. Por favor, inténtalo de nuevo.', }, + unblockCardPIN: { + cardUnlocked: '¡Tarjeta desbloqueada!', + cardUnlockedSubtitle: 'Tu tarjeta ha sido desbloqueada y está lista para usar.', + didNotUnlockCard: 'No pudimos desbloquear tu tarjeta. Por favor, inténtalo de nuevo.', + }, }, validateCodeModal: { successfulSignInTitle: 'Abracadabra,\n¡sesión iniciada!', @@ -2251,6 +2256,16 @@ ${amount} para ${merchant} - ${date}`, unfreezeCard: 'Descongelar tarjeta', freezeDescription: 'Una tarjeta congelada no se puede usar para compras ni transacciones. Puedes descongelarla en cualquier momento.', unfreezeDescription: 'Al descongelar esta tarjeta se volverán a permitir compras y transacciones. Continúa solo si estás seguro de que la tarjeta es segura para usar.', + pinBlocked: { + suspendedError: 'Tu tarjeta está bloqueada debido a demasiados intentos incorrectos de PIN. Toca Desbloquear tarjeta para recuperar el acceso.', + openError: 'Necesitas cambiar tu PIN en cualquier cajero automático para terminar de desbloquear tu tarjeta.', + unlockCard: 'Desbloquear tarjeta', + }, + changePINRequirement: { + title: 'Cambiar PIN', + heading: 'Cambia tu PIN en cualquier cajero automático', + description: 'Tu tarjeta ha sido desbloqueada, pero necesitas cambiar tu PIN en cualquier cajero automático para completar el proceso.', + }, frozen: 'Congelada', youFroze: ({date}: {date: string}) => `Congelaste esta tarjeta el ${date}.`, frozenBy: ({person, date}: {person: string; date: string}) => `${person} congeló esta tarjeta el ${date}.`, diff --git a/src/libs/API/parameters/UnblockCardPINParams.ts b/src/libs/API/parameters/UnblockCardPINParams.ts new file mode 100644 index 0000000000000..93013a1aacef0 --- /dev/null +++ b/src/libs/API/parameters/UnblockCardPINParams.ts @@ -0,0 +1,5 @@ +import type {MultifactorAuthenticationAPIParams} from '@components/MultifactorAuthentication/config/types'; + +type UnblockCardPINParams = MultifactorAuthenticationAPIParams<'UNBLOCK-CARD-PIN'>; + +export default UnblockCardPINParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 04085e5da02cc..08c12103ba370 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -408,6 +408,7 @@ export type {default as SetPersonalCardReimbursableParams} from './SetPersonalCa export type {default as SetCompanyCardExportAccountParams} from './SetCompanyCardExportAccountParams'; export type {default as SetPersonalDetailsAndShipExpensifyCardsParams} from './SetPersonalDetailsAndShipExpensifyCardsParams'; export type {default as SetPersonalDetailsAndShipExpensifyCardsWithPINParams} from './SetPersonalDetailsAndShipExpensifyCardsWithPINParams'; +export type {default as UnblockCardPINParams} from './UnblockCardPINParams'; export type {default as RequestFeedSetupParams} from './RequestFeedSetupParams'; export type {default as SetInvoicingTransferBankAccountParams} from './SetInvoicingTransferBankAccountParams'; export type {default as ConnectPolicyToQuickBooksDesktopParams} from './ConnectPolicyToQuickBooksDesktopParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 3616680109496..7a439e417c913 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -1372,6 +1372,7 @@ const SIDE_EFFECT_REQUEST_COMMANDS = { AUTHORIZE_TRANSACTION: 'AuthorizeTransaction', DENY_TRANSACTION: 'DenyTransaction', GET_TRANSACTIONS_PENDING_3DS_REVIEW: 'GetTransactionsPending3DSReview', + UNBLOCK_CARD_PIN: 'UnblockCardPIN', } as const; type SideEffectRequestCommand = ValueOf; @@ -1408,6 +1409,7 @@ type SideEffectRequestCommandParameters = { [SIDE_EFFECT_REQUEST_COMMANDS.AUTHORIZE_TRANSACTION]: Parameters.AuthorizeTransactionParams; [SIDE_EFFECT_REQUEST_COMMANDS.DENY_TRANSACTION]: Parameters.DenyTransactionParams; [SIDE_EFFECT_REQUEST_COMMANDS.GET_TRANSACTIONS_PENDING_3DS_REVIEW]: null; + [SIDE_EFFECT_REQUEST_COMMANDS.UNBLOCK_CARD_PIN]: Parameters.UnblockCardPINParams; }; type ApiRequestCommandParameters = WriteCommandParameters & ReadCommandParameters & SideEffectRequestCommandParameters; diff --git a/src/libs/MultifactorAuthentication/Biometrics/VALUES.ts b/src/libs/MultifactorAuthentication/Biometrics/VALUES.ts index f6ba636ad84bb..79b403d961788 100644 --- a/src/libs/MultifactorAuthentication/Biometrics/VALUES.ts +++ b/src/libs/MultifactorAuthentication/Biometrics/VALUES.ts @@ -188,6 +188,13 @@ const API_RESPONSE_MAP = { ...MULTIFACTOR_AUTHENTICATION_COMMAND_BASE_CLIENT_ERRORS, }, }, + + UNBLOCK_CARD_PIN: { + [HTTP_STATUS.SUCCESS]: REASON.BACKEND.AUTHORIZATION_SUCCESSFUL, + [HTTP_STATUS.CLIENT_ERROR]: { + ...MULTIFACTOR_AUTHENTICATION_COMMAND_BASE_CLIENT_ERRORS, + }, + }, } as const; /** diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index e008d6c8644d4..2fd4ba583f8e2 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -426,6 +426,7 @@ const SettingsModalStackNavigator = createModalStackNavigator('../../../../pages/settings/Wallet/ReportVirtualCardFraudVerifyAccountPage').default, [SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD_CONFIRMATION]: () => require('../../../../pages/settings/Wallet/ReportVirtualCardFraudConfirmationPage').default, [SCREENS.SETTINGS.WALLET.CARD_ACTIVATE]: () => require('../../../../pages/settings/Wallet/ActivatePhysicalCardPage').default, + [SCREENS.SETTINGS.WALLET.CARD_CHANGE_PIN_REQUIREMENT]: () => require('../../../../pages/settings/Wallet/ExpensifyCardPage/ChangePINRequirementPage').default, [SCREENS.SETTINGS.WALLET.TRANSFER_BALANCE]: () => require('../../../../pages/settings/Wallet/TransferBalancePage').default, [SCREENS.SETTINGS.WALLET.CHOOSE_TRANSFER_ACCOUNT]: () => require('../../../../pages/settings/Wallet/ChooseTransferAccountPage').default, [SCREENS.SETTINGS.WALLET.IMPORT_TRANSACTIONS]: () => require('../../../../pages/settings/Wallet/ImportTransactionsPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts index e6cbd85ac07a9..53d328a92cda2 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts @@ -33,6 +33,7 @@ const SETTINGS_TO_RHP: Partial['config'] = { path: ROUTES.SETTINGS_WALLET_CARD_ACTIVATE.route, exact: true, }, + [SCREENS.SETTINGS.WALLET.CARD_CHANGE_PIN_REQUIREMENT]: { + path: ROUTES.SETTINGS_WALLET_CARD_CHANGE_PIN_REQUIREMENT.route, + exact: true, + }, [SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS]: { path: ROUTES.SETTINGS_WALLET_CARD_DIGITAL_DETAILS_UPDATE_ADDRESS.route, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 202dfbfce6017..c12a7df67757c 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -185,6 +185,10 @@ type SettingsNavigatorParamList = { /** cardID of selected card */ cardID: string; }; + [SCREENS.SETTINGS.WALLET.CARD_CHANGE_PIN_REQUIREMENT]: { + /** cardID of the PIN-blocked card */ + cardID: string; + }; [SCREENS.WORKSPACE.WORKFLOWS_PAYER]: { policyID: string; }; diff --git a/src/libs/actions/MultifactorAuthentication/index.ts b/src/libs/actions/MultifactorAuthentication/index.ts index ca8f5022fe99c..97a978c4e9cfd 100644 --- a/src/libs/actions/MultifactorAuthentication/index.ts +++ b/src/libs/actions/MultifactorAuthentication/index.ts @@ -273,6 +273,25 @@ async function authorizeTransaction({transactionID, signedChallenge, authenticat } } +async function unblockCardPIN(params: MultifactorAuthenticationScenarioParameters['UNBLOCK-CARD-PIN']) { + try { + const response = await makeRequestWithSideEffects( + SIDE_EFFECT_REQUEST_COMMANDS.UNBLOCK_CARD_PIN, + { + ...params, + signedChallenge: JSON.stringify(params.signedChallenge), + }, + {}, + ); + + const {jsonCode, message} = response ?? {}; + return parseHttpRequest(jsonCode, CONST.MULTIFACTOR_AUTHENTICATION.API_RESPONSE_MAP.UNBLOCK_CARD_PIN, message); + } catch (error) { + Log.hmmm('[MultifactorAuthentication] Failed to unblock card PIN', {error}); + return parseHttpRequest(undefined, CONST.MULTIFACTOR_AUTHENTICATION.API_RESPONSE_MAP.UNBLOCK_CARD_PIN, undefined); + } +} + async function denyTransaction({transactionID}: DenyTransactionParams) { try { const response = await makeRequestWithSideEffects( @@ -330,4 +349,5 @@ export { denyTransaction, authorizeTransaction, fireAndForgetDenyTransaction, + unblockCardPIN, }; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage/ChangePINRequirementPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage/ChangePINRequirementPage.tsx new file mode 100644 index 0000000000000..87acb9a4a6220 --- /dev/null +++ b/src/pages/settings/Wallet/ExpensifyCardPage/ChangePINRequirementPage.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import ConfirmationPage from '@components/ConfirmationPage'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; + +type ChangePINRequirementPageProps = PlatformStackScreenProps; + +function ChangePINRequirementPage({route}: ChangePINRequirementPageProps) { + const {cardID} = route.params; + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const illustrations = useMemoizedLazyIllustrations(['EmptyCardState']); + + return ( + + Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAIN_CARD.getRoute(cardID))} + /> + { + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAIN_CARD.getRoute(cardID), {forceReplace: true}); + }} + buttonText={translate('common.buttonConfirm')} + containerStyle={styles.h100} + illustrationStyle={[styles.w100, StyleUtils.getSuccessReportCardLostIllustrationStyle()]} + innerContainerStyle={styles.ph0} + descriptionStyle={[styles.ph4, styles.textSupporting]} + /> + + ); +} + +ChangePINRequirementPage.displayName = 'ChangePINRequirementPage'; + +export default ChangePINRequirementPage; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx b/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx index aa3d26945172c..c4f738a1340e4 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx @@ -14,6 +14,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {useLockedAccountActions, useLockedAccountState} from '@components/LockedAccountModalProvider'; import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import {useMultifactorAuthentication} from '@components/MultifactorAuthentication/Context'; import {usePersonalDetails, useSession} from '@components/OnyxListItemProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; @@ -27,7 +28,17 @@ import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import {freezeCard, unfreezeCard} from '@libs/actions/Card'; import {resetValidateActionCodeSent} from '@libs/actions/User'; -import {formatCardExpiration, getCardCurrency, getCardHintText, getDomainCards, getTranslationKeyForLimitType, isCardFrozen, maskCard, maskPin} from '@libs/CardUtils'; +import { + formatCardExpiration, + getCardCurrency, + getCardHintText, + getDomainCards, + getTranslationKeyForLimitType, + isCardFrozen, + maskCard, + maskPin, + supportsPINManagementFeatures, +} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -130,6 +141,13 @@ function ExpensifyCardPage({route}: ExpensifyCardPageProps) { const isSignedInAsDelegate = !!account?.delegatedAccess?.delegate || false; + const {executeScenario} = useMultifactorAuthentication(); + + const isPINBlocked = !!currentCard?.nameValuePairs?.isPINBlocked; + const isUkEuCard = supportsPINManagementFeatures(currentCard); + const isCardSuspendedAndPINBlocked = isUkEuCard && currentCard?.state === CONST.EXPENSIFY_CARD.STATE.STATE_SUSPENDED && isPINBlocked; + const isCardOpenAndPINBlocked = isUkEuCard && currentCard?.state === CONST.EXPENSIFY_CARD.STATE.OPEN && isPINBlocked; + const session = useSession(); const isCardHolder = currentCard?.accountID === session?.accountID; @@ -146,6 +164,13 @@ function ExpensifyCardPage({route}: ExpensifyCardPageProps) { [], ); + const handleUnlockCardPress = useCallback(() => { + executeScenario(CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.UNBLOCK_CARD_PIN, { + cardID, + isOfflinePINMarket: !!currentCard?.nameValuePairs?.isOfflinePINMarket, + }); + }, [executeScenario, cardID, currentCard?.nameValuePairs?.isOfflinePINMarket]); + const [isFreezeModalVisible, setIsFreezeModalVisible] = useState(false); const [isUnfreezeModalVisible, setIsUnfreezeModalVisible] = useState(false); @@ -218,6 +243,33 @@ function ExpensifyCardPage({route}: ExpensifyCardPageProps) { /> )} + {isCardSuspendedAndPINBlocked && ( + <> + +