Skip to content

Commit 9984831

Browse files
authored
feat: add payment links guide (#521)
1 parent 37eb702 commit 9984831

15 files changed

Lines changed: 773 additions & 62 deletions

File tree

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@
2525
"@stripe/stripe-js": "^8.9.0",
2626
"@vercel/blob": "^2.3.1",
2727
"mermaid": "^11.12.2",
28-
"mppx": "https://pkg.pr.new/mppx@231",
28+
"mppx": "https://pkg.pr.new/mppx@283",
2929
"react": "^19",
3030
"react-dom": "^19",
3131
"stripe": "^20.4.1",
3232
"tailwindcss": "^4.1.18",
33-
"viem": "^2.46.2",
33+
"viem": "^2.47.5",
3434
"vocs": "https://pkg.pr.new/wevm/vocs@319c55c",
3535
"wagmi": "^3.4.2",
3636
"waku": "^1.0.0-alpha.4"
@@ -54,6 +54,7 @@
5454
"typescript": "^5",
5555
"unplugin-icons": "^23.0.1",
5656
"vite": "^7",
57+
"vite-plugin-mkcert": "^1.17.10",
5758
"vitest": "^4.0.18"
5859
},
5960
"packageManager": "pnpm@10.22.0",

pnpm-lock.yaml

Lines changed: 155 additions & 51 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/PaymentLinkDemo.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"use client";
2+
3+
const PAYMENT_LINK_URL = "/api/payment-link/photo";
4+
5+
export function PaymentLinkDemo() {
6+
return (
7+
<div className="not-prose">
8+
<div
9+
className="rounded-xl overflow-hidden"
10+
style={{
11+
border: "1px solid light-dark(#e5e5e5, #262626)",
12+
height: 420,
13+
}}
14+
>
15+
<iframe
16+
src={PAYMENT_LINK_URL}
17+
title="Payment link demo"
18+
style={{
19+
border: "none",
20+
display: "block",
21+
height: 560,
22+
transform: "scale(0.75)",
23+
transformOrigin: "top left",
24+
width: "133.33%",
25+
}}
26+
/>
27+
</div>
28+
</div>
29+
);
30+
}

src/components/cards.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ export function PayAsYouGoCard() {
7777
);
7878
}
7979

