Skip to content

Commit

Permalink
feat: added transaction components (#787)
Browse files Browse the repository at this point in the history
  • Loading branch information
abcrane123 authored Jul 16, 2024
1 parent 5c6ce95 commit 816abce
Show file tree
Hide file tree
Showing 18 changed files with 529 additions and 9 deletions.
58 changes: 58 additions & 0 deletions site/docs/components/TransactionWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useAccount } from 'wagmi';
import type { Config } from 'wagmi';
import type { ReactNode } from 'react';
import type {
UseSendCallsParameters,
UseSendCallsReturnType,
} from 'wagmi/experimental';

type TransactionWrapperChildren = UseSendCallsReturnType<
Config,
unknown
>['sendCalls']['arguments'] & {
mutation?: UseSendCallsParameters<Config, unknown>['mutation'];
} & { address: string };

type TransactionWrapperReact = {
children: (props: TransactionWrapperChildren) => ReactNode;
};

const myNFTABI = [
{
stateMutability: 'nonpayable',
type: 'function',
inputs: [{ name: 'to', type: 'address' }],
name: 'safeMint',
outputs: [],
},
] as const;

const myNFTAddress = '0x119Ea671030FBf79AB93b436D2E20af6ea469a19';

export default function TransactionWrapper({
children,
}: TransactionWrapperReact) {
const { address } = useAccount();

const contracts = [
{
address: myNFTAddress,
abi: myNFTABI,
functionName: 'safeMint',
args: [address],
},
{
address: myNFTAddress,
abi: myNFTABI,
functionName: 'safeMint',
args: [address],
},
];
return (
<main className="flex flex-col">
<div className="flex max-w-[450px] items-center rounded-lg bg-white p-4">
{children({ address, contracts })}
</div>
</main>
);
}
51 changes: 51 additions & 0 deletions site/docs/pages/transaction/transaction.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import App from '../../components/App';
import { Avatar, Name } from '@coinbase/onchainkit/identity';
import { Transaction } from '../../../../src/transaction/components/Transaction';
import { TransactionGasFee } from '../../../../src/transaction/components/TransactionGasFee';
import { TransactionGasFeeLabel } from '../../../../src/transaction/components/TransactionGasFeeLabel';
import { TransactionGasFeeEstimate } from '../../../../src/transaction/components/TransactionGasFeeEstimate';
import { TransactionGasFeeSponsoredBy } from '../../../../src/transaction/components/TransactionGasFeeSponsoredBy';
import { TransactionButton } from '../../../../src/transaction/components/TransactionButton';
import { TransactionStatus } from '../../../../src/transaction/components/TransactionStatus';
import { TransactionStatusLabel } from '../../../../src/transaction/components/TransactionStatusLabel';
import { TransactionStatusAction } from '../../../../src/transaction/components/TransactionStatusAction';
import TransactionWrapper from '../../components/TransactionWrapper';
import { Wallet, ConnectWallet } from '../../../../src/wallet';

# `<Transaction />`

:::warning
Component is actively in development. Stay tuned for upcoming releases.
:::

