Skip to content

Commit

Permalink
Implement payment method search
Browse files Browse the repository at this point in the history
  • Loading branch information
plondon committed Dec 14, 2022
1 parent bad7c76 commit 0979506
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 94 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
node_modules/
generated/
dist/
coverage/
15 changes: 15 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"root": true,

"plugins": [
"hooks",
"jest-dom",
"prettier",
"react",
Expand All @@ -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": [
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions src/steps/AssetSelection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ const AssetSelection: React.FC<Props> = () => {
if (error) {
return (
<ErrorWrapper
// description="We couldn't get a list of assets to select."
description={JSON.stringify(error)}
description="We couldn't get a list of assets to select."
header="Error Fetching Assets"
retry={() => refetch()}
stacktrace={JSON.stringify(error)}
/>
);
}
Expand Down
16 changes: 7 additions & 9 deletions src/steps/EnterAmount/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,22 @@ import { Context, Steps } from '../../providers/Store';
const BASE_FONT_SIZE = 48;

const EnterAmount: React.FC<Props> = () => {
const dummyInputRef = useRef<HTMLSpanElement>(null);
const dummySymbolRef = useRef<HTMLSpanElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const formRef = useRef<HTMLFormElement>(null);
const quoteRef = useRef<HTMLSpanElement>(null);
const connectRef = useRef<ConnectHandler>(null);

const [state, dispatch] = useContext(Context);
const [formError, setFormError] = useState<string | undefined>('');

const rate = state.asset?.price?.price;

const [formError, setFormError] = useState<string | undefined>('');
const [formValue, setFormValue] = useState<{
base: string;
inputSelected: 'crypto' | 'fiat';
quote: string;
}>({ base: '0', inputSelected: rate ? 'fiat' : 'crypto', quote: '0' });
const [amount, setAmount] = useState<string>('0');
const dummyInputRef = useRef<HTMLSpanElement>(null);
const dummySymbolRef = useRef<HTMLSpanElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const formRef = useRef<HTMLFormElement>(null);
const quoteRef = useRef<HTMLSpanElement>(null);
const connectRef = useRef<ConnectHandler>(null);

const { getChainID, sendTransaction, switchChain } = useWeb3();
const { getDepositAddress } = useDepositAddress();
Expand Down
10 changes: 10 additions & 0 deletions src/steps/PaymentMethod/PaymentMethod.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
194 changes: 120 additions & 74 deletions src/steps/PaymentMethod/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -12,6 +12,8 @@ import { Context, Steps } from '../../providers/Store';

const PaymentMethod: React.FC<Props> = () => {
const [state, dispatch] = useContext(Context);
const [formValue, setFormValue] = useState<FormData>();
const formRef = useRef<HTMLFormElement>(null);
const chainId = state.network?.identifiers?.chainId;
const { providers } = useWeb3();
const { data, error, loading, refetch } = useGetPaymentMethodsQuery({
Expand All @@ -34,6 +36,16 @@ const PaymentMethod: React.FC<Props> = () => {
);

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;
Expand Down Expand Up @@ -79,6 +91,9 @@ const PaymentMethod: React.FC<Props> = () => {
);
});

const isEmptySearch =
methodsForNetwork?.length === 0 && !!formValue?.get('method-search');

return (
<div className="flex h-full flex-col">
<div className="border-b border-neutral-200 bg-white dark:border-neutral-700 dark:bg-neutral-900">
Expand All @@ -90,6 +105,20 @@ const PaymentMethod: React.FC<Props> = () => {
Payment Method
</h3>
<h5 className="text-xs text-neutral-400">How do you want to send?</h5>
<form
className="mt-2"
onChange={(e) => setFormValue(new FormData(e.currentTarget))}
ref={formRef}
>
<Input
autoFocus
data-testid="method-search"
icon={<i className="fa fa-search" />}
name="method-search"
placeholder="Search for a payment method..."
rounded
/>
</form>
</InnerWrapper>

<div className="w-full border-t border-neutral-200 bg-neutral-100 px-4 py-3 font-bold leading-6 dark:border-neutral-700 dark:bg-neutral-800 dark:text-white">
Expand All @@ -106,81 +135,98 @@ const PaymentMethod: React.FC<Props> = () => {
</div>
<div className="flex h-full flex-col overflow-hidden">
<div className="relative z-10 flex flex-col overflow-y-auto dark:text-white">
{methodsForNetwork?.map((method) =>
method ? (
<div
className={`flex items-center justify-between border-b border-neutral-200 px-4 py-3 text-sm hover:bg-neutral-100 dark:border-neutral-700 hover:dark:bg-neutral-800 ${
method.flags?.enabled
? ''
: '!cursor-not-allowed opacity-50 hover:bg-white dark:hover:bg-neutral-900'
}`}
key={method.name + '-' + method.value}
onClick={() => {
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 ? (
<ErrorWrapper
description="We couldn't find any payment methods that matched your search."
header="No Payment Methods Found"
retry={() => {
if (!formRef.current) return;
const input = formRef.current.getElementsByTagName('input')[0];
input.value = '';
input.focus();
setFormValue(undefined);
}}
/>
) : (
methodsForNetwork?.map((method) =>
method ? (
<div
className={`flex items-center justify-between border-b border-neutral-200 px-4 py-3 text-sm hover:bg-neutral-100 dark:border-neutral-700 hover:dark:bg-neutral-800 ${
method.flags?.enabled
? ''
: '!cursor-not-allowed opacity-50 hover:bg-white dark:hover:bg-neutral-900'
}`}
key={method.name + '-' + method.value}
onClick={() => {
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"
>
<div className="flex items-center gap-2">
<MethodIcon method={method} />
<span>{method.name}</span>
{providers[method.name || ''] ? (
<Badge color="green">Installed</Badge>
) : 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"
>
<div className="flex items-center gap-2">
<MethodIcon method={method} />
<span>{method.name}</span>
{providers[method.name || ''] ? (
<Badge color="green">Installed</Badge>
) : null}
</div>
{`${state.method?.name}-${state.method?.value}` ===
`${method.name}-${method.value}` ? (
<i className="fa fa-check-circle text-green-400" />
) : (
<i className="fa fa-chevron-right text-xxs" />
)}
</div>
{`${state.method?.name}-${state.method?.value}` ===
`${method.name}-${method.value}` ? (
<i className="fa fa-check-circle text-green-400" />
) : (
<i className="fa fa-chevron-right text-xxs" />
)}
</div>
) : null
) : null
)
)}
</div>
</div>
Expand Down
5 changes: 0 additions & 5 deletions src/steps/WalletConnect/WalletConnect.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/steps/WalletConnect/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import { useModalSize } from '../../hooks/useModalSize';
import { Context, Steps } from '../../providers/Store';

const WalletConnect: React.FC<Props> = () => {
const [state, dispatch] = useContext(Context);
const [deeplink, setDeeplink] = useState<string | undefined>();
const [uri, setUri] = useState<string | undefined>();
const [showInstall, setShowInstall] = useState(false);
const [state, dispatch] = useContext(Context);

const { width } = useModalSize();

Expand Down
4 changes: 2 additions & 2 deletions src/steps/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ const Map3SdkSteps: React.FC<Props> = ({ onClose }) => {
className={step === 0 ? 'invisible' : 'visible'}
onClick={() => dispatch({ payload: step - 1, type: 'SET_STEP' })}
>
<i className="fa transition-color fa-long-arrow-left duration-75 dark:text-neutral-600 dark:hover:text-neutral-400" />
<i className="fa fa-long-arrow-left transition-colors duration-75 dark:text-neutral-600 dark:hover:text-neutral-400" />
</button>
<ProgressBar progress={step / (steps.length - 1)} />
<div>
<button onClick={onClose}>
<i className="fa transition-color fa-close duration-75 dark:text-neutral-600 dark:hover:text-neutral-400" />
<i className="fa fa-close transition-colors duration-75 dark:text-neutral-600 dark:hover:text-neutral-400" />
</button>
</div>
</div>
Expand Down
Loading

1 comment on commit 0979506

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines Statements Branches Functions
Coverage: 92%
91.9% (579/630) 68.53% (734/1071) 89.71% (96/107)

Please sign in to comment.