Skip to content

Feature/use checkout hook#102

Open
Rodriguespn wants to merge 8 commits intoalpic-ai:mainfrom
Rodriguespn:feature/use-checkout-hook
Open

Feature/use checkout hook#102
Rodriguespn wants to merge 8 commits intoalpic-ai:mainfrom
Rodriguespn:feature/use-checkout-hook

Conversation

@Rodriguespn
Copy link
Copy Markdown
Contributor

@Rodriguespn Rodriguespn commented Dec 16, 2025

Greptile Overview

Greptile Summary

This PR adds Instant Checkout support for ChatGPT apps through a new useCheckout hook, following the ACP specification. The implementation includes comprehensive TypeScript types, thorough test coverage, and good documentation.

Major changes:

  • New useCheckout hook for payment processing with window.openai.requestCheckout
  • Refactored useCallTool to use shared useAsyncOperation hook for state management
  • Added useAsyncOperation hook to extract common async operation patterns with deduplication support
  • Complete TypeScript type definitions for checkout sessions, responses, and errors
  • Comprehensive test suites for both new hooks (443 lines for checkout, 411 lines for async operations)
  • Detailed API documentation with multiple usage examples

Issues found:

  • Minor state inconsistency risk with sessionId when deduplication is enabled
  • Unrelated change in widgetsDevServer.ts that removes unused variable destructuring

The refactoring is well-executed, consolidating duplicate state management logic into a reusable hook. The checkout implementation follows existing patterns from useCallTool, providing both callback-based and async/await APIs.

Confidence Score: 4/5

  • This PR is safe to merge with minor considerations for the identified state management edge case
  • The implementation is well-designed with comprehensive tests, clear documentation, and follows established patterns in the codebase. The refactoring consolidates duplicate code effectively. The identified sessionId state inconsistency is a minor edge case that would only manifest under rapid sequential checkout calls, which is unlikely in practice. The unrelated widgetsDevServer change is cosmetic and unlikely to cause issues.
  • The use-checkout.ts file could benefit from addressing the sessionId state management if handling rapid sequential checkouts is a requirement

@fredericbarthelet
Copy link
Copy Markdown
Contributor

Thanks @Rodriguespn for taking on this one. Let me know when you want to have a first review to discuss the API and the implementation ;)

@fredericbarthelet fredericbarthelet self-assigned this Dec 16, 2025
@Rodriguespn
Copy link
Copy Markdown
Contributor Author

Thanks @Rodriguespn for taking on this one. Let me know when you want to have a first review to discuss the API and the implementation ;)

