Skip to content

[YOLO-3035] support tips in unified checkout - #1402

Open
cshing-godaddy wants to merge 78 commits into
godaddy:mainfrom
cshing-godaddy:main
Open

cshing-godaddy wants to merge 78 commits into
godaddy:mainfrom
cshing-godaddy:main

Conversation

@cshing-godaddy

@cshing-godaddy cshing-godaddy commented Jul 1, 2026

Copy link
Copy Markdown

Summary

Support tips in unified checkout

Relates to https://github.com/gdcorp-commerce/checkout-api/pull/108

Changeset

  • Changeset added (docs)

Test Plan

image

checkout-api

    operationName: "ConfirmCheckoutSession"
    paymentProvider: "POYNT"
    transactionType: "SALE"
    orderAmount: 5500
    tipAmount: 1234
    inputFeesTotal: 0
    transactionAmount: 6734
    inputFeesCount: 0
    message: "Calling transactionsApi.processTransaction"

@changeset-bot

changeset-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: eb8a9a2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@godaddy/react Patch
@godaddy/localizations Patch

Not sure what this means? Click here to learn what changesets are.

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

@cshing-godaddy
cshing-godaddy marked this pull request as ready for review July 9, 2026 22:34
@cshing-godaddy
cshing-godaddy requested a review from a team as a code owner July 9, 2026 22:34
};

const createOrder = async (_data, actions) => {
authorizedTipAmount.current = session?.enableTips ? tipAmount : null;

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.

Capture the tip after the synchronization flush so PayPal and confirmation use the same amount.

authorizedTipAmount is captured before await flushCheckoutSync(), but buildPaymentRequestsFromOrder() reads the current tip from form.getValues() afterward. I reproduced this with a regression test: start with a 500-cent tip, change it to 100 cents while the flush is pending, then approve. PayPal's order contains a 100-cent tip, while ConfirmCheckoutSession receives tipAmount: 500.

Move the snapshot below the flush, read current form state instead of the render-time tipAmount, and always rebuild the request to avoid the stale payPalRequest fallback:

const { latestOrder } = await flushCheckoutSync({
  includeCurrentFormDiff: true,
});

authorizedTipAmount.current = session?.enableTips
  ? (form.getValues('tipAmount') ?? 0)
  : null;

const request = buildPaymentRequestsFromOrder(
  latestOrder ?? undefined
).payPalRequest;

These reads run synchronously with no intervening await. Keep onApprove using the saved ref so edits after order creation cannot change the confirmation tip. Please add the regression case above and assert both requests contain 100 cents.

label: 'Order Total',
amount: formatCurrency({
amount: totalMinorUnits,
amount: session?.enableTips ? totalWithTipMinorUnits : totalMinorUnits,

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.

[P1] Preserve the standard-wallet tip through confirmation (Apple Pay, Google Pay, and Paze).

This request now includes the tip, but the standard-wallet payment_authorized handlers still omit tipAmount. useConfirmCheckout therefore reads the current form value after another synchronization flush. Regression probes using the real components/request builder/confirmation hook and mocked wallet SDKs reproduce a wallet request with a 500-cent tip followed by confirmation with 100 cents when the form tip changes while the wallet is open. The reverse change can submit more than the wallet displayed.

Suggested fix in Apple Pay, Google Pay, and Paze: after the click handler's flush, capture the enabled tip in a ref and synchronously rebuild with buildPaymentRequestsFromOrder(latestOrder ?? undefined).poyntStandardRequest, even when there is no latestOrder. Do not use the memoized request fallback. Keep that snapshot associated with the wallet attempt and pass it explicitly as tipAmount in payment_authorized, following the PayPal fix. Preserve zero explicitly and prevent a second attempt from overwriting an active attempt's snapshot.

Please add regression coverage for edits during synchronization and between wallet start and authorization, including an initial zero tip. Attaching here because the wallet handlers themselves are outside this PR's diff.

const squarePaymentRequest: SquarePaymentRequest = {
amount: formatCurrency({
amount: totals?.total?.value || 0,
amount: session?.enableTips ? totalWithTipMinorUnits : totalMinorUnits,

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.

[P1] Confirm the same tip passed to Square tokenization.

The Square button awaits card.tokenize(request) and then confirms without tipAmount. A regression probe with deferred SDK tokenization reproduces a request including a 500-cent tip followed by ConfirmCheckoutSession with 100 cents after the form changes during the await. This allows the confirmation amount to differ from the amount supplied for Square verification.

Suggested fix: immediately after flushCheckoutSync, capture the enabled tip in a local constant and synchronously rebuild buildPaymentRequestsFromOrder(latestOrder ?? undefined).squarePaymentRequest instead of falling back to the memoized request. After successful tokenization, pass that constant explicitly as tipAmount to confirmCheckout.mutateAsync. The existing confirmation hook already honors a supplied value, including zero.

Please add a deferred-tokenization regression test that changes the form tip before resolving the token and verifies confirmation uses the original snapshot, including an initial zero tip. Attaching to the new Square amount calculation because the button itself is outside this PR's diff.

Comment on lines 329 to 331
if (brickController && brickAmount === amountRef.current) {
const { formData } = await brickController.getFormData();
await handleSubmit({ formData });

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.

[P1] Preserve the brick's tip and reject stale results after getFormData().

The amount check occurs before an asynchronous SDK call. The tip can change and the brick can be invalidated/replaced while getFormData() is pending, but handleSubmit still confirms its returned token without a saved tip. I reproduced this with a mocked deferred SDK response and an actual tip-radio interaction: build the brick/preference with a 500-cent tip, click Pay now, select 15% while getFormData() waits, and release it; confirmation sends 375 cents for the old brick's token.

Suggested fix: retain the authorizedTipAmount returned by useAuthorizeCheckout alongside the specific controller/preference, and ensure the brick initialization amount matches that authorization. Before awaiting getFormData(), capture that controller and its tip. After the await, discard the result if that attempt was invalidated/replaced; otherwise pass the captured tip through handleSubmit and explicitly into confirmCheckout.mutateAsync. Subsequent validation/synchronization must not replace it with current form state. If the amount changed, rebuild and require a fresh payment attempt.

Please extend the regression tests to change the tip while getFormData() is pending, asserting that the old token is discarded or confirmation retains its matching tip, including an initial zero tip.

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