From 0979506ad34d27133b7f0150bdad5f3c7f1094a9 Mon Sep 17 00:00:00 2001 From: plondon Date: Wed, 14 Dec 2022 11:02:28 -0500 Subject: [PATCH] Implement payment method search --- .eslintignore | 2 + .eslintrc | 15 ++ package.json | 4 +- src/steps/AssetSelection/index.tsx | 4 +- src/steps/EnterAmount/index.tsx | 16 +- .../PaymentMethod/PaymentMethod.test.tsx | 10 + src/steps/PaymentMethod/index.tsx | 194 +++++++++++------- .../WalletConnect/WalletConnect.test.tsx | 5 - src/steps/WalletConnect/index.tsx | 2 +- src/steps/index.tsx | 4 +- yarn.lock | 7 + 11 files changed, 169 insertions(+), 94 deletions(-) diff --git a/.eslintignore b/.eslintignore index 3a2b6db9..e2d8d5fd 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,4 @@ node_modules/ generated/ +dist/ +coverage/ diff --git a/.eslintrc b/.eslintrc index 83f2d3bf..2a59effe 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,6 +4,7 @@ "root": true, "plugins": [ + "hooks", "jest-dom", "prettier", "react", @@ -16,6 +17,20 @@ ], "rules": { + "hooks/sort": [ + 2, + { + "groups": [ + "useReducer", + "useContext", + "useState", + "useRef", + "useDispatch", + "useCallback", + "useEffect" + ] + } + ], "prettier/prettier": "error", "react/jsx-sort-props": "error", "tailwindcss/no-custom-classname": [ diff --git a/package.json b/package.json index 022c472d..5393928b 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "scripts": { "build": "rm -rf dist && parcel build", "start": "parcel src/index.html --dist-dir output", - "lint": "eslint \"{**/*,*}.{js,ts,jsx,tsx}\" --fix", + "lint": "eslint \"{**/*,*}.{ts,tsx}\"", + "lint:fix": "eslint \"{**/*,*}.{ts,tsx}\" --fix", "codegen": "graphql-codegen --config codegen.ts", "test": "jest", "test:ci": "jest --ci" @@ -55,6 +56,7 @@ "assert": "^2.0.0", "eslint": "^8.24.0", "eslint-config-prettier": "^8.5.0", + "eslint-plugin-hooks": "^0.4.3", "eslint-plugin-jest-dom": "^4.0.2", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.31.7", diff --git a/src/steps/AssetSelection/index.tsx b/src/steps/AssetSelection/index.tsx index bb223e46..91f4a660 100644 --- a/src/steps/AssetSelection/index.tsx +++ b/src/steps/AssetSelection/index.tsx @@ -37,10 +37,10 @@ const AssetSelection: React.FC = () => { if (error) { return ( refetch()} + stacktrace={JSON.stringify(error)} /> ); } diff --git a/src/steps/EnterAmount/index.tsx b/src/steps/EnterAmount/index.tsx index 2de53cf7..ce3c21da 100644 --- a/src/steps/EnterAmount/index.tsx +++ b/src/steps/EnterAmount/index.tsx @@ -14,24 +14,22 @@ import { Context, Steps } from '../../providers/Store'; const BASE_FONT_SIZE = 48; const EnterAmount: React.FC = () => { - const dummyInputRef = useRef(null); - const dummySymbolRef = useRef(null); - const inputRef = useRef(null); - const formRef = useRef(null); - const quoteRef = useRef(null); - const connectRef = useRef(null); - const [state, dispatch] = useContext(Context); - const [formError, setFormError] = useState(''); - const rate = state.asset?.price?.price; + const [formError, setFormError] = useState(''); const [formValue, setFormValue] = useState<{ base: string; inputSelected: 'crypto' | 'fiat'; quote: string; }>({ base: '0', inputSelected: rate ? 'fiat' : 'crypto', quote: '0' }); const [amount, setAmount] = useState('0'); + const dummyInputRef = useRef(null); + const dummySymbolRef = useRef(null); + const inputRef = useRef(null); + const formRef = useRef(null); + const quoteRef = useRef(null); + const connectRef = useRef(null); const { getChainID, sendTransaction, switchChain } = useWeb3(); const { getDepositAddress } = useDepositAddress(); diff --git a/src/steps/PaymentMethod/PaymentMethod.test.tsx b/src/steps/PaymentMethod/PaymentMethod.test.tsx index 6f779168..60e3ee22 100644 --- a/src/steps/PaymentMethod/PaymentMethod.test.tsx +++ b/src/steps/PaymentMethod/PaymentMethod.test.tsx @@ -55,6 +55,16 @@ describe('Payment Selection', () => { expect(metamaskExtensions).toHaveLength(1); }); }); + describe('Search', () => { + it('searches for a payment method', async () => { + const rainbow = await screen.findByText('Rainbow'); + const searchInput = await screen.findByTestId('method-search'); + fireEvent.change(searchInput, { target: { value: 'metamask' } }); + const metamask = await screen.findByText('MetaMask'); + expect(metamask).toBeInTheDocument(); + expect(rainbow).not.toBeInTheDocument(); + }); + }); }); describe('Payment Method Errors', () => { diff --git a/src/steps/PaymentMethod/index.tsx b/src/steps/PaymentMethod/index.tsx index 03a2a351..185a7354 100644 --- a/src/steps/PaymentMethod/index.tsx +++ b/src/steps/PaymentMethod/index.tsx @@ -1,5 +1,5 @@ -import { Badge } from '@map3xyz/components'; -import React, { useContext } from 'react'; +import { Badge, Input } from '@map3xyz/components'; +import React, { useContext, useRef, useState } from 'react'; import { isAndroid, isIOS, isMobile } from 'react-device-detect'; import ErrorWrapper from '../../components/ErrorWrapper'; @@ -12,6 +12,8 @@ import { Context, Steps } from '../../providers/Store'; const PaymentMethod: React.FC = () => { const [state, dispatch] = useContext(Context); + const [formValue, setFormValue] = useState(); + const formRef = useRef(null); const chainId = state.network?.identifiers?.chainId; const { providers } = useWeb3(); const { data, error, loading, refetch } = useGetPaymentMethodsQuery({ @@ -34,6 +36,16 @@ const PaymentMethod: React.FC = () => { ); const methodsForNetwork = data?.methodsForNetwork?.filter((method) => { + const searchMatch = formValue?.get('method-search') + ? method?.name + ?.toLowerCase() + .includes( + (formValue.get('method-search') as string)?.toLowerCase() || '' + ) + : true; + + if (!searchMatch) return false; + const supportsChain = method?.walletConnect?.chains?.includes('eip155:' + chainId) || method?.walletConnect?.chains?.length === 0; @@ -79,6 +91,9 @@ const PaymentMethod: React.FC = () => { ); }); + const isEmptySearch = + methodsForNetwork?.length === 0 && !!formValue?.get('method-search'); + return (
@@ -90,6 +105,20 @@ const PaymentMethod: React.FC = () => { Payment Method
How do you want to send?
+
setFormValue(new FormData(e.currentTarget))} + ref={formRef} + > + } + name="method-search" + placeholder="Search for a payment method..." + rounded + /> +
@@ -106,81 +135,98 @@ const PaymentMethod: React.FC = () => {
- {methodsForNetwork?.map((method) => - method ? ( -
{ - if (!method.flags?.enabled) { - return; - } - dispatch({ - payload: method, - type: 'SET_PAYMENT_METHOD', - }); - if (method.value === 'qr') { - dispatch({ - payload: [ - 'AssetSelection', - 'NetworkSelection', - 'PaymentMethod', - 'QRCode', - ], - type: 'SET_STEPS', - }); - dispatch({ payload: Steps.QRCode, type: 'SET_STEP' }); - } else if (method.value === 'isWalletConnect') { - dispatch({ - payload: [ - 'AssetSelection', - 'NetworkSelection', - 'PaymentMethod', - 'WalletConnect', - 'EnterAmount', - 'Result', - ], - type: 'SET_STEPS', - }); - dispatch({ - payload: Steps.WalletConnect, - type: 'SET_STEP', - }); - } else { + {isEmptySearch ? ( + { + if (!formRef.current) return; + const input = formRef.current.getElementsByTagName('input')[0]; + input.value = ''; + input.focus(); + setFormValue(undefined); + }} + /> + ) : ( + methodsForNetwork?.map((method) => + method ? ( +
{ + if (!method.flags?.enabled) { + return; + } dispatch({ - payload: [ - 'AssetSelection', - 'NetworkSelection', - 'PaymentMethod', - 'EnterAmount', - 'Result', - ], - type: 'SET_STEPS', + payload: method, + type: 'SET_PAYMENT_METHOD', }); - dispatch({ payload: Steps.EnterAmount, type: 'SET_STEP' }); - } - }} - role="button" - > -
- - {method.name} - {providers[method.name || ''] ? ( - Installed - ) : null} + if (method.value === 'qr') { + dispatch({ + payload: [ + 'AssetSelection', + 'NetworkSelection', + 'PaymentMethod', + 'QRCode', + ], + type: 'SET_STEPS', + }); + dispatch({ payload: Steps.QRCode, type: 'SET_STEP' }); + } else if (method.value === 'isWalletConnect') { + dispatch({ + payload: [ + 'AssetSelection', + 'NetworkSelection', + 'PaymentMethod', + 'WalletConnect', + 'EnterAmount', + 'Result', + ], + type: 'SET_STEPS', + }); + dispatch({ + payload: Steps.WalletConnect, + type: 'SET_STEP', + }); + } else { + dispatch({ + payload: [ + 'AssetSelection', + 'NetworkSelection', + 'PaymentMethod', + 'EnterAmount', + 'Result', + ], + type: 'SET_STEPS', + }); + dispatch({ + payload: Steps.EnterAmount, + type: 'SET_STEP', + }); + } + }} + role="button" + > +
+ + {method.name} + {providers[method.name || ''] ? ( + Installed + ) : null} +
+ {`${state.method?.name}-${state.method?.value}` === + `${method.name}-${method.value}` ? ( + + ) : ( + + )}
- {`${state.method?.name}-${state.method?.value}` === - `${method.name}-${method.value}` ? ( - - ) : ( - - )} -
- ) : null + ) : null + ) )}
diff --git a/src/steps/WalletConnect/WalletConnect.test.tsx b/src/steps/WalletConnect/WalletConnect.test.tsx index 6d8592bf..7a435fed 100644 --- a/src/steps/WalletConnect/WalletConnect.test.tsx +++ b/src/steps/WalletConnect/WalletConnect.test.tsx @@ -246,11 +246,6 @@ describe('WalletConnect', () => { const walletConnect = await screen.findByText('Rainbow'); fireEvent.click(walletConnect); mockConnect.mockImplementation((event: string, callback: () => void) => { - if (event === 'connect') { - setTimeout(() => { - // callback(); - }, TIMEOUT_BEFORE_MOCK_CONNECT); - } if (event === 'disconnect') { setTimeout(() => { callback(); diff --git a/src/steps/WalletConnect/index.tsx b/src/steps/WalletConnect/index.tsx index db681a9e..51de35b0 100644 --- a/src/steps/WalletConnect/index.tsx +++ b/src/steps/WalletConnect/index.tsx @@ -13,10 +13,10 @@ import { useModalSize } from '../../hooks/useModalSize'; import { Context, Steps } from '../../providers/Store'; const WalletConnect: React.FC = () => { + const [state, dispatch] = useContext(Context); const [deeplink, setDeeplink] = useState(); const [uri, setUri] = useState(); const [showInstall, setShowInstall] = useState(false); - const [state, dispatch] = useContext(Context); const { width } = useModalSize(); diff --git a/src/steps/index.tsx b/src/steps/index.tsx index 4016694b..8c149e03 100644 --- a/src/steps/index.tsx +++ b/src/steps/index.tsx @@ -31,12 +31,12 @@ const Map3SdkSteps: React.FC = ({ onClose }) => { className={step === 0 ? 'invisible' : 'visible'} onClick={() => dispatch({ payload: step - 1, type: 'SET_STEP' })} > - +
diff --git a/yarn.lock b/yarn.lock index 5017437b..b2ef8eef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7078,6 +7078,13 @@ eslint-plugin-flowtype@^8.0.3: lodash "^4.17.21" string-natural-compare "^3.0.1" +eslint-plugin-hooks@^0.4.3: + version "0.4.3" + resolved "https://registry.npmjs.org/eslint-plugin-hooks/-/eslint-plugin-hooks-0.4.3.tgz#3c4a2c1390ab4b65692d25d04259ee68dab72427" + integrity sha512-Ra/YMBoTVFlM3zcU0c3vW8WQnauDyj/zVGnx6MUlIHaIMdZ/fZcptA20rotOGpRxZDQhReTlCYzz8BQmteJNWw== + dependencies: + requireindex "~1.2.0" + eslint-plugin-import@^2.25.3: version "2.26.0" resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz#f812dc47be4f2b72b478a021605a59fc6fe8b88b"