Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
Expand Down
374 changes: 374 additions & 0 deletions docs/docs/api-reference/use-checkout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,374 @@
---
sidebar_position: 2
---

# useCheckout

The `useCheckout` hook enables [Instant Checkout](https://platform.openai.com/docs/actions/monetization) in ChatGPT apps, allowing users to complete purchases directly within ChatGPT. It wraps [`window.openai.requestCheckout`](https://developers.openai.com/apps-sdk/build/chatgpt-ui#instant-checkout) with type-safe state management following the [ACP (Agentic Commerce Protocol) specification](https://developers.openai.com/commerce/specs/checkout).

:::note
Instant Checkout is currently in private beta and limited to select marketplace partners.
:::

## Basic usage

```tsx
import { useCheckout, CheckoutSessionRequest } from "skybridge/web";

function CheckoutButton({ session }: { session: CheckoutSessionRequest }) {
const { requestCheckout, isPending, isSuccess, order } = useCheckout();

return (
<div>
<button
disabled={isPending}
onClick={() => requestCheckout(session)}
>
{isPending ? "Processing..." : "Checkout"}
</button>
{isSuccess && order && <p>Order completed: {order.id}</p>}
</div>
);
}
```

## Parameters

```tsx
const {
data,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do you still expose data in addition to order? I feel like this might be redundant

error,
isError,
isIdle,
isPending,
isSuccess,
status,
order,
sessionId,
requestCheckout,
requestCheckoutAsync,
} = useCheckout(options);

requestCheckout(session, {
onError,
onSettled,
onSuccess,
});

await requestCheckoutAsync(session);
```

### `options`

Optional configuration object:

```tsx
type UseCheckoutOptions = {
checkoutSessionIdGenerator?: () => string;
};
```

- `checkoutSessionIdGenerator` - Function to generate unique checkout session IDs. Defaults to `crypto.randomUUID()`. Useful for custom ID formats or testing.

## Returns

### `requestCheckout`

```tsx
requestCheckout: (session: CheckoutSessionRequest, sideEffects?: CheckoutSideEffects) => void
```

Initiates the checkout flow. Opens the ChatGPT Instant Checkout UI with the provided session data.

- `session: CheckoutSessionRequest`
- **Required**
- The checkout session configuration (see [Checkout Session](#checkout-session) below)
- `sideEffects: CheckoutSideEffects`
- Optional callbacks:
- `onSuccess: (data: CheckoutSuccessResponse) => void` - Fires on successful checkout
- `onError: (error: CheckoutErrorResponse | Error) => void` - Fires on checkout failure
- `onSettled: (data, error) => void` - Fires when checkout completes (success or error)

### `requestCheckoutAsync`

```tsx
requestCheckoutAsync: (session: CheckoutSessionRequest) => Promise<CheckoutSuccessResponse>
```

Same as `requestCheckout` but returns a promise for async/await usage.

### `status`

```tsx
status: "idle" | "pending" | "success" | "error"
```

- `idle` - Initial state, no checkout initiated
- `pending` - Checkout UI is open, awaiting user action
- `success` - Checkout completed successfully
- `error` - Checkout failed or was cancelled

### `isIdle`, `isPending`, `isSuccess`, `isError`

```tsx
isIdle: boolean;
isPending: boolean;
isSuccess: boolean;
isError: boolean;
```

Boolean flags derived from `status` for convenience.

### `data`

```tsx
data: CheckoutSuccessResponse | undefined
```

The successful checkout response. Only available when `status` is `"success"`.

```tsx
type CheckoutSuccessResponse = {
id: string; // Checkout session ID
status: "completed";
currency: string;
order: {
id: string; // Order ID
checkout_session_id: string;
permalink_url?: string; // Link to order confirmation
created_at?: string;
status?: CheckoutOrderStatus;
};
};
```

### `error`

```tsx
error: CheckoutErrorResponse | Error | undefined
```

The error if checkout failed. Only available when `status` is `"error"`.

```tsx
type CheckoutErrorResponse = {
code: CheckoutErrorCode; // "payment_declined" | "requires_3ds" | "cancelled" | ...
message: string;
};
```

### `order`

```tsx
order: CheckoutOrder | undefined
```

Convenience accessor for the order details from the successful checkout. Equivalent to `data?.order`. Only available when `status` is `"success"`.

```tsx
type CheckoutOrder = {
id: string; // Order ID
checkout_session_id: string;
permalink_url?: string; // Link to order confirmation
created_at?: string;
status?: CheckoutOrderStatus;
};
```

### `sessionId`

```tsx
sessionId: string | undefined
```

The checkout session ID used for the current or most recent checkout operation. This is either the ID provided in the `CheckoutSessionRequest` or auto-generated using the `checkoutSessionIdGenerator`.

## Checkout Session

The `CheckoutSessionRequest` defines what the user is purchasing:

```tsx
type CheckoutSessionRequest = {
id: string; // Unique session ID
payment_provider: {
provider: string; // PSP name (e.g., "stripe", "adyen")
merchant_id: string; // Your merchant ID from PSP
supported_payment_methods?: SupportedPaymentMethod[];
};
status: CheckoutSessionStatus; // Usually "ready_for_payment"
currency: string; // ISO 4217 code (e.g., "USD")
totals: CheckoutTotal[]; // Price breakdown
links?: CheckoutLink[]; // Legal/policy links
payment_mode?: "live" | "test"; // Use "test" for test cards
line_items?: CheckoutLineItem[]; // Items being purchased
merchant_name?: string;
order_reference?: string;
};
```

## Examples

### With Side Effects

```tsx
import { useCheckout, CheckoutSessionRequest } from "skybridge/web";

function CheckoutWidget({ session }: { session: CheckoutSessionRequest }) {
const { requestCheckout, isPending, isError, error } = useCheckout();

const handleCheckout = () => {
requestCheckout(session, {
onSuccess: (data) => {
console.log("Order completed:", data.order.id);
},
onError: (error) => {
if ("code" in error) {
console.error("Checkout error:", error.code, error.message);
} else {
console.error("Unexpected error:", error.message);
}
},
});
};

return (
<div>
<button disabled={isPending} onClick={handleCheckout}>
{isPending ? "Processing..." : "Complete Purchase"}
</button>
{isError && <p>Error: {"message" in error ? error.message : String(error)}</p>}
</div>
);
}
```

### Async/Await Pattern

```tsx
import { useCheckout, CheckoutSessionRequest } from "skybridge/web";

function AsyncCheckout({ session }: { session: CheckoutSessionRequest }) {
const { requestCheckoutAsync, isPending } = useCheckout();

const handleCheckout = async () => {
try {
const result = await requestCheckoutAsync(session);
console.log("Order ID:", result.order.id);
// Redirect to confirmation or update UI
} catch (error) {
console.error("Checkout failed:", error);
}
};

return (
<button disabled={isPending} onClick={handleCheckout}>
Pay Now
</button>
);
}
```

### Building a Checkout Session

```tsx
import { useCheckout, CheckoutSessionRequest } from "skybridge/web";

function ProductCheckout({ productId, price }: { productId: string; price: number }) {
const { requestCheckout, isPending } = useCheckout();

const handleCheckout = () => {
const session: CheckoutSessionRequest = {
id: `checkout_${Date.now()}`,
payment_provider: {
provider: "stripe",
merchant_id: "your_merchant_id",
supported_payment_methods: ["card", "apple_pay", "google_pay"],
},
status: "ready_for_payment",
currency: "USD",
totals: [
{ type: "subtotal", display_text: "Subtotal", amount: price },
{ type: "tax", display_text: "Tax", amount: Math.round(price * 0.08) },
{ type: "total", display_text: "Total", amount: Math.round(price * 1.08) },
],
links: [
{ type: "terms_of_use", url: "https://example.com/terms" },
{ type: "privacy_policy", url: "https://example.com/privacy" },
],
payment_mode: "live",
};

requestCheckout(session);
};

return (
<button disabled={isPending} onClick={handleCheckout}>
Buy for ${(price / 100).toFixed(2)}
</button>
);
}
```

### Test Mode

Use `payment_mode: "test"` to test with test cards (e.g., 4242 4242 4242 4242):

```tsx
const testSession: CheckoutSessionRequest = {
// ...other fields
payment_mode: "test",
};
```

### Using Order and Session ID

```tsx
import { useCheckout, CheckoutSessionRequest } from "skybridge/web";

function CheckoutStatus({ session }: { session: CheckoutSessionRequest }) {
const { requestCheckout, isPending, isSuccess, order, sessionId } = useCheckout();

return (
<div>
<button disabled={isPending} onClick={() => requestCheckout(session)}>
{isPending ? "Processing..." : "Checkout"}
</button>
{sessionId && <p>Session ID: {sessionId}</p>}
{isSuccess && order && (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
{isSuccess && order && (
{isSuccess && (

I feel like isSuccess should be sufficient for order to be defined

<div>
<p>Order completed: {order.id}</p>
{order.permalink_url && (
<a href={order.permalink_url}>View order details</a>
)}
</div>
)}
</div>
);
}
```

### Custom Session ID Generator

```tsx
import { useCheckout } from "skybridge/web";

function CheckoutWithCustomId() {
const { requestCheckout, sessionId } = useCheckout({
checkoutSessionIdGenerator: () => `session_${Date.now()}_${Math.random().toString(36).slice(2)}`,
});

// sessionId will use your custom format
return (
<button onClick={() => requestCheckout(session)}>
Checkout
</button>
);
}
```

## Related

- [OpenAI Monetization Docs](https://platform.openai.com/docs/actions/monetization)
- [ACP Checkout Specification](https://developers.openai.com/commerce/specs/checkout)
- [Stripe Agentic Commerce](https://docs.stripe.com/agentic-commerce/apps)
- [Adyen Agentic Commerce](https://docs.adyen.com/online-payments/agentic-commerce)
2 changes: 1 addition & 1 deletion packages/core/src/server/widgetsDevServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const widgetsDevServer = async (): Promise<RequestHandler> => {
webAppRoot,
);

const { build, preview, ...devConfig } = configResult?.config || {};
const { ...devConfig } = configResult?.config || {};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think you might have a conflicting version of biome on your IDE (or at least different that the one being used on this project). We experienced a few issues recently on that front: the extreme step of re downloading the project from Github did the trick for most of the team.

Anyhow, this should not be part of this PR

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Removing build and preview from destructuring means these properties will now be passed to createServer via ...devConfig. While Vite likely ignores unknown properties, this unrelated change wasn't mentioned in the PR description and could introduce unexpected behavior.


const vite = await createServer({
...devConfig,
Expand Down
Loading