Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5559783
Add missing @types/node to docs devDependencies
Segfaultd Apr 10, 2026
18aaed1
Expand unit test suite from 512 to 3176 tests
Segfaultd Apr 10, 2026
6052ad1
Add real-world SDK simulation tests using official Stripe client
Segfaultd Apr 10, 2026
82fbf8d
Fix refund amount parsing, expand param format, and add tok_chargeDec…
Segfaultd Apr 10, 2026
df32059
Fix cursor-based pagination with composite (created, id) tiebreaker
Segfaultd Apr 10, 2026
6309334
Strengthen pagination tests now that composite cursors work
Segfaultd Apr 10, 2026
13f3bca
Remove dead import and unreachable branch from review cleanup
Segfaultd Apr 10, 2026
f08b9e5
Add design spec for demo e-commerce app
Segfaultd Apr 10, 2026
678bc75
Add implementation plan for demo e-commerce app
Segfaultd Apr 10, 2026
f8926e9
Scaffold Astro demo app with Node SSR adapter
Segfaultd Apr 10, 2026
94dff08
Add Stripe SDK client and product catalog with lazy bootstrap
Segfaultd Apr 10, 2026
e0edf95
Add shared Layout component with header, nav, and footer
Segfaultd Apr 10, 2026
30d2a8d
Add product listing page with grid layout
Segfaultd Apr 10, 2026
05602e1
Add CardForm component with test card selector and 3DS modal
Segfaultd Apr 10, 2026
f15864d
Add checkout page with order summary and payment form
Segfaultd Apr 10, 2026
9f2d5fe
Add /api/pay endpoint with full payment flow
Segfaultd Apr 10, 2026
2752d11
Add /api/confirm endpoint for 3DS re-confirmation
Segfaultd Apr 10, 2026
74c56f9
Add success and failed result pages
Segfaultd Apr 10, 2026
da3972d
Add orchestration script to start Strimulator and demo together
Segfaultd Apr 10, 2026
1502d54
Remove dead failed.astro page, fix 3DS button state reset
Segfaultd Apr 10, 2026
d4588cb
Fix bootstrap race condition, revert unrelated docs basePath removal
Segfaultd Apr 10, 2026
6ddfd1e
Merge branch 'main' into demo-app
Segfaultd Apr 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions demo/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from "astro/config";
import node from "@astrojs/node";

export default defineConfig({
output: "server",
adapter: node({ mode: "standalone" }),
server: { port: 4321 },
});
805 changes: 805 additions & 0 deletions demo/bun.lock

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "strimulator-demo",
"type": "module",
"private": true,
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview"
},
"dependencies": {
"astro": "^5.7.10",
"@astrojs/node": "^9.1.3",
"stripe": "^22.0.1"
}
}
271 changes: 271 additions & 0 deletions demo/src/components/CardForm.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
---
interface Props {
amount: string;
productIndex: number;
}

const { amount, productIndex } = Astro.props;

const testCards = [
{ label: "Visa (success)", token: "tok_visa", number: "4242 4242 4242 4242", brand: "visa" },
{ label: "Mastercard (success)", token: "tok_mastercard", number: "5555 5555 5555 4444", brand: "mastercard" },
{ label: "Declined card", token: "tok_chargeDeclined", number: "4000 0000 0000 0002", brand: "visa" },
{ label: "3DS Required", token: "tok_threeDSecureRequired", number: "4000 0000 0000 3220", brand: "visa" },
];
---

<form id="payment-form">
<input type="hidden" name="productIndex" value={productIndex} />
<input type="hidden" name="token" id="token-input" value={testCards[0].token} />

<div class="field">
<label for="card-select">Test Card</label>
<select id="card-select" name="cardSelect">
{testCards.map((card) => (
<option value={card.token} data-number={card.number}>{card.label}</option>
))}
</select>
</div>

<div class="field">
<label>Card Number</label>
<input type="text" id="card-number" value={testCards[0].number} readonly />
</div>

<div class="field-row">
<div class="field">
<label>Expiry</label>
<input type="text" value="12 / 34" readonly />
</div>
<div class="field">
<label>CVC</label>
<input type="text" value="123" readonly />
</div>
</div>

<button type="submit" id="pay-btn">
<span id="btn-text">Pay {amount}</span>
<span id="btn-spinner" class="hidden">Processing...</span>
</button>

<div id="error-message" class="hidden"></div>
</form>

<!-- 3DS modal -->
<div id="three-ds-modal" class="modal-overlay hidden">
<div class="modal">
<h3>3D Secure Authentication</h3>
<p>Your bank requires additional verification for this payment.</p>
<button id="three-ds-authorize">Authorize Payment</button>
</div>
</div>

<style>
form {
display: flex;
flex-direction: column;
gap: 1rem;
}

.field {
display: flex;
flex-direction: column;
gap: 0.35rem;
}

.field label {
font-size: 0.85rem;
font-weight: 500;
color: #444;
}

.field input,
.field select {
padding: 0.7rem;
border: 1px solid #d0d0d8;
border-radius: 6px;
font-size: 0.95rem;
background: white;
color: #1a1a2e;
transition: border-color 0.15s, box-shadow 0.15s;
}

