diff --git a/packages/example/components/test-plan/connect-wallet.tsx b/packages/example/components/test-plan/connect-wallet.tsx new file mode 100644 index 00000000..7af4d62e --- /dev/null +++ b/packages/example/components/test-plan/connect-wallet.tsx @@ -0,0 +1,42 @@ +import { Mainnet, useCelo } from '@celo/react-celo'; +import React, { useEffect } from 'react'; + +import { SuccessIcon } from './success-icon'; +import { Result, TestBlock } from './ui'; +import { Status, useTestStatus } from './useTestStatus'; + +export function ConnectWalletCheck() { + const { connect, address, updateNetwork } = useCelo(); + const { status, errorMessage, wrapActionWithStatus, setStatus } = + useTestStatus(); + + const onConnectWallet = wrapActionWithStatus(async () => { + await updateNetwork(Mainnet); + await connect(); + }); + + useEffect(() => { + if (address) { + setStatus.success(); + } + }, [address, setStatus]); + + return ( + + + +

Press the button above to choose a wallet to connect to.

+
+ + Connected to {address} + + {errorMessage} +
+
+ ); +} diff --git a/packages/example/components/test-plan/error-icon.tsx b/packages/example/components/test-plan/error-icon.tsx new file mode 100644 index 00000000..dc450068 --- /dev/null +++ b/packages/example/components/test-plan/error-icon.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +export function ErrorIcon() { + return ( + + + + + + + + + + + + + ); +} diff --git a/packages/example/components/test-plan/send-transaction.tsx b/packages/example/components/test-plan/send-transaction.tsx new file mode 100644 index 00000000..003eda1f --- /dev/null +++ b/packages/example/components/test-plan/send-transaction.tsx @@ -0,0 +1,38 @@ +import { useCelo } from '@celo/react-celo'; + +import { sendTestTransaction } from '../../utils/send-test-transaction'; +import { SuccessIcon } from './success-icon'; +import { Result, TestBlock } from './ui'; +import { useDisabledTest } from './useDisabledTest'; +import { useTestStatus } from './useTestStatus'; + +export function SendTransaction() { + const { performActions } = useCelo(); + const { status, errorMessage, wrapActionWithStatus } = useTestStatus(); + const [disabled, setDisabled] = useDisabledTest(); + + const onRunTest = wrapActionWithStatus(async () => { + setDisabled(true); + await sendTestTransaction(performActions); + }); + + return ( + + +

This sends a very small transaction to impact market contract.

+ +

You'll need to approve the transaction in the wallet.

+
+ + Transaction sent + + {errorMessage} +
+
+ ); +} diff --git a/packages/example/components/test-plan/success-icon.tsx b/packages/example/components/test-plan/success-icon.tsx new file mode 100644 index 00000000..2cfb48e1 --- /dev/null +++ b/packages/example/components/test-plan/success-icon.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +export function SuccessIcon() { + return ( + + + + + + + + + + + + + ); +} diff --git a/packages/example/components/test-plan/switch-networks.tsx b/packages/example/components/test-plan/switch-networks.tsx new file mode 100644 index 00000000..65e10ad6 --- /dev/null +++ b/packages/example/components/test-plan/switch-networks.tsx @@ -0,0 +1,47 @@ +import { Alfajores, useCelo } from '@celo/react-celo'; +import { useEffect, useState } from 'react'; + +import { SuccessIcon } from './success-icon'; +import { Result, TestBlock } from './ui'; +import { useDisabledTest } from './useDisabledTest'; +import { Status, useTestStatus } from './useTestStatus'; + +export function SwitchNetwork() { + const { updateNetwork, network } = useCelo(); + const { status, errorMessage, wrapActionWithStatus, setStatus } = + useTestStatus(); + const [disabledTest, setDisabledTest] = useDisabledTest(); + const [connectedNetwork, setConnectedNetwork] = useState(''); + + const onSwitchNetworks = wrapActionWithStatus(async () => { + setDisabledTest(true); + await updateNetwork(Alfajores); + }); + + useEffect(() => { + setConnectedNetwork(network.name); + if (status === Status.NotStarted && network.name === Alfajores.name) { + setStatus.error('Already set to Alfajores'); + setDisabledTest(true); + } + }, [network.name, setStatus, status, setDisabledTest]); + + return ( + + + +

Currently connected to {connectedNetwork}.

+
+ + Switched to Alfajores + + {errorMessage} +
+
+ ); +} diff --git a/packages/example/components/test-plan/ui.tsx b/packages/example/components/test-plan/ui.tsx new file mode 100644 index 00000000..05af33a8 --- /dev/null +++ b/packages/example/components/test-plan/ui.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { SecondaryButton } from '../buttons'; + +import { ErrorIcon } from './error-icon'; +import { Status } from './useTestStatus'; + +export function TestTag({ type }: { type: Status }) { + const getText = (type: Status) => { + return type.replace('-', ' '); + }; + + return {getText(type)}; +} + +export function TestBlock({ + status, + title, + onRunTest, + disabledTest, + children, +}: React.PropsWithChildren<{ + title: string; + status: Status; + disabledTest: boolean; + onRunTest: () => void; +}>) { + return ( +
+
+ +
+
+
+
{title}
+ + Run + +
+ {children} +
+
+ ); +} + +export const Header: React.FC = (props) => ( +

+); + +export const Text: React.FC = (props) => ( +
+); + +const ResultContext = React.createContext(''); + +function useResultContext() { + const context = React.useContext(ResultContext); + if (!context) { + throw new Error('Cannot use this element outside the Result component'); + } + return context; +} + +export function Result(props: React.PropsWithChildren<{ status: Status }>) { + return ( + +
{props.children}
+
+ ); +} + +export const Success: React.FC = (props) => { + const context = useResultContext(); + return context === Status.Success ? ( +

+ {props.children} +

+ ) : null; +}; + +export const ErrorText: React.FC = (props) => { + const context = useResultContext(); + return context === Status.Error ? ( +

+ {props.children} +

+ ) : null; +}; + +export const Default: React.FC = (props) => { + const context = useResultContext(); + return context === Status.NotStarted || context === Status.Pending ? ( + <>{props.children} + ) : null; +}; + +Result.Success = Success; +Result.Error = ErrorText; +Result.Default = Default; diff --git a/packages/example/components/test-plan/update-fee-currency.tsx b/packages/example/components/test-plan/update-fee-currency.tsx new file mode 100644 index 00000000..9087c216 --- /dev/null +++ b/packages/example/components/test-plan/update-fee-currency.tsx @@ -0,0 +1,53 @@ +import { CeloContract } from '@celo/contractkit'; +import { useCelo } from '@celo/react-celo'; +import { useEffect } from 'react'; + +import { SuccessIcon } from './success-icon'; +import { Result, TestBlock } from './ui'; +import { useDisabledTest } from './useDisabledTest'; +import { useTestStatus } from './useTestStatus'; + +export function UpdateFeeCurrency() { + const { updateFeeCurrency, feeCurrency, supportsFeeCurrency, address } = + useCelo(); + const [disabledTest, setDisabledTest] = useDisabledTest(); + const { status, errorMessage, wrapActionWithStatus, setStatus } = + useTestStatus(); + + useEffect(() => { + if (address && supportsFeeCurrency !== undefined && !supportsFeeCurrency) { + setDisabledTest(true); + setStatus.error('Wallet does not support updating fee currency.'); + } + }, [address, supportsFeeCurrency, setStatus, setDisabledTest]); + + const onUpdateCurrency = wrapActionWithStatus(async () => { + setDisabledTest(true); + await updateFeeCurrency(CeloContract.StableTokenBRL); + if (feeCurrency !== CeloContract.StableTokenBRL) { + throw new Error('Fee currency did not update.'); + } + }); + + return ( + + +

Fee currency used: {feeCurrency}

+ + <> +

Change the currency used in transactions.

+ +
+ + Fee currency {feeCurrency} + + {errorMessage} +
+
+ ); +} diff --git a/packages/example/components/test-plan/useDisabledTest.tsx b/packages/example/components/test-plan/useDisabledTest.tsx new file mode 100644 index 00000000..9ef7d516 --- /dev/null +++ b/packages/example/components/test-plan/useDisabledTest.tsx @@ -0,0 +1,15 @@ +import { useCelo } from '@celo/react-celo'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; + +export const useDisabledTest = (): [ + boolean, + Dispatch> +] => { + const { address } = useCelo(); + const [disabled, setDisabled] = useState(true); + useEffect(() => { + setDisabled(!address); + }, [address]); + + return [disabled, setDisabled]; +}; diff --git a/packages/example/components/test-plan/useTestStatus.tsx b/packages/example/components/test-plan/useTestStatus.tsx new file mode 100644 index 00000000..9a0c15b9 --- /dev/null +++ b/packages/example/components/test-plan/useTestStatus.tsx @@ -0,0 +1,73 @@ +import { useCelo } from '@celo/react-celo'; +import { useEffect, useMemo, useState } from 'react'; + +export enum Status { + NotStarted = 'not-started', + Success = 'success', + Error = 'error', + Pending = 'pending', +} + +function hasMessage(error: unknown): error is { message: string } { + if (!error || typeof error !== 'object') { + return false; + } + + return 'message' in error; +} + +export const useTestStatus = () => { + const { address } = useCelo(); + + const [status, setStatus] = useState(Status.NotStarted); + + const set = useMemo(() => { + return { + success: () => { + setStatus(Status.Success); + }, + error: (error: unknown) => { + setStatus(Status.Error); + + if (hasMessage(error)) { + setErrorMessage(error.message); + } else if (typeof error === 'string') { + setErrorMessage(error); + } else { + setErrorMessage(JSON.stringify(error)); + } + }, + pending: () => setStatus(Status.Pending), + notStarted: () => setStatus(Status.NotStarted), + }; + }, []); + + useEffect(() => { + if (!address) { + set.notStarted(); + } + + return () => { + set.notStarted(); + }; + }, [address, set]); + + const [errorMessage, setErrorMessage] = useState(''); + + const wrapActionWithStatus = (action: () => Promise) => async () => { + set.pending(); + try { + await action(); + set.success(); + } catch (error) { + set.error(error); + } + }; + + return { + status, + errorMessage, + setStatus: set, + wrapActionWithStatus, + }; +}; diff --git a/packages/example/pages/_app.tsx b/packages/example/pages/_app.tsx index 736720ee..c02709a6 100644 --- a/packages/example/pages/_app.tsx +++ b/packages/example/pages/_app.tsx @@ -32,7 +32,7 @@ function MyApp({ Component, pageProps, router }: AppProps): React.ReactElement { }, }} /> -
+
diff --git a/packages/example/pages/index.tsx b/packages/example/pages/index.tsx index 49df7259..2c26615f 100644 --- a/packages/example/pages/index.tsx +++ b/packages/example/pages/index.tsx @@ -213,7 +213,7 @@ export default function Home(): React.ReactElement {
-
+
react-celo
A{' '} diff --git a/packages/example/pages/wallet-test-plan.tsx b/packages/example/pages/wallet-test-plan.tsx new file mode 100644 index 00000000..32a50ed5 --- /dev/null +++ b/packages/example/pages/wallet-test-plan.tsx @@ -0,0 +1,48 @@ +import { CeloProvider, Mainnet, useCelo } from '@celo/react-celo'; +import React from 'react'; + +import { ConnectWalletCheck } from '../components/test-plan/connect-wallet'; +import { SendTransaction } from '../components/test-plan/send-transaction'; +import { SwitchNetwork } from '../components/test-plan/switch-networks'; +import { UpdateFeeCurrency } from '../components/test-plan/update-fee-currency'; + +export default function WalletTestPlan(): React.ReactElement { + const { destroy } = useCelo(); + return ( + +
+
Wallet Test Plan
+
+ A set of steps to help verify how well a given wallet interacts with + react-celo. +
+ You will need to connect a wallet before being able to run any other + test. +
+
+ +
+ + + + +
+
+ ); +} diff --git a/packages/example/pages/wallet.tsx b/packages/example/pages/wallet.tsx index 87eaf9ec..882b490b 100644 --- a/packages/example/pages/wallet.tsx +++ b/packages/example/pages/wallet.tsx @@ -443,7 +443,7 @@ export default function Wallet(): React.ReactElement { -
+
react-celo wallet
{ + const celo = await k.contracts.getGoldToken(); + await celo + .transfer( + // impact market contract + '0x73D20479390E1acdB243570b5B739655989412f5', + Web3.utils.toWei('0.00000001', 'ether') + ) + .sendAndWaitForReceipt({ + from: k.connection.defaultAccount, + gasPrice: k.connection.defaultGasPrice, + }); + }); +}