From 07cb232d36a0d3e8aa4bd0695ac79b7c83bfdcf5 Mon Sep 17 00:00:00 2001 From: Kieran Osgood Date: Mon, 15 Dec 2025 15:50:53 +0000 Subject: [PATCH 1/2] feat: support new Cart respondWith instead of CartInput --- ...yCheckoutSheetKit+EventSerialization.swift | 13 - .../checkout-sheet-kit/src/events.d.ts | 234 +++++------------- .../@shopify/checkout-sheet-kit/src/index.ts | 17 +- .../checkout-sheet-kit/tests/testFixtures.ts | 2 +- sample/src/screens/BuyNow/AddressScreen.tsx | 1 + sample/src/screens/BuyNow/CheckoutScreen.tsx | 30 ++- sample/src/screens/BuyNow/PaymentScreen.tsx | 131 +++++----- sample/src/screens/BuyNow/types.ts | 6 +- 8 files changed, 167 insertions(+), 267 deletions(-) diff --git a/modules/@shopify/checkout-sheet-kit/ios/ShopifyCheckoutSheetKit+EventSerialization.swift b/modules/@shopify/checkout-sheet-kit/ios/ShopifyCheckoutSheetKit+EventSerialization.swift index c2673f57..ba9cd6f9 100644 --- a/modules/@shopify/checkout-sheet-kit/ios/ShopifyCheckoutSheetKit+EventSerialization.swift +++ b/modules/@shopify/checkout-sheet-kit/ios/ShopifyCheckoutSheetKit+EventSerialization.swift @@ -46,19 +46,6 @@ internal enum ShopifyEventSerialization { return [:] } - /** - * Converts a JSON string to a dictionary. - */ - static func stringToJSON(from value: String?) -> [String: Any]? { - guard let data = value?.data(using: .utf8, allowLossyConversion: false) else { return [:] } - do { - return try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] - } catch { - print("Failed to convert string to JSON: \(error)", value ?? "nil") - return [:] - } - } - /** * Converts a CheckoutCompleteEvent to a React Native compatible dictionary. */ diff --git a/modules/@shopify/checkout-sheet-kit/src/events.d.ts b/modules/@shopify/checkout-sheet-kit/src/events.d.ts index 6ade383f..acf2d13a 100644 --- a/modules/@shopify/checkout-sheet-kit/src/events.d.ts +++ b/modules/@shopify/checkout-sheet-kit/src/events.d.ts @@ -242,36 +242,85 @@ export interface MailingAddress { */ export interface CartDelivery { /** List of selectable delivery addresses */ - addresses: CartDeliveryAddress[]; + addresses: CartSelectableAddress[]; } /** - * A delivery address with selection state. + * A selectable delivery address with selection and reuse settings. */ -export interface CartDeliveryAddress { +export interface CartSelectableAddress { /** The mailing address details */ address: MailingAddress; /** Whether this address is currently selected for delivery */ selected?: boolean; + /** Whether this address should only be used for this checkout */ + oneTimeUse?: boolean; } /** * A delivery address of the buyer that is interacting with the cart. * This is a union type to support future address types. - * Currently only CartDeliveryAddress is supported. + * Currently only CartSelectableAddress is supported. * * @see https://shopify.dev/docs/api/storefront/latest/unions/CartAddress */ -export type CartAddress = CartDeliveryAddress; +export type CartAddress = CartSelectableAddress; + +/** + * Remote token payment credential for delegated payment processing. + */ +export interface RemoteTokenPaymentCredential { + /** The payment token */ + token: string; + /** The type of token (e.g., "card") */ + tokenType: string; + /** The token handler (e.g., "delegated") */ + tokenHandler: string; +} + +/** + * Cart credential containing payment authentication data. + */ +export interface CartCredential { + /** Remote token payment credential for tokenized payments */ + remoteTokenPaymentCredential?: RemoteTokenPaymentCredential; +} /** * Payment instrument available for selection at checkout. - * Output type from Storefront API. + * Represents a credit card or other payment method with its details. * * @see https://shopify.dev/docs/api/storefront/latest/objects/CartPaymentInstrument */ export interface CartPaymentInstrument { - externalReference: string; + /** Type discriminator for the payment instrument */ + __typename?: string; + /** Unique identifier for this payment instrument */ + externalReferenceId: string; + /** Payment credentials for this instrument */ + credentials?: CartCredential[]; + /** Name of the cardholder */ + cardHolderName?: string; + /** Last digits of the card number */ + lastDigits?: string; + /** Expiry month (1-12) */ + month?: number; + /** Expiry year (4-digit) */ + year?: number; + /** Card brand (e.g., VISA, MASTERCARD) */ + brand?: CardBrand; + /** Billing address for the payment instrument */ + billingAddress?: MailingAddress; +} + +/** + * A payment method containing payment instruments. + */ +export interface CartPaymentMethod { + /** Type discriminator (e.g., "CreditCardPaymentMethod") */ + __typename?: string; + /** Payment instruments for this method */ + instruments: CartPaymentInstrument[]; } /** @@ -280,7 +329,8 @@ export interface CartPaymentInstrument { * @see https://shopify.dev/docs/api/storefront/latest/objects/CartPayment */ export interface CartPayment { - instruments: CartPaymentInstrument[]; + /** Payment methods available for the cart */ + methods: CartPaymentMethod[]; } /** @@ -408,108 +458,6 @@ export interface CheckoutResponseError { fieldTarget?: string; } -/** - * Mailing address input for delivery. - * - * @see https://shopify.dev/docs/api/storefront/latest/input-objects/MailingAddressInput - */ -export interface MailingAddressInput { - /** First line of the address (street address or PO Box) */ - address1?: string; - /** Second line of the address (apartment, suite, unit) */ - address2?: string; - /** City, district, village, or town */ - city?: string; - /** Company or organization name */ - company?: string; - /** - * Two-letter country code - REQUIRED - * Must be exactly 2 characters (ISO 3166-1 alpha-2 format) - * Examples: "US", "CA", "GB", "AU" - */ - countryCode: string; - /** First name of the customer */ - firstName?: string; - /** Last name of the customer */ - lastName?: string; - /** Phone number (E.164 format recommended, e.g., +16135551111) */ - phone?: string; - /** Province/state code (e.g., "CA", "ON") */ - provinceCode?: string; - /** Zip or postal code */ - zip?: string; -} - -/** - * Delivery address with selection state. - * Used within CartDeliveryInput. - */ -export interface CartDeliveryAddressInput { - /** - * The delivery address details. - */ - address: MailingAddressInput; - /** - * Whether this address is selected as the active delivery address. - * Optional - use to pre-select an address from multiple options. - */ - selected?: boolean; -} - -/** - * Delivery-related fields for the cart. - * - * @see https://shopify.dev/docs/api/storefront/latest/input-objects/CartDeliveryInput - */ -export interface CartDeliveryInput { - /** - * Array of selectable addresses presented to the buyer. - */ - addresses?: CartDeliveryAddressInput[]; -} - -/** - * The customer associated with the cart. - * - * @see https://shopify.dev/docs/api/storefront/latest/input-objects/CartBuyerIdentityInput - */ -export interface CartBuyerIdentityInput { - /** Email address of the buyer */ - email?: string; - /** Phone number of the buyer */ - phone?: string; - /** Two-letter country code for the buyer's location */ - countryCode?: string; -} - -/** - * Cart input for updating cart state via checkout events. - * Mirrors the Storefront API CartInput structure. - * - * @see https://shopify.dev/docs/api/storefront/latest/input-objects/CartInput - */ -export interface CartInput { - /** - * Delivery-related fields for the cart. - */ - delivery?: CartDeliveryInput; - /** - * The customer associated with the cart. - * Optional - use to update buyer identity information. - */ - buyerIdentity?: CartBuyerIdentityInput; - /** - * Case-insensitive discount codes. - * Optional - use to apply discount codes to the cart. - */ - discountCodes?: string[]; - /** - * Payment instruments for the cart. - * Optional - use to update payment methods. - */ - paymentInstruments?: CartPaymentInstrumentInput[]; -} - /** * Event emitted when checkout starts an address change flow. * @@ -551,9 +499,9 @@ export interface CheckoutAddressChangeStartEvent { */ export interface CheckoutAddressChangeStartResponsePayload { /** - * Updated cart input with the delivery addresses to set. + * Updated cart with the delivery addresses to set. */ - cart?: CartInput; + cart?: Cart; /** * Optional array of errors if the address selection failed. */ @@ -574,45 +522,6 @@ export type CardBrand = | 'MAESTRO' | 'UNKNOWN'; -/** - * Expiry date for a payment instrument. - */ -export interface ExpiryInput { - /** Month (1-12) */ - month: number; - /** Four-digit year */ - year: number; -} - -/** - * Display fields for a payment instrument shown to the buyer. - */ -export interface CartPaymentInstrumentDisplayInput { - /** Last 4 digits of the card number */ - last4: string; - /** Card brand (e.g., VISA, MASTERCARD) */ - brand: CardBrand; - /** Name of the cardholder */ - cardHolderName: string; - /** Card expiry date */ - expiry: ExpiryInput; -} - -/** - * Input type for creating/updating payment instruments, aligned with Storefront API. - * Display fields are grouped separately from the billing address. - * - * @see https://shopify.dev/docs/api/storefront/latest/input-objects/CartPaymentInstrumentInput - */ -export interface CartPaymentInstrumentInput { - /** Unique identifier for this payment instrument */ - externalReference: string; - /** Display information for the payment instrument */ - display: CartPaymentInstrumentDisplayInput; - /** Billing address for the payment instrument */ - billingAddress: MailingAddressInput; -} - /** * Event emitted when the buyer intends to change their payment method. */ @@ -631,9 +540,9 @@ export interface CheckoutPaymentMethodChangeStartEvent { */ export interface CheckoutPaymentMethodChangeStartResponsePayload { /** - * Updated cart input with the payment instruments to set. + * Updated cart with the payment methods to set. */ - cart?: CartInput; + cart?: Cart; /** * Optional array of errors if the payment method selection failed. */ @@ -669,31 +578,20 @@ export interface CheckoutSubmitStartEvent { sessionId: string; } -/** - * Payment token input for delegated payment processing. - */ -export interface PaymentTokenInput { - token: string; - tokenType: string; - tokenProvider: string; -} - /** * Response payload for CheckoutSubmitStartEvent event. * Use with ShopifyCheckoutEventProvider.respondToEvent() or useShopifyEvent().respondWith() * + * Payment credentials should be provided via cart.payment.methods[].instruments[].credentials + * * Note: This response is only used when native payment delegation is enabled * for the authenticated app. */ export interface CheckoutSubmitStartResponsePayload { /** - * Optional payment token information for delegated payment processing. - */ - payment?: PaymentTokenInput; - /** - * Updated cart input with delivery addresses and optional buyer identity. + * Updated cart with payment credentials. */ - cart?: CartInput; + cart?: Cart; /** * Optional array of errors if the submission failed. */ diff --git a/modules/@shopify/checkout-sheet-kit/src/index.ts b/modules/@shopify/checkout-sheet-kit/src/index.ts index 9aec7839..c8b7db04 100644 --- a/modules/@shopify/checkout-sheet-kit/src/index.ts +++ b/modules/@shopify/checkout-sheet-kit/src/index.ts @@ -501,27 +501,22 @@ export type { export type { Cart, CartAddress, - CartBuyerIdentityInput, - CartDeliveryAddressInput, - CartDeliveryInput, - CartInput, - CardBrand, + CartCredential, CartPayment, CartPaymentInstrument, - CartPaymentInstrumentDisplayInput, - CartPaymentInstrumentInput, - PaymentTokenInput, + CartPaymentMethod, + CartSelectableAddress, + CardBrand, CheckoutAddressChangeStartEvent, CheckoutAddressChangeStartResponsePayload, CheckoutCompleteEvent, - CheckoutPaymentMethodChangeStartEvent , + CheckoutPaymentMethodChangeStartEvent, CheckoutPaymentMethodChangeStartResponsePayload, CheckoutResponseError, - ExpiryInput, CheckoutStartEvent, CheckoutSubmitStartEvent, CheckoutSubmitStartResponsePayload, - MailingAddressInput, + RemoteTokenPaymentCredential, } from './events.d'; // Component types diff --git a/modules/@shopify/checkout-sheet-kit/tests/testFixtures.ts b/modules/@shopify/checkout-sheet-kit/tests/testFixtures.ts index 1be41ec2..0efd24cd 100644 --- a/modules/@shopify/checkout-sheet-kit/tests/testFixtures.ts +++ b/modules/@shopify/checkout-sheet-kit/tests/testFixtures.ts @@ -70,7 +70,7 @@ export function createTestCart(overrides?: Partial): Cart { ], }, payment: { - instruments: [], + methods: [], }, ...overrides, }; diff --git a/sample/src/screens/BuyNow/AddressScreen.tsx b/sample/src/screens/BuyNow/AddressScreen.tsx index 82cce6d7..40832e2c 100644 --- a/sample/src/screens/BuyNow/AddressScreen.tsx +++ b/sample/src/screens/BuyNow/AddressScreen.tsx @@ -88,6 +88,7 @@ export default function AddressScreen() { const response: CheckoutAddressChangeStartResponsePayload = { cart: { + ...route.params.cart, delivery: { addresses: [ { diff --git a/sample/src/screens/BuyNow/CheckoutScreen.tsx b/sample/src/screens/BuyNow/CheckoutScreen.tsx index 124e5422..5513c7bc 100644 --- a/sample/src/screens/BuyNow/CheckoutScreen.tsx +++ b/sample/src/screens/BuyNow/CheckoutScreen.tsx @@ -48,24 +48,42 @@ export default function CheckoutScreen(props: { const onAddressChangeStart = (event: CheckoutAddressChangeStartEvent) => { console.log(' onAddressChangeStart: ', event); - navigation.navigate('Address', {id: event.id}); + navigation.navigate('Address', {id: event.id, cart: event.cart}); }; const onPaymentMethodChangeStart = ( event: CheckoutPaymentMethodChangeStartEvent, ) => { console.log(' onPaymentMethodChangeStart: ', event); - navigation.navigate('Payment', {id: event.id}); + navigation.navigate('Payment', {id: event.id, cart: event.cart}); }; const onSubmitStart = async (event: CheckoutSubmitStartEvent) => { console.log(' onSubmitStart', event); try { await eventContext?.respondToEvent(event.id, { - payment: { - token: '1234567890', - tokenType: 'delegated', - tokenProvider: 'shopify', + cart: { + ...event.cart, + payment: { + methods: [ + { + instruments: [ + { + externalReferenceId: 'payment-instrument-123', + credentials: [ + { + remoteTokenPaymentCredential: { + token: '1234567890', + tokenType: 'delegated', + tokenHandler: 'shopify', + }, + }, + ], + }, + ], + }, + ], + }, }, }); } catch (error) { diff --git a/sample/src/screens/BuyNow/PaymentScreen.tsx b/sample/src/screens/BuyNow/PaymentScreen.tsx index b9fb2005..6c2251ec 100644 --- a/sample/src/screens/BuyNow/PaymentScreen.tsx +++ b/sample/src/screens/BuyNow/PaymentScreen.tsx @@ -24,9 +24,8 @@ import {Button, StyleSheet, Text, TouchableOpacity, View} from 'react-native'; import { useShopifyEvent, type CardBrand, - type CartPaymentInstrumentInput, + type CartPaymentInstrument, type CheckoutPaymentMethodChangeStartResponsePayload, - type MailingAddressInput, } from '@shopify/checkout-sheet-kit'; import {useCart} from '../../context/Cart'; import type {BuyNowStackParamList} from './types'; @@ -38,66 +37,70 @@ export default function PaymentScreen() { const {selectedPaymentIndex, setSelectedPaymentIndex} = useCart(); const paymentOptions: Array<{ - id: string; label: string; - cardHolderName: string; - last4: string; - brand: CardBrand; - expiry: {month: number; year: number}; - billingAddress: MailingAddressInput; + instrument: CartPaymentInstrument; }> = [ { - id: 'card-personal-visa-4242', label: 'Personal Visa', - cardHolderName: 'John Doe', - last4: '4242', - brand: 'VISA', - expiry: {month: 12, year: 2028}, - billingAddress: { - firstName: 'John', - lastName: 'Doe', - address1: '123 Main St', - city: 'San Francisco', - provinceCode: 'CA', - countryCode: 'US', - zip: '94102', + instrument: { + externalReferenceId: 'card-personal-visa-4242', + cardHolderName: 'John Doe', + lastDigits: '4242', + brand: 'VISA', + month: 12, + year: 2028, + billingAddress: { + firstName: 'John', + lastName: 'Doe', + address1: '123 Main St', + city: 'San Francisco', + provinceCode: 'CA', + countryCode: 'US', + zip: '94102', + }, }, }, { - id: 'card-business-mc-5555', label: 'Business MasterCard', - cardHolderName: 'Jane Smith', - last4: '5555', - brand: 'MASTERCARD', - expiry: {month: 6, year: 2027}, - billingAddress: { - firstName: 'Jane', - lastName: 'Smith', - address1: '456 Market St', - city: 'San Francisco', - provinceCode: 'CA', - countryCode: 'US', - zip: '94103', + instrument: { + externalReferenceId: 'card-business-mc-5555', + cardHolderName: 'Jane Smith', + lastDigits: '5555', + brand: 'MASTERCARD', + month: 6, + year: 2027, + billingAddress: { + firstName: 'Jane', + lastName: 'Smith', + address1: '456 Market St', + city: 'San Francisco', + provinceCode: 'CA', + countryCode: 'US', + zip: '94103', + }, }, }, { - id: 'card-corporate-amex-0005', label: 'Corporate Amex', - cardHolderName: 'Corporate Account', - last4: '0005', - brand: 'AMERICAN_EXPRESS', - expiry: {month: 3, year: 2026}, - billingAddress: { - firstName: 'Corporate', - lastName: 'Billing', - address1: '123 Business Blvd', - address2: 'Suite 500', - city: 'New York', - provinceCode: 'NY', - countryCode: 'US', - zip: '10001', - phone: '+1-212-555-0100', - company: 'Acme Corporation', + instrument: { + externalReferenceId: 'card-corporate-amex-0005', + cardHolderName: 'Corporate Account', + lastDigits: '0005', + brand: 'AMERICAN_EXPRESS', + month: 3, + year: 2026, + billingAddress: { + firstName: 'Corporate', + lastName: 'Billing', + address1: '123 Business Blvd', + address2: 'Suite 500', + city: 'New York', + provinceCode: 'NY', + countryCode: 'US', + zip: '10001', + phone: '+1-212-555-0100', + company: 'Acme Corporation', + }, }, }, ]; @@ -106,20 +109,16 @@ export default function PaymentScreen() { const selectedPayment = paymentOptions[selectedPaymentIndex]; if (!selectedPayment) return; - const paymentInstrument: CartPaymentInstrumentInput = { - externalReference: selectedPayment.id, - display: { - last4: selectedPayment.last4, - brand: selectedPayment.brand, - cardHolderName: selectedPayment.cardHolderName, - expiry: selectedPayment.expiry, - }, - billingAddress: selectedPayment.billingAddress, - }; - const response: CheckoutPaymentMethodChangeStartResponsePayload = { cart: { - paymentInstruments: [paymentInstrument], + ...route.params.cart, + payment: { + methods: [ + { + instruments: [selectedPayment.instrument], + }, + ], + }, }, }; @@ -127,7 +126,7 @@ export default function PaymentScreen() { navigation.goBack(); }; - const getCardIcon = (_brand: CardBrand) => { + const getCardIcon = (_brand: CardBrand | undefined) => { return '💳'; }; @@ -152,14 +151,14 @@ export default function PaymentScreen() { - {getCardIcon(option.brand)} + {getCardIcon(option.instrument.brand)} {option.label} - {option.brand} •••• {option.last4} + {option.instrument.brand} •••• {option.instrument.lastDigits} - {option.billingAddress.city}, {option.billingAddress.provinceCode} + {option.instrument.billingAddress?.city}, {option.instrument.billingAddress?.provinceCode} diff --git a/sample/src/screens/BuyNow/types.ts b/sample/src/screens/BuyNow/types.ts index 30312f15..d0e208f1 100644 --- a/sample/src/screens/BuyNow/types.ts +++ b/sample/src/screens/BuyNow/types.ts @@ -1,5 +1,7 @@ +import type {Cart} from '@shopify/checkout-sheet-kit'; + export type BuyNowStackParamList = { Checkout: {url: string, auth?: string}; - Address: {id: string}; - Payment: {id: string}; + Address: {id: string; cart: Cart}; + Payment: {id: string; cart: Cart}; }; From fe11a471e75a4c3d5049533f37cc472419959c8f Mon Sep 17 00:00:00 2001 From: Kieran Osgood Date: Tue, 16 Dec 2025 10:08:27 +0000 Subject: [PATCH 2/2] fix: swift test compilation --- .../ios/ReactNativeTests/RCTCheckoutWebViewTests.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sample/ios/ReactNativeTests/RCTCheckoutWebViewTests.swift b/sample/ios/ReactNativeTests/RCTCheckoutWebViewTests.swift index ed9c5244..f06ef6b1 100644 --- a/sample/ios/ReactNativeTests/RCTCheckoutWebViewTests.swift +++ b/sample/ios/ReactNativeTests/RCTCheckoutWebViewTests.swift @@ -321,7 +321,7 @@ class RCTCheckoutWebViewTests: XCTestCase { appliedGiftCards: [], discountAllocations: [], delivery: CartDelivery(addresses: []), - payment: .init(instruments: []) + payment: .init(methods: []) ) let event = CheckoutStartEvent(cart: cart, locale: "en-US") @@ -589,7 +589,7 @@ private func createTestCart( appliedGiftCards: [], discountAllocations: [], delivery: .init(addresses: []), - payment: .init(instruments: []) + payment: .init(methods: []) ) } @@ -597,10 +597,10 @@ private func createTestCart( private func createEmptyCheckoutCompleteEvent(id: String) -> CheckoutCompleteEvent { return CheckoutCompleteEvent( orderConfirmation: OrderConfirmation( - url: "https://example.com/order", order: OrderConfirmation.Order(id: id), - number: "1001", - isFirstOrder: true + isFirstOrder: true, + url: "https://example.com/order", + number: "1001" ), cart: createTestCart() )