diff --git a/assets/images/multifactorAuthentication/magnifying-glass-spy-mouth-closed-cropped.svg b/assets/images/multifactorAuthentication/magnifying-glass-spy-mouth-closed-cropped.svg new file mode 100644 index 0000000000000..68d21f5160f0a --- /dev/null +++ b/assets/images/multifactorAuthentication/magnifying-glass-spy-mouth-closed-cropped.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/multifactorAuthentication/magnifying-glass-spy-mouth-closed.svg b/assets/images/multifactorAuthentication/magnifying-glass-spy-mouth-closed.svg deleted file mode 100644 index 0767502831233..0000000000000 --- a/assets/images/multifactorAuthentication/magnifying-glass-spy-mouth-closed.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/cspell.json b/cspell.json index d55ae14f4fede..1fe605e9ec94f 100644 --- a/cspell.json +++ b/cspell.json @@ -861,7 +861,8 @@ "zoneinfo", "zxcv", "zxldvw", - "مثال" + "مثال", + "PINATM" ], "ignorePaths": [ "src/languages/de.ts", diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d5f42d95b1d6a..6f760a047862a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -511,6 +511,14 @@ const ROUTES = { route: 'settings/wallet/card/:cardID/report-card-lost-or-damaged/:reason/confirm-magic-code', getRoute: (cardID: string, reason: ReplacementReason) => `settings/wallet/card/${cardID}/report-card-lost-or-damaged/${reason}/confirm-magic-code` as const, }, + SETTINGS_WALLET_CARD_CHANGE_PIN: { + route: 'settings/wallet/card/:cardID/change-pin', + getRoute: (cardID: string) => `settings/wallet/card/${cardID}/change-pin` as const, + }, + SETTINGS_WALLET_CARD_CHANGE_PIN_ATM: { + route: 'settings/wallet/card/:cardID/change-pin-atm', + getRoute: (cardID: string) => `settings/wallet/card/${cardID}/change-pin-atm` as const, + }, SETTINGS_WALLET_CARD_ACTIVATE: { route: 'settings/wallet/card/:cardID/activate', getRoute: (cardID: string) => `settings/wallet/card/${cardID}/activate` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 961e910964233..037821774ddb1 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -191,6 +191,8 @@ const SCREENS = { TRANSACTIONS_IMPORTED: 'Settings_Wallet_Transactions_Imported', ENABLE_PAYMENTS: 'Settings_Wallet_EnablePayments', CARD_ACTIVATE: 'Settings_Wallet_Card_Activate', + CARD_CHANGE_PIN: 'Settings_Wallet_Card_Change_PIN', + CARD_CHANGE_PIN_ATM: 'Settings_Wallet_Card_Change_PIN_ATM', 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/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index be587c9e8d9e5..850efc6ebbf8a 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -98,6 +98,7 @@ import XeroSquare from '@assets/images/integrationicons/xero-icon-square.svg'; import ZenefitsSquare from '@assets/images/integrationicons/zenefits-icon-square.svg'; import InvoiceGeneric from '@assets/images/invoice-generic.svg'; import Invoice from '@assets/images/invoice.svg'; +import Key from '@assets/images/key.svg'; import LinkCopy from '@assets/images/link-copy.svg'; import Link from '@assets/images/link.svg'; import LuggageWithLinesPlus from '@assets/images/luggage-with-lines-plus.svg'; @@ -224,6 +225,7 @@ export { EyeDisabled, FallbackAvatar, Flag, + Key, FlagLevelOne, FlagLevelTwo, FlagLevelThree, diff --git a/src/components/Icon/chunks/illustrations.chunk.ts b/src/components/Icon/chunks/illustrations.chunk.ts index 30a3ead08efbe..ee192f47177d3 100644 --- a/src/components/Icon/chunks/illustrations.chunk.ts +++ b/src/components/Icon/chunks/illustrations.chunk.ts @@ -49,7 +49,7 @@ import ApprovedTransactionHand from '@assets/images/multifactorAuthentication/ap import DeniedTransactionHand from '@assets/images/multifactorAuthentication/denied-transaction-hand.svg'; import EncryptionMan from '@assets/images/multifactorAuthentication/encryption-man.svg'; import HumptyDumpty from '@assets/images/multifactorAuthentication/humpty-dumpty.svg'; -import MagnifyingGlassSpyMouthClosed from '@assets/images/multifactorAuthentication/magnifying-glass-spy-mouth-closed.svg'; +import MagnifyingGlassSpyMouthClosed from '@assets/images/multifactorAuthentication/magnifying-glass-spy-mouth-closed-cropped.svg'; import OpenPadlock from '@assets/images/multifactorAuthentication/open-padlock.svg'; import RunOutOfTime from '@assets/images/multifactorAuthentication/running-out-of-time.svg'; import PendingTravel from '@assets/images/pending-travel.svg'; diff --git a/src/components/MultifactorAuthentication/config/scenarios/ChangePIN.tsx b/src/components/MultifactorAuthentication/config/scenarios/ChangePIN.tsx new file mode 100644 index 0000000000000..a603f6f11d483 --- /dev/null +++ b/src/components/MultifactorAuthentication/config/scenarios/ChangePIN.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import {DefaultSuccessScreen} from '@components/MultifactorAuthentication/components/OutcomeScreen'; +import createScreenWithDefaults from '@components/MultifactorAuthentication/components/OutcomeScreen/createScreenWithDefaults'; +import {DefaultClientFailureScreen, DefaultServerFailureScreen} from '@components/MultifactorAuthentication/components/OutcomeScreen/FailureScreen/defaultScreens'; +import type {MultifactorAuthenticationScenarioCustomConfig} from '@components/MultifactorAuthentication/config/types'; +import {changePINForCard} from '@libs/actions/MultifactorAuthentication'; +import CONST from '@src/CONST'; + +/** + * Payload type for the CHANGE_PIN scenario. + * Contains the new PIN and cardID for the card whose PIN is being changed. + */ +type Payload = { + pin: string; + cardID: string; +}; + +const ClientFailureScreen = createScreenWithDefaults( + DefaultClientFailureScreen, + { + subtitle: 'multifactorAuthentication.changePin.didNotChange', + }, + 'ClientFailureScreen', +); + +const ServerFailureScreen = createScreenWithDefaults( + DefaultServerFailureScreen, + { + subtitle: 'multifactorAuthentication.changePin.didNotChange', + }, + 'ServerFailureScreen', +); + +const ChangePINSuccessScreen = createScreenWithDefaults( + DefaultSuccessScreen, + { + headerTitle: 'cardPage.pinChangedHeader', + title: 'cardPage.pinChanged', + subtitle: 'cardPage.pinChangedDescription', + illustration: 'Fireworks', + }, + 'ChangePINSuccessScreen', +); + +/** + * Configuration for the CHANGE_PIN multifactor authentication scenario. + * This scenario is used when a UK/EU cardholder changes the PIN of their physical card. + */ +export default { + allowedAuthenticationMethods: [CONST.MULTIFACTOR_AUTHENTICATION.TYPE.BIOMETRICS], + action: changePINForCard, + successScreen: , + defaultClientFailureScreen: , + defaultServerFailureScreen: , +} as const satisfies MultifactorAuthenticationScenarioCustomConfig; + +export type {Payload}; diff --git a/src/components/MultifactorAuthentication/config/scenarios/RevealPIN.tsx b/src/components/MultifactorAuthentication/config/scenarios/RevealPIN.tsx new file mode 100644 index 0000000000000..0cbf65a5353f9 --- /dev/null +++ b/src/components/MultifactorAuthentication/config/scenarios/RevealPIN.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import createScreenWithDefaults from '@components/MultifactorAuthentication/components/OutcomeScreen/createScreenWithDefaults'; +import {DefaultClientFailureScreen, DefaultServerFailureScreen} from '@components/MultifactorAuthentication/components/OutcomeScreen/FailureScreen/defaultScreens'; +import type { + MultifactorAuthenticationScenario, + MultifactorAuthenticationScenarioAdditionalParams, + MultifactorAuthenticationScenarioCustomConfig, +} from '@components/MultifactorAuthentication/config/types'; +import {revealPINForCard} from '@libs/actions/MultifactorAuthentication'; +import {setRevealedPIN} from '@libs/CardPINStore'; +import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +/** + * Payload type for the REVEAL_PIN scenario. + * Contains the cardID for the card whose PIN is being revealed. + */ +type Payload = { + cardID: string; +}; + +/** + * Type guard to verify the payload is a RevealPIN payload. + */ +function isRevealPINPayload(payload: MultifactorAuthenticationScenarioAdditionalParams | undefined): payload is Payload { + return !!payload && 'cardID' in payload; +} + +const ClientFailureScreen = createScreenWithDefaults( + DefaultClientFailureScreen, + { + subtitle: 'multifactorAuthentication.revealPin.couldNotReveal', + }, + 'ClientFailureScreen', +); + +const ServerFailureScreen = createScreenWithDefaults( + DefaultServerFailureScreen, + { + subtitle: 'multifactorAuthentication.revealPin.couldNotReveal', + }, + 'ServerFailureScreen', +); + +/** + * Configuration for the REVEAL_PIN multifactor authentication scenario. + * This scenario is used when a UK/EU cardholder reveals the PIN of their physical card. + * + * Callback behavior: + * - Success: Store the revealed PIN in CardPINStore and return SKIP_OUTCOME_SCREEN + * - Authentication failure: Return SHOW_OUTCOME_SCREEN to show failure screen + */ +export default { + allowedAuthenticationMethods: [CONST.MULTIFACTOR_AUTHENTICATION.TYPE.BIOMETRICS], + action: revealPINForCard, + callback: async (isSuccessful, callbackInput, payload) => { + if (isSuccessful && isRevealPINPayload(payload)) { + const pin = typeof callbackInput.body?.pin === 'string' ? callbackInput.body.pin : ''; + setRevealedPIN(payload.cardID, pin); + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAIN_CARD.getRoute(String(payload.cardID))); + return CONST.MULTIFACTOR_AUTHENTICATION.CALLBACK_RESPONSE.SKIP_OUTCOME_SCREEN; + } + + return CONST.MULTIFACTOR_AUTHENTICATION.CALLBACK_RESPONSE.SHOW_OUTCOME_SCREEN; + }, + + defaultClientFailureScreen: , + defaultServerFailureScreen: , +} as const satisfies MultifactorAuthenticationScenarioCustomConfig; + +export type {Payload}; diff --git a/src/components/MultifactorAuthentication/config/scenarios/SetPinOrderCard.tsx b/src/components/MultifactorAuthentication/config/scenarios/SetPINOrderCard.tsx similarity index 78% rename from src/components/MultifactorAuthentication/config/scenarios/SetPinOrderCard.tsx rename to src/components/MultifactorAuthentication/config/scenarios/SetPINOrderCard.tsx index d016426f441cf..0d6ef53897e91 100644 --- a/src/components/MultifactorAuthentication/config/scenarios/SetPinOrderCard.tsx +++ b/src/components/MultifactorAuthentication/config/scenarios/SetPINOrderCard.tsx @@ -1,6 +1,6 @@ import React from 'react'; import createScreenWithDefaults from '@components/MultifactorAuthentication/components/OutcomeScreen/createScreenWithDefaults'; -import {DefaultClientFailureScreen} from '@components/MultifactorAuthentication/components/OutcomeScreen/FailureScreen/defaultScreens'; +import {DefaultClientFailureScreen, DefaultServerFailureScreen} from '@components/MultifactorAuthentication/components/OutcomeScreen/FailureScreen/defaultScreens'; import type { MultifactorAuthenticationScenario, MultifactorAuthenticationScenarioAdditionalParams, @@ -33,18 +33,26 @@ type Payload = { }; /** - * Type guard to verify the payload is a SetPinOrderCard payload. + * Type guard to verify the payload is a SetPINOrderCard payload. */ -function isSetPinOrderCardPayload(payload: MultifactorAuthenticationScenarioAdditionalParams | undefined): payload is Payload { +function isSetPINOrderCardPayload(payload: MultifactorAuthenticationScenarioAdditionalParams | undefined): payload is Payload { return !!payload && 'cardID' in payload && 'pin' in payload; } -const AuthenticationCanceledFailureScreen = createScreenWithDefaults( +const ClientFailureScreen = createScreenWithDefaults( DefaultClientFailureScreen, { subtitle: 'multifactorAuthentication.setPin.didNotShipCard', }, - 'AuthenticationCanceledFailureScreen', + 'ClientFailureScreen', +); + +const ServerFailureScreen = createScreenWithDefaults( + DefaultServerFailureScreen, + { + subtitle: 'multifactorAuthentication.setPin.didNotShipCard', + }, + 'ServerFailureScreen', ); /** @@ -61,7 +69,7 @@ export default { action: setPersonalDetailsAndShipExpensifyCardsWithPIN, callback: async (isSuccessful, _callbackInput, payload) => { - if (isSuccessful && isSetPinOrderCardPayload(payload)) { + if (isSuccessful && isSetPINOrderCardPayload(payload)) { clearDraftValues(ONYXKEYS.FORMS.PERSONAL_DETAILS_FORM); Navigation.closeRHPFlow(); Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAIN_CARD.getRoute(String(payload.cardID))); @@ -71,9 +79,8 @@ export default { return CONST.MULTIFACTOR_AUTHENTICATION.CALLBACK_RESPONSE.SHOW_OUTCOME_SCREEN; }, - failureScreens: { - [CONST.MULTIFACTOR_AUTHENTICATION.REASON.EXPO.CANCELED]: , - }, + defaultClientFailureScreen: , + defaultServerFailureScreen: , } 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..999ae164edc2d 100644 --- a/src/components/MultifactorAuthentication/config/scenarios/index.ts +++ b/src/components/MultifactorAuthentication/config/scenarios/index.ts @@ -4,9 +4,13 @@ import CONST from '@src/CONST'; import type {Payload as AuthorizeTransactionPayload} from './AuthorizeTransaction'; import AuthorizeTransaction from './AuthorizeTransaction'; import BiometricsTest from './BiometricsTest'; +import type {Payload as ChangePINPayload} from './ChangePIN'; +import ChangePIN from './ChangePIN'; import {customConfig} from './DefaultUserInterface'; -import type {Payload as SetPinOrderCardPayload} from './SetPinOrderCard'; -import SetPinOrderCard from './SetPinOrderCard'; +import type {Payload as RevealPINPayload} from './RevealPIN'; +import RevealPIN from './RevealPIN'; +import type {Payload as SetPINOrderCardPayload} from './SetPINOrderCard'; +import SetPINOrderCard from './SetPINOrderCard'; /** * Payload types for multifactor authentication scenarios. @@ -14,8 +18,10 @@ import SetPinOrderCard from './SetPinOrderCard'; */ type Payloads = { [CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.BIOMETRICS_TEST]: EmptyObject; - [CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.SET_PIN_ORDER_CARD]: SetPinOrderCardPayload; + [CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.SET_PIN_ORDER_CARD]: SetPINOrderCardPayload; [CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.AUTHORIZE_TRANSACTION]: AuthorizeTransactionPayload; + [CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.REVEAL_PIN]: RevealPINPayload; + [CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.CHANGE_PIN]: ChangePINPayload; }; /** @@ -23,8 +29,10 @@ type Payloads = { */ const Configs = { [CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.BIOMETRICS_TEST]: customConfig(BiometricsTest), - [CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.SET_PIN_ORDER_CARD]: customConfig(SetPinOrderCard), + [CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.SET_PIN_ORDER_CARD]: customConfig(SetPINOrderCard), [CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.AUTHORIZE_TRANSACTION]: customConfig(AuthorizeTransaction), + [CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.REVEAL_PIN]: customConfig(RevealPIN), + [CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.CHANGE_PIN]: customConfig(ChangePIN), } 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..27e44e9cd2bb2 100644 --- a/src/components/MultifactorAuthentication/config/scenarios/names.ts +++ b/src/components/MultifactorAuthentication/config/scenarios/names.ts @@ -10,6 +10,8 @@ const SCENARIO_NAMES = { BIOMETRICS_TEST: 'BIOMETRICS-TEST', SET_PIN_ORDER_CARD: 'SET-PIN-ORDER-CARD', AUTHORIZE_TRANSACTION: 'AUTHORIZE-TRANSACTION', + REVEAL_PIN: 'REVEAL-PIN', + CHANGE_PIN: 'CHANGE-PIN', } as const; /** diff --git a/src/languages/de.ts b/src/languages/de.ts index 484c07db12e8a..af19dc91799f6 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -720,6 +720,8 @@ const translations: TranslationDeepObject = { }, verificationFailed: 'Überprüfung fehlgeschlagen', setPin: {didNotShipCard: 'Wir haben Ihre Karte nicht versendet. Bitte versuchen Sie es erneut.'}, + revealPin: {couldNotReveal: 'Wir konnten Ihre PIN nicht anzeigen. Bitte versuchen Sie es erneut.'}, + changePin: {didNotChange: 'Wir haben Ihre PIN nicht geändert. Bitte versuchen Sie es erneut.'}, }, validateCodeModal: { successfulSignInTitle: dedent(` @@ -2341,12 +2343,20 @@ ${amount} für ${merchant} – ${date}`, }, setYourPin: 'Legen Sie Ihre PIN fest.', confirmYourPin: 'Bestätigen Sie Ihre PIN.', + changeYourPin: 'Geben Sie eine neue PIN für Ihre Karte ein.', + confirmYourChangedPin: 'Bestätigen Sie Ihre neue PIN.', pinMustBeFourDigits: 'Die PIN muss genau 4 Ziffern lang sein.', invalidPin: 'Bitte wählen Sie eine sicherere PIN.', pinMismatch: 'PINs stimmen nicht überein. Bitte versuchen Sie es erneut.', revealPin: 'PIN anzeigen', hidePin: 'PIN ausblenden', pin: 'PIN', + changePin: 'PIN ändern', + pinChanged: 'PIN geändert!', + pinChangedHeader: 'PIN geändert', + pinChangedDescription: 'Sie können Ihre PIN jetzt verwenden.', + changePinAtATM: 'Ändern Sie Ihre PIN an jedem Geldautomaten', + changePinAtATMDescription: 'Dies ist in Ihrer Region erforderlich. Kontaktieren Sie Concierge falls Sie Fragen haben.', freezeCard: 'Karte sperren', unfreeze: 'Entsperren', unfreezeCard: 'Karte entsperren', diff --git a/src/languages/en.ts b/src/languages/en.ts index b0c9e30aca2bb..d6c2c09e79890 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -742,6 +742,12 @@ const translations = { setPin: { didNotShipCard: "We didn't ship your card. Please try again.", }, + revealPin: { + couldNotReveal: "We couldn't reveal your PIN. Please try again.", + }, + changePin: { + didNotChange: "We didn't change your PIN. Please try again.", + }, }, validateCodeModal: { successfulSignInTitle: dedent(` @@ -2379,12 +2385,20 @@ const translations = { }, setYourPin: 'Set the PIN for your card.', confirmYourPin: 'Enter your PIN again to confirm.', + changeYourPin: 'Enter a new PIN for your card.', + confirmYourChangedPin: 'Confirm your new PIN.', pinMustBeFourDigits: 'PIN must be exactly 4 digits.', invalidPin: 'Please choose a more secure PIN.', pinMismatch: 'PINs do not match. Please try again.', revealPin: 'Reveal PIN', hidePin: 'Hide PIN', pin: 'PIN', + pinChanged: 'PIN changed!', + pinChangedHeader: 'PIN changed', + pinChangedDescription: "You're all set to use your PIN now.", + changePin: 'Change PIN', + changePinAtATM: 'Change your PIN at any ATM', + changePinAtATMDescription: 'This is required in your region. Reach out to Concierge if you have any questions.', freezeCard: 'Freeze card', unfreeze: 'Unfreeze', unfreezeCard: 'Unfreeze card', diff --git a/src/languages/es.ts b/src/languages/es.ts index 81221f1bed1e5..233a068bf6d24 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -630,6 +630,12 @@ const translations: TranslationDeepObject = { setPin: { didNotShipCard: 'No enviamos tu tarjeta. Por favor, inténtalo de nuevo.', }, + revealPin: { + couldNotReveal: 'No pudimos revelar tu PIN. Por favor, inténtalo de nuevo.', + }, + changePin: { + didNotChange: 'No pudimos cambiar tu PIN. Por favor, inténtalo de nuevo.', + }, }, validateCodeModal: { successfulSignInTitle: 'Abracadabra,\n¡sesión iniciada!', @@ -2240,12 +2246,20 @@ ${amount} para ${merchant} - ${date}`, }, setYourPin: 'Establece tu PIN.', confirmYourPin: 'Confirma tu PIN.', + changeYourPin: 'Introduce un nuevo PIN para tu tarjeta.', + confirmYourChangedPin: 'Confirma tu nuevo PIN.', pinMustBeFourDigits: 'El PIN debe tener exactamente 4 dígitos.', invalidPin: 'Por favor, elige un PIN más seguro.', pinMismatch: 'Los PINs no coinciden. Por favor, inténtalo de nuevo.', revealPin: 'Mostrar PIN', hidePin: 'Ocultar PIN', pin: 'PIN', + changePin: 'Cambiar PIN', + pinChanged: '¡PIN cambiado!', + pinChangedHeader: 'PIN cambiado', + pinChangedDescription: 'Ya puedes usar tu nuevo PIN.', + changePinAtATM: 'Cambia tu PIN en cualquier cajero automático', + changePinAtATMDescription: 'Esto es obligatorio en tu región. Contacta a Concierge si tienes alguna pregunta.', freezeCard: 'Congelar tarjeta', unfreeze: 'Descongelar', unfreezeCard: 'Descongelar tarjeta', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 6e58a125d3d1b..1a86009c34c72 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -722,6 +722,8 @@ const translations: TranslationDeepObject = { }, verificationFailed: 'Échec de la vérification', setPin: {didNotShipCard: 'Nous n’avons pas envoyé votre carte. Veuillez réessayer.'}, + revealPin: {couldNotReveal: "Nous n'avons pas pu afficher votre code PIN. Veuillez réessayer."}, + changePin: {didNotChange: "Nous n'avons pas modifié votre code PIN. Veuillez réessayer."}, }, validateCodeModal: { successfulSignInTitle: dedent(` @@ -2348,12 +2350,20 @@ ${amount} pour ${merchant} - ${date}`, }, setYourPin: 'Définir votre code PIN.', confirmYourPin: 'Confirmez votre code PIN.', + changeYourPin: 'Saisissez un nouveau code PIN pour votre carte.', + confirmYourChangedPin: 'Confirmez votre nouveau code PIN.', pinMustBeFourDigits: 'Le code PIN doit comporter exactement 4 chiffres.', invalidPin: 'Veuillez choisir un code PIN plus sécurisé.', pinMismatch: 'Les codes PIN ne correspondent pas. Veuillez réessayer.', revealPin: 'Afficher le code PIN', hidePin: 'Masquer le code PIN', pin: 'Code PIN', + changePin: 'Modifier le code PIN', + pinChanged: 'Code PIN modifié !', + pinChangedHeader: 'Code PIN modifié', + pinChangedDescription: 'Vous êtes maintenant prêt à utiliser votre code PIN.', + changePinAtATM: 'Modifiez votre code PIN à n’importe quel distributeur automatique de billets', + changePinAtATMDescription: 'Ceci est obligatoire dans votre région. Contacter Concierge si vous avez des questions.', freezeCard: 'Geler la carte', unfreeze: 'Dégeler', unfreezeCard: 'Dégeler la carte', diff --git a/src/languages/it.ts b/src/languages/it.ts index 81f87d08a66f9..abf2762459d1a 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -721,6 +721,8 @@ const translations: TranslationDeepObject = { }, verificationFailed: 'Verifica non riuscita', setPin: {didNotShipCard: 'Non abbiamo spedito la tua carta. Riprova.'}, + revealPin: {couldNotReveal: 'Non siamo riusciti a mostrare il tuo PIN. Riprova.'}, + changePin: {didNotChange: 'Non abbiamo modificato il tuo PIN. Riprova.'}, }, validateCodeModal: { successfulSignInTitle: dedent(` @@ -2338,12 +2340,20 @@ ${amount} per ${merchant} - ${date}`, }, setYourPin: 'Imposta il tuo PIN.', confirmYourPin: 'Conferma il tuo PIN.', + changeYourPin: 'Inserisci un nuovo PIN per la tua carta.', + confirmYourChangedPin: 'Conferma il tuo nuovo PIN.', pinMustBeFourDigits: 'Il PIN deve essere composto da esattamente 4 cifre.', invalidPin: 'Scegli un PIN più sicuro.', pinMismatch: 'I PIN non corrispondono. Riprova.', revealPin: 'Mostra PIN', hidePin: 'Nascondi PIN', pin: 'PIN', + changePin: 'Cambia PIN', + pinChanged: 'PIN cambiato!', + pinChangedHeader: 'PIN modificato', + pinChangedDescription: 'Ora sei pronto a usare il tuo PIN.', + changePinAtATM: 'Cambia il tuo PIN a qualsiasi bancomat', + changePinAtATMDescription: 'Questo è obbligatorio nella tua regione. Contatta Concierge se hai domande.', freezeCard: 'Blocca carta', unfreeze: 'Sblocca', unfreezeCard: 'Sblocca carta', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index cc8b702bc7a5a..1c131024f37fb 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -718,6 +718,8 @@ const translations: TranslationDeepObject = { }, verificationFailed: '認証に失敗しました', setPin: {didNotShipCard: 'カードを発送できませんでした。もう一度お試しください。'}, + revealPin: {couldNotReveal: 'PIN を表示できませんでした。もう一度お試しください。'}, + changePin: {didNotChange: 'お客様の暗証番号は変更されていません。もう一度お試しください。'}, }, validateCodeModal: { successfulSignInTitle: dedent(` @@ -2322,12 +2324,20 @@ ${date} の ${merchant} への ${amount}`, }, setYourPin: 'PIN を設定.', confirmYourPin: 'PIN を確認してください.', + changeYourPin: 'カードの新しい暗証番号を入力してください。', + confirmYourChangedPin: '新しい暗証番号を確認してください。', pinMustBeFourDigits: 'PIN は 4 桁で入力してください。', invalidPin: 'より安全な暗証番号を選択してください。', pinMismatch: 'PIN が一致しません。もう一度お試しください。', revealPin: 'PIN を表示', hidePin: 'PIN を非表示', pin: 'PIN', + changePin: 'PIN を変更', + pinChanged: 'PIN を変更しました!', + pinChangedHeader: 'PIN を変更しました', + pinChangedDescription: 'これで暗証番号をすぐにご利用いただけます。', + changePinAtATM: 'PIN は任意の ATM で変更できます', + changePinAtATMDescription: 'これはお住まいの地域では必須です。ご不明な点がある場合はConcierge に連絡してください。', freezeCard: 'カードを一時停止', unfreeze: '再開', unfreezeCard: 'カードの一時停止を解除', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index abe453d051132..d933145574778 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -719,6 +719,8 @@ const translations: TranslationDeepObject = { }, verificationFailed: 'Verificatie mislukt', setPin: {didNotShipCard: 'We hebben je kaart niet verzonden. Probeer het opnieuw.'}, + revealPin: {couldNotReveal: 'We konden je pincode niet tonen. Probeer het opnieuw.'}, + changePin: {didNotChange: 'We hebben je pincode niet gewijzigd. Probeer het opnieuw.'}, }, validateCodeModal: { successfulSignInTitle: dedent(` @@ -2336,12 +2338,20 @@ ${amount} voor ${merchant} - ${date}`, }, setYourPin: 'Stel je pincode in.', confirmYourPin: 'Bevestig je pincode.', + changeYourPin: 'Voer een nieuwe pincode in voor je kaart.', + confirmYourChangedPin: 'Bevestig je nieuwe pincode.', pinMustBeFourDigits: 'Pincode moet precies 4 cijfers bevatten.', invalidPin: 'Kies alsjeblieft een veiligere pincode.', pinMismatch: 'Pincodes komen niet overeen. Probeer het opnieuw.', revealPin: 'Pincode weergeven', hidePin: 'Pincode verbergen', pin: 'Pincode', + changePin: 'Pincode wijzigen', + pinChanged: 'Pincode gewijzigd!', + pinChangedHeader: 'Pincode gewijzigd', + pinChangedDescription: 'Je bent helemaal klaar om je pincode te gebruiken.', + changePinAtATM: 'Wijzig je pincode bij elke geldautomaat', + changePinAtATMDescription: 'Dit is vereist in jouw regio. Neem contact op met Concierge als je vragen hebt.', freezeCard: 'Kaart blokkeren', unfreeze: 'Deblokkeren', unfreezeCard: 'Kaart deblokkeren', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 294fd53bd81e2..6dff2b732ef7e 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -720,6 +720,8 @@ const translations: TranslationDeepObject = { }, verificationFailed: 'Weryfikacja nie powiodła się', setPin: {didNotShipCard: 'Nie wysłaliśmy twojej karty. Spróbuj ponownie.'}, + revealPin: {couldNotReveal: 'Nie udało się wyświetlić twojego PIN-u. Spróbuj ponownie.'}, + changePin: {didNotChange: 'Nie zmieniliśmy twojego PIN-u. Spróbuj ponownie.'}, }, validateCodeModal: { successfulSignInTitle: dedent(` @@ -2332,12 +2334,20 @@ ${amount} dla ${merchant} - ${date}`, }, setYourPin: 'Ustaw swój PIN.', confirmYourPin: 'Potwierdź swój PIN.', + changeYourPin: 'Wprowadź nowy PIN do swojej karty.', + confirmYourChangedPin: 'Potwierdź swój nowy PIN.', pinMustBeFourDigits: 'PIN musi mieć dokładnie 4 cyfry.', invalidPin: 'Wybierz proszę bardziej bezpieczny PIN.', pinMismatch: 'Kody PIN nie są takie same. Spróbuj ponownie.', revealPin: 'Pokaż PIN', hidePin: 'Ukryj PIN', pin: 'PIN', + changePin: 'Zmień PIN', + pinChanged: 'PIN zmieniony!', + pinChangedHeader: 'PIN został zmieniony', + pinChangedDescription: 'Możesz już teraz używać swojego PIN-u.', + changePinAtATM: 'Zmień swój PIN w dowolnym bankomacie', + changePinAtATMDescription: 'To jest wymagane w twoim regionie. Skontaktuj się z Concierge jeśli masz jakieś pytania.', freezeCard: 'Zamroź kartę', unfreeze: 'Odmroź', unfreezeCard: 'Odmroź kartę', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 0b95faa376237..092b100eb611e 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -718,6 +718,8 @@ const translations: TranslationDeepObject = { }, verificationFailed: 'Falha na verificação', setPin: {didNotShipCard: 'Não enviamos seu cartão. Tente novamente.'}, + revealPin: {couldNotReveal: 'Não foi possível revelar seu PIN. Tente novamente.'}, + changePin: {didNotChange: 'Nós não alteramos seu PIN. Tente novamente.'}, }, validateCodeModal: { successfulSignInTitle: dedent(` @@ -2329,12 +2331,20 @@ ${amount} para ${merchant} - ${date}`, }, setYourPin: 'Defina seu PIN.', confirmYourPin: 'Confirme seu PIN.', + changeYourPin: 'Digite um novo PIN para o seu cartão.', + confirmYourChangedPin: 'Confirme seu novo PIN.', pinMustBeFourDigits: 'O PIN deve ter exatamente 4 dígitos.', invalidPin: 'Escolha um PIN mais seguro.', pinMismatch: 'Os PINs não coincidem. Tente novamente.', revealPin: 'Revelar PIN', hidePin: 'Ocultar PIN', pin: 'PIN', + changePin: 'Alterar PIN', + pinChanged: 'PIN alterado!', + pinChangedHeader: 'PIN alterado', + pinChangedDescription: 'Tudo pronto para você usar seu PIN agora.', + changePinAtATM: 'Altere seu PIN em qualquer caixa eletrônico', + changePinAtATMDescription: 'Isso é obrigatório na sua região. Falar com a Concierge se você tiver alguma dúvida.', freezeCard: 'Bloquear cartão', unfreeze: 'Desbloquear', unfreezeCard: 'Desbloquear cartão', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 49e48de160ded..afdbc6b6e0887 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -711,6 +711,8 @@ const translations: TranslationDeepObject = { }, verificationFailed: '验证失败', setPin: {didNotShipCard: '我们未能寄出您的卡。请重试。'}, + revealPin: {couldNotReveal: '我们无法显示您的 PIN。请重试。'}, + changePin: {didNotChange: '我们未更改您的 PIN。请重试。'}, }, validateCodeModal: { successfulSignInTitle: dedent(` @@ -2285,12 +2287,20 @@ ${amount},商户:${merchant} - 日期:${date}`, }, setYourPin: '设置您的 PIN.', confirmYourPin: '确认您的 PIN.', + changeYourPin: '为您的银行卡输入新 PIN 码。', + confirmYourChangedPin: '确认您的新 PIN。', pinMustBeFourDigits: 'PIN 必须正好为 4 位数字。', invalidPin: '请选择一个更安全的 PIN。', pinMismatch: 'PIN 不匹配。请重试。', revealPin: '显示 PIN', hidePin: '隐藏 PIN', pin: 'PIN', + changePin: '更改 PIN', + pinChanged: 'PIN 已更改!', + pinChangedHeader: 'PIN 已更改', + pinChangedDescription: '现在您可以开始使用您的 PIN 了。', + changePinAtATM: '在任意 ATM 更改您的 PIN', + changePinAtATMDescription: '这是您所在地区的必填项。如果你有任何问题,请联系 Concierge。', freezeCard: '冻结卡片', unfreeze: '解冻', unfreezeCard: '解冻卡片', diff --git a/src/libs/API/parameters/ChangeCardPINParams.ts b/src/libs/API/parameters/ChangeCardPINParams.ts new file mode 100644 index 0000000000000..7e6af1abacfb8 --- /dev/null +++ b/src/libs/API/parameters/ChangeCardPINParams.ts @@ -0,0 +1,5 @@ +import type {MultifactorAuthenticationAPIParams} from '@components/MultifactorAuthentication/config/types'; + +type ChangeCardPINParams = MultifactorAuthenticationAPIParams<'CHANGE-PIN'>; + +export default ChangeCardPINParams; diff --git a/src/libs/API/parameters/RevealCardPINParams.ts b/src/libs/API/parameters/RevealCardPINParams.ts new file mode 100644 index 0000000000000..cdb26cb352717 --- /dev/null +++ b/src/libs/API/parameters/RevealCardPINParams.ts @@ -0,0 +1,5 @@ +import type {MultifactorAuthenticationAPIParams} from '@components/MultifactorAuthentication/config/types'; + +type RevealCardPINParams = MultifactorAuthenticationAPIParams<'REVEAL-PIN'>; + +export default RevealCardPINParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 04085e5da02cc..e5055e3815280 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -499,5 +499,7 @@ export type {default as DeleteVacationDelegateParams} from './DeleteVacationDele export type {default as SetTwoFactorAuthExemptEmailForDomainParams} from './SetTwoFactorAuthExemptEmailForDomainParams'; export type {default as ResetDomainMemberTwoFactorAuthParams} from './ResetDomainMemberTwoFactorAuthParams'; export type {default as AuthorizeTransactionParams} from './AuthorizeTransactionParams'; +export type {default as RevealCardPINParams} from './RevealCardPINParams'; export type {default as DenyTransactionParams} from './DenyTransactionParams'; +export type {default as ChangeCardPINParams} from './ChangeCardPINParams'; export type {default as ExportDomainMembersCSVParams} from './ExportDomainMembersCSVParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 3616680109496..975b0d8915ab2 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -1372,6 +1372,8 @@ const SIDE_EFFECT_REQUEST_COMMANDS = { AUTHORIZE_TRANSACTION: 'AuthorizeTransaction', DENY_TRANSACTION: 'DenyTransaction', GET_TRANSACTIONS_PENDING_3DS_REVIEW: 'GetTransactionsPending3DSReview', + REVEAL_CARD_PIN: 'RevealCardPIN', + CHANGE_CARD_PIN: 'ChangeCardPIN', } as const; type SideEffectRequestCommand = ValueOf; @@ -1408,6 +1410,8 @@ 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.REVEAL_CARD_PIN]: Parameters.RevealCardPINParams; + [SIDE_EFFECT_REQUEST_COMMANDS.CHANGE_CARD_PIN]: Parameters.ChangeCardPINParams; }; type ApiRequestCommandParameters = WriteCommandParameters & ReadCommandParameters & SideEffectRequestCommandParameters; diff --git a/src/libs/CardPINStore.ts b/src/libs/CardPINStore.ts new file mode 100644 index 0000000000000..1eeb28622928a --- /dev/null +++ b/src/libs/CardPINStore.ts @@ -0,0 +1,45 @@ +import {useSyncExternalStore} from 'react'; + +/** + * In-memory store for revealed card PINs. + * PINs must NOT be persisted to disk (PCI compliance), so we use a module-level store + * with useSyncExternalStore for React integration instead of Onyx. + */ + +type Listener = () => void; + +const listeners = new Set(); +let revealedPINs: Record = {}; + +function subscribe(listener: Listener) { + listeners.add(listener); + return () => listeners.delete(listener); +} + +function notifyListeners() { + for (const listener of listeners) { + listener(); + } +} + +/** Intentionally replaces the entire map so only one PIN is stored in memory at a time. */ +function setRevealedPIN(cardID: string, pin: string) { + revealedPINs = {[cardID]: pin}; + notifyListeners(); +} + +function clearRevealedPIN() { + revealedPINs = {}; + notifyListeners(); +} + +function getSnapshot() { + return revealedPINs; +} + +function useRevealedPIN(cardID: string): string | undefined { + const pins = useSyncExternalStore(subscribe, getSnapshot, getSnapshot); + return cardID ? pins[cardID] : undefined; +} + +export {setRevealedPIN, clearRevealedPIN, useRevealedPIN}; diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 1add3b4cbf9e1..6c50439cbd39a 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -394,8 +394,8 @@ function getTranslationKeyForLimitType(limitType: ValueOf; + const sourceEntries = Object.entries(source) as Array<[string, MultifactorAuthenticationReason]>; const [, value] = sourceEntries.find(([backendMessage]) => message.endsWith(backendMessage)) ?? []; return value ?? VALUES.REASON.GENERIC.UNKNOWN_RESPONSE; }; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 1f3cb4fe362e9..88b15f1eb115d 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -426,6 +426,8 @@ 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]: () => require('../../../../pages/settings/Wallet/ExpensifyCardPage/ChangePINPage').default, + [SCREENS.SETTINGS.WALLET.CARD_CHANGE_PIN_ATM]: () => require('../../../../pages/settings/Wallet/ExpensifyCardPage/ChangePINATMPage').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/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index e38adcd58e4ca..9c2b655f498ae 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -49,7 +49,7 @@ const Stack = createPlatformStackNavigator const singleRHPWidth = variables.sideBarWidth; const getWideRHPWidth = (windowWidth: number) => variables.sideBarWidth + calculateReceiptPaneRHPWidth(windowWidth); -function MissingPersonalDetailsWithPinContext(props: Record) { +function MissingPersonalDetailsWithPINContext(props: Record) { return ( ['config'] = { path: ROUTES.SETTINGS_WALLET_CARD_ACTIVATE.route, exact: true, }, + [SCREENS.SETTINGS.WALLET.CARD_CHANGE_PIN]: { + path: ROUTES.SETTINGS_WALLET_CARD_CHANGE_PIN.route, + exact: true, + }, + [SCREENS.SETTINGS.WALLET.CARD_CHANGE_PIN_ATM]: { + path: ROUTES.SETTINGS_WALLET_CARD_CHANGE_PIN_ATM.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 82f78383a616d..b9d750d4d80ef 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -185,6 +185,14 @@ type SettingsNavigatorParamList = { /** cardID of selected card */ cardID: string; }; + [SCREENS.SETTINGS.WALLET.CARD_CHANGE_PIN]: { + /** cardID of selected card */ + cardID: string; + }; + [SCREENS.SETTINGS.WALLET.CARD_CHANGE_PIN_ATM]: { + /** cardID of selected 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..e80278a50984d 100644 --- a/src/libs/actions/MultifactorAuthentication/index.ts +++ b/src/libs/actions/MultifactorAuthentication/index.ts @@ -240,6 +240,39 @@ async function setPersonalDetailsAndShipExpensifyCardsWithPIN(params: Multifacto return parseHttpRequest(undefined, CONST.MULTIFACTOR_AUTHENTICATION.API_RESPONSE_MAP.SET_PERSONAL_DETAILS_AND_SHIP_EXPENSIFY_CARDS_WITH_PIN, undefined); } } +async function revealPINForCard({cardID, signedChallenge, authenticationMethod}: MultifactorAuthenticationScenarioParameters['REVEAL-PIN']) { + try { + const response = await makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.REVEAL_CARD_PIN, {cardID, signedChallenge: JSON.stringify(signedChallenge), authenticationMethod}, {}); + + const {jsonCode, message, pin} = response ?? {}; + const parsed = parseHttpRequest(jsonCode, CONST.MULTIFACTOR_AUTHENTICATION.API_RESPONSE_MAP.REVEAL_CARD_PIN, message); + + return { + ...parsed, + body: {pin: String(pin ?? '')}, + }; + } catch (error) { + Log.hmmm('[MultifactorAuthentication] Failed to reveal PIN for card', {error}); + return parseHttpRequest(undefined, CONST.MULTIFACTOR_AUTHENTICATION.API_RESPONSE_MAP.REVEAL_CARD_PIN, undefined); + } +} + +async function changePINForCard({cardID, pin, signedChallenge, authenticationMethod}: MultifactorAuthenticationScenarioParameters['CHANGE-PIN']) { + try { + const response = await makeRequestWithSideEffects( + SIDE_EFFECT_REQUEST_COMMANDS.CHANGE_CARD_PIN, + {cardID, pin, signedChallenge: JSON.stringify(signedChallenge), authenticationMethod}, + {}, + ); + + const {jsonCode, message} = response ?? {}; + return parseHttpRequest(jsonCode, CONST.MULTIFACTOR_AUTHENTICATION.API_RESPONSE_MAP.CHANGE_CARD_PIN, message); + } catch (error) { + Log.hmmm('[MultifactorAuthentication] Failed to change PIN for card', {error}); + return parseHttpRequest(undefined, CONST.MULTIFACTOR_AUTHENTICATION.API_RESPONSE_MAP.CHANGE_CARD_PIN, undefined); + } +} + /** Check whether a given transaction is still pending review and update the transactionsPending3DSReview key in Onyx */ async function isTransactionStillPending3DSReview(transactionID: string) { const response = await makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.GET_TRANSACTIONS_PENDING_3DS_REVIEW, null, {}); @@ -326,6 +359,8 @@ export { markHasAcceptedSoftPrompt, clearLocalMFAPublicKeyList, setPersonalDetailsAndShipExpensifyCardsWithPIN, + revealPINForCard, + changePINForCard, isTransactionStillPending3DSReview, denyTransaction, authorizeTransaction, diff --git a/src/pages/MissingPersonalDetails/MissingPersonalDetailsContent.tsx b/src/pages/MissingPersonalDetails/MissingPersonalDetailsContent.tsx index fd35b8d25569e..16f8345e49f0d 100644 --- a/src/pages/MissingPersonalDetails/MissingPersonalDetailsContent.tsx +++ b/src/pages/MissingPersonalDetails/MissingPersonalDetailsContent.tsx @@ -167,7 +167,7 @@ function MissingPersonalDetailsContent({privatePersonalDetails, draftValues, hea onMove={moveTo} currentPageName={currentPageName} personalDetailsValues={values} - shouldCollectPin={!!shouldCollectPIN} + shouldCollectPIN={!!shouldCollectPIN} /> ); diff --git a/src/pages/MissingPersonalDetails/subPages/Confirmation.tsx b/src/pages/MissingPersonalDetails/subPages/Confirmation.tsx index 28b941d47bbdd..1ccdc9de66205 100644 --- a/src/pages/MissingPersonalDetails/subPages/Confirmation.tsx +++ b/src/pages/MissingPersonalDetails/subPages/Confirmation.tsx @@ -6,11 +6,11 @@ import type {CustomSubPageProps} from '@pages/MissingPersonalDetails/types'; import CONST from '@src/CONST'; import INPUT_IDS from '@src/types/form/PersonalDetailsForm'; -function Confirmation({personalDetailsValues: values, onNext, onMove, isEditing, shouldCollectPin = false}: CustomSubPageProps) { +function Confirmation({personalDetailsValues: values, onNext, onMove, isEditing, shouldCollectPIN = false}: CustomSubPageProps) { const {translate} = useLocalize(); const {setIsConfirmStep} = usePINActions(); - const pageIndexes = shouldCollectPin ? CONST.MISSING_PERSONAL_DETAILS_INDEXES.MAPPING_WITH_PIN : CONST.MISSING_PERSONAL_DETAILS_INDEXES.MAPPING; + const pageIndexes = shouldCollectPIN ? CONST.MISSING_PERSONAL_DETAILS_INDEXES.MAPPING_WITH_PIN : CONST.MISSING_PERSONAL_DETAILS_INDEXES.MAPPING; const legalNameIndex = pageIndexes.LEGAL_NAME; const dateOfBirthIndex = pageIndexes.DATE_OF_BIRTH; const addressIndex = pageIndexes.ADDRESS; @@ -55,7 +55,7 @@ function Confirmation({personalDetailsValues: values, onNext, onMove, isEditing, }, ]; - if (shouldCollectPin) { + if (shouldCollectPIN) { baseItems.push({ description: translate('cardPage.physicalCardPin'), title: '••••', @@ -68,7 +68,7 @@ function Confirmation({personalDetailsValues: values, onNext, onMove, isEditing, } return baseItems; - }, [translate, values, onMove, legalNameIndex, dateOfBirthIndex, addressIndex, phoneNumberIndex, pinIndex, shouldCollectPin, setIsConfirmStep]); + }, [translate, values, onMove, legalNameIndex, dateOfBirthIndex, addressIndex, phoneNumberIndex, pinIndex, shouldCollectPIN, setIsConfirmStep]); return ( = 500; } diff --git a/src/pages/settings/Wallet/ExpensifyCardPage/ChangePINATMPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage/ChangePINATMPage.tsx new file mode 100644 index 0000000000000..267301727598c --- /dev/null +++ b/src/pages/settings/Wallet/ExpensifyCardPage/ChangePINATMPage.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import OutcomeScreenBase from '@components/MultifactorAuthentication/components/OutcomeScreen/OutcomeScreenBase'; +import useLocalize from '@hooks/useLocalize'; +import variables from '@styles/variables'; + +function ChangePINATMPage() { + const {translate} = useLocalize(); + + return ( + + ); +} + +ChangePINATMPage.displayName = 'ChangePINATMPage'; + +export default ChangePINATMPage; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage/ChangePINPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage/ChangePINPage.tsx new file mode 100644 index 0000000000000..27f9f7391c5f5 --- /dev/null +++ b/src/pages/settings/Wallet/ExpensifyCardPage/ChangePINPage.tsx @@ -0,0 +1,156 @@ +import React, {useCallback, useState} from 'react'; +import {View} from 'react-native'; +import Button from '@components/Button'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MagicCodeInput from '@components/MagicCodeInput'; +import {useMultifactorAuthentication} from '@components/MultifactorAuthentication/Context'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +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 {isValidPIN} from '@libs/ValidationUtils'; +import {PINContextProvider, usePINActions, usePINState} from '@pages/MissingPersonalDetails/PINContext'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type ChangePINPageProps = PlatformStackScreenProps; + +function ChangePINPageContent({cardID}: {cardID: string}) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {executeScenario} = useMultifactorAuthentication(); + const {isConfirmStep, isPINHidden} = usePINState(); + const {setIsConfirmStep, togglePINVisibility} = usePINActions(); + + const [enteredPIN, setEnteredPIN] = useState(''); + const [confirmPIN, setConfirmPIN] = useState(''); + const [error, setError] = useState(''); + + const handlePINChange = useCallback( + (value: string) => { + setError(''); + if (isConfirmStep) { + setConfirmPIN(value); + } else { + setEnteredPIN(value); + } + }, + [isConfirmStep], + ); + + const validatePIN = useCallback((): string => { + const PINToValidate = isConfirmStep ? confirmPIN : enteredPIN; + + if (PINToValidate.length !== CONST.EXPENSIFY_CARD.PIN.LENGTH) { + return translate('cardPage.pinMustBeFourDigits'); + } + + if (isConfirmStep && confirmPIN !== enteredPIN) { + return translate('cardPage.pinMismatch'); + } + + if (!isValidPIN(PINToValidate)) { + return translate('cardPage.invalidPin'); + } + return ''; + }, [isConfirmStep, confirmPIN, enteredPIN, translate]); + + const handleSubmit = useCallback(() => { + const validationError = validatePIN(); + if (validationError) { + setError(validationError); + return; + } + + if (!isConfirmStep) { + setIsConfirmStep(true); + setError(''); + return; + } + + executeScenario(CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.CHANGE_PIN, { + pin: confirmPIN, + cardID, + }); + }, [validatePIN, isConfirmStep, setIsConfirmStep, executeScenario, confirmPIN, cardID]); + + const currentPIN = isConfirmStep ? confirmPIN : enteredPIN; + const title = isConfirmStep ? translate('cardPage.confirmYourChangedPin') : translate('cardPage.changeYourPin'); + + return ( + + { + if (!isConfirmStep) { + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAIN_CARD.getRoute(String(cardID))); + return; + } + setIsConfirmStep(false); + setConfirmPIN(''); + setError(''); + }} + /> + + + {title} + + + + {!!error && {error}} + + + +