Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
13519d1
implement reveal PIN
DylanDylann Mar 4, 2026
490ac4b
update: Reveal Pin
DylanDylann Mar 4, 2026
7621951
update: reveal PIN
DylanDylann Mar 4, 2026
4ad284c
implement: change PIN
DylanDylann Mar 4, 2026
1822424
Merge branch 'main' into release-3-change-pin
DylanDylann Mar 12, 2026
3e15e43
fix conflict
DylanDylann Mar 12, 2026
871f7d0
resolve comment
DylanDylann Mar 13, 2026
7eee94b
UI update
DylanDylann Mar 13, 2026
32fc414
chore
DylanDylann Mar 13, 2026
67f8efb
fix comments
DylanDylann Mar 16, 2026
2bf4655
resolved comment
DylanDylann Mar 16, 2026
399b766
Merge branch 'main' into release-3-change-pin
DylanDylann Mar 16, 2026
30a39cf
Merge branch 'main' into release-3-change-pin
DylanDylann Mar 16, 2026
2da84d6
typo
DylanDylann Mar 16, 2026
272d5e2
Merge branch 'main' into release-3-change-pin
DylanDylann Mar 16, 2026
c3e16f3
update server failure screen
DylanDylann Mar 16, 2026
55631ce
update the default outcome screen
DylanDylann Mar 16, 2026
567ac23
remove redundant
DylanDylann Mar 16, 2026
2771559
lint check fix
DylanDylann Mar 16, 2026
b5b03c8
Merge branch 'main' into release-3-change-pin
DylanDylann Mar 19, 2026
a1a4294
run prettier
DylanDylann Mar 19, 2026
6cd37e2
update translation key
DylanDylann Mar 19, 2026
0922cd6
allow empty pin displayed
DylanDylann Mar 19, 2026
78cc0d8
update outcome navigation
DylanDylann Mar 19, 2026
bb59a16
revert outcome changes
DylanDylann Mar 19, 2026
b154ddc
Merge branch 'main' into release-3-change-pin
DylanDylann Mar 19, 2026
6df1015
add empty line
DylanDylann Mar 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -861,7 +861,8 @@
"zoneinfo",
"zxcv",
"zxldvw",
"مثال"
"مثال",
"PINATM"
],
"ignorePaths": [
"src/languages/de.ts",
Expand Down
8 changes: 8 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,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,
Expand Down
2 changes: 2 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions src/components/Icon/Expensicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -224,6 +225,7 @@ export {
EyeDisabled,
FallbackAvatar,
Flag,
Key,
FlagLevelOne,
FlagLevelTwo,
FlagLevelThree,
Expand Down
Original file line number Diff line number Diff line change
@@ -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: <ChangePINSuccessScreen />,
defaultClientFailureScreen: <ClientFailureScreen />,
defaultServerFailureScreen: <ServerFailureScreen />,
} as const satisfies MultifactorAuthenticationScenarioCustomConfig<Payload>;

export type {Payload};
Original file line number Diff line number Diff line change
@@ -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<MultifactorAuthenticationScenario> | 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: <ClientFailureScreen />,
defaultServerFailureScreen: <ServerFailureScreen />,
} as const satisfies MultifactorAuthenticationScenarioCustomConfig<Payload>;

export type {Payload};
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<MultifactorAuthenticationScenario> | undefined): payload is Payload {
function isSetPINOrderCardPayload(payload: MultifactorAuthenticationScenarioAdditionalParams<MultifactorAuthenticationScenario> | 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',
);

/**
Expand All @@ -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)));
Expand All @@ -71,9 +79,8 @@ export default {
return CONST.MULTIFACTOR_AUTHENTICATION.CALLBACK_RESPONSE.SHOW_OUTCOME_SCREEN;
},

failureScreens: {
[CONST.MULTIFACTOR_AUTHENTICATION.REASON.EXPO.CANCELED]: <AuthenticationCanceledFailureScreen />,
},
defaultClientFailureScreen: <ClientFailureScreen />,
defaultServerFailureScreen: <ServerFailureScreen />,
} as const satisfies MultifactorAuthenticationScenarioCustomConfig<Payload>;

export type {Payload};
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,35 @@ 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.
* Each scenario that requires additional parameters should have its payload type defined here.
*/
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;
};

/**
* Configuration records for all multifactor authentication scenarios.
*/
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down
10 changes: 10 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,8 @@ const translations: TranslationDeepObject<typeof en> = {
},
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(`
Expand Down Expand Up @@ -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. <concierge-link>Kontaktieren Sie Concierge</concierge-link> falls Sie Fragen haben.',
freezeCard: 'Karte sperren',
unfreeze: 'Entsperren',
unfreezeCard: 'Karte entsperren',
Expand Down
14 changes: 14 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(`
Expand Down Expand Up @@ -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. <concierge-link>Reach out to Concierge</concierge-link> if you have any questions.',
freezeCard: 'Freeze card',
unfreeze: 'Unfreeze',
unfreezeCard: 'Unfreeze card',
Expand Down
14 changes: 14 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,12 @@ const translations: TranslationDeepObject<typeof en> = {
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!',
Expand Down Expand Up @@ -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. <concierge-link>Contacta a Concierge</concierge-link> si tienes alguna pregunta.',
freezeCard: 'Congelar tarjeta',
unfreeze: 'Descongelar',
unfreezeCard: 'Descongelar tarjeta',
Expand Down
Loading
Loading