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 diff --git a/examples/nextjs/app/page.tsx b/examples/nextjs/app/page.tsx index 12ff2de4..17134c62 100644 --- a/examples/nextjs/app/page.tsx +++ b/examples/nextjs/app/page.tsx @@ -21,6 +21,19 @@ export default async function Home() { enableTaxCollection: true, 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/components/checkout/__tests__/checkout-tips.test.tsx b/packages/react/src/components/checkout/__tests__/checkout-tips.test.tsx index d634fea9..16e8f5d4 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,535 @@ 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, + }); + }); + + 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/checkout.tsx b/packages/react/src/components/checkout/checkout.tsx index e3efb6ff..cecfb7fd 100644 --- a/packages/react/src/components/checkout/checkout.tsx +++ b/packages/react/src/components/checkout/checkout.tsx @@ -188,7 +188,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(), 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/payment/checkout-buttons/mercadopago/mercadopago.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/mercadopago/mercadopago.tsx index 4bdcb036..3816c4b9 100644 --- a/packages/react/src/components/checkout/payment/checkout-buttons/mercadopago/mercadopago.tsx +++ b/packages/react/src/components/checkout/payment/checkout-buttons/mercadopago/mercadopago.tsx @@ -34,8 +34,12 @@ function getMercadoPagoInstance(publicKey: string) { export function MercadoPagoCheckoutButton() { const { t } = useGoDaddyContext(); - const { mercadoPagoConfig, setCheckoutErrors, isConfirmingCheckout } = - useCheckoutContext(); + const { + mercadoPagoConfig, + setCheckoutErrors, + isConfirmingCheckout, + session, + } = useCheckoutContext(); const isPaymentDisabled = useIsPaymentDisabled(); const { data: totals } = useDraftOrderTotals(); const form = useFormContext(); @@ -116,15 +120,16 @@ export function MercadoPagoCheckoutButton() { } else { // Create new brick const renderBrick = async () => { - // Convert from minor units (cents) to major units - const total = parseFloat( - formatCurrency({ - amount: totals?.total?.value || 0, - currencyCode: totals?.total?.currencyCode || 'USD', - inputInMinorUnits: true, - returnRaw: true, - }) - ); + const tipAmount = form.getValues('tipAmount') || 0; + const total = + parseFloat( + formatCurrency({ + amount: (totals?.total?.value || 0) + (session?.enableTips ? tipAmount : 0), + currencyCode: totals?.total?.currencyCode || 'USD', + inputInMinorUnits: true, + returnRaw: true, + }) + ) + (session?.enableTips ? tipAmount : 0); try { const container = document.getElementById(elementId); diff --git a/packages/react/src/components/checkout/payment/utils/use-build-payment-request.test.tsx b/packages/react/src/components/checkout/payment/utils/use-build-payment-request.test.tsx index e539b9f4..f15929e6 100644 --- a/packages/react/src/components/checkout/payment/utils/use-build-payment-request.test.tsx +++ b/packages/react/src/components/checkout/payment/utils/use-build-payment-request.test.tsx @@ -1,7 +1,11 @@ import { render, waitFor } from '@testing-library/react'; import React from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; import { describe, expect, it, vi } from 'vitest'; -import { checkoutContext } from '@/components/checkout/checkout'; +import { + type CheckoutFormData, + checkoutContext, +} from '@/components/checkout/checkout'; import { checkoutQueryKeys } from '@/components/checkout/utils/query-keys'; import { GoDaddyProvider } from '@/godaddy-provider'; import type { CheckoutSession, DraftOrder, SKUProduct } from '@/types'; @@ -48,6 +52,17 @@ function productNode(overrides: Partial = {}): SKUProduct { } as SKUProduct; } +function FormWrapper({ + defaultValues, + children, +}: { + defaultValues?: Partial; + children: React.ReactNode; +}) { + const methods = useForm({ defaultValues: defaultValues ?? {} }); + return {children}; +} + function PaymentRequestProbe({ onRequests, }: { @@ -66,10 +81,12 @@ async function renderUseBuildPaymentRequest({ draftOrderOverrides, sessionOverrides, products = [productNode()], + formDefaultValues, }: { draftOrderOverrides?: DeepPartial; sessionOverrides?: DeepPartial; products?: SKUProduct[]; + formDefaultValues?: Partial; } = {}) { const queryClient = createTestQueryClient(); const draftOrder = buildDraftOrder(draftOrderOverrides); @@ -104,7 +121,9 @@ async function renderUseBuildPaymentRequest({ setCheckoutErrors: () => undefined, }} > - + + + ); @@ -377,4 +396,141 @@ describe('useBuildPaymentRequest', () => { '1.234' ); }); + + it('includes tipAmount in payment request totals when enableTips is true', async () => { + const { requests } = await renderUseBuildPaymentRequest({ + sessionOverrides: { + enableTips: true, + }, + draftOrderOverrides: { + lineItems: [ + buildLineItem({ + name: 'Coffee Mug', + quantity: 1, + details: { sku: 'mug-sku' }, + totals: { + subTotal: money(2000), + discountTotal: money(0), + feeTotal: money(0), + taxTotal: money(0), + }, + unitAmount: money(2000), + }), + ], + shippingLines: [], + totals: { + subTotal: money(2000), + discountTotal: money(0), + shippingTotal: money(0), + taxTotal: money(0), + feeTotal: money(0), + total: money(2000), + }, + }, + products: [productNode({ code: 'mug-sku', label: 'Coffee Mug' })], + formDefaultValues: { tipAmount: 500 }, + }); + + // Apple Pay total includes tip + expect(requests.applePayRequest.total.amount).toBe('$25.00'); + expect(requests.applePayRequest.lineItems).toEqual( + expect.arrayContaining([ + expect.objectContaining({ label: 'Tip', amount: '$5.00', type: 'final' }), + ]) + ); + + // Google Pay total includes tip + expect(requests.googlePayRequest.transactionInfo.totalPrice).toBe('$25.00'); + expect(requests.googlePayRequest.transactionInfo.displayItems).toEqual( + expect.arrayContaining([ + expect.objectContaining({ label: 'Tip', price: 5, type: 'LINE_ITEM', status: 'FINAL' }), + ]) + ); + + // PayPal total includes tip in breakdown and items + expect(requests.payPalRequest.purchase_units[0].amount.value).toBe('25.00'); + expect(requests.payPalRequest.purchase_units[0].amount.breakdown.item_total.value).toBe('25.00'); + expect(requests.payPalRequest.purchase_units[0].items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'Tip', + unit_amount: { currency_code: 'USD', value: '5.00' }, + quantity: '1', + }), + ]) + ); + + // Square total includes tip + expect(requests.squarePaymentRequest.amount).toBe('25.00'); + + // Poynt Standard includes tip line item + expect(requests.poyntStandardRequest.lineItems).toEqual( + expect.arrayContaining([ + expect.objectContaining({ label: 'Tip', amount: '5.00' }), + ]) + ); + }); + + it('excludes tipAmount from payment requests when enableTips is false', async () => { + const { requests } = await renderUseBuildPaymentRequest({ + sessionOverrides: { + enableTips: false, + }, + draftOrderOverrides: { + lineItems: [ + buildLineItem({ + name: 'Coffee Mug', + quantity: 1, + details: { sku: 'mug-sku' }, + totals: { + subTotal: money(2000), + discountTotal: money(0), + feeTotal: money(0), + taxTotal: money(0), + }, + unitAmount: money(2000), + }), + ], + shippingLines: [], + totals: { + subTotal: money(2000), + discountTotal: money(0), + shippingTotal: money(0), + taxTotal: money(0), + feeTotal: money(0), + total: money(2000), + }, + }, + products: [productNode({ code: 'mug-sku', label: 'Coffee Mug' })], + formDefaultValues: { tipAmount: 500 }, + }); + + // Totals should NOT include tip when enableTips is false + expect(requests.applePayRequest.total.amount).toBe('$20.00'); + expect(requests.googlePayRequest.transactionInfo.totalPrice).toBe('$20.00'); + expect(requests.payPalRequest.purchase_units[0].amount.value).toBe('20.00'); + expect(requests.squarePaymentRequest.amount).toBe('20.00'); + + // No Tip line item in any request + expect(requests.applePayRequest.lineItems).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ label: 'Tip' }), + ]) + ); + expect(requests.googlePayRequest.transactionInfo.displayItems).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ label: 'Tip' }), + ]) + ); + expect(requests.payPalRequest.purchase_units[0].items).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'Tip' }), + ]) + ); + expect(requests.poyntStandardRequest.lineItems).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ label: 'Tip' }), + ]) + ); + }); }); diff --git a/packages/react/src/components/checkout/payment/utils/use-build-payment-request.ts b/packages/react/src/components/checkout/payment/utils/use-build-payment-request.ts index 3cd0c5f4..a8b707ca 100644 --- a/packages/react/src/components/checkout/payment/utils/use-build-payment-request.ts +++ b/packages/react/src/components/checkout/payment/utils/use-build-payment-request.ts @@ -3,6 +3,7 @@ import type { PaymentMethodCreateParams, } from '@stripe/stripe-js'; import { useMemo } from 'react'; +import { useFormContext } from 'react-hook-form'; import { useCheckoutContext } from '@/components/checkout/checkout'; import { useDraftOrder, @@ -180,6 +181,7 @@ export function useBuildPaymentRequest(): { } { const formatCurrency = useFormatCurrency(); const { session } = useCheckoutContext(); + const form = useFormContext(); const draftOrderTotalsQuery = useDraftOrderTotals(); const draftOrderQuery = useDraftOrder(); @@ -206,7 +208,9 @@ export function useBuildPaymentRequest(): { 0 ) || 0; const discountMinorUnits = totals?.discountTotal?.value || 0; + const tipAmount = form.getValues('tipAmount') || 0; const totalMinorUnits = totals?.total?.value || 0; + const totalWithTipMinorUnits = totalMinorUnits + tipAmount; const countryCode = useMemo( () => session?.shipping?.originAddress?.countryCode || 'US', @@ -259,7 +263,7 @@ export function useBuildPaymentRequest(): { total: { label: 'Order Total', amount: formatCurrency({ - amount: totals?.total?.value || 0, + amount: session?.enableTips ? totalWithTipMinorUnits : totalMinorUnits, currencyCode, inputInMinorUnits: true, }), @@ -316,6 +320,19 @@ export function useBuildPaymentRequest(): { }), type: 'final', }, + ...(session?.enableTips + ? [ + { + label: 'Tip', + amount: formatCurrency({ + amount: tipAmount, + currencyCode, + inputInMinorUnits: true, + }), + type: 'final', + }, + ] + : []), ].filter(item => Number.parseFloat(item.amount) !== 0), }; @@ -353,7 +370,7 @@ export function useBuildPaymentRequest(): { transactionInfo: { totalPriceStatus: 'FINAL', totalPrice: formatCurrency({ - amount: totals?.total?.value || 0, + amount: session?.enableTips ? totalWithTipMinorUnits : totalMinorUnits, currencyCode, inputInMinorUnits: true, }), @@ -418,6 +435,23 @@ export function useBuildPaymentRequest(): { type: 'LINE_ITEM', status: 'FINAL', }, + ...(session?.enableTips + ? [ + { + label: 'Tip', + price: Number.parseFloat( + formatCurrency({ + amount: tipAmount, + currencyCode, + inputInMinorUnits: true, + returnRaw: true, + }) + ), + type: 'LINE_ITEM', + status: 'FINAL', + }, + ] + : []), ].filter(item => item?.price !== 0), }, }; @@ -427,7 +461,8 @@ export function useBuildPaymentRequest(): { subtotalMinorUnits + taxMinorUnits + shippingMinorUnits - - discountMinorUnits; + discountMinorUnits + + (session?.enableTips ? tipAmount : 0); const payPalRequest: PayPalRequest = { purchase_units: [ @@ -444,7 +479,9 @@ export function useBuildPaymentRequest(): { item_total: { currency_code: currencyCode, value: formatCurrency({ - amount: subtotalMinorUnits, + amount: session?.enableTips + ? subtotalMinorUnits + tipAmount + : subtotalMinorUnits, currencyCode, inputInMinorUnits: true, returnRaw: true, @@ -479,19 +516,39 @@ export function useBuildPaymentRequest(): { }, }, }, - items: items.map(lineItem => ({ - name: lineItem?.name || '', - unit_amount: { - currency_code: currencyCode, - value: formatCurrency({ - amount: lineItem?.originalPrice || 0, - currencyCode, - inputInMinorUnits: true, - returnRaw: true, - }), - }, - quantity: (lineItem?.quantity || 1).toString(), - })), + items: items + .map(lineItem => ({ + name: lineItem?.name || '', + unit_amount: { + currency_code: currencyCode, + value: formatCurrency({ + amount: lineItem?.originalPrice || 0, + currencyCode, + inputInMinorUnits: true, + returnRaw: true, + }), + }, + quantity: (lineItem?.quantity || 1).toString(), + })) + .concat( + session?.enableTips + ? [ + { + name: 'Tip', + unit_amount: { + currency_code: currencyCode, + value: formatCurrency({ + amount: tipAmount, + currencyCode, + inputInMinorUnits: true, + returnRaw: true, + }), + }, + quantity: '1', + }, + ] + : [] + ), shipping: shippingAddress, billing: billingAddress, }, @@ -541,7 +598,7 @@ export function useBuildPaymentRequest(): { const squarePaymentRequest: SquarePaymentRequest = { amount: formatCurrency({ - amount: totals?.total?.value || 0, + amount: session?.enableTips ? totalWithTipMinorUnits : totalMinorUnits, currencyCode, inputInMinorUnits: true, returnRaw: true, @@ -640,6 +697,19 @@ export function useBuildPaymentRequest(): { returnRaw: true, }), }, + ...(session?.enableTips + ? [ + { + label: 'Tip', + amount: formatCurrency({ + amount: tipAmount, + currencyCode, + inputInMinorUnits: true, + returnRaw: true, + }), + }, + ] + : []), ], }; 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 8f54baff..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 @@ -179,6 +179,12 @@ export function useConfirmCheckout() { : undefined, }) : {}; + const tipAmount = form.getValues('tipAmount'); + const payload = { + ...confirmCheckoutInput, + ...pickUpData, + tipAmount, + }; // keep for debugging // console.log({ @@ -205,21 +211,11 @@ export function useConfirmCheckout() { const data = jwt ? await confirmCheckout( - { - ...confirmCheckoutInput, - ...(isPickup ? pickUpData : {}), - }, + payload, { accessToken: jwt, sessionId: session?.id || '' }, apiHost ) - : await confirmCheckout( - { - ...confirmCheckoutInput, - ...(isPickup ? pickUpData : {}), - }, - 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 e9523110..59cd405b 100644 --- a/packages/react/src/components/checkout/tips/tips-form.tsx +++ b/packages/react/src/components/checkout/tips/tips-form.tsx @@ -21,13 +21,17 @@ 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) { +const DEFAULT_TIP_PERCENTAGES = [15, 18, 20]; + +export function TipsForm({ subtotal, options, currencyCode }: TipsFormProps) { const { t } = useGoDaddyContext(); const form = useFormContext(); const formatCurrency = useFormatCurrency(); @@ -35,7 +39,25 @@ 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 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) => { @@ -51,7 +73,7 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { properties: { tipPercentage: percentage, tipAmount: tipAmount, - totalBeforeTip: total, + totalBeforeTip: subtotal, currencyCode, }, }); @@ -69,7 +91,7 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { properties: { tipPercentage: 0, tipAmount: 0, - totalBeforeTip: total, + totalBeforeTip: subtotal, currencyCode, }, }); @@ -77,6 +99,7 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { const handleCustomTip = () => { setShowCustomTip(true); + form.setValue('tipAmount', 0); form.setValue('tipPercentage', null); // Track custom tip selection @@ -84,14 +107,32 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { eventId: eventIds.enterCustomTip, type: TrackingEventType.CLICK, properties: { - totalBeforeTip: total, + totalBeforeTip: subtotal, currencyCode, }, }); }; - const tipPercentages = [15, 18, 20]; const tipPercentage = form.watch('tipPercentage'); + let tipPercentages = options?.default?.percentages; + + const tipAmount = form.watch('tipAmount'); + 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 (
@@ -100,30 +141,56 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { role='radiogroup' aria-label={t.tips?.title || 'Tip amount'} > - {tipPercentages.map(percentage => ( - - ))} + {tipAmounts?.length + ? tipAmounts.map(amount => ( + + )) + : (tipPercentages || DEFAULT_TIP_PERCENTAGES).map(percentage => ( + + ))}
- {showCustomTip && ( + {showCustomTip ? ( - )} + ) : null}
); } @@ -181,7 +251,7 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { */ interface CustomTipInputProps { currencyCode?: string; - total: number; + subtotal: number; formatCurrency: (options: FormatCurrencyOptions) => string; } @@ -216,7 +286,7 @@ function symbolPadding(symbol: string, position: 'prefix' | 'suffix') { function CustomTipInput({ currencyCode, - total, + subtotal, formatCurrency, }: CustomTipInputProps) { const { t } = useGoDaddyContext(); @@ -278,6 +348,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 +359,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 = ( 0 - ? Number(((tipAmount / total) * 100).toFixed(2)) + subtotal > 0 + ? Number(((tipAmount / subtotal) * 100).toFixed(2)) : 0, currencyCode, }, 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: { diff --git a/packages/react/src/lib/godaddy/checkout-env.ts b/packages/react/src/lib/godaddy/checkout-env.ts index c9ab79e9..f891aeb1 100644 --- a/packages/react/src/lib/godaddy/checkout-env.ts +++ b/packages/react/src/lib/godaddy/checkout-env.ts @@ -2139,6 +2139,15 @@ const introspection = { "args": [], "isDeprecated": false }, + { + "name": "tips", + "type": { + "kind": "OBJECT", + "name": "CheckoutSessionTips" + }, + "args": [], + "isDeprecated": false + }, { "name": "token", "type": { @@ -3999,6 +4008,236 @@ const introspection = { ], "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 + } + ], + "interfaces": [] + }, + { + "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 + } + ], + "interfaces": [] + }, + { + "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": "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": "OBJECT", + "name": "CheckoutSessionTipsThreshold", + "fields": [ + { + "name": "amounts", + "type": { + "kind": "LIST", + "ofType": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Int" + } + } + }, + "args": [], + "isDeprecated": false + }, + { + "name": "maxSubtotal", + "type": { + "kind": "SCALAR", + "name": "Int" + }, + "args": [], + "isDeprecated": false + }, + { + "name": "minSubtotal", + "type": { + "kind": "SCALAR", + "name": "Int" + }, + "args": [], + "isDeprecated": false + }, + { + "name": "percentages", + "type": { + "kind": "LIST", + "ofType": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Int" + } + } + }, + "args": [], + "isDeprecated": false + } + ], + "interfaces": [] + }, + { + "kind": "INPUT_OBJECT", + "name": "CheckoutSessionTipsThresholdInput", + "inputFields": [ + { + "name": "amounts", + "type": { + "kind": "LIST", + "ofType": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Int" + } + } + } + }, + { + "name": "maxSubtotal", + "type": { + "kind": "SCALAR", + "name": "Int" + } + }, + { + "name": "minSubtotal", + "type": { + "kind": "SCALAR", + "name": "Int" + } + }, + { + "name": "percentages", + "type": { + "kind": "LIST", + "ofType": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Int" + } + } + } + } + ], + "isOneOf": false + }, { "kind": "OBJECT", "name": "CheckoutSessionTotalTaxAmount", @@ -6334,6 +6573,46 @@ const introspection = { ], "isOneOf": false }, + { + "kind": "ENUM", + "name": "FeeProgramType", + "enumValues": [ + { + "name": "CASH_DISCOUNT", + "isDeprecated": false + }, + { + "name": "CONVENIENCE_FEE", + "isDeprecated": false + }, + { + "name": "SERVICE_FEE", + "isDeprecated": false + }, + { + "name": "SURCHARGE", + "isDeprecated": false + } + ] + }, + { + "kind": "ENUM", + "name": "FeeType", + "enumValues": [ + { + "name": "FIXED", + "isDeprecated": false + }, + { + "name": "HYBRID", + "isDeprecated": false + }, + { + "name": "PERCENTAGE", + "isDeprecated": false + } + ] + }, { "kind": "SCALAR", "name": "Float" @@ -7735,6 +8014,19 @@ const introspection = { "name": "CalculatedTaxesInput" } }, + { + "name": "fees", + "type": { + "kind": "LIST", + "ofType": { + "kind": "NON_NULL", + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TransactionFeeInput" + } + } + } + }, { "name": "fulfillmentEndAt", "type": { @@ -7819,6 +8111,13 @@ const introspection = { "kind": "INPUT_OBJECT", "name": "MoneyInput" } + }, + { + "name": "tipAmount", + "type": { + "kind": "SCALAR", + "name": "Int" + } } ], "isOneOf": false @@ -8091,6 +8390,13 @@ const introspection = { "name": "CheckoutSessionTaxesOptionsInput" } }, + { + "name": "tips", + "type": { + "kind": "INPUT_OBJECT", + "name": "CheckoutSessionTipsInput" + } + }, { "name": "url", "type": { @@ -8523,6 +8829,13 @@ const introspection = { "name": "String" } }, + { + "name": "tips", + "type": { + "kind": "INPUT_OBJECT", + "name": "CheckoutSessionTipsInput" + } + }, { "name": "url", "type": { @@ -10844,6 +11157,64 @@ const introspection = { ], "interfaces": [] }, + { + "kind": "INPUT_OBJECT", + "name": "TransactionFeeInput", + "inputFields": [ + { + "name": "amount", + "type": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Int" + } + } + }, + { + "name": "feeProgramId", + "type": { + "kind": "SCALAR", + "name": "String" + } + }, + { + "name": "feeProgramType", + "type": { + "kind": "NON_NULL", + "ofType": { + "kind": "ENUM", + "name": "FeeProgramType" + } + } + }, + { + "name": "feeType", + "type": { + "kind": "NON_NULL", + "ofType": { + "kind": "ENUM", + "name": "FeeType" + } + } + }, + { + "name": "required", + "type": { + "kind": "SCALAR", + "name": "Boolean" + } + }, + { + "name": "signature", + "type": { + "kind": "SCALAR", + "name": "String" + } + } + ], + "isOneOf": false + }, { "kind": "OBJECT", "name": "TransactionFundingSource", diff --git a/packages/react/src/lib/godaddy/checkout-mutations.ts b/packages/react/src/lib/godaddy/checkout-mutations.ts index 39f4780a..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 @@ -394,10 +406,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(` 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