80+
export function PaymentLinksCard() {
81+
return (
82+
<Card
83+
description="Create a link. Get paid."
84+
icon="lucide:link"
85+
title="Create a payment link"
86+
to="/guides/payment-links"
87+
/>
88+
);
89+
}
90+
8091
export function ProxyExistingServiceCard() {
8192
return (
8293
<Card

src/mppx-payment-link.server.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Mppx, tempo } from "mppx/server";
2+
import { createClient, http } from "viem";
3+
import { privateKeyToAccount } from "viem/accounts";
4+
import { tempoModerato } from "viem/chains";
5+
6+
const realm = process.env.REALM ?? "mpp.tempo.xyz";
7+
const account = privateKeyToAccount(
8+
(process.env.FEE_PAYER_PRIVATE_KEY ??
9+
"0x0000000000000000000000000000000000000000000000000000000000000001") as `0x${string}`,
10+
);
11+
12+
export const mppx = Mppx.create({
13+
methods: [
14+
tempo({
15+
account,
16+
currency: import.meta.env.VITE_DEFAULT_CURRENCY!,
17+
feePayer: true,
18+
getClient() {
19+
return createClient({
20+
chain: tempoModerato,
21+
transport: http(
22+
import.meta.env.RPC_URL ?? "https://rpc.moderato.tempo.xyz",
23+
),
24+
});
25+
},
26+
html: {
27+
theme: {
28+
accent: ["#000000", "#ffffff"],
29+
background: ["#ffffff", "#0a0a0a"],
30+
border: ["#e5e5e5", "#262626"],
31+
colorScheme: "light dark",
32+
fontFamily: "'Geist', system-ui, sans-serif",
33+
fontSizeBase: "16px",
34+
foreground: ["#0a0a0a", "#fafafa"],
35+
logo: {
36+
dark: "/logo-light.svg",
37+
light: "/logo-dark.svg",
38+
},
39+
muted: ["#737373", "#a3a3a3"],
40+
negative: ["#ef4444", "#f87171"],
41+
positive: ["#22c55e", "#4ade80"],
42+
radius: "8px",
43+
spacingUnit: "4px",
44+
surface: ["#f5f5f5", "#171717"],
45+
},
46+
text: {
47+
paymentRequired: "Payment Required",
48+
title: "MPP — Payment Required",
49+
},
50+
},
51+
sse: true,
52+
testnet: true,
53+
}),
54+
],
55+
realm,
56+
secretKey: "demo",
57+
});

src/pages.gen.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ type Page =
116116
| { path: '/guides/multiple-payment-methods'; render: 'static' }
117117
| { path: '/guides/one-time-payments'; render: 'static' }
118118
| { path: '/guides/pay-as-you-go'; render: 'static' }
119+
| { path: '/guides/payment-links'; render: 'static' }
119120
| { path: '/guides/proxy-existing-service'; render: 'static' }
120121
| { path: '/guides/split-payments'; render: 'static' }
121122
| { path: '/guides/streamed-payments'; render: 'static' }

src/pages/_api/api/demo/chat.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,12 @@ export default async function handler(request: Request) {
114114
const text = content.replaceAll("\n", "\t");
115115
const tokens = tokenize(text);
116116

117-
return result.withReceipt(async function* (stream) {
117+
return result.withReceipt(async function* (stream: any) {
118118
for (const token of tokens) {
119119
await stream.charge();
120120
yield token;
121121
}
122-
});
122+
} as any);
123123
}
124124
console.warn(
125125
"[demo/chat] OpenAI response did not contain message content",
@@ -151,10 +151,10 @@ export default async function handler(request: Request) {
151151
].join("\t");
152152
const tokens = tokenize(text);
153153

154-
return result.withReceipt(async function* (stream) {
154+
return result.withReceipt(async function* (stream: any) {
155155
for (const token of tokens) {
156156
await stream.charge();
157157
yield token;
158158
}
159-
});
159+
} as any);
160160
}

src/pages/_api/api/demo/poem.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,10 @@ export default async function handler(request: Request) {
111111
const text = poem.join("\t");
112112
const tokens = tokenize(text);
113113

114-
return result.withReceipt(async function* (stream) {
114+
return result.withReceipt(async function* (stream: any) {
115115
for (const token of tokens) {
116116
await stream.charge();
117117
yield token;
118118
}
119-
});
119+
} as any);
120120
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { mppx } from "../../../../mppx-payment-link.server";
2+
3+
export async function GET(request: Request) {
4+
const result = await mppx.charge({
5+
amount: "0.01",
6+
description: "A random unique image",
7+
})(request);
8+
9+
if (result.status === 402) return result.challenge;
10+
11+
const res = await fetch("https://picsum.photos/1024/1024");
12+
const imageUrl = res.url;
13+
14+
const html = `<!doctype html>
15+
<html lang="en">
16+
<head>
17+
<meta charset="UTF-8" />
18+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
19+
<title>Photo — MPP Demo</title>
20+
<style>
21+
:root { color-scheme: dark light; }
22+
* { margin: 0; padding: 0; box-sizing: border-box; }
23+
body {
24+
min-height: 100vh;
25+
display: flex;
26+
flex-direction: column;
27+
align-items: center;
28+
justify-content: center;
29+
gap: 16px;
30+
padding: 32px;
31+
font-family: system-ui, -apple-system, sans-serif;
32+
background: light-dark(#fafafa, #0a0a0a);
33+
color: light-dark(#111, #eee);
34+
}
35+
img {
36+
max-width: 480px;
37+
width: 100%;
38+
border-radius: 12px;
39+
box-shadow: 0 4px 24px rgba(0,0,0,0.15);
40+
}
41+
p {
42+
font-size: 13px;
43+
color: light-dark(#666, #888);
44+
}
45+
</style>
46+
</head>
47+
<body>
48+
<img src="${imageUrl}" alt="Random photo from Picsum" />
49+
<p>Paid via MPP — $0.01</p>
50+
</body>
51+
</html>`;
52+
53+
return result.withReceipt(
54+
new Response(html, {
55+
headers: {
56+
"Cache-Control": "no-store",
57+
"Content-Type": "text/html; charset=utf-8",
58+
},
59+
}),
60+
);
61+
}

src/pages/_api/api/sessions/poem.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,11 @@ export default async function handler(request: Request) {
6969
const poem = poems[Math.floor(Math.random() * poems.length)];
7070
const words = poem.lines.flatMap((line) => [...line.split(" "), "\\n"]);
7171

72-
return result.withReceipt(async function* (stream) {
72+
return result.withReceipt(async function* (stream: any) {
7373
yield JSON.stringify({ title: poem.title, author: poem.author });
7474
for (const word of words) {
7575
await stream.charge();
7676
yield word;
7777
}
78-
});
78+
} as any);
7979
}

0 commit comments

Comments
 (0)