<App>
<TransactionWrapper>
{({ address, contracts }) => {
if (address) {
return (
<Transaction address={address} contracts={contracts}>
<TransactionButton />
<TransactionGasFee>
<TransactionGasFeeLabel />
<TransactionGasFeeEstimate />
<TransactionGasFeeSponsoredBy />
</TransactionGasFee>
<TransactionStatus>
<TransactionStatusLabel />
<TransactionStatusAction />
</TransactionStatus>
</Transaction>
)
} else {
return (
<Wallet>
<ConnectWallet>
<Avatar className="h-6 w-6" />
<Name />
</ConnectWallet>
</Wallet>
)
}
}}
</TransactionWrapper>
</App>
15 changes: 15 additions & 0 deletions site/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,21 @@ export const sidebar = [
},
],
},
{
text: 'Transaction',
items: [
{
text: 'Components',
items: [
{
text: 'Transaction',
link: '/transaction/transaction',
},
],
},
],
link: '/transaction/transaction',
},
{
text: 'Wallet',
items: [
Expand Down
16 changes: 14 additions & 2 deletions src/transaction/components/Transaction.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import { TransactionProvider } from './TransactionProvider';
import { cn } from '../../styles/theme';
import type { TransactionReact } from '../types';

export function Transaction({ children }: TransactionReact) {
return <TransactionProvider>{children}</TransactionProvider>;
export function Transaction({
address,
className,
children,
contracts,
}: TransactionReact) {
return (
<TransactionProvider address={address} contracts={contracts}>
<div className={cn(className, 'flex w-full flex-col gap-2')}>
{children}
</div>
</TransactionProvider>
);
}
41 changes: 41 additions & 0 deletions src/transaction/components/TransactionButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Meta, StoryObj } from '@storybook/react';
import { TransactionProvider } from './TransactionProvider';
import { WagmiProvider, createConfig, http } from 'wagmi';
import { baseSepolia } from 'viem/chains';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { TransactionButton } from './TransactionButton';

const wagmiConfig = createConfig({
chains: [baseSepolia],
transports: {
[baseSepolia.id]: http(),
},
});

const meta = {
title: 'Transaction/Button',
decorators: [
(Story) => (
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={new QueryClient()}>
<TransactionProvider address="0x02feeb0AdE57b6adEEdE5A4EEea6Cf8c21BeB6B1">
<Story />
</TransactionProvider>
</QueryClientProvider>
</WagmiProvider>
),
],
component: TransactionButton,
tags: ['autodocs'],
argTypes: {
className: {
control: 'text',
},
},
} satisfies Meta<typeof TransactionButton>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Basic: Story = {};
35 changes: 35 additions & 0 deletions src/transaction/components/TransactionButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { background, cn, pressable, text } from '../../styles/theme';
import { useTransactionContext } from './TransactionProvider';
import { Spinner } from '../../internal/loading/Spinner';
import type { TransactionButtonReact } from '../types';

export function TransactionButton({
className,
text: buttonText = 'Transact',
}: TransactionButtonReact) {
const { address, contracts, isLoading, onSubmit, transactionId } =
useTransactionContext();

// TODO: should disable if transactionId exists ?
const isDisabled = isLoading || !contracts || !address || transactionId;

return (
<button
className={cn(
background.primary,
'w-full rounded-xl',
'mt-4 px-4 py-3 font-medium text-base text-white leading-6',
isDisabled && pressable.disabled,
text.headline,
className,
)}
onClick={onSubmit}
>
{isLoading ? (
<Spinner />
) : (
<span className={cn(text.headline, 'text-inverse')}>{buttonText}</span>
)}
</button>
);
}
11 changes: 11 additions & 0 deletions src/transaction/components/TransactionGasFee.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { cn } from '../../styles/theme';
import type { TransactionGasFeeReact } from '../types';

export function TransactionGasFee({
children,
className,
}: TransactionGasFeeReact) {
return (
<div className={cn('flex justify-between', className)}>{children}</div>
);
}
15 changes: 15 additions & 0 deletions src/transaction/components/TransactionGasFeeEstimate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { cn, color, text } from '../../styles/theme';
import { useTransactionContext } from './TransactionProvider';
import type { TransactionGasFeeEstimateReact } from '../types';

export function TransactionGasFeeEstimate({
className,
}: TransactionGasFeeEstimateReact) {
const { gasFee } = useTransactionContext();

return (
<div className={cn(text.label2, 'ml-auto', className)}>
{gasFee && <p className={color.foregroundMuted}>{`${gasFee} ETH`}</p>}
</div>
);
}
12 changes: 12 additions & 0 deletions src/transaction/components/TransactionGasFeeLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { cn, color, text } from '../../styles/theme';
import type { TransactionGasFeeLabelReact } from '../types';

export function TransactionGasFeeLabel({
className,
}: TransactionGasFeeLabelReact) {
return (
<div className={cn(text.label2, className)}>
<p className={color.foregroundMuted}>Gas fee</p>
</div>
);
}
18 changes: 18 additions & 0 deletions src/transaction/components/TransactionGasFeeSponsoredBy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { cn, color, text } from '../../styles/theme';
import type { TransactionGasFeeSponsoredByReact } from '../types';

export function TransactionGasFeeSponsoredBy({
className,
}: TransactionGasFeeSponsoredByReact) {
// TODO: replace with actual value
const sponsoredBy = 'Coinbase';

return (
<div className={cn(text.label2, 'pl-2', className)}>
<p className={color.foregroundMuted}>
{' '}Sponsored by{' '}
<span className={cn(text.label1, color.primary)}>{sponsoredBy}</span>
</p>
</div>
);
}
55 changes: 49 additions & 6 deletions src/transaction/components/TransactionProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { createContext, useContext } from 'react';
import type { TransactionContextType } from '../types';
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react';
import { useValue } from '../../internal/hooks/useValue';
import { useWriteContracts } from '../core/useWriteContracts';
import type {
TransactionContextType,
TransactionProviderReact,
} from '../types';

const emptyContext = {} as TransactionContextType;

Expand All @@ -18,13 +28,46 @@ export function useTransactionContext() {
}

export function TransactionProvider({
address,
children,
}: {
children: React.ReactNode;
}) {
contracts,
}: TransactionProviderReact) {
const [errorMessage, setErrorMessage] = useState('');
const [transactionId, setTransactionId] = useState('');
const [gasFee, setGasFee] = useState('');

const { status, writeContracts } = useWriteContracts({
setErrorMessage,
setTransactionId,
});

const handleSubmit = useCallback(async () => {
try {
setErrorMessage('');
await writeContracts({
contracts,
});
} catch (err) {
console.log({ err });
}
}, [contracts, writeContracts]);

useEffect(() => {
// TODO: replace with gas estimation call
setGasFee('0.03');
}, []);

const value = useValue({
address,
contracts,
error: undefined,
loading: false,
errorMessage,
gasFee,
isLoading: status === 'pending',
onSubmit: handleSubmit,
setErrorMessage,
transactionId,
setTransactionId,
});

return (
Expand Down
11 changes: 11 additions & 0 deletions src/transaction/components/TransactionStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { cn } from '../../styles/theme';
import type { TransactionStatusReact } from '../types';

export function TransactionStatus({
children,
className,
}: TransactionStatusReact) {
return (
<div className={cn('flex justify-between', className)}>{children}</div>
);
}
11 changes: 11 additions & 0 deletions src/transaction/components/TransactionStatusAction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { cn, text } from '../../styles/theme';
import { useGetTransactionStatus } from '../core/useGetTransactionStatus';
import type { TransactionStatusActionReact } from '../types';

export function TransactionStatusAction({
className,
}: TransactionStatusActionReact) {
const { actionElement } = useGetTransactionStatus();

return <div className={cn(text.label2, className)}>{actionElement}</div>;
}
Loading

0 comments on commit 816abce

Please sign in to comment.