-
Notifications
You must be signed in to change notification settings - Fork 256
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
feat: add transaction components #787
Changes from 14 commits
68a9c3c
07ae4e2
4be06c8
b849325
4077953
b0e80b3
989c4ce
deb0960
41fdf9b
b65e28d
e9739f2
3c6558d
945efe7
d10e2de
f2add9e
153a02b
d10be27
1ecae71
dc1ee3e
69274b0
6345027
960449f
80fc5a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { ReactNode } from 'react'; | ||
import { erc20Abi } from 'viem'; | ||
import { useAccount } from 'wagmi'; | ||
import type { Abi, ContractFunctionName, Hex, Address } from 'viem'; | ||
|
||
// TODO: move to types file | ||
type Contract = { | ||
address: Address; | ||
abi: Abi; | ||
functionName: ContractFunctionName; | ||
args?: { to: Hex; data?: Hex; value?: bigint }[]; | ||
}; | ||
|
||
type TransactionWrapperChildren = { | ||
address: Address | undefined; | ||
contracts?: Contract[]; | ||
}; | ||
|
||
type TransactionWrapperReact = { | ||
children: (props: TransactionWrapperChildren) => ReactNode; | ||
}; | ||
|
||
export default function TransactionWrapper({ | ||
children, | ||
}: TransactionWrapperReact) { | ||
const { address } = useAccount(); | ||
|
||
// TODO: replace with actual contract | ||
const contracts: Contract[] = [ | ||
{ | ||
address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', | ||
abi: erc20Abi, | ||
functionName: 'approve', | ||
}, | ||
]; | ||
return ( | ||
<main className="flex flex-col"> | ||
<div className="flex items-center p-4 bg-white max-w-[450px] rounded-lg"> | ||
{children({ address, contracts })} | ||
</div> | ||
</main> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
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 App from '../../components/App'; | ||
|
||
# `<Transaction />` | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add the
|
||
<App> | ||
<TransactionWrapper> | ||
{({ address, contracts }) => ( | ||
<Transaction address={address} contracts={contracts}> | ||
<TransactionButton /> | ||
<TransactionGasFee> | ||
<TransactionGasFeeLabel /> | ||
<TransactionGasFeeEstimate /> | ||
<TransactionGasFeeSponsoredBy /> | ||
</TransactionGasFee> | ||
<TransactionStatus> | ||
<TransactionStatusLabel /> | ||
<TransactionStatusAction /> | ||
</TransactionStatus> | ||
</Transaction> | ||
)} | ||
</TransactionWrapper> | ||
</App> |
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, 'w-full gap-2 flex flex-col')}> | ||
{children} | ||
</div> | ||
</TransactionProvider> | ||
); | ||
} |
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(), | ||
}, | ||
}); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Out of curiosity, were you able to get this Story working, because I was trying build some complex story with different Wagmi providers, and they were not quite working. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i was able to get it working for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All good, I asked Sid to help us out with Storybook in the future. |
||
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 = {}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { useCallback } from 'react'; | ||
import { background, cn, pressable, text } from '../../styles/theme'; | ||
import { useTransactionContext } from './TransactionProvider'; | ||
import { writeContracts } from 'viem/experimental'; | ||
import { useConfig } from 'wagmi'; | ||
import { base } from 'viem/chains'; | ||
import { Spinner } from '../../internal/loading/Spinner'; | ||
import type { TransactionButtonReact } from '../types'; | ||
|
||
export function TransactionButton({ | ||
className, | ||
text: buttonText = 'Transact', | ||
}: TransactionButtonReact) { | ||
const { | ||
address, | ||
contracts, | ||
isLoading, | ||
setErrorMessage, | ||
setIsLoading, | ||
setTransactionId, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep alphabetical order |
||
transactionId, | ||
} = useTransactionContext(); | ||
|
||
const wagmiConfig = useConfig(); | ||
|
||
const handleSubmit = useCallback(async () => { | ||
try { | ||
setErrorMessage(''); | ||
setIsLoading(true); | ||
// const id = await writeContracts(wagmiConfig, { | ||
// contracts, | ||
// account: address, | ||
// chain: base, | ||
// }); | ||
|
||
// TODO: remove - for testing purposes only | ||
const id = 'transaction-id'; | ||
setTransactionId(id); | ||
} catch (err) { | ||
console.log({ err }); | ||
setErrorMessage('an error occurred'); | ||
} finally { | ||
setIsLoading(false); | ||
} | ||
}, []); | ||
|
||
// 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={handleSubmit} | ||
> | ||
{isLoading ? ( | ||
<Spinner /> | ||
) : ( | ||
<span className={cn(text.headline, 'text-inverse')}>{buttonText}</span> | ||
)} | ||
</button> | ||
); | ||
} |
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> | ||
); | ||
} |
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> | ||
); | ||
} |
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> | ||
); | ||
} |
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> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,9 @@ | ||
import { createContext, useContext } from 'react'; | ||
import type { TransactionContextType } from '../types'; | ||
import { createContext, useContext, useEffect, useState } from 'react'; | ||
import { useValue } from '../../internal/hooks/useValue'; | ||
import type { | ||
TransactionContextType, | ||
TransactionProviderReact, | ||
} from '../types'; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. types always at the end |
||
const emptyContext = {} as TransactionContextType; | ||
|
||
|
@@ -18,13 +21,31 @@ export function useTransactionContext() { | |
} | ||
|
||
export function TransactionProvider({ | ||
address, | ||
children, | ||
}: { | ||
children: React.ReactNode; | ||
}) { | ||
contracts, | ||
}: TransactionProviderReact) { | ||
const [isLoading, setIsLoading] = useState(false); | ||
const [errorMessage, setErrorMessage] = useState(''); | ||
const [transactionId, setTransactionId] = useState(''); | ||
const [gasFee, setGasFee] = useState(''); | ||
|
||
useEffect(() => { | ||
// TODO: replace with gas estimation call | ||
setGasFee('0.03'); | ||
}, []); | ||
|
||
const value = useValue({ | ||
address, | ||
contracts, | ||
error: undefined, | ||
loading: false, | ||
errorMessage, | ||
gasFee, | ||
isLoading, | ||
setErrorMessage, | ||
setIsLoading, | ||
transactionId, | ||
setTransactionId, | ||
}); | ||
|
||
return ( | ||
|
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> | ||
); | ||
} |
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>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { cn, text } from '../../styles/theme'; | ||
import { useGetTransactionStatus } from '../core/useGetTransactionStatus'; | ||
import type { TransactionStatusLabelReact } from '../types'; | ||
|
||
export function TransactionStatusLabel({ | ||
className, | ||
}: TransactionStatusLabelReact) { | ||
const { label, labelClassName } = useGetTransactionStatus(); | ||
|
||
return ( | ||
<div className={cn(text.label2, className)}> | ||
<p className={labelClassName}>{label}</p> | ||
</div> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that I think about it, if we want to test a contract in our docs, probably that should be something super chill on Base Sepolia. Something to think about it on Monday.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah agreed going to ask in the group for help on this