Skip to content
Merged
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ REALM=mpp.dev
RPC_URL=https://user:pass@rpc.moderato.tempo.xyz
STRIPE_NETWORK_ID=internal
STRIPE_SECRET_KEY=sk_test_...
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_...
VITE_DEFAULT_CURRENCY=0x20c0000000000000000000000000000000000000
VITE_DEFAULT_RECIPIENT=0xa726a1CD723409074DF9108A2187cfA19899aCF8
VITE_DEMO_LIVE=true
Expand Down
16 changes: 6 additions & 10 deletions src/components/PaymentLinkDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
"use client";

const PAYMENT_LINK_URL = "/api/payment-link/photo";

export function PaymentLinkDemo() {
return (
<div className="not-prose">
<div
className="rounded-xl overflow-hidden"
className="overflow-hidden"
style={{
border: "1px solid light-dark(#e5e5e5, #262626)",
height: 420,
borderRadius: 12,
}}
>
<iframe
src={PAYMENT_LINK_URL}
title="Payment link demo"
src="/api/payment-link/photo"
title="Payment link demo — Tempo"
style={{
border: "none",
display: "block",
height: 560,
transform: "scale(0.75)",
transformOrigin: "top left",
width: "133.33%",
height: 420,
width: "100%",
}}
/>
</div>
Expand Down
19 changes: 19 additions & 0 deletions src/mppx-payment-link-stripe.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Mppx, stripe } from "mppx/server";

const realm = process.env.REALM ?? "mpp.tempo.xyz";

export const stripeMppx = Mppx.create({
methods: [
stripe({
html: {
createTokenUrl: "/api/demo/create-spt",
publishableKey: import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY!,
},
networkId: process.env.STRIPE_NETWORK_ID ?? "internal",
paymentMethodTypes: ["card"],
secretKey: process.env.STRIPE_SECRET_KEY!,
}),
],
realm,
secretKey: "demo",
});
10 changes: 4 additions & 6 deletions src/mppx-payment-link.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,15 @@ export const mppx = Mppx.create({
},
html: {
theme: {
logo: { dark: "/logo-dark.svg", light: "/logo-light.svg" },
accent: ["#000000", "#ffffff"],
background: ["#ffffff", "#0a0a0a"],
border: ["#e5e5e5", "#262626"],
colorScheme: "light dark",
fontFamily: "'Geist', system-ui, sans-serif",
fontSizeBase: "16px",
foreground: ["#0a0a0a", "#fafafa"],
logo: {
dark: "/logo-light.svg",
light: "/logo-dark.svg",
},

muted: ["#737373", "#a3a3a3"],
negative: ["#ef4444", "#f87171"],
positive: ["#22c55e", "#4ade80"],
Expand All @@ -44,8 +42,8 @@ export const mppx = Mppx.create({
surface: ["#f5f5f5", "#171717"],
},
text: {
paymentRequired: "Payment Required",
title: "MPP — Payment Required",
paymentRequired: "",
title: "Payment link demo",
},
},
sse: true,
Expand Down
71 changes: 14 additions & 57 deletions src/pages/_api/api/demo/create-spt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,70 +23,27 @@ export async function POST(request: Request) {
"usage_limits[max_amount]": params.amount,
"usage_limits[expires_at]": params.expiresAt.toString(),
});
if (params.networkId)
body.set("seller_details[network_id]", params.networkId);
if (params.metadata) {
for (const [key, value] of Object.entries(params.metadata)) {
body.set(`metadata[${key}]`, value);
}
}

const sptUrl =
"https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens";
const sptHeaders = {
Authorization: `Basic ${btoa(`${secretKey}:`)}`,
"Content-Type": "application/x-www-form-urlencoded",
};

let response = await fetch(sptUrl, {
method: "POST",
headers: sptHeaders,
body,
});
const response = await fetch(
"https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens",
{
method: "POST",
headers: {
Authorization: `Basic ${btoa(`${secretKey}:`)}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body,
},
);

if (!response.ok) {
const errorBody = (await response.json()) as {
error: { message: string };
};
console.warn(
`[demo/create-spt] initial Stripe SPT request failed: ${errorBody.error.message}`,
console.error(
`[demo/create-spt] Stripe SPT request failed: ${errorBody.error.message}`,
);
if (
(params.metadata || params.networkId) &&
errorBody.error.message.includes("Received unknown parameter")
) {
console.warn(
"[demo/create-spt] retrying Stripe SPT request without metadata/network_id",
);
const fallbackBody = new URLSearchParams({
payment_method: params.paymentMethod,
"usage_limits[currency]": params.currency,
"usage_limits[max_amount]": params.amount,
"usage_limits[expires_at]": params.expiresAt.toString(),
});
response = await fetch(sptUrl, {
method: "POST",
headers: sptHeaders,
body: fallbackBody,
});
if (!response.ok) {
const fallbackError = (await response.json()) as {
error: { message: string };
};
console.error(
`[demo/create-spt] Stripe fallback request failed: ${fallbackError.error.message}`,
);
return Response.json(
{ error: fallbackError.error.message },
{ status: 400 },
);
}
} else {
console.error(
`[demo/create-spt] Stripe SPT request failed: ${errorBody.error.message}`,
);
return Response.json({ error: errorBody.error.message }, { status: 400 });
}
return Response.json({ error: errorBody.error.message }, { status: 400 });
}

const { id } = (await response.json()) as { id: string };
Expand Down
63 changes: 63 additions & 0 deletions src/pages/_api/api/payment-link/photo-stripe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { stripeMppx } from "../../../../mppx-payment-link-stripe.server";

export async function GET(request: Request) {
const result = await stripeMppx.charge({
amount: "100",
currency: "usd",
decimals: 0,
description: "A random unique image",
})(request);

if (result.status === 402) return result.challenge;

const res = await fetch("https://picsum.photos/1024/1024");
const imageUrl = res.url;

const html = `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Photo — MPP Demo</title>
<style>
:root { color-scheme: dark light; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
padding: 32px;
font-family: system-ui, -apple-system, sans-serif;
background: light-dark(#fafafa, #0a0a0a);
color: light-dark(#111, #eee);
}
img {
max-width: 480px;
width: 100%;
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0,0,0,0.15);
}
p {
font-size: 13px;
color: light-dark(#666, #888);
}
</style>
</head>
<body>
<img src="${imageUrl}" alt="Random photo from Picsum" />
<p>Paid via MPP — $0.01</p>
</body>
</html>`;

return result.withReceipt(
new Response(html, {
headers: {
"Cache-Control": "no-store",
"Content-Type": "text/html; charset=utf-8",
},
}),
);
}
Loading