fix(thank-you): serve GiroCode as signed PNG URL instead of data URL
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import type { LoaderFunctionArgs } from "react-router";
|
||||
import { unauthenticated } from "../shopify.server";
|
||||
import db from "../db.server";
|
||||
import { buildGiroCodePngBuffer } from "../services/invoice/girocode";
|
||||
import { verifyGiroCodeUrl } from "../services/invoice/signedUrl";
|
||||
|
||||
/**
|
||||
* Public PNG endpoint that returns the GiroCode QR image bytes for an order.
|
||||
* Auth: short-lived HMAC-signed URL (issued by /api/public/payment-info).
|
||||
*
|
||||
* Required query params: shop, orderId, exp, sig.
|
||||
*/
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const url = new URL(request.url);
|
||||
const verified = verifyGiroCodeUrl(url.searchParams);
|
||||
if (!verified.ok) {
|
||||
return new Response(`unauthorized: ${verified.reason ?? "invalid"}`, { status: 401 });
|
||||
}
|
||||
const { shop, orderId } = verified;
|
||||
if (!shop || !orderId) {
|
||||
return new Response("bad request", { status: 400 });
|
||||
}
|
||||
|
||||
const settings = await db.shopSettings.findUnique({ where: { shopDomain: shop } });
|
||||
if (!settings?.iban) {
|
||||
return new Response("not found", { status: 404 });
|
||||
}
|
||||
|
||||
// Recompute payload server-side from order + settings (don't trust client).
|
||||
const numericId = orderId.replace(/[^0-9]/g, "");
|
||||
const orderGid = `gid://shopify/Order/${numericId}`;
|
||||
|
||||
const { admin } = await unauthenticated.admin(shop);
|
||||
const res = await admin.graphql(
|
||||
`#graphql
|
||||
query GiroCodeOrderInfo($id: ID!) {
|
||||
order(id: $id) {
|
||||
name
|
||||
currencyCode
|
||||
totalPriceSet { shopMoney { amount } }
|
||||
totalOutstandingSet { shopMoney { amount } }
|
||||
}
|
||||
}`,
|
||||
{ variables: { id: orderGid } },
|
||||
);
|
||||
const json = (await res.json()) as {
|
||||
data?: {
|
||||
order?: {
|
||||
name?: string;
|
||||
currencyCode?: string;
|
||||
totalPriceSet?: { shopMoney: { amount: string } };
|
||||
totalOutstandingSet?: { shopMoney: { amount: string } };
|
||||
} | null;
|
||||
};
|
||||
};
|
||||
const o = json.data?.order;
|
||||
if (!o) {
|
||||
return new Response("not found", { status: 404 });
|
||||
}
|
||||
|
||||
const total = parseFloat(o.totalPriceSet?.shopMoney.amount ?? "0");
|
||||
const outstanding = parseFloat(o.totalOutstandingSet?.shopMoney.amount ?? "0");
|
||||
const amount = outstanding > 0 ? outstanding : total;
|
||||
const remittance = o.name ?? numericId;
|
||||
|
||||
const png = await buildGiroCodePngBuffer({
|
||||
beneficiaryName: [settings.companyName, settings.legalForm].filter(Boolean).join(" "),
|
||||
iban: settings.iban,
|
||||
bic: settings.bic,
|
||||
amount,
|
||||
currency: o.currencyCode ?? "EUR",
|
||||
remittance,
|
||||
});
|
||||
|
||||
const body = new Uint8Array(png);
|
||||
return new Response(body, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "image/png",
|
||||
"Cache-Control": "private, max-age=300",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user