From 06505aec4e53b635bcbab49e33f6478de48dd4ac Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Mon, 25 May 2026 13:07:42 -0700 Subject: [PATCH 1/3] Reapply "Re-apply secure private personal details + fix address autocomplete and country auto-fill" This reverts commit 30d6f08da52b1aaf04c09a6636882b82341bfba5. --- package-lock.json | 12 +- src/CONST/index.ts | 821 ------------------ src/ROUTES.ts | 6 + src/SCREENS.ts | 2 + src/components/AddressForm.tsx | 7 +- src/languages/de.ts | 3 +- src/languages/en.ts | 3 +- src/languages/es.ts | 3 +- src/languages/fr.ts | 3 +- src/languages/it.ts | 3 +- src/languages/ja.ts | 3 +- src/languages/nl.ts | 3 +- src/languages/pl.ts | 3 +- src/languages/pt-BR.ts | 3 +- src/languages/zh-hans.ts | 3 +- .../UpdatePrivatePersonalDetailsParams.ts | 16 + src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + .../ModalStackNavigators/index.tsx | 6 + .../RELATIONS/SETTINGS_TO_RHP.ts | 2 + src/libs/Navigation/linkingConfig/config.ts | 8 + src/libs/Navigation/types.ts | 4 + src/libs/PersonalDetailsUtils.ts | 24 + src/libs/actions/PersonalDetails.ts | 53 ++ .../subPages/Address.tsx | 7 +- .../AddressFormFields.tsx | 2 +- ...atePersonalDetailsConfirmMagicCodePage.tsx | 81 ++ .../PrivatePersonalDetailsPage.tsx | 414 +++++++++ src/pages/settings/Profile/ProfilePage.tsx | 41 +- 29 files changed, 664 insertions(+), 875 deletions(-) create mode 100644 src/libs/API/parameters/UpdatePrivatePersonalDetailsParams.ts create mode 100644 src/pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsConfirmMagicCodePage.tsx create mode 100644 src/pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsPage.tsx diff --git a/package-lock.json b/package-lock.json index af8158d885dc..fe8b3194a5aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23823,12 +23823,6 @@ "ua-parser-js": "^1.0.38" } }, - "node_modules/expensify-common/node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, "node_modules/expensify-common/node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -31032,9 +31026,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash-es": { diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 3b8a9ad07f7b..45b7dcf87512 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -5479,827 +5479,6 @@ const CONST = { SEK: 'SE', } as Record, - // Sources: https://github.com/Expensify/App/issues/14958#issuecomment-1442138427 - // https://github.com/Expensify/App/issues/14958#issuecomment-1456026810 - COUNTRY_ZIP_REGEX_DATA: { - AC: { - regex: /^ASCN 1ZZ$/, - samples: 'ASCN 1ZZ', - }, - AD: { - regex: /^AD[1-7]0\d$/, - samples: 'AD206, AD403, AD106, AD406', - }, - - // We have kept the empty object for the countries which do not have any zip code validation - // to ensure consistency so that the amount of countries displayed and in this object are same - AE: {}, - AF: { - regex: /^\d{4}$/, - samples: '9536, 1476, 3842, 7975', - }, - AG: {}, - AI: { - regex: /^AI-2640$/, - samples: 'AI-2640', - }, - AL: { - regex: /^\d{4}$/, - samples: '1631, 9721, 2360, 5574', - }, - AM: { - regex: /^\d{4}$/, - samples: '5581, 7585, 8434, 2492', - }, - AO: {}, - AQ: {}, - AR: { - regex: /^((?:[A-HJ-NP-Z])?\d{4})([A-Z]{3})?$/, - samples: 'Q7040GFQ, K2178ZHR, P6240EJG, J6070IAE', - }, - AS: { - regex: /^96799$/, - samples: '96799', - }, - AT: { - regex: /^\d{4}$/, - samples: '4223, 2052, 3544, 5488', - }, - AU: { - regex: /^\d{4}$/, - samples: '7181, 7735, 9169, 8780', - }, - AW: {}, - AX: { - regex: /^22\d{3}$/, - samples: '22270, 22889, 22906, 22284', - }, - AZ: { - regex: /^(AZ) (\d{4})$/, - samples: 'AZ 6704, AZ 5332, AZ 3907, AZ 6892', - }, - BA: { - regex: /^\d{5}$/, - samples: '62722, 80420, 44595, 74614', - }, - BB: { - regex: /^BB\d{5}$/, - samples: 'BB64089, BB17494, BB73163, BB25752', - }, - BD: { - regex: /^\d{4}$/, - samples: '8585, 8175, 7381, 0154', - }, - BE: { - regex: /^\d{4}$/, - samples: '7944, 5303, 6746, 7921', - }, - BF: {}, - BG: { - regex: /^\d{4}$/, - samples: '6409, 7657, 1206, 7908', - }, - BH: { - regex: /^\d{3}\d?$/, - samples: '047, 1116, 490, 631', - }, - BI: {}, - BJ: {}, - BL: { - regex: /^97133$/, - samples: '97133', - }, - BM: { - regex: /^[A-Z]{2} ?[A-Z0-9]{2}$/, - samples: 'QV9P, OSJ1, PZ 3D, GR YK', - }, - BN: { - regex: /^[A-Z]{2} ?\d{4}$/, - samples: 'PF 9925, TH1970, SC 4619, NF0781', - }, - BO: {}, - BQ: {}, - BR: { - regex: /^\d{5}-?\d{3}$/, - samples: '18816-403, 95177-465, 43447-782, 39403-136', - }, - BS: {}, - BT: { - regex: /^\d{5}$/, - samples: '28256, 52484, 30608, 93524', - }, - BW: {}, - BY: { - regex: /^\d{6}$/, - samples: '504154, 360246, 741167, 895047', - }, - BZ: {}, - CA: { - regex: /^[ABCEGHJKLMNPRSTVXY]\d[ABCEGHJ-NPRSTV-Z] ?\d[ABCEGHJ-NPRSTV-Z]\d$/, - samples: 'S1A7K8, Y5H 4G6, H9V0P2, H1A1B5', - }, - CC: { - regex: /^6799$/, - samples: '6799', - }, - CD: {}, - CF: {}, - CG: {}, - CH: { - regex: /^\d{4}$/, - samples: '6370, 5271, 7873, 8220', - }, - CI: {}, - CK: {}, - CL: { - regex: /^\d{7}$/, - samples: '7565829, 8702008, 3161669, 1607703', - }, - CM: {}, - CN: { - regex: /^\d{6}$/, - samples: '240543, 870138, 295528, 861683', - }, - CO: { - regex: /^\d{6}$/, - samples: '678978, 775145, 823943, 913970', - }, - CR: { - regex: /^\d{5}$/, - samples: '28256, 52484, 30608, 93524', - }, - CU: { - regex: /^(?:CP)?(\d{5})$/, - samples: '28256, 52484, 30608, 93524', - }, - CV: { - regex: /^\d{4}$/, - samples: '9056, 8085, 0491, 4627', - }, - CW: {}, - CX: { - regex: /^6798$/, - samples: '6798', - }, - CY: { - regex: /^\d{4}$/, - samples: '9301, 2478, 1981, 6162', - }, - CZ: { - regex: /^\d{3} ?\d{2}$/, - samples: '150 56, 50694, 229 08, 82811', - }, - DE: { - regex: /^\d{5}$/, - samples: '33185, 37198, 81711, 44262', - }, - DJ: {}, - DK: { - regex: /^\d{4}$/, - samples: '1429, 2457, 0637, 5764', - }, - DM: {}, - DO: { - regex: /^\d{5}$/, - samples: '11877, 95773, 93875, 98032', - }, - DZ: { - regex: /^\d{5}$/, - samples: '26581, 64621, 57550, 72201', - }, - EC: { - regex: /^\d{6}$/, - samples: '541124, 873848, 011495, 334509', - }, - EE: { - regex: /^\d{5}$/, - samples: '87173, 01127, 73214, 52381', - }, - EG: { - regex: /^\d{5}$/, - samples: '98394, 05129, 91463, 77359', - }, - EH: { - regex: /^\d{5}$/, - samples: '30577, 60264, 16487, 38593', - }, - ER: {}, - ES: { - regex: /^\d{5}$/, - samples: '03315, 00413, 23179, 89324', - }, - ET: { - regex: /^\d{4}$/, - samples: '6269, 8498, 4514, 7820', - }, - FI: { - regex: /^\d{5}$/, - samples: '21859, 72086, 22422, 03774', - }, - FJ: {}, - FK: { - regex: /^FIQQ 1ZZ$/, - samples: 'FIQQ 1ZZ', - }, - FM: { - regex: /^(9694[1-4])(?:[ -](\d{4}))?$/, - samples: '96942-9352, 96944-4935, 96941 9065, 96943-5369', - }, - FO: { - regex: /^\d{3}$/, - samples: '334, 068, 741, 787', - }, - FR: { - regex: /^\d{2} ?\d{3}$/, - samples: '25822, 53 637, 55354, 82522', - }, - GA: {}, - GB: { - regex: /^[A-Z]{1,2}[0-9R][0-9A-Z]?\s*([0-9][ABD-HJLNP-UW-Z]{2})?$/, - samples: 'LA102UX, BL2F8FX, BD1S9LU, WR4G 6LH, W1U', - }, - GD: {}, - GE: { - regex: /^\d{4}$/, - samples: '1232, 9831, 4717, 9428', - }, - GF: { - regex: /^9[78]3\d{2}$/, - samples: '98380, 97335, 98344, 97300', - }, - GG: { - regex: /^GY\d[\dA-Z]? ?\d[ABD-HJLN-UW-Z]{2}$/, - samples: 'GY757LD, GY6D 6XL, GY3Y2BU, GY85 1YO', - }, - GH: {}, - GI: { - regex: /^GX11 1AA$/, - samples: 'GX11 1AA', - }, - GL: { - regex: /^39\d{2}$/, - samples: '3964, 3915, 3963, 3956', - }, - GM: {}, - GN: { - regex: /^\d{3}$/, - samples: '465, 994, 333, 078', - }, - GP: { - regex: /^9[78][01]\d{2}$/, - samples: '98069, 97007, 97147, 97106', - }, - GQ: {}, - GR: { - regex: /^\d{3} ?\d{2}$/, - samples: '98654, 319 78, 127 09, 590 52', - }, - GS: { - regex: /^SIQQ 1ZZ$/, - samples: 'SIQQ 1ZZ', - }, - GT: { - regex: /^\d{5}$/, - samples: '30553, 69925, 09376, 83719', - }, - GU: { - regex: /^((969)[1-3][0-2])$/, - samples: '96922, 96932, 96921, 96911', - }, - GW: { - regex: /^\d{4}$/, - samples: '1742, 7941, 4437, 7728', - }, - GY: {}, - HK: { - regex: /^999077$|^$/, - samples: '999077', - }, - HN: { - regex: /^\d{5}$/, - samples: '86238, 78999, 03594, 30406', - }, - HR: { - regex: /^\d{5}$/, - samples: '85240, 80710, 78235, 98766', - }, - HT: { - regex: /^(?:HT)?(\d{4})$/, - samples: '5101, HT6991, HT3871, 1126', - }, - HU: { - regex: /^\d{4}$/, - samples: '0360, 2604, 3362, 4775', - }, - ID: { - regex: /^\d{5}$/, - samples: '60993, 52656, 16521, 34931', - }, - IE: {}, - IL: { - regex: /^\d{5}(?:\d{2})?$/, - samples: '74213, 6978354, 2441689, 4971551', - }, - IM: { - regex: /^IM\d[\dA-Z]? ?\d[ABD-HJLN-UW-Z]{2}$/, - samples: 'IM2X1JP, IM4V 9JU, IM3B1UP, IM8E 5XF', - }, - IN: { - regex: /^\d{6}$/, - samples: '946956, 143659, 243258, 938385', - }, - IO: { - regex: /^BBND 1ZZ$/, - samples: 'BBND 1ZZ', - }, - IQ: { - regex: /^\d{5}$/, - samples: '63282, 87817, 38580, 47725', - }, - IR: { - regex: /^\d{5}-?\d{5}$/, - samples: '0666174250, 6052682188, 02360-81920, 25102-08646', - }, - IS: { - regex: /^\d{3}$/, - samples: '408, 013, 001, 936', - }, - IT: { - regex: /^\d{5}$/, - samples: '31701, 61341, 92781, 45609', - }, - JE: { - regex: /^JE\d[\dA-Z]? ?\d[ABD-HJLN-UW-Z]{2}$/, - samples: 'JE0D 2EX, JE59 2OF, JE1X1ZW, JE0V 1SO', - }, - JM: {}, - JO: { - regex: /^\d{5}$/, - samples: '20789, 02128, 52170, 40284', - }, - JP: { - regex: /^\d{3}-?\d{4}$/, - samples: '5429642, 046-1544, 6463599, 368-5362', - }, - KE: { - regex: /^\d{5}$/, - samples: '33043, 98830, 59324, 42876', - }, - KG: { - regex: /^\d{6}$/, - samples: '500371, 176592, 184133, 225279', - }, - KH: { - regex: /^\d{5,6}$/, - samples: '220281, 18824, 35379, 09570', - }, - KI: { - regex: /^KI\d{4}$/, - samples: 'KI0104, KI0109, KI0112, KI0306', - }, - KM: {}, - KN: { - regex: /^KN\d{4}(-\d{4})?$/, - samples: 'KN2522, KN2560-3032, KN3507, KN4440', - }, - KP: {}, - KR: { - regex: /^\d{5}$/, - samples: '67417, 66648, 08359, 93750', - }, - KW: { - regex: /^\d{5}$/, - samples: '74840, 53309, 71276, 59262', - }, - KY: { - regex: /^KY\d-\d{4}$/, - samples: 'KY0-3078, KY1-7812, KY8-3729, KY3-4664', - }, - KZ: { - regex: /^\d{6}$/, - samples: '129113, 976562, 226811, 933781', - }, - LA: { - regex: /^\d{5}$/, - samples: '08875, 50779, 87756, 75932', - }, - LB: { - regex: /^(?:\d{4})(?: ?(?:\d{4}))?$/, - samples: '5436 1302, 9830 7470, 76911911, 9453 1306', - }, - LC: { - regex: /^(LC)?\d{2} ?\d{3}$/, - samples: '21080, LC99127, LC24 258, 51 740', - }, - LI: { - regex: /^\d{4}$/, - samples: '6644, 2852, 4630, 4541', - }, - LK: { - regex: /^\d{5}$/, - samples: '44605, 27721, 90695, 65514', - }, - LR: { - regex: /^\d{4}$/, - samples: '6644, 2852, 4630, 4541', - }, - LS: { - regex: /^\d{3}$/, - samples: '779, 803, 104, 897', - }, - LT: { - regex: /^((LT)[-])?(\d{5})$/, - samples: 'LT-22248, LT-12796, 69822, 37280', - }, - LU: { - regex: /^((L)[-])?(\d{4})$/, - samples: '5469, L-4476, 6304, 9739', - }, - LV: { - regex: /^((LV)[-])?\d{4}$/, - samples: '9344, LV-5030, LV-0132, 8097', - }, - LY: {}, - MA: { - regex: /^\d{5}$/, - samples: '50219, 95871, 80907, 79804', - }, - MC: { - regex: /^980\d{2}$/, - samples: '98084, 98041, 98070, 98062', - }, - MD: { - regex: /^(MD[-]?)?(\d{4})$/, - samples: '6250, MD-9681, MD3282, MD-0652', - }, - ME: { - regex: /^\d{5}$/, - samples: '87622, 92688, 23129, 59566', - }, - MF: { - regex: /^9[78][01]\d{2}$/, - samples: '97169, 98180, 98067, 98043', - }, - MG: { - regex: /^\d{3}$/, - samples: '854, 084, 524, 064', - }, - MH: { - regex: /^((969)[6-7][0-9])(-\d{4})?/, - samples: '96962, 96969, 96970-8530, 96960-3226', - }, - MK: { - regex: /^\d{4}$/, - samples: '8299, 6904, 6144, 9753', - }, - ML: {}, - MM: { - regex: /^\d{5}$/, - samples: '59188, 93943, 40829, 69981', - }, - MN: { - regex: /^\d{5}$/, - samples: '94129, 29906, 53374, 80141', - }, - MO: {}, - MP: { - regex: /^(9695[012])(?:[ -](\d{4}))?$/, - samples: '96952 3162, 96950 1567, 96951 2994, 96950 8745', - }, - MQ: { - regex: /^9[78]2\d{2}$/, - samples: '98297, 97273, 97261, 98282', - }, - MR: {}, - MS: { - regex: /^[Mm][Ss][Rr]\s{0,1}\d{4}$/, - samples: 'MSR1110, MSR1230, MSR1250, MSR1330', - }, - MT: { - regex: /^[A-Z]{3} [0-9]{4}|[A-Z]{2}[0-9]{2}|[A-Z]{2} [0-9]{2}|[A-Z]{3}[0-9]{4}|[A-Z]{3}[0-9]{2}|[A-Z]{3} [0-9]{2}$/, - samples: 'DKV 8196, KSU9264, QII0259, HKH 1020', - }, - MU: { - regex: /^([0-9A-R]\d{4})$/, - samples: 'H8310, 52591, M9826, F5810', - }, - MV: { - regex: /^\d{5}$/, - samples: '16354, 20857, 50991, 72527', - }, - MW: {}, - MX: { - regex: /^\d{5}$/, - samples: '71530, 76424, 73811, 50503', - }, - MY: { - regex: /^\d{5}$/, - samples: '75958, 15826, 86715, 37081', - }, - MZ: { - regex: /^\d{4}$/, - samples: '0902, 6258, 7826, 7150', - }, - NA: { - regex: /^\d{5}$/, - samples: '68338, 63392, 21820, 61211', - }, - NC: { - regex: /^988\d{2}$/, - samples: '98865, 98813, 98820, 98855', - }, - NE: { - regex: /^\d{4}$/, - samples: '9790, 3270, 2239, 0400', - }, - NF: { - regex: /^2899$/, - samples: '2899', - }, - NG: { - regex: /^\d{6}$/, - samples: '289096, 223817, 199970, 840648', - }, - NI: { - regex: /^\d{5}$/, - samples: '86308, 60956, 49945, 15470', - }, - NL: { - regex: /^\d{4} ?[A-Z]{2}$/, - samples: '6998 VY, 5390 CK, 2476 PS, 8873OX', - }, - NO: { - regex: /^\d{4}$/, - samples: '0711, 4104, 2683, 5015', - }, - NP: { - regex: /^\d{5}$/, - samples: '42438, 73964, 66400, 33976', - }, - NR: { - regex: /^(NRU68)$/, - samples: 'NRU68', - }, - NU: { - regex: /^(9974)$/, - samples: '9974', - }, - NZ: { - regex: /^\d{4}$/, - samples: '7015, 0780, 4109, 1422', - }, - OM: { - regex: /^(?:PC )?\d{3}$/, - samples: 'PC 851, PC 362, PC 598, PC 499', - }, - PA: { - regex: /^\d{4}$/, - samples: '0711, 4104, 2683, 5015', - }, - PE: { - regex: /^\d{5}$/, - samples: '10013, 12081, 14833, 24615', - }, - PF: { - regex: /^987\d{2}$/, - samples: '98755, 98710, 98748, 98791', - }, - PG: { - regex: /^\d{3}$/, - samples: '193, 166, 880, 553', - }, - PH: { - regex: /^\d{4}$/, - samples: '0137, 8216, 2876, 0876', - }, - PK: { - regex: /^\d{5}$/, - samples: '78219, 84497, 62102, 12564', - }, - PL: { - regex: /^\d{2}-\d{3}$/, - samples: '63-825, 26-714, 05-505, 15-200', - }, - PM: { - regex: /^(97500)$/, - samples: '97500', - }, - PN: { - regex: /^PCRN 1ZZ$/, - samples: 'PCRN 1ZZ', - }, - PR: { - regex: /^(00[679]\d{2})(?:[ -](\d{4}))?$/, - samples: '00989 3603, 00639 0720, 00707-9803, 00610 7362', - }, - PS: { - regex: /^(00[679]\d{2})(?:[ -](\d{4}))?$/, - samples: '00748, 00663, 00779-4433, 00934 1559', - }, - PT: { - regex: /^\d{4}-\d{3}$/, - samples: '0060-917, 4391-979, 5551-657, 9961-093', - }, - PW: { - regex: /^(969(?:39|40))(?:[ -](\d{4}))?$/, - samples: '96940, 96939, 96939 6004, 96940-1871', - }, - PY: { - regex: /^\d{4}$/, - samples: '7895, 5835, 8783, 5887', - }, - QA: {}, - RE: { - regex: /^9[78]4\d{2}$/, - samples: '98445, 97404, 98421, 98434', - }, - RO: { - regex: /^\d{6}$/, - samples: '935929, 407608, 637434, 174574', - }, - RS: { - regex: /^\d{5,6}$/, - samples: '929863, 259131, 687739, 07011', - }, - RU: { - regex: /^\d{6}$/, - samples: '138294, 617323, 307906, 981238', - }, - RW: {}, - SA: { - regex: /^\d{5}(-{1}\d{4})?$/, - samples: '86020-1256, 72375, 70280, 96328', - }, - SB: {}, - SC: {}, - SD: { - regex: /^\d{5}$/, - samples: '78219, 84497, 62102, 12564', - }, - SE: { - regex: /^\d{3} ?\d{2}$/, - samples: '095 39, 41052, 84687, 563 66', - }, - SG: { - regex: /^\d{6}$/, - samples: '606542, 233985, 036755, 265255', - }, - SH: { - regex: /^(?:ASCN|TDCU|STHL) 1ZZ$/, - samples: 'STHL 1ZZ, ASCN 1ZZ, TDCU 1ZZ', - }, - SI: { - regex: /^\d{4}$/, - samples: '6898, 3413, 2031, 5732', - }, - SJ: { - regex: /^\d{4}$/, - samples: '7616, 3163, 5769, 0237', - }, - SK: { - regex: /^\d{3} ?\d{2}$/, - samples: '594 52, 813 34, 867 67, 41814', - }, - SL: {}, - SM: { - regex: /^4789\d$/, - samples: '47894, 47895, 47893, 47899', - }, - SN: { - regex: /^[1-8]\d{4}$/, - samples: '48336, 23224, 33261, 82430', - }, - SO: {}, - SR: {}, - SS: { - regex: /^[A-Z]{2} ?\d{5}$/, - samples: 'JQ 80186, CU 46474, DE33738, MS 59107', - }, - ST: {}, - SV: {}, - SX: {}, - SY: {}, - SZ: { - regex: /^[HLMS]\d{3}$/, - samples: 'H458, L986, M477, S916', - }, - TA: { - regex: /^TDCU 1ZZ$/, - samples: 'TDCU 1ZZ', - }, - TC: { - regex: /^TKCA 1ZZ$/, - samples: 'TKCA 1ZZ', - }, - TD: {}, - TF: {}, - TG: {}, - TH: { - regex: /^\d{5}$/, - samples: '30706, 18695, 21044, 42496', - }, - TJ: { - regex: /^\d{6}$/, - samples: '381098, 961344, 519925, 667883', - }, - TK: {}, - TL: {}, - TM: { - regex: /^\d{6}$/, - samples: '544985, 164362, 425224, 374603', - }, - TN: { - regex: /^\d{4}$/, - samples: '6075, 7340, 2574, 8988', - }, - TO: {}, - TR: { - regex: /^\d{5}$/, - samples: '42524, 81057, 50859, 42677', - }, - TT: { - regex: /^\d{6}$/, - samples: '041238, 033990, 763476, 981118', - }, - TV: {}, - TW: { - regex: /^\d{3}(?:\d{2})?$/, - samples: '21577, 76068, 68698, 08912', - }, - TZ: {}, - UA: { - regex: /^\d{5}$/, - samples: '10629, 81138, 15668, 30055', - }, - UG: {}, - UM: {}, - US: { - regex: /^[0-9]{5}(?:[- ][0-9]{4})?$/, - samples: '12345, 12345-1234, 12345 1234', - }, - UY: { - regex: /^\d{5}$/, - samples: '40073, 30136, 06583, 00021', - }, - UZ: { - regex: /^\d{6}$/, - samples: '205122, 219713, 441699, 287471', - }, - VA: { - regex: /^(00120)$/, - samples: '00120', - }, - VC: { - regex: /^VC\d{4}$/, - samples: 'VC0600, VC0176, VC0616, VC4094', - }, - VE: { - regex: /^\d{4}$/, - samples: '9692, 1953, 6680, 8302', - }, - VG: { - regex: /^VG\d{4}$/, - samples: 'VG1204, VG7387, VG3431, VG6021', - }, - VI: { - regex: /^(008(?:(?:[0-4]\d)|(?:5[01])))(?:[ -](\d{4}))?$/, - samples: '00820, 00804 2036, 00825 3344, 00811-5900', - }, - VN: { - regex: /^\d{6}$/, - samples: '133836, 748243, 894060, 020597', - }, - VU: {}, - WF: { - regex: /^986\d{2}$/, - samples: '98692, 98697, 98698, 98671', - }, - WS: { - regex: /^WS[1-2]\d{3}$/, - samples: 'WS1349, WS2798, WS1751, WS2090', - }, - XK: { - regex: /^[1-7]\d{4}$/, - samples: '56509, 15863, 46644, 21896', - }, - YE: {}, - YT: { - regex: /^976\d{2}$/, - samples: '97698, 97697, 97632, 97609', - }, - ZA: { - regex: /^\d{4}$/, - samples: '6855, 5179, 6956, 7147', - }, - ZM: { - regex: /^\d{5}$/, - samples: '77603, 97367, 80454, 94484', - }, - ZW: {}, - }, - - GENERIC_ZIP_CODE_REGEX: /^(?:(?![\s-])[\w -]{0,9}[\w])?$/, - // Values for checking if polyfill is required on a platform POLYFILL_TEST: { STYLE: 'currency', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 3966b0b9e5a8..2c9b79f2fc8e 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -384,6 +384,7 @@ const DYNAMIC_ROUTES = { path: 'country', entryScreens: [ SCREENS.SETTINGS.PROFILE.ADDRESS, + SCREENS.SETTINGS.PROFILE.PRIVATE_PERSONAL_DETAILS, SCREENS.WORKSPACE.DYNAMIC_WORKSPACE_OVERVIEW_ADDRESS, SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS, SCREENS.DOMAIN_CARD.DOMAIN_CARD_UPDATE_ADDRESS, @@ -1197,6 +1198,11 @@ const ROUTES = { SETTINGS_DATE_OF_BIRTH: 'settings/profile/date-of-birth', SETTINGS_PHONE_NUMBER: 'settings/profile/phone', SETTINGS_ADDRESS: 'settings/profile/address', + SETTINGS_PRIVATE_PERSONAL_DETAILS: { + route: 'settings/profile/private-personal-details', + getRoute: (fieldToFocus?: string) => `settings/profile/private-personal-details${fieldToFocus ? `?fieldToFocus=${encodeURIComponent(fieldToFocus)}` : ''}` as const, + }, + SETTINGS_PRIVATE_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE: 'settings/profile/private-personal-details/confirm', SETTINGS_ADDRESS_STATE: { route: 'settings/profile/address/state', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 8eaebf73b0d1..e0efc25942ed 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -161,6 +161,8 @@ const SCREENS = { DATE_OF_BIRTH: 'Settings_DateOfBirth', PHONE_NUMBER: 'Settings_PhoneNumber', ADDRESS: 'Settings_Address', + PRIVATE_PERSONAL_DETAILS: 'Settings_PrivatePersonalDetails', + PRIVATE_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE: 'Settings_PrivatePersonalDetails_ConfirmMagicCode', AVATAR: 'Settings_Avatar', DYNAMIC_ADDRESS_COUNTRY: 'Dynamic_Address_Country', ADDRESS_STATE: 'Settings_Address_State', diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx index e354ecc1c4ba..e36d09770bbc 100644 --- a/src/components/AddressForm.tsx +++ b/src/components/AddressForm.tsx @@ -1,3 +1,4 @@ +import {CONST as COMMON_CONST} from 'expensify-common'; import React, {useCallback} from 'react'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; @@ -81,7 +82,7 @@ function AddressForm({ const styles = useThemeStyles(); const {translate} = useLocalize(); - const zipSampleFormat = (country && (CONST.COUNTRY_ZIP_REGEX_DATA[country] as CountryZipRegex)?.samples) ?? ''; + const zipSampleFormat = (country && (COMMON_CONST.COUNTRY_ZIP_REGEX_DATA[country] as CountryZipRegex)?.samples) ?? ''; const zipFormat = translate('common.zipCodeExampleFormat', zipSampleFormat); @@ -136,7 +137,7 @@ function AddressForm({ } // If no country is selected, default value is an empty string and there's no related regex data so we default to an empty object - const countryRegexDetails = (values.country ? CONST.COUNTRY_ZIP_REGEX_DATA?.[values.country] : {}) as CountryZipRegex; + const countryRegexDetails = (values.country ? COMMON_CONST.COUNTRY_ZIP_REGEX_DATA?.[values.country] : {}) as CountryZipRegex; // The postal code system might not exist for a country, so no regex either for them. const countrySpecificZipRegex = countryRegexDetails?.regex; @@ -150,7 +151,7 @@ function AddressForm({ errors.zipPostCode = translate('common.error.fieldRequired'); } } - } else if (!CONST.GENERIC_ZIP_CODE_REGEX.test(values?.zipPostCode?.trim()?.toUpperCase() ?? '')) { + } else if (!COMMON_CONST.GENERIC_ZIP_CODE_REGEX.test(values?.zipPostCode?.trim()?.toUpperCase() ?? '')) { errors.zipPostCode = translate('privatePersonalDetails.error.incorrectZipFormat'); } diff --git a/src/languages/de.ts b/src/languages/de.ts index 0616e464d99c..92f50bf135e1 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -3334,6 +3334,7 @@ ${amount} für ${merchant} – ${date}`, enterPhoneNumber: 'Wie lautet deine Telefonnummer?', personalDetails: 'Persönliche Angaben', privateDataMessage: 'Diese Angaben werden für Reisen und Zahlungen verwendet. Sie werden niemals in deinem öffentlichen Profil angezeigt.', + basicDetails: 'Grundlegende Angaben', legalName: 'Rechtlicher Name', legalFirstName: 'Rechtlicher Vorname', legalLastName: 'Rechtlicher Nachname', @@ -3528,7 +3529,7 @@ ${amount} für ${merchant} – ${date}`, noBankAccountSelected: 'Bitte wähle ein Konto aus', taxID: 'Bitte geben Sie eine gültige Steueridentifikationsnummer ein', website: 'Bitte eine gültige Website eingeben', - zipCode: `Bitte gib eine gültige Postleitzahl im folgenden Format ein: ${CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`, + zipCode: `Bitte gib eine gültige Postleitzahl im folgenden Format ein: ${COMMON_CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`, phoneNumber: 'Bitte gib eine gültige Telefonnummer ein', email: 'Bitte gib eine gültige E-Mail-Adresse ein', companyName: 'Bitte gib einen gültigen Unternehmensnamen ein', diff --git a/src/languages/en.ts b/src/languages/en.ts index 1fec6e9d162b..8d35fdd9f443 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3410,6 +3410,7 @@ const translations = { enterPhoneNumber: "What's your phone number?", personalDetails: 'Personal details', privateDataMessage: 'These details are used for travel and payments. They are never shown on your public profile.', + basicDetails: 'Basic details', legalName: 'Legal name', legalFirstName: 'Legal first name', legalLastName: 'Legal last name', @@ -3607,7 +3608,7 @@ const translations = { noBankAccountSelected: 'Please choose an account', taxID: 'Please enter a valid tax ID number', website: 'Please enter a valid website', - zipCode: `Please enter a valid ZIP code using the format: ${CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`, + zipCode: `Please enter a valid ZIP code using the format: ${COMMON_CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`, phoneNumber: 'Please enter a valid phone number', email: 'Please enter a valid email address', companyName: 'Please enter a valid business name', diff --git a/src/languages/es.ts b/src/languages/es.ts index aee215e75186..3a8d9ce406cf 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3214,6 +3214,7 @@ ${amount} para ${merchant} - ${date}`, enterPhoneNumber: '¿Cuál es tu número de teléfono?', personalDetails: 'Datos personales', privateDataMessage: 'Estos detalles se utilizan para viajes y pagos. Nunca se mostrarán en tu perfil público.', + basicDetails: 'Datos básicos', legalName: 'Nombre completo', legalFirstName: 'Nombre legal', legalLastName: 'Apellidos legales', @@ -3411,7 +3412,7 @@ ${amount} para ${merchant} - ${date}`, noBankAccountSelected: 'Por favor, elige una cuenta bancaria', taxID: 'Por favor, introduce un número de identificación fiscal válido', website: 'Por favor, introduce un sitio web válido', - zipCode: `Formato de código postal incorrecto. Formato aceptable: ${CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}.`, + zipCode: `Formato de código postal incorrecto. Formato aceptable: ${COMMON_CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}.`, phoneNumber: 'Por favor, introduce un teléfono válido', email: 'Por favor, introduce una dirección de correo electrónico válida', companyName: 'Por favor, introduce un nombre comercial legal válido', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 959efe8ed797..8235e1a4c807 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -3342,6 +3342,7 @@ ${amount} pour ${merchant} - ${date}`, enterPhoneNumber: 'Quel est votre numéro de téléphone ?', personalDetails: 'Informations personnelles', privateDataMessage: 'Ces informations sont utilisées pour les déplacements et les paiements. Elles ne s’affichent jamais sur votre profil public.', + basicDetails: 'Détails de base', legalName: 'Nom légal', legalFirstName: 'Prénom légal', legalLastName: 'Nom de famille légal', @@ -3539,7 +3540,7 @@ ${amount} pour ${merchant} - ${date}`, noBankAccountSelected: 'Veuillez choisir un compte', taxID: 'Veuillez saisir un numéro d’identification fiscale valide', website: 'Veuillez saisir un site web valide', - zipCode: `Veuillez saisir un code postal valide au format : ${CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`, + zipCode: `Veuillez saisir un code postal valide au format : ${COMMON_CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`, phoneNumber: 'Veuillez saisir un numéro de téléphone valide', email: 'Veuillez saisir une adresse e-mail valide', companyName: 'Veuillez saisir un nom d’entreprise valide', diff --git a/src/languages/it.ts b/src/languages/it.ts index daa5ea7e1e89..522f68ce7662 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -3327,6 +3327,7 @@ ${amount} per ${merchant} - ${date}`, enterPhoneNumber: 'Qual è il tuo numero di telefono?', personalDetails: 'Dati personali', privateDataMessage: 'Questi dettagli vengono utilizzati per viaggi e pagamenti. Non vengono mai mostrati sul tuo profilo pubblico.', + basicDetails: 'Dettagli di base', legalName: 'Nome legale', legalFirstName: 'Nome legale di battesimo', legalLastName: 'Cognome legale', @@ -3520,7 +3521,7 @@ ${amount} per ${merchant} - ${date}`, noBankAccountSelected: 'Scegli un account per favore', taxID: 'Inserisci un numero di codice fiscale valido', website: 'Inserisci un sito web valido', - zipCode: `Inserisci un CAP valido usando il formato: ${CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`, + zipCode: `Inserisci un CAP valido usando il formato: ${COMMON_CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`, phoneNumber: 'Inserisci un numero di telefono valido', email: 'Inserisci un indirizzo email valido', companyName: 'Inserisci un nome aziendale valido', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index f1b29066cf4c..dd0e65638d86 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -3300,6 +3300,7 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' enterPhoneNumber: '電話番号は何ですか?', personalDetails: '個人情報', privateDataMessage: 'これらの詳細は出張と支払いに使用されます。公開プロフィールに表示されることは決してありません。', + basicDetails: '基本情報', legalName: '法的氏名', legalFirstName: '法的な名', legalLastName: '法的な姓', @@ -3491,7 +3492,7 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' noBankAccountSelected: 'アカウントを選択してください', taxID: '有効な納税者番号を入力してください', website: '有効なウェブサイトを入力してください', - zipCode: `有効なZIPコードを、次の形式で入力してください: ${CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`, + zipCode: `有効なZIPコードを、次の形式で入力してください: ${COMMON_CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`, phoneNumber: '有効な電話番号を入力してください', email: '有効なメールアドレスを入力してください', companyName: '有効な会社名を入力してください', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index e71d9830c956..6cdd31b09d83 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -3323,6 +3323,7 @@ ${amount} voor ${merchant} - ${date}`, enterPhoneNumber: 'Wat is je telefoonnummer?', personalDetails: 'Persoonlijke gegevens', privateDataMessage: 'Deze gegevens worden gebruikt voor reizen en betalingen. Ze worden nooit weergegeven op je openbare profiel.', + basicDetails: 'Basisgegevens', legalName: 'Wettelijke naam', legalFirstName: 'Juridische voornaam', legalLastName: 'Wettelijke achternaam', @@ -3515,7 +3516,7 @@ ${amount} voor ${merchant} - ${date}`, noBankAccountSelected: 'Kies een account', taxID: 'Voer een geldig btw-identificatienummer in', website: 'Voer een geldige website in', - zipCode: `Voer een geldige postcode in met het volgende formaat: ${CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`, + zipCode: `Voer een geldige postcode in met het volgende formaat: ${COMMON_CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`, phoneNumber: 'Voer een geldig telefoonnummer in', email: 'Voer een geldig e-mailadres in', companyName: 'Voer een geldige bedrijfsnaam in', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 2b04a281147b..72296059032f 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -3316,6 +3316,7 @@ ${amount} dla ${merchant} - ${date}`, enterPhoneNumber: 'Jaki jest Twój numer telefonu?', personalDetails: 'Dane osobiste', privateDataMessage: 'Te dane są używane do podróży i płatności. Nigdy nie są wyświetlane w Twoim publicznym profilu.', + basicDetails: 'Podstawowe dane', legalName: 'Imię i nazwisko (pełne)', legalFirstName: 'Imię (zgodnie z dokumentem tożsamości)', legalLastName: 'Nazwisko zgodne z dokumentami', @@ -3507,7 +3508,7 @@ ${amount} dla ${merchant} - ${date}`, noBankAccountSelected: 'Wybierz konto', taxID: 'Wprowadź prawidłowy numer identyfikacji podatkowej', website: 'Wprowadź prawidłową stronę internetową', - zipCode: `Wprowadź prawidłowy kod ZIP w formacie: ${CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`, + zipCode: `Wprowadź prawidłowy kod ZIP w formacie: ${COMMON_CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`, phoneNumber: 'Wprowadź prawidłowy numer telefonu', email: 'Wpisz prawidłowy adres e‑mail', companyName: 'Wprowadź prawidłową nazwę firmy', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 424f9336262d..5d1f8424d53c 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -3317,6 +3317,7 @@ ${amount} para ${merchant} - ${date}`, enterPhoneNumber: 'Qual é o seu número de telefone?', personalDetails: 'Dados pessoais', privateDataMessage: 'Esses dados são usados para viagens e pagamentos. Eles nunca são exibidos no seu perfil público.', + basicDetails: 'Detalhes básicos', legalName: 'Nome legal', legalFirstName: 'Primeiro nome legal', legalLastName: 'Sobrenome legal', @@ -3508,7 +3509,7 @@ ${amount} para ${merchant} - ${date}`, noBankAccountSelected: 'Escolha uma conta', taxID: 'Insira um número de identificação fiscal válido', website: 'Insira um site válido', - zipCode: `Insira um CEP válido usando o formato: ${CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`, + zipCode: `Insira um CEP válido usando o formato: ${COMMON_CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`, phoneNumber: 'Insira um número de telefone válido', email: 'Insira um endereço de e-mail válido', companyName: 'Insira um nome comercial válido', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 14d7085fbea2..0c9a4e0084ee 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -3240,6 +3240,7 @@ ${amount},商户:${merchant} - 日期:${date}`, enterPhoneNumber: '你的电话号码是多少?', personalDetails: '个人信息', privateDataMessage: '这些详细信息将用于出行和付款,绝不会显示在你的公开个人资料中。', + basicDetails: '基本信息', legalName: '法定姓名', legalFirstName: '法定名(名)', legalLastName: '法定姓氏', @@ -3428,7 +3429,7 @@ ${amount},商户:${merchant} - 日期:${date}`, noBankAccountSelected: '请选择一个账户', taxID: '请输入有效的税号', website: '请输入有效的网站', - zipCode: `请输入有效的邮政编码,格式为:${CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`, + zipCode: `请输入有效的邮政编码,格式为:${COMMON_CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`, phoneNumber: '请输入有效的电话号码', email: '请输入有效的邮箱地址', companyName: '请输入有效的公司名称', diff --git a/src/libs/API/parameters/UpdatePrivatePersonalDetailsParams.ts b/src/libs/API/parameters/UpdatePrivatePersonalDetailsParams.ts new file mode 100644 index 000000000000..177a5dfacfc6 --- /dev/null +++ b/src/libs/API/parameters/UpdatePrivatePersonalDetailsParams.ts @@ -0,0 +1,16 @@ +type UpdatePrivatePersonalDetailsParams = { + legalFirstName: string; + legalLastName: string; + phoneNumber: string; + addressCity: string; + addressStreet: string; + addressStreet2: string; + addressZip: string; + addressCountry: string; + dob: string; + validateCode: string; + addressState: string; + addressProvince: string; +}; + +export default UpdatePrivatePersonalDetailsParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 51e87764cee1..d5c1d61fb395 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -111,6 +111,7 @@ export type {default as UpdateHomeAddressParams} from './UpdateHomeAddressParams export type {default as UpdatePolicyAddressParams} from './UpdatePolicyAddressParams'; export type {default as UpdateLegalNameParams} from './UpdateLegalNameParams'; export type {default as UpdatePhoneNumberParams} from './UpdatePhoneNumberParams'; +export type {default as UpdatePrivatePersonalDetailsParams} from './UpdatePrivatePersonalDetailsParams'; export type {default as UpdateNewsletterSubscriptionParams} from './UpdateNewsletterSubscriptionParams'; export type {default as UpdatePersonalInformationForBankAccountParams} from './UpdatePersonalInformationForBankAccountParams'; export type {default as UpdatePreferredEmojiSkinToneParams} from './UpdatePreferredEmojiSkinToneParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 539c5b285b6d..96c61776cba3 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -73,6 +73,7 @@ const WRITE_COMMANDS = { UPDATE_DATE_OF_BIRTH: 'UpdateDateOfBirth', UPDATE_PHONE_NUMBER: 'UpdatePhoneNumber', UPDATE_HOME_ADDRESS: 'UpdateHomeAddress', + UPDATE_PRIVATE_PERSONAL_DETAILS: 'UpdatePrivatePersonalDetails', UPDATE_POLICY_ADDRESS: 'SetPolicyAddress', UPDATE_AUTOMATIC_TIMEZONE: 'UpdateAutomaticTimezone', UPDATE_SELECTED_TIMEZONE: 'UpdateSelectedTimezone', @@ -673,6 +674,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_PHONE_NUMBER]: Parameters.UpdatePhoneNumberParams; [WRITE_COMMANDS.UPDATE_POLICY_ADDRESS]: Parameters.UpdatePolicyAddressParams; [WRITE_COMMANDS.UPDATE_HOME_ADDRESS]: Parameters.UpdateHomeAddressParams; + [WRITE_COMMANDS.UPDATE_PRIVATE_PERSONAL_DETAILS]: Parameters.UpdatePrivatePersonalDetailsParams; [WRITE_COMMANDS.UPDATE_AUTOMATIC_TIMEZONE]: Parameters.UpdateAutomaticTimezoneParams; [WRITE_COMMANDS.UPDATE_SELECTED_TIMEZONE]: Parameters.UpdateSelectedTimezoneParams; [WRITE_COMMANDS.UPDATE_USER_AVATAR]: Parameters.UpdateUserAvatarParams; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 9eebea79c35b..5241583c5d08 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -428,6 +428,12 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Profile/PersonalDetails/DateOfBirthPage').default), [SCREENS.SETTINGS.PROFILE.PHONE_NUMBER]: withAgentAccessDenied(() => require('../../../../pages/settings/Profile/PersonalDetails/PhoneNumberPage').default), [SCREENS.SETTINGS.PROFILE.ADDRESS]: withAgentAccessDenied(() => require('../../../../pages/settings/Profile/PersonalDetails/PersonalAddressPage').default), + [SCREENS.SETTINGS.PROFILE.PRIVATE_PERSONAL_DETAILS]: withAgentAccessDenied( + () => require('../../../../pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsPage').default, + ), + [SCREENS.SETTINGS.PROFILE.PRIVATE_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE]: withAgentAccessDenied( + () => require('../../../../pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsConfirmMagicCodePage').default, + ), [SCREENS.SETTINGS.PROFILE.DYNAMIC_ADDRESS_COUNTRY]: withAgentAccessDenied( () => require('../../../../pages/settings/Profile/PersonalDetails/DynamicCountrySelectionPage').default, ), diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts index 8ea3a17509a2..bea4205dbde0 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts @@ -23,6 +23,8 @@ const SETTINGS_TO_RHP: Partial['config'] = { path: ROUTES.SETTINGS_ADDRESS, exact: true, }, + [SCREENS.SETTINGS.PROFILE.PRIVATE_PERSONAL_DETAILS]: { + path: ROUTES.SETTINGS_PRIVATE_PERSONAL_DETAILS.route, + exact: true, + }, + [SCREENS.SETTINGS.PROFILE.PRIVATE_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE]: { + path: ROUTES.SETTINGS_PRIVATE_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE, + exact: true, + }, [SCREENS.SETTINGS.PROFILE.DYNAMIC_ADDRESS_COUNTRY]: DYNAMIC_ROUTES.ADDRESS_COUNTRY.path, [SCREENS.SETTINGS.PROFILE.ADDRESS_STATE]: { path: ROUTES.SETTINGS_ADDRESS_STATE.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 675cff9a5059..06d7338abed4 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -81,6 +81,10 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.PROFILE.TIMEZONE_SELECT]: undefined; [SCREENS.SETTINGS.PROFILE.LEGAL_NAME]: undefined; [SCREENS.SETTINGS.PROFILE.DATE_OF_BIRTH]: undefined; + [SCREENS.SETTINGS.PROFILE.PRIVATE_PERSONAL_DETAILS]: { + fieldToFocus?: string; + }; + [SCREENS.SETTINGS.PROFILE.PRIVATE_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE]: undefined; [SCREENS.SETTINGS.PROFILE.ADDRESS]: { country?: Country | ''; }; diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 4a6115b84bdc..91bc328f7fbd 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -4,6 +4,8 @@ import Onyx from 'react-native-onyx'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {PersonalDetailsForm} from '@src/types/form'; +import INPUT_IDS from '@src/types/form/PersonalDetailsForm'; import type {OnyxInputOrEntry, PersonalDetails, PersonalDetailsList, PrivatePersonalDetails} from '@src/types/onyx'; import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; import type {OnyxData} from '@src/types/onyx/Request'; @@ -318,6 +320,27 @@ function getCurrentAddress(privatePersonalDetails: OnyxEntry, draftValues?: PersonalDetailsForm | null): PersonalDetailsForm { + const address = getCurrentAddress(privatePersonalDetails); + const [street1, street2] = getStreetLines(address?.street); + return { + [INPUT_IDS.LEGAL_FIRST_NAME]: draftValues?.[INPUT_IDS.LEGAL_FIRST_NAME] ?? privatePersonalDetails?.legalFirstName ?? '', + [INPUT_IDS.LEGAL_LAST_NAME]: draftValues?.[INPUT_IDS.LEGAL_LAST_NAME] ?? privatePersonalDetails?.legalLastName ?? '', + [INPUT_IDS.DATE_OF_BIRTH]: draftValues?.[INPUT_IDS.DATE_OF_BIRTH] ?? privatePersonalDetails?.dob ?? '', + [INPUT_IDS.PHONE_NUMBER]: draftValues?.[INPUT_IDS.PHONE_NUMBER] ?? privatePersonalDetails?.phoneNumber ?? '', + [INPUT_IDS.ADDRESS_LINE_1]: draftValues?.[INPUT_IDS.ADDRESS_LINE_1] ?? street1 ?? '', + [INPUT_IDS.ADDRESS_LINE_2]: draftValues?.[INPUT_IDS.ADDRESS_LINE_2] ?? street2 ?? '', + [INPUT_IDS.CITY]: draftValues?.[INPUT_IDS.CITY] ?? address?.city ?? '', + [INPUT_IDS.STATE]: draftValues?.[INPUT_IDS.STATE] ?? address?.state ?? '', + [INPUT_IDS.ZIP_POST_CODE]: draftValues?.[INPUT_IDS.ZIP_POST_CODE] ?? address?.zip ?? '', + [INPUT_IDS.COUNTRY]: draftValues?.[INPUT_IDS.COUNTRY] ?? address?.country ?? '', + }; +} + /** * Formats an address object into an easily readable string * @@ -487,6 +510,7 @@ export { getCurrentAddress, getFormattedAddress, getFormattedStreet, + getPrivatePersonalDetailsFormValues, getStreetLines, getEffectiveDisplayName, createDisplayName, diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index b5b721145641..ff2c09fa89d8 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -12,6 +12,7 @@ import type { UpdateHomeAddressParams, UpdateLegalNameParams, UpdatePhoneNumberParams, + UpdatePrivatePersonalDetailsParams, UpdatePronounsParams, UpdateSelectedTimezoneParams, UpdateUserAvatarParams, @@ -511,6 +512,57 @@ function deleteAvatar(currentUserPersonalDetails: Pick, validateCode: string, countryCode: number) { + const stateValue = (values.state ?? '').trim(); + const parameters: UpdatePrivatePersonalDetailsParams = { + 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: stateValue, + addressProvince: values.country !== CONST.COUNTRY.US ? stateValue : '', + dob: values.dob ?? '', + validateCode, + }; + + API.write(WRITE_COMMANDS.UPDATE_PRIVATE_PERSONAL_DETAILS, parameters, { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + value: { + isLoading: true, + errorFields: {personalDetails: null}, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + value: { + isLoading: false, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + value: { + isLoading: false, + errorFields: {personalDetails: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}, + }, + }, + ], }); } @@ -556,6 +608,7 @@ export { clearPhoneNumberError, updatePronouns, updateSelectedTimezone, + updatePrivatePersonalDetails, updatePersonalDetailsAndShipExpensifyCards, clearPersonalDetailsErrors, buildSetPersonalDetailsAndShipExpensifyCardsParams, diff --git a/src/pages/MissingPersonalDetails/subPages/Address.tsx b/src/pages/MissingPersonalDetails/subPages/Address.tsx index c222f4850405..ff69362c422e 100644 --- a/src/pages/MissingPersonalDetails/subPages/Address.tsx +++ b/src/pages/MissingPersonalDetails/subPages/Address.tsx @@ -1,3 +1,4 @@ +import {CONST as COMMON_CONST} from 'expensify-common'; import React, {useCallback, useRef, useState} from 'react'; import {View} from 'react-native'; import AddressSearch from '@components/AddressSearch'; @@ -77,7 +78,7 @@ function AddressStep({isEditing, onNext, personalDetailsValues}: CustomSubPagePr } // If no country is selected, default value is an empty string and there's no related regex data so we default to an empty object - const countryRegexDetails = (values.country ? CONST.COUNTRY_ZIP_REGEX_DATA?.[values.country] : {}) as CountryZipRegex; + const countryRegexDetails = (values.country ? COMMON_CONST.COUNTRY_ZIP_REGEX_DATA?.[values.country] : {}) as CountryZipRegex; // The postal code system might not exist for a country, so no regex either for them. const countrySpecificZipRegex = countryRegexDetails?.regex; @@ -90,7 +91,7 @@ function AddressStep({isEditing, onNext, personalDetailsValues}: CustomSubPagePr errors[INPUT_IDS.ZIP_POST_CODE] = translate('common.error.fieldRequired'); } } - } else if (!CONST.GENERIC_ZIP_CODE_REGEX.test(values[INPUT_IDS.ZIP_POST_CODE]?.trim()?.toUpperCase() ?? '')) { + } else if (!COMMON_CONST.GENERIC_ZIP_CODE_REGEX.test(values[INPUT_IDS.ZIP_POST_CODE]?.trim()?.toUpperCase() ?? '')) { errors[INPUT_IDS.ZIP_POST_CODE] = translate('privatePersonalDetails.error.incorrectZipFormat'); } return errors; @@ -128,7 +129,7 @@ function AddressStep({isEditing, onNext, personalDetailsValues}: CustomSubPagePr const isUSAForm = currentCountry === CONST.COUNTRY.US; - const zipSampleFormat = (currentCountry && (CONST.COUNTRY_ZIP_REGEX_DATA[currentCountry] as CountryZipRegex)?.samples) ?? ''; + const zipSampleFormat = (currentCountry && (COMMON_CONST.COUNTRY_ZIP_REGEX_DATA[currentCountry] as CountryZipRegex)?.samples) ?? ''; const zipFormat = translate('common.zipCodeExampleFormat', zipSampleFormat); diff --git a/src/pages/ReimbursementAccount/AddressFormFields.tsx b/src/pages/ReimbursementAccount/AddressFormFields.tsx index c92cb2acd0a5..f4d807819922 100644 --- a/src/pages/ReimbursementAccount/AddressFormFields.tsx +++ b/src/pages/ReimbursementAccount/AddressFormFields.tsx @@ -190,7 +190,7 @@ function AddressFormFields({ value={values?.zipCode} defaultValue={defaultValues?.zipCode} errorText={errors?.zipCode ? translate('bankAccount.error.zipCode') : ''} - hint={translate('common.zipCodeExampleFormat', CONST.COUNTRY_ZIP_REGEX_DATA.US.samples)} + hint={translate('common.zipCodeExampleFormat', COMMON_CONST.COUNTRY_ZIP_REGEX_DATA.US.samples)} containerStyles={styles.mt3} forwardedFSClass={forwardedFSClass} autoComplete="postal-code" diff --git a/src/pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsConfirmMagicCodePage.tsx b/src/pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsConfirmMagicCodePage.tsx new file mode 100644 index 000000000000..bac67536154e --- /dev/null +++ b/src/pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsConfirmMagicCodePage.tsx @@ -0,0 +1,81 @@ +import React, {useEffect, useRef} from 'react'; +import ValidateCodeActionContent from '@components/ValidateCodeActionModal/ValidateCodeActionContent'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import usePrimaryContactMethod from '@hooks/usePrimaryContactMethod'; +import {clearPersonalDetailsErrors, updatePrivatePersonalDetails} from '@libs/actions/PersonalDetails'; +import {requestValidateCodeAction, resetValidateActionCodeSent} from '@libs/actions/User'; +import {normalizeCountryCode} from '@libs/CountryUtils'; +import {getLatestErrorField, getLatestErrorMessageField} from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import {getPrivatePersonalDetailsFormValues} from '@libs/PersonalDetailsUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {PersonalDetailsForm} from '@src/types/form'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +function PrivatePersonalDetailsConfirmMagicCodePage() { + const {translate} = useLocalize(); + const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); + const [draftValues] = useOnyx(ONYXKEYS.FORMS.PERSONAL_DETAILS_FORM_DRAFT); + const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE); + const primaryLogin = usePrimaryContactMethod(); + + const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE); + + // Errors may be written to either errorFields (preferred) or errors depending on + // how the backend reports the failure, so check both. + const personalDetailsErrorField = getLatestErrorField(privatePersonalDetails, 'personalDetails'); + const personalDetailsError = getLatestErrorMessageField(privatePersonalDetails); + const submitError = !isEmptyObject(personalDetailsErrorField) ? personalDetailsErrorField : personalDetailsError; + const hasErrors = !isEmptyObject(submitError) || !isEmptyObject(validateCodeAction?.errorFields); + + const clearError = () => { + if (!hasErrors) { + return; + } + clearPersonalDetailsErrors(); + }; + + const wasLoading = useRef(false); + useEffect(() => { + if (privatePersonalDetails?.isLoading) { + wasLoading.current = true; + return; + } + if (wasLoading.current && !hasErrors) { + wasLoading.current = false; + resetValidateActionCodeSent(); + Navigation.goBack(ROUTES.SETTINGS_PROFILE.route); + } + wasLoading.current = false; + }, [privatePersonalDetails?.isLoading, hasErrors]); + + const values = normalizeCountryCode(getPrivatePersonalDetailsFormValues(privatePersonalDetails, draftValues)) as PersonalDetailsForm; + + const handleSubmitForm = (validateCode: string) => { + updatePrivatePersonalDetails(values, validateCode, countryCode); + }; + + return ( + requestValidateCodeAction()} + validateCodeActionErrorField="personalDetails" + handleSubmitForm={handleSubmitForm} + validateError={submitError} + clearError={clearError} + onClose={() => { + resetValidateActionCodeSent(); + Navigation.goBack(ROUTES.SETTINGS_PRIVATE_PERSONAL_DETAILS.route); + }} + isLoading={privatePersonalDetails?.isLoading} + /> + ); +} + +PrivatePersonalDetailsConfirmMagicCodePage.displayName = 'PrivatePersonalDetailsConfirmMagicCodePage'; + +export default PrivatePersonalDetailsConfirmMagicCodePage; diff --git a/src/pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsPage.tsx b/src/pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsPage.tsx new file mode 100644 index 000000000000..19889cf5664b --- /dev/null +++ b/src/pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsPage.tsx @@ -0,0 +1,414 @@ +import {useRoute} from '@react-navigation/native'; +import {subYears} from 'date-fns'; +import {CONST as COMMON_CONST} from 'expensify-common'; +import React, {useEffect, useState} from 'react'; +import {View} from 'react-native'; +import AddressSearch from '@components/AddressSearch'; +import CountrySelector from '@components/CountrySelector'; +import DatePicker from '@components/DatePicker'; +import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import type {State} from '@components/StateSelector'; +import StateSelector from '@components/StateSelector'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {clearDraftValues, setDraftValues} from '@libs/actions/FormActions'; +import {normalizeCountryCode} from '@libs/CountryUtils'; +import {appendCountryCode} from '@libs/LoginUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import {getCurrentAddress, getStreetLines} from '@libs/PersonalDetailsUtils'; +import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; +import {doesContainReservedWord, getAgeRequirementError, isRequiredFulfilled, isValidDisplayName, isValidPhoneNumber} from '@libs/ValidationUtils'; +import CONST from '@src/CONST'; +import type {Country} from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/PersonalDetailsForm'; +import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; + +// StateSelector keys on 2-letter codes, but stored addresses may use full state names (e.g. "California"). +function resolveStateCode(stateValue: string): string { + if (!stateValue || stateValue in COMMON_CONST.STATES) { + return stateValue; + } + const match = Object.entries(COMMON_CONST.STATES).find(([, v]) => v.stateName.toLowerCase() === stateValue.toLowerCase()); + return match ? match[0] : stateValue; +} + +function PrivatePersonalDetailsPage() { + const route = useRoute>(); + const fieldToFocus = route.params?.fieldToFocus; + const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); + const [isLoadingApp = true] = useOnyx(ONYXKEYS.IS_LOADING_APP); + const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE); + const [defaultCountry] = useOnyx(ONYXKEYS.COUNTRY); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const legalFirstName = privatePersonalDetails?.legalFirstName ?? ''; + const legalLastName = privatePersonalDetails?.legalLastName ?? ''; + const phoneNumber = privatePersonalDetails?.phoneNumber ?? ''; + const dob = privatePersonalDetails?.dob ?? ''; + const address = normalizeCountryCode(getCurrentAddress(privatePersonalDetails)) as Address | undefined; + const [street1, street2Fallback] = getStreetLines(address?.street); + const initialStreet1 = street1 ?? ''; + const initialStreet2 = address?.street2 ?? street2Fallback ?? ''; + const city = address?.city ?? ''; + const state = address?.state ?? ''; + const zip = address?.zip ?? ''; + const country = address?.country ?? ''; + + const normalizedState = resolveStateCode(state); + const [selectedCountry, setSelectedCountry] = useState(country || ((defaultCountry as Country | undefined) ?? '')); + const [selectedState, setSelectedState] = useState(normalizedState); + + useEffect( + () => () => { + clearDraftValues(ONYXKEYS.FORMS.PERSONAL_DETAILS_FORM); + }, + [], + ); + + const validate = (values: FormOnyxValues): FormInputErrors => { + const errors: FormInputErrors = {}; + + const firstNameValue = values[INPUT_IDS.LEGAL_FIRST_NAME] ?? ''; + if (!firstNameValue.trim()) { + errors[INPUT_IDS.LEGAL_FIRST_NAME] = translate('common.error.fieldRequired'); + } else if (!isValidDisplayName(firstNameValue)) { + errors[INPUT_IDS.LEGAL_FIRST_NAME] = translate('privatePersonalDetails.error.cannotIncludeCommaOrSemicolon'); + } else if (firstNameValue.length > CONST.LEGAL_NAME.MAX_LENGTH) { + errors[INPUT_IDS.LEGAL_FIRST_NAME] = translate('common.error.characterLimitExceedCounter', firstNameValue.length, CONST.LEGAL_NAME.MAX_LENGTH); + } else if (doesContainReservedWord(firstNameValue, CONST.DISPLAY_NAME.RESERVED_NAMES)) { + errors[INPUT_IDS.LEGAL_FIRST_NAME] = translate('personalDetails.error.containsReservedWord'); + } + + const lastNameValue = values[INPUT_IDS.LEGAL_LAST_NAME] ?? ''; + if (!lastNameValue.trim()) { + errors[INPUT_IDS.LEGAL_LAST_NAME] = translate('common.error.fieldRequired'); + } else if (!isValidDisplayName(lastNameValue)) { + errors[INPUT_IDS.LEGAL_LAST_NAME] = translate('privatePersonalDetails.error.cannotIncludeCommaOrSemicolon'); + } else if (lastNameValue.length > CONST.LEGAL_NAME.MAX_LENGTH) { + errors[INPUT_IDS.LEGAL_LAST_NAME] = translate('common.error.characterLimitExceedCounter', lastNameValue.length, CONST.LEGAL_NAME.MAX_LENGTH); + } else if (doesContainReservedWord(lastNameValue, CONST.DISPLAY_NAME.RESERVED_NAMES)) { + errors[INPUT_IDS.LEGAL_LAST_NAME] = translate('personalDetails.error.containsReservedWord'); + } + + const dobValue = values[INPUT_IDS.DATE_OF_BIRTH] ?? ''; + if (!dobValue) { + errors[INPUT_IDS.DATE_OF_BIRTH] = translate('common.error.fieldRequired'); + } else { + const dateError = getAgeRequirementError(translate, dobValue, CONST.DATE_BIRTH.MIN_AGE_FOR_PAYMENT, CONST.DATE_BIRTH.MAX_AGE); + if (dateError) { + errors[INPUT_IDS.DATE_OF_BIRTH] = dateError; + } + } + + const phoneValue = values[INPUT_IDS.PHONE_NUMBER] ?? ''; + if (!isRequiredFulfilled(phoneValue)) { + errors[INPUT_IDS.PHONE_NUMBER] = translate('common.error.fieldRequired'); + } else { + const phoneWithCountryCode = appendCountryCode(phoneValue, countryCode); + if (!isValidPhoneNumber(phoneWithCountryCode)) { + errors[INPUT_IDS.PHONE_NUMBER] = translate('common.error.phoneNumber'); + } + } + + const streetValue = values[INPUT_IDS.ADDRESS_LINE_1] ?? ''; + if (!streetValue.trim()) { + errors[INPUT_IDS.ADDRESS_LINE_1] = translate('common.error.fieldRequired'); + } + + const cityValue = values[INPUT_IDS.CITY] ?? ''; + if (!cityValue.trim()) { + errors[INPUT_IDS.CITY] = translate('common.error.fieldRequired'); + } + + const stateValue = values[INPUT_IDS.STATE] || selectedState || ''; + const effectiveCountry = (values[INPUT_IDS.COUNTRY] || selectedCountry) ?? ''; + if (!stateValue.trim()) { + errors[INPUT_IDS.STATE] = translate('common.error.fieldRequired'); + } + + const zipValue = values[INPUT_IDS.ZIP_POST_CODE] ?? ''; + const countryRegexDetails = effectiveCountry ? (COMMON_CONST.COUNTRY_ZIP_REGEX_DATA?.[effectiveCountry] as {regex?: RegExp; samples?: string}) : undefined; + const countrySpecificZipRegex = countryRegexDetails?.regex; + if (countrySpecificZipRegex) { + if (!countrySpecificZipRegex.test(zipValue.trim().toUpperCase())) { + if (isRequiredFulfilled(zipValue.trim())) { + errors[INPUT_IDS.ZIP_POST_CODE] = translate('privatePersonalDetails.error.incorrectZipFormat', countryRegexDetails?.samples ?? ''); + } else { + errors[INPUT_IDS.ZIP_POST_CODE] = translate('common.error.fieldRequired'); + } + } + } else if (!COMMON_CONST.GENERIC_ZIP_CODE_REGEX.test(zipValue.trim().toUpperCase())) { + errors[INPUT_IDS.ZIP_POST_CODE] = translate('privatePersonalDetails.error.incorrectZipFormat'); + } + + if (!effectiveCountry) { + errors[INPUT_IDS.COUNTRY] = translate('common.error.fieldRequired'); + } + + return errors; + }; + + const hasChanges = (values: FormOnyxValues): boolean => { + if ((values[INPUT_IDS.LEGAL_FIRST_NAME] ?? '') !== legalFirstName) { + return true; + } + if ((values[INPUT_IDS.LEGAL_LAST_NAME] ?? '') !== legalLastName) { + return true; + } + if ((values[INPUT_IDS.DATE_OF_BIRTH] ?? '') !== dob) { + return true; + } + if ((values[INPUT_IDS.PHONE_NUMBER] ?? '') !== phoneNumber) { + return true; + } + if ((values[INPUT_IDS.ADDRESS_LINE_1] ?? '') !== initialStreet1) { + return true; + } + if ((values[INPUT_IDS.ADDRESS_LINE_2] ?? '') !== initialStreet2) { + return true; + } + if ((values[INPUT_IDS.CITY] ?? '') !== city) { + return true; + } + if ((values[INPUT_IDS.STATE] ?? '') !== normalizedState) { + return true; + } + if ((values[INPUT_IDS.ZIP_POST_CODE] ?? '') !== zip) { + return true; + } + if ((values[INPUT_IDS.COUNTRY] ?? '') !== country) { + return true; + } + return false; + }; + + const onSubmit = (values: FormOnyxValues) => { + if (!hasChanges(values)) { + Navigation.goBack(); + return; + } + // UI-prefilled values (geolocation country, normalized state) only live in component state until the user touches + // a field, so write the full form values to the draft before navigating so the confirm step submits them. + setDraftValues(ONYXKEYS.FORMS.PERSONAL_DETAILS_FORM, values); + Navigation.navigate(ROUTES.SETTINGS_PRIVATE_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE); + }; + + const reasonAttributes: SkeletonSpanReasonAttributes = {context: 'PrivatePersonalDetailsPage', isLoadingApp}; + + if (isLoadingApp) { + return ( + + ); + } + + return ( + + + Navigation.goBack()} + /> + + {translate('privatePersonalDetails.basicDetails')} + + + + + + + + + + + + + {translate('privatePersonalDetails.address')} + + { + if (key === INPUT_IDS.COUNTRY) { + setSelectedCountry((value ?? '') as Country | ''); + } else if (key === INPUT_IDS.STATE) { + setSelectedState((value ?? '') as string); + } + }} + /> + + + + + + + + {selectedCountry === CONST.COUNTRY.US ? ( + + setSelectedState((value ?? '') as string)} + shouldSaveDraft + /> + + ) : ( + + + + )} + + + + + { + const newCountry = (value ?? '') as Country | ''; + setSelectedCountry(newCountry); + if (newCountry === CONST.COUNTRY.US) { + const resolved = resolveStateCode(selectedState); + if (resolved !== selectedState) { + setSelectedState(resolved); + } + } + }} + shouldSaveDraft + /> + + + + + ); +} + +PrivatePersonalDetailsPage.displayName = 'PrivatePersonalDetailsPage'; + +export default PrivatePersonalDetailsPage; diff --git a/src/pages/settings/Profile/ProfilePage.tsx b/src/pages/settings/Profile/ProfilePage.tsx index 7c3f8e7cf52c..57e78733fd8f 100755 --- a/src/pages/settings/Profile/ProfilePage.tsx +++ b/src/pages/settings/Profile/ProfilePage.tsx @@ -41,6 +41,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/PersonalDetailsForm'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; function ProfilePage() { @@ -132,55 +133,39 @@ function ProfilePage() { : []), ]; + const navigateToPrivateDetails = (fieldToFocus?: string) => { + if (isActingAsDelegate) { + showDelegateNoAccessModal(); + return; + } + Navigation.navigate(ROUTES.SETTINGS_PRIVATE_PERSONAL_DETAILS.getRoute(fieldToFocus)); + }; + const privateOptions = [ { description: translate('privatePersonalDetails.legalName'), title: legalName, sentryLabel: CONST.SENTRY_LABEL.SETTINGS_PROFILE.LEGAL_NAME, - action: () => { - if (isActingAsDelegate) { - showDelegateNoAccessModal(); - return; - } - Navigation.navigate(ROUTES.SETTINGS_LEGAL_NAME); - }, + action: () => navigateToPrivateDetails(INPUT_IDS.LEGAL_FIRST_NAME), }, { description: translate('common.dob'), title: privateDetails.dob ?? '', sentryLabel: CONST.SENTRY_LABEL.SETTINGS_PROFILE.DATE_OF_BIRTH, - action: () => { - if (isActingAsDelegate) { - showDelegateNoAccessModal(); - return; - } - Navigation.navigate(ROUTES.SETTINGS_DATE_OF_BIRTH); - }, + action: () => navigateToPrivateDetails(INPUT_IDS.DATE_OF_BIRTH), }, { description: translate('common.phoneNumber'), title: privateDetails.phoneNumber ?? '', sentryLabel: CONST.SENTRY_LABEL.SETTINGS_PROFILE.PHONE_NUMBER, - action: () => { - if (isActingAsDelegate) { - showDelegateNoAccessModal(); - return; - } - Navigation.navigate(ROUTES.SETTINGS_PHONE_NUMBER); - }, + action: () => navigateToPrivateDetails(INPUT_IDS.PHONE_NUMBER), brickRoadIndicator: privatePersonalDetails?.errorFields?.phoneNumber ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }, { description: translate('privatePersonalDetails.address'), title: getFormattedAddress(privateDetails), sentryLabel: CONST.SENTRY_LABEL.SETTINGS_PROFILE.ADDRESS, - action: () => { - if (isActingAsDelegate) { - showDelegateNoAccessModal(); - return; - } - Navigation.navigate(ROUTES.SETTINGS_ADDRESS); - }, + action: () => navigateToPrivateDetails(INPUT_IDS.ADDRESS_LINE_1), }, ]; From 7a57604f84ee08b0ea63ced60bf50758b6f244ea Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Mon, 25 May 2026 13:24:29 -0700 Subject: [PATCH 2/3] Fix blockers #91602/#91604/#91608/#91611 in private personal details - Preserve form draft across the navigation to the magic-code RHP. The prior cleanup cleared the draft on every unmount, so navigating to the magic-code screen wiped the payload before submission. For new users with no stored address that resulted in Auth returning an error on UpdatePrivatePersonalDetails (#91602). It also caused the country to fall back to the geolocation default when the user backed out of the magic-code page and remounted the form (#91608). Now the draft is cleared only when the form unmounts without navigating to the magic code screen, and the magic-code success effect clears the draft after the API write completes. - Seed selectedCountry / selectedState from the form draft (with the stored address and geolocation as further fallbacks) so a back-from- magic-code remount restores the user's choices instead of reverting to geolocation (#91608). - Pop the magic-code RHP with a plain Navigation.goBack() instead of goBack(ROUTES.SETTINGS_PRIVATE_PERSONAL_DETAILS). The route-based back path compares params, which never matched the existing PrivatePersonalDetails route (it carried a fieldToFocus param) and fell through to REPLACE, leaving a duplicate PrivatePersonalDetails on the RHP stack so the next back-press only popped the duplicate (#91611). - AddressSearch now accepts an autoFocus prop and forwards it to the underlying TextInput, so opening PrivatePersonalDetailsPage with ?fieldToFocus=addressLine1 actually focuses Address line 1 (#91604). --- src/components/AddressSearch/index.tsx | 2 ++ src/components/AddressSearch/types.ts | 3 +++ ...atePersonalDetailsConfirmMagicCodePage.tsx | 8 ++++++- .../PrivatePersonalDetailsPage.tsx | 21 ++++++++++++++++--- 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index e07fae3e7e3f..4f497876b99a 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -113,6 +113,7 @@ function AddressSearch({ lng: 'addressLng', }, autoComplete = 'off', + autoFocus = false, resultTypes = 'address', shouldSaveDraft = false, value, @@ -469,6 +470,7 @@ function AddressSearch({ onBlur?.(); }, autoComplete, + autoFocus, onInputChange: (text: string) => { setSearchValue(text); setIsTyping(true); diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts index 45bc264d7b00..684bcfff0329 100644 --- a/src/components/AddressSearch/types.ts +++ b/src/components/AddressSearch/types.ts @@ -80,6 +80,9 @@ type AddressSearchProps = ForwardedFSClassProps & { /** Auto complete attribute for the input field */ autoComplete?: TextInputProps['autoComplete']; + /** Whether the input should request focus on mount */ + autoFocus?: boolean; + /** Maximum number of characters allowed in search input */ maxInputLength?: number; diff --git a/src/pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsConfirmMagicCodePage.tsx b/src/pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsConfirmMagicCodePage.tsx index bac67536154e..0c7c1c5406aa 100644 --- a/src/pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsConfirmMagicCodePage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsConfirmMagicCodePage.tsx @@ -3,6 +3,7 @@ import ValidateCodeActionContent from '@components/ValidateCodeActionModal/Valid import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePrimaryContactMethod from '@hooks/usePrimaryContactMethod'; +import {clearDraftValues} from '@libs/actions/FormActions'; import {clearPersonalDetailsErrors, updatePrivatePersonalDetails} from '@libs/actions/PersonalDetails'; import {requestValidateCodeAction, resetValidateActionCodeSent} from '@libs/actions/User'; import {normalizeCountryCode} from '@libs/CountryUtils'; @@ -47,6 +48,7 @@ function PrivatePersonalDetailsConfirmMagicCodePage() { if (wasLoading.current && !hasErrors) { wasLoading.current = false; resetValidateActionCodeSent(); + clearDraftValues(ONYXKEYS.FORMS.PERSONAL_DETAILS_FORM); Navigation.goBack(ROUTES.SETTINGS_PROFILE.route); } wasLoading.current = false; @@ -69,7 +71,11 @@ function PrivatePersonalDetailsConfirmMagicCodePage() { clearError={clearError} onClose={() => { resetValidateActionCodeSent(); - Navigation.goBack(ROUTES.SETTINGS_PRIVATE_PERSONAL_DETAILS.route); + // Plain goBack pops the magic-code RHP screen. Passing the SETTINGS_PRIVATE_PERSONAL_DETAILS route + // here would compare params against the existing PrivatePersonalDetails route (which carries a + // fieldToFocus param), miss, and REPLACE — leaving a duplicate PrivatePersonalDetails on the stack + // so the next back-press only pops the duplicate. + Navigation.goBack(); }} isLoading={privatePersonalDetails?.isLoading} /> diff --git a/src/pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsPage.tsx b/src/pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsPage.tsx index 19889cf5664b..332ae906f63b 100644 --- a/src/pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsPage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsPage.tsx @@ -1,7 +1,7 @@ import {useRoute} from '@react-navigation/native'; import {subYears} from 'date-fns'; import {CONST as COMMON_CONST} from 'expensify-common'; -import React, {useEffect, useState} from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import AddressSearch from '@components/AddressSearch'; import CountrySelector from '@components/CountrySelector'; @@ -50,6 +50,7 @@ function PrivatePersonalDetailsPage() { const route = useRoute>(); const fieldToFocus = route.params?.fieldToFocus; const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); + const [draftValues] = useOnyx(ONYXKEYS.FORMS.PERSONAL_DETAILS_FORM_DRAFT); const [isLoadingApp = true] = useOnyx(ONYXKEYS.IS_LOADING_APP); const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE); const [defaultCountry] = useOnyx(ONYXKEYS.COUNTRY); @@ -70,11 +71,23 @@ function PrivatePersonalDetailsPage() { const country = address?.country ?? ''; const normalizedState = resolveStateCode(state); - const [selectedCountry, setSelectedCountry] = useState(country || ((defaultCountry as Country | undefined) ?? '')); - const [selectedState, setSelectedState] = useState(normalizedState); + // Draft values seed local state on remount (e.g. returning from the magic-code page) so the address-search + // suggestion's country/state choices survive a back-navigation instead of reverting to geolocation defaults. + const draftCountry = draftValues?.[INPUT_IDS.COUNTRY] ?? ''; + const draftState = draftValues?.[INPUT_IDS.STATE] ?? ''; + const [selectedCountry, setSelectedCountry] = useState(draftCountry || country || ((defaultCountry as Country | undefined) ?? '')); + const [selectedState, setSelectedState] = useState(draftState || normalizedState); + + // The draft is what feeds the confirm-magic-code page; clearing it on every unmount wipes the submission + // payload as soon as we navigate to the magic-code RHP. Skip clearing in that case and let the magic-code + // success path (or the next intentional back out) handle cleanup. + const skipClearDraftOnUnmountRef = useRef(false); useEffect( () => () => { + if (skipClearDraftOnUnmountRef.current) { + return; + } clearDraftValues(ONYXKEYS.FORMS.PERSONAL_DETAILS_FORM); }, [], @@ -205,6 +218,7 @@ function PrivatePersonalDetailsPage() { // UI-prefilled values (geolocation country, normalized state) only live in component state until the user touches // a field, so write the full form values to the draft before navigating so the confirm step submits them. setDraftValues(ONYXKEYS.FORMS.PERSONAL_DETAILS_FORM, values); + skipClearDraftOnUnmountRef.current = true; Navigation.navigate(ROUTES.SETTINGS_PRIVATE_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE); }; @@ -305,6 +319,7 @@ function PrivatePersonalDetailsPage() { defaultValue={initialStreet1} shouldSaveDraft autoComplete="address-line1" + autoFocus={fieldToFocus === INPUT_IDS.ADDRESS_LINE_1} renamedInputKeys={{ street: INPUT_IDS.ADDRESS_LINE_1, street2: INPUT_IDS.ADDRESS_LINE_2, From ca52befdd40f057e4e51c329219caa42a18a832c Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Tue, 26 May 2026 10:57:10 -0700 Subject: [PATCH 3/3] Fix state validation fallback and preserve street2 in form rebuild MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the `|| selectedState` / `|| selectedCountry` fallbacks in the PrivatePersonalDetailsPage validator. The form values already reflect the selectors (FormProvider syncs controlled `value` into inputValues), and on the non-US path the state TextInput has no handler to keep selectedState in sync — so clearing a prefilled state let the required check pass against the stale local value and submit an empty addressState. Add `address?.street2` to the precedence chain in getPrivatePersonalDetailsFormValues so a separately-stored line 2 is preserved when the form is rebuilt without a draft, matching the precedence the page itself uses. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/libs/PersonalDetailsUtils.ts | 2 +- .../Profile/PersonalDetails/PrivatePersonalDetailsPage.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 91bc328f7fbd..8d28b536a587 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -333,7 +333,7 @@ function getPrivatePersonalDetailsFormValues(privatePersonalDetails: OnyxEntry