Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Improve transaction feedback #90

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ module.exports = {
"@typescript-eslint/no-unnecessary-type-assertion": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-misused-promises": "off",
"react-hooks-addons/no-unused-deps": ["warn", { effectComment: "used" }]
"react-hooks-addons/no-unused-deps": ["warn", { effectComment: "used" }],
"@typescript-eslint/no-unused-vars": ["warn", { vars: "local", args: "after-used", ignoreRestSiblings: true }]
},
globals: {
JSX: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/good-design/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
"@babel/preset-react": "^7.16.0",
"@babel/preset-typescript": "^7.16.0",
"@babel/runtime": "^7.18.9",
"@gooddollar/web3sdk-v2": "latest",
"@storybook/addon-actions": "^6.5.12",
"@storybook/addon-essentials": "^6.5.12",
"@storybook/addon-links": "^6.5.12",
Expand Down Expand Up @@ -73,6 +72,7 @@
"@babel/core": "^7.18.10",
"@babel/runtime": "^7.18.9",
"@gooddollar/goodprotocol": "^2.0.3",
"@gooddollar/web3sdk-v2": "workspace:^",
"@lingui/macro": "^3.14.0",
"@lingui/react": "^3.14.0",
"@magiklabs/react-sdk": "^1.0.8",
Expand Down
2 changes: 1 addition & 1 deletion packages/good-design/src/core/layout/ActionHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Title } from "./";

interface ActionHeaderProps {
textColor: any;
/** text to complete the 'To complete this action, ...' sentence */
/** text to complete the 'To complete this action, ...' copy */
actionText: string;
}

Expand Down
3 changes: 1 addition & 2 deletions packages/good-design/src/core/web3/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from "./WalletAndChainGuard";
export * from "./SwitchChainModal";
export * from "./modals";
export * from "./ExplorerLink";
export * from "./KimaModal";
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { useEffect } from "react";
import { useModal } from "../../hooks/useModal";
import { useModal } from "../../../hooks/useModal";
import { Box, Text } from "native-base";
import { LearnButton } from "../buttons";
import { Title } from "../layout";
import { LearnButton } from "../../buttons";
import { Title } from "../../layout";

