From c4c5f0219a3fc87ec9fcfebb74cff537b85947a3 Mon Sep 17 00:00:00 2001 From: Catherine Shing Date: Tue, 30 Jun 2026 23:02:29 -0700 Subject: [PATCH 01/22] add button cursor-pointer --- packages/react/src/components/ui/button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/components/ui/button.tsx b/packages/react/src/components/ui/button.tsx index 25d70383..ae374c7a 100644 --- a/packages/react/src/components/ui/button.tsx +++ b/packages/react/src/components/ui/button.tsx @@ -6,7 +6,7 @@ import { useCheckoutContext } from '@/components/checkout/checkout'; import { cn } from '@/lib/utils'; const buttonVariants = cva( - 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', { variants: { variant: { From 17853222375216311ace1dabded310c41e3cf847 Mon Sep 17 00:00:00 2001 From: Catherine Shing Date: Tue, 30 Jun 2026 23:03:01 -0700 Subject: [PATCH 02/22] improve tip btn styling, fix custom tip input --- .../components/checkout/tips/tips-form.tsx | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/react/src/components/checkout/tips/tips-form.tsx b/packages/react/src/components/checkout/tips/tips-form.tsx index e9523110..5ba3873f 100644 --- a/packages/react/src/components/checkout/tips/tips-form.tsx +++ b/packages/react/src/components/checkout/tips/tips-form.tsx @@ -104,17 +104,19 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { - {showCustomTip && ( + {showCustomTip ? ( - )} + ) : null} ); } @@ -278,6 +283,10 @@ function CustomTipInput({ }); }; + // Ref to avoid `form` (unstable reference) in the dependency array. + const formRef = useRef(form); + formRef.current = form; + // When the debounced value settles and the input is still focused, // sync to form state and format the display — the same effect as blur // but triggered by 1.5s of inactivity. This keeps the order summary @@ -285,11 +294,11 @@ function CustomTipInput({ useEffect(() => { if (!isFocused.current || debouncedLocal === null) return; const tipAmount = convertMajorToMinorUnits(debouncedLocal ?? '', code); - form.setValue('tipAmount', tipAmount); + formRef.current.setValue('tipAmount', tipAmount); // Clear local state so the display derives from the formatted form // value (e.g. "10.5" → "10.50"), same as the blur handler. setLocalValue(null); - }, [debouncedLocal, code, form]); + }, [debouncedLocal, code]); const symbolEl = ( Date: Tue, 30 Jun 2026 23:03:29 -0700 Subject: [PATCH 03/22] enable tips in nextjs example --- examples/nextjs/app/page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/nextjs/app/page.tsx b/examples/nextjs/app/page.tsx index 12ff2de4..88778aaa 100644 --- a/examples/nextjs/app/page.tsx +++ b/examples/nextjs/app/page.tsx @@ -21,6 +21,7 @@ export default async function Home() { enableTaxCollection: true, enableNotesCollection: true, enablePromotionCodes: true, + enableTips: true, shipping: { fulfillmentLocationId: 'default-location', originAddress: { From cfed7a4106202731884a23de842466c118e794ae Mon Sep 17 00:00:00 2001 From: Catherine Shing Date: Tue, 30 Jun 2026 23:03:59 -0700 Subject: [PATCH 04/22] fix tipPercentage schema --- packages/react/src/components/checkout/checkout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/components/checkout/checkout.tsx b/packages/react/src/components/checkout/checkout.tsx index 143ea113..1dd496cb 100644 --- a/packages/react/src/components/checkout/checkout.tsx +++ b/packages/react/src/components/checkout/checkout.tsx @@ -186,7 +186,7 @@ export const baseCheckoutSchema = z.object({ pickupLeadTime: z.number().nullish(), pickupTimezone: z.string().nullish(), tipAmount: z.number().optional(), - tipPercentage: z.number().optional(), + tipPercentage: z.number().nullish(), paymentMethod: z.string().min(1, 'Select a payment method'), stripePaymentIntent: z.string().optional(), stripePaymentIntentId: z.string().optional(), From 0d559e53ef3393e7524d24a50b599f2fb8690831 Mon Sep 17 00:00:00 2001 From: Catherine Shing Date: Tue, 30 Jun 2026 23:04:29 -0700 Subject: [PATCH 05/22] pass tipAmount in confirmCheckout --- .../payment/utils/use-confirm-checkout.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/react/src/components/checkout/payment/utils/use-confirm-checkout.ts b/packages/react/src/components/checkout/payment/utils/use-confirm-checkout.ts index 998fb3f9..0e72d58f 100644 --- a/packages/react/src/components/checkout/payment/utils/use-confirm-checkout.ts +++ b/packages/react/src/components/checkout/payment/utils/use-confirm-checkout.ts @@ -169,6 +169,12 @@ export function useConfirmCheckout() { defaultTimezone: session?.defaultOperatingHours?.timeZone, }) : {}; + const tipAmount = form.getValues('tipAmount'); + const payload = { + ...confirmCheckoutInput, + ...pickUpData, + tipAmount, + } // keep for debugging // console.log({ @@ -195,18 +201,12 @@ export function useConfirmCheckout() { const data = jwt ? await confirmCheckout( - { - ...confirmCheckoutInput, - ...(isPickup ? pickUpData : {}), - }, + payload, { accessToken: jwt, sessionId: session?.id || '' }, apiHost ) : await confirmCheckout( - { - ...confirmCheckoutInput, - ...(isPickup ? pickUpData : {}), - }, + payload, session, apiHost ); From ac650af48231818382d1d95498735f7b5a94b0ef Mon Sep 17 00:00:00 2001 From: Catherine Shing Date: Tue, 30 Jun 2026 23:05:16 -0700 Subject: [PATCH 06/22] add tipAmount definition --- packages/react/src/lib/godaddy/checkout-env.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/react/src/lib/godaddy/checkout-env.ts b/packages/react/src/lib/godaddy/checkout-env.ts index 4e7d5c1c..7d5ac970 100644 --- a/packages/react/src/lib/godaddy/checkout-env.ts +++ b/packages/react/src/lib/godaddy/checkout-env.ts @@ -7804,6 +7804,13 @@ const introspection = { name: 'MoneyInput', }, }, + { + name: 'tipAmount', + type: { + kind: 'SCALAR', + name: 'Int', + }, + }, ], isOneOf: false, }, From 813e2a3247e52ddb7de3fe0a15363bb59b73b17c Mon Sep 17 00:00:00 2001 From: Catherine Shing Date: Tue, 30 Jun 2026 23:05:27 -0700 Subject: [PATCH 07/22] formatting --- packages/react/src/lib/godaddy/checkout-mutations.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react/src/lib/godaddy/checkout-mutations.ts b/packages/react/src/lib/godaddy/checkout-mutations.ts index b5af9be9..8b94ffbb 100644 --- a/packages/react/src/lib/godaddy/checkout-mutations.ts +++ b/packages/react/src/lib/godaddy/checkout-mutations.ts @@ -392,10 +392,10 @@ export const ApplyCheckoutSessionDiscountMutation = graphql(` export const ConfirmCheckoutSessionMutation = graphql(` mutation ConfirmCheckoutSession($input: MutationConfirmCheckoutSessionInput!, $sessionId: String!) { - confirmCheckoutSession(input: $input, sessionId: $sessionId) { - status - } + confirmCheckoutSession(input: $input, sessionId: $sessionId) { + status } + } `); export const ApplyCheckoutSessionShippingMethodMutation = graphql(` From 3878b05a31785866de9d281ad7b1bff613bc1048 Mon Sep 17 00:00:00 2001 From: Catherine Shing Date: Tue, 30 Jun 2026 23:05:49 -0700 Subject: [PATCH 08/22] add tests --- .../checkout/__tests__/checkout-tips.test.tsx | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/packages/react/src/components/checkout/__tests__/checkout-tips.test.tsx b/packages/react/src/components/checkout/__tests__/checkout-tips.test.tsx index d634fea9..3af41864 100644 --- a/packages/react/src/components/checkout/__tests__/checkout-tips.test.tsx +++ b/packages/react/src/components/checkout/__tests__/checkout-tips.test.tsx @@ -8,6 +8,7 @@ import { waitForCheckoutReady, waitForOperation, } from './checkout-test-env'; +import { getLastConfirmInput } from './checkout-test-fixtures'; vi.mock('@/tracking/track', async importOriginal => { const actual = await importOriginal(); @@ -346,4 +347,119 @@ describe('Checkout tips', () => { expect(screen.queryByPlaceholderText('0')).not.toBeInTheDocument(); }); }); + + it('includes tipAmount in the ConfirmCheckoutSession mutation payload', async () => { + const { user } = renderCheckout({ + sessionOverrides: { + enableTips: true, + enableShipping: false, + enableLocalPickup: false, + enableTaxCollection: false, + paymentMethods: { + card: { + processor: 'godaddy', + checkoutTypes: ['standard'], + }, + }, + }, + draftOrderOverrides: { + totals: { + subTotal: { value: 2500, currencyCode: 'USD' }, + discountTotal: { value: 0, currencyCode: 'USD' }, + shippingTotal: { value: 0, currencyCode: 'USD' }, + taxTotal: { value: 0, currencyCode: 'USD' }, + feeTotal: { value: 0, currencyCode: 'USD' }, + total: { value: 2500, currencyCode: 'USD' }, + }, + }, + }); + await waitForCheckoutReady(); + clearOperations(); + + await user.click(await screen.findByRole('button', { name: /20%/ })); + await waitFor(() => { + expect(screen.getAllByText('$5.00').length).toBeGreaterThan(0); + }); + + await user.click(await screen.findByRole('button', { name: /pay now/i })); + await waitForOperation('ConfirmCheckoutSession'); + + expect(getLastConfirmInput()).toMatchObject({ + tipAmount: 500, + }); + }); + + it('includes a custom tipAmount when entering a custom tip before confirming', async () => { + const { user } = renderCheckout({ + sessionOverrides: { + enableTips: true, + enableShipping: false, + enableLocalPickup: false, + enableTaxCollection: false, + paymentMethods: { + card: { + processor: 'godaddy', + checkoutTypes: ['standard'], + }, + }, + }, + draftOrderOverrides: { + totals: { + subTotal: { value: 2500, currencyCode: 'USD' }, + discountTotal: { value: 0, currencyCode: 'USD' }, + shippingTotal: { value: 0, currencyCode: 'USD' }, + taxTotal: { value: 0, currencyCode: 'USD' }, + feeTotal: { value: 0, currencyCode: 'USD' }, + total: { value: 2500, currencyCode: 'USD' }, + }, + }, + }); + await waitForCheckoutReady(); + clearOperations(); + + await user.click( + await screen.findByRole('button', { name: /custom amount/i }) + ); + const input = await screen.findByPlaceholderText('0.00'); + await user.click(input); + await user.type(input, '7.50'); + await user.tab(); + + await waitFor(() => { + expect(screen.getAllByText('$7.50').length).toBeGreaterThan(0); + }); + + await user.click(await screen.findByRole('button', { name: /pay now/i })); + await waitForOperation('ConfirmCheckoutSession'); + + expect(getLastConfirmInput()).toMatchObject({ + tipAmount: 750, + }); + }); + + it('sends tipAmount as 0 when no tip is selected', async () => { + const { user } = renderCheckout({ + sessionOverrides: { + enableTips: true, + enableShipping: false, + enableLocalPickup: false, + enableTaxCollection: false, + paymentMethods: { + card: { + processor: 'godaddy', + checkoutTypes: ['standard'], + }, + }, + }, + }); + await waitForCheckoutReady(); + clearOperations(); + + await user.click(await screen.findByRole('button', { name: /pay now/i })); + await waitForOperation('ConfirmCheckoutSession'); + + expect(getLastConfirmInput()).toMatchObject({ + tipAmount: 0, + }); + }); }); From d1d3bb30bc8fd7d95d7f4a50c0e2b98730b5a0e7 Mon Sep 17 00:00:00 2001 From: Catherine Shing Date: Tue, 30 Jun 2026 23:23:33 -0700 Subject: [PATCH 09/22] changeset --- .changeset/fruity-dots-jog.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fruity-dots-jog.md diff --git a/.changeset/fruity-dots-jog.md b/.changeset/fruity-dots-jog.md new file mode 100644 index 00000000..e9a4fbd5 --- /dev/null +++ b/.changeset/fruity-dots-jog.md @@ -0,0 +1,5 @@ +--- +"@godaddy/react": patch +--- + +Support tips in unified checkout From 9f4fc312bb69d088ffed37abe79222bee954a3c1 Mon Sep 17 00:00:00 2001 From: Catherine Shing Date: Thu, 9 Jul 2026 14:25:56 -0700 Subject: [PATCH 10/22] calculate tips from subtotal instead of total --- .../checkout/form/checkout-form.tsx | 3 +- .../components/checkout/tips/tips-form.tsx | 28 ++++++++++--------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/react/src/components/checkout/form/checkout-form.tsx b/packages/react/src/components/checkout/form/checkout-form.tsx index c6045576..da16bd53 100644 --- a/packages/react/src/components/checkout/form/checkout-form.tsx +++ b/packages/react/src/components/checkout/form/checkout-form.tsx @@ -434,7 +434,8 @@ export function CheckoutForm({ diff --git a/packages/react/src/components/checkout/tips/tips-form.tsx b/packages/react/src/components/checkout/tips/tips-form.tsx index 5ba3873f..d15dc5c1 100644 --- a/packages/react/src/components/checkout/tips/tips-form.tsx +++ b/packages/react/src/components/checkout/tips/tips-form.tsx @@ -21,13 +21,15 @@ import { useGoDaddyContext } from '@/godaddy-provider'; import { cn } from '@/lib/utils'; import { eventIds } from '@/tracking/events'; import { TrackingEventType, track } from '@/tracking/track'; +import { type CheckoutSession } from '@/types'; interface TipsFormProps { - total: number; + subtotal: number; + options?: CheckoutSession['tips']; currencyCode?: string; } -export function TipsForm({ total, currencyCode }: TipsFormProps) { +export function TipsForm({ subtotal, options, currencyCode }: TipsFormProps) { const { t } = useGoDaddyContext(); const form = useFormContext(); const formatCurrency = useFormatCurrency(); @@ -35,7 +37,7 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { const calculateTipAmount = (percentage: number): number => { // total is in minor units, so calculate percentage and return in minor units - return Math.round((total * percentage) / 100); + return Math.round((subtotal * percentage) / 100); }; const handlePercentageSelect = (percentage: number) => { @@ -51,7 +53,7 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { properties: { tipPercentage: percentage, tipAmount: tipAmount, - totalBeforeTip: total, + totalBeforeTip: subtotal, currencyCode, }, }); @@ -69,7 +71,7 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { properties: { tipPercentage: 0, tipAmount: 0, - totalBeforeTip: total, + totalBeforeTip: subtotal, currencyCode, }, }); @@ -84,13 +86,13 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { eventId: eventIds.enterCustomTip, type: TrackingEventType.CLICK, properties: { - totalBeforeTip: total, + totalBeforeTip: subtotal, currencyCode, }, }); }; - const tipPercentages = [15, 18, 20]; + const tipPercentages = options?.default?.percentages || [15, 18, 20]; const tipPercentage = form.watch('tipPercentage'); return ( @@ -162,7 +164,7 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { {showCustomTip ? ( ) : null} @@ -186,7 +188,7 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { */ interface CustomTipInputProps { currencyCode?: string; - total: number; + subtotal: number; formatCurrency: (options: FormatCurrencyOptions) => string; } @@ -221,7 +223,7 @@ function symbolPadding(symbol: string, position: 'prefix' | 'suffix') { function CustomTipInput({ currencyCode, - total, + subtotal, formatCurrency, }: CustomTipInputProps) { const { t } = useGoDaddyContext(); @@ -375,10 +377,10 @@ function CustomTipInput({ type: TrackingEventType.CLICK, properties: { tipAmount: tipAmount, - totalBeforeTip: total, + totalBeforeTip: subtotal, tipPercentage: - total > 0 - ? Number(((tipAmount / total) * 100).toFixed(2)) + subtotal > 0 + ? Number(((tipAmount / subtotal) * 100).toFixed(2)) : 0, currencyCode, }, From 112c1616e39c232222ca677400802ac81b5ef38a Mon Sep 17 00:00:00 2001 From: Catherine Shing Date: Thu, 9 Jul 2026 14:27:28 -0700 Subject: [PATCH 11/22] add tips definition --- examples/nextjs/app/page.tsx | 12 + packages/react/README.md | 38 +++ .../react/src/lib/godaddy/checkout-env.ts | 243 ++++++++++++++++++ .../src/lib/godaddy/checkout-mutations.ts | 12 + .../react/src/lib/godaddy/checkout-queries.ts | 12 + 5 files changed, 317 insertions(+) diff --git a/examples/nextjs/app/page.tsx b/examples/nextjs/app/page.tsx index 88778aaa..17134c62 100644 --- a/examples/nextjs/app/page.tsx +++ b/examples/nextjs/app/page.tsx @@ -22,6 +22,18 @@ export default async function Home() { enableNotesCollection: true, enablePromotionCodes: true, enableTips: true, + tips: { + default: { + percentages: [ 20, 40, 60 ] + }, + thresholds: [ + { + minSubtotal: 0, + maxSubtotal: 1000, + amounts: [ 300, 500, 700 ] + } + ] + }, shipping: { fulfillmentLocationId: 'default-location', originAddress: { diff --git a/packages/react/README.md b/packages/react/README.md index 25641b21..7e6b81c1 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -41,6 +41,7 @@ The first parameter accepts all checkout session configuration options from the - **`enableSurcharge`** (boolean): Enable surcharge fees - **`enableTaxCollection`** (boolean): Enable tax collection - **`enableTips`** (boolean): Enable tip/gratuity options +- **`tips`** (CheckoutSessionTipsInput): Tip option configuration (see [Tips](#tips)) - **`enabledLocales`** ([String!]): List of enabled locales - **`enabledPaymentProviders`** ([String!]): List of enabled payment providers - **`environment`** (enum): Environment - `ote`, `prod` @@ -135,6 +136,43 @@ operatingHours: { - **Timezone handling** — All date/time logic uses the store's `timeZone`, not the customer's browser timezone. A store in Phoenix shows Phoenix hours regardless of where the customer is browsing from. - **No available slots** — In `dateAndTime` mode, when leadTime exceeds the entire pickup window, no days are enabled, or no selectable slots exist, a "No available time slots" banner is shown. +### Tips + +The `tips` field configures preset tip options shown to the customer when `enableTips` is `true`. Tips supports a `default` preset and optional `thresholds` that activate based on the order subtotal. Only one of `amounts` or `percentages` should be provided — not both. + +```typescript +tips: { + default: { + percentages: [15, 18, 20], + }, + thresholds: [ + { + minSubtotal: 0, + maxSubtotal: 1000, + amounts: [100, 200, 500], + }, + ], +} +``` + +#### `tips.default` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `amounts` | number[] | No | Fixed tip amounts in the smallest currency unit (e.g. cents). | +| `percentages` | number[] | No | Tip percentage options (integers between 0 and 100). | + +#### `tips.thresholds` + +An array of threshold objects that override the default tips when the order subtotal falls within the specified range. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `minSubtotal` | number | No | Minimum order subtotal (inclusive) in the smallest currency unit for this threshold to apply. | +| `maxSubtotal` | number | No | Maximum order subtotal (exclusive) in the smallest currency unit for this threshold to apply. | +| `amounts` | number[] | No | Fixed tip amounts in the smallest currency unit (e.g. cents). | +| `percentages` | number[] | No | Tip percentage options (integers between 0 and 100). | + ### Appearance The `appearance` field customizes the checkout's look and feel. diff --git a/packages/react/src/lib/godaddy/checkout-env.ts b/packages/react/src/lib/godaddy/checkout-env.ts index 3f6f8c2d..7dd9d28c 100644 --- a/packages/react/src/lib/godaddy/checkout-env.ts +++ b/packages/react/src/lib/godaddy/checkout-env.ts @@ -1870,6 +1870,15 @@ const introspection = { "args": [], "isDeprecated": false }, + { + "name": "tips", + "type": { + "kind": "OBJECT", + "name": "CheckoutSessionTips" + }, + "args": [], + "isDeprecated": false + }, { "name": "enabledLocales", "type": { @@ -3679,6 +3688,233 @@ const introspection = { ], "isOneOf": false }, + { + "kind": "INPUT_OBJECT", + "name": "CheckoutSessionTipsInput", + "inputFields": [ + { + "name": "default", + "type": { + "kind": "INPUT_OBJECT", + "name": "CheckoutSessionTipsDefaultInput" + } + }, + { + "name": "thresholds", + "type": { + "kind": "LIST", + "ofType": { + "kind": "NON_NULL", + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CheckoutSessionTipsThresholdInput" + } + } + } + } + ], + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CheckoutSessionTipsDefaultInput", + "inputFields": [ + { + "name": "amounts", + "type": { + "kind": "LIST", + "ofType": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Int" + } + } + } + }, + { + "name": "percentages", + "type": { + "kind": "LIST", + "ofType": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Int" + } + } + } + } + ], + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "CheckoutSessionTipsThresholdInput", + "inputFields": [ + { + "name": "minSubtotal", + "type": { + "kind": "SCALAR", + "name": "Int" + } + }, + { + "name": "maxSubtotal", + "type": { + "kind": "SCALAR", + "name": "Int" + } + }, + { + "name": "amounts", + "type": { + "kind": "LIST", + "ofType": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Int" + } + } + } + }, + { + "name": "percentages", + "type": { + "kind": "LIST", + "ofType": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Int" + } + } + } + } + ], + "isOneOf": false + }, + { + "kind": "OBJECT", + "name": "CheckoutSessionTips", + "fields": [ + { + "name": "default", + "type": { + "kind": "OBJECT", + "name": "CheckoutSessionTipsDefault" + }, + "args": [], + "isDeprecated": false + }, + { + "name": "thresholds", + "type": { + "kind": "LIST", + "ofType": { + "kind": "NON_NULL", + "ofType": { + "kind": "OBJECT", + "name": "CheckoutSessionTipsThreshold" + } + } + }, + "args": [], + "isDeprecated": false + } + ] + }, + { + "kind": "OBJECT", + "name": "CheckoutSessionTipsDefault", + "fields": [ + { + "name": "amounts", + "type": { + "kind": "LIST", + "ofType": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Int" + } + } + }, + "args": [], + "isDeprecated": false + }, + { + "name": "percentages", + "type": { + "kind": "LIST", + "ofType": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Int" + } + } + }, + "args": [], + "isDeprecated": false + } + ] + }, + { + "kind": "OBJECT", + "name": "CheckoutSessionTipsThreshold", + "fields": [ + { + "name": "minSubtotal", + "type": { + "kind": "SCALAR", + "name": "Int" + }, + "args": [], + "isDeprecated": false + }, + { + "name": "maxSubtotal", + "type": { + "kind": "SCALAR", + "name": "Int" + }, + "args": [], + "isDeprecated": false + }, + { + "name": "amounts", + "type": { + "kind": "LIST", + "ofType": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Int" + } + } + }, + "args": [], + "isDeprecated": false + }, + { + "name": "percentages", + "type": { + "kind": "LIST", + "ofType": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Int" + } + } + }, + "args": [], + "isDeprecated": false + } + ] + }, { "kind": "OBJECT", "name": "CheckoutSessionShippingOptions", @@ -7953,6 +8189,13 @@ const introspection = { "name": "Boolean" } }, + { + "name": "tips", + "type": { + "kind": "INPUT_OBJECT", + "name": "CheckoutSessionTipsInput" + } + }, { "name": "enabledLocales", "type": { diff --git a/packages/react/src/lib/godaddy/checkout-mutations.ts b/packages/react/src/lib/godaddy/checkout-mutations.ts index d9febd2f..89c49081 100644 --- a/packages/react/src/lib/godaddy/checkout-mutations.ts +++ b/packages/react/src/lib/godaddy/checkout-mutations.ts @@ -16,6 +16,18 @@ export const CreateCheckoutSessionMutation = graphql(` storeName environment enableTips + tips { + default { + amounts + percentages + } + thresholds { + minSubtotal + maxSubtotal + amounts + percentages + } + } enabledLocales enableSurcharge enableLocalPickup diff --git a/packages/react/src/lib/godaddy/checkout-queries.ts b/packages/react/src/lib/godaddy/checkout-queries.ts index 4e3e6185..5c3315b0 100644 --- a/packages/react/src/lib/godaddy/checkout-queries.ts +++ b/packages/react/src/lib/godaddy/checkout-queries.ts @@ -16,6 +16,18 @@ export const GetCheckoutSessionQuery = graphql(` storeName environment enableTips + tips { + default { + amounts + percentages + } + thresholds { + minSubtotal + maxSubtotal + amounts + percentages + } + } enabledLocales enableSurcharge enableLocalPickup From 8197e348288f2c0ba3151f07faccfb836c5ec32f Mon Sep 17 00:00:00 2001 From: Catherine Shing Date: Thu, 9 Jul 2026 15:08:19 -0700 Subject: [PATCH 12/22] handle tip amounts --- .../components/checkout/tips/tips-form.tsx | 88 +++++++++++++------ 1 file changed, 62 insertions(+), 26 deletions(-) diff --git a/packages/react/src/components/checkout/tips/tips-form.tsx b/packages/react/src/components/checkout/tips/tips-form.tsx index d15dc5c1..bd16b8ce 100644 --- a/packages/react/src/components/checkout/tips/tips-form.tsx +++ b/packages/react/src/components/checkout/tips/tips-form.tsx @@ -29,6 +29,8 @@ interface TipsFormProps { currencyCode?: string; } +const DEFAULT_TIP_PERCENTAGES = [15, 18, 20]; + export function TipsForm({ subtotal, options, currencyCode }: TipsFormProps) { const { t } = useGoDaddyContext(); const form = useFormContext(); @@ -92,8 +94,16 @@ export function TipsForm({ subtotal, options, currencyCode }: TipsFormProps) { }); }; - const tipPercentages = options?.default?.percentages || [15, 18, 20]; const tipPercentage = form.watch('tipPercentage'); + const tipPercentages = options?.default?.percentages || DEFAULT_TIP_PERCENTAGES; + + const tipAmount = form.watch('tipAmount'); + let tipAmounts: number[] = []; + if (options?.thresholds?.[0]?.maxSubtotal && subtotal < Number(options?.thresholds?.[0]?.maxSubtotal)) { + tipAmounts = options?.thresholds?.[0]?.amounts || []; + } else if (options?.default?.amounts) { + tipAmounts = options?.default?.amounts; + } return (
@@ -102,31 +112,57 @@ export function TipsForm({ subtotal, options, currencyCode }: TipsFormProps) { role='radiogroup' aria-label={t.tips?.title || 'Tip amount'} > - {tipPercentages.map(percentage => ( - + {tipAmounts?.length ? ( + tipAmounts.map((amount) => ( + + )) + ) : ( + tipPercentages.map(percentage => ( + + ) ))} From 2eb2f6488d2cecfb80be9e5dcde779fe3fcb19c0 Mon Sep 17 00:00:00 2001 From: Catherine Shing Date: Thu, 9 Jul 2026 15:47:51 -0700 Subject: [PATCH 13/22] fix tip amount selection --- .../components/checkout/tips/tips-form.tsx | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/react/src/components/checkout/tips/tips-form.tsx b/packages/react/src/components/checkout/tips/tips-form.tsx index bd16b8ce..f711223b 100644 --- a/packages/react/src/components/checkout/tips/tips-form.tsx +++ b/packages/react/src/components/checkout/tips/tips-form.tsx @@ -42,6 +42,24 @@ export function TipsForm({ subtotal, options, currencyCode }: TipsFormProps) { return Math.round((subtotal * percentage) / 100); }; + const handleAmountSelect = (amount: number) => { + form.setValue('tipAmount', amount); + form.setValue('tipPercentage', null); + setShowCustomTip(false); + + // Track tip amount selection + track({ + eventId: eventIds.selectTipAmount, + type: TrackingEventType.CLICK, + properties: { + tipPercentage: null, + tipAmount: amount, + totalBeforeTip: subtotal, + currencyCode, + }, + }); + }; + const handlePercentageSelect = (percentage: number) => { const tipAmount = calculateTipAmount(percentage); form.setValue('tipAmount', tipAmount); @@ -81,6 +99,7 @@ export function TipsForm({ subtotal, options, currencyCode }: TipsFormProps) { const handleCustomTip = () => { setShowCustomTip(true); + form.setValue('tipAmount', 0); form.setValue('tipPercentage', null); // Track custom tip selection @@ -124,7 +143,7 @@ export function TipsForm({ subtotal, options, currencyCode }: TipsFormProps) { ? 'border-muted-foreground' : 'bg-card active:ring' )} - onClick={() => form.setValue('tipAmount', amount)} + onClick={() => handleAmountSelect(amount)} aria-checked={tipAmount === amount ? 'true' : 'false'} > @@ -176,10 +195,10 @@ export function TipsForm({ subtotal, options, currencyCode }: TipsFormProps) { variant='outline' className={cn( 'h-12 font-normal hover:bg-muted', - tipPercentage === 0 && 'border-muted-foreground' + !tipAmount && tipPercentage === 0 && 'border-muted-foreground' )} onClick={handleNoTip} - aria-checked={tipPercentage === 0 ? 'true' : 'false'} + aria-checked={!tipAmount && tipPercentage === 0 ? 'true' : 'false'} > {t.tips.noTip} From 0e5649f0ceae3ae1714970f465a8ea1ab64964d4 Mon Sep 17 00:00:00 2001 From: Catherine Shing Date: Fri, 10 Jul 2026 09:04:37 -0700 Subject: [PATCH 14/22] lint --- .../payment/utils/use-confirm-checkout.ts | 8 +- .../components/checkout/tips/tips-form.tsx | 110 +++++++++--------- 2 files changed, 58 insertions(+), 60 deletions(-) diff --git a/packages/react/src/components/checkout/payment/utils/use-confirm-checkout.ts b/packages/react/src/components/checkout/payment/utils/use-confirm-checkout.ts index 32d7c1ce..cdcb190e 100644 --- a/packages/react/src/components/checkout/payment/utils/use-confirm-checkout.ts +++ b/packages/react/src/components/checkout/payment/utils/use-confirm-checkout.ts @@ -184,7 +184,7 @@ export function useConfirmCheckout() { ...confirmCheckoutInput, ...pickUpData, tipAmount, - } + }; // keep for debugging // console.log({ @@ -215,11 +215,7 @@ export function useConfirmCheckout() { { accessToken: jwt, sessionId: session?.id || '' }, apiHost ) - : await confirmCheckout( - payload, - session, - apiHost - ); + : await confirmCheckout(payload, session, apiHost); if (!data) { throw new Error('Checkout confirmation failed'); diff --git a/packages/react/src/components/checkout/tips/tips-form.tsx b/packages/react/src/components/checkout/tips/tips-form.tsx index f711223b..4fd45811 100644 --- a/packages/react/src/components/checkout/tips/tips-form.tsx +++ b/packages/react/src/components/checkout/tips/tips-form.tsx @@ -114,11 +114,15 @@ export function TipsForm({ subtotal, options, currencyCode }: TipsFormProps) { }; const tipPercentage = form.watch('tipPercentage'); - const tipPercentages = options?.default?.percentages || DEFAULT_TIP_PERCENTAGES; + const tipPercentages = + options?.default?.percentages || DEFAULT_TIP_PERCENTAGES; const tipAmount = form.watch('tipAmount'); let tipAmounts: number[] = []; - if (options?.thresholds?.[0]?.maxSubtotal && subtotal < Number(options?.thresholds?.[0]?.maxSubtotal)) { + if ( + options?.thresholds?.[0]?.maxSubtotal && + subtotal < Number(options?.thresholds?.[0]?.maxSubtotal) + ) { tipAmounts = options?.thresholds?.[0]?.amounts || []; } else if (options?.default?.amounts) { tipAmounts = options?.default?.amounts; @@ -131,58 +135,56 @@ export function TipsForm({ subtotal, options, currencyCode }: TipsFormProps) { role='radiogroup' aria-label={t.tips?.title || 'Tip amount'} > - {tipAmounts?.length ? ( - tipAmounts.map((amount) => ( - - )) - ) : ( - tipPercentages.map(percentage => ( - - ) - ))} + {tipAmounts?.length + ? tipAmounts.map(amount => ( + + )) + : tipPercentages.map(percentage => ( + + ))}
Date: Fri, 10 Jul 2026 10:11:27 -0700 Subject: [PATCH 15/22] handle tip thresholds --- .../checkout/__tests__/checkout-tips.test.tsx | 422 ++++++++++++++++++ .../components/checkout/tips/tips-form.tsx | 28 +- 2 files changed, 439 insertions(+), 11 deletions(-) diff --git a/packages/react/src/components/checkout/__tests__/checkout-tips.test.tsx b/packages/react/src/components/checkout/__tests__/checkout-tips.test.tsx index 3af41864..c757ed08 100644 --- a/packages/react/src/components/checkout/__tests__/checkout-tips.test.tsx +++ b/packages/react/src/components/checkout/__tests__/checkout-tips.test.tsx @@ -462,4 +462,426 @@ describe('Checkout tips', () => { tipAmount: 0, }); }); + + describe('options.thresholds', () => { + it('uses default percentages when no thresholds match the subtotal', async () => { + renderCheckout({ + sessionOverrides: { + enableTips: true, + enableShipping: false, + enableLocalPickup: false, + enableTaxCollection: false, + tips: { + default: { percentages: [10, 15, 20], amounts: null }, + thresholds: [ + { + minSubtotal: 100000, + maxSubtotal: 200000, + percentages: [5, 8, 12], + amounts: null, + }, + ], + }, + }, + draftOrderOverrides: { + totals: { + subTotal: { value: 2500, currencyCode: 'USD' }, + discountTotal: { value: 0, currencyCode: 'USD' }, + shippingTotal: { value: 0, currencyCode: 'USD' }, + taxTotal: { value: 0, currencyCode: 'USD' }, + feeTotal: { value: 0, currencyCode: 'USD' }, + total: { value: 2500, currencyCode: 'USD' }, + }, + }, + }); + await waitForCheckoutReady(); + + expect(await screen.findByRole('button', { name: /10%/ })).toBeVisible(); + expect(await screen.findByRole('button', { name: /15%/ })).toBeVisible(); + expect(await screen.findByRole('button', { name: /20%/ })).toBeVisible(); + expect( + screen.queryByRole('button', { name: /\b5%/ }) + ).not.toBeInTheDocument(); + }); + + it('uses threshold percentages when subtotal falls within a threshold range', async () => { + renderCheckout({ + sessionOverrides: { + enableTips: true, + enableShipping: false, + enableLocalPickup: false, + enableTaxCollection: false, + tips: { + default: { percentages: [10, 15, 20], amounts: null }, + thresholds: [ + { + minSubtotal: 2000, + maxSubtotal: 5000, + percentages: [5, 8, 12], + amounts: null, + }, + ], + }, + }, + draftOrderOverrides: { + totals: { + subTotal: { value: 2500, currencyCode: 'USD' }, + discountTotal: { value: 0, currencyCode: 'USD' }, + shippingTotal: { value: 0, currencyCode: 'USD' }, + taxTotal: { value: 0, currencyCode: 'USD' }, + feeTotal: { value: 0, currencyCode: 'USD' }, + total: { value: 2500, currencyCode: 'USD' }, + }, + }, + }); + await waitForCheckoutReady(); + + expect(await screen.findByRole('button', { name: /5%/ })).toBeVisible(); + expect(await screen.findByRole('button', { name: /8%/ })).toBeVisible(); + expect(await screen.findByRole('button', { name: /12%/ })).toBeVisible(); + expect( + screen.queryByRole('button', { name: /10%/ }) + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: /15%/ }) + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: /20%/ }) + ).not.toBeInTheDocument(); + }); + + it('uses threshold amounts (flat values) when a matching threshold specifies amounts', async () => { + renderCheckout({ + sessionOverrides: { + enableTips: true, + enableShipping: false, + enableLocalPickup: false, + enableTaxCollection: false, + tips: { + default: { percentages: [10, 15, 20], amounts: null }, + thresholds: [ + { + minSubtotal: 2000, + maxSubtotal: 5000, + amounts: [100, 200, 500], + percentages: null, + }, + ], + }, + }, + draftOrderOverrides: { + totals: { + subTotal: { value: 2500, currencyCode: 'USD' }, + discountTotal: { value: 0, currencyCode: 'USD' }, + shippingTotal: { value: 0, currencyCode: 'USD' }, + taxTotal: { value: 0, currencyCode: 'USD' }, + feeTotal: { value: 0, currencyCode: 'USD' }, + total: { value: 2500, currencyCode: 'USD' }, + }, + }, + }); + await waitForCheckoutReady(); + + expect( + await screen.findByRole('button', { name: /\$1\.00/ }) + ).toBeVisible(); + expect( + await screen.findByRole('button', { name: /\$2\.00/ }) + ).toBeVisible(); + expect( + await screen.findByRole('button', { name: /\$5\.00/ }) + ).toBeVisible(); + expect( + screen.queryByRole('button', { name: /10%/ }) + ).not.toBeInTheDocument(); + }); + + it('threshold amounts take priority over threshold percentages when both are provided', async () => { + renderCheckout({ + sessionOverrides: { + enableTips: true, + enableShipping: false, + enableLocalPickup: false, + enableTaxCollection: false, + tips: { + default: { percentages: [10, 15, 20], amounts: null }, + thresholds: [ + { + minSubtotal: 2000, + maxSubtotal: 5000, + amounts: [100, 200, 500], + percentages: [5, 8, 12], + }, + ], + }, + }, + draftOrderOverrides: { + totals: { + subTotal: { value: 2500, currencyCode: 'USD' }, + discountTotal: { value: 0, currencyCode: 'USD' }, + shippingTotal: { value: 0, currencyCode: 'USD' }, + taxTotal: { value: 0, currencyCode: 'USD' }, + feeTotal: { value: 0, currencyCode: 'USD' }, + total: { value: 2500, currencyCode: 'USD' }, + }, + }, + }); + await waitForCheckoutReady(); + + expect( + await screen.findByRole('button', { name: /\$1\.00/ }) + ).toBeVisible(); + expect( + await screen.findByRole('button', { name: /\$2\.00/ }) + ).toBeVisible(); + expect( + await screen.findByRole('button', { name: /\$5\.00/ }) + ).toBeVisible(); + expect( + screen.queryByRole('button', { name: /5%/ }) + ).not.toBeInTheDocument(); + }); + + it('matches the correct threshold when multiple thresholds are defined', async () => { + renderCheckout({ + sessionOverrides: { + enableTips: true, + enableShipping: false, + enableLocalPickup: false, + enableTaxCollection: false, + tips: { + default: { percentages: [15, 18, 20], amounts: null }, + thresholds: [ + { + minSubtotal: 1000, + maxSubtotal: 3000, + percentages: [5, 8, 10], + amounts: null, + }, + { + minSubtotal: 3001, + maxSubtotal: 10000, + percentages: [3, 5, 7], + amounts: null, + }, + ], + }, + }, + draftOrderOverrides: { + totals: { + subTotal: { value: 5000, currencyCode: 'USD' }, + discountTotal: { value: 0, currencyCode: 'USD' }, + shippingTotal: { value: 0, currencyCode: 'USD' }, + taxTotal: { value: 0, currencyCode: 'USD' }, + feeTotal: { value: 0, currencyCode: 'USD' }, + total: { value: 5000, currencyCode: 'USD' }, + }, + }, + }); + await waitForCheckoutReady(); + + expect(await screen.findByRole('button', { name: /3%/ })).toBeVisible(); + expect(await screen.findByRole('button', { name: /5%/ })).toBeVisible(); + expect(await screen.findByRole('button', { name: /7%/ })).toBeVisible(); + expect( + screen.queryByRole('button', { name: /15%/ }) + ).not.toBeInTheDocument(); + }); + + it('applies threshold at boundary: subtotal equals minSubtotal', async () => { + renderCheckout({ + sessionOverrides: { + enableTips: true, + enableShipping: false, + enableLocalPickup: false, + enableTaxCollection: false, + tips: { + default: { percentages: [15, 18, 20], amounts: null }, + thresholds: [ + { + minSubtotal: 2500, + maxSubtotal: 5000, + percentages: [5, 8, 12], + amounts: null, + }, + ], + }, + }, + draftOrderOverrides: { + totals: { + subTotal: { value: 2500, currencyCode: 'USD' }, + discountTotal: { value: 0, currencyCode: 'USD' }, + shippingTotal: { value: 0, currencyCode: 'USD' }, + taxTotal: { value: 0, currencyCode: 'USD' }, + feeTotal: { value: 0, currencyCode: 'USD' }, + total: { value: 2500, currencyCode: 'USD' }, + }, + }, + }); + await waitForCheckoutReady(); + + expect(await screen.findByRole('button', { name: /5%/ })).toBeVisible(); + expect(await screen.findByRole('button', { name: /8%/ })).toBeVisible(); + expect(await screen.findByRole('button', { name: /12%/ })).toBeVisible(); + }); + + it('applies threshold at boundary: subtotal equals maxSubtotal', async () => { + renderCheckout({ + sessionOverrides: { + enableTips: true, + enableShipping: false, + enableLocalPickup: false, + enableTaxCollection: false, + tips: { + default: { percentages: [15, 18, 20], amounts: null }, + thresholds: [ + { + minSubtotal: 2000, + maxSubtotal: 2500, + percentages: [5, 8, 12], + amounts: null, + }, + ], + }, + }, + draftOrderOverrides: { + totals: { + subTotal: { value: 2500, currencyCode: 'USD' }, + discountTotal: { value: 0, currencyCode: 'USD' }, + shippingTotal: { value: 0, currencyCode: 'USD' }, + taxTotal: { value: 0, currencyCode: 'USD' }, + feeTotal: { value: 0, currencyCode: 'USD' }, + total: { value: 2500, currencyCode: 'USD' }, + }, + }, + }); + await waitForCheckoutReady(); + + expect(await screen.findByRole('button', { name: /5%/ })).toBeVisible(); + expect(await screen.findByRole('button', { name: /8%/ })).toBeVisible(); + expect(await screen.findByRole('button', { name: /12%/ })).toBeVisible(); + }); + + it('clicking a threshold amount button selects it and updates the total', async () => { + const { user } = renderCheckout({ + sessionOverrides: { + enableTips: true, + enableShipping: false, + enableLocalPickup: false, + enableTaxCollection: false, + tips: { + default: { percentages: null, amounts: null }, + thresholds: [ + { + minSubtotal: 2000, + maxSubtotal: 5000, + amounts: [200, 500, 1000], + percentages: null, + }, + ], + }, + }, + draftOrderOverrides: { + totals: { + subTotal: { value: 2500, currencyCode: 'USD' }, + discountTotal: { value: 0, currencyCode: 'USD' }, + shippingTotal: { value: 0, currencyCode: 'USD' }, + taxTotal: { value: 0, currencyCode: 'USD' }, + feeTotal: { value: 0, currencyCode: 'USD' }, + total: { value: 2500, currencyCode: 'USD' }, + }, + }, + }); + await waitForCheckoutReady(); + + const fiveDollarBtn = await screen.findByRole('button', { + name: /\$5\.00/, + }); + await user.click(fiveDollarBtn); + + await waitFor(() => { + expect(fiveDollarBtn).toHaveAttribute('aria-checked', 'true'); + expect(screen.getAllByText('$30.00').length).toBeGreaterThan(0); + }); + }); + + it('uses default amounts when options.default.amounts is provided and no threshold matches', async () => { + renderCheckout({ + sessionOverrides: { + enableTips: true, + enableShipping: false, + enableLocalPickup: false, + enableTaxCollection: false, + tips: { + default: { percentages: null, amounts: [100, 300, 500] }, + thresholds: [ + { + minSubtotal: 100000, + maxSubtotal: 200000, + percentages: [1, 2, 3], + amounts: null, + }, + ], + }, + }, + draftOrderOverrides: { + totals: { + subTotal: { value: 2500, currencyCode: 'USD' }, + discountTotal: { value: 0, currencyCode: 'USD' }, + shippingTotal: { value: 0, currencyCode: 'USD' }, + taxTotal: { value: 0, currencyCode: 'USD' }, + feeTotal: { value: 0, currencyCode: 'USD' }, + total: { value: 2500, currencyCode: 'USD' }, + }, + }, + }); + await waitForCheckoutReady(); + + expect( + await screen.findByRole('button', { name: /\$1\.00/ }) + ).toBeVisible(); + expect( + await screen.findByRole('button', { name: /\$3\.00/ }) + ).toBeVisible(); + expect( + await screen.findByRole('button', { name: /\$5\.00/ }) + ).toBeVisible(); + expect( + screen.queryByRole('button', { name: /15%/ }) + ).not.toBeInTheDocument(); + }); + + it('falls back to DEFAULT_TIP_PERCENTAGES when no options are provided', async () => { + renderCheckout({ + sessionOverrides: { + enableTips: true, + enableShipping: false, + enableLocalPickup: false, + enableTaxCollection: false, + tips: null, + }, + draftOrderOverrides: { + totals: { + subTotal: { value: 2500, currencyCode: 'USD' }, + discountTotal: { value: 0, currencyCode: 'USD' }, + shippingTotal: { value: 0, currencyCode: 'USD' }, + taxTotal: { value: 0, currencyCode: 'USD' }, + feeTotal: { value: 0, currencyCode: 'USD' }, + total: { value: 2500, currencyCode: 'USD' }, + }, + }, + }); + await waitForCheckoutReady(); + + expect( + await screen.findByRole('button', { name: /15%/ }) + ).toBeVisible(); + expect( + await screen.findByRole('button', { name: /18%/ }) + ).toBeVisible(); + expect( + await screen.findByRole('button', { name: /20%/ }) + ).toBeVisible(); + }); + }); }); diff --git a/packages/react/src/components/checkout/tips/tips-form.tsx b/packages/react/src/components/checkout/tips/tips-form.tsx index 4fd45811..59cd405b 100644 --- a/packages/react/src/components/checkout/tips/tips-form.tsx +++ b/packages/react/src/components/checkout/tips/tips-form.tsx @@ -114,18 +114,24 @@ export function TipsForm({ subtotal, options, currencyCode }: TipsFormProps) { }; const tipPercentage = form.watch('tipPercentage'); - const tipPercentages = - options?.default?.percentages || DEFAULT_TIP_PERCENTAGES; + let tipPercentages = options?.default?.percentages; const tipAmount = form.watch('tipAmount'); - let tipAmounts: number[] = []; - if ( - options?.thresholds?.[0]?.maxSubtotal && - subtotal < Number(options?.thresholds?.[0]?.maxSubtotal) - ) { - tipAmounts = options?.thresholds?.[0]?.amounts || []; - } else if (options?.default?.amounts) { - tipAmounts = options?.default?.amounts; + let tipAmounts = options?.default?.amounts; + + const threshold = options?.thresholds?.find( + thres => + thres?.minSubtotal && + thres?.maxSubtotal && + subtotal >= thres.minSubtotal && + subtotal <= thres.maxSubtotal + ); + if (threshold) { + if (threshold.amounts) { + tipAmounts = threshold.amounts; + } else if (threshold.percentages) { + tipPercentages = threshold.percentages; + } } return ( @@ -159,7 +165,7 @@ export function TipsForm({ subtotal, options, currencyCode }: TipsFormProps) { )) - : tipPercentages.map(percentage => ( + : (tipPercentages || DEFAULT_TIP_PERCENTAGES).map(percentage => (