first commit
This commit is contained in:
18
extensions/fiscal-checkout-block/locales/en.default.json
Normal file
18
extensions/fiscal-checkout-block/locales/en.default.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "Fiscal Order Status Block",
|
||||
"hutko": {
|
||||
"fiscal_receipts": "Fiscal Receipts",
|
||||
"loading": "Loading fiscal receipts...",
|
||||
"no_receipts": "No fiscal receipts found.",
|
||||
"view_receipt": "View Receipt",
|
||||
"download_xml": "Download XML",
|
||||
"tax_service": "Tax Service",
|
||||
"provider": "Provider",
|
||||
"pdf": "PDF",
|
||||
"waiting_for_fiscalisation": "Receipt {{id}} — waiting for fiscalisation",
|
||||
"failed_to_load_receipts": "Failed to load fiscal receipts. Please try again.",
|
||||
"currency": "UAH",
|
||||
"refresh": "Refresh",
|
||||
"fiscal_number_label": "FN: "
|
||||
}
|
||||
}
|
||||
18
extensions/fiscal-checkout-block/locales/uk.json
Normal file
18
extensions/fiscal-checkout-block/locales/uk.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "Блок статусу фіскального замовлення",
|
||||
"hutko": {
|
||||
"fiscal_receipts": "Фіскальні чеки",
|
||||
"loading": "Завантаження фіскальних чеків...",
|
||||
"no_receipts": "Фіскальних чеків не знайдено.",
|
||||
"view_receipt": "Переглянути чек",
|
||||
"download_xml": "Завантажити XML",
|
||||
"tax_service": "Податкова служба",
|
||||
"provider": "Провайдер",
|
||||
"pdf": "PDF",
|
||||
"waiting_for_fiscalisation": "Чек {{id}} — очікує фіскалізації",
|
||||
"failed_to_load_receipts": "Не вдалося завантажити фіскальні чеки. Будь ласка, спробуйте ще раз.",
|
||||
"currency": "грн",
|
||||
"refresh": "Оновити",
|
||||
"fiscal_number_label": "ФН: "
|
||||
}
|
||||
}
|
||||
7
extensions/fiscal-checkout-block/shopify.d.ts
vendored
Normal file
7
extensions/fiscal-checkout-block/shopify.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import '@shopify/ui-extensions';
|
||||
|
||||
//@ts-ignore
|
||||
declare module './src/OrderStatusExtension.jsx' {
|
||||
const shopify: import('@shopify/ui-extensions/customer-account.order-status.block.render').Api;
|
||||
const globalThis: { shopify: typeof shopify };
|
||||
}
|
||||
17
extensions/fiscal-checkout-block/shopify.extension.toml
Normal file
17
extensions/fiscal-checkout-block/shopify.extension.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
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"
|
||||
293
extensions/fiscal-checkout-block/src/OrderStatusExtension.jsx
Normal file
293
extensions/fiscal-checkout-block/src/OrderStatusExtension.jsx
Normal file
@@ -0,0 +1,293 @@
|
||||
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(
|
||||
'shopify://customer-account/api/2025-10/graphql.json',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query, variables }),
|
||||
}
|
||||
);
|
||||
if (!res.ok) throw new Error(`API request failed: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export default function extension() {
|
||||
render(<Extension />, document.body);
|
||||
}
|
||||
|
||||
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 fetchFiscalData = useCallback(async () => {
|
||||
if (!orderId) {
|
||||
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;
|
||||
if (metafields && metafields.length > 0) {
|
||||
const entry = metafields.find(
|
||||
(e) =>
|
||||
String(e.target?.type) === 'order' &&
|
||||
e.metafield?.namespace === 'hutko' &&
|
||||
e.metafield?.key === 'fiscal_receipts',
|
||||
);
|
||||
if (entry?.metafield?.value && typeof entry.metafield.value === 'string') {
|
||||
try {
|
||||
receiptIds = JSON.parse(entry.metafield.value);
|
||||
} catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: query via Customer Account API if appMetafields didn't have it
|
||||
if (receiptIds.length === 0) {
|
||||
const indexRes = await queryCustomerAccountAPI(
|
||||
`query getOrderMetafield($id: ID!) {
|
||||
order(id: $id) {
|
||||
metafield(namespace: "hutko", key: "fiscal_receipts") { value }
|
||||
}
|
||||
}`,
|
||||
{ id: orderId },
|
||||
);
|
||||
|
||||
try {
|
||||
receiptIds = JSON.parse(
|
||||
indexRes?.data?.order?.metafield?.value || '[]'
|
||||
);
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
if (receiptIds.length === 0) {
|
||||
setReceipts([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Fetch per-receipt info & storage 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!) {
|
||||
order(id: $orderId) {
|
||||
info: metafield(namespace: "hutko", key: $infoKey) { value }
|
||||
storage: metafield(namespace: "hutko", key: $storageKey) { value }
|
||||
}
|
||||
}`,
|
||||
{
|
||||
orderId,
|
||||
infoKey: `fiscal_info_${id}`,
|
||||
storageKey: `fiscal_storage_${id}`,
|
||||
},
|
||||
);
|
||||
|
||||
let info = {};
|
||||
try {
|
||||
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 };
|
||||
})
|
||||
);
|
||||
|
||||
setReceipts(detailed);
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch fiscal data:', e);
|
||||
setError(translate('hutko.failed_to_load_receipts'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [orderId, translate]);
|
||||
|
||||
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]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<s-box padding="base" border="base" borderRadius="base">
|
||||
<s-stack direction="inline" alignItems="center" gap="base">
|
||||
<s-spinner size="small"></s-spinner>
|
||||
<s-text>{translate('hutko.loading')}</s-text>
|
||||
</s-stack>
|
||||
</s-box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <s-banner tone="critical">{error}</s-banner>;
|
||||
}
|
||||
|
||||
if (receipts.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 (<s-box>
|
||||
<s-stack gap="base" padding="base" border="base" borderRadius="base">
|
||||
|
||||
<s-heading>{translate('hutko.fiscal_receipts')}</s-heading>
|
||||
|
||||
{receipts.map((receipt) => (
|
||||
<s-box key={receipt.id} padding="base" border="base" borderRadius="base">
|
||||
<s-stack gap="base">
|
||||
<s-stack direction="inline" justifyContent="space-between" alignItems="center">
|
||||
<s-stack gap="none">
|
||||
{receipt.info.display_datetime ? (
|
||||
<>
|
||||
<s-text type="strong">{receipt.info.display_datetime}</s-text>
|
||||
{receipt.info.amount && (
|
||||
<s-text>{receipt.info.amount} {translate('hutko.currency')}</s-text>
|
||||
)}
|
||||
{receipt.info.fiscal_number && (
|
||||
<s-text type="small">{translate('hutko.fiscal_number_label')}{receipt.info.fiscal_number}</s-text>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<s-text>{translate('hutko.waiting_for_fiscalisation', { id: receipt.id })}</s-text>
|
||||
)}
|
||||
</s-stack>
|
||||
</s-stack>
|
||||
|
||||
<s-divider></s-divider>
|
||||
|
||||
<s-stack direction="inline" gap="base">
|
||||
{receipt.storage.html && (
|
||||
<s-button
|
||||
onClick={() =>
|
||||
openBase64AsBlob(receipt.storage.html, 'text/html')
|
||||
}
|
||||
>
|
||||
{translate('hutko.view_receipt')}
|
||||
</s-button>
|
||||
)}
|
||||
{receipt.storage.xml && (
|
||||
<s-button
|
||||
onClick={() =>
|
||||
downloadBase64AsFile(
|
||||
receipt.storage.xml,
|
||||
`fiscal_receipt_${receipt.id}.xml`,
|
||||
'application/xml'
|
||||
)
|
||||
}
|
||||
>
|
||||
{translate('hutko.download_xml')}
|
||||
</s-button>
|
||||
)}
|
||||
</s-stack>
|
||||
|
||||
{receipt.info.links && (
|
||||
<s-stack direction="inline" gap="base">
|
||||
{receipt.info.links.tax_service && (
|
||||
<s-link href={receipt.info.links.tax_service} target="_blank">
|
||||
{translate('hutko.tax_service')}
|
||||
</s-link>
|
||||
)}
|
||||
{receipt.info.links.external_provider && (
|
||||
<s-link href={receipt.info.links.external_provider} target="_blank">
|
||||
{translate('hutko.provider')}
|
||||
</s-link>
|
||||
)}
|
||||
{receipt.info.links.pdf && (
|
||||
<s-link href={receipt.info.links.pdf} target="_blank">
|
||||
{translate('hutko.pdf')}
|
||||
</s-link>
|
||||
)}
|
||||
</s-stack>
|
||||
)}
|
||||
</s-stack>
|
||||
</s-box>
|
||||
))}
|
||||
<s-stack direction="inline" justifyContent="space-between" alignItems="center">
|
||||
<s-image
|
||||
src={logoSrc}
|
||||
alt="Hutko"
|
||||
aspectRatio="4/1"
|
||||
objectFit="contain"
|
||||
sizes="120px"
|
||||
inlineSize="auto"
|
||||
loading="lazy"
|
||||
></s-image>
|
||||
<s-stack direction="inline" justifyContent="end">
|
||||
<s-button
|
||||
onClick={fetchFiscalData}
|
||||
disabled={loading}
|
||||
>
|
||||
{translate('hutko.refresh')}
|
||||
</s-button>
|
||||
</s-stack>
|
||||
</s-stack>
|
||||
</s-stack></s-box>
|
||||
);
|
||||
}
|
||||
|
||||
13
extensions/fiscal-checkout-block/tsconfig.json
Normal file
13
extensions/fiscal-checkout-block/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact",
|
||||
"target": "ES2020",
|
||||
"checkJs": true,
|
||||
"allowJs": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user