feat(thank-you): payment instructions extension (GiroCode + bank details) for manual payment orders

This commit is contained in:
Gerhard Scheikl
2026-05-09 20:48:08 +02:00
parent 93aec2f368
commit 884070cddc
7 changed files with 360 additions and 0 deletions
@@ -0,0 +1,11 @@
{
"name": "invoice-thank-you-payment",
"version": "1.0.0",
"private": true,
"devDependencies": {
"@preact/signals": "^1.3.0",
"@shopify/ui-extensions": "^2026.1.0",
"preact": "^10.22.0",
"typescript": "^5.6.0"
}
}
+7
View File
@@ -0,0 +1,7 @@
import '@shopify/ui-extensions';
//@ts-ignore
declare module './src/Checkout.tsx' {
const shopify: import('@shopify/ui-extensions/purchase.thank-you.block.render').Api;
const globalThis: { shopify: typeof shopify };
}
@@ -0,0 +1,15 @@
api_version = "2026-01"
[[extensions]]
name = "Invoice payment instructions"
handle = "invoice-thank-you-payment"
type = "ui_extension"
uid = "linumiq-invoice-thank-you-payment"
[[extensions.targeting]]
target = "purchase.thank-you.block.render"
module = "./src/Checkout.tsx"
[extensions.capabilities]
network_access = true
api_access = false
@@ -0,0 +1,103 @@
import "@shopify/ui-extensions/preact";
import { render } from "preact";
import { useEffect, useState } from "preact/hooks";
const APP_URL = "https://invoice-app.linumiq.com";
interface PaymentInstructions {
language: "de" | "en";
heading: string;
giroCodeDataUrl: string;
recipient: string;
bankName: string;
iban: string;
bic: string;
amountFormatted: string;
reference: string;
dueDateFormatted: string | null;
instructions: string;
labels: {
recipient: string;
bank: string;
iban: string;
bic: string;
amount: string;
reference: string;
};
}
export default async () => {
render(<Extension />, document.body);
};
function Extension() {
const shopify = (globalThis as any).shopify;
const [data, setData] = useState<PaymentInstructions | null>(null);
const [done, setDone] = useState(false);
useEffect(() => {
let cancelled = false;
async function load() {
try {
const orderId: string | undefined = shopify?.orderConfirmation?.value?.order?.id;
if (!orderId) {
setDone(true);
return;
}
const token: string = await shopify.sessionToken.get();
const res = await fetch(
`${APP_URL}/api/public/payment-info?orderId=${encodeURIComponent(orderId)}`,
{ headers: { Authorization: `Bearer ${token}` } },
);
if (!res.ok) {
setDone(true);
return;
}
const json = (await res.json()) as {
showPaymentInstructions: boolean;
payload?: PaymentInstructions;
};
if (cancelled) return;
if (json.showPaymentInstructions && json.payload) {
setData(json.payload);
}
} catch {
// ignore — render nothing on error
} finally {
if (!cancelled) setDone(true);
}
}
load();
return () => {
cancelled = true;
};
}, []);
if (!done) {
return <s-skeleton-paragraph />;
}
if (!data) {
return null;
}
return (
<s-section heading={data.heading}>
<s-paragraph>{data.instructions}</s-paragraph>
<s-stack direction="inline" gap="base" align-items="start">
<s-image src={data.giroCodeDataUrl} alt="GiroCode" />
<s-stack direction="block" gap="small-200">
<s-text>{data.labels.recipient}: {data.recipient}</s-text>
{data.bankName ? (
<s-text>{data.labels.bank}: {data.bankName}</s-text>
) : null}
<s-text>{data.labels.iban}: {data.iban}</s-text>
{data.bic ? (
<s-text>{data.labels.bic}: {data.bic}</s-text>
) : null}
<s-text>{data.labels.amount}: {data.amountFormatted}</s-text>
<s-text>{data.labels.reference}: {data.reference}</s-text>
</s-stack>
</s-stack>
</s-section>
);
}
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"jsx": "react-jsx",
"jsxImportSource": "preact",
"strict": true,
"skipLibCheck": true,
"isolatedModules": true,
"noEmit": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}