Skip to content

feat(pos): add TypeScript types for pos.app.ready.data background target#4123

Open
vctrchu wants to merge 8 commits into2026-07-rcfrom
vchu/bgx-types-pos-app-ready-data
Open

feat(pos): add TypeScript types for pos.app.ready.data background target#4123
vctrchu wants to merge 8 commits into2026-07-rcfrom
vchu/bgx-types-pos-app-ready-data

Conversation

@vctrchu
Copy link
Copy Markdown
Contributor

@vctrchu vctrchu commented Mar 17, 2026

What

Add TypeScript types for the pos.app.ready.data persistent 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 typed Event subclasses (transactioncomplete, cashtrackingsessionstart, cashtrackingsessioncomplete).
  • DataExtensionTargets — New target interface with pos.app.ready.data as a RunnableExtension<DataTargetApi, undefined>.
  • DataExtensionTarget — Type alias for data target keys.
  • ShopifyGlobal — POS shopify global type, augmented with addEventListener / removeEventListener keyed on ShopifyEventMap. Follows the same declare global pattern Checkout uses.

Event types

  • TransactionCompleteEvent — Discriminated union of three module-local variant interfaces (SaleCompleteEvent | ReturnCompleteEvent | ExchangeCompleteEvent), each extending BaseTransactionCompleteEvent. All fields are readonly. Narrow on event.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 — only TransactionCompleteEvent is part of the public surface.
  • BaseTransactionCompleteEvent — Internal shared base extending Event. Contains the 12 fields common to Sale/Return/Exchange (orderId, customer, grandTotal, taxTotal, paymentMethods, shippingLines, etc.).
  • CashTrackingSessionStartEventEvent subclass with id: number and openingTime: string (ISO 8601).
  • CashTrackingSessionCompleteEvent — Adds closingTime: string.

Folder structure

Event types live at the surface-level point-of-sale/events/ (sibling to api/ and globals.ts), not nested inside api/data-target-api/. Host events are dispatched on the global shopify object and consumable by any target, so they aren't scoped to a specific API. The layout mirrors the existing api.ts + api/ pattern:

point-of-sale/
├── api.ts     + api/       (APIs)
├── events.ts  + events/    (host events)
└── globals.ts

shopify global augmentation

POS host events are received via shopify.addEventListener() on the global shopify object, consistent with TAG proposal Shopify/ui-api-design#1418.

shopify.addEventListener('transactioncomplete', (event) => {
  if (event.transactionType === 'Sale') {
    console.log(event.lineItems, event.grandTotal);
  }
});

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.observe targets with a single long-lived target that receives events via shopify.addEventListener, aligning with the shape TAG approved in Shopify/ui-api-design#1418.

Related PRs

  • ui-api-design (TAG proposal): Shopify/ui-api-design#1418
  • extensibility host (eventListenerPlugin + eventDispatchFactory): Shopify/extensibility#1064
  • pos-mobile wiring: shop/world#508117, shop/world#508119
  • Observe target deprecation: Mark POS .observe extension targets as @deprecated #4146

Status

Draft until TAG approves Shopify/ui-api-design#1418. Types will publish with the 2026-07 quarterly release train.

@github-actions
Copy link
Copy Markdown
Contributor

🚨🚨🚨 Docs migration in progress 🚨🚨🚨

We are actively migrating UI extension reference docs to MDX in the areas/platforms/shopify-dev zone of the monorepo. This impacts docs for the following surfaces:

During this migration, please be aware of the following:

.doc.ts files are being deprecated. Changes to .doc.ts files in this repo will not be reflected in the new MDX-based docs. If you need to update docs for a reference that has already been migrated, make your changes directly in the areas/platforms/shopify-dev zone of the monorepo instead.

Doc comments in .ts source files (the comment blocks above types and functions) are also affected. Generating docs from these comments currently requires a newer version of the @shopify/generate-docs library that isn't yet available. Updates to doc comments may not produce the expected output until the migration is complete.

Examples that previously lived in this repo are being moved to the areas/platforms/shopify-dev zone of the monorepo and should be authored there going forward.

What should I do?

  • If your PR includes changes to .doc.ts files, doc comments, or examples, please reach out to us in #devtools-proj-templated-refs so we can help ensure your updates are captured correctly.
  • If your PR is limited to source code changes (non-docs), you can ignore this notice.

Thanks for your patience while we complete the migration! 🙏

