feat(customer-account): payment extension for order page (shares /api/public/payment-info; dual auth)
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "customer-account-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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import '@shopify/ui-extensions';
|
||||
|
||||
//@ts-ignore
|
||||
declare module './src/CustomerAccount.tsx' {
|
||||
const shopify: import('@shopify/ui-extensions/customer-account.order.page.render').Api;
|
||||
const globalThis: { shopify: typeof shopify };
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
api_version = "2026-01"
|
||||
|
||||
[[extensions]]
|
||||
name = "Invoice payment instructions (account)"
|
||||
handle = "customer-account-payment"
|
||||
type = "ui_extension"
|
||||
uid = "linumiq-customer-account-payment"
|
||||
|
||||
[[extensions.targeting]]
|
||||
target = "customer-account.order.page.render"
|
||||
module = "./src/CustomerAccount.tsx"
|
||||
|
||||
[extensions.capabilities]
|
||||
network_access = true
|
||||
api_access = false
|
||||
@@ -0,0 +1,110 @@
|
||||
import "@shopify/ui-extensions/preact";
|
||||
import { render } from "preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
const APP_URL_PROD = "https://invoice-app.linumiq.com";
|
||||
const APP_URL_DEV = "https://invoice-app-dev.linumiq.com";
|
||||
const DEV_SHOPS = new Set(["linumiq-dev.myshopify.com"]);
|
||||
|
||||
function resolveAppUrl(shopify: any): string {
|
||||
const shop: string | undefined =
|
||||
shopify?.shop?.myshopifyDomain ?? shopify?.shop?.value?.myshopifyDomain;
|
||||
if (shop && DEV_SHOPS.has(shop)) return APP_URL_DEV;
|
||||
return APP_URL_PROD;
|
||||
}
|
||||
|
||||
interface PaymentInstructions {
|
||||
language: "de" | "en";
|
||||
heading: string;
|
||||
giroCodeUrl: 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?.order?.value?.id;
|
||||
if (!orderId) {
|
||||
setDone(true);
|
||||
return;
|
||||
}
|
||||
const token: string = await shopify.sessionToken.get();
|
||||
const appUrl = resolveAppUrl(shopify);
|
||||
const res = await fetch(
|
||||
`${appUrl}/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 {
|
||||
// swallow; render nothing
|
||||
} finally {
|
||||
if (!cancelled) setDone(true);
|
||||
}
|
||||
}
|
||||
load();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!done || !data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<s-section heading={data.heading}>
|
||||
<s-paragraph>{data.instructions}</s-paragraph>
|
||||
<s-grid gridTemplateColumns="200px 1fr" gap="base" alignItems="start">
|
||||
<s-image src={data.giroCodeUrl} alt="GiroCode" inlineSize="fill" aspectRatio="1" />
|
||||
<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-grid>
|
||||
</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/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user