type BridgeNetworks = {
origin: string;
Expand Down
96 changes: 96 additions & 0 deletions packages/good-design/src/core/web3/modals/SignTxModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { useEffect } from "react";
import { Box, Heading, useColorModeValue, useToast, Text } from "native-base";
import { useNotifications } from "@usedapp/core";
import type { TransactionReceipt, TransactionRequest, TransactionResponse } from "@ethersproject/abstract-provider";
import { useModal } from "../../../hooks/useModal";
import { ActionHeader } from "../../layout";
import { LearnButton } from "../../buttons";

// usedapp type definitions without walletConnected
type NotificationPayload = { submittedAt: number } & (
| { type: "transactionPendingSignature"; transactionName?: string; transactionRequest?: TransactionRequest }
| { type: "transactionStarted"; transaction: TransactionResponse; transactionName?: string }
| {
type: "transactionSucceed";
transaction: TransactionResponse;
receipt: TransactionReceipt;
transactionName?: string;
originalTransaction?: TransactionResponse;
}
| {
type: "transactionFailed";
transaction: TransactionResponse;
receipt: TransactionReceipt;
transactionName?: string;
originalTransaction?: TransactionResponse;
}
);

export type Notification = { id: string } & NotificationPayload;

//todo: add proper (customizable) styles
const SimpleTxToast = ({ title, desc }: { title: string; desc: string }) => (
<Box backgroundColor="main" px={2} py={2}>
<Heading>{title}</Heading>
<Text> txName: {desc} </Text>
</Box>
);

export type SignTxProps = {
children?: any;
} & ({ withToast: boolean; onSubmit?: never } | { withToast?: never; onSubmit: () => void });

/**
* A modal to wrap your component or page with and show a modal re-active to a
* pending signature for a usedapp useContractFunction call
* it assumes you have already wrapped your app with the Web3Provider out of the @gooddollar/sdk-v2 package
* @param children
* @param withToast - if true, will show a simple toast
* @param onSubmitted - if withToast false, provide a callback to handle what happens after tx is submitted
* @returns JSX.Element
*/
export const SignTxModal = ({ children, withToast, onSubmit }: SignTxProps) => {
const doWithToast = withToast;
const { notifications } = useNotifications();
const toast = useToast();
const textColor = useColorModeValue("goodGrey.500", "white");

const { Modal, showModal, hideModal } = useModal();

useEffect(() => {
const localNotif = notifications as Notification[];
if (localNotif[0]?.type.includes("transaction")) {
const { type, transactionName = "" } = localNotif[0];
switch (type) {
case "transactionPendingSignature":
showModal();
break;
case "transactionStarted":
hideModal();
if (doWithToast) {
toast.show({
render: () => <SimpleTxToast title="Transaction submitted" desc={transactionName} />,
placement: "top-right"
});
} else {
onSubmit?.();
}
break;
default:
hideModal();
break;
}
}
}, [notifications]);

return (
<React.Fragment>
<Modal
header={<ActionHeader textColor={textColor} actionText={`continue in your wallet`} />}
body={<LearnButton source="signing" />}
closeText="x"
/>
{children}
</React.Fragment>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import React, { useEffect, useState } from "react";
import { useConfig } from "@usedapp/core";
import { useSwitchNetwork } from "@gooddollar/web3sdk-v2";
import { find } from "lodash";
import { useModal } from "../../hooks/useModal";
import { ActionHeader } from "../layout";
import { LearnButton } from "../buttons";
import { useModal } from "../../../hooks/useModal";
import { ActionHeader } from "../../layout";
import { LearnButton } from "../../buttons";

export interface SwitchChainProps {
children?: any;
Expand All @@ -14,7 +14,6 @@ export interface SwitchChainProps {
/**
* A modal to wrap your component or page with and show a modal re-active to switchChain requests
* it assumes you have already wrapped your app with the Web3Provider out of the @gooddollar/sdk-v2 package
* @param {boolean} switching indicating if there is a pending switch request triggered
* @param children
* @returns JSX.Element
*/
Expand All @@ -37,7 +36,6 @@ export const SwitchChainModal = ({ children }: SwitchChainProps) => {
}
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setOnSwitchNetwork]);

const networkName = find(config.networks, _ => _.chainId === requestedChain)?.chainName;
Expand Down
3 changes: 3 additions & 0 deletions packages/good-design/src/core/web3/modals/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./SwitchChainModal";
export * from "./SignTxModal";
export * from "./KimaModal";
5 changes: 4 additions & 1 deletion packages/good-design/src/stories/W3Wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ const config: Config = {
readOnlyUrls: {
122: "https://rpc.fuse.io",
42220: "https://forno.celo.org"
},
notifications: {
expirationPeriod: 0
}
};

Expand All @@ -30,7 +33,7 @@ export const W3Wrapper = ({ children, withMetaMask, env = "fuse" }: PageProps) =

if (!withMetaMask) {
const rpc = new ethers.providers.JsonRpcProvider("https://rpc.fuse.io");

rpc.getSigner = () => w as any;
setProvider(rpc);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { WalletAndChainGuard } from "../../../core/web3/WalletAndChainGuard";
import React from "react";
import { MicroBridgeController } from "../../../apps/bridge/MicroBridgeController";
import { W3Wrapper } from "../../W3Wrapper";
import { SwitchChainModal } from "../../../core/web3/SwitchChainModal";
import { SwitchChainModal } from "../../../core/web3/modals/SwitchChainModal";

export default {
title: "Apps/MicroBridgeController",
component: MicroBridgeController,
decorators: [
(Story:any) => (
(Story: any) => (
<W3Wrapper withMetaMask={true} env="fuse">
<SwitchChainModal>
<WalletAndChainGuard validChains={[122, 42220]}>
Expand Down
114 changes: 114 additions & 0 deletions packages/good-design/src/stories/core/web3/SignTxModal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React, { useCallback, useEffect, useState } from "react";
import { SignTxModal } from "../../../core";
import { W3Wrapper } from "../../W3Wrapper";
import BaseButton from "../../../core/buttons/BaseButton";
import { Box, HStack, Center, Heading } from "native-base";
import { useNotifications, useEthers } from "@usedapp/core";
import { ethers } from "ethers";
import { JsonRpcSigner } from "@ethersproject/providers";
import useTestContractFunction from "../../hooks/useTestContractFunction";

const SignTxModalExample = () => {
const { notifications } = useNotifications();
const [signer, setSigner] = useState<undefined | JsonRpcSigner>(undefined);
const { send, transferState } = useTestContractFunction({ signer });
const { activateBrowserWallet, library, account } = useEthers();
const [connected, setConnected] = useState<boolean>(false);

const connect = useCallback(async () => {
await (activateBrowserWallet as any)();
setConnected(true);
}, []);

useEffect(() => {
if (library && !signer && account) {
const signer = (library as ethers.providers.JsonRpcProvider)?.getSigner();
setSigner(signer);
}
}, [library, connected]);

useEffect(() => {
console.log("notifications -->", { notifications, transferState });
}, [notifications, transferState]);

return (
<SignTxModal withToast>
<Box w="50%" h="50%" display="flex" flexDir="row" justifyContent="center" alignItems="center">
{!connected ? (
<BaseButton
onPress={connect}
backgroundColor="main"
px="2"
py="2"
_hover={{ bg: "blue.200" }}
innerText={{ color: "black" }}
text={"Connect Browser Wallet"}
/>
) : (
<>
<BaseButton
onPress={send}
backgroundColor="main"
px="2"
py="2"
_hover={{ bg: "blue.200" }}
innerText={{ color: "black" }}
text={"Transfer 10G$ to self"}
/>
</>
)}
</Box>
<Box display="flex" justifyContent="center" alignItems="center">
{notifications.map(notification => {
// this first if check is needed because of a weird type inference
if (notification.type === "walletConnected") return;
else if (notification.type.includes("transaction"))
return (
<Box key={notification.id}>
<Heading>Notifications</Heading>
<HStack
key={notification.id}
space={1}
alignItems="center"
borderWidth="1"
borderColor="black"
py="1"
px="1"
>
<Center px="1" borderWidth="1" borderColor="blue">
id: {notification.id}{" "}
</Center>
<Center px="1" borderWidth="1" borderColor="blue">
type: {notification.type}{" "}
</Center>
<Center px="1" borderWidth="1" borderColor="blue">
transactionName: {notification.transactionName}{" "}
</Center>
<Center px="1" borderWidth="1" borderColor="blue">
submittedAt: {notification.submittedAt}{" "}
</Center>
</HStack>
</Box>
);
})}
</Box>
</SignTxModal>
);
};

export default {
title: "Core/Web3/SignTxModal",
component: SignTxModalExample,
decorators: [
(Story: any) => (
<W3Wrapper withMetaMask={true} env="fuse">
<Story />
</W3Wrapper>
)
],
argTypes: {}
};

export const SignTxModalStory = {
args: {}
};
37 changes: 37 additions & 0 deletions packages/good-design/src/stories/hooks/useTestContractFunction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { useCallback, useState } from "react";
import { useEthers } from "@usedapp/core";
import { ethers, Contract } from "ethers";
import { JsonRpcSigner } from "@ethersproject/providers";
import { useContractFunctionWithDefaultGasFees } from "@gooddollar/web3sdk-v2";
import Contracts from "@gooddollar/goodprotocol/releases/deployment.json";
// import {
// IGoodDollar,
// } from "@gooddollar/goodprotocol/types";
import GoodDollarABI from "@gooddollar/goodprotocol/artifacts/abis/IGoodDollar.min.json";

interface CustomProps {
signer?: JsonRpcSigner;
}

const useTestContractFunction = (params: CustomProps) => {
const { signer } = params;
const { account } = useEthers();
const gdContract = new Contract(Contracts["fuse"].GoodDollar, GoodDollarABI.abi, signer) as any;

const { state: transferState, send: sendTransfer } = useContractFunctionWithDefaultGasFees(
gdContract,
"transferAndCall",
{
transactionName: "SelfTransfer"
}
);

const send = useCallback(async () => {
const callData = ethers.constants.HashZero;
await sendTransfer(account, "10000", callData);
}, [account, sendTransfer, signer]);

return { send, transferState };
};

export default useTestContractFunction;
3 changes: 2 additions & 1 deletion packages/sdk-v2/src/sdk/base/hooks/useGasFees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ export const useGasFees = () => {
export function useContractFunctionWithDefaultGasFees<T extends TypedContract, FN extends ContractFunctionNames<T>>(
contract: T | Falsy,
functionName: FN,
options?: TransactionOptions
customOptions?: TransactionOptions
) {
const options = { enablePendingSignatureNotification: true, ...customOptions };
const { send, ...rest } = useContractFunction(contract, functionName, options);
const gasFees = useGasFees();
const newSend = useCallback(
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-v2/src/sdk/claim/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const useClaim = (refresh: QueryParams["refresh"] = "never") => {

const ubi = useGetContract("UBIScheme", true, "claim", chainId) as UBIScheme;
const identity = useGetContract("Identity", true, "claim", chainId) as IIdentity;
const claimCall = useContractFunctionWithDefaultGasFees(ubi, "claim");
const claimCall = useContractFunctionWithDefaultGasFees(ubi, "claim", { transactionName: "Claimed Daily UBI" });

const results = useCalls(
[
Expand Down
Loading