[YOLO-3035] support tips in unified checkout - #1402
cshing-godaddy wants to merge 78 commits into
Conversation
🦋 Changeset detectedLatest commit: eb8a9a2 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
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 |
| }; | ||
|
|
||
| const createOrder = async (_data, actions) => { | ||
| authorizedTipAmount.current = session?.enableTips ? tipAmount : null; |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
[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, |
There was a problem hiding this comment.
[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.
| if (brickController && brickAmount === amountRef.current) { | ||
| const { formData } = await brickController.getFormData(); | ||
| await handleSubmit({ formData }); |
There was a problem hiding this comment.
[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.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Summary
Support tips in unified checkout
Relates to https://github.com/gdcorp-commerce/checkout-api/pull/108
Changeset
Test Plan
checkout-api