diff --git a/.config/shopify-cli-kit-nodejs/config.json b/.config/shopify-cli-kit-nodejs/config.json index f0a54f8..430427e 100644 --- a/.config/shopify-cli-kit-nodejs/config.json +++ b/.config/shopify-cli-kit-nodejs/config.json @@ -2,17 +2,14 @@ "cache": { "rate-limited-occurrences-send-error-to-bugsnag": { "value": [ - 1779040828657, - 1779040839929, - 1779040845225, - 1779040846291, - 1779040846470 + 1779288547675, + 1779288569233 ], - "timestamp": 1779040846470 + "timestamp": 1779288569233 }, "most-recent-occurrence-warn-on-multiple-versions": { "value": true, - "timestamp": 1779040828354 + "timestamp": 1779288547364 } } } \ No newline at end of file diff --git a/.gitignore b/.gitignore index 43a02ea..a212202 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,7 @@ extensions/*/dist - +.config # Ignore shopify files created during app dev .shopify/* .shopify.lock \ No newline at end of file diff --git a/assistant.md b/assistant.md deleted file mode 100644 index c900aaf..0000000 --- a/assistant.md +++ /dev/null @@ -1,825 +0,0 @@ -You’ve got a solid feature there — the main issue is that your setup is straddling two different eras of the admin UI extensions stack: - -Your API version 2025-07 + @shopify/ui-extensions-react/admin usage is the old React-based model. -Your tsconfig.json + shopify.d.ts are partially configured for the new Preact + global shopify model described in Upgrading to 2025-10. -This mismatch can cause confusing type errors and runtime issues. - -Below I’ll walk through: - -What’s “off” in your current setup. -Two clear paths: -A. Minimal changes to keep your current React-based block working on 2025-07. -B. Recommended long-term: upgrade to 2025-10 and migrate this block to Preact + web components. -1. What’s “off” in your current config -a) API version vs implementation style -You have: - -toml -Copy -1 -2 -3 -4 -# shopify.extension.toml -api_version = "2025-07" -... -target = "admin.order-details.block.render" -2025-07 still supports React-based UI extensions with @shopify/ui-extensions-react/admin, which is what your BlockExtension.jsx is using. - -But the 2025-10+ Preact stack: - -replaces React components (BlockStack, Button, etc.) with Polaris web components (, , etc.), -and uses the global shopify object instead of useApi(). -Your BlockExtension.jsx is still the old style React code, which is fine for 2025-07 — but your TypeScript and type declarations are pointing to the newer Preact/global-shopify world. - -b) Mixing React extensions with Preact hooks -You’re doing: - -js -Copy -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -import { - reactExtension, - BlockStack, - InlineStack, - Text, - Button, - Banner, - Divider, - Heading, - Box, - Link, - useApi, -} from '@shopify/ui-extensions-react/admin'; -import { useState, useEffect, useCallback } from 'preact/hooks'; -For React-based extensions, Shopify’s docs expect: - -reactExtension + React components, and -Hooks imported from react, not preact/hooks. -Using preact/hooks here is non-standard and can cause subtle issues unless you’re deliberately aliasing React to Preact at build time (which the docs don’t assume). - -c) tsconfig.json and shopify.d.ts -Your tsconfig.json: - -json -Copy -1 -2 -3 -4 -5 -6 -7 -{ - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact", - ... - } -} -jsx: "react-jsx" with jsxImportSource: "preact" tells TypeScript/Babel to compile JSX as Preact, not React. -But React-based UI extensions are built assuming React as the renderer for @shopify/ui-extensions-react/admin. -Your shopify.d.ts: - -ts -Copy -1 -2 -3 -4 -5 -6 -7 -import '@shopify/ui-extensions'; - -//@ts-ignore -declare module './src/BlockExtension.jsx' { - const shopify: import('@shopify/ui-extensions/admin.order-details.block.render').Api; - const globalThis: { shopify: typeof shopify }; -} -This is the new 2025-10 pattern, where shopify is a global API object injected into the extension module. However: - -In React 2025-07, you are not supposed to read from shopify directly. -Instead, you use useApi() from @shopify/ui-extensions-react/admin. -So right now your types say “this module has a shopify global API”, but your code is actually using useApi(). That’s just confusing for tooling. - -2. Path A – Minimal changes to keep your existing React block on 2025-07 -If your goal is “make this work now, minimal refactor”, do this: - -A1. Use React hooks, not Preact hooks -Change this: - -js -Copy -1 -import { useState, useEffect, useCallback } from 'preact/hooks'; -To this: - -js -Copy -1 -import React, {useState, useEffect, useCallback} from 'react'; -And ensure you have these dependencies in package.json (versions per Upgrading to 2025-10, but locked to 2025.4 or 2025.7): - -json -Copy -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -{ - "dependencies": { - "react": "^18.0.0", - "@shopify/ui-extensions": "2025.4.x", // or 2025.7.x - "@shopify/ui-extensions-react": "2025.4.x", // or 2025.7.x - "react-reconciler": "0.29.0" - }, - "devDependencies": { - "@types/react": "^18.0.0" - } -} -A2. Simplify tsconfig.json for React -For a React-based extension, a simple tsconfig.json in the extension directory is enough: - -json -Copy -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -{ - "compilerOptions": { - "jsx": "react-jsx", - "target": "ES2020", - "checkJs": true, - "allowJs": true, - "moduleResolution": "node", - "esModuleInterop": true, - "noEmit": true, - "skipLibCheck": true - } -} -Remove jsxImportSource: "preact" — you’re not compiling JSX as Preact here. -A3. Remove the shopify.d.ts override for this module -For 2025-07 + React, you don’t use the global shopify object; useApi() is the right way. - -You can delete or comment out this shopify.d.ts: - -ts -Copy -1 -2 -3 -4 -5 -6 -7 -import '@shopify/ui-extensions'; - -//@ts-ignore -declare module './src/BlockExtension.jsx' { - const shopify: import('@shopify/ui-extensions/admin.order-details.block.render').Api; - const globalThis: { shopify: typeof shopify }; -} -Instead, type safety comes from @shopify/ui-extensions-react/admin and its hooks. - -A4. Keep your extension code as-is (logic is fine) -Once A1–A3 are fixed, the rest of your logic is conceptually solid: - -useApi() to get api.data.selected?.[0]?.id for the order ID. -api.query(...) to read metafields (hutko/fiscal_receipts, fiscal_info_*, fiscal_storage_*). -Local helpers openBase64AsBlob and downloadBase64AsFile. -Rendering a nice stack of receipts. -You’ll then be running a legacy React-based admin order block, which is supported on 2025-07. - -3. Path B – Recommended long-term: upgrade to 2025-10 + Preact + web components -If you’re willing to refactor now, you’ll be aligned with Shopify’s forward direction: - -Upgrading to 2025-10 -React-based UI components are deprecated after 2025-07. -Preact + Polaris web components (, , , , etc.) are the new model. -At a high level, you would: - -B1. Update shopify.extension.toml -toml -Copy -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -api_version = "2025-10" - -[[extensions]] -name = "t:name" -handle = "fiscal-order-block" -type = "ui_extension" -uid = "dfc94970-d01c-b839-2220-ffd1b6ab1c86ac3cce4a" - -[[extensions.targeting]] -module = "./src/BlockExtension.jsx" -target = "admin.order-details.block.render" - -[extensions.capabilities] -network_access = true -B2. Update dependencies to Preact + @shopify/ui-extensions (no React) -From the docs: - -json -Copy -1 -2 -3 -4 -5 -6 -7 -{ - "dependencies": { - "preact": "^10.10.x", - "@preact/signals": "^2.3.x", - "@shopify/ui-extensions": "2025.10.x" - } -} -B3. Keep your current tsconfig.json (it’s already Preact-ready) -Your current tsconfig.json is already aligned with the Preact model from the upgrade guide: - -json -Copy -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -{ - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact", - "target": "ES2020", - "checkJs": true, - "allowJs": true, - "moduleResolution": "node", - "esModuleInterop": true, - "noEmit": true, - "skipLibCheck": true - } -} -That’s exactly what Shopify suggests for Preact-based extensions. - -B4. Rewrite BlockExtension.jsx to: -Use import '@shopify/ui-extensions/preact'; and render from preact. -Use Polaris web components instead of React components: -BlockStack → -InlineStack → -Box → -Banner → -Button → -Link → -Divider → -etc. -Use the global shopify object instead of useApi(): -const orderId = shopify.data.selected?.[0]?.id; -For GraphQL calls, use the network feature: -fetch('shopify:admin/api/graphql.json', { ... }). -You’d also then use shopify.d.ts to type the global shopify object, but in the new style. - -If you want to go down this path, I can take your existing logic (metafield queries, base64 helpers, receipts rendering) and rewrite the whole block into a validated Preact + web-components version for admin.order-details.block.render. - -What I recommend right now -Given your current config: - -Quick win / minimal change: -Follow Path A: -Switch your hooks from preact/hooks to react. -Adjust tsconfig.json to be React-oriented. -Remove the shopify.d.ts global override. -Ensure you have React and @shopify/ui-extensions-react/admin in your package.json. -That should get your existing block working reliably on 2025-07. - -Then, when you’re ready, we can: - -Upgrade to 2025-10, and -Migrate this specific block to Preact + web components, keeping exactly the same UX. -If you tell me which path you want (A: keep React on 2025-07, or B: migrate now to Preact/2025-10), I’ll either: - -Give you the exact diffs to make your current React block correct, or -Provide a full Preact + implementation of your fiscal receipts block for admin.order-details.block.render and explain the changes line-by-line. - - - - -For this fiscal-checkout-block (actually a customer account order status block), you’re very close conceptually, but the implementation is still in the old React-based style, and your shopify.extension.toml is pinned to 2025-07, not 2025-10. - -For a “pure Preact build” aligned with the latest Customer account UI extensions and Polaris web components, you should: - -Upgrade your extension to use Preact + web components + global shopify APIs, and -Bump your API version in shopify.extension.toml. -Below I’ll walk through: - -What’s wrong / mismatched in your current fiscal-checkout-block. -A validated Preact + web components rewrite of OrderStatusExtension.jsx for customer-account.order-status.block.render. -The shopify.extension.toml change you’ll want for a 2025‑10 style build. -Notes on your GraphQL + metafield access. -1. Issues in your current fiscal-checkout-block -Relevant docs to keep in mind: - -Customer account UI extensions – Overview & APIs -Customer account UI extensions – Polaris web components -Customer account UI extensions – Configuration -Customer account UI extensions – Targets -a) API version in shopify.extension.toml -You have: - -toml -Copy -1 -2 -3 -api_version = "2025-07" -... -target = "customer-account.order-status.block.render" -That’s a Shopify global API version, but for customer-account UI extensions you want to track the latest customer-account UI extensions API (2025-10 and beyond). -Since your goal is “2025-10 pure preact build”, you will want to bump this to at least 2025-10 when you migrate. -(I’ll show what to change after the code.) - -b) Using React wrappers + Preact hooks instead of the Preact/web‑components model -Your current code: - -js -Copy -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -import { - reactExtension, - BlockStack, - InlineStack, - Text, - Button, - Banner, - Divider, - Heading, - View, - Link, - Spinner, - useApi, -} from '@shopify/ui-extensions-react/customer-account'; -import { useState, useEffect, useCallback } from 'preact/hooks'; -Problems: - -reactExtension + React components (BlockStack, Button, etc.) are part of the React-based API, not the web components model. -You then mix in Preact hooks instead of React hooks, which is not what the React wrappers expect. -For the 2025-10+ Polaris UI Framework, customer account extensions are intended to use: -import '@shopify/ui-extensions/preact'; -render from preact -Polaris web components: , , , , , , etc. -The global shopify object for contextual APIs (like shopify.order). -c) useApi() vs global shopify signals -You currently do: - -js -Copy -1 -2 -3 -4 -5 -function Extension() { - const { order } = useApi(); - const orderId = order?.id?.value || order?.id; - ... -} -In the Preact + web components model for customer account UI extensions (APIs reference): - -Contextual data like the current order is exposed as signals on shopify, e.g. shopify.order. -APIs with a .value property are signals: you access shopify.order.value inside your component. -So in the Preact model you’ll want something like: - -js -Copy -1 -2 -const order = shopify.order.value; -const orderId = order?.id; -2. Migrated Preact + web components OrderStatusExtension.jsx (validated) -Below is a complete Preact rewrite of your OrderStatusExtension.jsx for the target customer-account.order-status.block.render, using: - -@shopify/ui-extensions/preact -Preact hooks -Global shopify.order -Polaris web components: -, , , , , , , -This code has been validated against the polaris-customer-account-extensions API for the target customer-account.order-status.block.render. - -tsx -Copy -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -import '@shopify/ui-extensions/preact'; - {receipts.map((receipt) => ( - - - {/* Receipt summary */} - - {receipt.info.display_datetime ? ( - <> - - {receipt.info.display_datetime} - - {receipt.info.amount && ( - {receipt.info.amount} грн - )} - {receipt.info.fiscal_number && ( - - ФН: {receipt.info.fiscal_number} - - )} - - ) : ( - - Receipt {receipt.id} — waiting for fiscalisation - - )} - - - - - {/* Action buttons */} - - {receipt.storage.html && ( - - openBase64AsBlob(receipt.storage.html, 'text/html') - } - > - View Receipt - - )} - {receipt.storage.xml && ( - - downloadBase64AsFile( - receipt.storage.xml, - `fiscal_receipt_${receipt.id}.xml`, - 'application/xml', - ) - } - > - Download XML - - )} - - - {/* External links */} - {receipt.info.links && ( - - {receipt.info.links.tax_service && ( - - Tax Service - - )} - {receipt.info.links.external_provider && ( - - Provider - - )} - {receipt.info.links.pdf && ( - - PDF - - )} - - )} - - - ))} - - ); -} -Key differences vs your original: - -Entry point: -Old: export default reactExtension(..., () => ); -New: export default function extension() { render(, document.body); } -Context API: -Old: const { order } = useApi(); -New: const order = shopify.order.value; -UI layer: -Old: React components (BlockStack, View, Button, Link, etc.). -New: Polaris web components: , , , , , etc. -State / hooks: -Still useState, useEffect, useCallback — now correctly from preact/hooks. -3. Updating shopify.extension.toml for a 2025‑10 Preact build -Currently: - -toml -Copy -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -api_version = "2025-07" -uid = "ca81c72e-43fb-0709-a9a3-373f38c511faf3b68f36" - -[[extensions]] -name = "Fiscal Receipt Info" -handle = "fiscal-checkout-block" -type = "ui_extension" - -[[extensions.targeting]] -module = "./src/OrderStatusExtension.jsx" -target = "customer-account.order-status.block.render" - -[extensions.capabilities] -network_access = true - -[[extensions.metafields]] -namespace = "hutko" -key = "fiscal_receipts" -For a Preact + web components build aligned with the latest customer account UI extensions API, update to: - -toml -Copy -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -api_version = "2025-10" -uid = "ca81c72e-43fb-0709-a9a3-373f38c511faf3b68f36" - -[[extensions]] -name = "Fiscal Receipt Info" -handle = "fiscal-checkout-block" -type = "ui_extension" - -[[extensions.targeting]] -module = "./src/OrderStatusExtension.jsx" -target = "customer-account.order-status.block.render" - -[extensions.capabilities] -network_access = true - -[[extensions.metafields]] -namespace = "hutko" -key = "fiscal_receipts" -Keep the same target (customer-account.order-status.block.render), which is one of the documented customer account targets. -Just bump api_version and ensure your package.json has the Preact dependencies: -json -Copy -1 -2 -3 -4 -5 -6 -7 - { - "dependencies": { - "preact": "^10.10.x", - "@preact/signals": "^2.3.x", - "@shopify/ui-extensions": "2025.10.x" - } - } -4. Notes on your GraphQL and metafield usage -Your original logic: - -Index metafield: -Operation -Open in GraphiQL -Copy -1 -2 -3 -4 -5 - query getOrderMetafield($id: ID!) { - order(id: $id) { - metafield(namespace: "hutko", key: "fiscal_receipts") { value } - } - } -Per-receipt metafields: -Operation -Open in GraphiQL -Copy -1 -2 -3 -4 -5 -6 - query getReceiptData($orderId: ID!, $infoKey: String!, $storageKey: String!) { - order(id: $orderId) { - info: metafield(namespace: "hutko", key: $infoKey) { value } - storage: metafield(namespace: "hutko", key: $storageKey) { value } - } - } -and then JSON parse on each value. - -This pattern is totally fine to keep in the Preact version — the only changes I made are: - -Bumping API_VERSION from '2025-07' to '2025-10'. -Using shopify.order.value rather than useApi() to get orderId. -If your metafield schema is already live in production, this Preact migration should not require any back‑end changes. - -What to do next -Replace your OrderStatusExtension.jsx with the Preact/web-components version above. -Update shopify.extension.toml to api_version = "2025-10". -Ensure package.json for this extension has: -preact, @preact/signals, @shopify/ui-extensions (and no @shopify/ui-extensions-react/...). -Run: -bash -Copy -1 - shopify app dev -And test on the Order status page under the customer-account.order-status.block.render target. - -If you’d like, you can paste your package.json for this extension and I’ll align it exactly with the 2025‑10 Preact + customer-account UI extensions stack, and we can similarly migrate your admin.order-details.block.render block so both sides (admin + customer account) use the same Preact/web-components approach. - -Sources -API: Customer Account Ui Extensions -Customer account UI extensions -API: Customer Account Ui Extensions > Polaris Web Components -Polaris web components -API: Customer Account Ui Extensions > Configuration -Customer account UI extensions – Configuration \ No newline at end of file diff --git a/extensions/fiscal-checkout-block/locales/en.default.json b/extensions/fiscal-checkout-block/locales/en.default.json index 9575e27..ea04559 100644 --- a/extensions/fiscal-checkout-block/locales/en.default.json +++ b/extensions/fiscal-checkout-block/locales/en.default.json @@ -1,18 +1,27 @@ { "name": "Fiscal Order Status Block", "hutko": { + "fiscal_receipts_title": "hutko Fiscal Receipts", "fiscal_receipts": "Fiscal Receipts", - "loading": "Loading fiscal receipts...", - "no_receipts": "No fiscal receipts found.", + "refresh": "Refresh", + "loading": "Loading...", + "no_receipts": "No fiscal receipts found for this order.", "view_receipt": "View Receipt", "download_xml": "Download XML", "tax_service": "Tax Service", - "provider": "Provider", + "provider": "Open Receipt", + "copy_link": "Copy link", "pdf": "PDF", "waiting_for_fiscalisation": "Receipt {{id}} — waiting for fiscalisation", - "failed_to_load_receipts": "Failed to load fiscal receipts. Please try again.", + "no_order_selected": "No order selected.", + "failed_to_load_receipts": "Failed to load fiscal receipts: ", "currency": "UAH", - "refresh": "Refresh", - "fiscal_number_label": "FN: " + "customer_actions": "Customer actions", + "done": "Done", + "processing": "Processing", + "ready": "Ready", + "transaction_type_purchase": "Purchase", + "transaction_type_reverse": "Refund", + "fn": "FN" } -} +} \ No newline at end of file diff --git a/extensions/fiscal-checkout-block/locales/uk.json b/extensions/fiscal-checkout-block/locales/uk.json index 309513f..71aaaa9 100644 --- a/extensions/fiscal-checkout-block/locales/uk.json +++ b/extensions/fiscal-checkout-block/locales/uk.json @@ -1,18 +1,27 @@ { "name": "Блок статусу фіскального замовлення", "hutko": { + "fiscal_receipts_title": "Фіскальні чеки hutko", "fiscal_receipts": "Фіскальні чеки", - "loading": "Завантаження фіскальних чеків...", - "no_receipts": "Фіскальних чеків не знайдено.", + "refresh": "Оновити", + "loading": "Завантаження...", + "no_receipts": "Для цього замовлення не знайдено фіскальних чеків.", "view_receipt": "Переглянути чек", "download_xml": "Завантажити XML", "tax_service": "Податкова служба", - "provider": "Провайдер", + "provider": "Відкрити чек", + "copy_link": "Скопіювати посилання", "pdf": "PDF", "waiting_for_fiscalisation": "Чек {{id}} — очікує фіскалізації", - "failed_to_load_receipts": "Не вдалося завантажити фіскальні чеки. Будь ласка, спробуйте ще раз.", + "no_order_selected": "Замовлення не вибрано.", + "failed_to_load_receipts": "Не вдалося завантажити фіскальні чеки: ", "currency": "грн", - "refresh": "Оновити", - "fiscal_number_label": "ФН: " + "customer_actions": "Дії клієнта", + "done": "Готово", + "processing": "Обробляється", + "ready": "Готово", + "transaction_type_purchase": "Продаж", + "transaction_type_reverse": "Повернення", + "fn": "ФН" } -} +} \ No newline at end of file diff --git a/extensions/fiscal-checkout-block/shopify.extension.toml b/extensions/fiscal-checkout-block/shopify.extension.toml index 6e5b03b..6f28e11 100644 --- a/extensions/fiscal-checkout-block/shopify.extension.toml +++ b/extensions/fiscal-checkout-block/shopify.extension.toml @@ -13,5 +13,5 @@ target = "customer-account.order-status.block.render" network_access = true [[extensions.metafields]] -namespace = "hutko" +namespace = "hutko-fiscal" key = "fiscal_receipts" diff --git a/extensions/fiscal-checkout-block/src/OrderStatusExtension copy.jsx b/extensions/fiscal-checkout-block/src/OrderStatusExtension copy.jsx new file mode 100644 index 0000000..83d0e8e --- /dev/null +++ b/extensions/fiscal-checkout-block/src/OrderStatusExtension copy.jsx @@ -0,0 +1,330 @@ +import '@shopify/ui-extensions/customer-account'; +import { render } from 'preact'; +import { useState, useEffect, useCallback } from 'preact/hooks'; + + + +async function queryCustomerAccountAPI(query, variables = {}) { + console.log('[HutkoFiscalDebug] queryCustomerAccountAPI call:', { query, variables }); + try { + const res = await fetch( + 'shopify://customer-account/api/2025-10/graphql.json', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query, variables }), + } + ); + console.log('[HutkoFiscalDebug] queryCustomerAccountAPI response status:', res.status, res.statusText); + if (!res.ok) { + throw new Error(`API request failed: ${res.status} ${res.statusText}`); + } + const data = await res.json(); + console.log('[HutkoFiscalDebug] queryCustomerAccountAPI response data:', data); + return data; + } catch (error) { + console.error('[HutkoFiscalDebug] queryCustomerAccountAPI exception:', error); + throw error; + } +} + +export default function extension() { + render(, document.body); +} + +const MOCK_RECEIPTS = [ + { + id: 9871, + info: { + payment_id: "rNrWsih9Uj06ivn87af9HWiuj", + amount: 2.0, + currency: "UAH", + order_name: "#1082", + hutko_order_id: 8961, + order_time: "2026-05-08 12:40:41.737232", + hutko_merchant_id: 1000142, + order_status: "reversed", + card_number: "535528*9003", + email: "v.paliienko@hutko.org", + fiscal_status: "done", + fiscal_provider_name: "ereceipt", + transaction_type: "purchase", + fiscal_date: "21052026", + fiscal_time: "182702", + fiscal_number: 4001317034, + fiscal_receipt_number: "6914923030", + display_datetime: "21.05.2026 18:27:02", + links: { + external_provider: "https://cabinet.tax.gov.ua/cashregs/check?id=6914923030&fn=4001317034&date=20260521&time=18.27&sm=2.0" + } + } + }, + { + id: 9873, + info: { + payment_id: "rNrWsih9Uj06ivn87af9HWiuj", + amount: 2.0, + currency: "UAH", + order_name: "#1082", + hutko_order_id: 8961, + order_time: "2026-05-08 12:40:41.737232", + hutko_merchant_id: 1000142, + order_status: "reversed", + card_number: "535528*9003", + email: "v.paliienko@hutko.org", + fiscal_status: "done", + fiscal_provider_name: "ereceipt", + transaction_type: "reverse", + fiscal_date: "21052026", + fiscal_time: "182823", + fiscal_number: 4001317034, + fiscal_receipt_number: "6914937076", + display_datetime: "21.05.2026 18:28:23", + links: { + external_provider: "https://cabinet.tax.gov.ua/cashregs/check?id=6914937076&fn=4001317034&date=20260521&time=18.28&sm=2.0" + } + } + } +]; + +function Extension() { + const translate = shopify.i18n.translate; + // Get order ID from the order status context signal + const orderId = shopify.order?.value?.id; + + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [receipts, setReceipts] = useState([]); + const [pollIndex, setPollIndex] = useState(0); + + const fetchFiscalData = useCallback(async (isManual = false) => { + console.log('[HutkoFiscalDebug] fetchFiscalData triggered. isManual:', isManual, 'orderId:', orderId); + if (isManual) { + setPollIndex(0); + } + + if (!orderId) { + console.warn('[HutkoFiscalDebug] fetchFiscalData called but orderId is missing/undefined. Loading stopped.'); + setLoading(false); + return; + } + + setLoading(true); + setError(null); + try { + // Step 1: Read fiscal_receipts from pre-loaded appMetafields (declared in TOML) + let receiptIds = []; + const metafields = shopify.appMetafields?.value; + console.log('[HutkoFiscalDebug] shopify.appMetafields?.value:', metafields); + + if (metafields && metafields.length > 0) { + const entry = metafields.find( + (e) => + e.metafield?.namespace === 'hutko-fiscal' && + e.metafield?.key === 'fiscal_receipts', + ); + console.log('[HutkoFiscalDebug] Found metafield entry for hutko-fiscal.fiscal_receipts:', entry); + if (entry?.metafield?.value && typeof entry.metafield.value === 'string') { + try { + receiptIds = JSON.parse(entry.metafield.value); + console.log('[HutkoFiscalDebug] Parsed receiptIds from appMetafields:', receiptIds); + } catch (e) { + console.error('[HutkoFiscalDebug] Error parsing fiscal_receipts from appMetafields:', e, 'Raw value:', entry.metafield.value); + } + } + } + + // Fallback: query via Customer Account API if appMetafields didn't have it + if (receiptIds.length === 0) { + console.log('[HutkoFiscalDebug] receiptIds is empty. Querying Customer Account API fallback...'); + const indexRes = await queryCustomerAccountAPI( + `query getOrderMetafield($id: ID!) { + order(id: $id) { + metafield(namespace: "hutko-fiscal", key: "fiscal_receipts") { value } + } + }`, + { id: orderId }, + ); + + console.log('[HutkoFiscalDebug] Fallback query indexRes:', indexRes); + + const rawValue = indexRes?.data?.order?.metafield?.value; + console.log('[HutkoFiscalDebug] Fallback query raw value:', rawValue); + try { + receiptIds = JSON.parse(rawValue || '[]'); + console.log('[HutkoFiscalDebug] Parsed receiptIds from fallback query:', receiptIds); + } catch (e) { + console.error('[HutkoFiscalDebug] Error parsing fallback query fiscal_receipts JSON:', e); + } + } + + if (receiptIds.length === 0) { + console.warn('[HutkoFiscalDebug] No receipt IDs found after appMetafields and fallback query. Setting empty receipts.'); + setReceipts([]); + setLoading(false); + return; + } + + // Step 2: Fetch per-receipt info via Customer Account API + console.log('[HutkoFiscalDebug] Fetching detailed info for receipt IDs:', receiptIds); + const detailed = await Promise.all( + receiptIds.map(async (id) => { + console.log(`[HutkoFiscalDebug] Fetching data for receipt ID: ${id}`); + const res = await queryCustomerAccountAPI( + `query getReceiptData($orderId: ID!, $infoKey: String!) { + order(id: $orderId) { + info: metafield(namespace: "hutko-fiscal", key: $infoKey) { value } + + } + }`, + { + orderId, + infoKey: `fiscal_info_${id}`, + + }, + ); + + console.log(`[HutkoFiscalDebug] Detailed query response for ID ${id}:`, res); + + + let info = {}; + try { + info = JSON.parse(res?.data?.order?.info?.value || '{}'); + } catch (e) { } + + + return { id, info }; + }) + ); + + console.log('[HutkoFiscalDebug] All detailed receipt data resolved successfully:', detailed); + setReceipts(detailed); + } catch (e) { + console.error('[HutkoFiscalDebug] Critical error in fetchFiscalData:', e); + setError(shopify.i18n.translate('hutko.failed_to_load_receipts')); + } finally { + setLoading(false); + } + }, [orderId]); + + useEffect(() => { + fetchFiscalData(); + }, [fetchFiscalData]); + + useEffect(() => { + const isWaiting = receipts.some((receipt) => receipt.info.fiscal_status !== 'done'); + console.log('[HutkoFiscalDebug] Polling check. receipts count:', receipts.length, 'isWaiting:', isWaiting, 'pollIndex:', pollIndex); + if (!isWaiting) { + if (pollIndex !== 0) { + console.log('[HutkoFiscalDebug] No receipts are waiting. Resetting pollIndex to 0.'); + setPollIndex(0); + } + return; + } + + const delays = [5000, 15000, 20000]; + if (pollIndex < delays.length) { + console.log(`[HutkoFiscalDebug] Setting poll timer. Delay: ${delays[pollIndex]}ms. Current pollIndex: ${pollIndex}`); + const timer = setTimeout(() => { + console.log(`[HutkoFiscalDebug] Poll timer fired. Advancing pollIndex to ${pollIndex + 1} and fetching data.`); + setPollIndex((prev) => prev + 1); + fetchFiscalData(); + }, delays[pollIndex]); + + return () => clearTimeout(timer); + } else { + console.log('[HutkoFiscalDebug] pollIndex reached max delay threshold. Stopping auto-poll.'); + } + }, [fetchFiscalData, receipts, pollIndex]); + + if (loading) { + return ( + + + + {translate('hutko.loading')} + + + ); + } + + if (error) { + return {error}; + } + + // For testing purposes, if no receipts are found, use mock receipts + const displayReceipts = receipts.length === 2 ? MOCK_RECEIPTS : receipts; + + if (displayReceipts.length === 0) { + return null; + } + const logoSrc = "data:image/svg+xml,%3Csvg width='120' height='30' viewBox='188 423 624 154' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M747.38 473.832C762.386 473.832 774.926 478.66 784.944 488.305C794.962 497.957 800 509.817 800 523.877C800 537.938 794.962 549.594 784.944 559.246C774.876 568.897 762.386 573.72 747.38 573.72C732.374 573.72 719.898 568.947 709.923 559.344C699.955 549.742 694.967 537.938 694.967 523.871C694.967 509.803 700.005 498.104 710.023 488.403C720.095 478.713 732.504 473.835 747.38 473.824V473.832ZM474.833 530.079C474.833 535.826 476.46 540.445 479.713 543.992C482.966 547.531 487.373 549.332 492.984 549.333C499.491 549.333 504.687 547.433 508.621 543.634C512.555 539.835 514.547 534.648 514.547 528.032V475.729H540.883V571.51L540.832 571.461H514.705V561.963C507.467 569.407 497.972 573.15 486.27 573.15C474.568 573.15 465.653 569.61 458.781 562.524C451.909 555.438 448.498 546.102 448.498 534.55V475.721H474.833V530.079ZM266.725 535.497L322.589 501.641L256.912 571.921L255.929 543.519L200.008 571.921H200L269.09 426.28L266.725 535.497ZM375.332 485.276C382.57 477.832 392.065 474.089 403.768 474.089C415.47 474.089 424.384 477.629 431.256 484.715C438.128 491.802 441.539 501.138 441.539 512.689V571.517H415.204V517.161C415.204 511.413 413.577 506.794 410.324 503.248C407.071 499.708 402.664 497.907 397.053 497.907C390.546 497.907 385.35 499.806 381.416 503.605C377.482 507.404 375.49 512.591 375.49 519.208V571.51H349.154V475.729L349.205 452.648H375.332V485.276ZM588.437 475.729H611.991V498.88H588.443V537.326C588.443 541.531 589.44 544.616 591.482 546.459C593.532 548.31 596.836 549.284 601.407 549.284H611.991V571.51H599.508C574.434 571.51 561.943 560.576 561.943 538.658V498.929H547.833V475.729H561.943V452.647H588.437V475.729ZM645.649 518.639H645.707L678.126 475.729H708.238L670.674 523.315L709.75 571.461H676.75L645.649 532.09V571.461H618.941V475.729H645.649V518.639ZM747.38 497.389C739.877 497.389 733.635 499.905 728.598 504.938C723.567 509.971 721.044 516.279 721.044 523.877C721.044 531.475 723.517 537.994 728.39 543.027C733.27 548.059 739.404 550.569 746.807 550.569C754.209 550.569 760.666 548.003 765.904 542.921C771.1 537.84 773.722 531.475 773.722 523.877C773.722 516.279 771.207 509.964 766.169 504.938H766.162C761.132 499.905 754.883 497.389 747.38 497.389Z' fill='%23E40B2D'/%3E%3C/svg%3E"; + + return ( + + + hutko {translate('hutko.fiscal_receipts')} + + {displayReceipts.map((receipt) => ( + + + + + {(receipt.info.fiscal_status === 'done') || receipt.info.display_datetime ? ( + <> + {receipt.info.display_datetime && ( + {receipt.info.display_datetime} + )} + {receipt.info.amount && ( + {receipt.info.amount} {receipt.info.currency || translate('hutko.currency')} + )} + {receipt.info.fiscal_receipt_number && ( + #{receipt.info.fiscal_receipt_number} + )} + {receipt.info.fiscal_number && !receipt.info.fiscal_receipt_number && ( + {translate('hutko.fiscal_number_label')}{receipt.info.fiscal_number} + )} + + ) : ( + {translate('hutko.waiting_for_fiscalisation', { id: receipt.id })} + )} + + + + + {receipt.info.links && ( + + {receipt.info.links.external_provider && ( + + {translate('hutko.provider')} + + )} + + + )} + + + ))} + + + + { fetchFiscalData(true); }} + disabled={loading} + > + {translate('hutko.refresh')} + + + + + ); +} + diff --git a/extensions/fiscal-checkout-block/src/OrderStatusExtension.jsx b/extensions/fiscal-checkout-block/src/OrderStatusExtension.jsx index 7f2483f..b7c2479 100644 --- a/extensions/fiscal-checkout-block/src/OrderStatusExtension.jsx +++ b/extensions/fiscal-checkout-block/src/OrderStatusExtension.jsx @@ -2,46 +2,7 @@ import '@shopify/ui-extensions/customer-account'; import { render } from 'preact'; import { useState, useEffect, useCallback } from 'preact/hooks'; -function openBase64AsBlob(base64String, mimeType) { - try { - const decoded = atob(base64String); - const bytes = new Uint8Array(decoded.length); - for (let i = 0; i < decoded.length; i++) { - bytes[i] = decoded.charCodeAt(i); - } - const blob = new Blob([bytes], { type: mimeType }); - const url = URL.createObjectURL(blob); - const win = window.open(url, '_blank'); - if (win) { - setTimeout(() => URL.revokeObjectURL(url), 10000); - } - return !!win; - } catch (e) { - console.error('Failed to open blob:', e); - return false; - } -} -function downloadBase64AsFile(base64String, filename, mimeType) { - try { - const decoded = atob(base64String); - const bytes = new Uint8Array(decoded.length); - for (let i = 0; i < decoded.length; i++) { - bytes[i] = decoded.charCodeAt(i); - } - const blob = new Blob([bytes], { type: mimeType }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - setTimeout(() => URL.revokeObjectURL(url), 5000); - } catch (e) { - console.error('Failed to download file:', e); - } -} async function queryCustomerAccountAPI(query, variables = {}) { const res = await fetch( @@ -52,14 +13,19 @@ async function queryCustomerAccountAPI(query, variables = {}) { body: JSON.stringify({ query, variables }), } ); - if (!res.ok) throw new Error(`API request failed: ${res.status}`); - return res.json(); + if (!res.ok) { + throw new Error(`API request failed: ${res.status} ${res.statusText}`); + } + const data = await res.json(); + return data; } export default function extension() { render(, document.body); } + + function Extension() { const translate = shopify.i18n.translate; // Get order ID from the order status context signal @@ -68,8 +34,13 @@ function Extension() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [receipts, setReceipts] = useState([]); + const [pollIndex, setPollIndex] = useState(0); + + const fetchFiscalData = useCallback(async (isManual = false) => { + if (isManual) { + setPollIndex(0); + } - const fetchFiscalData = useCallback(async () => { if (!orderId) { setLoading(false); return; @@ -81,11 +52,11 @@ function Extension() { // Step 1: Read fiscal_receipts from pre-loaded appMetafields (declared in TOML) let receiptIds = []; const metafields = shopify.appMetafields?.value; + if (metafields && metafields.length > 0) { const entry = metafields.find( (e) => - String(e.target?.type) === 'order' && - e.metafield?.namespace === 'hutko' && + e.metafield?.namespace === 'hutko-fiscal' && e.metafield?.key === 'fiscal_receipts', ); if (entry?.metafield?.value && typeof entry.metafield.value === 'string') { @@ -100,16 +71,15 @@ function Extension() { const indexRes = await queryCustomerAccountAPI( `query getOrderMetafield($id: ID!) { order(id: $id) { - metafield(namespace: "hutko", key: "fiscal_receipts") { value } + metafield(namespace: "hutko-fiscal", key: "fiscal_receipts") { value } } }`, { id: orderId }, ); + const rawValue = indexRes?.data?.order?.metafield?.value; try { - receiptIds = JSON.parse( - indexRes?.data?.order?.metafield?.value || '[]' - ); + receiptIds = JSON.parse(rawValue || '[]'); } catch (e) { } } @@ -119,20 +89,19 @@ function Extension() { return; } - // Step 2: Fetch per-receipt info & storage via Customer Account API + // Step 2: Fetch per-receipt info via Customer Account API const detailed = await Promise.all( receiptIds.map(async (id) => { const res = await queryCustomerAccountAPI( - `query getReceiptData($orderId: ID!, $infoKey: String!, $storageKey: String!) { + `query getReceiptData($orderId: ID!, $infoKey: String!) { order(id: $orderId) { - info: metafield(namespace: "hutko", key: $infoKey) { value } - storage: metafield(namespace: "hutko", key: $storageKey) { value } + info: metafield(namespace: "hutko-fiscal", key: $infoKey) { value } + } }`, { orderId, infoKey: `fiscal_info_${id}`, - storageKey: `fiscal_storage_${id}`, }, ); @@ -141,38 +110,42 @@ function Extension() { info = JSON.parse(res?.data?.order?.info?.value || '{}'); } catch (e) { } - let storage = {}; - try { - storage = JSON.parse(res?.data?.order?.storage?.value || '{}'); - } catch (e) { } - - return { id, info, storage }; + return { id, info }; }) ); setReceipts(detailed); } catch (e) { - console.error('Failed to fetch fiscal data:', e); - setError(translate('hutko.failed_to_load_receipts')); + setError(shopify.i18n.translate('hutko.failed_to_load_receipts')); } finally { setLoading(false); } - }, [orderId, translate]); + }, [orderId]); useEffect(() => { fetchFiscalData(); - - const t1 = setTimeout(fetchFiscalData, 5000); - const t2 = setTimeout(fetchFiscalData, 20000); - const t3 = setTimeout(fetchFiscalData, 40000); - - return () => { - clearTimeout(t1); - clearTimeout(t2); - clearTimeout(t3); - }; }, [fetchFiscalData]); + useEffect(() => { + const isWaiting = receipts.some((receipt) => receipt.info.fiscal_status !== 'done'); + if (!isWaiting) { + if (pollIndex !== 0) { + setPollIndex(0); + } + return; + } + + const delays = [5000, 15000, 20000]; + if (pollIndex < delays.length) { + const timer = setTimeout(() => { + setPollIndex((prev) => prev + 1); + fetchFiscalData(); + }, delays[pollIndex]); + + return () => clearTimeout(timer); + } + }, [fetchFiscalData, receipts, pollIndex]); + if (loading) { return ( @@ -188,7 +161,10 @@ function Extension() { return {error}; } - if (receipts.length === 0) { + // For testing purposes, append MOCK_RECEIPTS to receipts + const displayReceipts = [...receipts]; + + if (displayReceipts.length === 0) { return null; } const logoSrc = "data:image/svg+xml,%3Csvg width='120' height='30' viewBox='188 423 624 154' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M747.38 473.832C762.386 473.832 774.926 478.66 784.944 488.305C794.962 497.957 800 509.817 800 523.877C800 537.938 794.962 549.594 784.944 559.246C774.876 568.897 762.386 573.72 747.38 573.72C732.374 573.72 719.898 568.947 709.923 559.344C699.955 549.742 694.967 537.938 694.967 523.871C694.967 509.803 700.005 498.104 710.023 488.403C720.095 478.713 732.504 473.835 747.38 473.824V473.832ZM474.833 530.079C474.833 535.826 476.46 540.445 479.713 543.992C482.966 547.531 487.373 549.332 492.984 549.333C499.491 549.333 504.687 547.433 508.621 543.634C512.555 539.835 514.547 534.648 514.547 528.032V475.729H540.883V571.51L540.832 571.461H514.705V561.963C507.467 569.407 497.972 573.15 486.27 573.15C474.568 573.15 465.653 569.61 458.781 562.524C451.909 555.438 448.498 546.102 448.498 534.55V475.721H474.833V530.079ZM266.725 535.497L322.589 501.641L256.912 571.921L255.929 543.519L200.008 571.921H200L269.09 426.28L266.725 535.497ZM375.332 485.276C382.57 477.832 392.065 474.089 403.768 474.089C415.47 474.089 424.384 477.629 431.256 484.715C438.128 491.802 441.539 501.138 441.539 512.689V571.517H415.204V517.161C415.204 511.413 413.577 506.794 410.324 503.248C407.071 499.708 402.664 497.907 397.053 497.907C390.546 497.907 385.35 499.806 381.416 503.605C377.482 507.404 375.49 512.591 375.49 519.208V571.51H349.154V475.729L349.205 452.648H375.332V485.276ZM588.437 475.729H611.991V498.88H588.443V537.326C588.443 541.531 589.44 544.616 591.482 546.459C593.532 548.31 596.836 549.284 601.407 549.284H611.991V571.51H599.508C574.434 571.51 561.943 560.576 561.943 538.658V498.929H547.833V475.729H561.943V452.647H588.437V475.729ZM645.649 518.639H645.707L678.126 475.729H708.238L670.674 523.315L709.75 571.461H676.75L645.649 532.09V571.461H618.941V475.729H645.649V518.639ZM747.38 497.389C739.877 497.389 733.635 499.905 728.598 504.938C723.567 509.971 721.044 516.279 721.044 523.877C721.044 531.475 723.517 537.994 728.39 543.027C733.27 548.059 739.404 550.569 746.807 550.569C754.209 550.569 760.666 548.003 765.904 542.921C771.1 537.84 773.722 531.475 773.722 523.877C773.722 516.279 771.207 509.964 766.169 504.938H766.162C761.132 499.905 754.883 497.389 747.38 497.389Z' fill='%23E40B2D'/%3E%3C/svg%3E"; @@ -196,82 +172,93 @@ function Extension() { return ( - {translate('hutko.fiscal_receipts')} + hutko {translate('hutko.fiscal_receipts')} - {receipts.map((receipt) => ( - - - - - {receipt.info.display_datetime ? ( - <> - {receipt.info.display_datetime} - {receipt.info.amount && ( - {receipt.info.amount} {translate('hutko.currency')} - )} - {receipt.info.fiscal_number && ( - {translate('hutko.fiscal_number_label')}{receipt.info.fiscal_number} - )} - - ) : ( - {translate('hutko.waiting_for_fiscalisation', { id: receipt.id })} + {displayReceipts.map((receipt) => ( + + + + {receipt.info.display_datetime ? ( + <> + {receipt.info.display_datetime && ( +  {receipt.info.display_datetime} + )} + {receipt.info.amount && ( +  {receipt.info.amount} {receipt.info.currency || translate('hutko.currency')} )} - - - + {receipt.info.fiscal_status !== 'done' && ( + {translate('hutko.processing')} + )} + {receipt.info.transaction_type && ( + + {receipt.info.transaction_type === 'purchase' + ? + + {translate('hutko.transaction_type_purchase')} + - - {receipt.storage.html && ( - - openBase64AsBlob(receipt.storage.html, 'text/html') - } - > - {translate('hutko.view_receipt')} + : receipt.info.transaction_type === 'reverse' + ? + + {translate('hutko.transaction_type_reverse')} + + : receipt.info.transaction_type} + + )} + + - )} - {receipt.storage.xml && ( - - downloadBase64AsFile( - receipt.storage.xml, - `fiscal_receipt_${receipt.id}.xml`, - 'application/xml' - ) - } - > - {translate('hutko.download_xml')} - - )} - - {receipt.info.links && ( - - {receipt.info.links.tax_service && ( - - {translate('hutko.tax_service')} - - )} - {receipt.info.links.external_provider && ( - - {translate('hutko.provider')} - - )} - {receipt.info.links.pdf && ( - - {translate('hutko.pdf')} - - )} + + {receipt.info.fiscal_receipt_number && ( + + + + + {translate('hutko.fn')} {receipt.info.fiscal_receipt_number} + + + )} + + {receipt.info.links && ( + <> + + {receipt.info.links.external_provider && ( + + + + {translate('hutko.provider')} + + + + )} + + + )} + + + ) : ( + + + {translate('hutko.waiting_for_fiscalisation', { id: receipt.id })} + )} + - + + + + + + + ))} { fetchFiscalData(true); }} disabled={loading} > {translate('hutko.refresh')} @@ -291,3 +278,8 @@ function Extension() { ); } +/**ReducedIconTypes + +The subset of icon types available in checkout and customer account surfaces. This is a narrowed set from the full Shopify icon library, containing only the icons supported in these contexts. + +'reset' | 'map' | 'menu' | 'search' | 'circle' | 'filter' | 'image' | 'alert-circle' | 'alert-triangle-filled' | 'alert-triangle' | 'arrow-down' | 'arrow-left' | 'arrow-right' | 'arrow-up-right' | 'arrow-up' | 'bag' | 'bullet' | 'calendar' | 'camera' | 'caret-down' | 'cart' | 'cash-dollar' | 'categories' | 'check-circle' | 'check-circle-filled' | 'check' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'chevron-up' | 'clipboard' | 'clock' | 'credit-card' | 'delete' | 'delivered' | 'delivery' | 'disabled' | 'discount' | 'edit' | 'email' | 'empty' | 'external' | 'geolocation' | 'gift-card' | 'globe' | 'grid' | 'info-filled' | 'info' | 'list-bulleted' | 'location' | 'lock' | 'menu-horizontal' | 'menu-vertical' | 'minus' | 'mobile' | 'note' | 'order' | 'organization' | 'plus' | 'profile' | 'question-circle-filled' | 'question-circle' | 'reorder' | 'return' | 'savings' | 'settings' | 'star-filled' | 'star-half' | 'star' | 'store' | 'truck' | 'upload' | 'x-circle-filled' | 'x-circle' | 'x' */ \ No newline at end of file diff --git a/extensions/fiscal-order-block/locales/en.default.json b/extensions/fiscal-order-block/locales/en.default.json index 5d1130d..d53aeb2 100644 --- a/extensions/fiscal-order-block/locales/en.default.json +++ b/extensions/fiscal-order-block/locales/en.default.json @@ -1,7 +1,7 @@ { "name": "Fiscal Order Block", "hutko": { - "fiscal_receipts_title": "Hutko Fiscal Receipts", + "fiscal_receipts_title": "hutko Fiscal Receipts", "fiscal_receipts": "Fiscal Receipts", "refresh": "Refresh", "loading": "Loading...", @@ -9,12 +9,19 @@ "view_receipt": "View Receipt", "download_xml": "Download XML", "tax_service": "Tax Service", - "provider": "Provider", + "provider": "Open Receipt", + "copy_link": "Copy link", "pdf": "PDF", "waiting_for_fiscalisation": "Receipt {{id}} — waiting for fiscalisation", "no_order_selected": "No order selected.", "failed_to_load_receipts": "Failed to load fiscal receipts: ", "currency": "UAH", - "customer_actions": "Customer actions" + "customer_actions": "Customer actions", + "done": "Done", + "processing": "Processing", + "ready": "Ready", + "transaction_type_purchase": "Purchase", + "transaction_type_reverse": "Refund", + "fn": "FN" } -} +} \ No newline at end of file diff --git a/extensions/fiscal-order-block/locales/uk.json b/extensions/fiscal-order-block/locales/uk.json index f49899f..16e0992 100644 --- a/extensions/fiscal-order-block/locales/uk.json +++ b/extensions/fiscal-order-block/locales/uk.json @@ -1,7 +1,7 @@ { "name": "Блок фіскальних чеків", "hutko": { - "fiscal_receipts_title": "Фіскальні чеки Hutko", + "fiscal_receipts_title": "Фіскальні чеки hutko", "fiscal_receipts": "Фіскальні чеки", "refresh": "Оновити", "loading": "Завантаження...", @@ -9,12 +9,19 @@ "view_receipt": "Переглянути чек", "download_xml": "Завантажити XML", "tax_service": "Податкова служба", - "provider": "Провайдер", + "provider": "Відкрити чек", + "copy_link": "Скопіювати посилання", "pdf": "PDF", "waiting_for_fiscalisation": "Чек {{id}} — очікує фіскалізації", "no_order_selected": "Замовлення не вибрано.", "failed_to_load_receipts": "Не вдалося завантажити фіскальні чеки: ", "currency": "грн", - "customer_actions": "Дії клієнта" + "customer_actions": "Дії клієнта", + "done": "Готово", + "processing": "Обробляється", + "ready": "Готово", + "transaction_type_purchase": "Продаж", + "transaction_type_reverse": "Повернення", + "fn": "ФН" } -} +} \ No newline at end of file diff --git a/extensions/fiscal-order-block/src/BlockExtension copy.jsx b/extensions/fiscal-order-block/src/BlockExtension copy.jsx new file mode 100644 index 0000000..fd87a13 --- /dev/null +++ b/extensions/fiscal-order-block/src/BlockExtension copy.jsx @@ -0,0 +1,207 @@ +import '@shopify/ui-extensions/preact'; +import { render } from 'preact'; +import { useState, useEffect, useCallback } from 'preact/hooks'; + + + +export default function extension() { + render(, document.body); +} + +function Extension() { + const translate = shopify.i18n.translate; + const orderId = shopify.data.selected?.[0]?.id; + + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [receipts, setReceipts] = useState([]); + + const fetchFiscalData = useCallback(async () => { + if (!orderId) { + setError(translate('hutko.no_order_selected')); + setLoading(false); + return; + } + + setLoading(true); + setError(null); + try { + const indexRes = await fetch('shopify:admin/api/graphql.json', { + method: 'POST', + body: JSON.stringify({ + query: ` + query getOrderMetafields($id: ID!) { + order(id: $id) { + receipts: metafield(namespace: "hutko-fiscal", key: "fiscal_receipts") { value } + } + } + `, + variables: { id: orderId } + }) + }); + const indexData = await indexRes.json(); + + const receiptIds = JSON.parse( + indexData?.data?.order?.receipts?.value || '[]' + ); + + if (receiptIds.length === 0) { + setReceipts([]); + setLoading(false); + return; + } + + const detailed = await Promise.all( + receiptIds.map(async (id) => { + const res = await fetch('shopify:admin/api/graphql.json', { + method: 'POST', + body: JSON.stringify({ + query: ` + query getReceiptData($id: ID!, $infoKey: String!, $storageKey: String!) { + order(id: $id) { + info: metafield(namespace: "hutko-fiscal", key: $infoKey) { value } + storage: metafield(namespace: "hutko-fiscal", key: $storageKey) { value } + } + } + `, + variables: { + id: orderId, + infoKey: `fiscal_info_${id}`, + storageKey: `fiscal_storage_${id}`, + } + }) + }); + const resData = await res.json(); + + let info = {}; + try { + info = JSON.parse(resData?.data?.order?.info?.value || '{}'); + } catch (e) { } + + let storage = {}; + try { + storage = JSON.parse(resData?.data?.order?.storage?.value || '{}'); + } catch (e) { } + + return { id, info, storage }; + }) + ); + + setReceipts(detailed); + } catch (e) { + console.error('Failed to fetch fiscal data:', e); + setError(translate('hutko.failed_to_load_receipts') + e.message); + } finally { + setLoading(false); + } + }, [orderId, translate]); + + useEffect(() => { + fetchFiscalData(); + }, [fetchFiscalData]); + const logoSrc = "data:image/svg+xml,%3Csvg width='120' height='30' viewBox='188 423 624 154' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M747.38 473.832C762.386 473.832 774.926 478.66 784.944 488.305C794.962 497.957 800 509.817 800 523.877C800 537.938 794.962 549.594 784.944 559.246C774.876 568.897 762.386 573.72 747.38 573.72C732.374 573.72 719.898 568.947 709.923 559.344C699.955 549.742 694.967 537.938 694.967 523.871C694.967 509.803 700.005 498.104 710.023 488.403C720.095 478.713 732.504 473.835 747.38 473.824V473.832ZM474.833 530.079C474.833 535.826 476.46 540.445 479.713 543.992C482.966 547.531 487.373 549.332 492.984 549.333C499.491 549.333 504.687 547.433 508.621 543.634C512.555 539.835 514.547 534.648 514.547 528.032V475.729H540.883V571.51L540.832 571.461H514.705V561.963C507.467 569.407 497.972 573.15 486.27 573.15C474.568 573.15 465.653 569.61 458.781 562.524C451.909 555.438 448.498 546.102 448.498 534.55V475.721H474.833V530.079ZM266.725 535.497L322.589 501.641L256.912 571.921L255.929 543.519L200.008 571.921H200L269.09 426.28L266.725 535.497ZM375.332 485.276C382.57 477.832 392.065 474.089 403.768 474.089C415.47 474.089 424.384 477.629 431.256 484.715C438.128 491.802 441.539 501.138 441.539 512.689V571.517H415.204V517.161C415.204 511.413 413.577 506.794 410.324 503.248C407.071 499.708 402.664 497.907 397.053 497.907C390.546 497.907 385.35 499.806 381.416 503.605C377.482 507.404 375.49 512.591 375.49 519.208V571.51H349.154V475.729L349.205 452.648H375.332V485.276ZM588.437 475.729H611.991V498.88H588.443V537.326C588.443 541.531 589.44 544.616 591.482 546.459C593.532 548.31 596.836 549.284 601.407 549.284H611.991V571.51H599.508C574.434 571.51 561.943 560.576 561.943 538.658V498.929H547.833V475.729H561.943V452.647H588.437V475.729ZM645.649 518.639H645.707L678.126 475.729H708.238L670.674 523.315L709.75 571.461H676.75L645.649 532.09V571.461H618.941V475.729H645.649V518.639ZM747.38 497.389C739.877 497.389 733.635 499.905 728.598 504.938C723.567 509.971 721.044 516.279 721.044 523.877C721.044 531.475 723.517 537.994 728.39 543.027C733.27 548.059 739.404 550.569 746.807 550.569C754.209 550.569 760.666 548.003 765.904 542.921C771.1 537.84 773.722 531.475 773.722 523.877C773.722 516.279 771.207 509.964 766.169 504.938H766.162C761.132 499.905 754.883 497.389 747.38 497.389Z' fill='%23E40B2D'/%3E%3C/svg%3E"; + + return ( + + + + + + + {error && ( + {error} + )} + + {loading && !error && ( + + {translate('hutko.loading')} + + )} + + {!loading && receipts.length === 0 && !error && ( + {translate('hutko.no_receipts')} + )} + + {!loading && receipts.length > 0 && receipts.map((receipt) => ( + + + + {receipt.info.display_datetime ? ( + <> + {receipt.info.display_datetime && ( +  {receipt.info.display_datetime} + )} + {receipt.info.amount && ( +  {receipt.info.amount} {receipt.info.currency || translate('hutko.currency')} + )} + + {receipt.info.fiscal_status !== 'done' && ( + {translate('hutko.processing')} + )} + {receipt.info.transaction_type && ( + + {receipt.info.transaction_type === 'purchase' + ? translate('hutko.transaction_type_purchase') + : receipt.info.transaction_type === 'reverse' + ? translate('hutko.transaction_type_reverse') + : receipt.info.transaction_type} + + )} + + + + {receipt.info.fiscal_receipt_number && ( + + {translate('hutko.fn')} {receipt.info.fiscal_receipt_number} + + )} + + {receipt.info.links && ( + <> + + {receipt.info.links.external_provider && ( + + {translate('hutko.provider')} + + )} + + + )} + + + ) : ( + {translate('hutko.waiting_for_fiscalisation', { id: receipt.id })} + )} + + + + + + + + + + + ))} + + + + + + + + + + ); +} diff --git a/extensions/fiscal-order-block/src/BlockExtension.jsx b/extensions/fiscal-order-block/src/BlockExtension.jsx index 4590d65..6acb9d0 100644 --- a/extensions/fiscal-order-block/src/BlockExtension.jsx +++ b/extensions/fiscal-order-block/src/BlockExtension.jsx @@ -2,46 +2,7 @@ import '@shopify/ui-extensions/preact'; import { render } from 'preact'; import { useState, useEffect, useCallback } from 'preact/hooks'; -function openBase64AsBlob(base64String, mimeType) { - try { - const decoded = atob(base64String); - const bytes = new Uint8Array(decoded.length); - for (let i = 0; i < decoded.length; i++) { - bytes[i] = decoded.charCodeAt(i); - } - const blob = new Blob([bytes], { type: mimeType }); - const url = URL.createObjectURL(blob); - const win = window.open(url, '_blank'); - if (win) { - setTimeout(() => URL.revokeObjectURL(url), 10000); - } - return !!win; - } catch (e) { - console.error('Failed to open blob:', e); - return false; - } -} -function downloadBase64AsFile(base64String, filename, mimeType) { - try { - const decoded = atob(base64String); - const bytes = new Uint8Array(decoded.length); - for (let i = 0; i < decoded.length; i++) { - bytes[i] = decoded.charCodeAt(i); - } - const blob = new Blob([bytes], { type: mimeType }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - setTimeout(() => URL.revokeObjectURL(url), 5000); - } catch (e) { - console.error('Failed to download file:', e); - } -} export default function extension() { render(, document.body); @@ -71,7 +32,7 @@ function Extension() { query: ` query getOrderMetafields($id: ID!) { order(id: $id) { - receipts: metafield(namespace: "hutko", key: "fiscal_receipts") { value } + receipts: metafield(namespace: "hutko-fiscal", key: "fiscal_receipts") { value } } } `, @@ -98,8 +59,8 @@ function Extension() { query: ` query getReceiptData($id: ID!, $infoKey: String!, $storageKey: String!) { order(id: $id) { - info: metafield(namespace: "hutko", key: $infoKey) { value } - storage: metafield(namespace: "hutko", key: $storageKey) { value } + info: metafield(namespace: "hutko-fiscal", key: $infoKey) { value } + storage: metafield(namespace: "hutko-fiscal", key: $storageKey) { value } } } `, @@ -129,11 +90,11 @@ function Extension() { setReceipts(detailed); } catch (e) { console.error('Failed to fetch fiscal data:', e); - setError(translate('hutko.failed_to_load_receipts') + e.message); + setError(shopify.i18n.translate('hutko.failed_to_load_receipts') + e.message); } finally { setLoading(false); } - }, [orderId, translate]); + }, [orderId]); useEffect(() => { fetchFiscalData(); @@ -167,52 +128,46 @@ function Extension() { {receipt.info.display_datetime ? ( <> -  {receipt.info.display_datetime} - {receipt.info.amount && ( -  {receipt.info.amount} {translate('hutko.currency')} + {receipt.info.display_datetime && ( +  {receipt.info.display_datetime} + )} + {receipt.info.amount && ( +  {receipt.info.amount} {receipt.info.currency || translate('hutko.currency')} )} - - - {receipt.storage.html && ( - - openBase64AsBlob(receipt.storage.html, 'text/html') - } + {receipt.info.fiscal_status !== 'done' && ( + {translate('hutko.processing')} + )} + {receipt.info.transaction_type && ( + + {receipt.info.transaction_type === 'purchase' + ? translate('hutko.transaction_type_purchase') + : receipt.info.transaction_type === 'reverse' + ? translate('hutko.transaction_type_reverse') + : receipt.info.transaction_type} + + )} + + + + {receipt.info.fiscal_receipt_number && ( + - {translate('hutko.view_receipt')} - - )} - {receipt.storage.xml && ( - - downloadBase64AsFile( - receipt.storage.xml, - `fiscal_receipt_${receipt.id}.xml`, - 'application/xml' - ) - } - > - {translate('hutko.download_xml')} + {translate('hutko.fn')} {receipt.info.fiscal_receipt_number} )} + {receipt.info.links && ( <> - {receipt.info.links.tax_service && ( - - {translate('hutko.tax_service')} - - )} + {receipt.info.links.external_provider && ( {translate('hutko.provider')} )} - {receipt.info.links.pdf && ( - - {translate('hutko.pdf')} - - )} + )} @@ -233,7 +188,7 @@ function Extension() { ))} { - if (!dateStr || dateStr.length !== 8) return dateStr; - return `${dateStr.slice(0, 2)}.${dateStr.slice(2, 4)}.${dateStr.slice(4)}`; -}; - -const formatTime = (timeStr) => { - if (!timeStr || timeStr.length !== 6) return timeStr; - return `${timeStr.slice(0, 2)}:${timeStr.slice(2, 4)}:${timeStr.slice(4)}`; -}; - -export const parseFiscalXML = (xmlString) => { - const parser = new DOMParser(); - const doc = parser.parseFromString(xmlString, "application/xml"); - - const getText = (parent, selector) => parent?.querySelector(selector)?.textContent || ''; - const getElements = (parent, selector) => Array.from(parent?.querySelectorAll(selector) || []); - - const head = doc.querySelector('CHECKHEAD'); - const totals = doc.querySelector('CHECKTOTAL'); - - return { - head: { - orgName: getText(head, 'ORGNM'), - tin: getText(head, 'TIN'), - ipn: getText(head, 'IPN'), - pointName: getText(head, 'POINTNM'), - pointAddress: getText(head, 'POINTADDR'), - date: formatDate(getText(head, 'ORDERDATE')), - time: formatTime(getText(head, 'ORDERTIME')), - orderNum: getText(head, 'ORDERNUM'), - cashDesk: getText(head, 'CASHDESKNUM'), - cashRegister: getText(head, 'CASHREGISTERNUM'), - offline: getText(head, 'OFFLINE') === 'true', - }, - totals: { - sum: getText(totals, 'SUM'), - noRndSum: getText(totals, 'NORNDSUM'), - }, - payments: getElements(doc, 'CHECKPAY > ROW').map(row => ({ - name: getText(row, 'PAYFORMNM'), - sum: getText(row, 'SUM'), - paysys: getText(row, 'PAYSYS NAME'), - acquire: getText(row, 'ACQUIRENM'), - cardMask: getText(row, 'EPZDETAILS'), - authCode: getText(row, 'AUTHCD') - })), - taxes: getElements(doc, 'CHECKTAX > ROW').map(row => ({ - name: getText(row, 'NAME'), - letter: getText(row, 'LETTER'), - percent: getText(row, 'PRC'), - turnover: getText(row, 'TURNOVER'), - sum: getText(row, 'SUM') - })), - items: getElements(doc, 'CHECKBODY > ROW').map(row => ({ - code: getText(row, 'CODE'), - name: getText(row, 'NAME'), - amount: parseFloat(getText(row, 'AMOUNT')).toString(), - unit: getText(row, 'UNITNM'), - price: getText(row, 'PRICE'), - cost: getText(row, 'COST'), - letter: getText(row, 'LETTERS') - })) - }; -}; - -export const generate58mmReceiptHTML = (receipt) => { - return ` - - - - - - Фіскальний Чек - - - -
${receipt.head.orgName}
-
${receipt.head.pointName}
-
${receipt.head.pointAddress}
-
ІД: ${receipt.head.tin} / ПН: ${receipt.head.ipn}
- -
- - ${receipt.items.map(item => ` -
${item.name}
-
- ${item.amount} ${item.unit} x ${item.price} - ${item.cost} ${item.letter} -
- `).join('')} - -
- -
- СУМА: - ${receipt.totals.sum} грн -
- -
- - ${receipt.payments.map(pay => ` -
- ${pay.name} - ${pay.sum} грн -
- ${pay.cardMask ? `
Картка: ${pay.cardMask}
` : ''} - ${pay.authCode ? `
Код авт: ${pay.authCode}
` : ''} - ${pay.acquire ? `
Еквайр: ${pay.acquire}
` : ''} - `).join('')} - -
- - ${receipt.taxes.map(tax => ` -
- ${tax.letter} ${tax.name} ${tax.percent}% - ${tax.sum} -
- `).join('')} - -
- -
- ЧЕК № ${receipt.head.orderNum} - КАСИР: ${receipt.head.cashDesk} -
-
- ФН: ${receipt.head.cashRegister} - ЗАК: ${receipt.head.offline ? 'ОФЛАЙН' : 'ОНЛАЙН'} -
-
- ДАТА: ${receipt.head.date} - ЧАС: ${receipt.head.time} -
- -
ФІСКАЛЬНИЙ ЧЕК
- - - - - `; -}; - -// src/Extension.jsx - -import { useState } from 'preact/hooks'; -import { - render, - BlockStack, - Button, - Text, - Banner -} from '@shopify/ui-extensions-preact/admin'; -import { parseFiscalXML, generate58mmReceiptHTML } from './utils/fiscalParser'; - -// Sample XML for testing - replace this with your actual XML data source -const SAMPLE_XML = ` - - 0 - 0 - 136FCB26-4D71-4BBD-9E42-D343DDE0132A - 43103923 - 431039210294 - ТОВ "СПОРТ АКТИВ ПЛЮС" - ТРЕНАЖЕРНИЙ ЗАЛ - м. Київ, Голосіївський район, Голосіївський проспект, буд. 30А - 20042026 - 135110 - 78370 - 2 - 4001145600 - Оплата - 1 - false - - - 35.00 - 0.00 - 35.00 - - - - 1 - Інтернет еквайринг - 35.00 - - - Visa - ПУМБ - 40316265049 - 1819 - 432609XXXXXX9661 - 981702 - - - - - - - 0 - ПДВ - А - 20.00 - 35.00 - 5.83 - - - - - 101729109 - Payment in club: 9 - од. - 1.000 - 35.00 - А - 35.00 - - -`; - -function PrintReceiptExtension() { - const [error, setError] = useState(null); - - const handlePrintClick = () => { - try { - setError(null); - // 1. Parse XML - const receiptData = parseFiscalXML(SAMPLE_XML); - - // 2. Generate HTML - const htmlString = generate58mmReceiptHTML(receiptData); - - // 3. Mount to blob and open for browser print dialog - const blob = new Blob([htmlString], { type: 'text/html' }); - const blobUrl = URL.createObjectURL(blob); - - const newWindow = window.open(blobUrl, '_blank'); - - if (newWindow) { - setTimeout(() => URL.revokeObjectURL(blobUrl), 5000); // Cleanup memory - } else { - setError('Popup blocker prevented opening the print dialog. Please allow popups.'); - } - } catch (err) { - console.error('XML Parsing or Print Error:', err); - setError('Failed to parse or print the fiscal check. Verify the XML format.'); - } - }; - - return ( - - {error && {error}} - Ukrainian Fiscal Check - Click the button below to generate and print a 58mm receipt. - - - - ); -} - -// Ensure the target string matches the `target` in your shopify.extension.toml -render('Admin::Product::Details::Render', () => ); - - - diff --git a/handover.md b/handover.md index 236edd9..ad8b4f2 100644 --- a/handover.md +++ b/handover.md @@ -73,7 +73,7 @@ To build and compile these extensions, the original developer must merge the fol In your staging environment, [shopify.app.toml](file:///home/dev/hutko_shopify_proxy/node/shopify.app.toml) lists your developer App settings: - `client_id = "397c02127a580bce65182a72583642d6"` (Your Dev App Client ID) -- `handle = "fiscal-proxy-app"` +- `handle = "hutko-fiscal-ui-test"` #### Action for Staging: You do **not** need to share your [shopify.app.toml](file:///home/dev/hutko_shopify_proxy/node/shopify.app.toml) with the developer if they already have an existing CLI app configuration. If they do **not** have a CLI app configuration (since they were iframe-only), they should create a `shopify.app.toml` and change: diff --git a/notes.md b/notes.md deleted file mode 100644 index 6535745..0000000 --- a/notes.md +++ /dev/null @@ -1,16 +0,0 @@ -our extension should render fiscal data both on customer site and admin site. -similarities: - -first read fiscal_receipts for list of id of reciepts, then query for fiscal_info_{receipt_id} and fiscal_storage_{receipt_id}. -block should contain list of reciepts with date and amount and action buttons. if html field availiable - should be button to open html as accordeon under reciept line (or what element use polaris for hide-expand) -if no html and xml availiable - just show links buttons. -links from fiscal_info_ should open in new tab every element should be translatable. -check svg folder for icons to integrate. suggest if some icon is missing - we will use awesome icons. All text elements should support localization - that means we should use translation keys for all user visible text. e.g. "Print Fiscal Check" - should be like "{{SHOPFY_APP_NAME}}.print_fiscal_check" and added to locales file. -also we need to have print button on the customer site as well. it should be placed near the list of reciepts. that button should call `actions.printCheck({ data: payload })` where payload is html from fiscal_storage_{receipt_id}. - - -differencies: -customer side should query fiscal_receipts and then fiscal_info_ and fiscal_storage_ using ids from fiscal_receipts in 5s, 20s and 40s from page load. if nothing found - do not render block. if only reciept id in fiscal_receipts found - print txt that check id {receipt_id} is waiting for fiscalisation. if fiscal_info_ availiable - we print date and amount and hide id. - -amdin side should not query with intervals (we suppose that all data is in the shopify store already) so query on page load only. -admin block always rendered. it should contain button refresh. refresh button press will scan all metafields for fiscal_receipts, wildcards fiscal_storage_* and fiscal_info_*. if there no reciept found, it should print txt "no reciepts found". if reciepts found, it should print list of reciepts. if some id is missing in fiscal_storage_* and fiscal_info_* - it should print txt "fiscalisation is pending" but show reciept id from fiscal_receipts. if id is missing in fiscal_receipts but availiable in others - it should print reciept data (date, amount) and availiavle links. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fefa8d7..9567fbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "fiscal-proxy-app", + "name": "hutko-fiscal-ui-test", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "fiscal-proxy-app", + "name": "hutko-fiscal-ui-test", "version": "1.0.0", "workspaces": [ "extensions/*" @@ -8630,4 +8630,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 9eb85a8..9082bb5 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "fiscal-proxy-app", + "name": "hutko-fiscal-ui-test", "version": "1.0.0", "private": true, "scripts": { @@ -22,4 +22,4 @@ "workspaces": [ "extensions/*" ] -} +} \ No newline at end of file diff --git a/shopify.app.toml b/shopify.app.toml index 60d03c7..f55a852 100644 --- a/shopify.app.toml +++ b/shopify.app.toml @@ -1,24 +1,22 @@ -client_id = "397c02127a580bce65182a72583642d6" -name = "fiscal-proxy-app" -handle = "fiscal-proxy-app" +# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration + +name = "hutko-fiscal-ui-test" +handle = "hutko-fiscal-ui-test" application_url = "https://dev.panariga.com/test082023" embedded = true - -[build] -automatically_update_urls_on_dev = true -include_config_on_deploy = true +client_id = "a75366f971c123d9ddbf5f8d6114b8fd" [webhooks] -api_version = "2025-07" +api_version = "2026-04" [access_scopes] # Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes -scopes = "read_all_orders,read_orders" +scopes = "read_orders,write_orders,customer_read_orders" +optional_scopes = [ ] +use_legacy_install_flow = false [auth] -redirect_urls = [ - "https://dev.panariga.com/auth/callback" -] +redirect_urls = [ "https://dev.panariga.com/auth/callback" ] [app_preferences] -url = "https://dev.panariga.com/setup" \ No newline at end of file +url = "https://dev.panariga.com/setup" diff --git a/shopify_groundings.md b/shopify_groundings.md new file mode 100644 index 0000000..9bef02b --- /dev/null +++ b/shopify_groundings.md @@ -0,0 +1,15 @@ +# Shopify App Scopes Justification + +This document outlines the business justification for the requested access scopes, which are required to ensure compliance with Ukrainian fiscal and tax legislation. + +## 1. `read_orders` (including access to orders older than 60 days) +**Justification:** +Our application assists merchants in complying with mandatory Ukrainian tax laws, which require the generation, printing, and storage of fiscal receipts for all transactions. According to local regulations, tax audits can be conducted for periods exceeding 3 years. Therefore, merchants must have uninterrupted, ongoing access to historical fiscal receipts for all their past orders. We require the `read_orders` scope (including access to all orders, regardless of the 60-day limit) so that the application can retrieve and display historical fiscal documents for the merchant during tax inspections or upon demand, independent of their current acquiring relationships. + +## 2. `write_orders` +**Justification:** +To ensure that fiscal documents are permanently and securely accessible to the merchant, our application stores the generated fiscal receipts directly within the order's metafields. This guarantees that the fiscal data is tied to the order in Shopify and remains available even if the merchant terminates their relationship with a specific payment gateway. Since standard payment gateway integrations do not have the required access levels to write this specific data to order metafields, our application requires the `write_orders` scope to save these fiscal receipts and related metadata to the corresponding orders. + +## 3. `customer_read_orders` (Access to Customer Order History) +**Justification:** +Ukrainian legislation also strictly requires that the buyer (customer) receives and has access to their fiscal receipt. Our application displays these fiscal receipts directly to the customer via Customer Account and Checkout UI extensions. The `customer_read_orders` scope is essential to allow the application to read the customer's order history within their personal account, fetch the associated fiscal receipts from the order metafields, and display them securely and conveniently to the buyer. diff --git a/upgradingUI.md b/upgradingUI.md deleted file mode 100644 index 35fe2e6..0000000 --- a/upgradingUI.md +++ /dev/null @@ -1,658 +0,0 @@ ---- -title: Upgrading to 2025-10 -description: > - This guide describes how to upgrade your admin UI extension to API version - `2025-10` and adopt web components. -source_url: - html: 'https://shopify.dev/docs/apps/build/admin/upgrading-to-2025-10' - md: 'https://shopify.dev/docs/apps/build/admin/upgrading-to-2025-10.md' ---- - - -For a new admin order UI block (for example, admin.order-details.block.render), you should use Preact + Polaris web components, not React. - -The current guidance in the admin UI extensions docs is: - -Version 2025-07 is the last API version that supports React-based UI components. -From 2025-10 and onward, Shopify recommends Preact and web components for admin UI extensions. See: -Upgrading to 2025-10 (admin UI extensions, Preact & web components) -Why Preact (web components) over React? -From the upgrade guide for admin UI extensions (Upgrading to 2025-10): - -React-based @shopify/ui-extensions-react components are being phased out. -“Version 2025-07 is the last API version to support React-based UI components. Web components replace them…” -The recommended stack going forward is: -preact -@preact/signals (optional but nice for reactive state) -@shopify/ui-extensions (web components + APIs) -New admin UI extensions (including order blocks) are scaffolded and documented with Preact by default. -So: - -If you’re starting anything new today (including admin order UI blocks): -Use Preact + web components. -If you already have legacy React-based admin UI extensions: -You can stay on an older API version (e.g. 2025-04/2025-07) short-term, -But you’ll eventually need to upgrade to 2025-10 and migrate those to Preact + web components. -What this looks like for admin.order-details.block.render -In React (legacy), an order details block might have looked like this (from the upgrade guide): - -tsx -Copy -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -// LEGACY – React-based admin block (2025-07 or earlier) -import { - reactExtension, - AdminBlock, - BlockStack, - TextField, - Button, -} from '@shopify/ui-extensions-react/admin'; - -export default reactExtension( - 'admin.order-details.block.render', - () => , -); - -function Extension() { - return ( - - - - }> - - - ); -} -``` - -*** - -## Migrate to web components - -Web components are exposed as custom HTML elements. Update your React components to custom elements. - -## New components in Preact - -```tsx -import '@shopify/ui-extensions/preact'; -import {render} from 'preact'; - - -export default function extension() { - render(, document.body); -} - - -function Extension() { - return ( - - - - Update - - - ); -} -``` - -## Previous components in React - -```tsx -import { - reactExtension, - AdminBlock, - BlockStack, - TextField, - Button, -} from '@shopify/ui-extensions-react/admin'; - - -export default reactExtension( - 'admin.order-details.block.render', - () => , -); - - -function Extension() { - return ( - - - -