From c37a8e9ad6db4a56dbfc59df54a14400951f5581 Mon Sep 17 00:00:00 2001 From: Victor Chu Date: Fri, 18 Sep 2026 16:35:07 -0700 Subject: [PATCH 1/9] Add temporary POS cart product-check template --- pos-action/README.md.liquid | 28 +++++++++ pos-action/locales/en.default.json.liquid | 7 ++- pos-action/locales/fr.json.liquid | 7 ++- pos-action/package.json.liquid | 18 +----- pos-action/shopify.extension.toml.liquid | 28 ++++----- pos-action/src/Action.liquid | 75 ----------------------- pos-action/src/Background.liquid | 19 ++++++ pos-action/src/CartResolution.liquid | 47 ++++++++++++++ pos-action/src/MenuItem.liquid | 48 --------------- pos-action/src/ProductCheck.liquid | 3 + pos-action/tsconfig.json.liquid | 12 ---- 11 files changed, 123 insertions(+), 169 deletions(-) create mode 100644 pos-action/README.md.liquid delete mode 100644 pos-action/src/Action.liquid create mode 100644 pos-action/src/Background.liquid create mode 100644 pos-action/src/CartResolution.liquid delete mode 100644 pos-action/src/MenuItem.liquid create mode 100644 pos-action/src/ProductCheck.liquid diff --git a/pos-action/README.md.liquid b/pos-action/README.md.liquid new file mode 100644 index 00000000..88ae808c --- /dev/null +++ b/pos-action/README.md.liquid @@ -0,0 +1,28 @@ +# {{ name }} + +Temporary cart product-check example for a POS intercept bug bash. This branch overrides the existing `pos_action` template; it is not a new catalog entry and must not be merged into the default template. + +## Setup + +- Use Shopify CLI 4.8.0 or later and the pinned `@shopify/ui-extensions@2026.10.0-rc.7` dependency. The app must be approved for POS intercepts and the POS environment must be preview-enabled. Installing the CLI or SDK does not grant access. +- In `src/ProductCheck.{{ srcFileExtension }}`, replace `1234567890` with a test product's numeric **product ID**, not a variant ID or `gid://` string. Both modules import this one setting. Make the product available in the test POS environment. +- Keep the generated extension identity. `shopify.extension.toml` uses API `2026-10`, offline support, and the paired background/cart-resolution targets. Install dependencies and run `shopify app build` from your app root before testing in your authorized POS environment. + +After the branch has been published, generate this override into an existing app: + +```shell +pnpm shopify app generate extension --template pos_action --name pos-intercept-bugbash --clone-url "https://github.com/Shopify/extensions-templates#vctrchu/pos-intercept-bugbash" --path ./your-app-root +``` + +Do not pass `--flavor`; the current `pos_action` catalog entry selects Preact. This command requires the branch to exist remotely. Generation does not approve or deploy the app. + +## Test in POS + +1. Start a fresh cart and add the configured product without `example_product_checked`. The synchronous background callback returns an `ERROR` with handle `product-check-required`, targeted at that cart line's current UUID. Confirm checkout is blocked. +2. Select the cart validation banner to open resolution. Check the product and select **Product checked**. Wait for the saved confirmation. A failed save shows an error; retry it without assuming the check was saved. +3. Use Shopify-owned **Save**, **Next**, or **Done** to revalidate. The callback no longer reports the line whose `example_product_checked` property is the string `'true'`. Writing the property does not itself complete resolution or authorize checkout. Other validators still apply. +4. In a fresh cart, try another product: this interceptor must add no finding. With multiple unchecked lines of the configured product, acknowledge each line. Remove and re-add a test line without the property to require a new check. + +This is a generic staff acknowledgment **per cart line**, not per unit, age or identity verification, or evidence of regulatory compliance. Quantity changes on an acknowledged line do not require another check. No identity or customer data is collected. Cash and payment validation are outside this example. + +A local generation, build, or simulated API smoke check does not verify the real POS banner, navigation, checkout blocking, or Shopify-owned revalidation controls. Those checks require the approved app in a preview-enabled POS environment. diff --git a/pos-action/locales/en.default.json.liquid b/pos-action/locales/en.default.json.liquid index 495007f5..ad309541 100644 --- a/pos-action/locales/en.default.json.liquid +++ b/pos-action/locales/en.default.json.liquid @@ -1,5 +1,10 @@ { "name": "{{ name }}", - "welcome": "Welcome to the {% raw %}{{flavor}}{% endraw %} action extension" + "instructions": "Check each listed product, then select Product checked. Wait for the saved confirmation, then use Save, Next, or Done in POS to revalidate.", + "product": "Product", + "productChecked": "Product checked", + "saved": "Product check saved. Use the POS controls to revalidate.", + "saveFailed": "Could not save the check. Try again.", + "noChecksRequired": "No product checks remain for this example. Use the POS controls to revalidate." } diff --git a/pos-action/locales/fr.json.liquid b/pos-action/locales/fr.json.liquid index 480dd627..e9ef8a7e 100644 --- a/pos-action/locales/fr.json.liquid +++ b/pos-action/locales/fr.json.liquid @@ -1,5 +1,10 @@ { "name": "{{ name }}", - "welcome": "Bienvenue dans l'extension d'action {% raw %}{{flavor}}{% endraw %}" + "instructions": "Vérifiez chaque produit répertorié, puis sélectionnez Produit vérifié. Attendez la confirmation d’enregistrement, puis utilisez Enregistrer, Suivant ou Terminé dans PDV pour relancer la validation.", + "product": "Produit", + "productChecked": "Produit vérifié", + "saved": "Vérification du produit enregistrée. Utilisez les commandes de PDV pour relancer la validation.", + "saveFailed": "Impossible d’enregistrer la vérification. Réessayez.", + "noChecksRequired": "Il ne reste aucun produit à vérifier pour cet exemple. Utilisez les commandes de PDV pour relancer la validation." } diff --git a/pos-action/package.json.liquid b/pos-action/package.json.liquid index 82a1d2f0..61909086 100644 --- a/pos-action/package.json.liquid +++ b/pos-action/package.json.liquid @@ -3,25 +3,9 @@ "private": true, "version": "1.0.0", "license": "UNLICENSED", -{%- if flavor contains "preact" %} "dependencies": { - "@shopify/ui-extensions": "2026.7.x", + "@shopify/ui-extensions": "2026.10.0-rc.7", "preact": "^10.10.x", "@preact/signals": "^2.3.x" } -{%- elsif flavor contains "react" %} - "dependencies": { - "react": "^18.0.0", - "@shopify/ui-extensions": "2025.7.x", - "@shopify/ui-extensions-react": "2025.7.x", - "react-reconciler": "0.29.0" - }, - "devDependencies": { - "@types/react": "^18.0.0" - } -{%- else %} - "dependencies": { - "@shopify/ui-extensions": "2025.7.x" - } -{%- endif %} } diff --git a/pos-action/shopify.extension.toml.liquid b/pos-action/shopify.extension.toml.liquid index 10bdca08..535f2d01 100644 --- a/pos-action/shopify.extension.toml.liquid +++ b/pos-action/shopify.extension.toml.liquid @@ -1,25 +1,23 @@ -{%- if flavor contains "preact" -%} -api_version = "2026-07" -{% else %} -api_version = "2025-07" -{% endif %} +api_version = "2026-10" [[extensions]] type = "ui_extension" # Change the merchant-facing name of the extension in locales/en.default.json name = "t:name" {% if uid %}uid = "{{ uid }}"{% endif %} handle = "{{ handle }}" -description = "A {{ flavor }} POS UI extension" +description = "A cart product-check acknowledgment example" + +[extensions.supported_features] +runs_offline = true -# module: path to the file that contains your extension’s source code -# target: string that describes where your extension appears in POS [[extensions.targeting]] -module = "./src/Action.{{ srcFileExtension }}" -target = "pos.product-details.action.render" +module = "./src/Background.{{ srcFileExtension }}" +target = "pos.app.ready.data" + +[[extensions.targeting.capabilities.intercepts]] +event = "cartvalidations" +blocking = true -# You can omit the menu-item target and POS will provide a default menu item for this extension. -# The menu item will present the action target on click. -# This template has the same behavior as the POS-provided default, but is included for completeness. [[extensions.targeting]] -module = "./src/MenuItem.{{ srcFileExtension }}" -target = "pos.product-details.action.menu-item.render" +module = "./src/CartResolution.{{ srcFileExtension }}" +target = "pos.cart.validations.resolution.render" diff --git a/pos-action/src/Action.liquid b/pos-action/src/Action.liquid deleted file mode 100644 index 51288c23..00000000 --- a/pos-action/src/Action.liquid +++ /dev/null @@ -1,75 +0,0 @@ -{%- if flavor contains "preact" -%} -import "@shopify/ui-extensions/preact"; -import {render} from 'preact'; - -export default async () => { - render(, document.body); -}; - -function Extension() { - const {i18n} = shopify; - - return ( - - - - {i18n.translate('welcome', {flavor: '{{flavor}}'})} - - - - ); -} - -{%- elsif flavor contains "react" -%} -import { - Text, - Screen, - ScrollView, - Navigator, - reactExtension, - useApi, -} from '@shopify/ui-extensions-react/point-of-sale'; - -const Modal = () => { - {% if flavor contains "typescript" %}const api = useApi<"pos.product-details.action.render">(); - {% else %}const api = useApi(); - {% endif %} - return ( - - - - {`Product ID: ${api.product.id}`} - - - - ); -}; - -export default reactExtension('pos.product-details.action.render', () => ( - -)); -{%- else -%} -import { - Navigator, - Screen, - ScrollView, - Text, - extension, -} from '@shopify/ui-extensions/point-of-sale'; - -export default extension('pos.product-details.action.render', (root, api) => { - const navigator = root.createComponent(Navigator); - const screen = root.createComponent(Screen, { - name: 'ProductDetails', - title: 'POS action', - }); - const scrollView = root.createComponent(ScrollView); - const text = root.createComponent(Text); - - text.append(api.i18n.translate('welcome', {flavor: '{{flavor}}'})); - scrollView.append(text); - screen.append(scrollView); - navigator.append(screen); - root.append(navigator); -}); -{%- endif -%} diff --git a/pos-action/src/Background.liquid b/pos-action/src/Background.liquid new file mode 100644 index 00000000..4ffd8267 --- /dev/null +++ b/pos-action/src/Background.liquid @@ -0,0 +1,19 @@ +// @extension-target: pos.app.ready.data +import {PRODUCT_ID} from './ProductCheck.{{ srcFileExtension }}'; + +export default function extension() { + shopify.intercept('cartvalidations', ({cart}) => ({ + operations: cart.lineItems + .filter((item) => + item.productId === PRODUCT_ID && + item.properties.example_product_checked !== 'true', + ) + .map((item) => ({ + validationAdd: { + level: 'ERROR', + handle: 'product-check-required', + target: `$.cart.lineItems['${item.uuid}']`, + }, + })), + })); +} diff --git a/pos-action/src/CartResolution.liquid b/pos-action/src/CartResolution.liquid new file mode 100644 index 00000000..f370766a --- /dev/null +++ b/pos-action/src/CartResolution.liquid @@ -0,0 +1,47 @@ +import "@shopify/ui-extensions/preact"; +import {render} from 'preact'; +import {useEffect, useState} from 'preact/hooks'; +import {PRODUCT_ID} from './ProductCheck.{{ srcFileExtension }}'; + +export default async () => { + render(, document.body); +}; + +function Extension() { + const {i18n} = shopify; + const [event, setEvent] = useState(shopify.resolution.event.value); + + useEffect(() => { + const unsubscribe = shopify.resolution.event.subscribe(setEvent); + setEvent(shopify.resolution.event.value); + return unsubscribe; + }, []); + + const uncheckedItems = event.cart.lineItems.filter((item) => + item.productId === PRODUCT_ID && + item.properties.example_product_checked !== 'true', + ); + + return ( + + {i18n.translate(uncheckedItems.length ? 'instructions' : 'noChecksRequired')} + {uncheckedItems.map((item) => ( + + {item.title ?? i18n.translate('product')} + { + try { + await shopify.cart.addLineItemProperties(item.uuid, { + example_product_checked: 'true', + }); + shopify.toast.show(i18n.translate('saved')); + } catch { + shopify.toast.show(i18n.translate('saveFailed')); + } + }}> + {i18n.translate('productChecked')} + + + ))} + + ); +} diff --git a/pos-action/src/MenuItem.liquid b/pos-action/src/MenuItem.liquid deleted file mode 100644 index 467a117c..00000000 --- a/pos-action/src/MenuItem.liquid +++ /dev/null @@ -1,48 +0,0 @@ -{%- if flavor contains "preact" -%} -import "@shopify/ui-extensions/preact"; -import {render} from 'preact'; - -export default async () => { - render(, document.body); -} - -function Extension() { - return ( - shopify.action.presentModal()} /> - ); -} - -{%- elsif flavor contains "react" -%} -import { - reactExtension, - Button, - useApi, -} from '@shopify/ui-extensions-react/point-of-sale'; - -const ButtonComponent = () => { - {% if flavor contains "typescript" %}const api = useApi<"pos.product-details.action.menu-item.render">(); - {% else %}const api = useApi(); - {% endif %} - return