feat(pos): add TypeScript types for pos.app.ready.data background target#4123
feat(pos): add TypeScript types for pos.app.ready.data background target#4123vctrchu wants to merge 8 commits into2026-07-rcfrom
Conversation
🚨🚨🚨 Docs migration in progress 🚨🚨🚨We are actively migrating UI extension reference docs to MDX in the
During this migration, please be aware of the following:
Doc comments in Examples that previously lived in this repo are being moved to the What should I do?
Thanks for your patience while we complete the migration! 🙏 |
1ac2792 to
405dc2b
Compare
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
6528efa to
2d36a20
Compare
NathanJolly
left a comment
There was a problem hiding this comment.
This PR has the target definition and DataTargetApi, but it's missing two things that should ship together:
-
addEventListener / removeEventListener — needs to be part of
DataTargetApi(or the target's API surface). The generic types are defined in the TAG proposal (ui-api-design PR #1418), and the surface-specificPosEventMapwith concrete event types + payloads should be defined here. -
PosEventMap — the typed event map with
transaction_complete,cash_tracking_session_start,cash_tracking_session_completeand their payload interfaces. This is what makes addEventListener type-safe for POS.
Without these, extensions targeting pos.app.ready.data have no way to type-safely listen for events.
|
Added both. |
8ce7778 to
06eeda4
Compare
e2c600a to
e63759e
Compare
c9e53ce to
83610ef
Compare
|
/snapit |
83610ef to
3b55663
Compare
|
🫰✨ Thanks @vctrchu! Your snapshots have been published to npm. Test the snapshots by updating your "@shopify/ui-extensions": "0.0.0-snapshot-20260330221356",
"@shopify/ui-extensions-tester": "0.0.0-snapshot-20260330221356" |
93c4e1d to
6edab18
Compare
6edab18 to
5d0a28d
Compare
|
🫰✨ Thanks @vctrchu! Your snapshots have been published to npm. Test the snapshots by updating your "@shopify/ui-extensions": "0.0.0-snapshot-20260420225138",
"@shopify/ui-extensions-tester": "0.0.0-snapshot-20260420225138" |
js-goupil
left a comment
There was a problem hiding this comment.
Couple of tiny things that we should address before merging so we don't break the navigation listener
| /** | ||
| * The unique numeric identifier for the Shopify order created by this transaction. This ID links the POS transaction to the order record in Shopify's system and can be used for order lookups, tracking, and API operations. Returns `undefined` when order creation is pending. | ||
| */ | ||
| readonly orderId?: number; |
There was a problem hiding this comment.
Should this be Order { id: number } like customer?
There was a problem hiding this comment.
Kept it raw to match the existing shape. BaseTransactionComplete already uses orderId?: number (base-transaction-complete.ts:20), and the sibling IDs on this same event (refundId, returnId, exchangeId) are all raw numbers too.
Customer is the outlier in POS, it seems like every other ID field (productId, variantId, customerId, shopId, locationId, etc.) is a raw number
There was a problem hiding this comment.
It would make sense then to refactor them all together in a future version and deprecate these fields. I just don't think the way we currently do things is correct
| * their return values are ignored, and their errors are caught without | ||
| * affecting the host or other listeners. | ||
| */ | ||
| addEventListener<K extends keyof ShopifyEventMap>( |
There was a problem hiding this comment.
This might override the addEventListener that we create in navigation-api.ts. If anything, we should move 'currententrychange key into the ShopifyEventMap in events.ts
There was a problem hiding this comment.
These are on two different globals: shopify.addEventListener (globals.ts:16) vs navigation.addEventListener (navigation-api.ts:64). No TS-level override; the nav listener keeps its own signature.
Unifying currententrychange under ShopifyEventMap is worth considering, but it'd touch the existing navigation API and depends on how the host dispatches (Shopify/extensibility#1064). I'd rather keep this PR types-only for the new data target and open a separate thread for unification if we want it
|
Review summary: NEEDS CHANGES (one HIGH, two MEDIUMs). Type design is solid overall, discriminated union on HIGH, Event name strings are an untested contract with the runtime
MEDIUM, No mock
|
Extensions at `pos.app.ready.data` register listeners via `shopify.addEventListener`, but the tester's mock `shopify` global only carried the target API — consumer tests hit `assertExists` on the deepWritableProxy and threw before any assertion ran. Augment the mock with `addEventListener` / `removeEventListener` backed by a per-extension `Map<string, Set<listener>>`, and expose `extension.dispatch(type, event)` on the harness so tests can fire host events deterministically. Listeners are fire-and-forget: errors in one are swallowed so later listeners still run, matching host semantics. Adds `shopify-events.test.ts` covering register/fire/remove, error isolation, dedup on double-add, and teardown cleanup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review feedback follow-ups: - Export POS_EVENT_NAMES constants for the three host events so consumers can avoid stringly-typed event names. ShopifyEventMap now keys off these constants rather than raw literals. - Wire the constants + event types through the generated surface d.ts via buildTargetDts, and re-export from the point-of-sale surface entry so `@shopify/ui-extensions/pos.app.ready.data` exposes them directly. - Split ShopifyGlobal into the base (always available) and a BackgroundShopifyGlobal that extends it with addEventListener / removeEventListener. Non-background targets now type-error on those calls; the background d.ts swaps in the wider interface. - Extract CashTrackingSessionEvent base shared by start/complete events instead of duplicating `id` and `openingTime` across both. - Trim type-level JSDoc that enumerated properties or restated field names already visible in the type definition. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks for the review. Went through each item: HIGH — event-name contract. POS_EVENT_NAMES now exported from point-of-sale/events.ts and threaded through the generated pos.app.ready.data d.ts; ShopifyEventMap keys off the constants. MEDIUM — tester mock. Done. addEventListener / removeEventListener on the mock shopify global plus extension.dispatch(type, event) on the harness. 10 tests cover register/fire/remove, error isolation, dedup, teardown. Validated end-to-end against my own test extension. MEDIUM — @ts-ignore. Not fixed. Tightened the comment to explain the collision, but the structural fix (shared base + per-surface declaration merging) is a cross-surface refactor outside the scope of this PR. LOW — AddEventListenerOptions. Skipping. Adding an optional third param later is non-breaking. LOW — Cash session duplication. Fixed. Shared CashTrackingSessionEvent base. LOW — parallel hierarchies. Legacy event/data.ts types aren't imported anywhere in-package. We'll be deprecating in a follow-up PR |
- Collapse #addEventListener params to a single line (prettier) - Rename "every" -> "all" in test title (jest/valid-title) - Break ternary across lines in buildTargetDts template (prettier) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Instead of enumerating each event type in the buildTargetDts template, re-export everything from `../events`. Adding a new POS event now only requires updating events.ts; the target d.ts picks it up automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
runmad
left a comment
There was a problem hiding this comment.
Re-review: APPROVED ✅
Thanks for the thorough follow-up. Each item addressed:
HIGH — event-name contract. POS_EVENT_NAMES constants + ShopifyEventMap keyed off them is the right shape. One follow-up worth tracking: the new tester tests are self-referential (tester dispatches, tester listens), so they don't yet verify host-side dispatch actually reaches listeners. When the mobile dispatcher wiring lands, add a contract test that exercises the real host path.
MEDIUM — tester mock. Covered. shopify.addEventListener / removeEventListener on the mock global plus extension.dispatch helper, with 10 tests for register/fire, fan-out, cross-event isolation, remove, dedup, error isolation, teardown cleanup, and no-op on unregistered. Good coverage.
MEDIUM — @ts-ignore on shopify global. Deferred is fine. The structural fix (shared base + per-surface declaration merging) is a legitimately cross-surface refactor. The tightened comment makes the intent explicit. Worth a follow-up ticket.
LOWs. Cash session base shared ✓. AddEventListenerOptions deferred (non-breaking) ✓. Legacy event/data.ts deprecation in follow-up PR ✓.
Re: @js-goupil's navigation collision concern — agree with Victor's reply, different globals (shopify. vs navigation.), no TS-level override. Unifying currententrychange into ShopifyEventMap is reasonable future work but out of scope here.
Two optional follow-ups to track after merge:
- Host-contract test for event dispatch once the dispatcher wiring lands
- Cross-surface
shopifyglobal declaration cleanup to remove the@ts-ignore
aaronschubert0
left a comment
There was a problem hiding this comment.
Couple of questions 🙂 otherwise looks good.
| * | ||
| * ```ts | ||
| * shopify.addEventListener('transactioncomplete', (event) => { ... }); | ||
| * extension.dispatch('transactioncomplete', { transaction: {...} }); |
There was a problem hiding this comment.
@vctrchu this is throwing me a little. Is this just assigned to the extension because of how we've structured our ui-extensions-tester or in what scenario would an extension dispatch host specific events?
There was a problem hiding this comment.
Theextension.dispatch() is purely a test-side method to simulate the host firing an event, an extension doesn't ever use it.
Checkout outlines this in their README
| * extension.dispatch('transactioncomplete', { transaction: {...} }); | ||
| * ``` | ||
| */ | ||
| dispatch(type: string, event?: unknown): void; |
There was a problem hiding this comment.
Are we able to type this more strictly to match the ShopifyEventMap that was defined in events.ts?
There was a problem hiding this comment.
Good call, yea I'll update it to use the real types
js-goupil
left a comment
There was a problem hiding this comment.
Looks great! We can refactor the ID stuff later
| /** | ||
| * The unique numeric identifier for the Shopify order created by this transaction. This ID links the POS transaction to the order record in Shopify's system and can be used for order lookups, tracking, and API operations. Returns `undefined` when order creation is pending. | ||
| */ | ||
| readonly orderId?: number; |
There was a problem hiding this comment.
It would make sense then to refactor them all together in a future version and deprecate these fields. I just don't think the way we currently do things is correct
8eb046e to
39a5dc0
Compare
Replace the hardcoded `parts.join('.') === 'pos.app.ready.data'` check
with an `isDataTarget` flag derived from the interface the target was
declared in. Any target added to `DataExtensionTargets` now gets the
wider `BackgroundShopifyGlobal` automatically — no buildTargetDts edit
required.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
39a5dc0 to
5573d55
Compare
|
/snapit |
|
🫰✨ Thanks @vctrchu! Your snapshots have been published to npm. Test the snapshots by updating your "@shopify/ui-extensions": "0.0.0-snapshot-20260424224637",
"@shopify/ui-extensions-tester": "0.0.0-snapshot-20260424224637" |
89cde45 to
1de9812
Compare
|
/snapit |
|
🫰✨ Thanks @vctrchu! Your snapshots have been published to npm. Test the snapshots by updating your "@shopify/ui-extensions": "0.0.0-snapshot-20260424233851",
"@shopify/ui-extensions-tester": "0.0.0-snapshot-20260424233851" |

What
Add TypeScript types for the
pos.app.ready.datapersistent background extension target.New types
DataTargetApi<T>— API surface for non-rendering data targets. Includes non-UI APIs (cart, storage, session, locale, connectivity, device, product search); excludes UI-presenting APIs.ShopifyEventMap— Maps event names to their typedEventsubclasses (transactioncomplete,cashtrackingsessionstart,cashtrackingsessioncomplete).DataExtensionTargets— New target interface withpos.app.ready.dataas aRunnableExtension<DataTargetApi, undefined>.DataExtensionTarget— Type alias for data target keys.ShopifyGlobal— POSshopifyglobal type, augmented withaddEventListener/removeEventListenerkeyed onShopifyEventMap. Follows the samedeclare globalpattern Checkout uses.Event types
TransactionCompleteEvent— Discriminated union of three module-local variant interfaces (SaleCompleteEvent | ReturnCompleteEvent | ExchangeCompleteEvent), each extendingBaseTransactionCompleteEvent. All fields arereadonly. Narrow onevent.transactionType('Sale'|'Return'|'Exchange') to access per-type fields (lineItems,refundId,lineItemsAdded, etc.). Shared fields (orderId,grandTotal,customer,paymentMethods, …) are available without narrowing. The three variants are internal scaffolding — onlyTransactionCompleteEventis part of the public surface.BaseTransactionCompleteEvent— Internal shared base extendingEvent. Contains the 12 fields common to Sale/Return/Exchange (orderId, customer, grandTotal, taxTotal, paymentMethods, shippingLines, etc.).CashTrackingSessionStartEvent—Eventsubclass withid: numberandopeningTime: string(ISO 8601).CashTrackingSessionCompleteEvent— AddsclosingTime: string.Folder structure
Event types live at the surface-level
point-of-sale/events/(sibling toapi/andglobals.ts), not nested insideapi/data-target-api/. Host events are dispatched on the globalshopifyobject and consumable by any target, so they aren't scoped to a specific API. The layout mirrors the existingapi.ts+api/pattern:shopifyglobal augmentationPOS host events are received via
shopify.addEventListener()on the globalshopifyobject, consistent with TAG proposal Shopify/ui-api-design#1418.Why
POS apps need a persistent background extension that starts when POS loads and runs for the session lifetime. This replaces the existing fire-and-forget
.event.observetargets with a single long-lived target that receives events viashopify.addEventListener, aligning with the shape TAG approved in Shopify/ui-api-design#1418.Related PRs
eventListenerPlugin+eventDispatchFactory): Shopify/extensibility#1064Status
Draft until TAG approves Shopify/ui-api-design#1418. Types will publish with the 2026-07 quarterly release train.