Skip to content

Conversation

@hamzanasir
Copy link

@hamzanasir hamzanasir commented Oct 15, 2025

This PR implements the venmo one time payment session hook, the proposed usage for this hook would be as follows:

const { handleClick, handleCancel, handleDestroy } = useVenmoOneTimePaymentSession({
    onApprove: async (data) => {
      // onApprove logic
    },
    onCancel: () => {
       // onCancel logic
    },
    onError: (error: Error) => {
       // onError logic
    },
    createOrder, // or you could pass orderId instead to not do lazy order generation
    presentationMode,
  });

Then all the merchant needs to do is pass the click handler to the respective button:

<venmo-button
    onClick={handleClick}
    type="pay"
    id="venmo-button"
></venmo-button>

@changeset-bot
Copy link

changeset-bot bot commented Oct 15, 2025

⚠️ No Changeset found

Latest commit: 1a1a8e6

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor

@imbrian imbrian left a comment

Choose a reason for hiding this comment

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

looks good. what's the testing strategy here? this wouldn't take long to test.

return ref.current;
}

export function useProxyProps<T extends Record<PropertyKey, unknown>>(
Copy link
Contributor

Choose a reason for hiding this comment

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

How do we want to deal with duplicate code like this?

The original implementation has tests too: https://github.com/paypal/paypal-js/blob/main/packages/react-paypal-js/src/hooks/useProxyProps.test.ts

Copy link

Choose a reason for hiding this comment

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

Is there a limitation to importing code from the old react implementation?

Copy link
Author

Choose a reason for hiding this comment

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

@kand had an idea to maybe create a shared utils folder between the v6 implementation and v5 implementation. We could pursue something like that 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

Currently the V6 specific tsconfig instructs rollup to not allow imports outside of the v6 directory. In a discussion yesterday we talked about including a barrel file to export this methods for v6 usage. We can also update the v6-specific tsconfig to allow importing this hook specifically or more hooks generally.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, discussed this with @gregjopa and he suggested that we go with a WET approach in order to make V5 "very deletable" in the future.

Copy link

Choose a reason for hiding this comment

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

Alright, sounds good. Did you mention yesterday we are sharing other stuff from v5 currently though? Do we need to duplicate that code?

Copy link

Choose a reason for hiding this comment

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

I moved this util into the base branch so we can all share it while we work on our hooks.

https://github.com/paypal/paypal-js/pull/695/files#r2440686418

Copy link
Contributor

Choose a reason for hiding this comment

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

Did you mention yesterday we are sharing other stuff from v5 currently though? Do we need to duplicate that code?

@kand In a prior iteration of the loadCoreSdkScript function, it was a nearly identical method to the loadScript function that V5 leverages. So it was basically copy-pasted over for V6 to use. I mentioned this as an example of the WET approach we're currently taking.


useEffect(() => {
if (!sdkInstance) {
throw new Error("no sdk instance available");
Copy link
Contributor

Choose a reason for hiding this comment

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

How do we want to teach error handling? I think a hook throwing versus returning an error state forces a specific integration pattern, because it dictates where a merchant needs to place error boundaries.

An error state allows merchant to conditionally handle gracefully in the consuming component versus in a higher error boundary. If that's what we want, then we need our examples to show wrapping the components that do this setup in an error boundary (and we should probably provide this error boundary component too).

Copy link
Author

Choose a reason for hiding this comment

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

Ahh that's a good callout @imbrian! I think the v5 implementation also relies on throwing an error right? If that's the case we may want to follow the same pattern here and teach using ErrorBoundary

Copy link

@kand kand Oct 17, 2025

Choose a reason for hiding this comment

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

I like the idea of returning some kind of error state rather than throwing an error. Requiring merchants to use an <ErrorBoundary> in a specific place (i.e. outside the component using the hook) feels unnecessarily restrictive.

}: UseVenmoOneTimePaymentSessionProps): UseVenmoOneTimePaymentSessionReturn {
const { sdkInstance } = usePayPal();
const sessionRef = useRef<VenmoOneTimePaymentSession | null>(null);
const proxyCallbacks = useProxyProps(callbacks);
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I see what you're doing. Leveraging the stable ref in useProxyProps to satisfy the useeffect dependency array below, while allowing the underlying functions to change. It's the same use case as v5.

Copy link
Author

Choose a reason for hiding this comment

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

That's good to hear @imbrian! It's an awesome hook to use 🙂

Comment on lines 37 to 40
if (sessionRef.current) {
sessionRef.current.destroy();
sessionRef.current = null;
}
Copy link

Choose a reason for hiding this comment

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

Would it work to shorten these lines to the following?

Suggested change
if (sessionRef.current) {
sessionRef.current.destroy();
sessionRef.current = null;
}
sessionRef.current?.destroy();
sessionRef.current = null;

Copy link

Choose a reason for hiding this comment

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

Could this entire return even just be return handleDestroy? Might be clearer that this is the exact same behavior.

Copy link
Author

Choose a reason for hiding this comment

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

Yes that's a great suggestion @kand ! Thank you!


export type UseVenmoOneTimePaymentSessionProps =
| (Omit<VenmoOneTimePaymentSessionOptions, "orderId"> & {
createOrder: () => VenmoOneTimePaymentSessionPromise;
Copy link

@kand kand Oct 17, 2025

Choose a reason for hiding this comment

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

Not a part of this PR, but out of curiosity, do we need a different type for VenmoOneTimePaymentSessionPromise? It appears to be equivalent to BasePaymentSessionPromise.

Edit: Slack thread here

@hamzanasir hamzanasir marked this pull request as ready for review October 20, 2025 21:05
@hamzanasir hamzanasir requested a review from a team as a code owner October 20, 2025 21:05
mockSdkInstance.createVenmoOneTimePaymentSession,
).toHaveBeenCalledWith({
orderId: "test-order-id",
onApprove: expect.any(Function),
Copy link

Choose a reason for hiding this comment

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

Is expect.any(Function) leaving a mock function in place? All good, but I've usually seen jest.fn() in place of this or an empty arrow function () => {}.

Copy link
Author

Choose a reason for hiding this comment

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

I believe this is the check to see if createVenmoOneTimePaymentSession was called with the right things, if we have jest.fn() in place of this then it will return a new mock function which will fail the equality check I believe 🤔

Copy link

Choose a reason for hiding this comment

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

Ah, sorry, I saw this usage in a few places and glossed over this being used in expect. There are several locations though, where I think the AI got confused and is using this where you'd typically see jest.fn(). I'll highlight one of those locations in another comment.

Copy link
Author

Choose a reason for hiding this comment

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

Oh are you talking about passing props

? Yeah I think you're right that needs to change to use jest.fn()

Copy link
Author

Choose a reason for hiding this comment

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

Good call @kand !

Copy link
Contributor

@gregjopa gregjopa left a comment

Choose a reason for hiding this comment

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

Nice work @hamzanasir!

Comment on lines +310 to +314
await expect(
act(async () => {
await result.current.handleClick();
}),
).rejects.toThrow("Venmo session not available");
Copy link

Choose a reason for hiding this comment

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

Suggested change
await expect(
act(async () => {
await result.current.handleClick();
}),
).rejects.toThrow("Venmo session not available");
expect(
act(() => {
result.current.handleClick();
}),
).rejects.toThrow("Venmo session not available");

I'm fairly certain async / await isn't needed here. My tests didn't end up needing it. Though, I am a bit confused by the sparse documentation on act(). In this example, async / await is used, but not the inner await; i.e. in this case, remove the await in await result.current.handleClick().

https://legacy.reactjs.org/docs/testing-recipes.html#data-fetching

Copy link
Author

Choose a reason for hiding this comment

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

Hmm for some reason my tests seem to fail if I don't use async / await. Using async / await makes sense to me since handleClick is an async function 🤔

Comment on lines 128 to 130
onApprove: expect.any(Function),
onCancel: expect.any(Function),
onError: expect.any(Function),
Copy link

@kand kand Oct 21, 2025

Choose a reason for hiding this comment

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

Here's an example where we should be using jest.fn() rather than expect.any(Function). There are several more of these.

});

describe("callback proxying", () => {
test("should proxy callbacks correctly through useProxyProps", () => {
Copy link

Choose a reason for hiding this comment

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

In my tests, I mocked out useProxyProps:

https://github.com/paypal/paypal-js/pull/697/files#diff-7739eba6c04a62ff31d3d1edb1d736a8d486a9d9405688b749b095da209eee18R19-R23

Which I think is slightly more robust than the unit test here because this test isn't checking that the functions went through useProxyProps; for example, createSessionCall.onApprove could be true here and the test would pass. What do you think?

VenmoOneTimePaymentSessionPromise,
} from "@paypal/paypal-js/sdk-v6";

export type UseVenmoOneTimePaymentSessionProps =
Copy link
Contributor

@EvanReinstein EvanReinstein Oct 21, 2025

Choose a reason for hiding this comment

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

As this type is defined, I'm not sure if the hook can accept fullPageOverlay as an argument. I was taking another look at this while looking into the redirect and direct-app-switch presentation modes for PayPal.

Copy link
Author

Choose a reason for hiding this comment

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

This is a great callout @EvanReinstein! I think a follow up ticket would be great to get parity on all the different types of start options we can pass 🙂

@hamzanasir hamzanasir merged commit b12ebc2 into feature/react-paypal-js-v6 Oct 21, 2025
2 checks passed
@hamzanasir hamzanasir deleted the feature/use-venmo-one-time-hook branch October 21, 2025 21:47
kand pushed a commit that referenced this pull request Oct 22, 2025
feat: Use Venmo One Time Payment Session Hook
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.

6 participants