Hey @fredericbarthelet, I've marked the PR as a review because I didn't have the time to properly test it on chatgpt. The implementation seems to be ready to be tested (it's vibe coded so I would proceed with caution). Will try to find some time between today and tomorrow to test this on chatgpt but if you find the time yourself feel free to test it on your end.

@Rodriguespn Rodriguespn force-pushed the feature/use-checkout-hook branch from 05e3525 to faa9ba7 Compare December 17, 2025 09:21
@Rodriguespn Rodriguespn marked this pull request as ready for review December 19, 2025 17:05
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Additional Comments (1)

  1. src/web/hooks/use-checkout.ts, line 155-163 (link)

    logic: double state update when requestCheckout returns an error response - state is set here (lines 157-161) then thrown and caught by the catch block below which sets state again (lines 179-183)

    this causes redundant setCheckoutState calls. consider removing the throw on line 162 and just returning early after setting the error state, or remove the state update here and let the catch block handle it

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

5 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@Rodriguespn Rodriguespn force-pushed the feature/use-checkout-hook branch from 9ce2143 to 17007c5 Compare December 19, 2025 17:19
@Rodriguespn
Copy link
Copy Markdown
Contributor Author

@fredericbarthelet tested and ready to be reviewed. I've also added a examples package with a very simple checkout app using Skybridge and Stripe based on an app made by xmcp. Let me know if you guys want to keep it. Happy to remove it if not 😄

@Rodriguespn Rodriguespn force-pushed the feature/use-checkout-hook branch from 1d7666c to c781f67 Compare December 23, 2025 13:05
@Rodriguespn
Copy link
Copy Markdown
Contributor Author

Hey @fredericbarthelet @qchuchu, just checking if this feature still makes sense to be in Skybridge. Happy to steer this in the right direction.

@fredericbarthelet
Copy link
Copy Markdown
Contributor

Hey @Rodriguespn, this feature still makes sense. Sorry for not reviewing this earlier. Adapting for MCP App compatibility took a bit of bandwitdth.

I've red your implementations of useCheckout. Thanks for putting this together! It achieves a great lot and we're looking forward to release this in Skybridge.

I need to ask for a few changes in order to make to move this forward:

  • the exemple app does not leverage useCheckout hook you just added. It will be difficult to provide an example implementation ATM since only a subset of approved partners have access to the feature. We're fine leaving the example implementation outside the scope of this PR in the meantime
  • please do not use in file comments as documentation
  • the codebase of useCheckout contains a lot of duplicated code from useCallTool. Might be worth refactor and consolidate the shared code base.
  • there are specificities in checkout implementation that diverges from a pure copy of useCallTool. Namely:
    • the unique checkout session id generation (the hook can provide one by default and be overridable by hook argument checkoutSessionIdGenerator which by default could be checkoutSessionIdGenerator: () => randomUUID(). You should expose this id in the return payload of the hook as well
    • the response that is an order straight away instead of a data attribute
  • the ACP implementation from OpenAI requires a server tool implementation as well - docs. This tool could be configured and properly typed with an additional superset method of the Server class exposed by skybridge/server. Could you make an implementation proposal in a separate PR at a later stage too :)?

WDYT ?

Rodriguespn and others added 8 commits January 6, 2026 22:21
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement Fred's suggestions #2 and alpic-ai#3:

**Code Consolidation (#2):**
- Create shared useAsyncOperation base hook
- Extract common async state management patterns
- Refactor useCheckout to use base hook
- Refactor useCallTool to use base hook with deduplication
- Eliminate ~80-100 lines of duplicated code

**Checkout-Specific Features (alpic-ai#3):**
- Add automatic session ID generation with crypto.randomUUID()
- Add UseCheckoutOptions for custom session ID generator
- Expose sessionId directly in useCheckout return value
- Expose order directly in useCheckout return value
- Maintain error discrimination with isCheckoutErrorResponse

**Tests:**
- Add 7 new tests for session ID generation and order access
- All 95 tests passing (up from 88)
- Full backward compatibility maintained

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@Rodriguespn Rodriguespn force-pushed the feature/use-checkout-hook branch from c781f67 to 8605a5d Compare January 6, 2026 23:44
@Rodriguespn
Copy link
Copy Markdown
Contributor Author

Hey @fred! Thanks for the feedback and patience. I've addressed all your review points you mentioned besides the server tool implementation for ACP implementation:

  • Code Consolidation: Created a shared useAsyncOperation base hook that extracts the common async state management pattern. Both useCheckout and useCallTool now use this base, which eliminated around 80-100 lines of duplicated code. The refactor maintains full backward compatibility with all 95 tests passing.

  • Checkout-Specific Features: Added automatic session ID generation with crypto.randomUUID() that's overridable via the checkoutSessionIdGenerator option. The sessionId is now exposed directly in the hook's return value. Also changed the response to return order directly instead of nesting it in a data attribute. Added 7 new tests covering session ID generation and order access patterns.

  • Documentation: Removed all JSDoc and in-file comments from hook files. Improved the use-checkout.md documentation with comprehensive examples including session ID generation and a full API reference.

  • Maintenance: Upgraded biome from 2.3.10 to 2.3.11 to align with the version of the dev dependency on package.json. Also removed unused variables in packages/core/src/server/widgetsDevServer.ts that were causing format issues when running pnpm run test:format. Happy to revert these changes or open a new PR for them.

Removed the example app since is out of scope, removed in-file comments and moved them to proper docs, consolidated the shared codebase with the new base hook, and implemented the checkout-specific features you mentioned.

Regarding the ACP implementation for OpenAI's server tool, happy to tackle that in a separate PR later as you suggested.


```tsx
const {
data,
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.

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

{isPending ? "Processing..." : "Checkout"}
</button>
{sessionId && <p>Session ID: {sessionId}</p>}
{isSuccess && order && (
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.

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

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

);

const { build, preview, ...devConfig } = configResult?.config || {};
const { ...devConfig } = configResult?.config || {};
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.

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

Comment on lines +29 to +32
let resolveOperation: (value: string) => void;
const operationPromise = new Promise<string>((resolve) => {
resolveOperation = resolve;
});
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.

You have a very convenient Promise syntax to create such "to be resolved" promise: Promise.withResolvers

Comment on lines +49 to +51
export type UseAsyncOperationConfig = {
enableDeduplication?: boolean;
};
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.

All the implementations of this hook seem to enable deduplication. I feel like this config parameter is not useful

error: TError;
};

export type AsyncOperationState<TData, TError = unknown> =
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.

There is not a single location where AsyncOperationState is defined without its generic types, so you can drop the default generic value

Suggested change
export type AsyncOperationState<TData, TError = unknown> =
export type AsyncOperationState<TData, TError> =

@fredericbarthelet
Copy link
Copy Markdown
Contributor

fredericbarthelet commented Jan 8, 2026

@greptile could you review again?

Comment on lines 70 to +78
const execute = async (
toolArgs: ToolArgs,
): Promise<CombinedCallToolResponse> => {
const callId = ++callIdRef.current;
setCallToolState({ status: "pending", data: undefined, error: undefined });

try {
const data = await adaptor.callTool<ToolArgs, CombinedCallToolResponse>(
return executeAsync(async () => {
return adaptor.callTool<ToolArgs, CombinedCallToolResponse>(
name,
toolArgs,
);
if (callId === callIdRef.current) {
setCallToolState({ status: "success", data, error: undefined });
}

return data;
} catch (error) {
if (callId === callIdRef.current) {
setCallToolState({ status: "error", data: undefined, error });
}
throw error;
}
});
Copy link
Copy Markdown
Contributor

@fredericbarthelet fredericbarthelet Jan 8, 2026

Choose a reason for hiding this comment

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

why not use execute returned by useAsyncOperation directly and instead redefining a monad here?

Copy link
Copy Markdown
Contributor

@fredericbarthelet fredericbarthelet Jan 8, 2026

Choose a reason for hiding this comment

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

Maybe it will be easier to understand if useAsyncOperation takes in the async callback as argument instead?

const { state, execute } = useAsyncOperation<
    CombinedCallToolResponse,
    unknown
  >((toolArgs) => adaptor.callTool(name, toolArgs));

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

* Checkout session request payload following the ACP specification.
*/
export type CheckoutSessionRequest = {
id: string;
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.

The id field is marked as required, but useCheckout hook has logic to auto-generate IDs when not provided (line 83-85 in use-checkout.ts). This creates a type inconsistency where the hook expects id to be optional but the type declares it as required.

Consider making the field optional to match the actual behavior:

Suggested change
id: string;
id?: string;

This would align the type with the hook's auto-injection feature and eliminate the need for type assertions in tests (see line 332 in use-checkout.test.ts).

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

? session
: { ...session, id: generateSessionId() };

setSessionId(sessionWithId.id);
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.

setSessionId is called inside the async operation, which can lead to state inconsistency when deduplication is enabled. If a second checkout is initiated before the first completes, both will update sessionId, but only the second operation's result will be reflected in the state. This means sessionId could reference a session whose result was discarded due to deduplication.

);

const { build, preview, ...devConfig } = configResult?.config || {};
const { ...devConfig } = configResult?.config || {};
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.

return execute(session);
};

const requestCheckout: RequestCheckoutFn = ((
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.

can't this be part of the useAsyncOperation as well? I feel like it's duplicated as well

Copy link
Copy Markdown
Contributor

@fredericbarthelet fredericbarthelet left a comment

Choose a reason for hiding this comment

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

Hey @Rodriguespn thanks for your contribution. It's close to be ready!
I just shared a few comments to improve tests suite and useAsyncOperation hook. Let me know what you think of it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants