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}}
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+ChangePINPageContent.displayName = 'ChangePINPageContent';
+
+function ChangePINPage({
+ route: {
+ params: {cardID = ''},
+ },
+}: ChangePINPageProps) {
+ return (
+
+
+
+ );
+}
+
+ChangePINPage.displayName = 'ChangePINPage';
+
+export default ChangePINPage;
diff --git a/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx b/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx
index aa3d26945172c..f89ca8aa6357c 100644
--- a/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx
+++ b/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx
@@ -11,9 +11,11 @@ import ConfirmModal from '@components/ConfirmModal';
import DotIndicatorMessage from '@components/DotIndicatorMessage';
import FrozenCardHeader from '@components/FrozenCardHeader';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import * as Expensicons from '@components/Icon/Expensicons';
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 +29,18 @@ 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 {clearRevealedPIN, useRevealedPIN} from '@libs/CardPINStore';
+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';
@@ -77,6 +90,7 @@ function ExpensifyCardPage({route}: ExpensifyCardPageProps) {
const styles = useThemeStyles();
const {isOffline} = useNetwork();
const {translate} = useLocalize();
+ const {executeScenario} = useMultifactorAuthentication();
const isTravelCard = cardList?.[cardID]?.nameValuePairs?.isTravelCard;
const shouldDisplayCardDomain = !isTravelCard && (!cardList?.[cardID]?.nameValuePairs?.issuedBy || !cardList?.[cardID]?.nameValuePairs?.isVirtual);
const domain = cardList?.[cardID]?.domainName ?? '';
@@ -84,7 +98,7 @@ function ExpensifyCardPage({route}: ExpensifyCardPageProps) {
const pageTitle = shouldDisplayCardDomain ? expensifyCardTitle : (cardList?.[cardID]?.nameValuePairs?.cardTitle ?? expensifyCardTitle);
const {displayName} = useCurrentUserPersonalDetails();
const personalDetails = usePersonalDetails();
- const expensifyIcons = useMemoizedLazyExpensifyIcons(['Flag', 'MoneySearch', 'FreezeCard']);
+ const expensifyIcons = useMemoizedLazyExpensifyIcons(['Flag', 'MoneySearch', 'FreezeCard', 'Key']);
const cardsToShow = useMemo(() => {
if (shouldDisplayCardDomain) {
@@ -103,12 +117,15 @@ function ExpensifyCardPage({route}: ExpensifyCardPageProps) {
const {cardsDetails, isCardDetailsLoading, cardsDetailsErrors} = useExpensifyCardState();
const {setCardsDetails} = useExpensifyCardActions();
+ const currentPhysicalCard = useMemo(() => physicalCards?.find((card) => String(card?.cardID) === cardID) ?? physicalCards?.at(0), [physicalCards, cardID]);
+ const revealedPIN = useRevealedPIN(String(currentPhysicalCard?.cardID));
- // Resets card details when navigating away from the page.
+ // Resets card details and revealed PIN when navigating away from the page.
useFocusEffect(
useCallback(() => {
return () => {
setCardsDetails((oldCardDetails) => ({...oldCardDetails, [cardID]: null}));
+ clearRevealedPIN();
};
}, [cardID, setCardsDetails]),
);
@@ -118,13 +135,14 @@ function ExpensifyCardPage({route}: ExpensifyCardPageProps) {
const hasDetectedDomainFraud = cardsToShow?.some((card) => card?.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN);
const hasDetectedIndividualFraud = cardsToShow?.some((card) => card?.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL);
- const currentPhysicalCard = useMemo(() => physicalCards?.find((card) => String(card?.cardID) === cardID) ?? physicalCards?.at(0), [physicalCards, cardID]);
// Cards that are already activated and working (OPEN) and cards shipped but not activated yet can be reported as missing or damaged
const shouldShowReportLostCardButton = currentPhysicalCard?.state === CONST.EXPENSIFY_CARD.STATE.NOT_ACTIVATED || currentPhysicalCard?.state === CONST.EXPENSIFY_CARD.STATE.OPEN;
const currency = getCardCurrency(currentCard, cardSettings);
const shouldShowPIN = currency !== CONST.CURRENCY.USD;
+ const canChangePIN = supportsPINManagementFeatures(currentPhysicalCard) && currentPhysicalCard?.state === CONST.EXPENSIFY_CARD.STATE.OPEN;
+ const canRevealPIN = canChangePIN && revealedPIN === undefined;
const formattedAvailableSpendAmount = convertToDisplayString(currentCard?.availableSpend, currency);
const {limitNameKey, limitTitleKey} = getLimitTypeTranslationKeys(currentCard?.nameValuePairs?.limitType);
@@ -399,9 +417,39 @@ function ExpensifyCardPage({route}: ExpensifyCardPageProps) {
{shouldShowPIN && (
{
+ executeScenario(CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.REVEAL_PIN, {
+ cardID: String(currentPhysicalCard?.cardID),
+ });
+ }}
+ isDisabled={isOffline}
+ />
+ ) : undefined
+ }
+ />
+ )}
+ {canChangePIN && (
+