@vctrchu vctrchu self-assigned this Mar 17, 2026
@vctrchu vctrchu force-pushed the vchu/bgx-types-pos-app-ready-data branch from 1ac2792 to 405dc2b Compare March 17, 2026 23:23
@vctrchu vctrchu changed the base branch from 2026-04-rc to vchu/fix-doc-gen-world-path March 17, 2026 23:23
Copy link
Copy Markdown
Contributor Author

vctrchu commented Mar 17, 2026

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.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

Copy link
Copy Markdown
Contributor

@NathanJolly NathanJolly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR has the target definition and DataTargetApi, but it's missing two things that should ship together:

  1. 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-specific PosEventMap with concrete event types + payloads should be defined here.

  2. PosEventMap — the typed event map with transaction_complete, cash_tracking_session_start, cash_tracking_session_complete and 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.

@vctrchu
Copy link
Copy Markdown
Contributor Author

vctrchu commented Mar 18, 2026

Added both. DataTargetApi now includes addEventListener and removeEventListener using the generic types from the TAG proposal (ui-api-design #1418). Also added PosEventMap with transaction_complete, cash_tracking_session_start, and cash_tracking_session_complete — no cart_update since that's state, not an event.

@vctrchu vctrchu changed the title Add TypeScript types for pos.app.ready.data background target feat(pos): add BGX types — pos.app.ready.data target + storage.keys subscribable Mar 27, 2026
@vctrchu vctrchu force-pushed the vchu/bgx-types-pos-app-ready-data branch from 8ce7778 to 06eeda4 Compare March 27, 2026 23:15
@vctrchu vctrchu changed the title feat(pos): add BGX types — pos.app.ready.data target + storage.keys subscribable feat(pos): add TypeScript types for pos.app.ready.data background target Mar 27, 2026
@vctrchu vctrchu force-pushed the vchu/bgx-types-pos-app-ready-data branch 2 times, most recently from e2c600a to e63759e Compare March 30, 2026 21:36
@vctrchu vctrchu marked this pull request as ready for review March 30, 2026 21:39
@vctrchu vctrchu changed the base branch from vchu/fix-doc-gen-world-path to 2026-04-rc March 30, 2026 21:56
@vctrchu vctrchu force-pushed the vchu/bgx-types-pos-app-ready-data branch 3 times, most recently from c9e53ce to 83610ef Compare March 30, 2026 22:07
@vctrchu
Copy link
Copy Markdown
Contributor Author

vctrchu commented Mar 30, 2026

/snapit

@vctrchu vctrchu force-pushed the vchu/bgx-types-pos-app-ready-data branch from 83610ef to 3b55663 Compare March 30, 2026 22:14
@shopify-github-actions-access
Copy link
Copy Markdown
Contributor

🫰✨ Thanks @vctrchu! Your snapshots have been published to npm.

Test the snapshots by updating your package.json with the newly published versions:

"@shopify/ui-extensions": "0.0.0-snapshot-20260330221356",
"@shopify/ui-extensions-tester": "0.0.0-snapshot-20260330221356"

@vctrchu vctrchu force-pushed the vchu/bgx-types-pos-app-ready-data branch 3 times, most recently from 93c4e1d to 6edab18 Compare March 31, 2026 01:07
@vctrchu vctrchu requested review from a team and NathanJolly March 31, 2026 01:14
@vctrchu vctrchu marked this pull request as draft March 31, 2026 23:26
@vctrchu vctrchu force-pushed the vchu/bgx-types-pos-app-ready-data branch from 6edab18 to 5d0a28d Compare April 1, 2026 20:10
@shopify-github-actions-access
Copy link
Copy Markdown
Contributor

🫰✨ Thanks @vctrchu! Your snapshots have been published to npm.

Test the snapshots by updating your package.json with the newly published versions:

"@shopify/ui-extensions": "0.0.0-snapshot-20260420225138",
"@shopify/ui-extensions-tester": "0.0.0-snapshot-20260420225138"

Copy link
Copy Markdown
Contributor

@js-goupil js-goupil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be Order { id: number } like customer?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@vctrchu vctrchu requested review from a team and js-goupil April 22, 2026 17:56
@runmad runmad requested review from aaronschubert0, andy-chhuon and fatbattk and removed request for a team April 23, 2026 03:46
@runmad
Copy link
Copy Markdown

runmad commented Apr 23, 2026

Review summary: NEEDS CHANGES (one HIGH, two MEDIUMs). Type design is solid overall, discriminated union on transactionType, readonly fields, clean event/API separation.

HIGH, Event name strings are an untested contract with the runtime

ShopifyEventMap uses transactioncomplete, cashtrackingsessionstart, cashtrackingsessioncomplete. These type-check regardless of what the host dispatches, so a mismatch is silent: listeners compile but never fire. Please confirm the exact strings against the host eventDispatchFactory / mobile wiring, export them as named constants, and add a contract test that registers a listener and verifies dispatch reaches it.

MEDIUM, No mock shopify global in the tester harness

createDataTargetMock returns a correct DataTargetApi, but extension authors will call shopify.addEventListener in their code. Without a tester-provided shopify shim, tests throw at runtime. Add a mock shopify global with addEventListener/removeEventListener and a dispatch helper so tests can fire events deterministically.

MEDIUM, @ts-ignore on the shopify global declaration is fragile

The @ts-ignore suppressing the collision with build/ts/globals.d.ts hides real merge conflicts. If another surface redeclares shopify with a different shape, precedence is compile-order-dependent with no error. Prefer declare global { interface } merging or a shared base type across surfaces.

LOW (optional follow-ups)

  • addEventListener lacks options?: AddEventListenerOptions, adding it now avoids a breaking change later for once / AbortSignal.
  • CashTrackingSessionCompleteEvent duplicates id and openingTime from the start event instead of extending it.
  • Naming inconsistency: new ShopifyEventMap uses lowercase keys while legacy event/data.ts uses PascalCase and now re-exports the new types, creating two parallel hierarchies. Worth a doc note on the canonical path.

Happy to re-review once the HIGH and at least the tester-mock MEDIUM are addressed.

Copy link
Copy Markdown

@runmad runmad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comments ^

vctrchu and others added 2 commits April 23, 2026 16:07
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>
@vctrchu
Copy link
Copy Markdown
Contributor Author

vctrchu commented Apr 23, 2026

@runmad

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

vctrchu and others added 2 commits April 23, 2026 17:07
- 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>
Copy link
Copy Markdown

@runmad runmad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Host-contract test for event dispatch once the dispatcher wiring lands
  2. Cross-surface shopify global declaration cleanup to remove the @ts-ignore

Copy link
Copy Markdown

@aaronschubert0 aaronschubert0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of questions 🙂 otherwise looks good.

*
* ```ts
* shopify.addEventListener('transactioncomplete', (event) => { ... });
* extension.dispatch('transactioncomplete', { transaction: {...} });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we able to type this more strictly to match the ShopifyEventMap that was defined in events.ts?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, yea I'll update it to use the real types

Copy link
Copy Markdown
Contributor

@js-goupil js-goupil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@vctrchu vctrchu force-pushed the vchu/bgx-types-pos-app-ready-data branch from 8eb046e to 39a5dc0 Compare April 24, 2026 21:22
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>
@vctrchu vctrchu force-pushed the vchu/bgx-types-pos-app-ready-data branch from 39a5dc0 to 5573d55 Compare April 24, 2026 21:24
@vctrchu
Copy link
Copy Markdown
Contributor Author

vctrchu commented Apr 24, 2026

/snapit

@shopify-github-actions-access
Copy link
Copy Markdown
Contributor

🫰✨ Thanks @vctrchu! Your snapshots have been published to npm.

Test the snapshots by updating your package.json with the newly published versions:

"@shopify/ui-extensions": "0.0.0-snapshot-20260424224637",
"@shopify/ui-extensions-tester": "0.0.0-snapshot-20260424224637"

@vctrchu vctrchu force-pushed the vchu/bgx-types-pos-app-ready-data branch from 89cde45 to 1de9812 Compare April 24, 2026 23:24
@vctrchu
Copy link
Copy Markdown
Contributor Author

vctrchu commented Apr 24, 2026

/snapit

@shopify-github-actions-access
Copy link
Copy Markdown
Contributor

🫰✨ Thanks @vctrchu! Your snapshots have been published to npm.

Test the snapshots by updating your package.json with the newly published versions:

"@shopify/ui-extensions": "0.0.0-snapshot-20260424233851",
"@shopify/ui-extensions-tester": "0.0.0-snapshot-20260424233851"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

#gsd:49396 POS App: Persistent background extension capability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants