From 81ef02967f426521a087c87ff4608f695757b621 Mon Sep 17 00:00:00 2001 From: RR3b3l0 Date: Thu, 28 Dec 2023 14:40:44 +0000 Subject: [PATCH 1/7] [TS migration] Migrate KYCWall component --- src/components/KYCWall/BaseKYCWall.js | 247 ---------------- src/components/KYCWall/BaseKYCWall.tsx | 279 ++++++++++++++++++ .../{index.native.js => index.native.ts} | 0 .../KYCWall/{index.js => index.tsx} | 6 +- src/components/KYCWall/kycWallPropTypes.js | 91 ------ src/components/KYCWall/types.ts | 86 ++++++ src/libs/actions/BankAccounts.ts | 2 +- src/libs/actions/PaymentMethods.ts | 8 +- 8 files changed, 373 insertions(+), 346 deletions(-) delete mode 100644 src/components/KYCWall/BaseKYCWall.js create mode 100644 src/components/KYCWall/BaseKYCWall.tsx rename src/components/KYCWall/{index.native.js => index.native.ts} (100%) rename src/components/KYCWall/{index.js => index.tsx} (66%) delete mode 100644 src/components/KYCWall/kycWallPropTypes.js create mode 100644 src/components/KYCWall/types.ts diff --git a/src/components/KYCWall/BaseKYCWall.js b/src/components/KYCWall/BaseKYCWall.js deleted file mode 100644 index 5ad25d23f484..000000000000 --- a/src/components/KYCWall/BaseKYCWall.js +++ /dev/null @@ -1,247 +0,0 @@ -import lodashGet from 'lodash/get'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; -import {Dimensions} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu'; -import * as BankAccounts from '@libs/actions/BankAccounts'; -import getClickedTargetLocation from '@libs/getClickedTargetLocation'; -import Log from '@libs/Log'; -import Navigation from '@libs/Navigation/Navigation'; -import * as PaymentUtils from '@libs/PaymentUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as PaymentMethods from '@userActions/PaymentMethods'; -import * as Policy from '@userActions/Policy'; -import * as Wallet from '@userActions/Wallet'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import {defaultProps, propTypes} from './kycWallPropTypes'; - -// This sets the Horizontal anchor position offset for POPOVER MENU. -const POPOVER_MENU_ANCHOR_POSITION_HORIZONTAL_OFFSET = 20; - -// This component allows us to block various actions by forcing the user to first add a default payment method and successfully make it through our Know Your Customer flow -// before continuing to take whatever action they originally intended to take. It requires a button as a child and a native event so we can get the coordinates and use it -// to render the AddPaymentMethodMenu in the correct location. -function KYCWall({ - addBankAccountRoute, - addDebitCardRoute, - anchorAlignment, - bankAccountList, - chatReportID, - children, - enablePaymentsRoute, - fundList, - iouReport, - onSelectPaymentMethod, - onSuccessfulKYC, - reimbursementAccount, - shouldIncludeDebitCard, - shouldListenForResize, - source, - userWallet, - walletTerms, - shouldShowPersonalBankAccountOption, -}) { - const anchorRef = useRef(null); - const transferBalanceButtonRef = useRef(null); - - const [shouldShowAddPaymentMenu, setShouldShowAddPaymentMenu] = useState(false); - const [anchorPosition, setAnchorPosition] = useState({ - anchorPositionVertical: 0, - anchorPositionHorizontal: 0, - }); - - /** - * @param {DOMRect} domRect - * @returns {Object} - */ - const getAnchorPosition = useCallback( - (domRect) => { - if (anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP) { - return { - anchorPositionVertical: domRect.top + domRect.height + CONST.MODAL.POPOVER_MENU_PADDING, - anchorPositionHorizontal: domRect.left + POPOVER_MENU_ANCHOR_POSITION_HORIZONTAL_OFFSET, - }; - } - - return { - anchorPositionVertical: domRect.top - CONST.MODAL.POPOVER_MENU_PADDING, - anchorPositionHorizontal: domRect.left, - }; - }, - [anchorAlignment.vertical], - ); - - /** - * Set position of the transfer payment menu - * - * @param {Object} position - */ - const setPositionAddPaymentMenu = ({anchorPositionVertical, anchorPositionHorizontal}) => { - setAnchorPosition({ - anchorPositionVertical, - anchorPositionHorizontal, - }); - }; - - const setMenuPosition = useCallback(() => { - if (!transferBalanceButtonRef.current) { - return; - } - const buttonPosition = getClickedTargetLocation(transferBalanceButtonRef.current); - const position = getAnchorPosition(buttonPosition); - - setPositionAddPaymentMenu(position); - }, [getAnchorPosition]); - - useEffect(() => { - let dimensionsSubscription = null; - - PaymentMethods.kycWallRef.current = this; - - if (shouldListenForResize) { - dimensionsSubscription = Dimensions.addEventListener('change', setMenuPosition); - } - - return () => { - if (shouldListenForResize && dimensionsSubscription) { - dimensionsSubscription.remove(); - } - - PaymentMethods.kycWallRef.current = null; - }; - }, [chatReportID, setMenuPosition, shouldListenForResize]); - - /** - * @param {String} paymentMethod - */ - const selectPaymentMethod = (paymentMethod) => { - onSelectPaymentMethod(paymentMethod); - if (paymentMethod === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { - BankAccounts.openPersonalBankAccountSetupView(); - } else if (paymentMethod === CONST.PAYMENT_METHODS.DEBIT_CARD) { - Navigation.navigate(addDebitCardRoute); - } else if (paymentMethod === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT) { - if (ReportUtils.isIOUReport(iouReport)) { - const policyID = Policy.createWorkspaceFromIOUPayment(iouReport); - - // Navigate to the bank account set up flow for this specific policy - Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', policyID)); - return; - } - Navigation.navigate(addBankAccountRoute); - } - }; - - /** - * Take the position of the button that calls this method and show the Add Payment method menu when the user has no valid payment method. - * If they do have a valid payment method they are navigated to the "enable payments" route to complete KYC checks. - * If they are already KYC'd we will continue whatever action is gated behind the KYC wall. - * - * @param {Event} event - * @param {String} iouPaymentType - */ - const continueAction = (event, iouPaymentType) => { - const currentSource = lodashGet(walletTerms, 'source', source); - - /** - * Set the source, so we can tailor the process according to how we got here. - * We do not want to set this on mount, as the source can change upon completing the flow, e.g. when upgrading the wallet to Gold. - */ - Wallet.setKYCWallSource(source, chatReportID); - - if (shouldShowAddPaymentMenu) { - setShouldShowAddPaymentMenu(false); - - return; - } - - // Use event target as fallback if anchorRef is null for safety - const targetElement = anchorRef.current || event.nativeEvent.target; - - transferBalanceButtonRef.current = targetElement; - const isExpenseReport = ReportUtils.isExpenseReport(iouReport); - const paymentCardList = fundList || {}; - - // Check to see if user has a valid payment method on file and display the add payment popover if they don't - if ( - (isExpenseReport && lodashGet(reimbursementAccount, 'achData.state', '') !== CONST.BANK_ACCOUNT.STATE.OPEN) || - (!isExpenseReport && !PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, bankAccountList, shouldIncludeDebitCard)) - ) { - Log.info('[KYC Wallet] User does not have valid payment method'); - if (!shouldIncludeDebitCard) { - selectPaymentMethod(CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT); - return; - } - - const clickedElementLocation = getClickedTargetLocation(targetElement); - const position = getAnchorPosition(clickedElementLocation); - - setPositionAddPaymentMenu(position); - setShouldShowAddPaymentMenu(true); - - return; - } - - if (!isExpenseReport) { - // Ask the user to upgrade to a gold wallet as this means they have not yet gone through our Know Your Customer (KYC) checks - const hasActivatedWallet = userWallet.tierName && _.contains([CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM], userWallet.tierName); - if (!hasActivatedWallet) { - Log.info('[KYC Wallet] User does not have active wallet'); - Navigation.navigate(enablePaymentsRoute); - return; - } - } - Log.info('[KYC Wallet] User has valid payment method and passed KYC checks or did not need them'); - onSuccessfulKYC(iouPaymentType, currentSource); - }; - - return ( - <> - setShouldShowAddPaymentMenu(false)} - anchorRef={anchorRef} - anchorPosition={{ - vertical: anchorPosition.anchorPositionVertical, - horizontal: anchorPosition.anchorPositionHorizontal, - }} - anchorAlignment={anchorAlignment} - onItemSelected={(item) => { - setShouldShowAddPaymentMenu(false); - selectPaymentMethod(item); - }} - shouldShowPersonalBankAccountOption={shouldShowPersonalBankAccountOption} - /> - {children(continueAction, anchorRef)} - - ); -} - -KYCWall.propTypes = propTypes; -KYCWall.defaultProps = defaultProps; -KYCWall.displayName = 'BaseKYCWall'; - -export default withOnyx({ - userWallet: { - key: ONYXKEYS.USER_WALLET, - }, - walletTerms: { - key: ONYXKEYS.WALLET_TERMS, - }, - fundList: { - key: ONYXKEYS.FUND_LIST, - }, - bankAccountList: { - key: ONYXKEYS.BANK_ACCOUNT_LIST, - }, - reimbursementAccount: { - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - }, - chatReport: { - key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, - }, -})(KYCWall); diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx new file mode 100644 index 000000000000..4ec193a66bb8 --- /dev/null +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -0,0 +1,279 @@ +import _ from 'lodash'; +import React, {SyntheticEvent, useCallback, useEffect, useRef, useState} from 'react'; +import {Dimensions, EmitterSubscription, NativeTouchEvent} from 'react-native'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import {ValueOf} from 'type-fest'; +import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu'; +import * as BankAccounts from '@libs/actions/BankAccounts'; +import getClickedTargetLocation from '@libs/getClickedTargetLocation'; +import Log from '@libs/Log'; +import Navigation from '@libs/Navigation/Navigation'; +import * as PaymentUtils from '@libs/PaymentUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import * as PaymentMethods from '@userActions/PaymentMethods'; +import * as Policy from '@userActions/Policy'; +import * as Wallet from '@userActions/Wallet'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import {BankAccountList, FundList, ReimbursementAccount, Report, UserWallet, WalletTerms} from '@src/types/onyx'; +import {AnchorPosition, DOMRectProperties, KYCWallProps, PaymentMethod, TransferMethod} from './types'; + +// This sets the Horizontal anchor position offset for POPOVER MENU. +const POPOVER_MENU_ANCHOR_POSITION_HORIZONTAL_OFFSET = 20; + +type BaseKYCWallOnyxProps = { + userWallet: OnyxEntry; + walletTerms: OnyxEntry; + fundList: OnyxEntry; + bankAccountList: OnyxEntry; + reimbursementAccount: OnyxEntry; + chatReport: OnyxEntry; +}; + +type BaseKYCWallProps = KYCWallProps & BaseKYCWallOnyxProps; + +// This component allows us to block various actions by forcing the user to first add a default payment method and successfully make it through our Know Your Customer flow +// before continuing to take whatever action they originally intended to take. It requires a button as a child and a native event so we can get the coordinates and use it +// to render the AddPaymentMethodMenu in the correct location. +function KYCWall({ + addBankAccountRoute, + addDebitCardRoute, + anchorAlignment = { + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, + }, + bankAccountList = {}, + chatReportID = '', + children, + enablePaymentsRoute, + fundList, + iouReport, + onSelectPaymentMethod = () => {}, + onSuccessfulKYC, + reimbursementAccount, + shouldIncludeDebitCard = true, + shouldListenForResize = false, + source, + userWallet, + walletTerms, + shouldShowPersonalBankAccountOption = false, +}: KYCWallProps) { + const anchorRef = useRef(null); + const transferBalanceButtonRef = useRef(null); + + const [shouldShowAddPaymentMenu, setShouldShowAddPaymentMenu] = useState(false); + const [anchorPosition, setAnchorPosition] = useState({ + anchorPositionVertical: 0, + anchorPositionHorizontal: 0, + }); + + /** + * @param domRect + */ + const getAnchorPosition = useCallback( + (domRect: Pick): AnchorPosition => { + if (anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP) { + return { + anchorPositionVertical: domRect.top + domRect.height + CONST.MODAL.POPOVER_MENU_PADDING, + anchorPositionHorizontal: domRect.left + POPOVER_MENU_ANCHOR_POSITION_HORIZONTAL_OFFSET, + }; + } + + return { + anchorPositionVertical: domRect.top - CONST.MODAL.POPOVER_MENU_PADDING, + anchorPositionHorizontal: domRect.left, + }; + }, + [anchorAlignment.vertical], + ); + + /** + * Set position of the transfer payment menu + * + * @param position + */ + const setPositionAddPaymentMenu = ({anchorPositionVertical, anchorPositionHorizontal}: AnchorPosition) => { + setAnchorPosition({ + anchorPositionVertical, + anchorPositionHorizontal, + }); + }; + + const setMenuPosition = useCallback(() => { + if (!transferBalanceButtonRef.current) { + return; + } + const buttonPosition = getClickedTargetLocation(transferBalanceButtonRef.current); + const position = getAnchorPosition(buttonPosition); + + setPositionAddPaymentMenu(position); + }, [getAnchorPosition]); + + const selectPaymentMethod = useCallback( + (paymentMethod: PaymentMethod) => { + onSelectPaymentMethod(paymentMethod); + if (paymentMethod === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { + BankAccounts.openPersonalBankAccountSetupView(); + } else if (paymentMethod === CONST.PAYMENT_METHODS.DEBIT_CARD) { + Navigation.navigate(addDebitCardRoute); + } else if (paymentMethod === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT) { + if (ReportUtils.isIOUReport(iouReport)) { + const policyID = Policy.createWorkspaceFromIOUPayment(iouReport); + + // Navigate to the bank account set up flow for this specific policy + Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', policyID)); + return; + } + Navigation.navigate(addBankAccountRoute); + } + }, + [addBankAccountRoute, addDebitCardRoute, iouReport, onSelectPaymentMethod], + ); + + /** + * Take the position of the button that calls this method and show the Add Payment method menu when the user has no valid payment method. + * If they do have a valid payment method they are navigated to the "enable payments" route to complete KYC checks. + * If they are already KYC'd we will continue whatever action is gated behind the KYC wall. + * + */ + const continueAction = useCallback( + (event?: SyntheticEvent, iouPaymentType?: TransferMethod) => { + const currentSource = walletTerms?.source ?? source; + + /** + * Set the source, so we can tailor the process according to how we got here. + * We do not want to set this on mount, as the source can change upon completing the flow, e.g. when upgrading the wallet to Gold. + */ + Wallet.setKYCWallSource(source, chatReportID); + + if (shouldShowAddPaymentMenu) { + setShouldShowAddPaymentMenu(false); + + return; + } + + // Use event target as fallback if anchorRef is null for safety + const targetElement = anchorRef.current ?? (event?.nativeEvent.target as Element); + + transferBalanceButtonRef.current = targetElement; + const isExpenseReport = ReportUtils.isExpenseReport(iouReport); + const paymentCardList = fundList ?? {}; + + // Check to see if user has a valid payment method on file and display the add payment popover if they don't + if ( + (isExpenseReport && (reimbursementAccount?.achData?.state ?? '') !== CONST.BANK_ACCOUNT.STATE.OPEN) || + (!isExpenseReport && !PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, bankAccountList, shouldIncludeDebitCard)) + ) { + Log.info('[KYC Wallet] User does not have valid payment method'); + if (!shouldIncludeDebitCard) { + selectPaymentMethod(CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT); + return; + } + + const clickedElementLocation = getClickedTargetLocation(targetElement); + const position = getAnchorPosition(clickedElementLocation); + + setPositionAddPaymentMenu(position); + setShouldShowAddPaymentMenu(true); + + return; + } + + if (!isExpenseReport) { + // Ask the user to upgrade to a gold wallet as this means they have not yet gone through our Know Your Customer (KYC) checks + const hasActivatedWallet = userWallet?.tierName && [CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM].includes(userWallet.tierName); + if (!hasActivatedWallet) { + Log.info('[KYC Wallet] User does not have active wallet'); + Navigation.navigate(enablePaymentsRoute); + return; + } + } + Log.info('[KYC Wallet] User has valid payment method and passed KYC checks or did not need them'); + onSuccessfulKYC(currentSource, iouPaymentType); + }, + [ + bankAccountList, + chatReportID, + enablePaymentsRoute, + fundList, + getAnchorPosition, + iouReport, + onSuccessfulKYC, + reimbursementAccount?.achData?.state, + selectPaymentMethod, + shouldIncludeDebitCard, + shouldShowAddPaymentMenu, + source, + userWallet?.tierName, + walletTerms?.source, + ], + ); + + useEffect(() => { + let dimensionsSubscription: EmitterSubscription | null = null; + + PaymentMethods.kycWallRef.current = { + continueAction: (event?: SyntheticEvent, iouPaymentType?: TransferMethod) => { + continueAction(event, iouPaymentType); + }, + }; + + if (shouldListenForResize) { + dimensionsSubscription = Dimensions.addEventListener('change', setMenuPosition); + } + + return () => { + if (shouldListenForResize && dimensionsSubscription) { + dimensionsSubscription.remove(); + } + + PaymentMethods.kycWallRef.current = null; + }; + }, [chatReportID, setMenuPosition, shouldListenForResize, continueAction]); + + return ( + <> + setShouldShowAddPaymentMenu(false)} + anchorRef={anchorRef} + anchorPosition={{ + vertical: anchorPosition.anchorPositionVertical, + horizontal: anchorPosition.anchorPositionHorizontal, + }} + anchorAlignment={anchorAlignment} + onItemSelected={(item: ValueOf) => { + setShouldShowAddPaymentMenu(false); + selectPaymentMethod(item); + }} + shouldShowPersonalBankAccountOption={shouldShowPersonalBankAccountOption} + /> + {children(continueAction, anchorRef)} + + ); +} + +KYCWall.displayName = 'BaseKYCWall'; + +export default withOnyx({ + userWallet: { + key: ONYXKEYS.USER_WALLET, + }, + walletTerms: { + key: ONYXKEYS.WALLET_TERMS, + }, + fundList: { + key: ONYXKEYS.FUND_LIST, + }, + bankAccountList: { + key: ONYXKEYS.BANK_ACCOUNT_LIST, + }, + reimbursementAccount: { + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + }, + chatReport: { + key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, + }, +})(KYCWall); diff --git a/src/components/KYCWall/index.native.js b/src/components/KYCWall/index.native.ts similarity index 100% rename from src/components/KYCWall/index.native.js rename to src/components/KYCWall/index.native.ts diff --git a/src/components/KYCWall/index.js b/src/components/KYCWall/index.tsx similarity index 66% rename from src/components/KYCWall/index.js rename to src/components/KYCWall/index.tsx index 49329c73d474..7f28404171ad 100644 --- a/src/components/KYCWall/index.js +++ b/src/components/KYCWall/index.tsx @@ -1,8 +1,8 @@ import React from 'react'; import BaseKYCWall from './BaseKYCWall'; -import {defaultProps, propTypes} from './kycWallPropTypes'; +import {KYCWallProps} from './types'; -function KYCWall(props) { +function KYCWall(props: KYCWallProps) { return ( {}, - shouldShowPersonalBankAccountOption: false, -}; - -export {propTypes, defaultProps}; diff --git a/src/components/KYCWall/types.ts b/src/components/KYCWall/types.ts new file mode 100644 index 000000000000..b1f7f6438ba3 --- /dev/null +++ b/src/components/KYCWall/types.ts @@ -0,0 +1,86 @@ +import CONST from '@src/CONST'; +import {BankAccountList, FundList, ReimbursementAccount, Report, UserWallet, WalletTerms} from '@src/types/onyx'; +import { Route } from '@src/ROUTES'; +import { ValueOf } from 'type-fest'; +import { ForwardedRef, SyntheticEvent } from 'react'; +import { NativeTouchEvent } from 'react-native'; + +type Source = ValueOf + +type WalletTermsWithSource = WalletTerms & {source: Source} + +type TransferMethod = ValueOf + +type KYCWallProps = { + /** Route for the Add Bank Account screen for a given navigation stack */ + addBankAccountRoute: Route; + + /** Route for the Add Debit Card screen for a given navigation stack */ + addDebitCardRoute?: Route; + + /** Route for the KYC enable payments screen for a given navigation stack */ + enablePaymentsRoute: Route; + + /** Listen for window resize event on web and desktop */ + shouldListenForResize?: boolean; + + /** Wrapped components should be disabled, and not in spinner/loading state */ + isDisabled?: boolean; + + /** The user's wallet */ + userWallet?: UserWallet; + + /** Information related to the last step of the wallet activation flow */ + walletTerms?: WalletTermsWithSource; + + /** The source that triggered the KYC wall */ + source: Source; + + /** When the button is opened via an IOU, ID for the chatReport that the IOU is linked to */ + chatReportID?: string; + + /** List of user's cards */ + fundList?: FundList; + + /** List of bank accounts */ + bankAccountList?: BankAccountList; + + /** The chat report this report is linked to */ + chatReport?: Report; + + /** The IOU/Expense report we are paying */ + iouReport: Report; + + /** The reimbursement account linked to the Workspace */ + reimbursementAccount?: ReimbursementAccount; + + /** Where the popover should be positioned relative to the anchor points. */ + anchorAlignment?: { + horizontal: ValueOf; + vertical: ValueOf; + }; + + /** Whether the option to add a debit card should be included */ + shouldIncludeDebitCard?: boolean; + + /** Callback for when a payment method has been selected */ + onSelectPaymentMethod?: (paymentMethod: PaymentMethod) => void; + + /** Whether the personal bank account option should be shown */ + shouldShowPersonalBankAccountOption?: boolean; + + onSuccessfulKYC: (currentSource: Source, iouPaymentType?: TransferMethod) => void; + + children: (continueAction: (event: SyntheticEvent, method: TransferMethod) => void, anchorRef: ForwardedRef) => void +}; + +type DOMRectProperties = 'top' | 'bottom' | 'left' | 'right' | 'height' | 'x' | 'y'; + +type AnchorPosition = { + anchorPositionVertical: number; + anchorPositionHorizontal: number; +}; + +type PaymentMethod = ValueOf + +export type {AnchorPosition, KYCWallProps, DOMRectProperties, PaymentMethod, TransferMethod}; diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 8d0c59ab8d60..e8d42031b404 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -50,7 +50,7 @@ function setPlaidEvent(eventName: string) { /** * Open the personal bank account setup flow, with an optional exitReportID to redirect to once the flow is finished. */ -function openPersonalBankAccountSetupView(exitReportID: string) { +function openPersonalBankAccountSetupView(exitReportID?: string) { clearPlaid().then(() => { if (exitReportID) { Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {exitReportID}); diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts index 3547a3053a02..bdec9cb75119 100644 --- a/src/libs/actions/PaymentMethods.ts +++ b/src/libs/actions/PaymentMethods.ts @@ -1,7 +1,9 @@ -import {createRef} from 'react'; +import {createRef, MutableRefObject, SyntheticEvent} from 'react'; +import {NativeTouchEvent} from 'react-native'; import Onyx, {OnyxUpdate} from 'react-native-onyx'; import {OnyxEntry} from 'react-native-onyx/lib/types'; import {ValueOf} from 'type-fest'; +import {TransferMethod} from '@components/KYCWall/types'; import * as API from '@libs/API'; import * as CardUtils from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -13,13 +15,13 @@ import PaymentMethod from '@src/types/onyx/PaymentMethod'; import {FilterMethodPaymentType} from '@src/types/onyx/WalletTransfer'; type KYCWallRef = { - continueAction?: () => void; + continueAction?: (event?: SyntheticEvent, iouPaymentType?: TransferMethod) => void; }; /** * Sets up a ref to an instance of the KYC Wall component. */ -const kycWallRef = createRef(); +const kycWallRef: MutableRefObject = createRef(); /** * When we successfully add a payment method or pass the KYC checks we will continue with our setup action if we have one set. From a63b0b522925d2ebc0cea7354a0efab4da3a368e Mon Sep 17 00:00:00 2001 From: RR3b3l0 Date: Thu, 28 Dec 2023 14:48:59 +0000 Subject: [PATCH 2/7] fix: Added todo and expect error on the expected AddPaymentMethod prop --- src/components/KYCWall/BaseKYCWall.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index 4ec193a66bb8..ab2fb316f970 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -235,6 +235,7 @@ function KYCWall({ return ( <> setShouldShowAddPaymentMenu(false)} From 44ac5f4ca8b62efe0dc8572b0670295076896384 Mon Sep 17 00:00:00 2001 From: RR3b3l0 Date: Fri, 29 Dec 2023 10:15:07 +0000 Subject: [PATCH 3/7] [TS migration] Code adjustments --- src/components/KYCWall/BaseKYCWall.tsx | 44 +++++++++++--------------- src/components/KYCWall/types.ts | 44 ++++++++------------------ src/types/onyx/WalletTerms.ts | 5 +++ 3 files changed, 37 insertions(+), 56 deletions(-) diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index ab2fb316f970..e43b14a7edaa 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -1,8 +1,6 @@ -import _ from 'lodash'; import React, {SyntheticEvent, useCallback, useEffect, useRef, useState} from 'react'; import {Dimensions, EmitterSubscription, NativeTouchEvent} from 'react-native'; import {OnyxEntry, withOnyx} from 'react-native-onyx'; -import {ValueOf} from 'type-fest'; import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu'; import * as BankAccounts from '@libs/actions/BankAccounts'; import getClickedTargetLocation from '@libs/getClickedTargetLocation'; @@ -16,19 +14,27 @@ import * as Wallet from '@userActions/Wallet'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {BankAccountList, FundList, ReimbursementAccount, Report, UserWallet, WalletTerms} from '@src/types/onyx'; -import {AnchorPosition, DOMRectProperties, KYCWallProps, PaymentMethod, TransferMethod} from './types'; +import {BankAccountList, FundList, ReimbursementAccount, UserWallet, WalletTerms} from '@src/types/onyx'; +import {AnchorPosition, DomRect, KYCWallProps, PaymentMethod, TransferMethod} from './types'; // This sets the Horizontal anchor position offset for POPOVER MENU. const POPOVER_MENU_ANCHOR_POSITION_HORIZONTAL_OFFSET = 20; type BaseKYCWallOnyxProps = { + /** The user's wallet */ userWallet: OnyxEntry; + + /** Information related to the last step of the wallet activation flow */ walletTerms: OnyxEntry; + + /** List of user's cards */ fundList: OnyxEntry; + + /** List of bank accounts */ bankAccountList: OnyxEntry; + + /** The reimbursement account linked to the Workspace */ reimbursementAccount: OnyxEntry; - chatReport: OnyxEntry; }; type BaseKYCWallProps = KYCWallProps & BaseKYCWallOnyxProps; @@ -58,8 +64,8 @@ function KYCWall({ userWallet, walletTerms, shouldShowPersonalBankAccountOption = false, -}: KYCWallProps) { - const anchorRef = useRef(null); +}: BaseKYCWallProps) { + const anchorRef = useRef(null); const transferBalanceButtonRef = useRef(null); const [shouldShowAddPaymentMenu, setShouldShowAddPaymentMenu] = useState(false); @@ -68,11 +74,8 @@ function KYCWall({ anchorPositionHorizontal: 0, }); - /** - * @param domRect - */ const getAnchorPosition = useCallback( - (domRect: Pick): AnchorPosition => { + (domRect: DomRect): AnchorPosition => { if (anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP) { return { anchorPositionVertical: domRect.top + domRect.height + CONST.MODAL.POPOVER_MENU_PADDING, @@ -90,8 +93,6 @@ function KYCWall({ /** * Set position of the transfer payment menu - * - * @param position */ const setPositionAddPaymentMenu = ({anchorPositionVertical, anchorPositionHorizontal}: AnchorPosition) => { setAnchorPosition({ @@ -162,8 +163,8 @@ function KYCWall({ // Check to see if user has a valid payment method on file and display the add payment popover if they don't if ( - (isExpenseReport && (reimbursementAccount?.achData?.state ?? '') !== CONST.BANK_ACCOUNT.STATE.OPEN) || - (!isExpenseReport && !PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, bankAccountList, shouldIncludeDebitCard)) + (isExpenseReport && reimbursementAccount?.achData?.state !== CONST.BANK_ACCOUNT.STATE.OPEN) || + (!isExpenseReport && bankAccountList !== null && !PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, bankAccountList, shouldIncludeDebitCard)) ) { Log.info('[KYC Wallet] User does not have valid payment method'); if (!shouldIncludeDebitCard) { @@ -182,7 +183,7 @@ function KYCWall({ if (!isExpenseReport) { // Ask the user to upgrade to a gold wallet as this means they have not yet gone through our Know Your Customer (KYC) checks - const hasActivatedWallet = userWallet?.tierName && [CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM].includes(userWallet.tierName); + const hasActivatedWallet = userWallet?.tierName && [CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM].some((name) => name === userWallet.tierName); if (!hasActivatedWallet) { Log.info('[KYC Wallet] User does not have active wallet'); Navigation.navigate(enablePaymentsRoute); @@ -213,11 +214,7 @@ function KYCWall({ useEffect(() => { let dimensionsSubscription: EmitterSubscription | null = null; - PaymentMethods.kycWallRef.current = { - continueAction: (event?: SyntheticEvent, iouPaymentType?: TransferMethod) => { - continueAction(event, iouPaymentType); - }, - }; + PaymentMethods.kycWallRef.current = {continueAction}; if (shouldListenForResize) { dimensionsSubscription = Dimensions.addEventListener('change', setMenuPosition); @@ -245,7 +242,7 @@ function KYCWall({ horizontal: anchorPosition.anchorPositionHorizontal, }} anchorAlignment={anchorAlignment} - onItemSelected={(item: ValueOf) => { + onItemSelected={(item: PaymentMethod) => { setShouldShowAddPaymentMenu(false); selectPaymentMethod(item); }} @@ -274,7 +271,4 @@ export default withOnyx({ reimbursementAccount: { key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, }, - chatReport: { - key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, - }, })(KYCWall); diff --git a/src/components/KYCWall/types.ts b/src/components/KYCWall/types.ts index b1f7f6438ba3..068665a5aef9 100644 --- a/src/components/KYCWall/types.ts +++ b/src/components/KYCWall/types.ts @@ -1,15 +1,17 @@ +import {ForwardedRef, SyntheticEvent} from 'react'; +import {NativeTouchEvent} from 'react-native'; +import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; -import {BankAccountList, FundList, ReimbursementAccount, Report, UserWallet, WalletTerms} from '@src/types/onyx'; -import { Route } from '@src/ROUTES'; -import { ValueOf } from 'type-fest'; -import { ForwardedRef, SyntheticEvent } from 'react'; -import { NativeTouchEvent } from 'react-native'; +import {Route} from '@src/ROUTES'; +import {Report} from '@src/types/onyx'; -type Source = ValueOf +type Source = ValueOf; -type WalletTermsWithSource = WalletTerms & {source: Source} +type TransferMethod = ValueOf; -type TransferMethod = ValueOf +type DOMRectProperties = 'top' | 'bottom' | 'left' | 'right' | 'height' | 'x' | 'y'; + +type DomRect = Pick; type KYCWallProps = { /** Route for the Add Bank Account screen for a given navigation stack */ @@ -27,33 +29,15 @@ type KYCWallProps = { /** Wrapped components should be disabled, and not in spinner/loading state */ isDisabled?: boolean; - /** The user's wallet */ - userWallet?: UserWallet; - - /** Information related to the last step of the wallet activation flow */ - walletTerms?: WalletTermsWithSource; - /** The source that triggered the KYC wall */ source: Source; /** When the button is opened via an IOU, ID for the chatReport that the IOU is linked to */ chatReportID?: string; - /** List of user's cards */ - fundList?: FundList; - - /** List of bank accounts */ - bankAccountList?: BankAccountList; - - /** The chat report this report is linked to */ - chatReport?: Report; - /** The IOU/Expense report we are paying */ iouReport: Report; - /** The reimbursement account linked to the Workspace */ - reimbursementAccount?: ReimbursementAccount; - /** Where the popover should be positioned relative to the anchor points. */ anchorAlignment?: { horizontal: ValueOf; @@ -71,16 +55,14 @@ type KYCWallProps = { onSuccessfulKYC: (currentSource: Source, iouPaymentType?: TransferMethod) => void; - children: (continueAction: (event: SyntheticEvent, method: TransferMethod) => void, anchorRef: ForwardedRef) => void + children: (continueAction: (event: SyntheticEvent, method: TransferMethod) => void, anchorRef: ForwardedRef) => void; }; -type DOMRectProperties = 'top' | 'bottom' | 'left' | 'right' | 'height' | 'x' | 'y'; - type AnchorPosition = { anchorPositionVertical: number; anchorPositionHorizontal: number; }; -type PaymentMethod = ValueOf +type PaymentMethod = ValueOf; -export type {AnchorPosition, KYCWallProps, DOMRectProperties, PaymentMethod, TransferMethod}; +export type {AnchorPosition, KYCWallProps, PaymentMethod, TransferMethod, DomRect}; diff --git a/src/types/onyx/WalletTerms.ts b/src/types/onyx/WalletTerms.ts index 5394f126c33c..f43b2871ee5b 100644 --- a/src/types/onyx/WalletTerms.ts +++ b/src/types/onyx/WalletTerms.ts @@ -1,3 +1,5 @@ +import {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; import * as OnyxCommon from './OnyxCommon'; type WalletTerms = { @@ -6,6 +8,9 @@ type WalletTerms = { /** When the user accepts the Wallet's terms in order to pay an IOU, this is the ID of the chatReport the IOU is linked to */ chatReportID?: string; + + /** The source that triggered the KYC wall */ + source?: ValueOf; }; export default WalletTerms; From 570e0a437416f0cb13475a2cb46742addb2584c0 Mon Sep 17 00:00:00 2001 From: RR3b3l0 Date: Fri, 29 Dec 2023 14:17:56 +0000 Subject: [PATCH 4/7] [TS migration] Code adjustments --- src/components/KYCWall/BaseKYCWall.tsx | 49 ++++---------------------- src/components/KYCWall/types.ts | 9 +++-- src/libs/ReportUtils.ts | 2 +- 3 files changed, 14 insertions(+), 46 deletions(-) diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index e43b14a7edaa..336bc495ed8a 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -16,29 +16,21 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import {BankAccountList, FundList, ReimbursementAccount, UserWallet, WalletTerms} from '@src/types/onyx'; import {AnchorPosition, DomRect, KYCWallProps, PaymentMethod, TransferMethod} from './types'; - // This sets the Horizontal anchor position offset for POPOVER MENU. const POPOVER_MENU_ANCHOR_POSITION_HORIZONTAL_OFFSET = 20; - type BaseKYCWallOnyxProps = { /** The user's wallet */ userWallet: OnyxEntry; - /** Information related to the last step of the wallet activation flow */ walletTerms: OnyxEntry; - /** List of user's cards */ fundList: OnyxEntry; - /** List of bank accounts */ bankAccountList: OnyxEntry; - /** The reimbursement account linked to the Workspace */ reimbursementAccount: OnyxEntry; }; - type BaseKYCWallProps = KYCWallProps & BaseKYCWallOnyxProps; - // This component allows us to block various actions by forcing the user to first add a default payment method and successfully make it through our Know Your Customer flow // before continuing to take whatever action they originally intended to take. It requires a button as a child and a native event so we can get the coordinates and use it // to render the AddPaymentMethodMenu in the correct location. @@ -65,15 +57,13 @@ function KYCWall({ walletTerms, shouldShowPersonalBankAccountOption = false, }: BaseKYCWallProps) { - const anchorRef = useRef(null); - const transferBalanceButtonRef = useRef(null); - + const anchorRef = useRef(null); + const transferBalanceButtonRef = useRef(null); const [shouldShowAddPaymentMenu, setShouldShowAddPaymentMenu] = useState(false); const [anchorPosition, setAnchorPosition] = useState({ anchorPositionVertical: 0, anchorPositionHorizontal: 0, }); - const getAnchorPosition = useCallback( (domRect: DomRect): AnchorPosition => { if (anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP) { @@ -82,7 +72,6 @@ function KYCWall({ anchorPositionHorizontal: domRect.left + POPOVER_MENU_ANCHOR_POSITION_HORIZONTAL_OFFSET, }; } - return { anchorPositionVertical: domRect.top - CONST.MODAL.POPOVER_MENU_PADDING, anchorPositionHorizontal: domRect.left, @@ -90,7 +79,6 @@ function KYCWall({ }, [anchorAlignment.vertical], ); - /** * Set position of the transfer payment menu */ @@ -100,17 +88,14 @@ function KYCWall({ anchorPositionHorizontal, }); }; - const setMenuPosition = useCallback(() => { if (!transferBalanceButtonRef.current) { return; } const buttonPosition = getClickedTargetLocation(transferBalanceButtonRef.current); const position = getAnchorPosition(buttonPosition); - setPositionAddPaymentMenu(position); }, [getAnchorPosition]); - const selectPaymentMethod = useCallback( (paymentMethod: PaymentMethod) => { onSelectPaymentMethod(paymentMethod); @@ -119,9 +104,8 @@ function KYCWall({ } else if (paymentMethod === CONST.PAYMENT_METHODS.DEBIT_CARD) { Navigation.navigate(addDebitCardRoute); } else if (paymentMethod === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT) { - if (ReportUtils.isIOUReport(iouReport)) { + if (iouReport && ReportUtils.isIOUReport(iouReport)) { const policyID = Policy.createWorkspaceFromIOUPayment(iouReport); - // Navigate to the bank account set up flow for this specific policy Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', policyID)); return; @@ -131,7 +115,6 @@ function KYCWall({ }, [addBankAccountRoute, addDebitCardRoute, iouReport, onSelectPaymentMethod], ); - /** * Take the position of the button that calls this method and show the Add Payment method menu when the user has no valid payment method. * If they do have a valid payment method they are navigated to the "enable payments" route to complete KYC checks. @@ -141,26 +124,20 @@ function KYCWall({ const continueAction = useCallback( (event?: SyntheticEvent, iouPaymentType?: TransferMethod) => { const currentSource = walletTerms?.source ?? source; - /** * Set the source, so we can tailor the process according to how we got here. * We do not want to set this on mount, as the source can change upon completing the flow, e.g. when upgrading the wallet to Gold. */ - Wallet.setKYCWallSource(source, chatReportID); - + Wallet.setKYCWallSource(source ?? '', chatReportID); if (shouldShowAddPaymentMenu) { setShouldShowAddPaymentMenu(false); - return; } - // Use event target as fallback if anchorRef is null for safety - const targetElement = anchorRef.current ?? (event?.nativeEvent.target as Element); - + const targetElement = anchorRef.current ?? (event?.nativeEvent.target as HTMLDivElement); transferBalanceButtonRef.current = targetElement; - const isExpenseReport = ReportUtils.isExpenseReport(iouReport); + const isExpenseReport = ReportUtils.isExpenseReport(iouReport ?? null); const paymentCardList = fundList ?? {}; - // Check to see if user has a valid payment method on file and display the add payment popover if they don't if ( (isExpenseReport && reimbursementAccount?.achData?.state !== CONST.BANK_ACCOUNT.STATE.OPEN) || @@ -171,16 +148,12 @@ function KYCWall({ selectPaymentMethod(CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT); return; } - const clickedElementLocation = getClickedTargetLocation(targetElement); const position = getAnchorPosition(clickedElementLocation); - setPositionAddPaymentMenu(position); setShouldShowAddPaymentMenu(true); - return; } - if (!isExpenseReport) { // Ask the user to upgrade to a gold wallet as this means they have not yet gone through our Know Your Customer (KYC) checks const hasActivatedWallet = userWallet?.tierName && [CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM].some((name) => name === userWallet.tierName); @@ -210,25 +183,19 @@ function KYCWall({ walletTerms?.source, ], ); - useEffect(() => { let dimensionsSubscription: EmitterSubscription | null = null; - PaymentMethods.kycWallRef.current = {continueAction}; - if (shouldListenForResize) { dimensionsSubscription = Dimensions.addEventListener('change', setMenuPosition); } - return () => { if (shouldListenForResize && dimensionsSubscription) { dimensionsSubscription.remove(); } - PaymentMethods.kycWallRef.current = null; }; }, [chatReportID, setMenuPosition, shouldListenForResize, continueAction]); - return ( <> ); } - KYCWall.displayName = 'BaseKYCWall'; - export default withOnyx({ userWallet: { key: ONYXKEYS.USER_WALLET, @@ -271,4 +236,4 @@ export default withOnyx({ reimbursementAccount: { key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, }, -})(KYCWall); +})(KYCWall); \ No newline at end of file diff --git a/src/components/KYCWall/types.ts b/src/components/KYCWall/types.ts index 068665a5aef9..ca90d508af45 100644 --- a/src/components/KYCWall/types.ts +++ b/src/components/KYCWall/types.ts @@ -4,6 +4,7 @@ import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import {Route} from '@src/ROUTES'; import {Report} from '@src/types/onyx'; +import { OnyxEntry } from 'react-native-onyx'; type Source = ValueOf; @@ -30,13 +31,13 @@ type KYCWallProps = { isDisabled?: boolean; /** The source that triggered the KYC wall */ - source: Source; + source?: Source; /** When the button is opened via an IOU, ID for the chatReport that the IOU is linked to */ chatReportID?: string; /** The IOU/Expense report we are paying */ - iouReport: Report; + iouReport?: OnyxEntry; /** Where the popover should be positioned relative to the anchor points. */ anchorAlignment?: { @@ -53,8 +54,10 @@ type KYCWallProps = { /** Whether the personal bank account option should be shown */ shouldShowPersonalBankAccountOption?: boolean; - onSuccessfulKYC: (currentSource: Source, iouPaymentType?: TransferMethod) => void; + /** Callback for the end of the onContinue trigger on option selection */ + onSuccessfulKYC: (currentSource?: Source, iouPaymentType?: TransferMethod) => void; + /** Children to build the KYC */ children: (continueAction: (event: SyntheticEvent, method: TransferMethod) => void, anchorRef: ForwardedRef) => void; }; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 81bbf1df6273..933a3d1f6740 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -467,7 +467,7 @@ function getReportParticipantsTitle(accountIDs: number[]): string { /** * Checks if a report is a chat report. */ -function isChatReport(report: OnyxEntry): boolean { +function isChatReport(report: OnyxEntry | EmptyObject): boolean { return report?.type === CONST.REPORT.TYPE.CHAT; } From ea0e2655a95eccff30129e003810141060da6a73 Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Mon, 8 Jan 2024 12:05:12 +0000 Subject: [PATCH 5/7] [TS migration] Code spacing/quality adjustments --- src/components/KYCWall/BaseKYCWall.tsx | 38 +++++++++++++++++++++++++- src/components/KYCWall/types.ts | 2 +- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index 336bc495ed8a..c943b59fdee7 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -16,8 +16,10 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import {BankAccountList, FundList, ReimbursementAccount, UserWallet, WalletTerms} from '@src/types/onyx'; import {AnchorPosition, DomRect, KYCWallProps, PaymentMethod, TransferMethod} from './types'; + // This sets the Horizontal anchor position offset for POPOVER MENU. const POPOVER_MENU_ANCHOR_POSITION_HORIZONTAL_OFFSET = 20; + type BaseKYCWallOnyxProps = { /** The user's wallet */ userWallet: OnyxEntry; @@ -30,7 +32,9 @@ type BaseKYCWallOnyxProps = { /** The reimbursement account linked to the Workspace */ reimbursementAccount: OnyxEntry; }; + type BaseKYCWallProps = KYCWallProps & BaseKYCWallOnyxProps; + // This component allows us to block various actions by forcing the user to first add a default payment method and successfully make it through our Know Your Customer flow // before continuing to take whatever action they originally intended to take. It requires a button as a child and a native event so we can get the coordinates and use it // to render the AddPaymentMethodMenu in the correct location. @@ -59,11 +63,14 @@ function KYCWall({ }: BaseKYCWallProps) { const anchorRef = useRef(null); const transferBalanceButtonRef = useRef(null); + const [shouldShowAddPaymentMenu, setShouldShowAddPaymentMenu] = useState(false); + const [anchorPosition, setAnchorPosition] = useState({ anchorPositionVertical: 0, anchorPositionHorizontal: 0, }); + const getAnchorPosition = useCallback( (domRect: DomRect): AnchorPosition => { if (anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP) { @@ -79,6 +86,7 @@ function KYCWall({ }, [anchorAlignment.vertical], ); + /** * Set position of the transfer payment menu */ @@ -88,26 +96,37 @@ function KYCWall({ anchorPositionHorizontal, }); }; + const setMenuPosition = useCallback(() => { if (!transferBalanceButtonRef.current) { return; } + const buttonPosition = getClickedTargetLocation(transferBalanceButtonRef.current); const position = getAnchorPosition(buttonPosition); + setPositionAddPaymentMenu(position); }, [getAnchorPosition]); + const selectPaymentMethod = useCallback( (paymentMethod: PaymentMethod) => { onSelectPaymentMethod(paymentMethod); + if (paymentMethod === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { + BankAccounts.openPersonalBankAccountSetupView(); + } else if (paymentMethod === CONST.PAYMENT_METHODS.DEBIT_CARD) { + Navigation.navigate(addDebitCardRoute); + } else if (paymentMethod === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT) { if (iouReport && ReportUtils.isIOUReport(iouReport)) { const policyID = Policy.createWorkspaceFromIOUPayment(iouReport); + // Navigate to the bank account set up flow for this specific policy Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', policyID)); + return; } Navigation.navigate(addBankAccountRoute); @@ -115,6 +134,7 @@ function KYCWall({ }, [addBankAccountRoute, addDebitCardRoute, iouReport, onSelectPaymentMethod], ); + /** * Take the position of the button that calls this method and show the Add Payment method menu when the user has no valid payment method. * If they do have a valid payment method they are navigated to the "enable payments" route to complete KYC checks. @@ -124,6 +144,7 @@ function KYCWall({ const continueAction = useCallback( (event?: SyntheticEvent, iouPaymentType?: TransferMethod) => { const currentSource = walletTerms?.source ?? source; + /** * Set the source, so we can tailor the process according to how we got here. * We do not want to set this on mount, as the source can change upon completing the flow, e.g. when upgrading the wallet to Gold. @@ -133,11 +154,14 @@ function KYCWall({ setShouldShowAddPaymentMenu(false); return; } + // Use event target as fallback if anchorRef is null for safety const targetElement = anchorRef.current ?? (event?.nativeEvent.target as HTMLDivElement); transferBalanceButtonRef.current = targetElement; + const isExpenseReport = ReportUtils.isExpenseReport(iouReport ?? null); const paymentCardList = fundList ?? {}; + // Check to see if user has a valid payment method on file and display the add payment popover if they don't if ( (isExpenseReport && reimbursementAccount?.achData?.state !== CONST.BANK_ACCOUNT.STATE.OPEN) || @@ -148,10 +172,13 @@ function KYCWall({ selectPaymentMethod(CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT); return; } + const clickedElementLocation = getClickedTargetLocation(targetElement); const position = getAnchorPosition(clickedElementLocation); + setPositionAddPaymentMenu(position); setShouldShowAddPaymentMenu(true); + return; } if (!isExpenseReport) { @@ -159,10 +186,12 @@ function KYCWall({ const hasActivatedWallet = userWallet?.tierName && [CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM].some((name) => name === userWallet.tierName); if (!hasActivatedWallet) { Log.info('[KYC Wallet] User does not have active wallet'); + Navigation.navigate(enablePaymentsRoute); return; } } + Log.info('[KYC Wallet] User has valid payment method and passed KYC checks or did not need them'); onSuccessfulKYC(currentSource, iouPaymentType); }, @@ -183,19 +212,24 @@ function KYCWall({ walletTerms?.source, ], ); + useEffect(() => { let dimensionsSubscription: EmitterSubscription | null = null; + PaymentMethods.kycWallRef.current = {continueAction}; if (shouldListenForResize) { dimensionsSubscription = Dimensions.addEventListener('change', setMenuPosition); } + return () => { if (shouldListenForResize && dimensionsSubscription) { dimensionsSubscription.remove(); } PaymentMethods.kycWallRef.current = null; }; + }, [chatReportID, setMenuPosition, shouldListenForResize, continueAction]); + return ( <> ); } + KYCWall.displayName = 'BaseKYCWall'; + export default withOnyx({ userWallet: { key: ONYXKEYS.USER_WALLET, @@ -236,4 +272,4 @@ export default withOnyx({ reimbursementAccount: { key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, }, -})(KYCWall); \ No newline at end of file +})(KYCWall); diff --git a/src/components/KYCWall/types.ts b/src/components/KYCWall/types.ts index ca90d508af45..95d2e5320c10 100644 --- a/src/components/KYCWall/types.ts +++ b/src/components/KYCWall/types.ts @@ -1,10 +1,10 @@ import {ForwardedRef, SyntheticEvent} from 'react'; import {NativeTouchEvent} from 'react-native'; +import {OnyxEntry} from 'react-native-onyx'; import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import {Route} from '@src/ROUTES'; import {Report} from '@src/types/onyx'; -import { OnyxEntry } from 'react-native-onyx'; type Source = ValueOf; From ac1d0a7cb8ae7aa6bd5899d0f7281cdbfff98c6a Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Mon, 8 Jan 2024 13:43:00 +0000 Subject: [PATCH 6/7] [TS migration] Code adjustments --- src/components/KYCWall/BaseKYCWall.tsx | 28 ++++++++++++------- src/components/KYCWall/index.tsx | 2 +- src/components/KYCWall/types.ts | 38 ++++++++++++++------------ 3 files changed, 39 insertions(+), 29 deletions(-) diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index c943b59fdee7..c364df99d5f3 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -1,6 +1,6 @@ -import React, {SyntheticEvent, useCallback, useEffect, useRef, useState} from 'react'; -import {Dimensions, EmitterSubscription, NativeTouchEvent} from 'react-native'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import React, {type SyntheticEvent, useCallback, useEffect, useRef, useState} from 'react'; +import {Dimensions, type EmitterSubscription, type NativeTouchEvent} from 'react-native'; +import {type OnyxEntry, withOnyx} from 'react-native-onyx'; import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu'; import * as BankAccounts from '@libs/actions/BankAccounts'; import getClickedTargetLocation from '@libs/getClickedTargetLocation'; @@ -14,8 +14,8 @@ import * as Wallet from '@userActions/Wallet'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {BankAccountList, FundList, ReimbursementAccount, UserWallet, WalletTerms} from '@src/types/onyx'; -import {AnchorPosition, DomRect, KYCWallProps, PaymentMethod, TransferMethod} from './types'; +import type {BankAccountList, FundList, ReimbursementAccount, UserWallet, WalletTerms} from '@src/types/onyx'; +import type {AnchorPosition, DomRect, KYCWallProps, PaymentMethod, TransferMethod} from './types'; // This sets the Horizontal anchor position offset for POPOVER MENU. const POPOVER_MENU_ANCHOR_POSITION_HORIZONTAL_OFFSET = 20; @@ -23,12 +23,16 @@ const POPOVER_MENU_ANCHOR_POSITION_HORIZONTAL_OFFSET = 20; type BaseKYCWallOnyxProps = { /** The user's wallet */ userWallet: OnyxEntry; + /** Information related to the last step of the wallet activation flow */ walletTerms: OnyxEntry; + /** List of user's cards */ fundList: OnyxEntry; + /** List of bank accounts */ bankAccountList: OnyxEntry; + /** The reimbursement account linked to the Workspace */ reimbursementAccount: OnyxEntry; }; @@ -79,6 +83,7 @@ function KYCWall({ anchorPositionHorizontal: domRect.left + POPOVER_MENU_ANCHOR_POSITION_HORIZONTAL_OFFSET, }; } + return { anchorPositionVertical: domRect.top - CONST.MODAL.POPOVER_MENU_PADDING, anchorPositionHorizontal: domRect.left, @@ -113,13 +118,9 @@ function KYCWall({ onSelectPaymentMethod(paymentMethod); if (paymentMethod === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { - BankAccounts.openPersonalBankAccountSetupView(); - } else if (paymentMethod === CONST.PAYMENT_METHODS.DEBIT_CARD) { - Navigation.navigate(addDebitCardRoute); - } else if (paymentMethod === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT) { if (iouReport && ReportUtils.isIOUReport(iouReport)) { const policyID = Policy.createWorkspaceFromIOUPayment(iouReport); @@ -150,6 +151,7 @@ function KYCWall({ * We do not want to set this on mount, as the source can change upon completing the flow, e.g. when upgrading the wallet to Gold. */ Wallet.setKYCWallSource(source ?? '', chatReportID); + if (shouldShowAddPaymentMenu) { setShouldShowAddPaymentMenu(false); return; @@ -157,6 +159,7 @@ function KYCWall({ // Use event target as fallback if anchorRef is null for safety const targetElement = anchorRef.current ?? (event?.nativeEvent.target as HTMLDivElement); + transferBalanceButtonRef.current = targetElement; const isExpenseReport = ReportUtils.isExpenseReport(iouReport ?? null); @@ -168,6 +171,7 @@ function KYCWall({ (!isExpenseReport && bankAccountList !== null && !PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, bankAccountList, shouldIncludeDebitCard)) ) { Log.info('[KYC Wallet] User does not have valid payment method'); + if (!shouldIncludeDebitCard) { selectPaymentMethod(CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT); return; @@ -184,15 +188,18 @@ function KYCWall({ if (!isExpenseReport) { // Ask the user to upgrade to a gold wallet as this means they have not yet gone through our Know Your Customer (KYC) checks const hasActivatedWallet = userWallet?.tierName && [CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM].some((name) => name === userWallet.tierName); + if (!hasActivatedWallet) { Log.info('[KYC Wallet] User does not have active wallet'); Navigation.navigate(enablePaymentsRoute); + return; } } Log.info('[KYC Wallet] User has valid payment method and passed KYC checks or did not need them'); + onSuccessfulKYC(currentSource, iouPaymentType); }, [ @@ -217,6 +224,7 @@ function KYCWall({ let dimensionsSubscription: EmitterSubscription | null = null; PaymentMethods.kycWallRef.current = {continueAction}; + if (shouldListenForResize) { dimensionsSubscription = Dimensions.addEventListener('change', setMenuPosition); } @@ -225,9 +233,9 @@ function KYCWall({ if (shouldListenForResize && dimensionsSubscription) { dimensionsSubscription.remove(); } + PaymentMethods.kycWallRef.current = null; }; - }, [chatReportID, setMenuPosition, shouldListenForResize, continueAction]); return ( diff --git a/src/components/KYCWall/index.tsx b/src/components/KYCWall/index.tsx index 7f28404171ad..e99efee67210 100644 --- a/src/components/KYCWall/index.tsx +++ b/src/components/KYCWall/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import BaseKYCWall from './BaseKYCWall'; -import {KYCWallProps} from './types'; +import type {KYCWallProps} from './types'; function KYCWall(props: KYCWallProps) { return ( diff --git a/src/components/KYCWall/types.ts b/src/components/KYCWall/types.ts index 95d2e5320c10..aee5b569cc46 100644 --- a/src/components/KYCWall/types.ts +++ b/src/components/KYCWall/types.ts @@ -1,10 +1,10 @@ -import {ForwardedRef, SyntheticEvent} from 'react'; -import {NativeTouchEvent} from 'react-native'; -import {OnyxEntry} from 'react-native-onyx'; -import {ValueOf} from 'type-fest'; -import CONST from '@src/CONST'; -import {Route} from '@src/ROUTES'; -import {Report} from '@src/types/onyx'; +import type {ForwardedRef, SyntheticEvent} from 'react'; +import type {NativeTouchEvent} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; +import type {Route} from '@src/ROUTES'; +import type {Report} from '@src/types/onyx'; type Source = ValueOf; @@ -14,6 +14,18 @@ type DOMRectProperties = 'top' | 'bottom' | 'left' | 'right' | 'height' | 'x' | type DomRect = Pick; +type AnchorAlignment = { + horizontal: ValueOf; + vertical: ValueOf; +}; + +type AnchorPosition = { + anchorPositionVertical: number; + anchorPositionHorizontal: number; +}; + +type PaymentMethod = ValueOf; + type KYCWallProps = { /** Route for the Add Bank Account screen for a given navigation stack */ addBankAccountRoute: Route; @@ -40,10 +52,7 @@ type KYCWallProps = { iouReport?: OnyxEntry; /** Where the popover should be positioned relative to the anchor points. */ - anchorAlignment?: { - horizontal: ValueOf; - vertical: ValueOf; - }; + anchorAlignment?: AnchorAlignment; /** Whether the option to add a debit card should be included */ shouldIncludeDebitCard?: boolean; @@ -61,11 +70,4 @@ type KYCWallProps = { children: (continueAction: (event: SyntheticEvent, method: TransferMethod) => void, anchorRef: ForwardedRef) => void; }; -type AnchorPosition = { - anchorPositionVertical: number; - anchorPositionHorizontal: number; -}; - -type PaymentMethod = ValueOf; - export type {AnchorPosition, KYCWallProps, PaymentMethod, TransferMethod, DomRect}; From 9348564f0eb499284e61ff5219221d59b200189a Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Wed, 17 Jan 2024 10:34:03 +0000 Subject: [PATCH 7/7] [TS migration] Fixed ts issues on Wallet.ts --- src/components/KYCWall/BaseKYCWall.tsx | 2 +- src/libs/actions/Wallet.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index 1dea54818a3f..04c8397bc33b 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -153,7 +153,7 @@ function KYCWall({ * Set the source, so we can tailor the process according to how we got here. * We do not want to set this on mount, as the source can change upon completing the flow, e.g. when upgrading the wallet to Gold. */ - Wallet.setKYCWallSource(source ?? '', chatReportID); + Wallet.setKYCWallSource(source, chatReportID); if (shouldShowAddPaymentMenu) { setShouldShowAddPaymentMenu(false); diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index bc2fb518d8e6..e7a272343209 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -88,7 +88,7 @@ function setAdditionalDetailsErrorMessage(additionalErrorMessage: string) { /** * Save the source that triggered the KYC wall and optionally the chat report ID associated with the IOU */ -function setKYCWallSource(source: string, chatReportID = '') { +function setKYCWallSource(source?: ValueOf, chatReportID = '') { Onyx.merge(ONYXKEYS.WALLET_TERMS, {source, chatReportID}); }