From 432d2c963eb1d9d35bcdd1f660ceb757c764bcc6 Mon Sep 17 00:00:00 2001 From: js-goupil Date: Thu, 3 Sep 2026 14:07:12 -0400 Subject: [PATCH] Add cart.updateLineItemQuantity to the POS Cart API (2026-10) Line-scoped quantity update that preserves the line item's properties, discounts, and selling plans. Available on API version 2026-10 and later; quantity must be a positive integer (use removeLineItem to remove a line), and a uuid matching multiple split-line allocations throws rather than guessing the target line. Includes a docs example that reads the uuid from the cart signal, tester mock support, and a changeset. Assisted-By: devx/7cc61f9e-de33-47f7-a151-9c7b33f92276 --- .../pos-cart-update-line-item-quantity.md | 6 +++++ .../src/point-of-sale/README.md | 22 ++++++++++++++++-- .../src/point-of-sale/factories.ts | 1 + .../src/tests/pos-resolution-targets.test.ts | 1 + .../cart-api/update-line-item-quantity.jsx | 23 +++++++++++++++++++ .../point-of-sale/api/cart-api/cart-api.ts | 14 +++++++++++ 6 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 .changeset/pos-cart-update-line-item-quantity.md create mode 100644 packages/ui-extensions/docs/surfaces/point-of-sale/reference/examples/cart-api/update-line-item-quantity.jsx diff --git a/.changeset/pos-cart-update-line-item-quantity.md b/.changeset/pos-cart-update-line-item-quantity.md new file mode 100644 index 0000000000..57f7d20903 --- /dev/null +++ b/.changeset/pos-cart-update-line-item-quantity.md @@ -0,0 +1,6 @@ +--- +'@shopify/ui-extensions': minor +'@shopify/ui-extensions-tester': patch +--- + +Add `cart.updateLineItemQuantity` to the POS Cart API for API version 2026-10. The method updates one line item's quantity while preserving its properties, discounts, and selling plans. diff --git a/packages/ui-extensions-tester/src/point-of-sale/README.md b/packages/ui-extensions-tester/src/point-of-sale/README.md index a30a20cc41..81dc8ef8c6 100644 --- a/packages/ui-extensions-tester/src/point-of-sale/README.md +++ b/packages/ui-extensions-tester/src/point-of-sale/README.md @@ -49,9 +49,27 @@ extension.shopify.storage = createStorage({ }); ``` -## 🔒 Mocking mutation return values +## 🔒 Mocking cart mutations -Replace mutation functions with `vi.fn()` and use `createResult()` to build typed return values. The first argument is the mutation name; the second is an optional result override. +The target API mock includes asynchronous stubs for Cart API mutations, including `updateLineItemQuantity`. Replace a stub with a spy when you need to verify a call: + +```ts +const updateLineItemQuantity = vi.spyOn( + extension.shopify.cart, + 'updateLineItemQuantity', +); + +await extension.shopify.cart.updateLineItemQuantity( + 'line-item-uuid', + 2, +); + +expect( + updateLineItemQuantity, +).toHaveBeenCalledWith('line-item-uuid', 2); +``` + +For mutations that return data, replace the function with `vi.fn()` and use `createResult()` to build typed return values. The first argument is the mutation name; the second is an optional result override. ```ts import { diff --git a/packages/ui-extensions-tester/src/point-of-sale/factories.ts b/packages/ui-extensions-tester/src/point-of-sale/factories.ts index 65a7596eeb..b581e0663e 100644 --- a/packages/ui-extensions-tester/src/point-of-sale/factories.ts +++ b/packages/ui-extensions-tester/src/point-of-sale/factories.ts @@ -224,6 +224,7 @@ function createMockCartApi(): CartApi { removeCustomer: async () => {}, addCustomSale: async () => '', addLineItem: async () => '', + updateLineItemQuantity: async () => {}, removeLineItem: async () => {}, addCartProperties: async () => {}, removeCartProperties: async () => {}, diff --git a/packages/ui-extensions-tester/src/tests/pos-resolution-targets.test.ts b/packages/ui-extensions-tester/src/tests/pos-resolution-targets.test.ts index b3de09332e..599c42a9b0 100644 --- a/packages/ui-extensions-tester/src/tests/pos-resolution-targets.test.ts +++ b/packages/ui-extensions-tester/src/tests/pos-resolution-targets.test.ts @@ -17,6 +17,7 @@ describe('pos.cart.validations.resolution.render', () => { expect(api.cart.current).toBeDefined(); expect(typeof api.cart.addLineItem).toBe('function'); + expect(typeof api.cart.updateLineItemQuantity).toBe('function'); expect(api.scanner).toBeDefined(); }); diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/reference/examples/cart-api/update-line-item-quantity.jsx b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/examples/cart-api/update-line-item-quantity.jsx new file mode 100644 index 0000000000..072528f62b --- /dev/null +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/reference/examples/cart-api/update-line-item-quantity.jsx @@ -0,0 +1,23 @@ +import {render} from 'preact'; + +export default async () => { + render(, document.body); +}; + +const Extension = () => { + return ( + { + const [lineItem] = shopify.cart.current.value.lineItems; + if (lineItem) { + shopify.cart.updateLineItemQuantity( + lineItem.uuid, + lineItem.quantity + 1, + ); + } + }} + /> + ); +}; diff --git a/packages/ui-extensions/src/surfaces/point-of-sale/api/cart-api/cart-api.ts b/packages/ui-extensions/src/surfaces/point-of-sale/api/cart-api/cart-api.ts index d1aba4d46d..fbaf469a6b 100644 --- a/packages/ui-extensions/src/surfaces/point-of-sale/api/cart-api/cart-api.ts +++ b/packages/ui-extensions/src/surfaces/point-of-sale/api/cart-api/cart-api.ts @@ -138,6 +138,20 @@ export interface MutableCartApiContent { options?: AddLineItemOptions, ): Promise; + /** + * Set the quantity of an existing line item identified by its `UUID`, preserving the line item's properties, discounts, and selling plans. This is equivalent to a merchant adjusting the quantity on the native cart line. + * + * If POS has split the line into multiple allocations that share the same `UUID` (for example, lines split across delivery methods), the target line is ambiguous and the call throws instead of guessing. + * + * Only available on API version `2026-10` and later. + * + * @param uuid the UUID of the line item to update + * @param quantity the new absolute quantity; must be an integer of 1 or greater. To remove a line item, use `removeLineItem` instead. + * @returns A promise that resolves after the cart state reflects the change. + * @throws {Error} if the line item is not found, the `UUID` matches multiple split-line allocations, the quantity is invalid, the cart is not editable because it is a return or exchange, or the POS app version does not support this method. + */ + updateLineItemQuantity(uuid: string, quantity: number): Promise; + /** * Remove a specific line item from the cart using its `UUID`. The line item will be completely removed from the cart along with any associated discounts, properties, or selling plans. *