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

feat: add transaction components #787

Merged
merged 23 commits into from
Jul 16, 2024
Merged
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions site/docs/components/TransactionWrapper.tsx
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',
},
Copy link
Contributor

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.

Copy link
Contributor Author

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

];
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>
);
}
33 changes: 33 additions & 0 deletions site/docs/pages/transaction/transaction.mdx
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 />`

Copy link
Contributor

Choose a reason for hiding this comment

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

Add the

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

<App>
<TransactionWrapper>
{({ address, contracts }) => (
<Transaction address={address} contracts={contracts}>
<TransactionButton />
<TransactionGasFee>
<TransactionGasFeeLabel />
<TransactionGasFeeEstimate />
<TransactionGasFeeSponsoredBy />
</TransactionGasFee>
<TransactionStatus>
<TransactionStatusLabel />
<TransactionStatusAction />
</TransactionStatus>
</Transaction>
)}
</TransactionWrapper>
</App>
15 changes: 15 additions & 0 deletions site/sidebar.ts
Original file line number Diff line number Diff line change
@@ -233,6 +233,21 @@ export const sidebar = [
],
link: '/token/introduction',
},
{
text: 'Transaction',
items: [
{
text: 'Components',
items: [
{
text: 'Transaction',
link: '/transaction/transaction',
},
],
},
],
link: '/transaction/transaction',
},
{
text: 'Wallet',
items: [
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, 'w-full gap-2 flex flex-col')}>
{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(),
},
});

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i was able to get it working for TransactionButton but i tried to create a more complex one for the Transaction component and haven't been able to figure that one out yet which is why i created the docs pages

Copy link
Contributor

Choose a reason for hiding this comment

The 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 = {};
69 changes: 69 additions & 0 deletions src/transaction/components/TransactionButton.tsx
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,
Copy link
Contributor

Choose a reason for hiding this comment

The 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>
);
}
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>
);
}
33 changes: 27 additions & 6 deletions src/transaction/components/TransactionProvider.tsx
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';

Copy link
Contributor

Choose a reason for hiding this comment

The 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 (
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>;
}
15 changes: 15 additions & 0 deletions src/transaction/components/TransactionStatusLabel.tsx
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>
);
}
Loading