From 71e6a2fd7d2f3489d9531246718ffae07e6d14ab Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 25 Sep 2023 16:13:49 +0200 Subject: [PATCH 001/344] ref: move LocalNotification to TS --- ...tifications.js => BrowserNotifications.ts} | 44 ++++++------------- .../{index.desktop.js => index.desktop.ts} | 5 ++- .../LocalNotification/focusApp/index.ts | 6 +++ .../focusApp/index.website.js | 2 - .../LocalNotification/focusApp/types.ts | 3 ++ .../{index.desktop.js => index.desktop.ts} | 8 ++-- .../{index.native.js => index.native.ts} | 0 .../{index.website.js => index.ts} | 7 ++- .../Notification/LocalNotification/types.ts | 24 ++++++++++ 9 files changed, 61 insertions(+), 38 deletions(-) rename src/libs/Notification/LocalNotification/{BrowserNotifications.js => BrowserNotifications.ts} (77%) rename src/libs/Notification/LocalNotification/focusApp/{index.desktop.js => index.desktop.ts} (59%) create mode 100644 src/libs/Notification/LocalNotification/focusApp/index.ts delete mode 100644 src/libs/Notification/LocalNotification/focusApp/index.website.js create mode 100644 src/libs/Notification/LocalNotification/focusApp/types.ts rename src/libs/Notification/LocalNotification/{index.desktop.js => index.desktop.ts} (57%) rename src/libs/Notification/LocalNotification/{index.native.js => index.native.ts} (100%) rename src/libs/Notification/LocalNotification/{index.website.js => index.ts} (68%) create mode 100644 src/libs/Notification/LocalNotification/types.ts diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.js b/src/libs/Notification/LocalNotification/BrowserNotifications.ts similarity index 77% rename from src/libs/Notification/LocalNotification/BrowserNotifications.js rename to src/libs/Notification/LocalNotification/BrowserNotifications.ts index e55c0430fe17..fcf795c6769c 100644 --- a/src/libs/Notification/LocalNotification/BrowserNotifications.js +++ b/src/libs/Notification/LocalNotification/BrowserNotifications.ts @@ -1,18 +1,16 @@ // Web and desktop implementation only. Do not import for direct use. Use LocalNotification. -import _ from 'underscore'; -import focusApp from './focusApp'; import * as AppUpdate from '../../actions/AppUpdate'; import EXPENSIFY_ICON_URL from '../../../../assets/images/expensify-logo-round-clearspace.png'; import * as ReportUtils from '../../ReportUtils'; +import {PushParams, ReportCommentParams} from './types'; +import focusApp from './focusApp'; const DEFAULT_DELAY = 4000; /** * Checks if the user has granted permission to show browser notifications - * - * @return {Promise} */ -function canUseBrowserNotifications() { +function canUseBrowserNotifications(): Promise { return new Promise((resolve) => { // They have no browser notifications so we can't use this feature if (!window.Notification) { @@ -36,18 +34,9 @@ function canUseBrowserNotifications() { /** * Light abstraction around browser push notifications. * Checks for permission before determining whether to send. - * - * @param {Object} params - * @param {String} params.title - * @param {String} params.body - * @param {String} [params.icon] Path to icon - * @param {Number} [params.delay] - * @param {Function} [params.onClick] - * @param {String} [params.tag] - * - * @return {Promise} - resolves with Notification object or undefined + * @return resolves with Notification object or undefined */ -function push({title, body, delay = DEFAULT_DELAY, onClick = () => {}, tag = '', icon}) { +function push({title, body, delay = DEFAULT_DELAY, onClick = () => {}, tag = '', icon}: PushParams): Promise { return new Promise((resolve) => { if (!title || !body) { throw new Error('BrowserNotification must include title and body parameter.'); @@ -62,7 +51,7 @@ function push({title, body, delay = DEFAULT_DELAY, onClick = () => {}, tag = '', const notification = new Notification(title, { body, tag, - icon, + icon: String(icon), }); // If we pass in a delay param greater than 0 the notification @@ -91,27 +80,22 @@ function push({title, body, delay = DEFAULT_DELAY, onClick = () => {}, tag = '', export default { /** * Create a report comment notification - * - * @param {Object} params - * @param {Object} params.report - * @param {Object} params.reportAction - * @param {Function} params.onClick - * @param {Boolean} usesIcon true if notification uses right circular icon + * @param usesIcon true if notification uses right circular icon */ - pushReportCommentNotification({report, reportAction, onClick}, usesIcon = false) { - let title; - let body; + pushReportCommentNotification({report, reportAction, onClick}: ReportCommentParams, usesIcon = false) { + let title: string | undefined; + let body: string | undefined; const isChatRoom = ReportUtils.isChatRoom(report); const {person, message} = reportAction; - const plainTextPerson = _.map(person, (f) => f.text).join(); + const plainTextPerson = person?.map((f) => f.text).join(); // Specifically target the comment part of the message - const plainTextMessage = (_.find(message, (f) => f.type === 'COMMENT') || {}).text; + const plainTextMessage = message?.find((f) => f.type === 'COMMENT')?.text; if (isChatRoom) { - const roomName = _.get(report, 'displayName', ''); + const roomName = report.displayName ?? ''; title = roomName; body = `${plainTextPerson}: ${plainTextMessage}`; } else { @@ -120,7 +104,7 @@ export default { } push({ - title, + title: title ?? '', body, delay: 0, onClick, diff --git a/src/libs/Notification/LocalNotification/focusApp/index.desktop.js b/src/libs/Notification/LocalNotification/focusApp/index.desktop.ts similarity index 59% rename from src/libs/Notification/LocalNotification/focusApp/index.desktop.js rename to src/libs/Notification/LocalNotification/focusApp/index.desktop.ts index f00c72dc1af8..2b6f01457f44 100644 --- a/src/libs/Notification/LocalNotification/focusApp/index.desktop.js +++ b/src/libs/Notification/LocalNotification/focusApp/index.desktop.ts @@ -1,5 +1,8 @@ import ELECTRON_EVENTS from '../../../../../desktop/ELECTRON_EVENTS'; +import FocusApp from './types'; -export default () => { +const focusApp: FocusApp = () => { window.electron.send(ELECTRON_EVENTS.REQUEST_FOCUS_APP); }; + +export default focusApp; diff --git a/src/libs/Notification/LocalNotification/focusApp/index.ts b/src/libs/Notification/LocalNotification/focusApp/index.ts new file mode 100644 index 000000000000..8504fde1bcb2 --- /dev/null +++ b/src/libs/Notification/LocalNotification/focusApp/index.ts @@ -0,0 +1,6 @@ +import FocusApp from './types'; + +// On web this is up to the browser that shows the notifications +const focusApp: FocusApp = () => {}; + +export default focusApp; diff --git a/src/libs/Notification/LocalNotification/focusApp/index.website.js b/src/libs/Notification/LocalNotification/focusApp/index.website.js deleted file mode 100644 index ce7439d94cc3..000000000000 --- a/src/libs/Notification/LocalNotification/focusApp/index.website.js +++ /dev/null @@ -1,2 +0,0 @@ -// On web this is up to the browser that shows the notifications -export default () => {}; diff --git a/src/libs/Notification/LocalNotification/focusApp/types.ts b/src/libs/Notification/LocalNotification/focusApp/types.ts new file mode 100644 index 000000000000..534b7500fc71 --- /dev/null +++ b/src/libs/Notification/LocalNotification/focusApp/types.ts @@ -0,0 +1,3 @@ +type FocusApp = () => void; + +export default FocusApp; diff --git a/src/libs/Notification/LocalNotification/index.desktop.js b/src/libs/Notification/LocalNotification/index.desktop.ts similarity index 57% rename from src/libs/Notification/LocalNotification/index.desktop.js rename to src/libs/Notification/LocalNotification/index.desktop.ts index 2bef51cea0a6..acc3b8857c0f 100644 --- a/src/libs/Notification/LocalNotification/index.desktop.js +++ b/src/libs/Notification/LocalNotification/index.desktop.ts @@ -1,6 +1,7 @@ -import BrowserNotifications from './BrowserNotifications'; +import BrowserNotifications from './BrowserNotifications.js'; +import {LocalNotificationModule, ReportCommentParams} from './types.js'; -function showCommentNotification({report, reportAction, onClick}) { +function showCommentNotification({report, reportAction, onClick}: ReportCommentParams) { BrowserNotifications.pushReportCommentNotification({report, reportAction, onClick}); } @@ -8,7 +9,8 @@ function showUpdateAvailableNotification() { BrowserNotifications.pushUpdateAvailableNotification(); } -export default { +const LocalNotification: LocalNotificationModule = { showCommentNotification, showUpdateAvailableNotification, }; +export default LocalNotification; diff --git a/src/libs/Notification/LocalNotification/index.native.js b/src/libs/Notification/LocalNotification/index.native.ts similarity index 100% rename from src/libs/Notification/LocalNotification/index.native.js rename to src/libs/Notification/LocalNotification/index.native.ts diff --git a/src/libs/Notification/LocalNotification/index.website.js b/src/libs/Notification/LocalNotification/index.ts similarity index 68% rename from src/libs/Notification/LocalNotification/index.website.js rename to src/libs/Notification/LocalNotification/index.ts index 3410b3144caf..c0182d156d46 100644 --- a/src/libs/Notification/LocalNotification/index.website.js +++ b/src/libs/Notification/LocalNotification/index.ts @@ -1,6 +1,7 @@ import BrowserNotifications from './BrowserNotifications'; +import {LocalNotificationModule, ReportCommentParams} from './types'; -function showCommentNotification({report, reportAction, onClick}) { +function showCommentNotification({report, reportAction, onClick}: ReportCommentParams) { BrowserNotifications.pushReportCommentNotification({report, reportAction, onClick}, true); } @@ -8,7 +9,9 @@ function showUpdateAvailableNotification() { BrowserNotifications.pushUpdateAvailableNotification(); } -export default { +const LocalNotification: LocalNotificationModule = { showCommentNotification, showUpdateAvailableNotification, }; + +export default LocalNotification; diff --git a/src/libs/Notification/LocalNotification/types.ts b/src/libs/Notification/LocalNotification/types.ts new file mode 100644 index 000000000000..576297467155 --- /dev/null +++ b/src/libs/Notification/LocalNotification/types.ts @@ -0,0 +1,24 @@ +import {ImageSourcePropType} from 'react-native'; +import {ReportAction, Report} from '../../../types/onyx'; + +type PushParams = { + title: string; + body?: string; + icon?: string | ImageSourcePropType; + delay?: number; + onClick?: () => void; + tag?: string; +}; + +type ReportCommentParams = { + report: Report; + reportAction: ReportAction; + onClick: () => void; +}; + +type LocalNotificationModule = { + showCommentNotification: (reportCommentParams: ReportCommentParams) => void; + showUpdateAvailableNotification: () => void; +}; + +export type {PushParams, ReportCommentParams, LocalNotificationModule}; From 0354131250232a1bcffa04acf90cdf6d0b3f890e Mon Sep 17 00:00:00 2001 From: miljakljajic Date: Fri, 29 Sep 2023 16:08:36 +0800 Subject: [PATCH 002/344] Update Uber.md Updating the Uber integration page --- .../integrations/travel-integrations/Uber.md | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md b/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md index 3ee1c8656b4b..b88cbc0aca39 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md @@ -1,5 +1,26 @@ --- -title: Coming Soon -description: Coming Soon +Uber integration +Connecting your Uber account to Expensify --- -## Resource Coming Soon! +## Overview + +Link Expensify directly to your Uber account so your Uber for Business receipts populate automatically in Expensify. + +# How to connect Uber to Expensify + +You can do this right in the Uber app: + +1. Head to Account > Business hub > Get started +2. Tap Create an individual account > Get started +3. Enter your business email and tap Next +4. Select the payment card you'd like to use for your business profile +5. Choose how frequently you’d like to receive travel summaries +6. Select Expensify as your expense provider +Expensify and Uber are now connected! + +Now, every time you use Uber for Business – be it for rides or meals – the receipt will be imported and scanned into Expensify automatically. + +![Alt text for accessibility] +(https://help.expensify.com/assets/images/Uber1.png){:width="100%"} +![Alt text for accessibility] +(https://help.expensify.com/assets/images/Uber2.png){:width="100%"} From ebaea0f0a39ff5750d1ef077748e49013462fd30 Mon Sep 17 00:00:00 2001 From: miljakljajic Date: Tue, 3 Oct 2023 10:59:43 +0200 Subject: [PATCH 003/344] Update docs/articles/expensify-classic/integrations/travel-integrations/Uber.md Co-authored-by: Rushat Gabhane --- .../expensify-classic/integrations/travel-integrations/Uber.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md b/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md index b88cbc0aca39..52b46a631367 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md @@ -1,5 +1,5 @@ --- -Uber integration +title: Uber integration Connecting your Uber account to Expensify --- ## Overview From b88fc1fe768e3aea1aac6d31d8f919b3d417be81 Mon Sep 17 00:00:00 2001 From: miljakljajic Date: Tue, 3 Oct 2023 10:59:50 +0200 Subject: [PATCH 004/344] Update docs/articles/expensify-classic/integrations/travel-integrations/Uber.md Co-authored-by: Rushat Gabhane --- .../expensify-classic/integrations/travel-integrations/Uber.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md b/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md index 52b46a631367..c4f2ed78f2cd 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md @@ -1,6 +1,6 @@ --- title: Uber integration -Connecting your Uber account to Expensify +description: Connecting your Uber account to Expensify --- ## Overview From 5bbde269e9a4d8a16c67558bc5dcde8cea007f05 Mon Sep 17 00:00:00 2001 From: miljakljajic Date: Wed, 4 Oct 2023 10:11:44 +0200 Subject: [PATCH 005/344] Update Uber.md Made the suggested updates to the accessibility description --- .../integrations/travel-integrations/Uber.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md b/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md index c4f2ed78f2cd..73ad2c050cd4 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md @@ -20,7 +20,7 @@ Expensify and Uber are now connected! Now, every time you use Uber for Business – be it for rides or meals – the receipt will be imported and scanned into Expensify automatically. -![Alt text for accessibility] +![Uber integration set up steps: Connecting your account] (https://help.expensify.com/assets/images/Uber1.png){:width="100%"} -![Alt text for accessibility] +![Uber integration set up steps: Selecting Expensify] (https://help.expensify.com/assets/images/Uber2.png){:width="100%"} From 400156a0a29e04433fe830bab235a6c08f0e3fe7 Mon Sep 17 00:00:00 2001 From: Nikki Wines Date: Fri, 13 Oct 2023 11:57:50 +0900 Subject: [PATCH 006/344] add native view for SAML SignInPage --- .../signin/SAMLSignInPage/index.native.js | 44 +++++++++++++++++++ src/pages/signin/SignInPage.js | 12 ++--- 2 files changed, 47 insertions(+), 9 deletions(-) create mode 100644 src/pages/signin/SAMLSignInPage/index.native.js diff --git a/src/pages/signin/SAMLSignInPage/index.native.js b/src/pages/signin/SAMLSignInPage/index.native.js new file mode 100644 index 000000000000..c4b0ae78bc30 --- /dev/null +++ b/src/pages/signin/SAMLSignInPage/index.native.js @@ -0,0 +1,44 @@ +import React, {useEffect, useRef} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import PropTypes from 'prop-types'; +import ONYXKEYS from '../../../ONYXKEYS'; +import CONFIG from '../../../CONFIG'; +import WebView from 'react-native-webview'; +import FullScreenLoadingIndicator from '../../../components/FullscreenLoadingIndicator'; + +const propTypes = { + /** The credentials of the logged in person */ + credentials: PropTypes.shape({ + /** The email/phone the user logged in with */ + login: PropTypes.string, + }), +}; + +const defaultProps = { + credentials: {}, +}; + +const renderLoading = () => ; + +function SAMLSignInPage({credentials}) { + const webViewRef = useRef(); + const samlLoginURL = `${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`; + return ( + {console.log("meep meep navigation changed: " + JSON.stringify(ref))}} + /> + ); +} + +SAMLSignInPage.propTypes = propTypes; +SAMLSignInPage.defaultProps = defaultProps; + +export default withOnyx({ + credentials: {key: ONYXKEYS.CREDENTIALS}, +})(SAMLSignInPage); diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js index 7da41e2ab474..81abbdb9d1ae 100644 --- a/src/pages/signin/SignInPage.js +++ b/src/pages/signin/SignInPage.js @@ -96,15 +96,9 @@ function getRenderOptions({hasLogin, hasValidateCode, account, isPrimaryLogin, i const isSAMLRequired = Boolean(account.isSAMLRequired); const hasEmailDeliveryFailure = Boolean(account.hasEmailDeliveryFailure); - // SAML is temporarily restricted to users on the beta or to users signing in on web and mweb - let shouldShowChooseSSOOrMagicCode = false; - let shouldInitiateSAMLLogin = false; - const platform = getPlatform(); - if (Permissions.canUseSAML() || platform === CONST.PLATFORM.WEB || platform === CONST.PLATFORM.DESKTOP) { - // True if the user has SAML required and we're not already loading their account - shouldInitiateSAMLLogin = hasAccount && hasLogin && isSAMLRequired && !hasInitiatedSAMLLogin && account.isLoading; - shouldShowChooseSSOOrMagicCode = hasAccount && hasLogin && isSAMLEnabled && !isSAMLRequired && !isUsingMagicCode; - } + // True if the user has SAML required and we're not already loading their account + const shouldInitiateSAMLLogin = hasAccount && hasLogin && isSAMLRequired && !hasInitiatedSAMLLogin && account.isLoading; + const shouldShowChooseSSOOrMagicCode = hasAccount && hasLogin && isSAMLEnabled && !isSAMLRequired && !isUsingMagicCode; // SAML required users may reload the login page after having already entered their login details, in which // case we want to clear their sign in data so they don't end up in an infinite loop redirecting back to their From 8991e7ef264595e150ed631c8e733291ec213be1 Mon Sep 17 00:00:00 2001 From: Nikki Wines Date: Fri, 13 Oct 2023 20:18:43 +0900 Subject: [PATCH 007/344] log in with shortlivedtoken when one is returned --- .../signin/SAMLSignInPage/index.native.js | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/pages/signin/SAMLSignInPage/index.native.js b/src/pages/signin/SAMLSignInPage/index.native.js index c4b0ae78bc30..3825cca0fa97 100644 --- a/src/pages/signin/SAMLSignInPage/index.native.js +++ b/src/pages/signin/SAMLSignInPage/index.native.js @@ -1,10 +1,11 @@ -import React, {useEffect, useRef} from 'react'; +import React, {useCallback, useRef} from 'react'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; import ONYXKEYS from '../../../ONYXKEYS'; import CONFIG from '../../../CONFIG'; import WebView from 'react-native-webview'; import FullScreenLoadingIndicator from '../../../components/FullscreenLoadingIndicator'; +import * as Session from '../../../libs/actions/Session'; const propTypes = { /** The credentials of the logged in person */ @@ -23,6 +24,24 @@ const renderLoading = () => ; function SAMLSignInPage({credentials}) { const webViewRef = useRef(); const samlLoginURL = `${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`; + + /** + * Handles in-app navigation once we get a response back from Expensify + * + * @param {String} params.type + * @param {String} params.url + */ + const handleNavigationStateChange = useCallback( + ({type, url}) => { + const searchParams = new URLSearchParams(new URL(url).search); + if (searchParams.has('shortLivedAuthToken')) { + const shortLivedAuthToken = searchParams.get('shortLivedAuthToken'); + Session.signInWithShortLivedAuthToken(credentials.login, shortLivedAuthToken); + return; + } + }, + [webViewRef], + ); return ( {console.log("meep meep navigation changed: " + JSON.stringify(ref))}} + onNavigationStateChange={handleNavigationStateChange} /> ); } From 8386979f78ee4b0e3e3e6ebb6c315f1b3e871106 Mon Sep 17 00:00:00 2001 From: David Cardoza Date: Fri, 20 Oct 2023 13:42:47 +0800 Subject: [PATCH 008/344] Create Partner-Billing-Guide https://github.com/Expensify/Expensify/issues/318124 --- .../Partner-Billing-Guide | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 docs/articles/expensify-classic/expensify-partner-program/Partner-Billing-Guide diff --git a/docs/articles/expensify-classic/expensify-partner-program/Partner-Billing-Guide b/docs/articles/expensify-classic/expensify-partner-program/Partner-Billing-Guide new file mode 100644 index 000000000000..f87cdc974cc7 --- /dev/null +++ b/docs/articles/expensify-classic/expensify-partner-program/Partner-Billing-Guide @@ -0,0 +1,89 @@ +--- +title: Partner Billing Guide +description: Understand how Expensify bills partners and their clients +--- + +# Overview + +The ExpensifyApproved! Partner Program offers exclusive billing rates and features tailored for accountants to ensure seamless client management. If you are an accountant or consultant who recommends spend management solutions to your clients, becoming an ExpensifyApproved! Accountant may be a great certification for you. This guide will walk you through the unique billing perks available to ExpensifyApproved! partners, emphasizing the importance of understanding and managing client billing effectively. To learn what perks partners receive, check out the ExpensifyApproved! program details here. + +# Get exclusive partner pricing + +All ExpensifyApproved! Partners are automatically eligible for a special rate of $9/seat monthly, without an annual commitment when they adopt the Expensify Card. This provides flexibility as active users can vary. Here are the specifics on pricing for our Approved! Partners’ clients: +- **Bundled pricing:** US Clients using the Expensify Card get up to a 50% discount, bringing their monthly bill to $9/seat. Reach out to your Partner Manager to discuss exclusive international pricing discount.s +- **Unbundled pricing (or pay-per-use pricing):** Clients not using the Expensify Card are billed at $18/seat monthly. +- **No annual commitment:** Partners pay only for what they use, with no annual subscriptions required. +- **Expensify Card revenue share:** All partners receive a 0.5% revenue share on Expensify Card transactions made by clients. This revenue share can be passed back to the client for an additional discount to offset their Expensify bill + +# Understanding the billing process + +Expensify bills the owner of the expense policy for the activity on that policy. If accountants retain ownership of client policies, they receive the bill and can then re-bill their clients based on individual agreements. + +Each month, Expensify will send a consolidated bill detailing: +- **Pay-per-use seats:** This is the number of active clients and their users for the month. +- **Expensify Card discount**: This amount reflects how much spend is put on your card, which then determines the discount for that month. +- **Total monthly price:** This amount is the overall price of Expensify when using the Expensify Card discount to offset the cost of the pay-per-use seats +- **Policy list:** This is an overview of all client policies with their respective active seats. + +## Consolidated Domain Billing + +If your firm wishes to consolidate all Expensify billing to a single account, the Consolidated Domain Billing feature is your go-to tool. It centralizes payment for all group policies owned by any domain member of your firm. + +### Activating Consolidated Domain Billing: + 1. Claim and verify your firm’s domain. + 2. Navigate to **Settings > Domains > [Domain Name] > Domain Admins** and set a **"Primary Domain Admin"** by using the drop down toggle to select an email address. + 3. Enable **Consolidated Domain Billing** in the same section. + +The Consolidated Domain Billing tool ensures that billing takes place under a single Expensify account associated with your firm. This eliminates the need to add multiple payment cards across various accounts to cover payments for multiple clients. + +## Maintaining a Console of all clients: + +If your firm wants to have a console view of all client policies and domains, you will want to create a single, centralized login to manage all client policies and domains, such as accounting@myfirm.com. + + 1. Create a dedicated email address that will act as the universal policy owner, for example, accounting@myfirm.com. + 2. Register this email with Expensify or your chosen platform and ensure it is verified and secured. + 3. Within each client policy settings, add your centralized email (e.g. accounting@myfirm.com) as a policy admin. + 4. Do the same with each client domain. + + +## Applying Client IDs to a bill + +Using client IDs for Optimized Billing in Expensify: A unique identifier feature for ExpensifyApproved! accountants. Streamline client Workspace recognition and make your billing process more efficient. + +# How to assign a client ID to a workspace + 1. Log in to your account: Ensure you’re using an Approved! accountant account. + 2. Navigate to the desired Workspace: Go to **Settings > Workspaces > [Workspace Name] > Overview**. + 3. Input the identifier: Here, you can input an alphanumeric unique identifier for each client workspace. +**Note:** If a client has multiple workspaces, ensure each workspace has a consistent client ID. + +# How to access and download billing receipts +Accessing Billing **Settings: Go to Settings > Your Account > Payments > Billing History.** +Download the Receipt: Click on **"Download Receipt CSV".** + + +# Deep Dive +Using client IDs for all Workspaces: It's beneficial to use client IDs for all Workspaces to ensure each one is easily recognizable. +Benefits of itemized billing receipts: Employing client IDs offers itemized billing by client, with each itemization detailing unique active users. + +# FAQ + +**Do I automatically get the special billing rate as an ExpensifyApproved! Partner?** +- Yes, when you join the ExpensifyApproved! program, you will automatically get the special billing rate. To join the ExpensifyApproved! Program, you need to enroll in ExpensifyApproved! University. + +**How can I check my billing details?** +- To check your billing details, be on the lookout for your detailed billing statement sent out at the end of each month. + +**Can I pass the bill to my clients?** +- Yes, you can pass the bill on to your clients. If you retain ownership of client policies in Expensify, you can re-bill your clients. If you’d like the client to own the billing of the Expensify, they can take over billing. + +**What if I don't want to use the Expensify Card?** +- If you prefer not to use the Expensify Card, your clients will be billed at $18/seat monthly. + +**Why use client IDs?** +- Client IDs provide a streamlined method to identify and manage policies, especially beneficial when a client has multiple policies. + +**Do I need a client ID for each Workspace?** +- Yes, if you want to ensure seamless identification and billing processes. + +** Where can I see the Billing Receipts?** +- All billing owners receive an emailed PDF of their monthly billing receipt, but a CSV version can also be downloaded from the platform. From b0c9b955e8df53f51281b405aa235fb804c2a92b Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Sun, 22 Oct 2023 00:40:27 +0500 Subject: [PATCH 009/344] fix: unread logic --- src/libs/ReportUtils.js | 10 ++++++++++ src/libs/UnreadIndicatorUpdater/index.js | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 75ee6257caab..7b55903bedf4 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -2992,6 +2992,15 @@ function buildTransactionThread(reportAction, moneyRequestReportID) { ); } +/** + * @param {Object} report + * @returns {Boolean} + */ +function isEmptyReport(report) { + const lastVisibleMessage = ReportActionsUtils.getLastVisibleMessage(report.reportID); + return !report.lastMessageText && !report.lastMessageTranslationKey && !lastVisibleMessage.lastMessageText && !lastVisibleMessage.lastMessageTranslationKey; +} + /** * @param {Object} report * @returns {Boolean} @@ -4000,6 +4009,7 @@ export { navigateToDetailsPage, generateReportID, hasReportNameError, + isEmptyReport, isUnread, isUnreadWithMention, buildOptimisticWorkspaceChats, diff --git a/src/libs/UnreadIndicatorUpdater/index.js b/src/libs/UnreadIndicatorUpdater/index.js index 09fa82612314..983fc57c511c 100644 --- a/src/libs/UnreadIndicatorUpdater/index.js +++ b/src/libs/UnreadIndicatorUpdater/index.js @@ -8,7 +8,7 @@ Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, waitForCollectionCallback: true, callback: (reportsFromOnyx) => { - const unreadReports = _.filter(reportsFromOnyx, ReportUtils.isUnread); + const unreadReports = _.filter(reportsFromOnyx, (report) => ReportUtils.unread(report) && ReportUtils.isEmptyReport(report)); updateUnread(_.size(unreadReports)); }, }); From 90c915b71daf148b34c3b8ea2282c7fbcff3de3c Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Sun, 22 Oct 2023 02:08:23 +0500 Subject: [PATCH 010/344] fix: empty condition --- src/libs/UnreadIndicatorUpdater/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/UnreadIndicatorUpdater/index.js b/src/libs/UnreadIndicatorUpdater/index.js index 983fc57c511c..be11b3cc3a62 100644 --- a/src/libs/UnreadIndicatorUpdater/index.js +++ b/src/libs/UnreadIndicatorUpdater/index.js @@ -8,7 +8,7 @@ Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, waitForCollectionCallback: true, callback: (reportsFromOnyx) => { - const unreadReports = _.filter(reportsFromOnyx, (report) => ReportUtils.unread(report) && ReportUtils.isEmptyReport(report)); + const unreadReports = _.filter(reportsFromOnyx, (report) => ReportUtils.isUnread(report) && !ReportUtils.isEmptyReport(report)); updateUnread(_.size(unreadReports)); }, }); From 15294bed03d9df1331630eaeb4cc02a6bbef2aaa Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Mon, 23 Oct 2023 01:47:50 +0500 Subject: [PATCH 011/344] feat: mimic lhn --- src/libs/UnreadIndicatorUpdater/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/UnreadIndicatorUpdater/index.js b/src/libs/UnreadIndicatorUpdater/index.js index be11b3cc3a62..a2c523d082f3 100644 --- a/src/libs/UnreadIndicatorUpdater/index.js +++ b/src/libs/UnreadIndicatorUpdater/index.js @@ -1,5 +1,6 @@ import _ from 'underscore'; import Onyx from 'react-native-onyx'; +import Navigation from '../Navigation/Navigation'; import ONYXKEYS from '../../ONYXKEYS'; import updateUnread from './updateUnread/index'; import * as ReportUtils from '../ReportUtils'; @@ -8,7 +9,7 @@ Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, waitForCollectionCallback: true, callback: (reportsFromOnyx) => { - const unreadReports = _.filter(reportsFromOnyx, (report) => ReportUtils.isUnread(report) && !ReportUtils.isEmptyReport(report)); + const unreadReports = _.filter(reportsFromOnyx, (report) => ReportUtils.isUnread(report) && ReportUtils.shouldReportBeInOptionList(report, Navigation.getTopmostReportId())); updateUnread(_.size(unreadReports)); }, }); From 024ab748dbc7d4371866ee1070a99193199551ec Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 23 Oct 2023 09:45:06 +0700 Subject: [PATCH 012/344] fix: 30062 --- src/pages/home/ReportScreen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 81000c2dab92..9b1a72f90b9f 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -317,7 +317,7 @@ function ReportScreen({ prevOnyxReportID === routeReportID && !onyxReportID && prevReport.statusNum === CONST.REPORT.STATUS.OPEN && - (report.statusNum === CONST.REPORT.STATUS.CLOSED || (!report.statusNum && !prevReport.parentReportID))) + (report.statusNum === CONST.REPORT.STATUS.CLOSED || (!report.statusNum && !prevReport.parentReportID && prevReport.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ROOM))) ) { Navigation.dismissModal(); if (Navigation.getTopmostReportId() === prevOnyxReportID) { From 0168e72eb1427af2bcc9af8c3f36f208e9e70c4a Mon Sep 17 00:00:00 2001 From: Nikki Wines Date: Mon, 23 Oct 2023 11:58:50 -0700 Subject: [PATCH 013/344] include const --- src/pages/signin/SignInPage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js index dd43970d5412..3ccd2061d794 100644 --- a/src/pages/signin/SignInPage.js +++ b/src/pages/signin/SignInPage.js @@ -100,8 +100,8 @@ function getRenderOptions({hasLogin, hasValidateCode, account, isPrimaryLogin, i const hasEmailDeliveryFailure = Boolean(account.hasEmailDeliveryFailure); // True if the user has SAML required and we haven't already initiated SAML for their account - shouldInitiateSAMLLogin = hasAccount && hasLogin && isSAMLRequired && !hasInitiatedSAMLLogin && account.isLoading; - shouldShowChooseSSOOrMagicCode = hasAccount && hasLogin && isSAMLEnabled && !isSAMLRequired && !isUsingMagicCode; + const shouldInitiateSAMLLogin = hasAccount && hasLogin && isSAMLRequired && !hasInitiatedSAMLLogin && account.isLoading; + const shouldShowChooseSSOOrMagicCode = hasAccount && hasLogin && isSAMLEnabled && !isSAMLRequired && !isUsingMagicCode; // SAML required users may reload the login page after having already entered their login details, in which // case we want to clear their sign in data so they don't end up in an infinite loop redirecting back to their From 9e4b7ae1ba30fe91d62878d4168579b83061b4ea Mon Sep 17 00:00:00 2001 From: Nikki Wines Date: Mon, 23 Oct 2023 18:33:04 -0700 Subject: [PATCH 014/344] style and clean up --- src/pages/signin/SAMLSignInPage/index.native.js | 11 +++++------ src/pages/signin/SignInPage.js | 1 - 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/pages/signin/SAMLSignInPage/index.native.js b/src/pages/signin/SAMLSignInPage/index.native.js index 3825cca0fa97..4b4624b56cde 100644 --- a/src/pages/signin/SAMLSignInPage/index.native.js +++ b/src/pages/signin/SAMLSignInPage/index.native.js @@ -1,11 +1,12 @@ import React, {useCallback, useRef} from 'react'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; +import WebView from 'react-native-webview'; import ONYXKEYS from '../../../ONYXKEYS'; import CONFIG from '../../../CONFIG'; -import WebView from 'react-native-webview'; -import FullScreenLoadingIndicator from '../../../components/FullscreenLoadingIndicator'; import * as Session from '../../../libs/actions/Session'; +import SAMLLoadingIndicator from '../../../components/SAMLLoadingIndicator'; +import FullScreenLoadingIndicator from '../../../components/FullscreenLoadingIndicator'; const propTypes = { /** The credentials of the logged in person */ @@ -28,11 +29,10 @@ function SAMLSignInPage({credentials}) { /** * Handles in-app navigation once we get a response back from Expensify * - * @param {String} params.type * @param {String} params.url */ const handleNavigationStateChange = useCallback( - ({type, url}) => { + ({url}) => { const searchParams = new URLSearchParams(new URL(url).search); if (searchParams.has('shortLivedAuthToken')) { const shortLivedAuthToken = searchParams.get('shortLivedAuthToken'); @@ -40,11 +40,10 @@ function SAMLSignInPage({credentials}) { return; } }, - [webViewRef], + [credentials.login], ); return ( Date: Mon, 23 Oct 2023 18:33:21 -0700 Subject: [PATCH 015/344] add SAMLLOadingINdicator --- src/components/SAMLLoadingIndicator.js | 43 +++++++++++++++++++ src/pages/signin/SAMLSignInPage/index.js | 35 +-------------- .../signin/SAMLSignInPage/index.native.js | 4 +- 3 files changed, 46 insertions(+), 36 deletions(-) create mode 100644 src/components/SAMLLoadingIndicator.js diff --git a/src/components/SAMLLoadingIndicator.js b/src/components/SAMLLoadingIndicator.js new file mode 100644 index 000000000000..32438c5359c2 --- /dev/null +++ b/src/components/SAMLLoadingIndicator.js @@ -0,0 +1,43 @@ +import _ from 'underscore'; +import React from 'react'; +import {ActivityIndicator, View, StyleSheet} from 'react-native'; +import styles from '../styles/styles'; +import themeColors from '../styles/themes/default'; +import Icon from './Icon'; +import Text from './Text'; +import * as Expensicons from './Icon/Expensicons'; +import * as Illustrations from './Icon/Illustrations'; +import useLocalize from '../hooks/useLocalize'; + +function SAMLLoadingIndicator() { + const {translate} = useLocalize(); + return ( + + + + + + {translate('samlSignIn.launching')} + + {translate('samlSignIn.oneMoment')} + + + + + + + ); +} + +SAMLLoadingIndicator.displayName = 'SAMLLoadingIndicator'; + +export default SAMLLoadingIndicator; diff --git a/src/pages/signin/SAMLSignInPage/index.js b/src/pages/signin/SAMLSignInPage/index.js index 23ce9b93b8cc..f27a3a310597 100644 --- a/src/pages/signin/SAMLSignInPage/index.js +++ b/src/pages/signin/SAMLSignInPage/index.js @@ -1,16 +1,9 @@ import React, {useEffect} from 'react'; import {withOnyx} from 'react-native-onyx'; -import {View} from 'react-native'; import PropTypes from 'prop-types'; import ONYXKEYS from '../../../ONYXKEYS'; import CONFIG from '../../../CONFIG'; -import Icon from '../../../components/Icon'; -import Text from '../../../components/Text'; -import * as Expensicons from '../../../components/Icon/Expensicons'; -import * as Illustrations from '../../../components/Icon/Illustrations'; -import styles from '../../../styles/styles'; -import themeColors from '../../../styles/themes/default'; -import useLocalize from '../../../hooks/useLocalize'; +import SAMLLoadingIndicator from '../../../components/SAMLLoadingIndicator'; const propTypes = { /** The credentials of the logged in person */ @@ -25,36 +18,12 @@ const defaultProps = { }; function SAMLSignInPage({credentials}) { - const {translate} = useLocalize(); - useEffect(() => { window.open(`${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`, '_self'); }, [credentials.login]); return ( - - - - - - {translate('samlSignIn.launching')} - - {translate('samlSignIn.oneMoment')} - - - - - - + ); } diff --git a/src/pages/signin/SAMLSignInPage/index.native.js b/src/pages/signin/SAMLSignInPage/index.native.js index 4b4624b56cde..25affdbf26fd 100644 --- a/src/pages/signin/SAMLSignInPage/index.native.js +++ b/src/pages/signin/SAMLSignInPage/index.native.js @@ -20,10 +20,8 @@ const defaultProps = { credentials: {}, }; -const renderLoading = () => ; function SAMLSignInPage({credentials}) { - const webViewRef = useRef(); const samlLoginURL = `${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`; /** @@ -48,7 +46,7 @@ function SAMLSignInPage({credentials}) { source={{uri: samlLoginURL}} incognito // 'incognito' prop required for Android, issue here https://github.com/react-native-webview/react-native-webview/issues/1352 startInLoadingState - renderLoading={renderLoading} + renderLoading={() => } onNavigationStateChange={handleNavigationStateChange} /> ); From 89dc792ec697c1d12be8ab566c464318e9685eda Mon Sep 17 00:00:00 2001 From: Nikki Wines Date: Mon, 23 Oct 2023 18:51:18 -0700 Subject: [PATCH 016/344] style --- src/components/SAMLLoadingIndicator.js | 3 +-- src/pages/signin/SAMLSignInPage/index.native.js | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/SAMLLoadingIndicator.js b/src/components/SAMLLoadingIndicator.js index 32438c5359c2..1169436e79e5 100644 --- a/src/components/SAMLLoadingIndicator.js +++ b/src/components/SAMLLoadingIndicator.js @@ -1,6 +1,5 @@ -import _ from 'underscore'; import React from 'react'; -import {ActivityIndicator, View, StyleSheet} from 'react-native'; +import {View, StyleSheet} from 'react-native'; import styles from '../styles/styles'; import themeColors from '../styles/themes/default'; import Icon from './Icon'; diff --git a/src/pages/signin/SAMLSignInPage/index.native.js b/src/pages/signin/SAMLSignInPage/index.native.js index 25affdbf26fd..ea5621a22a65 100644 --- a/src/pages/signin/SAMLSignInPage/index.native.js +++ b/src/pages/signin/SAMLSignInPage/index.native.js @@ -1,4 +1,4 @@ -import React, {useCallback, useRef} from 'react'; +import React, {useCallback} from 'react'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; import WebView from 'react-native-webview'; @@ -6,7 +6,6 @@ import ONYXKEYS from '../../../ONYXKEYS'; import CONFIG from '../../../CONFIG'; import * as Session from '../../../libs/actions/Session'; import SAMLLoadingIndicator from '../../../components/SAMLLoadingIndicator'; -import FullScreenLoadingIndicator from '../../../components/FullscreenLoadingIndicator'; const propTypes = { /** The credentials of the logged in person */ @@ -35,7 +34,6 @@ function SAMLSignInPage({credentials}) { if (searchParams.has('shortLivedAuthToken')) { const shortLivedAuthToken = searchParams.get('shortLivedAuthToken'); Session.signInWithShortLivedAuthToken(credentials.login, shortLivedAuthToken); - return; } }, [credentials.login], From 526cf8f5f124d62d758491fc8e929a5b92d02b0c Mon Sep 17 00:00:00 2001 From: Nikki Wines Date: Mon, 23 Oct 2023 19:07:06 -0700 Subject: [PATCH 017/344] prettier --- src/pages/signin/SAMLSignInPage/index.js | 4 +--- src/pages/signin/SAMLSignInPage/index.native.js | 9 ++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/pages/signin/SAMLSignInPage/index.js b/src/pages/signin/SAMLSignInPage/index.js index f27a3a310597..283a3113b61b 100644 --- a/src/pages/signin/SAMLSignInPage/index.js +++ b/src/pages/signin/SAMLSignInPage/index.js @@ -22,9 +22,7 @@ function SAMLSignInPage({credentials}) { window.open(`${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`, '_self'); }, [credentials.login]); - return ( - - ); + return ; } SAMLSignInPage.propTypes = propTypes; diff --git a/src/pages/signin/SAMLSignInPage/index.native.js b/src/pages/signin/SAMLSignInPage/index.native.js index ea5621a22a65..afb1dd7d8b4b 100644 --- a/src/pages/signin/SAMLSignInPage/index.native.js +++ b/src/pages/signin/SAMLSignInPage/index.native.js @@ -19,15 +19,14 @@ const defaultProps = { credentials: {}, }; - function SAMLSignInPage({credentials}) { const samlLoginURL = `${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`; /** - * Handles in-app navigation once we get a response back from Expensify - * - * @param {String} params.url - */ + * Handles in-app navigation once we get a response back from Expensify + * + * @param {String} params.url + */ const handleNavigationStateChange = useCallback( ({url}) => { const searchParams = new URLSearchParams(new URL(url).search); From c386ce4a67cdd50a36b3c7b2d8ba5d3730ddd06e Mon Sep 17 00:00:00 2001 From: Nikki Wines Date: Mon, 23 Oct 2023 19:24:27 -0700 Subject: [PATCH 018/344] include platform --- src/pages/signin/SAMLSignInPage/index.native.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/signin/SAMLSignInPage/index.native.js b/src/pages/signin/SAMLSignInPage/index.native.js index afb1dd7d8b4b..ad478c3c0dfd 100644 --- a/src/pages/signin/SAMLSignInPage/index.native.js +++ b/src/pages/signin/SAMLSignInPage/index.native.js @@ -6,6 +6,7 @@ import ONYXKEYS from '../../../ONYXKEYS'; import CONFIG from '../../../CONFIG'; import * as Session from '../../../libs/actions/Session'; import SAMLLoadingIndicator from '../../../components/SAMLLoadingIndicator'; +import getPlatform from '../../../libs/getPlatform'; const propTypes = { /** The credentials of the logged in person */ @@ -20,7 +21,7 @@ const defaultProps = { }; function SAMLSignInPage({credentials}) { - const samlLoginURL = `${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`; + const samlLoginURL = `${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}&platform=${getPlatform()}`; /** * Handles in-app navigation once we get a response back from Expensify From 20dea7c2c9567ff9a645880098faf9ae8571d596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 24 Oct 2023 05:26:32 -0300 Subject: [PATCH 019/344] Rename file to TSX --- src/components/{BigNumberPad.js => BigNumberPad.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/components/{BigNumberPad.js => BigNumberPad.tsx} (100%) diff --git a/src/components/BigNumberPad.js b/src/components/BigNumberPad.tsx similarity index 100% rename from src/components/BigNumberPad.js rename to src/components/BigNumberPad.tsx From 63559ae0a6ff3d98ef179f32b91e8745f3842eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 24 Oct 2023 06:40:05 -0300 Subject: [PATCH 020/344] Starts migration of BigNumberPad --- src/components/BigNumberPad.tsx | 57 +++++++++++++++------------------ 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/src/components/BigNumberPad.tsx b/src/components/BigNumberPad.tsx index 5587808a09fd..aeadce2c3025 100644 --- a/src/components/BigNumberPad.tsx +++ b/src/components/BigNumberPad.tsx @@ -1,29 +1,22 @@ import React, {useState} from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; -import PropTypes from 'prop-types'; +import useWindowDimensions from '../hooks/useWindowDimensions'; +import ControlSelection from '../libs/ControlSelection'; import styles from '../styles/styles'; import Button from './Button'; -import ControlSelection from '../libs/ControlSelection'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; -import useWindowDimensions from '../hooks/useWindowDimensions'; +import withLocalize from './withLocalize'; -const propTypes = { +type BigNumberPadProps = { /** Callback to inform parent modal with key pressed */ - numberPressed: PropTypes.func.isRequired, + numberPressed: (key: string) => void; /** Callback to inform parent modal whether user is long pressing the "<" (backspace) button */ - longPressHandlerStateChanged: PropTypes.func, + longPressHandlerStateChanged?: (isUserLongPressingBackspace: boolean) => void; /** Used to locate this view from native classes. */ - nativeID: PropTypes.string, + nativeID?: string; - ...withLocalizePropTypes, -}; - -const defaultProps = { - longPressHandlerStateChanged: () => {}, - nativeID: 'numPadView', + // TODO: Add withLocalize props (withLocalizePropTypes) }; const padNumbers = [ @@ -33,57 +26,61 @@ const padNumbers = [ ['.', '0', '<'], ]; -function BigNumberPad(props) { - const [timer, setTimer] = useState(null); +function BigNumberPad({numberPressed, longPressHandlerStateChanged = () => {}, nativeID = 'numPadView'}: BigNumberPadProps) { + const [timer, setTimer] = useState(null); const {isExtraSmallScreenHeight} = useWindowDimensions(); /** * Handle long press key on number pad. * Only handles the '<' key and starts the continuous input timer. - * - * @param {String} key */ - const handleLongPress = (key) => { + const handleLongPress = (key: string) => { if (key !== '<') { return; } - props.longPressHandlerStateChanged(true); + longPressHandlerStateChanged(true); const newTimer = setInterval(() => { - props.numberPressed(key); + numberPressed(key); }, 100); + setTimer(newTimer); }; return ( - {_.map(padNumbers, (row, rowIndex) => ( + {padNumbers.map((row, rowIndex) => ( - {_.map(row, (column, columnIndex) => { + {row.map((column, columnIndex) => { // Adding margin between buttons except first column to // avoid unccessary space before the first column. const marginLeft = columnIndex > 0 ? styles.ml3 : {}; + return (