diff --git a/.changeset/pos-remove-action-api-from-action-render.md b/.changeset/pos-remove-action-api-from-action-render.md new file mode 100644 index 0000000000..e1dcab2f69 --- /dev/null +++ b/.changeset/pos-remove-action-api-from-action-render.md @@ -0,0 +1,10 @@ +--- +'@shopify/ui-extensions': major +'@shopify/ui-extensions-tester': major +--- + +Remove the POS Action API (`shopify.action.presentModal()`) from action render targets (`*.action.render`). + +Action render targets are themselves the modal destination that `presentModal()` opens, so exposing the Action API there had no meaningful destination — calling `shopify.action.presentModal()` from `pos.cart.line-item-details.action.render` (the one action render target that still exposed it) pushed another instance of the same target onto the navigation stack, rendering a duplicate modal on top of the already-open one. + +Action render targets now expose only `ActionTargetApi` plus their contextual APIs, matching every other `*.action.render` target. Extensions that need in-workflow navigation should use the Navigation API (`shopify.navigation`). This is a versioned breaking change: the surface is removed for API versions `2026-10` and later; older API versions keep their published types. 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 981223a93b..621d3871f7 100644 --- a/packages/ui-extensions-tester/src/point-of-sale/factories.ts +++ b/packages/ui-extensions-tester/src/point-of-sale/factories.ts @@ -406,13 +406,12 @@ function createStandardActionCartLineItemMock( }; } -// Group N: ActionTargetApi + ActionApi + CartApi + CartLineItemApi -function createActionTargetActionCartLineItemMock< - T extends RenderExtensionTarget, ->(target: T): ActionTargetApi & ActionApi & CartApi & CartLineItemApi { +// Group N: ActionTargetApi + CartApi + CartLineItemApi +function createActionTargetCartLineItemMock( + target: T, +): ActionTargetApi & CartApi & CartLineItemApi { return { ...createMockActionTargetApi(target), - ...createMockActionApi(), ...createMockCartApi(), ...createMockCartLineItemApi(), }; @@ -564,9 +563,9 @@ const posMockFactories: PosMockFactory = { 'pos.cart.line-item-details.action.menu-item.render': createStandardActionCartLineItemMock, - // Group N: ActionTargetApi + ActionApi + CartApi + CartLineItemApi + // Group N: ActionTargetApi + CartApi + CartLineItemApi 'pos.cart.line-item-details.action.render': - createActionTargetActionCartLineItemMock, + createActionTargetCartLineItemMock, // Group O: Receipt targets 'pos.receipt-footer.block.render': createReceiptMock, diff --git a/packages/ui-extensions-tester/src/tests/pos-action-render-targets.test.ts b/packages/ui-extensions-tester/src/tests/pos-action-render-targets.test.ts new file mode 100644 index 0000000000..333d743e04 --- /dev/null +++ b/packages/ui-extensions-tester/src/tests/pos-action-render-targets.test.ts @@ -0,0 +1,36 @@ +import {createMockPosTargetApi} from '../point-of-sale/factories'; + +// Regression guard: action render targets are the modal destination that +// `shopify.action.presentModal()` opens, so they must not expose the Action +// API. The mock for the one-time exception should mirror the published type. +describe('pos.cart.line-item-details.action.render', () => { + it('does not expose the Action API (modal destination target)', () => { + const api = createMockPosTargetApi( + 'pos.cart.line-item-details.action.render', + ); + + expect(api.action).toBeUndefined(); + expect(api).not.toHaveProperty('action'); + }); + + it('exposes the actionable surface (cart + cartLineItem + standard APIs)', () => { + const api = createMockPosTargetApi( + 'pos.cart.line-item-details.action.render', + ); + + expect(api.cart).toBeDefined(); + expect(api.cartLineItem).toBeDefined(); + expect(api.extensionPoint).toBe('pos.cart.line-item-details.action.render'); + }); +}); + +describe('pos.cart.line-item-details.action.menu-item.render', () => { + it('still exposes the Action API (menu items launch the modal)', () => { + const api = createMockPosTargetApi( + 'pos.cart.line-item-details.action.menu-item.render', + ); + + expect(api.action).toBeDefined(); + expect(typeof api.action.presentModal).toBe('function'); + }); +}); diff --git a/packages/ui-extensions/src/surfaces/point-of-sale/extension-targets.ts b/packages/ui-extensions/src/surfaces/point-of-sale/extension-targets.ts index a34fdea2ca..a51345cd1d 100644 --- a/packages/ui-extensions/src/surfaces/point-of-sale/extension-targets.ts +++ b/packages/ui-extensions/src/surfaces/point-of-sale/extension-targets.ts @@ -309,11 +309,10 @@ export interface RenderExtensionTargets { /** * Renders a full-screen modal interface launched from cart line item menu items. Use this target for complex line item workflows that require forms, multi-step processes, or detailed information displays beyond what a simple button can provide. * - * Extensions at this target have access to detailed line item data through the Cart Line Item API and support workflows with multiple screens, navigation, and interactive components. + * Extensions at this target have access to detailed line item data through the Cart Line Item API and support workflows with multiple screens, navigation, and interactive components. Action render targets are themselves the modal destination, so the Action API (`shopify.action.presentModal()`) is not available here; use the Navigation API for in-workflow navigation. */ 'pos.cart.line-item-details.action.render': RenderExtension< ActionTargetApi<'pos.cart.line-item-details.action.render'> & - ActionApi & CartApi & CartLineItemApi, BasicComponents