diff --git a/.eslintrc.js b/.eslintrc.js index 54d035ab3850..6194ccd39d3f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -24,6 +24,26 @@ const restrictedImportPaths = [ importNames: ['CSSProperties'], message: "Please use 'ViewStyle', 'TextStyle', 'ImageStyle' from 'react-native' instead.", }, + { + name: '@styles/index', + importNames: ['default', 'defaultStyles'], + message: 'Do not import styles directly. Please use the `useThemeStyles` hook or `withThemeStyles` HOC instead.', + }, + { + name: '@styles/utils', + importNames: ['default', 'DefaultStyleUtils'], + message: 'Do not import StyleUtils directly. Please use the `useStyleUtils` hook or `withStyleUtils` HOC instead.', + }, + { + name: '@styles/theme', + importNames: ['default', 'defaultTheme'], + + message: 'Do not import themes directly. Please use the `useTheme` hook or `withTheme` HOC instead.', + }, + { + name: '@styles/theme/illustrations', + message: 'Do not import theme illustrations directly. Please use the `useThemeIllustrations` hook instead.', + }, ]; const restrictedImportPatterns = [ @@ -31,6 +51,18 @@ const restrictedImportPatterns = [ group: ['**/assets/animations/**/*.json'], message: "Do not import animations directly. Please use the 'src/components/LottieAnimations' import instead.", }, + { + group: ['@styles/theme/themes/**'], + message: 'Do not import themes directly. Please use the `useTheme` hook or `withTheme` HOC instead.', + }, + { + group: ['@styles/utils/**', '!@styles/utils/FontUtils', '!@styles/utils/types'], + message: 'Do not import style util functions directly. Please use the `useStyleUtils` hook or `withStyleUtils` HOC instead.', + }, + { + group: ['@styles/theme/illustrations/themes/**'], + message: 'Do not import theme illustrations directly. Please use the `useThemeIllustrations` hook instead.', + }, ]; module.exports = { diff --git a/.github/actions/javascript/validateReassureOutput/action.yml b/.github/actions/javascript/validateReassureOutput/action.yml index 4fd53e838fb5..b3b18c244a8f 100644 --- a/.github/actions/javascript/validateReassureOutput/action.yml +++ b/.github/actions/javascript/validateReassureOutput/action.yml @@ -7,9 +7,6 @@ inputs: COUNT_DEVIATION: description: Allowable deviation for the mean count in regression test results. required: true - REGRESSION_OUTPUT: - description: Refers to the results obtained from regression tests `.reassure/output.json`. - required: true runs: using: 'node20' main: './index.js' diff --git a/.github/actions/javascript/validateReassureOutput/index.js b/.github/actions/javascript/validateReassureOutput/index.js index 6cc59af1de48..e70c379697cd 100644 --- a/.github/actions/javascript/validateReassureOutput/index.js +++ b/.github/actions/javascript/validateReassureOutput/index.js @@ -8,9 +8,10 @@ /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { const core = __nccwpck_require__(186); +const fs = __nccwpck_require__(147); const run = () => { - const regressionOutput = JSON.parse(core.getInput('REGRESSION_OUTPUT', {required: true})); + const regressionOutput = JSON.parse(fs.readFileSync('.reassure/output.json', 'utf8')); const countDeviation = core.getInput('COUNT_DEVIATION', {required: true}); const durationDeviation = core.getInput('DURATION_DEVIATION_PERCENTAGE', {required: true}); diff --git a/.github/actions/javascript/validateReassureOutput/validateReassureOutput.js b/.github/actions/javascript/validateReassureOutput/validateReassureOutput.js index da81d88c9885..214dc9e8b6d4 100644 --- a/.github/actions/javascript/validateReassureOutput/validateReassureOutput.js +++ b/.github/actions/javascript/validateReassureOutput/validateReassureOutput.js @@ -1,7 +1,8 @@ const core = require('@actions/core'); +const fs = require('fs'); const run = () => { - const regressionOutput = JSON.parse(core.getInput('REGRESSION_OUTPUT', {required: true})); + const regressionOutput = JSON.parse(fs.readFileSync('.reassure/output.json', 'utf8')); const countDeviation = core.getInput('COUNT_DEVIATION', {required: true}); const durationDeviation = core.getInput('DURATION_DEVIATION_PERCENTAGE', {required: true}); diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml index c49530c46faa..116f178868c1 100644 --- a/.github/workflows/reassurePerformanceTests.yml +++ b/.github/workflows/reassurePerformanceTests.yml @@ -36,17 +36,10 @@ jobs: npm install --force npx reassure --branch - - name: Read output.json - id: reassure - uses: juliangruber/read-file-action@v1 - with: - path: .reassure/output.json - - name: Validate output.json id: validateReassureOutput uses: ./.github/actions/javascript/validateReassureOutput with: DURATION_DEVIATION_PERCENTAGE: 20 COUNT_DEVIATION: 0 - REGRESSION_OUTPUT: ${{ steps.reassure.outputs.content }} diff --git a/android/app/build.gradle b/android/app/build.gradle index 4c6289456041..63aa4215cd90 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -96,8 +96,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001042104 - versionName "1.4.21-4" + versionCode 1001042203 + versionName "1.4.22-3" } flavorDimensions "default" diff --git a/assets/emojis/index.ts b/assets/emojis/index.ts index 5e5565da1499..02328001674e 100644 --- a/assets/emojis/index.ts +++ b/assets/emojis/index.ts @@ -1,10 +1,13 @@ +import type {Locale} from '@src/types/onyx'; import emojis from './common'; import enEmojis from './en'; import esEmojis from './es'; -import type {Emoji} from './types'; +import type {Emoji, EmojisList} from './types'; type EmojiTable = Record; +type LocaleEmojis = Partial>; + const emojiNameTable = emojis.reduce((prev, cur) => { const newValue = prev; if (!('header' in cur) && cur.name) { @@ -26,10 +29,10 @@ const emojiCodeTableWithSkinTones = emojis.reduce((prev, cur) => { return newValue; }, {}); -const localeEmojis = { +const localeEmojis: LocaleEmojis = { en: enEmojis, es: esEmojis, -} as const; +}; export default emojis; export {emojiNameTable, emojiCodeTableWithSkinTones, localeEmojis}; diff --git a/assets/emojis/types.ts b/assets/emojis/types.ts index e8c222fde948..e659924a7fa4 100644 --- a/assets/emojis/types.ts +++ b/assets/emojis/types.ts @@ -3,7 +3,7 @@ import type IconAsset from '@src/types/utils/IconAsset'; type Emoji = { code: string; name: string; - types?: string[]; + types?: readonly string[]; }; type HeaderEmoji = { @@ -12,8 +12,10 @@ type HeaderEmoji = { code: string; }; -type PickerEmojis = Array; +type PickerEmoji = Emoji | HeaderEmoji; + +type PickerEmojis = PickerEmoji[]; type EmojisList = Record; -export type {Emoji, HeaderEmoji, EmojisList, PickerEmojis}; +export type {Emoji, HeaderEmoji, EmojisList, PickerEmojis, PickerEmoji}; diff --git a/assets/images/simple-illustrations/simple-illustration__commentbubbles.svg b/assets/images/simple-illustrations/simple-illustration__commentbubbles.svg new file mode 100644 index 000000000000..829d3ee2e3fe --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__commentbubbles.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__hourglass.svg b/assets/images/simple-illustrations/simple-illustration__hourglass.svg new file mode 100644 index 000000000000..539e1e45b795 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__hourglass.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__trashcan.svg b/assets/images/simple-illustrations/simple-illustration__trashcan.svg new file mode 100644 index 000000000000..4e66efa0a67e --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__trashcan.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index ac41382486a5..f58687c66c63 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.21 + 1.4.22 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.21.4 + 1.4.22.3 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index ab58f3c293bd..b7b8c9d3416b 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.21 + 1.4.22 CFBundleSignature ???? CFBundleVersion - 1.4.21.4 + 1.4.22.3 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 77c390c46416..ee54d98895a5 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -614,7 +614,7 @@ PODS: - React-Core - react-native-pager-view (6.2.0): - React-Core - - react-native-pdf (6.7.3): + - react-native-pdf (6.7.4): - React-Core - react-native-performance (5.1.0): - React-Core @@ -1265,7 +1265,7 @@ SPEC CHECKSUMS: react-native-key-command: 5af6ee30ff4932f78da6a2109017549042932aa5 react-native-netinfo: ccbe1085dffd16592791d550189772e13bf479e2 react-native-pager-view: 0ccb8bf60e2ebd38b1f3669fa3650ecce81db2df - react-native-pdf: b4ca3d37a9a86d9165287741c8b2ef4d8940c00e + react-native-pdf: 79aa75e39a80c1d45ffe58aa500f3cf08f267a2e react-native-performance: cef2b618d47b277fb5c3280b81a3aad1e72f2886 react-native-plaid-link-sdk: df1618a85a615d62ff34e34b76abb7a56497fbc1 react-native-quick-sqlite: bcc7a7a250a40222f18913a97cd356bf82d0a6c4 diff --git a/package-lock.json b/package-lock.json index 15964d8c5f3e..55bfafbec2f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.21-4", + "version": "1.4.22-3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.21-4", + "version": "1.4.22-3", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -96,7 +96,7 @@ "react-native-modal": "^13.0.0", "react-native-onyx": "1.0.118", "react-native-pager-view": "^6.2.0", - "react-native-pdf": "^6.7.3", + "react-native-pdf": "^6.7.4", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.9.3", "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5", @@ -47635,9 +47635,9 @@ } }, "node_modules/react-native-pdf": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/react-native-pdf/-/react-native-pdf-6.7.3.tgz", - "integrity": "sha512-bK1fVkj18kBA5YlRFNJ3/vJ1bEX3FDHyAPY6ArtIdVs+vv0HzcK5WH9LSd2bxUsEMIyY9CSjP4j8BcxNXTiQkQ==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/react-native-pdf/-/react-native-pdf-6.7.4.tgz", + "integrity": "sha512-sBeNcsrTRnLjmiU9Wx7Uk0K2kPSQtKIIG+FECdrEG16TOdtmQ3iqqEwt0dmy0pJegpg07uES5BXqiKsKkRUIFw==", "dependencies": { "crypto-js": "4.2.0", "deprecated-react-native-prop-types": "^2.3.0" @@ -90531,9 +90531,9 @@ "requires": {} }, "react-native-pdf": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/react-native-pdf/-/react-native-pdf-6.7.3.tgz", - "integrity": "sha512-bK1fVkj18kBA5YlRFNJ3/vJ1bEX3FDHyAPY6ArtIdVs+vv0HzcK5WH9LSd2bxUsEMIyY9CSjP4j8BcxNXTiQkQ==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/react-native-pdf/-/react-native-pdf-6.7.4.tgz", + "integrity": "sha512-sBeNcsrTRnLjmiU9Wx7Uk0K2kPSQtKIIG+FECdrEG16TOdtmQ3iqqEwt0dmy0pJegpg07uES5BXqiKsKkRUIFw==", "requires": { "crypto-js": "4.2.0", "deprecated-react-native-prop-types": "^2.3.0" diff --git a/package.json b/package.json index 29ade80b518d..7264cb5fa25e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.21-4", + "version": "1.4.22-3", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -144,7 +144,7 @@ "react-native-modal": "^13.0.0", "react-native-onyx": "1.0.118", "react-native-pager-view": "^6.2.0", - "react-native-pdf": "^6.7.3", + "react-native-pdf": "^6.7.4", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.9.3", "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5", diff --git a/src/CONST.ts b/src/CONST.ts index b5563825e016..2fd592f539c2 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -103,6 +103,10 @@ const CONST = { MERCHANT_NAME_MAX_LENGTH: 255, + REQUEST_PREVIEW: { + MAX_LENGTH: 83, + }, + CALENDAR_PICKER: { // Numbers were arbitrarily picked. MIN_YEAR: CURRENT_YEAR - 100, @@ -3054,6 +3058,42 @@ const CONST = { CAROUSEL: 3, }, + VIOLATIONS: { + ALL_TAG_LEVELS_REQUIRED: 'allTagLevelsRequired', + AUTO_REPORTED_REJECTED_EXPENSE: 'autoReportedRejectedExpense', + BILLABLE_EXPENSE: 'billableExpense', + CASH_EXPENSE_WITH_NO_RECEIPT: 'cashExpenseWithNoReceipt', + CATEGORY_OUT_OF_POLICY: 'categoryOutOfPolicy', + CONVERSION_SURCHARGE: 'conversionSurcharge', + CUSTOM_UNIT_OUT_OF_POLICY: 'customUnitOutOfPolicy', + DUPLICATED_TRANSACTION: 'duplicatedTransaction', + FIELD_REQUIRED: 'fieldRequired', + FUTURE_DATE: 'futureDate', + INVOICE_MARKUP: 'invoiceMarkup', + MAX_AGE: 'maxAge', + MISSING_CATEGORY: 'missingCategory', + MISSING_COMMENT: 'missingComment', + MISSING_TAG: 'missingTag', + MODIFIED_AMOUNT: 'modifiedAmount', + MODIFIED_DATE: 'modifiedDate', + NON_EXPENSIWORKS_EXPENSE: 'nonExpensiworksExpense', + OVER_AUTO_APPROVAL_LIMIT: 'overAutoApprovalLimit', + OVER_CATEGORY_LIMIT: 'overCategoryLimit', + OVER_LIMIT: 'overLimit', + OVER_LIMIT_ATTENDEE: 'overLimitAttendee', + PER_DAY_LIMIT: 'perDayLimit', + RECEIPT_NOT_SMART_SCANNED: 'receiptNotSmartScanned', + RECEIPT_REQUIRED: 'receiptRequired', + RTER: 'rter', + SMARTSCAN_FAILED: 'smartscanFailed', + SOME_TAG_LEVELS_REQUIRED: 'someTagLevelsRequired', + TAG_OUT_OF_POLICY: 'tagOutOfPolicy', + TAX_AMOUNT_CHANGED: 'taxAmountChanged', + TAX_OUT_OF_POLICY: 'taxOutOfPolicy', + TAX_RATE_CHANGED: 'taxRateChanged', + TAX_REQUIRED: 'taxRequired', + }, + /** Context menu types */ CONTEXT_MENU_TYPES: { LINK: 'LINK', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 35fa4bbf0837..b6e62814466b 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -358,9 +358,9 @@ const ROUTES = { getUrlWithBackToParam(`create/${iouType}/distance/${transactionID}/${reportID}/`, backTo), }, MONEY_REQUEST_STEP_MERCHANT: { - route: 'create/:iouType/merchante/:transactionID/:reportID/', + route: 'create/:iouType/merchant/:transactionID/:reportID/', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/merchante/${transactionID}/${reportID}/`, backTo), + getUrlWithBackToParam(`create/${iouType}/merchant/${transactionID}/${reportID}/`, backTo), }, MONEY_REQUEST_STEP_PARTICIPANTS: { route: 'create/:iouType/participants/:transactionID/:reportID/', @@ -479,6 +479,7 @@ const ROUTES = { route: 'referral/:contentType', getRoute: (contentType: string) => `referral/${contentType}` as const, }, + PROCESS_MONEY_REQUEST_HOLD: 'hold-request-educational', } as const; export {getUrlWithBackToParam}; diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 91c4153bacd2..703cb309d641 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -108,6 +108,7 @@ const SCREENS = { ROOM_MEMBERS: 'RoomMembers', ROOM_INVITE: 'RoomInvite', REFERRAL: 'Referral', + PROCESS_MONEY_REQUEST_HOLD: 'ProcessMoneyRequestHold', }, SIGN_IN_WITH_APPLE_DESKTOP: 'AppleSignInDesktop', SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop', @@ -231,6 +232,7 @@ const SCREENS = { SIGN_IN_ROOT: 'SignIn_Root', DETAILS_ROOT: 'Details_Root', PROFILE_ROOT: 'Profile_Root', + PROCESS_MONEY_REQUEST_HOLD_ROOT: 'ProcessMoneyRequestHold_Root', REPORT_WELCOME_MESSAGE_ROOT: 'Report_WelcomeMessage_Root', REPORT_PARTICIPANTS_ROOT: 'ReportParticipants_Root', ROOM_MEMBERS_ROOT: 'RoomMembers_Root', diff --git a/src/TIMEZONES.ts b/src/TIMEZONES.ts index 1eb49f291495..238563134872 100644 --- a/src/TIMEZONES.ts +++ b/src/TIMEZONES.ts @@ -1,5 +1,6 @@ +/* eslint-disable @typescript-eslint/naming-convention */ // All timezones were taken from: https://raw.githubusercontent.com/leon-do/Timezones/main/timezone.json -export default [ +const TIMEZONES = [ 'Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa', @@ -419,3 +420,137 @@ export default [ 'Pacific/Wake', 'Pacific/Wallis', ] as const; + +/** + * The timezones supported in browser and on native devices differ, so we must map each timezone to its supported equivalent. + * Data sourced from https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + */ +const timezoneBackwardMap: Record = { + 'Africa/Asmera': 'Africa/Nairobi', + 'Africa/Timbuktu': 'Africa/Abidjan', + 'America/Argentina/ComodRivadavia': 'America/Argentina/Catamarca', + 'America/Atka': 'America/Adak', + 'America/Buenos_Aires': 'America/Argentina/Buenos_Aires', + 'America/Catamarca': 'America/Argentina/Catamarca', + 'America/Coral_Harbour': 'America/Panama', + 'America/Cordoba': 'America/Argentina/Cordoba', + 'America/Ensenada': 'America/Tijuana', + 'America/Fort_Wayne': 'America/Indiana/Indianapolis', + 'America/Godthab': 'America/Nuuk', + 'America/Indianapolis': 'America/Indiana/Indianapolis', + 'America/Jujuy': 'America/Argentina/Jujuy', + 'America/Knox_IN': 'America/Indiana/Knox', + 'America/Louisville': 'America/Kentucky/Louisville', + 'America/Mendoza': 'America/Argentina/Mendoza', + 'America/Montreal': 'America/Toronto', + 'America/Nipigon': 'America/Toronto', + 'America/Pangnirtung': 'America/Iqaluit', + 'America/Porto_Acre': 'America/Rio_Branco', + 'America/Rainy_River': 'America/Winnipeg', + 'America/Rosario': 'America/Argentina/Cordoba', + 'America/Santa_Isabel': 'America/Tijuana', + 'America/Shiprock': 'America/Denver', + 'America/Thunder_Bay': 'America/Toronto', + 'America/Virgin': 'America/Puerto_Rico', + 'America/Yellowknife': 'America/Edmonton', + 'Antarctica/South_Pole': 'Pacific/Auckland', + 'Asia/Ashkhabad': 'Asia/Ashgabat', + 'Asia/Calcutta': 'Asia/Kolkata', + 'Asia/Chongqing': 'Asia/Shanghai', + 'Asia/Chungking': 'Asia/Shanghai', + 'Asia/Dacca': 'Asia/Dhaka', + 'Asia/Harbin': 'Asia/Shanghai', + 'Asia/Istanbul': 'Europe/Istanbul', + 'Asia/Kashgar': 'Asia/Urumqi', + 'Asia/Katmandu': 'Asia/Kathmandu', + 'Asia/Macao': 'Asia/Macau', + 'Asia/Rangoon': 'Asia/Yangon', + 'Asia/Saigon': 'Asia/Ho_Chi_Minh', + 'Asia/Tel_Aviv': 'Asia/Jerusalem', + 'Asia/Thimbu': 'Asia/Thimphu', + 'Asia/Ujung_Pandang': 'Asia/Makassar', + 'Asia/Ulan_Bator': 'Asia/Ulaanbaatar', + 'Atlantic/Faeroe': 'Atlantic/Faroe', + 'Atlantic/Jan_Mayen': 'Europe/Berlin', + 'Australia/ACT': 'Australia/Sydney', + 'Australia/Canberra': 'Australia/Sydney', + 'Australia/Currie': 'Australia/Hobart', + 'Australia/LHI': 'Australia/Lord_Howe', + 'Australia/NSW': 'Australia/Sydney', + 'Australia/North': 'Australia/Darwin', + 'Australia/Queensland': 'Australia/Brisbane', + 'Australia/South': 'Australia/Adelaide', + 'Australia/Tasmania': 'Australia/Hobart', + 'Australia/Victoria': 'Australia/Melbourne', + 'Australia/West': 'Australia/Perth', + 'Australia/Yancowinna': 'Australia/Broken_Hill', + 'Brazil/Acre': 'America/Rio_Branco', + 'Brazil/DeNoronha': 'America/Noronha', + 'Brazil/East': 'America/Sao_Paulo', + 'Brazil/West': 'America/Manaus', + 'Canada/Atlantic': 'America/Halifax', + 'Canada/Central': 'America/Winnipeg', + 'Canada/Eastern': 'America/Toronto', + 'Canada/Mountain': 'America/Edmonton', + 'Canada/Newfoundland': 'America/St_Johns', + 'Canada/Pacific': 'America/Vancouver', + 'Canada/Saskatchewan': 'America/Regina', + 'Canada/Yukon': 'America/Whitehorse', + 'Chile/Continental': 'America/Santiago', + 'Chile/EasterIsland': 'Pacific/Easter', + Cuba: 'America/Havana', + Egypt: 'Africa/Cairo', + Eire: 'Europe/Dublin', + 'Europe/Belfast': 'Europe/London', + 'Europe/Kiev': 'Europe/Kyiv', + 'Europe/Nicosia': 'Asia/Nicosia', + 'Europe/Tiraspol': 'Europe/Chisinau', + 'Europe/Uzhgorod': 'Europe/Kyiv', + 'Europe/Zaporozhye': 'Europe/Kyiv', + GB: 'Europe/London', + 'GB-Eire': 'Europe/London', + Hongkong: 'Asia/Hong_Kong', + Iceland: 'Africa/Abidjan', + Iran: 'Asia/Tehran', + Israel: 'Asia/Jerusalem', + Jamaica: 'America/Jamaica', + Japan: 'Asia/Tokyo', + Kwajalein: 'Pacific/Kwajalein', + Libya: 'Africa/Tripoli', + 'Mexico/BajaNorte': 'America/Tijuana', + 'Mexico/BajaSur': 'America/Mazatlan', + 'Mexico/General': 'America/Mexico_City', + NZ: 'Pacific/Auckland', + 'NZ-CHAT': 'Pacific/Chatham', + Navajo: 'America/Denver', + PRC: 'Asia/Shanghai', + 'Pacific/Enderbury': 'Pacific/Kanton', + 'Pacific/Johnston': 'Pacific/Honolulu', + 'Pacific/Ponape': 'Pacific/Guadalcanal', + 'Pacific/Samoa': 'Pacific/Pago_Pago', + 'Pacific/Truk': 'Pacific/Port_Moresby', + 'Pacific/Yap': 'Pacific/Port_Moresby', + Poland: 'Europe/Warsaw', + Portugal: 'Europe/Lisbon', + ROC: 'Asia/Taipei', + ROK: 'Asia/Seoul', + Singapore: 'Asia/Singapore', + Turkey: 'Europe/Istanbul', + 'US/Alaska': 'America/Anchorage', + 'US/Aleutian': 'America/Adak', + 'US/Arizona': 'America/Phoenix', + 'US/Central': 'America/Chicago', + 'US/East-Indiana': 'America/Indiana/Indianapolis', + 'US/Eastern': 'America/New_York', + 'US/Hawaii': 'Pacific/Honolulu', + 'US/Indiana-Starke': 'America/Indiana/Knox', + 'US/Michigan': 'America/Detroit', + 'US/Mountain': 'America/Denver', + 'US/Pacific': 'America/Los_Angeles', + 'US/Samoa': 'Pacific/Pago_Pago', + 'W-SU': 'Europe/Moscow', +}; + +export {timezoneBackwardMap}; + +export default TIMEZONES; diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index 31b04a3d954f..9f2635633318 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -349,6 +349,7 @@ function AddressSearch({ lat: successData.coords.latitude, lng: successData.coords.longitude, address: CONST.YOUR_LOCATION_TEXT, + name: CONST.YOUR_LOCATION_TEXT, }; onPress(location); }, diff --git a/src/components/AmountTextInput.js b/src/components/AmountTextInput.js index 25e1ce6f05ec..231a99f0e6a6 100644 --- a/src/components/AmountTextInput.js +++ b/src/components/AmountTextInput.js @@ -32,7 +32,7 @@ const propTypes = { style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), /** Style for the container */ - containerStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + touchableInputWrapperStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), /** Function to call to handle key presses in the text input */ onKeyPress: PropTypes.func, @@ -44,7 +44,7 @@ const defaultProps = { onSelectionChange: () => {}, onKeyPress: () => {}, style: {}, - containerStyles: {}, + touchableInputWrapperStyle: {}, }; function AmountTextInput(props) { @@ -67,7 +67,7 @@ function AmountTextInput(props) { onSelectionChange={props.onSelectionChange} role={CONST.ROLE.PRESENTATION} onKeyPress={props.onKeyPress} - containerStyles={[...StyleUtils.parseStyleAsArray(props.containerStyles)]} + touchableInputWrapperStyle={props.touchableInputWrapperStyle} /> ); } diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index d24d1e18907f..1b4d350f7d4f 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -19,7 +19,6 @@ import fileDownload from '@libs/fileDownload'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import useNativeDriver from '@libs/useNativeDriver'; import reportPropTypes from '@pages/reportPropTypes'; @@ -95,6 +94,9 @@ const propTypes = { /** Whether it is a receipt attachment or not */ isReceiptAttachment: PropTypes.bool, + + /** Whether the receipt can be replaced */ + canEditReceipt: PropTypes.bool, }; const defaultProps = { @@ -113,6 +115,7 @@ const defaultProps = { onCarouselAttachmentChange: () => {}, isWorkspaceAvatar: false, isReceiptAttachment: false, + canEditReceipt: false, }; function AttachmentModal(props) { @@ -126,7 +129,7 @@ function AttachmentModal(props) { const [isAuthTokenRequired, setIsAuthTokenRequired] = useState(props.isAuthTokenRequired); const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState(''); const [attachmentInvalidReason, setAttachmentInvalidReason] = useState(null); - const [source, setSource] = useState(props.source); + const [source, setSource] = useState(() => props.source); const [modalType, setModalType] = useState(CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE); const [isConfirmButtonDisabled, setIsConfirmButtonDisabled] = useState(false); const [confirmButtonFadeAnimation] = useState(() => new Animated.Value(1)); @@ -359,7 +362,7 @@ function AttachmentModal(props) { }, []); useEffect(() => { - setSource(props.source); + setSource(() => props.source); }, [props.source]); useEffect(() => { @@ -372,13 +375,9 @@ function AttachmentModal(props) { if (!props.isReceiptAttachment || !props.parentReport || !props.parentReportActions) { return []; } - const menuItems = []; - const parentReportAction = props.parentReportActions[props.report.parentReportActionID]; - const canEdit = - ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, props.parentReport.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT, props.transaction) && - !TransactionUtils.isDistanceRequest(props.transaction); - if (canEdit) { + const menuItems = []; + if (props.canEditReceipt) { menuItems.push({ icon: Expensicons.Camera, text: props.translate('common.replace'), @@ -393,7 +392,7 @@ function AttachmentModal(props) { text: props.translate('common.download'), onSelected: () => downloadAttachment(source), }); - if (TransactionUtils.hasReceipt(props.transaction) && !TransactionUtils.isReceiptBeingScanned(props.transaction) && canEdit) { + if (TransactionUtils.hasReceipt(props.transaction) && !TransactionUtils.isReceiptBeingScanned(props.transaction) && props.canEditReceipt) { menuItems.push({ icon: Expensicons.Trashcan, text: props.translate('receipt.deleteReceipt'), diff --git a/src/components/ConfirmPopover.js b/src/components/ConfirmPopover.js deleted file mode 100644 index 83001736b471..000000000000 --- a/src/components/ConfirmPopover.js +++ /dev/null @@ -1,85 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ConfirmContent from './ConfirmContent'; -import Popover from './Popover'; -import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; - -const propTypes = { - /** Title of the modal */ - title: PropTypes.string.isRequired, - - /** A callback to call when the form has been submitted */ - onConfirm: PropTypes.func.isRequired, - - /** A callback to call when the form has been closed */ - onCancel: PropTypes.func, - - /** Modal visibility */ - isVisible: PropTypes.bool.isRequired, - - /** Confirm button text */ - confirmText: PropTypes.string, - - /** Cancel button text */ - cancelText: PropTypes.string, - - /** Is the action destructive */ - danger: PropTypes.bool, - - /** Whether we should show the cancel button */ - shouldShowCancelButton: PropTypes.bool, - - /** Modal content text/element */ - prompt: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), - - /** Where the popover should be positioned */ - anchorPosition: PropTypes.shape({ - top: PropTypes.number, - left: PropTypes.number, - }).isRequired, - - /** Styles for view */ - // eslint-disable-next-line react/forbid-prop-types - contentStyles: PropTypes.arrayOf(PropTypes.object), - - ...windowDimensionsPropTypes, -}; - -const defaultProps = { - confirmText: '', - cancelText: '', - danger: false, - onCancel: () => {}, - shouldShowCancelButton: true, - prompt: '', - contentStyles: [], -}; - -function ConfirmPopover(props) { - return ( - - - - ); -} - -ConfirmPopover.propTypes = propTypes; -ConfirmPopover.defaultProps = defaultProps; -ConfirmPopover.displayName = 'ConfirmPopover'; -export default withWindowDimensions(ConfirmPopover); diff --git a/src/components/EmojiSuggestions.tsx b/src/components/EmojiSuggestions.tsx index 5904b1521f98..1c0306741048 100644 --- a/src/components/EmojiSuggestions.tsx +++ b/src/components/EmojiSuggestions.tsx @@ -1,9 +1,9 @@ import type {ReactElement} from 'react'; import React, {useCallback} from 'react'; import {View} from 'react-native'; +import type {Emoji} from '@assets/emojis/types'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {SimpleEmoji} from '@libs/EmojiTrie'; import * as EmojiUtils from '@libs/EmojiUtils'; import getStyledTextArray from '@libs/GetStyledTextArray'; import AutoCompleteSuggestions from './AutoCompleteSuggestions'; @@ -16,7 +16,7 @@ type EmojiSuggestionsProps = { highlightedEmojiIndex?: number; /** Array of suggested emoji */ - emojis: SimpleEmoji[]; + emojis: Emoji[]; /** Fired when the user selects an emoji */ onSelect: (index: number) => void; @@ -40,7 +40,7 @@ type EmojiSuggestionsProps = { /** * Create unique keys for each emoji item */ -const keyExtractor = (item: SimpleEmoji, index: number): string => `${item.name}+${index}}`; +const keyExtractor = (item: Emoji, index: number): string => `${item.name}+${index}}`; function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferredSkinToneIndex, highlightedEmojiIndex = 0, measureParentContainer = () => {}}: EmojiSuggestionsProps) { const styles = useThemeStyles(); @@ -49,7 +49,7 @@ function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferr * Render an emoji suggestion menu item component. */ const renderSuggestionMenuItem = useCallback( - (item: SimpleEmoji): ReactElement => { + (item: Emoji): ReactElement => { const styledTextArray = getStyledTextArray(item.name, prefix); return ( diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js index 0b5cbad29983..46d04ca9404d 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js @@ -4,7 +4,7 @@ import {defaultHTMLElementModels, RenderHTMLConfigProvider, TRenderEngineProvide import _ from 'underscore'; import useThemeStyles from '@hooks/useThemeStyles'; import convertToLTR from '@libs/convertToLTR'; -import singleFontFamily from '@styles/utils/fontFamily/singleFontFamily'; +import FontUtils from '@styles/utils/FontUtils'; import * as HTMLEngineUtils from './htmlEngineUtils'; import htmlRenderers from './HTMLRenderers'; @@ -62,7 +62,7 @@ function BaseHTMLEngineProvider(props) { 'mention-here': defaultHTMLElementModels.span.extend({tagName: 'mention-here'}), 'next-step': defaultHTMLElementModels.span.extend({ tagName: 'next-step', - mixedUAStyles: {...styles.textLabelSupporting}, + mixedUAStyles: {...styles.textLabelSupporting, ...styles.lh16}, }), 'next-step-email': defaultHTMLElementModels.span.extend({tagName: 'next-step-email'}), video: defaultHTMLElementModels.div.extend({ @@ -70,7 +70,7 @@ function BaseHTMLEngineProvider(props) { mixedUAStyles: {whiteSpace: 'pre'}, }), }), - [styles.colorMuted, styles.formError, styles.mb0, styles.textLabelSupporting], + [styles.colorMuted, styles.formError, styles.mb0, styles.textLabelSupporting, styles.lh16], ); // We need to memoize this prop to make it referentially stable. @@ -82,7 +82,7 @@ function BaseHTMLEngineProvider(props) { baseStyle={styles.webViewStyles.baseFontStyle} tagsStyles={styles.webViewStyles.tagStyles} enableCSSInlineProcessing={false} - systemFonts={_.values(singleFontFamily)} + systemFonts={_.values(FontUtils.fontFamily.single)} domVisitors={{ // eslint-disable-next-line no-param-reassign onText: (text) => (text.data = convertToLTR(text.data)), diff --git a/src/components/HeaderPageLayout.js b/src/components/HeaderPageLayout.tsx similarity index 55% rename from src/components/HeaderPageLayout.js rename to src/components/HeaderPageLayout.tsx index 9ef5d4f83a06..304bb2ce49b1 100644 --- a/src/components/HeaderPageLayout.js +++ b/src/components/HeaderPageLayout.tsx @@ -1,56 +1,54 @@ -import PropTypes from 'prop-types'; import React, {useMemo} from 'react'; +import type {ReactNode} from 'react'; import {ScrollView, View} from 'react-native'; -import _ from 'underscore'; +import type {StyleProp, ViewStyle} from 'react-native'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; import FixedFooter from './FixedFooter'; import HeaderWithBackButton from './HeaderWithBackButton'; -import headerWithBackButtonPropTypes from './HeaderWithBackButton/headerWithBackButtonPropTypes'; +import type HeaderWithBackButtonProps from './HeaderWithBackButton/types'; import ScreenWrapper from './ScreenWrapper'; -const propTypes = { - ...headerWithBackButtonPropTypes, +type HeaderPageLayoutProps = ChildrenProps & + HeaderWithBackButtonProps & { + /** The background color to apply in the upper half of the screen. */ + backgroundColor?: string; - /** Children to display in the lower half of the page (below the header section w/ an animation) */ - children: PropTypes.node.isRequired, + /** A fixed footer to display at the bottom of the page. */ + footer?: ReactNode; - /** The background color to apply in the upper half of the screen. */ - backgroundColor: PropTypes.string, + /** The image to display in the upper half of the screen. */ + headerContent?: ReactNode; - /** A fixed footer to display at the bottom of the page. */ - footer: PropTypes.node, + /** Style to apply to the header image container */ + headerContainerStyles?: StyleProp; - /** The image to display in the upper half of the screen. */ - header: PropTypes.node, + /** Style to apply to the ScrollView container */ + scrollViewContainerStyles?: StyleProp; - /** Style to apply to the header image container */ - // eslint-disable-next-line react/forbid-prop-types - headerContainerStyles: PropTypes.arrayOf(PropTypes.object), + /** Style to apply to the children container */ + childrenContainerStyles?: StyleProp; - /** Style to apply to the ScrollView container */ - // eslint-disable-next-line react/forbid-prop-types - scrollViewContainerStyles: PropTypes.arrayOf(PropTypes.object), + /** Style to apply to the whole section container */ + style?: StyleProp; + }; - /** Style to apply to the children container */ - // eslint-disable-next-line react/forbid-prop-types - childrenContainerStyles: PropTypes.arrayOf(PropTypes.object), -}; - -const defaultProps = { - backgroundColor: undefined, - header: null, - headerContainerStyles: [], - scrollViewContainerStyles: [], - childrenContainerStyles: [], - footer: null, -}; - -function HeaderPageLayout({backgroundColor, children, footer, headerContainerStyles, scrollViewContainerStyles, childrenContainerStyles, style, headerContent, ...propsToPassToHeader}) { +function HeaderPageLayout({ + backgroundColor, + children, + footer, + headerContainerStyles, + scrollViewContainerStyles, + childrenContainerStyles, + style, + headerContent, + ...rest +}: HeaderPageLayoutProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -58,7 +56,7 @@ function HeaderPageLayout({backgroundColor, children, footer, headerContainerSty const {isOffline} = useNetwork(); const appBGColor = StyleUtils.getBackgroundColorStyle(theme.appBG); const {titleColor, iconFill} = useMemo(() => { - const isColorfulBackground = (backgroundColor || theme.appBG) !== theme.appBG && (backgroundColor || theme.highlightBG) !== theme.highlightBG; + const isColorfulBackground = (backgroundColor ?? theme.appBG) !== theme.appBG && (backgroundColor ?? theme.highlightBG) !== theme.highlightBG; return { titleColor: isColorfulBackground ? theme.textColorfulBackground : undefined, iconFill: isColorfulBackground ? theme.iconColorfulBackground : undefined, @@ -67,7 +65,7 @@ function HeaderPageLayout({backgroundColor, children, footer, headerContainerSty return ( - + {/** Safari on ios/mac has a bug where overscrolling the page scrollview shows green background color. This is a workaround to fix that. https://github.com/Expensify/App/issues/23422 */} {Browser.isSafari() && ( - + )} - - {!Browser.isSafari() && } - + + {!Browser.isSafari() && } + {headerContent} {children} - {!_.isNull(footer) && {footer}} + {!!footer && {footer}} )} @@ -107,8 +102,7 @@ function HeaderPageLayout({backgroundColor, children, footer, headerContainerSty ); } -HeaderPageLayout.propTypes = propTypes; -HeaderPageLayout.defaultProps = defaultProps; HeaderPageLayout.displayName = 'HeaderPageLayout'; +export type {HeaderPageLayoutProps}; export default HeaderPageLayout; diff --git a/src/components/HoldMenuSectionList.tsx b/src/components/HoldMenuSectionList.tsx new file mode 100644 index 000000000000..aa5dd75ce159 --- /dev/null +++ b/src/components/HoldMenuSectionList.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {ImageSourcePropType} from 'react-native'; +import type {SvgProps} from 'react-native-svg'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import type {TranslationPaths} from '@src/languages/types'; +import Icon from './Icon'; +import * as Illustrations from './Icon/Illustrations'; +import Text from './Text'; + +type HoldMenuSection = { + /** The icon supplied with the section */ + icon: React.FC | ImageSourcePropType; + + /** Translation key for the title */ + titleTranslationKey: TranslationPaths; + + /** Translation key for the description */ + descriptionTranslationKey: TranslationPaths; +}; + +function HoldMenuSectionList() { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const holdMenuSections: HoldMenuSection[] = [ + { + icon: Illustrations.Hourglass, + titleTranslationKey: 'iou.whatIsHoldTitle', + descriptionTranslationKey: 'iou.whatIsHoldExplain', + }, + { + icon: Illustrations.CommentBubbles, + titleTranslationKey: 'iou.holdIsTemporaryTitle', + descriptionTranslationKey: 'iou.holdIsTemporaryExplain', + }, + { + icon: Illustrations.TrashCan, + titleTranslationKey: 'iou.deleteHoldTitle', + descriptionTranslationKey: 'iou.deleteHoldExplain', + }, + ]; + + return ( + <> + {holdMenuSections.map((section, i) => ( + + + + {translate(section.titleTranslationKey)} + + {translate(section.descriptionTranslationKey)} + + + + ))} + + ); +} + +HoldMenuSectionList.displayName = 'HoldMenuSectionList'; + +export type {HoldMenuSection}; + +export default HoldMenuSectionList; diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index 1e574504001d..954c8d0392fc 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -32,6 +32,7 @@ import BigRocket from '@assets/images/simple-illustrations/simple-illustration__ import PinkBill from '@assets/images/simple-illustrations/simple-illustration__bill.svg'; import ChatBubbles from '@assets/images/simple-illustrations/simple-illustration__chatbubbles.svg'; import CoffeeMug from '@assets/images/simple-illustrations/simple-illustration__coffeemug.svg'; +import CommentBubbles from '@assets/images/simple-illustrations/simple-illustration__commentbubbles.svg'; import ConciergeBubble from '@assets/images/simple-illustrations/simple-illustration__concierge-bubble.svg'; import ConciergeNew from '@assets/images/simple-illustrations/simple-illustration__concierge.svg'; import CreditCardsNew from '@assets/images/simple-illustrations/simple-illustration__credit-cards.svg'; @@ -39,6 +40,7 @@ import EmailAddress from '@assets/images/simple-illustrations/simple-illustratio import HandCard from '@assets/images/simple-illustrations/simple-illustration__handcard.svg'; import HandEarth from '@assets/images/simple-illustrations/simple-illustration__handearth.svg'; import HotDogStand from '@assets/images/simple-illustrations/simple-illustration__hotdogstand.svg'; +import Hourglass from '@assets/images/simple-illustrations/simple-illustration__hourglass.svg'; import InvoiceBlue from '@assets/images/simple-illustrations/simple-illustration__invoice.svg'; import LockOpen from '@assets/images/simple-illustrations/simple-illustration__lockopen.svg'; import Luggage from '@assets/images/simple-illustrations/simple-illustration__luggage.svg'; @@ -53,6 +55,7 @@ import ShieldYellow from '@assets/images/simple-illustrations/simple-illustratio import SmallRocket from '@assets/images/simple-illustrations/simple-illustration__smallrocket.svg'; import ThumbsUpStars from '@assets/images/simple-illustrations/simple-illustration__thumbsupstars.svg'; import TrackShoe from '@assets/images/simple-illustrations/simple-illustration__track-shoe.svg'; +import TrashCan from '@assets/images/simple-illustrations/simple-illustration__trashcan.svg'; import TreasureChest from '@assets/images/simple-illustrations/simple-illustration__treasurechest.svg'; export { @@ -111,5 +114,8 @@ export { Hands, HandEarth, SmartScan, + Hourglass, + CommentBubbles, + TrashCan, TeleScope, }; diff --git a/src/components/IllustratedHeaderPageLayout.js b/src/components/IllustratedHeaderPageLayout.js deleted file mode 100644 index 9980d8a7879a..000000000000 --- a/src/components/IllustratedHeaderPageLayout.js +++ /dev/null @@ -1,67 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import HeaderPageLayout from './HeaderPageLayout'; -import headerWithBackButtonPropTypes from './HeaderWithBackButton/headerWithBackButtonPropTypes'; -import Lottie from './Lottie'; - -const propTypes = { - ...headerWithBackButtonPropTypes, - - /** Children to display in the lower half of the page (below the header section w/ an animation) */ - children: PropTypes.node.isRequired, - - /** The illustration to display in the header. Can be either an SVG component or a JSON object representing a Lottie animation. */ - illustration: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired, - - /** The background color to apply in the upper half of the screen. */ - backgroundColor: PropTypes.string, - - /** A fixed footer to display at the bottom of the page. */ - footer: PropTypes.node, - - /** Overlay content to display on top of animation */ - overlayContent: PropTypes.func, -}; - -const defaultProps = { - backgroundColor: undefined, - footer: null, - overlayContent: null, -}; - -function IllustratedHeaderPageLayout({backgroundColor, children, illustration, footer, overlayContent, ...propsToPassToHeader}) { - const theme = useTheme(); - const styles = useThemeStyles(); - return ( - - - {overlayContent && overlayContent()} - - } - headerContainerStyles={[styles.justifyContentCenter, styles.w100]} - footer={footer} - // eslint-disable-next-line react/jsx-props-no-spreading - {...propsToPassToHeader} - > - {children} - - ); -} - -IllustratedHeaderPageLayout.propTypes = propTypes; -IllustratedHeaderPageLayout.defaultProps = defaultProps; -IllustratedHeaderPageLayout.displayName = 'IllustratedHeaderPageLayout'; - -export default IllustratedHeaderPageLayout; diff --git a/src/components/IllustratedHeaderPageLayout.tsx b/src/components/IllustratedHeaderPageLayout.tsx new file mode 100644 index 000000000000..72ec0adf7672 --- /dev/null +++ b/src/components/IllustratedHeaderPageLayout.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import type {ReactNode} from 'react'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import HeaderPageLayout from './HeaderPageLayout'; +import type {HeaderPageLayoutProps} from './HeaderPageLayout'; +import Lottie from './Lottie'; +import type DotLottieAnimation from './LottieAnimations/types'; + +type IllustratedHeaderPageLayoutProps = HeaderPageLayoutProps & { + /** The illustration to display in the header. Can be a JSON object representing a Lottie animation. */ + illustration: DotLottieAnimation; + + /** The background color to apply in the upper half of the screen. */ + backgroundColor?: string; + + /** Overlay content to display on top of animation */ + overlayContent?: () => ReactNode; +}; + +function IllustratedHeaderPageLayout({backgroundColor, children, illustration, overlayContent, ...rest}: IllustratedHeaderPageLayoutProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + return ( + + + {overlayContent?.()} + + } + headerContainerStyles={[styles.justifyContentCenter, styles.w100]} + // eslint-disable-next-line react/jsx-props-no-spreading + {...rest} + > + {children} + + ); +} + +IllustratedHeaderPageLayout.displayName = 'IllustratedHeaderPageLayout'; + +export default IllustratedHeaderPageLayout; diff --git a/src/components/InvertedFlatList/index.native.tsx b/src/components/InvertedFlatList/index.native.tsx index 76c4b774d0cc..70cabf5a536a 100644 --- a/src/components/InvertedFlatList/index.native.tsx +++ b/src/components/InvertedFlatList/index.native.tsx @@ -1,18 +1,15 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef} from 'react'; import type {FlatList, FlatListProps} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; import BaseInvertedFlatList from './BaseInvertedFlatList'; import CellRendererComponent from './CellRendererComponent'; function BaseInvertedFlatListWithRef(props: FlatListProps, ref: ForwardedRef) { - const styles = useThemeStyles(); return ( ({onScroll: onScrollProp = () => {}, contentContainerStyle, ...props}: FlatListProps, ref: ForwardedRef) { +function InvertedFlatList({onScroll: onScrollProp = () => {}, ...props}: FlatListProps, ref: ForwardedRef) { const lastScrollEvent = useRef(null); const scrollEndTimeout = useRef(null); const updateInProgress = useRef(false); @@ -86,7 +86,6 @@ function InvertedFlatList({onScroll: onScrollProp = () => {}, contentContaine // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={ref} - contentContainerStyle={contentContainerStyle} onScroll={handleScroll} /> ); diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js index 06f8ee4cfeb6..45326edb4610 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.js @@ -183,8 +183,8 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError onError={onError} onLoadEnd={() => setImageLoaded(true)} onLoad={(e) => { - const width = (e.nativeEvent?.width || 0) / PixelRatio.get(); - const height = (e.nativeEvent?.height || 0) / PixelRatio.get(); + const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); + const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); setImageDimensions({...imageDimensions, lightboxSize: {width, height}}); }} /> @@ -205,8 +205,8 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError isAuthTokenRequired={isAuthTokenRequired} onLoadEnd={() => setFallbackLoaded(true)} onLoad={(e) => { - const width = e.nativeEvent?.width || 0; - const height = e.nativeEvent?.height || 0; + const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); + const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); if (imageDimensions?.lightboxSize != null) { return; diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 14dfd0e8dbc4..86e77ae4bfc3 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -35,20 +35,6 @@ import RenderHTML from './RenderHTML'; import SelectCircle from './SelectCircle'; import Text from './Text'; -type ResponsiveProps = { - /** Function to fire when component is pressed */ - onPress: (event: GestureResponderEvent | KeyboardEvent) => void; - - interactive?: true; -}; - -type UnresponsiveProps = { - onPress?: undefined; - - /** Whether the menu item should be interactive at all */ - interactive: false; -}; - type IconProps = { /** Flag to choose between avatar image or an icon */ iconType?: typeof CONST.ICON_TYPE_ICON; @@ -69,170 +55,175 @@ type NoIcon = { icon?: undefined; }; -type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & - (IconProps | AvatarProps | NoIcon) & { - /** Text to be shown as badge near the right end. */ - badgeText?: string; +type MenuItemProps = (IconProps | AvatarProps | NoIcon) & { + /** Function to fire when component is pressed */ + onPress?: (event: GestureResponderEvent | KeyboardEvent) => void; + + /** Whether the menu item should be interactive at all */ + interactive?: boolean; - /** Used to apply offline styles to child text components */ - style?: ViewStyle; + /** Text to be shown as badge near the right end. */ + badgeText?: string; - /** Any additional styles to apply */ - wrapperStyle?: StyleProp; + /** Used to apply offline styles to child text components */ + style?: ViewStyle; - /** Any additional styles to apply on the outer element */ - containerStyle?: StyleProp; + /** Any additional styles to apply */ + wrapperStyle?: StyleProp; - /** Used to apply styles specifically to the title */ - titleStyle?: ViewStyle; + /** Any additional styles to apply on the outer element */ + containerStyle?: StyleProp; - /** Any adjustments to style when menu item is hovered or pressed */ - hoverAndPressStyle?: StyleProp>; + /** Used to apply styles specifically to the title */ + titleStyle?: ViewStyle; - /** Additional styles to style the description text below the title */ - descriptionTextStyle?: StyleProp; + /** Any adjustments to style when menu item is hovered or pressed */ + hoverAndPressStyle?: StyleProp>; - /** The fill color to pass into the icon. */ - iconFill?: string; + /** Additional styles to style the description text below the title */ + descriptionTextStyle?: StyleProp; - /** Secondary icon to display on the left side of component, right of the icon */ - secondaryIcon?: IconAsset; + /** The fill color to pass into the icon. */ + iconFill?: string; - /** The fill color to pass into the secondary icon. */ - secondaryIconFill?: string; + /** Secondary icon to display on the left side of component, right of the icon */ + secondaryIcon?: IconAsset; - /** Icon Width */ - iconWidth?: number; + /** The fill color to pass into the secondary icon. */ + secondaryIconFill?: string; - /** Icon Height */ - iconHeight?: number; + /** Icon Width */ + iconWidth?: number; - /** Any additional styles to pass to the icon container. */ - iconStyles?: StyleProp; + /** Icon Height */ + iconHeight?: number; - /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ - fallbackIcon?: IconAsset; + /** Any additional styles to pass to the icon container. */ + iconStyles?: StyleProp; - /** An icon to display under the main item */ - furtherDetailsIcon?: IconAsset; + /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ + fallbackIcon?: IconAsset; - /** Boolean whether to display the title right icon */ - shouldShowTitleIcon?: boolean; + /** An icon to display under the main item */ + furtherDetailsIcon?: IconAsset; - /** Icon to display at right side of title */ - titleIcon?: IconAsset; + /** Boolean whether to display the title right icon */ + shouldShowTitleIcon?: boolean; - /** Boolean whether to display the right icon */ - shouldShowRightIcon?: boolean; + /** Icon to display at right side of title */ + titleIcon?: IconAsset; - /** Overrides the icon for shouldShowRightIcon */ - iconRight?: IconAsset; + /** Boolean whether to display the right icon */ + shouldShowRightIcon?: boolean; - /** Should render component on the right */ - shouldShowRightComponent?: boolean; + /** Overrides the icon for shouldShowRightIcon */ + iconRight?: IconAsset; - /** Component to be displayed on the right */ - rightComponent?: ReactNode; + /** Should render component on the right */ + shouldShowRightComponent?: boolean; - /** A description text to show under the title */ - description?: string; + /** Component to be displayed on the right */ + rightComponent?: ReactNode; - /** Should the description be shown above the title (instead of the other way around) */ - shouldShowDescriptionOnTop?: boolean; + /** A description text to show under the title */ + description?: string; - /** Error to display below the title */ - error?: string; + /** Should the description be shown above the title (instead of the other way around) */ + shouldShowDescriptionOnTop?: boolean; - /** Error to display at the bottom of the component */ - errorText?: string; + /** Error to display below the title */ + error?: string; - /** A boolean flag that gives the icon a green fill if true */ - success?: boolean; + /** Error to display at the bottom of the component */ + errorText?: string; - /** Whether item is focused or active */ - focused?: boolean; + /** A boolean flag that gives the icon a green fill if true */ + success?: boolean; - /** Should we disable this menu item? */ - disabled?: boolean; + /** Whether item is focused or active */ + focused?: boolean; - /** Text that appears above the title */ - label?: string; + /** Should we disable this menu item? */ + disabled?: boolean; - /** Label to be displayed on the right */ - rightLabel?: string; + /** Text that appears above the title */ + label?: string; - /** Text to display for the item */ - title?: string; + /** Label to be displayed on the right */ + rightLabel?: string; - /** A right-aligned subtitle for this menu option */ - subtitle?: string | number; + /** Text to display for the item */ + title?: string; - /** Should the title show with normal font weight (not bold) */ - shouldShowBasicTitle?: boolean; + /** A right-aligned subtitle for this menu option */ + subtitle?: string | number; - /** Should we make this selectable with a checkbox */ - shouldShowSelectedState?: boolean; + /** Should the title show with normal font weight (not bold) */ + shouldShowBasicTitle?: boolean; - /** Whether this item is selected */ - isSelected?: boolean; + /** Should we make this selectable with a checkbox */ + shouldShowSelectedState?: boolean; - /** Prop to identify if we should load avatars vertically instead of diagonally */ - shouldStackHorizontally?: boolean; + /** Whether this item is selected */ + isSelected?: boolean; - /** Prop to represent the size of the avatar images to be shown */ - avatarSize?: (typeof CONST.AVATAR_SIZE)[keyof typeof CONST.AVATAR_SIZE]; + /** Prop to identify if we should load avatars vertically instead of diagonally */ + shouldStackHorizontally?: boolean; - /** Avatars to show on the right of the menu item */ - floatRightAvatars?: IconType[]; + /** Prop to represent the size of the avatar images to be shown */ + avatarSize?: (typeof CONST.AVATAR_SIZE)[keyof typeof CONST.AVATAR_SIZE]; - /** Prop to represent the size of the float right avatar images to be shown */ - floatRightAvatarSize?: ValueOf; + /** Avatars to show on the right of the menu item */ + floatRightAvatars?: IconType[]; - /** Affects avatar size */ - viewMode?: ValueOf; + /** Prop to represent the size of the float right avatar images to be shown */ + floatRightAvatarSize?: ValueOf; - /** Used to truncate the text with an ellipsis after computing the text layout */ - numberOfLinesTitle?: number; + /** Affects avatar size */ + viewMode?: ValueOf; - /** Whether we should use small avatar subscript sizing the for menu item */ - isSmallAvatarSubscriptMenu?: boolean; + /** Used to truncate the text with an ellipsis after computing the text layout */ + numberOfLinesTitle?: number; - /** The type of brick road indicator to show. */ - brickRoadIndicator?: ValueOf; + /** Whether we should use small avatar subscript sizing the for menu item */ + isSmallAvatarSubscriptMenu?: boolean; - /** Should render the content in HTML format */ - shouldRenderAsHTML?: boolean; + /** The type of brick road indicator to show. */ + brickRoadIndicator?: ValueOf; - /** Should we grey out the menu item when it is disabled? */ - shouldGreyOutWhenDisabled?: boolean; + /** Should render the content in HTML format */ + shouldRenderAsHTML?: boolean; - /** The action accept for anonymous user or not */ - isAnonymousAction?: boolean; + /** Should we grey out the menu item when it is disabled? */ + shouldGreyOutWhenDisabled?: boolean; - /** Flag to indicate whether or not text selection should be disabled from long-pressing the menu item. */ - shouldBlockSelection?: boolean; + /** The action accept for anonymous user or not */ + isAnonymousAction?: boolean; - /** Whether should render title as HTML or as Text */ - shouldParseTitle?: false; + /** Flag to indicate whether or not text selection should be disabled from long-pressing the menu item. */ + shouldBlockSelection?: boolean; - /** Should check anonymous user in onPress function */ - shouldCheckActionAllowedOnPress?: boolean; + /** Whether should render title as HTML or as Text */ + shouldParseTitle?: false; - /** Text to display under the main item */ - furtherDetails?: string; + /** Should check anonymous user in onPress function */ + shouldCheckActionAllowedOnPress?: boolean; - /** The function that should be called when this component is LongPressed or right-clicked. */ - onSecondaryInteraction?: () => void; + /** Text to display under the main item */ + furtherDetails?: string; - /** Array of objects that map display names to their corresponding tooltip */ - titleWithTooltips?: DisplayNameWithTooltip[]; + /** The function that should be called when this component is LongPressed or right-clicked. */ + onSecondaryInteraction?: (event: GestureResponderEvent | MouseEvent) => void; - /** Icon should be displayed in its own color */ - displayInDefaultIconColor?: boolean; + /** Array of objects that map display names to their corresponding tooltip */ + titleWithTooltips?: DisplayNameWithTooltip[]; - /** Determines how the icon should be resized to fit its container */ - contentFit?: ImageContentFit; - }; + /** Icon should be displayed in its own color */ + displayInDefaultIconColor?: boolean; + + /** Determines how the icon should be resized to fit its container */ + contentFit?: ImageContentFit; +}; function MenuItem( { @@ -525,17 +516,19 @@ function MenuItem( {error} )} - {furtherDetailsIcon && !!furtherDetails && ( + {!!furtherDetails && ( - + {!!furtherDetailsIcon && ( + + )} {furtherDetails} diff --git a/src/components/MenuItemList.js b/src/components/MenuItemList.js deleted file mode 100644 index c9eee8e888e1..000000000000 --- a/src/components/MenuItemList.js +++ /dev/null @@ -1,62 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import _ from 'underscore'; -import useSingleExecution from '@hooks/useSingleExecution'; -import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; -import CONST from '@src/CONST'; -import MenuItem from './MenuItem'; -import menuItemPropTypes from './menuItemPropTypes'; - -const propTypes = { - /** An array of props that are pass to individual MenuItem components */ - menuItems: PropTypes.arrayOf(PropTypes.shape(menuItemPropTypes)), - - /** Whether or not to use the single execution hook */ - shouldUseSingleExecution: PropTypes.bool, -}; -const defaultProps = { - menuItems: [], - shouldUseSingleExecution: false, -}; - -function MenuItemList(props) { - let popoverAnchor; - const {isExecuting, singleExecution} = useSingleExecution(); - - /** - * Handle the secondary interaction for a menu item. - * - * @param {*} link the menu item link or function to get the link - * @param {Event} e the interaction event - */ - const secondaryInteraction = (link, e) => { - if (typeof link === 'function') { - link().then((url) => ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, e, url, popoverAnchor)); - } else if (!_.isEmpty(link)) { - ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, e, link, popoverAnchor); - } - }; - - return ( - <> - {_.map(props.menuItems, (menuItemProps) => ( - secondaryInteraction(menuItemProps.link, e) : undefined} - ref={(el) => (popoverAnchor = el)} - shouldBlockSelection={Boolean(menuItemProps.link)} - // eslint-disable-next-line react/jsx-props-no-spreading - {...menuItemProps} - disabled={menuItemProps.disabled || isExecuting} - onPress={props.shouldUseSingleExecution ? singleExecution(menuItemProps.onPress) : menuItemProps.onPress} - /> - ))} - - ); -} - -MenuItemList.displayName = 'MenuItemList'; -MenuItemList.propTypes = propTypes; -MenuItemList.defaultProps = defaultProps; - -export default MenuItemList; diff --git a/src/components/MenuItemList.tsx b/src/components/MenuItemList.tsx new file mode 100644 index 000000000000..f83f173a644f --- /dev/null +++ b/src/components/MenuItemList.tsx @@ -0,0 +1,63 @@ +import React, {useRef} from 'react'; +import type {GestureResponderEvent, View} from 'react-native'; +import useSingleExecution from '@hooks/useSingleExecution'; +import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import type {MenuItemProps} from './MenuItem'; +import MenuItem from './MenuItem'; + +type MenuItemLink = string | (() => Promise); + +type MenuItemWithLink = MenuItemProps & { + /** The link to open when the menu item is clicked */ + link: MenuItemLink; +}; + +type MenuItemListProps = { + /** An array of props that are pass to individual MenuItem components */ + menuItems: MenuItemWithLink[]; + + /** Whether or not to use the single execution hook */ + shouldUseSingleExecution?: boolean; +}; + +function MenuItemList({menuItems = [], shouldUseSingleExecution = false}: MenuItemListProps) { + const popoverAnchor = useRef(null); + const {isExecuting, singleExecution} = useSingleExecution(); + + /** + * Handle the secondary interaction for a menu item. + * + * @param link the menu item link or function to get the link + * @param event the interaction event + */ + const secondaryInteraction = (link: MenuItemLink, event: GestureResponderEvent | MouseEvent) => { + if (typeof link === 'function') { + link().then((url) => ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, event, url, popoverAnchor.current)); + } else if (link) { + ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, event, link, popoverAnchor.current); + } + }; + + return ( + <> + {menuItems.map((menuItemProps) => ( + secondaryInteraction(menuItemProps.link, e) : undefined} + ref={popoverAnchor} + shouldBlockSelection={!!menuItemProps.link} + // eslint-disable-next-line react/jsx-props-no-spreading + {...menuItemProps} + disabled={!!menuItemProps.disabled || isExecuting} + onPress={shouldUseSingleExecution ? singleExecution(menuItemProps.onPress) : menuItemProps.onPress} + /> + ))} + + ); +} + +MenuItemList.displayName = 'MenuItemList'; + +export type {MenuItemWithLink}; +export default MenuItemList; diff --git a/src/components/MoneyReportHeader.js b/src/components/MoneyReportHeader.js index 8ed6d0746438..5b59fca6cdae 100644 --- a/src/components/MoneyReportHeader.js +++ b/src/components/MoneyReportHeader.js @@ -87,9 +87,9 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); const policyType = lodashGet(policy, 'type'); const isPolicyAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && lodashGet(policy, 'role') === CONST.POLICY.ROLE.ADMIN; - const isGroupPolicy = _.contains([CONST.POLICY.TYPE.CORPORATE, CONST.POLICY.TYPE.TEAM], policyType); + const isPaidGroupPolicy = ReportUtils.isPaidGroupPolicy(moneyRequestReport); const isManager = ReportUtils.isMoneyRequestReport(moneyRequestReport) && lodashGet(session, 'accountID', null) === moneyRequestReport.managerID; - const isPayer = isGroupPolicy + const isPayer = isPaidGroupPolicy ? // In a group policy, the admin approver can pay the report directly by skipping the approval step isPolicyAdmin && (isApproved || isManager) : isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager); @@ -99,11 +99,11 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt [isPayer, isDraft, isSettled, moneyRequestReport, reimbursableTotal, chatReport], ); const shouldShowApproveButton = useMemo(() => { - if (!isGroupPolicy) { + if (!isPaidGroupPolicy) { return false; } return isManager && !isDraft && !isApproved && !isSettled; - }, [isGroupPolicy, isManager, isDraft, isApproved, isSettled]); + }, [isPaidGroupPolicy, isManager, isDraft, isApproved, isSettled]); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; const shouldShowSubmitButton = isDraft && reimbursableTotal !== 0; const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 7ec95aec951f..2fee67a3d632 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -277,7 +277,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const shouldShowTags = isPolicyExpenseChat && OptionsListUtils.hasEnabledOptions(_.values(policyTagList)); // A flag for showing tax rate - const shouldShowTax = isPolicyExpenseChat && policy.isTaxTrackingEnabled; + const shouldShowTax = isPolicyExpenseChat && policy && policy.isTaxTrackingEnabled; // A flag for showing the billable field const shouldShowBillable = !lodashGet(policy, 'disabledFields.defaultBillable', true); diff --git a/src/components/Onfido/BaseOnfidoWeb.js b/src/components/Onfido/BaseOnfidoWeb.js index 09ec96cf5b1e..ee206b15fc24 100644 --- a/src/components/Onfido/BaseOnfidoWeb.js +++ b/src/components/Onfido/BaseOnfidoWeb.js @@ -5,8 +5,7 @@ import _ from 'underscore'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import Log from '@libs/Log'; -import fontFamily from '@styles/utils/fontFamily'; -import fontWeightBold from '@styles/utils/fontWeight/bold'; +import FontUtils from '@styles/utils/FontUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import './index.css'; @@ -18,11 +17,11 @@ function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLo containerId: CONST.ONFIDO.CONTAINER_ID, useMemoryHistory: true, customUI: { - fontFamilyTitle: `${fontFamily.EXP_NEUE}, -apple-system, serif`, - fontFamilySubtitle: `${fontFamily.EXP_NEUE}, -apple-system, serif`, - fontFamilyBody: `${fontFamily.EXP_NEUE}, -apple-system, serif`, + fontFamilyTitle: `${FontUtils.fontFamily.platform.EXP_NEUE}, -apple-system, serif`, + fontFamilySubtitle: `${FontUtils.fontFamily.platform.EXP_NEUE}, -apple-system, serif`, + fontFamilyBody: `${FontUtils.fontFamily.platform.EXP_NEUE}, -apple-system, serif`, fontSizeTitle: `${variables.fontSizeLarge}px`, - fontWeightTitle: fontWeightBold, + fontWeightTitle: FontUtils.fontWeight.bold, fontWeightSubtitle: 400, fontSizeSubtitle: `${variables.fontSizeNormal}px`, colorContentTitle: theme.text, diff --git a/src/components/PopoverWithMeasuredContent.tsx b/src/components/PopoverWithMeasuredContent.tsx index b636f90cd7ca..792002441ac6 100644 --- a/src/components/PopoverWithMeasuredContent.tsx +++ b/src/components/PopoverWithMeasuredContent.tsx @@ -36,6 +36,12 @@ function PopoverWithMeasuredContent({ }, children, withoutOverlay = false, + fullscreen = true, + shouldCloseOnOutsideClick = false, + shouldSetModalVisibility = true, + statusBarTranslucent = true, + avoidKeyboard = false, + hideModalContentWhileAnimating = false, ...props }: PopoverWithMeasuredContentProps) { const styles = useThemeStyles(); @@ -115,6 +121,12 @@ function PopoverWithMeasuredContent({ anchorAlignment={anchorAlignment} isVisible={isVisible} withoutOverlay={withoutOverlay} + fullscreen={fullscreen} + shouldCloseOnOutsideClick={shouldCloseOnOutsideClick} + shouldSetModalVisibility={shouldSetModalVisibility} + statusBarTranslucent={statusBarTranslucent} + avoidKeyboard={avoidKeyboard} + hideModalContentWhileAnimating={hideModalContentWhileAnimating} // eslint-disable-next-line react/jsx-props-no-spreading {...props} anchorPosition={shiftedAnchorPosition} diff --git a/src/components/ProcessMoneyRequestHoldMenu.tsx b/src/components/ProcessMoneyRequestHoldMenu.tsx new file mode 100644 index 000000000000..1b711633ed3b --- /dev/null +++ b/src/components/ProcessMoneyRequestHoldMenu.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import {View} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Button from './Button'; +import HoldMenuSectionList from './HoldMenuSectionList'; +import type {PopoverAnchorPosition} from './Modal/types'; +import Popover from './Popover'; +import type {AnchorAlignment} from './Popover/types'; +import Text from './Text'; +import TextPill from './TextPill'; + +type ProcessMoneyRequestHoldMenuProps = { + /** Whether the content is visible */ + isVisible: boolean; + + /** Method to trigger when pressing outside of the popover menu to close it */ + onClose: () => void; + + /** Method to trigger when pressing confirm button */ + onConfirm: () => void; + + /** The anchor position of the popover menu */ + anchorPosition?: PopoverAnchorPosition; + + /** The anchor alignment of the popover menu */ + anchorAlignment: AnchorAlignment; + + /** The anchor ref of the popover menu */ + anchorRef: React.RefObject; +}; + +function ProcessMoneyRequestHoldMenu({isVisible, onClose, onConfirm, anchorPosition, anchorAlignment, anchorRef}: ProcessMoneyRequestHoldMenuProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + return ( + + + + {translate('iou.holdEducationalTitle')} + {translate('iou.hold')}; + + +