.field input:focus,
.field select:focus {
outline: none;
border-color: #5469d4;
box-shadow: 0 0 0 3px rgba(84, 105, 212, 0.15);
}

.field input[readonly] {
background: #fafafa;
color: #666;
}

.field-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}

button[type="submit"] {
margin-top: 0.5rem;
padding: 0.85rem;
background: #5469d4;
color: white;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}

button[type="submit"]:hover {
background: #4257b2;
}

button[type="submit"]:disabled {
background: #9aa5ce;
cursor: not-allowed;
}

#error-message {
color: #df1b41;
font-size: 0.9rem;
padding: 0.75rem;
background: #fef2f4;
border-radius: 6px;
}

.hidden {
display: none;
}

.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}

.modal {
background: white;
padding: 2rem;
border-radius: 12px;
max-width: 400px;
text-align: center;
}

.modal h3 {
margin-bottom: 0.75rem;
}

.modal p {
color: #555;
margin-bottom: 1.5rem;
font-size: 0.95rem;
}

.modal button {
padding: 0.75rem 2rem;
background: #5469d4;
color: white;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
}

.modal button:hover {
background: #4257b2;
}
</style>

<script>
const form = document.getElementById("payment-form") as HTMLFormElement;
const select = document.getElementById("card-select") as HTMLSelectElement;
const cardNumber = document.getElementById("card-number") as HTMLInputElement;
const tokenInput = document.getElementById("token-input") as HTMLInputElement;
const payBtn = document.getElementById("pay-btn") as HTMLButtonElement;
const btnText = document.getElementById("btn-text")!;
const btnSpinner = document.getElementById("btn-spinner")!;
const errorMessage = document.getElementById("error-message")!;
const modal = document.getElementById("three-ds-modal")!;
const authorizeBtn = document.getElementById("three-ds-authorize")!;

select.addEventListener("change", () => {
const option = select.selectedOptions[0];
cardNumber.value = option.dataset.number ?? "";
tokenInput.value = select.value;
});

form.addEventListener("submit", async (e) => {
e.preventDefault();
errorMessage.classList.add("hidden");
payBtn.disabled = true;
btnText.classList.add("hidden");
btnSpinner.classList.remove("hidden");

const body = {
token: tokenInput.value,
productIndex: parseInt(new FormData(form).get("productIndex") as string, 10),
};

try {
const res = await fetch("/api/pay", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await res.json();

if (data.success) {
window.location.href = `/success?payment_intent=${data.paymentIntentId}&amount=${data.amount}`;
} else if (data.requires_action) {
// Show 3DS modal
payBtn.disabled = false;
btnText.classList.remove("hidden");
btnSpinner.classList.add("hidden");
modal.classList.remove("hidden");

authorizeBtn.onclick = async () => {
authorizeBtn.textContent = "Authorizing...";
(authorizeBtn as HTMLButtonElement).disabled = true;
const confirmRes = await fetch("/api/confirm", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ paymentIntentId: data.paymentIntentId }),
});
const confirmData = await confirmRes.json();
if (confirmData.success) {
window.location.href = `/success?payment_intent=${confirmData.paymentIntentId}&amount=${confirmData.amount}`;
} else {
modal.classList.add("hidden");
authorizeBtn.textContent = "Authorize Payment";
(authorizeBtn as HTMLButtonElement).disabled = false;
errorMessage.textContent = confirmData.error || "3DS authorization failed.";
errorMessage.classList.remove("hidden");
}
};
} else {
errorMessage.textContent = data.error || "Payment failed.";
errorMessage.classList.remove("hidden");
}
} catch {
errorMessage.textContent = "Network error. Is Strimulator running?";
errorMessage.classList.remove("hidden");
} finally {
payBtn.disabled = false;
btnText.classList.remove("hidden");
btnSpinner.classList.add("hidden");
}
});
</script>
99 changes: 99 additions & 0 deletions demo/src/layouts/Layout.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
interface Props {
title: string;
}

const { title } = Astro.props;
---

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title} — Strimulator Demo</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: #1a1a2e;
background: #f7f7fa;
min-height: 100vh;
display: flex;
flex-direction: column;
}

header {
background: #1a1a2e;
color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}

header a {
color: white;
text-decoration: none;
font-weight: 600;
font-size: 1.1rem;
}

header nav a {
font-weight: 400;
font-size: 0.9rem;
opacity: 0.8;
margin-left: 1.5rem;
}

header nav a:hover {
opacity: 1;
}

main {
flex: 1;
max-width: 960px;
margin: 2rem auto;
padding: 0 1.5rem;
width: 100%;
}

footer {
text-align: center;
padding: 1.5rem;
font-size: 0.85rem;
color: #666;
border-top: 1px solid #e0e0e0;
}

footer a {
color: #5469d4;
text-decoration: none;
}

footer a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<header>
<a href="/">Strimulator Demo Shop</a>
<nav>
<a href="/">Products</a>
<a href="http://localhost:12111/dashboard" target="_blank">Dashboard</a>
</nav>
</header>
<main>
<slot />
</main>
<footer>
Powered by <a href="http://localhost:12111/dashboard" target="_blank">Strimulator</a> — a local Stripe API simulator
</footer>
</body>
</html>
Loading
Loading