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

Alec/pay 04 #1352

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/hot-rocks-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@coinbase/onchainkit': patch
---

**feat**: [Pay] components. by @0xAlec #1349
38 changes: 38 additions & 0 deletions playground/nextjs-app-router/app/api/createCharge/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ENVIRONMENT, ENVIRONMENT_VARIABLES } from '@/lib/constants';
import axios from 'axios';
import { NextResponse } from 'next/server';

type CommerceCharge = {
data: {
id: string;
};
};

export async function POST(req: Request) {
// Generates a chargeId
const { name, description } = await req.json();
try {
const response = await axios.post<CommerceCharge>(
'https://api.commerce.coinbase.com/charges/',
{
name,
description,
pricing_type: 'fixed_price',
local_price: {
amount: '0.01',
currency: 'USD',
},
},
{
headers: {
'Content-Type': 'application/json',
'X-CC-Api-Key': ENVIRONMENT_VARIABLES[ENVIRONMENT.COMMERCE_API_KEY],
},
},
);
return NextResponse.json({ id: response.data.data.id }, { status: 200 });
} catch (error) {
console.error(error);
}
return NextResponse.json({ status: 500 });
}
2 changes: 1 addition & 1 deletion playground/nextjs-app-router/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Demo from '@/components/Demo';

export default function Home() {
return (
<main className="flex min-h-screen w-full bg-muted/40">
<main className="flex min-h-screen w-full bg-muted/40 items-center justify-center">
<AppProvider>
<Demo />
</AppProvider>
Expand Down
Binary file modified playground/nextjs-app-router/bun.lockb
Binary file not shown.
38 changes: 37 additions & 1 deletion playground/nextjs-app-router/components/AppProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { useConnect, useConnectors } from 'wagmi';
import { WalletPreference } from './form/wallet-type';

export enum OnchainKitComponent {
Fund = 'fund',
Identity = 'identity',
Pay = 'pay',
Swap = 'swap',
Transaction = 'transaction',
Wallet = 'wallet',
Expand All @@ -22,6 +22,19 @@ export type Paymaster = {
url: string;
enabled: boolean;
};

export type PayOptions = {
name?: string;
description?: string;
price?: string;
productId?: string;
};

export enum PayTypes {
ChargeID = 'chargeId',
ProductID = 'productId',
}

type State = {
activeComponent?: OnchainKitComponent;
setActiveComponent?: (component: OnchainKitComponent) => void;
Expand All @@ -36,6 +49,10 @@ type State = {
setTransactionType?: (transactionType: TransactionTypes) => void;
paymasters?: Record<number, Paymaster>; // paymasters is per network
setPaymaster?: (chainId: number, url: string, enabled: boolean) => void;
payOptions?: PayOptions;
setPayOptions?: (payOptions: PayOptions) => void;
payTypes?: PayTypes;
setPayTypes?: (payTypes: PayTypes) => void;
};

const defaultState: State = {
Expand All @@ -56,6 +73,8 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const [transactionType, setTransactionTypeState] = useState<TransactionTypes>(
TransactionTypes.Contracts,
);
const [payOptions, setPayOptionsState] = useState<PayOptions>();
const [payTypes, setPayTypesState] = useState<PayTypes>(PayTypes.ProductID);
const [paymasters, setPaymastersState] =
useState<Record<number, Paymaster>>();
const [defaultMaxSlippage, setDefaultMaxSlippageState] = useState<number>(3);
Expand All @@ -69,6 +88,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const storedPaymasters = localStorage.getItem('paymasters');
const storedTransactionType = localStorage.getItem('transactionType');
const storedDefaultMaxSlippage = localStorage.getItem('defaultMaxSlippage');
const storedProductId = localStorage.getItem('productId');

if (storedActiveComponent) {
setActiveComponent(storedActiveComponent as OnchainKitComponent);
Expand All @@ -88,6 +108,9 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
if (storedDefaultMaxSlippage) {
setDefaultMaxSlippage(Number(storedDefaultMaxSlippage));
}
if (storedProductId) {
setPayOptions({ productId: storedProductId });
}
}, []);

// Connect to wallet if walletType changes
Expand Down Expand Up @@ -143,6 +166,15 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
setTransactionTypeState(transactionType);
};

const setPayOptions = (payOptions: PayOptions) => {
localStorage.setItem('productId', payOptions.productId || '');
setPayOptionsState(payOptions);
};

const setPayTypes = (payTypes: PayTypes) => {
setPayTypesState(payTypes);
};

return (
<AppContext.Provider
value={{
Expand All @@ -159,6 +191,10 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
setTransactionType,
defaultMaxSlippage,
setDefaultMaxSlippage,
payOptions,
setPayOptions,
payTypes,
setPayTypes,
}}
>
{children}
Expand Down
36 changes: 16 additions & 20 deletions playground/nextjs-app-router/components/Demo.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
'use client';
import { AppContext, OnchainKitComponent } from '@/components/AppProvider';
import { Chain } from '@/components/form/chain';
import { PaymasterUrl } from '@/components/form/paymaster';
import { SwapConfig } from '@/components/form/swap-config';
import { WalletType } from '@/components/form/wallet-type';
import { useContext, useEffect, useState } from 'react';
import FundDemo from './demo/Fund';
import IdentityDemo from './demo/Identity';
import PayDemo from './demo/Pay';
import SwapDemo from './demo/Swap';
import TransactionDemo from './demo/Transaction';
import WalletDemo from './demo/Wallet';
import { ActiveComponent } from './form/active-component';
import { TransactionOptions } from './form/transaction-options';
import { COMPONENT_OPTIONS } from './form/component-options';

function Demo() {
const { activeComponent } = useContext(AppContext);
Expand Down Expand Up @@ -41,10 +37,6 @@ function Demo() {
}`;

function renderActiveComponent() {
if (activeComponent === OnchainKitComponent.Fund) {
return <FundDemo />;
}

if (activeComponent === OnchainKitComponent.Identity) {
return <IdentityDemo />;
}
Expand All @@ -61,13 +53,19 @@ function Demo() {
return <WalletDemo />;
}

if (activeComponent === OnchainKitComponent.Pay) {
return <PayDemo />;
}

return <></>;
}

return (
<>
<div
className={`absolute top-0 right-0 bottom-0 left-0 z-20 flex w-full min-w-120 flex-col border-r bg-background p-6 transition-[height] sm:static sm:z-0 sm:w-1/4 ${sideBarVisible ? 'h-full min-h-screen' : 'h-[5rem] overflow-hidden'}`}
className={`absolute top-0 right-0 bottom-0 left-0 z-20 flex w-full min-w-120 flex-col border-r bg-background p-6 transition-[height] sm:static sm:z-0 sm:w-1/4 ${
sideBarVisible ? 'h-full min-h-screen' : 'h-[5rem] overflow-hidden'
}`}
>
<div className="mb-12 flex justify-between">
<div className="self-center font-semibold text-xl">
Expand All @@ -76,7 +74,9 @@ function Demo() {
<button
type="button"
onClick={toggleSidebar}
className={`${buttonStyles} px-1 transition-transform sm:hidden ${sideBarVisible ? '-rotate-90' : 'rotate-90'}`}
className={`${buttonStyles} px-1 transition-transform sm:hidden ${
sideBarVisible ? '-rotate-90' : 'rotate-90'
}`}
>
<span className="pl-2">&rang;</span>
</button>
Expand All @@ -86,15 +86,11 @@ function Demo() {
</button>
<form className="mt-4 grid gap-8">
<ActiveComponent />
<WalletType />
<Chain />
<TransactionOptions />
<PaymasterUrl />
<SwapConfig />
{activeComponent && COMPONENT_OPTIONS[activeComponent]()}
</form>
<div className="bottom-6 left-6 text-sm sm:absolute">
<div className="bottom-6 left-6 sm:absolute text-sm">
<a
className="opacity-100 transition-opacity duration-200 hover:opacity-70"
className="duration-200 hover:opacity-70 opacity-100 transition-opacity"
href="https://github.com/coinbase/onchainkit/tree/main/playground"
rel="noreferrer"
target="_blank"
Expand All @@ -103,7 +99,7 @@ function Demo() {
Github ↗
</a>
<a
className="pl-4 opacity-100 transition-opacity duration-200 hover:opacity-70"
className="duration-200 hover:opacity-70 opacity-100 pl-4 transition-opacity"
href="https://onchainkit.xyz"
rel="noreferrer"
target="_blank"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
'use client';
import { ENVIRONMENT, ENVIRONMENT_VARIABLES } from '@/lib/constants';
import { OnchainKitProvider } from '@coinbase/onchainkit';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { ReactNode } from 'react';
import { http, createConfig } from 'wagmi';
import { WagmiProvider } from 'wagmi';
import { base, baseSepolia } from 'wagmi/chains';
import { coinbaseWallet } from 'wagmi/connectors';
import { OnchainKitProvider } from '../onchainkit/src';

export const config = createConfig({
chains: [base, baseSepolia],
Expand Down
75 changes: 75 additions & 0 deletions playground/nextjs-app-router/components/demo/Pay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { ENVIRONMENT, ENVIRONMENT_VARIABLES } from '@/lib/constants';
import { useCallback, useMemo } from 'react';
import { useContext } from 'react';
import { Pay, PayButton, PayStatus } from '../../onchainkit/src/pay';
import { AppContext, PayTypes } from '../AppProvider';

function PayComponent() {
const { payTypes, payOptions } = useContext(AppContext);

const chargeIDKey = useMemo(() => {
return `${payOptions?.description}-${payOptions?.name}`;
}, [payOptions]);

const productIDKey = useMemo(() => {
return `${payOptions?.productId}`;
}, [payOptions]);

const handleOnStatus = useCallback((status: any) => {
console.log('Playground.Pay.onStatus:', status);
}, []);

const createCharge = useCallback(async () => {
try {
const res = await fetch(
`${ENVIRONMENT_VARIABLES[ENVIRONMENT.API_URL]}/api/createCharge`,
{
method: 'POST',
body: JSON.stringify(payOptions),
},
);
const data = await res.json();

return data.id;
} catch (error) {
console.error('Error creating charge:', error);
throw error;
}
}, [payOptions]);

const chargeIDDisabled = !payOptions?.description || !payOptions?.name;
const productIDDisabled = !payOptions?.productId;

return (
<div className="mx-auto grid w-1/2 gap-8">
{payTypes === PayTypes.ProductID && (
<>
<Pay
key={productIDKey}
productId={payOptions?.productId}
onStatus={handleOnStatus}
>
<PayButton coinbaseBranded={true} disabled={productIDDisabled} />
<PayStatus />
</Pay>
</>
)}
{payTypes === PayTypes.ChargeID && (
<>
<Pay
key={chargeIDKey}
chargeHandler={createCharge}
onStatus={handleOnStatus}
>
<PayButton coinbaseBranded={true} disabled={chargeIDDisabled} />
<PayStatus />
</Pay>
</>
)}
</div>
);
}

export default function PayDemo() {
return <PayComponent />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export function ActiveComponent() {
<SelectValue placeholder="Select component" />
</SelectTrigger>
<SelectContent>
<SelectItem value={OnchainKitComponent.Fund}>Fund</SelectItem>
<SelectItem value={OnchainKitComponent.Identity}>Identity</SelectItem>
<SelectItem value={OnchainKitComponent.Pay}>Pay</SelectItem>
<SelectItem value={OnchainKitComponent.Transaction}>
Transaction
</SelectItem>
Expand Down
53 changes: 53 additions & 0 deletions playground/nextjs-app-router/components/form/component-options.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { OnchainKitComponent } from '../AppProvider';
import { Chain } from './chain';
import { PayOptions } from './pay-options';
import { PaymasterUrl } from './paymaster';
import { SwapConfig } from './swap-config';
import { TransactionOptions } from './transaction-options';
import { WalletType } from './wallet-type';

export const COMPONENT_OPTIONS = {
[OnchainKitComponent.Pay]: () => {
return (
<>
<WalletType />
<PayOptions />
</>
);
},
[OnchainKitComponent.Swap]: () => {
return (
<>
<WalletType />
<Chain />
<SwapConfig />
</>
);
},
[OnchainKitComponent.Transaction]: () => {
return (
<>
<WalletType />
<Chain />
<PaymasterUrl />
<TransactionOptions />
</>
);
},
[OnchainKitComponent.Wallet]: () => {
return (
<>
<WalletType />
<Chain />
</>
);
},
[OnchainKitComponent.Identity]: () => {
return (
<>
<WalletType />
<Chain />
</>
);
},
};
Loading
Loading