diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 3b192bd39c728..15941dd43cb90 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -273,6 +273,10 @@ const ROUTES = { route: 'settings/wallet/card/:cardID/confirm-magic-code', getRoute: (cardID: string) => `settings/wallet/card/${cardID}/confirm-magic-code` as const, }, + SETTINGS_WALLET_CARD_MISSING_DETAILS: { + route: 'settings/wallet/card/:cardID/missing-details', + getRoute: (cardID: string) => `settings/wallet/card/${cardID}/missing-details` as const, + }, SETTINGS_DOMAIN_CARD_DETAIL: { route: 'settings/card/:cardID?', getRoute: (cardID: string) => `settings/card/${cardID}` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index acbdd26627cba..96dd5dcb51438 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -149,6 +149,7 @@ const SCREENS = { VERIFY_ACCOUNT: 'Settings_Wallet_VerifyAccount', DOMAIN_CARD: 'Settings_Wallet_DomainCard', DOMAIN_CARD_CONFIRM_MAGIC_CODE: 'Settings_Wallet_DomainCard_ConfirmMagicCode', + CARD_MISSING_DETAILS: 'Settings_Wallet_Card_MissingDetails', TRANSFER_BALANCE: 'Settings_Wallet_Transfer_Balance', CHOOSE_TRANSFER_ACCOUNT: 'Settings_Wallet_Choose_Transfer_Account', ENABLE_PAYMENTS: 'Settings_Wallet_EnablePayments', diff --git a/src/components/HeaderPageLayout.tsx b/src/components/HeaderPageLayout.tsx index 7371e86f61080..c7ad79a8c83cb 100644 --- a/src/components/HeaderPageLayout.tsx +++ b/src/components/HeaderPageLayout.tsx @@ -84,6 +84,7 @@ function HeaderPageLayout({ offlineIndicatorStyle={[appBGColor]} testID={testID} shouldShowOfflineIndicatorInWideScreen={shouldShowOfflineIndicatorInWideScreen} + shouldEnableMaxHeight > {({safeAreaPaddingBottomStyle}) => ( <> diff --git a/src/hooks/useAccountTabIndicatorStatus.ts b/src/hooks/useAccountTabIndicatorStatus.ts index d884e33102ff3..51b0e453cf808 100644 --- a/src/hooks/useAccountTabIndicatorStatus.ts +++ b/src/hooks/useAccountTabIndicatorStatus.ts @@ -35,7 +35,7 @@ function useAccountTabIndicatorStatus(): AccountTabIndicatorStatusResult { // we only care if a single error / info condition exists anywhere. const errorChecking: Partial> = { [CONST.INDICATOR_STATUS.HAS_USER_WALLET_ERRORS]: Object.keys(userWallet?.errors ?? {}).length > 0, - [CONST.INDICATOR_STATUS.HAS_PAYMENT_METHOD_ERROR]: hasPaymentMethodError(bankAccountList, fundList), + [CONST.INDICATOR_STATUS.HAS_PAYMENT_METHOD_ERROR]: hasPaymentMethodError(bankAccountList, fundList, allCards), [CONST.INDICATOR_STATUS.HAS_REIMBURSEMENT_ACCOUNT_ERRORS]: Object.keys(reimbursementAccount?.errors ?? {}).length > 0, [CONST.INDICATOR_STATUS.HAS_LOGIN_LIST_ERROR]: !!loginList && hasLoginListError(loginList), // Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead) diff --git a/src/hooks/useIndicatorStatus.ts b/src/hooks/useIndicatorStatus.ts index 1cbe70b6ff2aa..d515cf27de6d7 100644 --- a/src/hooks/useIndicatorStatus.ts +++ b/src/hooks/useIndicatorStatus.ts @@ -57,7 +57,7 @@ function useIndicatorStatus(): IndicatorStatusResult { // we only care if a single error / info condition exists anywhere. const errorChecking: Partial> = { [CONST.INDICATOR_STATUS.HAS_USER_WALLET_ERRORS]: Object.keys(userWallet?.errors ?? {}).length > 0, - [CONST.INDICATOR_STATUS.HAS_PAYMENT_METHOD_ERROR]: hasPaymentMethodError(bankAccountList, fundList), + [CONST.INDICATOR_STATUS.HAS_PAYMENT_METHOD_ERROR]: hasPaymentMethodError(bankAccountList, fundList, allCards), ...(Object.fromEntries(Object.entries(policyErrors).map(([error, policy]) => [error, !!policy])) as Record), [CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_ERRORS]: hasSubscriptionRedDotError(stripeCustomerId, retryBillingSuccessful, billingDisputePending), [CONST.INDICATOR_STATUS.HAS_REIMBURSEMENT_ACCOUNT_ERRORS]: Object.keys(reimbursementAccount?.errors ?? {}).length > 0, diff --git a/src/languages/de.ts b/src/languages/de.ts index e157b251cf9a0..fe6b717c807c7 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -2041,6 +2041,9 @@ const translations: TranslationDeepObject = { validateCardTitle: 'Lassen Sie uns sicherstellen, dass Sie es sind', enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Bitte geben Sie den magischen Code ein, der an ${contactMethod} gesendet wurde, um Ihre Kartendetails anzuzeigen. Er sollte in ein bis zwei Minuten ankommen.`, + missingPrivateDetails: ({missingDetailsLink}: {missingDetailsLink: string}) => + `Bitte fügen Sie Ihre persönlichen Daten hinzu und versuchen Sie es dann erneut.`, + unexpectedError: 'Beim Abrufen Ihrer Expensify-Kartendaten ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.', cardFraudAlert: { confirmButtonText: 'Ja, das tue ich.', reportFraudButtonText: 'Nein, das war ich nicht.', @@ -4630,7 +4633,7 @@ ${amount} für ${merchant} - ${date}`, issuedCardNoShippingDetails: ({assignee}: AssigneeParams) => `Für ${assignee} wurde eine Expensify Card ausgestellt! Die Karte wird versendet, sobald die Versanddetails bestätigt wurden.`, issuedCardVirtual: ({assignee, link}: IssueVirtualCardParams) => `hat ${assignee} eine virtuelle ${link} ausgestellt! Die Karte kann sofort verwendet werden.`, - addedShippingDetails: ({assignee}: AssigneeParams) => `${assignee} hat Versanddetails hinzugefügt. Die Expensify Card wird in 2-3 Werktagen ankommen.`, + addedShippingDetails: ({assignee}: AssigneeParams) => `${assignee} hat Versanddetails hinzugefügt. Die Expensify Card trifft in 2–3 Werktagen ein.`, verifyingHeader: 'Überprüfen', bankAccountVerifiedHeader: 'Bankkonto verifiziert', verifyingBankAccount: 'Bankkonto wird überprüft...', diff --git a/src/languages/en.ts b/src/languages/en.ts index 6440b71281619..4b6ff031eff59 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2026,6 +2026,8 @@ const translations = { cardDetailsLoadingFailure: 'An error occurred while loading the card details. Please check your internet connection and try again.', validateCardTitle: "Let's make sure it's you", enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Please enter the magic code sent to ${contactMethod} to view your card details. It should arrive within a minute or two.`, + missingPrivateDetails: ({missingDetailsLink}: {missingDetailsLink: string}) => `Please add your personal details, then try again.`, + unexpectedError: 'There was an error trying to get your Expensify card details. Please try again.', cardFraudAlert: { confirmButtonText: 'Yes, I do', reportFraudButtonText: "No, it wasn't me", diff --git a/src/languages/es.ts b/src/languages/es.ts index 0cc8b4e4d32b9..ec2747b92c42f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1699,6 +1699,8 @@ const translations: TranslationDeepObject = { cardDetailsLoadingFailure: 'Se ha producido un error al cargar los datos de la tarjeta. Comprueba tu conexión a Internet e inténtalo de nuevo.', validateCardTitle: 'Asegurémonos de que eres tú', enterMagicCode: ({contactMethod}) => `Introduzca el código mágico enviado a ${contactMethod} para ver los datos de su tarjeta. Debería llegar en un par de minutos.`, + missingPrivateDetails: ({missingDetailsLink}: {missingDetailsLink: string}) => `Por favor, agrega tus datos personales y vuelve a intentarlo.`, + unexpectedError: 'Se produjo un error al intentar obtener los detalles de tu tarjeta Expensify. Vuelve a intentarlo.', cardFraudAlert: { confirmButtonText: 'Sí, lo hago', reportFraudButtonText: 'No, no fui yo', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index a81c354e81ef2..30328bd396613 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -2042,6 +2042,8 @@ const translations: TranslationDeepObject = { validateCardTitle: "Assurons-nous que c'est bien vous", enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Veuillez entrer le code magique envoyé à ${contactMethod} pour voir les détails de votre carte. Il devrait arriver dans une minute ou deux.`, + missingPrivateDetails: ({missingDetailsLink}: {missingDetailsLink: string}) => `Veuillez ajouter vos informations personnelles, puis réessayez.`, + unexpectedError: 'Une erreur s’est produite lors de la récupération des informations de votre carte Expensify. Veuillez réessayer.', cardFraudAlert: { confirmButtonText: 'Oui, je le fais', reportFraudButtonText: "Non, ce n'était pas moi.", @@ -4637,9 +4639,9 @@ ${amount} pour ${merchant} - ${date}`, addShippingDetails: "Ajouter les détails d'expédition", issuedCard: ({assignee}: AssigneeParams) => `a émis une carte Expensify à ${assignee} ! La carte arrivera dans 2-3 jours ouvrables.`, issuedCardNoShippingDetails: ({assignee}: AssigneeParams) => - `a délivré une Expensify Card à ${assignee} ! La carte sera expédiée une fois que les détails d’expédition auront été confirmés.`, + `a délivré une Expensify Card à ${assignee} ! La carte sera expédiée une fois que les détails d'expédition auront été confirmés.`, issuedCardVirtual: ({assignee, link}: IssueVirtualCardParams) => `a émis une ${link} virtuelle à ${assignee} ! La carte peut être utilisée immédiatement.`, - addedShippingDetails: ({assignee}: AssigneeParams) => `${assignee} a ajouté les détails d'expédition. La carte Expensify arrivera dans 2-3 jours ouvrables.`, + addedShippingDetails: ({assignee}: AssigneeParams) => `${assignee} a ajouté les informations d’expédition. La carte Expensify arrivera dans 2 à 3 jours ouvrés.`, verifyingHeader: 'Vérification en cours', bankAccountVerifiedHeader: 'Compte bancaire vérifié', verifyingBankAccount: 'Vérification du compte bancaire...', diff --git a/src/languages/it.ts b/src/languages/it.ts index 5117a2c950a90..adb58f8c798c7 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -2034,6 +2034,8 @@ const translations: TranslationDeepObject = { validateCardTitle: 'Verifichiamo che sei tu', enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Inserisci il codice magico inviato a ${contactMethod} per visualizzare i dettagli della tua carta. Dovrebbe arrivare entro un minuto o due.`, + missingPrivateDetails: ({missingDetailsLink}: {missingDetailsLink: string}) => `Per favore aggiungi i tuoi dati personali, poi riprova.`, + unexpectedError: 'Si è verificato un errore durante il recupero dei dettagli della tua carta Expensify. Riprova.', cardFraudAlert: { confirmButtonText: 'Sì, lo faccio', reportFraudButtonText: 'No, non ero io', @@ -4646,7 +4648,7 @@ ${amount} per ${merchant} - ${date}`, issuedCardNoShippingDetails: ({assignee}: AssigneeParams) => `È stata emessa una Expensify Card per ${assignee}! La carta verrà spedita una volta confermati i dettagli di spedizione.`, issuedCardVirtual: ({assignee, link}: IssueVirtualCardParams) => `ha emesso ${assignee} una ${link} virtuale! La carta può essere utilizzata immediatamente.`, - addedShippingDetails: ({assignee}: AssigneeParams) => `${assignee} ha aggiunto i dettagli di spedizione. La carta Expensify arriverà in 2-3 giorni lavorativi.`, + addedShippingDetails: ({assignee}: AssigneeParams) => `${assignee} ha aggiunto i dettagli di spedizione. La Expensify Card arriverà in 2-3 giorni lavorativi.`, verifyingHeader: 'Verifica in corso', bankAccountVerifiedHeader: 'Conto bancario verificato', verifyingBankAccount: 'Verifica del conto bancario in corso...', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 550b85477cf1f..499dfda67801a 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -2027,6 +2027,8 @@ const translations: TranslationDeepObject = { cardDetailsLoadingFailure: 'カードの詳細を読み込む際にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。', validateCardTitle: 'あなたであることを確認しましょう', enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `カードの詳細を表示するには、${contactMethod} に送信されたマジックコードを入力してください。1~2分以内に届くはずです。`, + missingPrivateDetails: ({missingDetailsLink}: {missingDetailsLink: string}) => `個人情報を追加してから、もう一度お試しください。`, + unexpectedError: 'Expensifyカードの詳細を取得しようとしてエラーが発生しました。もう一度お試しください。', cardFraudAlert: { confirmButtonText: 'はい、そうです。', reportFraudButtonText: 'いいえ、それは私ではありませんでした。', @@ -2569,15 +2571,15 @@ ${date} - ${merchant}に${amount}`, splitExpenseTask: { title: '経費を分割する', description: - '*経費を分割する* には、1人または複数の人と共有します。\n' + + '1人以上の相手と*経費を分割*します。' + '\n' + - '1. 緑色の*+*ボタンをクリックします。\n' + - '2. *チャットを開始*を選択します。\n' + - '3. メールアドレスまたは電話番号を入力します。\n' + - '4. チャット内の灰色の*+*ボタンをクリック > *経費を分割*。\n' + - '5. *手動* 、*スキャン* 、または*距離*を選択して経費を作成します。\n' + + `${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE} ボタンをクリックします。` + + '2.「*Start chat*」を選択します。' + + '3. メールアドレスまたは電話番号を入力します..' + + '4. チャットでグレーの*+*ボタンをクリック > *経費を分割*。' + + '5. *Manual*、*Scan*、または*Distance* を選択して経費を作成します。' + '\n' + - '必要ならば詳細を追加するか、単に送信します。払い戻しをありましょう!', + '必要なら詳細を追加しても、そのまま送信しても構いません。さあ、精算してもらいましょう!', }, reviewWorkspaceSettingsTask: { title: ({workspaceSettingsLink}) => `[ワークスペース設定](${workspaceSettingsLink})を確認する`, @@ -4605,7 +4607,7 @@ ${date} - ${merchant}に${amount}`, issuedCard: ({assignee}: AssigneeParams) => `${assignee}にExpensifyカードを発行しました!カードは2~3営業日で到着します。`, issuedCardNoShippingDetails: ({assignee}: AssigneeParams) => `${assignee} に Expensify Card を発行しました!配送情報が確認され次第、カードは発送されます。`, issuedCardVirtual: ({assignee, link}: IssueVirtualCardParams) => `${assignee}にバーチャル${link}を発行しました!カードはすぐに使用できます。`, - addedShippingDetails: ({assignee}: AssigneeParams) => `${assignee}が配送情報を追加しました。Expensify Cardは2~3営業日で到着します。`, + addedShippingDetails: ({assignee}: AssigneeParams) => `${assignee} が配送情報を追加しました。Expensify Card は2~3営業日で到着します。`, verifyingHeader: '確認中', bankAccountVerifiedHeader: '銀行口座が確認されました', verifyingBankAccount: '銀行口座を確認しています...', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index bbd73c9cd0daa..caa979b345a35 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -2033,6 +2033,8 @@ const translations: TranslationDeepObject = { validateCardTitle: 'Laten we ervoor zorgen dat jij het bent', enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Voer de magische code in die naar ${contactMethod} is gestuurd om uw kaartgegevens te bekijken. Het zou binnen een minuut of twee moeten aankomen.`, + missingPrivateDetails: ({missingDetailsLink}: {missingDetailsLink: string}) => `Voeg je persoonlijke gegevens toe en probeer het daarna opnieuw.`, + unexpectedError: 'Er is een fout opgetreden bij het ophalen van je Expensify-kaartgegevens. Probeer het opnieuw.', cardFraudAlert: { confirmButtonText: 'Ja, dat doe ik.', reportFraudButtonText: 'Nee, dat was ik niet.', @@ -2579,7 +2581,7 @@ ${amount} voor ${merchant} - ${date}`, splitExpenseTask: { title: 'Splits een uitgave', description: - '*Splits uitgaven* met één of meer personen.\n' + + '*Uitgaven splitsen* met één of meer personen.' + '\n' + `1. Klik op de +-knop.\n` + '2. Kies *Start chat*.\n' + @@ -4641,7 +4643,7 @@ ${amount} voor ${merchant} - ${date}`, issuedCard: ({assignee}: AssigneeParams) => `heeft ${assignee} een Expensify Card uitgegeven! De kaart zal binnen 2-3 werkdagen arriveren.`, issuedCardNoShippingDetails: ({assignee}: AssigneeParams) => `heeft ${assignee} een Expensify Card uitgegeven! De kaart wordt verzonden zodra de verzendgegevens zijn bevestigd.`, issuedCardVirtual: ({assignee, link}: IssueVirtualCardParams) => `heeft ${assignee} een virtuele ${link} uitgegeven! De kaart kan direct worden gebruikt.`, - addedShippingDetails: ({assignee}: AssigneeParams) => `${assignee} heeft verzendgegevens toegevoegd. Expensify Card zal binnen 2-3 werkdagen arriveren.`, + addedShippingDetails: ({assignee}: AssigneeParams) => `${assignee} heeft verzendgegevens toegevoegd. Expensify Card wordt binnen 2-3 werkdagen bezorgd.`, verifyingHeader: 'Verifiëren', bankAccountVerifiedHeader: 'Bankrekening geverifieerd', verifyingBankAccount: 'Bankrekening verifiëren...', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index bcbf824e9d95c..12e483170ce42 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -2030,6 +2030,8 @@ const translations: TranslationDeepObject = { validateCardTitle: 'Upewnijmy się, że to Ty', enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Proszę wprowadzić magiczny kod wysłany na ${contactMethod}, aby zobaczyć szczegóły swojej karty. Powinien dotrzeć w ciągu minuty lub dwóch.`, + missingPrivateDetails: ({missingDetailsLink}: {missingDetailsLink: string}) => `Proszę dodać swoje dane osobowe, a następnie spróbuj ponownie.`, + unexpectedError: 'Wystąpił błąd podczas próby pobrania szczegółów Twojej karty Expensify. Spróbuj ponownie.', cardFraudAlert: { confirmButtonText: 'Tak, robię', reportFraudButtonText: 'Nie, to nie byłem ja', @@ -2572,7 +2574,7 @@ ${amount} dla ${merchant} - ${date}`, 'Elke chat wordt ook omgezet in een e-mail of sms waar ze direct op kunnen reageren.', }, splitExpenseTask: { - title: 'Splits een uitgave', + title: 'Podziel wydatek', description: '*Splits uitgaven* met één of meer personen.\n' + '\n' + @@ -4629,7 +4631,7 @@ ${amount} dla ${merchant} - ${date}`, issuedCard: ({assignee}: AssigneeParams) => `wydano ${assignee} kartę Expensify! Karta dotrze w ciągu 2-3 dni roboczych.`, issuedCardNoShippingDetails: ({assignee}: AssigneeParams) => `Wydano ${assignee} kartę Expensify! Karta zostanie wysłana po potwierdzeniu danych wysyłkowych.`, issuedCardVirtual: ({assignee, link}: IssueVirtualCardParams) => `wydano ${assignee} wirtualną ${link}! Karta może być używana od razu.`, - addedShippingDetails: ({assignee}: AssigneeParams) => `${assignee} dodał szczegóły wysyłki. Karta Expensify dotrze w ciągu 2-3 dni roboczych.`, + addedShippingDetails: ({assignee}: AssigneeParams) => `${assignee} dodał(a) szczegóły wysyłki. Expensify Card dotrze w ciągu 2-3 dni roboczych.`, verifyingHeader: 'Weryfikacja', bankAccountVerifiedHeader: 'Zweryfikowano konto bankowe', verifyingBankAccount: 'Weryfikacja konta bankowego...', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 2b3d086dacf39..fb43a6ed1f2a5 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -2031,6 +2031,8 @@ const translations: TranslationDeepObject = { validateCardTitle: 'Vamos garantir que é você', enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Por favor, insira o código mágico enviado para ${contactMethod} para visualizar os detalhes do seu cartão. Ele deve chegar dentro de um ou dois minutos.`, + missingPrivateDetails: ({missingDetailsLink}: {missingDetailsLink: string}) => `Por favor, adicione seus dados pessoais e tente novamente.`, + unexpectedError: 'Ocorreu um erro ao tentar obter os detalhes do seu cartão Expensify. Tente novamente.', cardFraudAlert: { confirmButtonText: 'Sim, eu aceito', reportFraudButtonText: 'Não, não fui eu', @@ -4635,7 +4637,7 @@ ${amount} para ${merchant} - ${date}`, issuedCardNoShippingDetails: ({assignee}: AssigneeParams) => `Foi emitido para ${assignee} um Expensify Card! O cartão será enviado assim que os detalhes de envio forem confirmados.`, issuedCardVirtual: ({assignee, link}: IssueVirtualCardParams) => `emitiu ${assignee} um ${link} virtual! O cartão pode ser usado imediatamente.`, - addedShippingDetails: ({assignee}: AssigneeParams) => `${assignee} adicionou os detalhes de envio. O Cartão Expensify chegará em 2-3 dias úteis.`, + addedShippingDetails: ({assignee}: AssigneeParams) => `${assignee} adicionou informações de envio. O Expensify Card chegará em 2-3 dias úteis.`, verifyingHeader: 'Verificando', bankAccountVerifiedHeader: 'Conta bancária verificada', verifyingBankAccount: 'Verificando conta bancária...', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 69ee8bd100938..1ccc342cf94ee 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -2002,6 +2002,8 @@ const translations: TranslationDeepObject = { cardDetailsLoadingFailure: '加载卡片详情时发生错误。请检查您的互联网连接并重试。', validateCardTitle: '让我们确认一下身份', enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `请输入发送到${contactMethod}的验证码以查看您的卡详细信息。验证码应在一两分钟内到达。`, + missingPrivateDetails: ({missingDetailsLink}: {missingDetailsLink: string}) => `请添加您的个人信息,然后重试。`, + unexpectedError: '尝试获取您的 Expensify 卡片详情时出错。请重试。', cardFraudAlert: { confirmButtonText: '是的,我愿意。', reportFraudButtonText: '不,不是我', @@ -2539,15 +2541,15 @@ ${merchant}的${amount} - ${date}`, splitExpenseTask: { title: '拆分支出', description: - '*与一个或多个人拆分支出*。\n' + + '与一人或多人一起*分摊费用*。' + '\n' + - '1. 点击绿色的 *+* 按钮。\n' + - '2. 选择 *开始聊天*。\n' + - '3. 输入电子邮件或电话号码。\n' + - '4. 点击聊天中的灰色 *+* 按钮 > *拆分支出*。\n' + - '5. 通过选择 *手动*、*扫描*或 *距离*创建支出。\n' + + `点击${CONST.CUSTOM_EMOJIS.GLOBAL_CREATE}按钮。` + + '2. 选择*开始聊天*。' + + '3. 输入电子邮件地址或电话号码..' + + '4. 在聊天中点击灰色的*+*按钮 > *拆分费用*。' + + '5. 通过选择*手动*、*扫描*或*距离*来创建费用。' + '\n' + - '如有需要,随意添加更多详情,或直接发送。让我们让您获得报销!', + '如果你愿意,可以补充更多细节,或者直接提交。让我们帮你尽快拿到报销款!', }, reviewWorkspaceSettingsTask: { title: ({workspaceSettingsLink}) => `查看您的[工作区设置](${workspaceSettingsLink})`, @@ -4541,7 +4543,7 @@ ${merchant}的${amount} - ${date}`, issuedCard: ({assignee}: AssigneeParams) => `已为${assignee}发放了一张Expensify卡!该卡将在2-3个工作日内送达。`, issuedCardNoShippingDetails: ({assignee}: AssigneeParams) => `已向${assignee}发放一张 Expensify Card!确认运送信息后将寄出该卡。`, issuedCardVirtual: ({assignee, link}: IssueVirtualCardParams) => `已向${assignee}发放了一张虚拟${link}!该卡可以立即使用。`, - addedShippingDetails: ({assignee}: AssigneeParams) => `${assignee} 添加了送货详情。Expensify Card 将在2-3个工作日内送达。`, + addedShippingDetails: ({assignee}: AssigneeParams) => `${assignee} 已添加发货详情。Expensify Card 将在 2-3 个工作日内送达。`, verifyingHeader: '验证中', bankAccountVerifiedHeader: '银行账户已验证', verifyingBankAccount: '正在验证银行账户...', diff --git a/src/libs/API/parameters/SetPersonalDetailsAndRevealExpensifyCardParams.ts b/src/libs/API/parameters/SetPersonalDetailsAndRevealExpensifyCardParams.ts new file mode 100644 index 0000000000000..a10bb96bc3059 --- /dev/null +++ b/src/libs/API/parameters/SetPersonalDetailsAndRevealExpensifyCardParams.ts @@ -0,0 +1,16 @@ +type SetPersonalDetailsAndRevealExpensifyCardParams = { + legalFirstName: string; + legalLastName: string; + phoneNumber: string; + addressCity: string; + addressStreet: string; + addressStreet2: string; + addressZip: string; + addressCountry: string; + addressState: string; + dob: string; + validateCode: string; + cardID: number; +}; + +export default SetPersonalDetailsAndRevealExpensifyCardParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 9bc2d39d5b2b5..63fac4774afa3 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -376,6 +376,7 @@ export type {default as UpdateCompanyCard} from './UpdateCompanyCard'; export type {default as UpdateCompanyCardNameParams} from './UpdateCompanyCardNameParams'; export type {default as SetCompanyCardExportAccountParams} from './SetCompanyCardExportAccountParams'; export type {default as SetPersonalDetailsAndShipExpensifyCardsParams} from './SetPersonalDetailsAndShipExpensifyCardsParams'; +export type {default as SetPersonalDetailsAndRevealExpensifyCardParams} from './SetPersonalDetailsAndRevealExpensifyCardParams'; export type {default as RequestFeedSetupParams} from './RequestFeedSetupParams'; export type {default as SetInvoicingTransferBankAccountParams} from './SetInvoicingTransferBankAccountParams'; export type {default as ConnectPolicyToQuickBooksDesktopParams} from './ConnectPolicyToQuickBooksDesktopParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index c3a659e47c255..e63ab634418a9 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -1220,6 +1220,7 @@ const SIDE_EFFECT_REQUEST_COMMANDS = { OPEN_OLD_DOT_LINK: 'OpenOldDotLink', RECONNECT_APP: 'ReconnectApp', REVEAL_EXPENSIFY_CARD_DETAILS: 'RevealExpensifyCardDetails', + SET_PERSONAL_DETAILS_AND_REVEAL_EXPENSIFY_CARD: 'SetPersonalDetailsAndRevealExpensifyCard', TWO_FACTOR_AUTH_VALIDATE: 'TwoFactorAuth_Validate', CONNECT_AS_DELEGATE: 'ConnectAsDelegate', ACTIVATE_PHYSICAL_EXPENSIFY_CARD: 'ActivatePhysicalExpensifyCard', @@ -1247,6 +1248,7 @@ type SideEffectRequestCommandParameters = { [SIDE_EFFECT_REQUEST_COMMANDS.AUTHENTICATE_PUSHER]: Parameters.AuthenticatePusherParams; [SIDE_EFFECT_REQUEST_COMMANDS.OPEN_OLD_DOT_LINK]: Parameters.OpenOldDotLinkParams; [SIDE_EFFECT_REQUEST_COMMANDS.REVEAL_EXPENSIFY_CARD_DETAILS]: Parameters.RevealExpensifyCardDetailsParams; + [SIDE_EFFECT_REQUEST_COMMANDS.SET_PERSONAL_DETAILS_AND_REVEAL_EXPENSIFY_CARD]: Parameters.SetPersonalDetailsAndRevealExpensifyCardParams; [SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES]: Parameters.GetMissingOnyxMessagesParams; [SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP]: Parameters.ReconnectAppParams; [SIDE_EFFECT_REQUEST_COMMANDS.GENERATE_SPOTNANA_TOKEN]: Parameters.GenerateSpotnanaTokenParams; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 7434e4ff9c72b..26e7f153036b7 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -374,6 +374,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Wallet/ExpensifyCardPage/index').default, [SCREENS.SETTINGS.WALLET.DOMAIN_CARD_CONFIRM_MAGIC_CODE]: () => require('../../../../pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardVerifyAccountPage').default, + [SCREENS.SETTINGS.WALLET.CARD_MISSING_DETAILS]: () => require('../../../../pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardMissingDetailsPage').default, [SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD]: () => require('../../../../pages/settings/Wallet/ReportVirtualCardFraudPage').default, [SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD_CONFIRM_MAGIC_CODE]: () => require('../../../../pages/settings/Wallet/ReportVirtualCardFraudVerifyAccountPage').default, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 8f024598db677..56a20ead92635 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -228,6 +228,10 @@ const config: LinkingOptions['config'] = { path: ROUTES.SETTINGS_WALLET_DOMAIN_CARD_CONFIRM_MAGIC_CODE.route, exact: true, }, + [SCREENS.SETTINGS.WALLET.CARD_MISSING_DETAILS]: { + path: ROUTES.SETTINGS_WALLET_CARD_MISSING_DETAILS.route, + exact: true, + }, [SCREENS.SETTINGS.WALLET.VERIFY_ACCOUNT]: { path: ROUTES.SETTINGS_WALLET_VERIFY_ACCOUNT, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 28ad145a4de66..0a32fc43d3d7e 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -171,6 +171,10 @@ type SettingsNavigatorParamList = { /** cardID of selected card */ cardID: string; }; + [SCREENS.SETTINGS.WALLET.CARD_MISSING_DETAILS]: { + /** cardID of selected card */ + cardID: string; + }; [SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD]: { /** cardID of selected card */ cardID: string; diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index ef06d8dc30557..cb41e98be9d78 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -294,6 +294,18 @@ function revealVirtualCardDetails(cardID: number, validateCode: string): Promise return; } + if (response?.jsonCode === 404) { + // eslint-disable-next-line prefer-promise-reject-errors + reject('cardPage.missingPrivateDetails'); + return; + } + + if (response?.jsonCode === 500) { + // eslint-disable-next-line prefer-promise-reject-errors + reject('cardPage.unexpectedError'); + return; + } + // eslint-disable-next-line prefer-promise-reject-errors reject('cardPage.cardDetailsLoadingFailure'); return; diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts index 641b7e5f1a4e3..4aae775d07209 100644 --- a/src/libs/actions/PaymentMethods.ts +++ b/src/libs/actions/PaymentMethods.ts @@ -23,7 +23,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/AddPaymentCardForm'; -import type {BankAccountList, FundList} from '@src/types/onyx'; +import type {BankAccountList, CardList, FundList} from '@src/types/onyx'; import type PaymentMethod from '@src/types/onyx/PaymentMethod'; import type {OnyxData} from '@src/types/onyx/Request'; import type {FilterMethodPaymentType} from '@src/types/onyx/WalletTransfer'; @@ -407,8 +407,8 @@ function dismissSuccessfulTransferBalancePage() { * Looks through each payment method to see if there is an existing error * */ -function hasPaymentMethodError(bankList: OnyxEntry, fundList: OnyxEntry): boolean { - const combinedPaymentMethods = {...bankList, ...fundList}; +function hasPaymentMethodError(bankList: OnyxEntry, fundList: OnyxEntry, cardList: OnyxEntry): boolean { + const combinedPaymentMethods = {...bankList, ...fundList, ...cardList}; return Object.values(combinedPaymentMethods).some((item) => Object.keys(item.errors ?? {}).length); } diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 3f1228a9aae10..3e44635801b31 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -6,6 +6,7 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; import * as API from '@libs/API'; import type { OpenPublicProfilePageParams, + SetPersonalDetailsAndRevealExpensifyCardParams, SetPersonalDetailsAndShipExpensifyCardsParams, UpdateAutomaticTimezoneParams, UpdateDateOfBirthParams, @@ -17,7 +18,7 @@ import type { UpdateSelectedTimezoneParams, UpdateUserAvatarParams, } from '@libs/API/parameters'; -import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import DateUtils from '@libs/DateUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -538,6 +539,105 @@ function updatePersonalDetailsAndShipExpensifyCards(values: FormOnyxValues, + validateCode: string, + countryCode: number, + cardID: number, +): Promise<{pan: string; expiration: string; cvv: string}> { + return new Promise((resolve, reject) => { + const parameters: SetPersonalDetailsAndRevealExpensifyCardParams = { + legalFirstName: values.legalFirstName?.trim() ?? '', + legalLastName: values.legalLastName?.trim() ?? '', + phoneNumber: LoginUtils.appendCountryCode(values.phoneNumber?.trim() ?? '', countryCode), + addressCity: values.city.trim(), + addressStreet: values.addressLine1?.trim() ?? '', + addressStreet2: values.addressLine2?.trim() ?? '', + addressZip: values.zipPostCode?.trim().toUpperCase() ?? '', + addressCountry: values.country, + addressState: values.state.trim(), + dob: values.dob, + validateCode, + cardID, + }; + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + value: { + isLoading: true, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: {isLoading: true}, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + value: { + isLoading: false, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: {isLoading: false}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: {[cardID]: {errors: null}}, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + value: { + isLoading: false, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: {isLoading: false}, + }, + ]; + + // eslint-disable-next-line rulesdir/no-api-side-effects-method + API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.SET_PERSONAL_DETAILS_AND_REVEAL_EXPENSIFY_CARD, parameters, { + optimisticData, + successData, + failureData, + }) + .then((response) => { + if (response?.jsonCode !== CONST.JSON_CODE.SUCCESS) { + if (response?.jsonCode === CONST.JSON_CODE.INCORRECT_MAGIC_CODE) { + // eslint-disable-next-line prefer-promise-reject-errors + reject('validateCodeForm.error.incorrectMagicCode'); + return; + } + + // eslint-disable-next-line prefer-promise-reject-errors + reject('cardPage.unexpectedError'); + return; + } + resolve(response as {pan: string; expiration: string; cvv: string}); + }) + .catch(() => { + // eslint-disable-next-line prefer-promise-reject-errors + reject('cardPage.cardDetailsLoadingFailure'); + }); + }); +} + export { clearAvatarErrors, deleteAvatar, @@ -554,5 +654,6 @@ export { updatePronouns, updateSelectedTimezone, updatePersonalDetailsAndShipExpensifyCards, + setPersonalDetailsAndRevealExpensifyCard, clearPersonalDetailsErrors, }; diff --git a/src/pages/MissingPersonalDetails/MissingPersonalDetailsContent.tsx b/src/pages/MissingPersonalDetails/MissingPersonalDetailsContent.tsx index f7bd83ca9df46..26abb79a033f0 100644 --- a/src/pages/MissingPersonalDetails/MissingPersonalDetailsContent.tsx +++ b/src/pages/MissingPersonalDetails/MissingPersonalDetailsContent.tsx @@ -30,11 +30,17 @@ import {getInitialSubstep, getSubstepValues} from './utils'; type MissingPersonalDetailsContentProps = { privatePersonalDetails: OnyxEntry; draftValues: OnyxEntry; + + /** Optional custom header title */ + headerTitle?: string; + + /** Optional custom completion handler */ + onComplete?: (values: PersonalDetailsForm, validateCode: string) => void; }; const formSteps = [LegalName, DateOfBirth, Address, PhoneNumber, Confirmation]; -function MissingPersonalDetailsContent({privatePersonalDetails, draftValues}: MissingPersonalDetailsContentProps) { +function MissingPersonalDetailsContent({privatePersonalDetails, draftValues, headerTitle, onComplete}: MissingPersonalDetailsContentProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(false); @@ -84,9 +90,13 @@ function MissingPersonalDetailsContent({privatePersonalDetails, draftValues}: Mi const handleSubmitForm = useCallback( (validateCode: string) => { + if (onComplete) { + onComplete(values, validateCode); + return; + } updatePersonalDetailsAndShipExpensifyCards(values, validateCode, countryCode); }, - [countryCode, values], + [countryCode, values, onComplete], ); const handleNextScreen = useCallback(() => { @@ -115,7 +125,7 @@ function MissingPersonalDetailsContent({privatePersonalDetails, draftValues}: Mi shouldShowOfflineIndicatorInWideScreen={!!isValidateCodeActionModalVisible} > diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index fe69b095d7df1..a25344c530fb0 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -119,7 +119,9 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr const hasBrokenFeedConnection = checkIfFeedConnectionIsBroken(allCards, CONST.EXPENSIFY_CARD.BANK); const walletBrickRoadIndicator = - hasPaymentMethodError(bankAccountList, fundList) || !isEmptyObject(userWallet?.errors) || !isEmptyObject(walletTerms?.errors) || hasBrokenFeedConnection ? 'error' : undefined; + hasPaymentMethodError(bankAccountList, fundList, allCards) || !isEmptyObject(userWallet?.errors) || !isEmptyObject(walletTerms?.errors) || hasBrokenFeedConnection + ? 'error' + : undefined; const [shouldShowSignoutConfirmModal, setShouldShowSignoutConfirmModal] = useState(false); diff --git a/src/pages/settings/Profile/PersonalDetails/DateOfBirthPage.tsx b/src/pages/settings/Profile/PersonalDetails/DateOfBirthPage.tsx index eab021b7648ea..91842344ee9c5 100644 --- a/src/pages/settings/Profile/PersonalDetails/DateOfBirthPage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/DateOfBirthPage.tsx @@ -19,10 +19,11 @@ import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/DateOfBirthForm'; function DateOfBirthPage() { - const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); - const [isLoadingApp = true] = useOnyx(ONYXKEYS.IS_LOADING_APP); + const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); + const [isLoadingApp = true] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); const {translate} = useLocalize(); const styles = useThemeStyles(); + /** * @returns An object containing the errors for each inputID */ diff --git a/src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx b/src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx index 0f4adea9d6938..40024cac51067 100644 --- a/src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx @@ -106,19 +106,19 @@ function PhoneNumberPage() { InputComponent={TextInput} ref={inputCallbackRef} inputID={INPUT_IDS.PHONE_NUMBER} - name="legalFirstName" + name="phoneNumber" label={translate('common.phoneNumber')} aria-label={translate('common.phoneNumber')} role={CONST.ROLE.PRESENTATION} defaultValue={phoneNumber} spellCheck={false} + inputMode={CONST.INPUT_MODE.TEL} onBlur={() => { if (!validateLoginError) { return; } clearPhoneNumberError(); }} - inputMode={CONST.INPUT_MODE.TEL} /> diff --git a/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx b/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx index d6d8c40bef09c..ecb0ff6d749b4 100644 --- a/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx +++ b/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx @@ -1,6 +1,5 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; -import BigNumberPad from '@components/BigNumberPad'; import Button from '@components/Button'; import IllustratedHeaderPageLayout from '@components/IllustratedHeaderPageLayout'; import LottieAnimations from '@components/LottieAnimations'; @@ -13,7 +12,6 @@ import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import {getLatestErrorMessage} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -30,7 +28,6 @@ import {getEmptyObject, isEmptyObject} from '@src/types/utils/EmptyObject'; type ActivatePhysicalCardPageProps = PlatformStackScreenProps; const LAST_FOUR_DIGITS_LENGTH = 4; -const MAGIC_INPUT_MIN_HEIGHT = 86; function ActivatePhysicalCardPage({ route: { @@ -46,7 +43,6 @@ function ActivatePhysicalCardPage({ const [formError, setFormError] = useState(''); const [lastFourDigits, setLastFourDigits] = useState(''); - const [lastPressedDigit, setLastPressedDigit] = useState(''); const [canShowError, setCanShowError] = useState(false); const inactiveCard = cardList?.[cardID]; @@ -70,23 +66,8 @@ function ActivatePhysicalCardPage({ return; } clearCardListErrors(inactiveCard?.cardID); - - return () => { - if (!inactiveCard?.cardID) { - return; - } - clearCardListErrors(inactiveCard?.cardID); - }; }, [inactiveCard?.cardID]); - /** - * Update lastPressedDigit with value that was pressed on BigNumberPad. - * - * NOTE: If the same digit is pressed twice in a row, append it to the end of the string - * so that useEffect inside MagicCodeInput will be triggered by artificial change of the value. - */ - const updateLastPressedDigit = useCallback((key: string) => setLastPressedDigit(lastPressedDigit === key ? lastPressedDigit + key : key), [lastPressedDigit]); - /** * Handle card activation code input */ @@ -131,21 +112,18 @@ function ActivatePhysicalCardPage({ shouldShowOfflineIndicatorInWideScreen > {translate('activateCardPage.pleaseEnterLastFour')} - + - {canUseTouchScreen() && }