@emulators/stripe 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -191,12 +191,26 @@ function customerRoutes({ app, store, webhooks }) {
191
191
  });
192
192
  app.get("/v1/customers/:id", (c) => {
193
193
  const customer = ss.customers.findOneBy("stripe_id", c.req.param("id"));
194
- if (!customer) return stripeError(c, 404, "invalid_request_error", `No such customer: '${c.req.param("id")}'`, "resource_missing");
194
+ if (!customer)
195
+ return stripeError(
196
+ c,
197
+ 404,
198
+ "invalid_request_error",
199
+ `No such customer: '${c.req.param("id")}'`,
200
+ "resource_missing"
201
+ );
195
202
  return c.json(formatCustomer(customer));
196
203
  });
197
204
  app.post("/v1/customers/:id", async (c) => {
198
205
  const customer = ss.customers.findOneBy("stripe_id", c.req.param("id"));
199
- if (!customer) return stripeError(c, 404, "invalid_request_error", `No such customer: '${c.req.param("id")}'`, "resource_missing");
206
+ if (!customer)
207
+ return stripeError(
208
+ c,
209
+ 404,
210
+ "invalid_request_error",
211
+ `No such customer: '${c.req.param("id")}'`,
212
+ "resource_missing"
213
+ );
200
214
  const body = await parseStripeBody(c);
201
215
  const updated = ss.customers.update(customer.id, {
202
216
  ...body.email !== void 0 && { email: body.email },
@@ -214,7 +228,14 @@ function customerRoutes({ app, store, webhooks }) {
214
228
  });
215
229
  app.delete("/v1/customers/:id", async (c) => {
216
230
  const customer = ss.customers.findOneBy("stripe_id", c.req.param("id"));
217
- if (!customer) return stripeError(c, 404, "invalid_request_error", `No such customer: '${c.req.param("id")}'`, "resource_missing");
231
+ if (!customer)
232
+ return stripeError(
233
+ c,
234
+ 404,
235
+ "invalid_request_error",
236
+ `No such customer: '${c.req.param("id")}'`,
237
+ "resource_missing"
238
+ );
218
239
  for (const pi of ss.paymentIntents.findBy("customer_id", customer.stripe_id)) {
219
240
  ss.paymentIntents.update(pi.id, { customer_id: null });
220
241
  }
@@ -253,10 +274,24 @@ function paymentIntentRoutes({ app, store, webhooks }) {
253
274
  app.post("/v1/payment_intents", async (c) => {
254
275
  const body = await parseStripeBody(c);
255
276
  if (!body.amount || !body.currency) {
256
- return stripeError(c, 400, "invalid_request_error", "Missing required param: amount and currency are required.", void 0, "amount");
277
+ return stripeError(
278
+ c,
279
+ 400,
280
+ "invalid_request_error",
281
+ "Missing required param: amount and currency are required.",
282
+ void 0,
283
+ "amount"
284
+ );
257
285
  }
258
286
  if (body.customer && !ss.customers.findOneBy("stripe_id", body.customer)) {
259
- return stripeError(c, 400, "invalid_request_error", `No such customer: '${body.customer}'`, "resource_missing", "customer");
287
+ return stripeError(
288
+ c,
289
+ 400,
290
+ "invalid_request_error",
291
+ `No such customer: '${body.customer}'`,
292
+ "resource_missing",
293
+ "customer"
294
+ );
260
295
  }
261
296
  const status = body.payment_method ? "requires_confirmation" : "requires_payment_method";
262
297
  const pi = ss.paymentIntents.insert({
@@ -279,14 +314,28 @@ function paymentIntentRoutes({ app, store, webhooks }) {
279
314
  });
280
315
  app.get("/v1/payment_intents/:id", (c) => {
281
316
  const pi = ss.paymentIntents.findOneBy("stripe_id", c.req.param("id"));
282
- if (!pi) return stripeError(c, 404, "invalid_request_error", `No such payment_intent: '${c.req.param("id")}'`, "resource_missing");
317
+ if (!pi)
318
+ return stripeError(
319
+ c,
320
+ 404,
321
+ "invalid_request_error",
322
+ `No such payment_intent: '${c.req.param("id")}'`,
323
+ "resource_missing"
324
+ );
283
325
  const expand = parseExpand(c);
284
326
  const result = applyExpand(formatPaymentIntent(pi), expand, expandResolvers);
285
327
  return c.json(result);
286
328
  });
287
329
  app.post("/v1/payment_intents/:id", async (c) => {
288
330
  const pi = ss.paymentIntents.findOneBy("stripe_id", c.req.param("id"));
289
- if (!pi) return stripeError(c, 404, "invalid_request_error", `No such payment_intent: '${c.req.param("id")}'`, "resource_missing");
331
+ if (!pi)
332
+ return stripeError(
333
+ c,
334
+ 404,
335
+ "invalid_request_error",
336
+ `No such payment_intent: '${c.req.param("id")}'`,
337
+ "resource_missing"
338
+ );
290
339
  const body = await parseStripeBody(c);
291
340
  const updates = {};
292
341
  if (body.amount !== void 0) updates.amount = body.amount;
@@ -304,10 +353,23 @@ function paymentIntentRoutes({ app, store, webhooks }) {
304
353
  });
305
354
  app.post("/v1/payment_intents/:id/confirm", async (c) => {
306
355
  const pi = ss.paymentIntents.findOneBy("stripe_id", c.req.param("id"));
307
- if (!pi) return stripeError(c, 404, "invalid_request_error", `No such payment_intent: '${c.req.param("id")}'`, "resource_missing");
356
+ if (!pi)
357
+ return stripeError(
358
+ c,
359
+ 404,
360
+ "invalid_request_error",
361
+ `No such payment_intent: '${c.req.param("id")}'`,
362
+ "resource_missing"
363
+ );
308
364
  const body = await parseStripeBody(c);
309
365
  if (pi.status !== "requires_confirmation" && pi.status !== "requires_payment_method") {
310
- return stripeError(c, 400, "invalid_request_error", `This PaymentIntent's status is ${pi.status}, which does not allow confirmation.`, "payment_intent_unexpected_state");
366
+ return stripeError(
367
+ c,
368
+ 400,
369
+ "invalid_request_error",
370
+ `This PaymentIntent's status is ${pi.status}, which does not allow confirmation.`,
371
+ "payment_intent_unexpected_state"
372
+ );
311
373
  }
312
374
  if (body.payment_method) {
313
375
  ss.paymentIntents.update(pi.id, { payment_method: body.payment_method });
@@ -332,16 +394,40 @@ function paymentIntentRoutes({ app, store, webhooks }) {
332
394
  await webhooks.dispatch(
333
395
  "charge.succeeded",
334
396
  void 0,
335
- { type: "charge.succeeded", data: { object: { id: charge.stripe_id, object: "charge", amount: charge.amount, currency: charge.currency, status: charge.status } } },
397
+ {
398
+ type: "charge.succeeded",
399
+ data: {
400
+ object: {
401
+ id: charge.stripe_id,
402
+ object: "charge",
403
+ amount: charge.amount,
404
+ currency: charge.currency,
405
+ status: charge.status
406
+ }
407
+ }
408
+ },
336
409
  "stripe"
337
410
  );
338
411
  return c.json(formatPaymentIntent(updated));
339
412
  });
340
413
  app.post("/v1/payment_intents/:id/cancel", async (c) => {
341
414
  const pi = ss.paymentIntents.findOneBy("stripe_id", c.req.param("id"));
342
- if (!pi) return stripeError(c, 404, "invalid_request_error", `No such payment_intent: '${c.req.param("id")}'`, "resource_missing");
415
+ if (!pi)
416
+ return stripeError(
417
+ c,
418
+ 404,
419
+ "invalid_request_error",
420
+ `No such payment_intent: '${c.req.param("id")}'`,
421
+ "resource_missing"
422
+ );
343
423
  if (pi.status === "succeeded" || pi.status === "canceled") {
344
- return stripeError(c, 400, "invalid_request_error", `This PaymentIntent's status is ${pi.status}, which does not allow cancellation.`, "payment_intent_unexpected_state");
424
+ return stripeError(
425
+ c,
426
+ 400,
427
+ "invalid_request_error",
428
+ `This PaymentIntent's status is ${pi.status}, which does not allow cancellation.`,
429
+ "payment_intent_unexpected_state"
430
+ );
345
431
  }
346
432
  const updated = ss.paymentIntents.update(pi.id, { status: "canceled" });
347
433
  await webhooks.dispatch(
@@ -362,6 +448,33 @@ function paymentIntentRoutes({ app, store, webhooks }) {
362
448
  });
363
449
  }
364
450
 
451
+ // src/routes/payment-methods.ts
452
+ function paymentMethodRoutes({ app, store }) {
453
+ const ss = getStripeStore(store);
454
+ app.get("/v1/payment_methods", (c) => {
455
+ const customerId = c.req.query("customer");
456
+ if (customerId && !ss.customers.findOneBy("stripe_id", customerId)) {
457
+ return stripeError(
458
+ c,
459
+ 400,
460
+ "invalid_request_error",
461
+ `No such customer: '${customerId}'`,
462
+ "resource_missing",
463
+ "customer"
464
+ );
465
+ }
466
+ return c.json(
467
+ {
468
+ object: "list",
469
+ url: "/v1/payment_methods",
470
+ has_more: false,
471
+ data: []
472
+ },
473
+ 200
474
+ );
475
+ });
476
+ }
477
+
365
478
  // src/routes/charges.ts
366
479
  function formatCharge(ch) {
367
480
  return {
@@ -392,7 +505,8 @@ function chargeRoutes({ app, store }) {
392
505
  };
393
506
  app.get("/v1/charges/:id", (c) => {
394
507
  const charge = ss.charges.findOneBy("stripe_id", c.req.param("id"));
395
- if (!charge) return stripeError(c, 404, "invalid_request_error", `No such charge: '${c.req.param("id")}'`, "resource_missing");
508
+ if (!charge)
509
+ return stripeError(c, 404, "invalid_request_error", `No such charge: '${c.req.param("id")}'`, "resource_missing");
396
510
  const expand = parseExpand(c);
397
511
  const result = applyExpand(formatCharge(charge), expand, expandResolvers);
398
512
  return c.json(result);
@@ -424,7 +538,8 @@ function productRoutes({ app, store, webhooks }) {
424
538
  const ss = getStripeStore(store);
425
539
  app.post("/v1/products", async (c) => {
426
540
  const body = await parseStripeBody(c);
427
- if (!body.name) return stripeError(c, 400, "invalid_request_error", "Missing required param: name.", void 0, "name");
541
+ if (!body.name)
542
+ return stripeError(c, 400, "invalid_request_error", "Missing required param: name.", void 0, "name");
428
543
  const product = ss.products.insert({
429
544
  stripe_id: stripeId("prod"),
430
545
  name: body.name,
@@ -442,7 +557,14 @@ function productRoutes({ app, store, webhooks }) {
442
557
  });
443
558
  app.get("/v1/products/:id", (c) => {
444
559
  const product = ss.products.findOneBy("stripe_id", c.req.param("id"));
445
- if (!product) return stripeError(c, 404, "invalid_request_error", `No such product: '${c.req.param("id")}'`, "resource_missing");
560
+ if (!product)
561
+ return stripeError(
562
+ c,
563
+ 404,
564
+ "invalid_request_error",
565
+ `No such product: '${c.req.param("id")}'`,
566
+ "resource_missing"
567
+ );
446
568
  return c.json(formatProduct(product));
447
569
  });
448
570
  app.get("/v1/products", (c) => {
@@ -469,7 +591,14 @@ function formatPrice(p) {
469
591
  };
470
592
  }
471
593
  function formatProduct2(p) {
472
- return { id: p.stripe_id, object: "product", name: p.name, active: p.active, created: toUnixTimestamp(p.created_at), livemode: false };
594
+ return {
595
+ id: p.stripe_id,
596
+ object: "product",
597
+ name: p.name,
598
+ active: p.active,
599
+ created: toUnixTimestamp(p.created_at),
600
+ livemode: false
601
+ };
473
602
  }
474
603
  function priceRoutes({ app, store, webhooks }) {
475
604
  const ss = getStripeStore(store);
@@ -482,10 +611,24 @@ function priceRoutes({ app, store, webhooks }) {
482
611
  app.post("/v1/prices", async (c) => {
483
612
  const body = await parseStripeBody(c);
484
613
  if (!body.currency || !body.product) {
485
- return stripeError(c, 400, "invalid_request_error", "Missing required param: currency and product are required.", void 0, "currency");
614
+ return stripeError(
615
+ c,
616
+ 400,
617
+ "invalid_request_error",
618
+ "Missing required param: currency and product are required.",
619
+ void 0,
620
+ "currency"
621
+ );
486
622
  }
487
623
  if (!ss.products.findOneBy("stripe_id", body.product)) {
488
- return stripeError(c, 400, "invalid_request_error", `No such product: '${body.product}'`, "resource_missing", "product");
624
+ return stripeError(
625
+ c,
626
+ 400,
627
+ "invalid_request_error",
628
+ `No such product: '${body.product}'`,
629
+ "resource_missing",
630
+ "product"
631
+ );
489
632
  }
490
633
  const price = ss.prices.insert({
491
634
  stripe_id: stripeId("price"),
@@ -506,7 +649,8 @@ function priceRoutes({ app, store, webhooks }) {
506
649
  });
507
650
  app.get("/v1/prices/:id", (c) => {
508
651
  const price = ss.prices.findOneBy("stripe_id", c.req.param("id"));
509
- if (!price) return stripeError(c, 404, "invalid_request_error", `No such price: '${c.req.param("id")}'`, "resource_missing");
652
+ if (!price)
653
+ return stripeError(c, 404, "invalid_request_error", `No such price: '${c.req.param("id")}'`, "resource_missing");
510
654
  const expand = parseExpand(c);
511
655
  const result = applyExpand(formatPrice(price), expand, expandResolvers);
512
656
  return c.json(result);
@@ -542,6 +686,7 @@ var FONTS = {
542
686
  "geist-sans.woff2": readFileSync(join(__dirname, "fonts", "geist-sans.woff2")),
543
687
  "GeistPixel-Square.woff2": readFileSync(join(__dirname, "fonts", "GeistPixel-Square.woff2"))
544
688
  };
689
+ var FAVICON = readFileSync(join(__dirname, "fonts", "favicon.ico"));
545
690
  function escapeHtml(s) {
546
691
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
547
692
  }
@@ -693,6 +838,132 @@ body{
693
838
  .app-link-name{font-weight:600;font-size:.875rem;color:#33ff00;}
694
839
  .app-link-scopes{font-size:.6875rem;color:#1a8c00;margin-top:1px;}
695
840
  .empty{color:#1a8c00;text-align:center;padding:28px 0;font-size:.875rem;}
841
+
842
+ .inspector-layout{max-width:960px;margin:0 auto;padding:28px 20px;}
843
+ .inspector-tabs{display:flex;gap:4px;margin-bottom:20px;}
844
+ .inspector-tabs a{
845
+ padding:7px 16px;border-radius:6px;text-decoration:none;
846
+ font-size:.8125rem;color:#1a8c00;border:1px solid transparent;
847
+ transition:color .15s,border-color .15s;
848
+ }
849
+ .inspector-tabs a:hover{color:#33ff00;}
850
+ .inspector-tabs a.active{color:#33ff00;font-weight:600;border-color:#0a3300;background:#0a3300;}
851
+ .inspector-section{margin-bottom:24px;}
852
+ .inspector-section h2{
853
+ font-family:'Geist Pixel',monospace;
854
+ font-size:1rem;font-weight:600;color:#33ff00;margin-bottom:10px;
855
+ }
856
+ .inspector-section h3{
857
+ font-family:'Geist Pixel',monospace;
858
+ font-size:.875rem;font-weight:600;color:#1a8c00;margin:16px 0 8px;
859
+ }
860
+ .inspector-table{width:100%;border-collapse:collapse;margin-bottom:12px;}
861
+ .inspector-table th,.inspector-table td{
862
+ text-align:left;padding:8px 12px;border-bottom:1px solid #0a3300;
863
+ font-size:.8125rem;
864
+ }
865
+ .inspector-table th{color:#1a8c00;font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;}
866
+ .inspector-table td{color:#33ff00;}
867
+ .inspector-table tbody tr{transition:background .1s;}
868
+ .inspector-table tbody tr:hover{background:#0a3300;}
869
+ .inspector-empty{color:#1a8c00;text-align:center;padding:20px 0;font-size:.8125rem;}
870
+
871
+ .checkout-layout{
872
+ display:flex;min-height:calc(100vh - 42px);
873
+ }
874
+ .checkout-summary{
875
+ flex:1;background:#020;padding:48px 40px 48px 10%;
876
+ display:flex;flex-direction:column;justify-content:center;
877
+ border-right:1px solid #0a3300;
878
+ }
879
+ .checkout-form-side{
880
+ flex:1;background:#000;padding:48px 10% 48px 40px;
881
+ display:flex;flex-direction:column;justify-content:center;
882
+ }
883
+ .checkout-merchant{
884
+ display:flex;align-items:center;gap:10px;margin-bottom:6px;
885
+ }
886
+ .checkout-merchant-name{
887
+ font-family:'Geist Pixel',monospace;
888
+ font-size:.9375rem;font-weight:600;color:#33ff00;
889
+ }
890
+ .checkout-test-badge{
891
+ font-size:.625rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
892
+ background:#0a3300;color:#1a8c00;padding:2px 8px;border-radius:4px;
893
+ }
894
+ .checkout-total{
895
+ font-family:'Geist Pixel',monospace;
896
+ font-size:2rem;font-weight:700;color:#33ff00;margin:8px 0 28px;
897
+ }
898
+ .checkout-line-item{
899
+ display:flex;align-items:center;gap:14px;padding:14px 0;
900
+ border-bottom:1px solid #0a3300;
901
+ }
902
+ .checkout-line-item:first-child{border-top:1px solid #0a3300;}
903
+ .checkout-item-icon{
904
+ width:42px;height:42px;border-radius:6px;background:#0a3300;
905
+ display:flex;align-items:center;justify-content:center;flex-shrink:0;
906
+ font-family:'Geist Pixel',monospace;font-size:.875rem;font-weight:700;color:#116600;
907
+ }
908
+ .checkout-item-details{flex:1;min-width:0;}
909
+ .checkout-item-name{font-size:.875rem;font-weight:600;color:#33ff00;}
910
+ .checkout-item-qty{font-size:.75rem;color:#1a8c00;margin-top:2px;}
911
+ .checkout-item-price{
912
+ font-size:.875rem;font-weight:600;color:#33ff00;text-align:right;white-space:nowrap;
913
+ }
914
+ .checkout-item-unit{font-size:.6875rem;color:#1a8c00;text-align:right;margin-top:2px;}
915
+ .checkout-totals{margin-top:20px;}
916
+ .checkout-totals-row{
917
+ display:flex;justify-content:space-between;padding:6px 0;
918
+ font-size:.8125rem;color:#1a8c00;
919
+ }
920
+ .checkout-totals-row.total{
921
+ border-top:1px solid #0a3300;margin-top:8px;padding-top:14px;
922
+ font-size:.9375rem;font-weight:600;color:#33ff00;
923
+ }
924
+ .checkout-form-section{margin-bottom:24px;}
925
+ .checkout-form-label{
926
+ font-size:.8125rem;font-weight:600;color:#33ff00;margin-bottom:8px;display:block;
927
+ }
928
+ .checkout-input{
929
+ width:100%;padding:10px 12px;border:1px solid #0a3300;border-radius:6px;
930
+ background:#020;color:#33ff00;font:inherit;font-size:.875rem;
931
+ transition:border-color .15s;outline:none;
932
+ }
933
+ .checkout-input:focus{border-color:#33ff00;}
934
+ .checkout-input::placeholder{color:#116600;}
935
+ .checkout-card-box{
936
+ border:1px solid #0a3300;border-radius:6px;padding:14px;
937
+ background:#020;
938
+ }
939
+ .checkout-card-row{
940
+ display:flex;gap:12px;margin-top:10px;
941
+ }
942
+ .checkout-card-row .checkout-input{flex:1;}
943
+ .checkout-sim-note{
944
+ font-size:.6875rem;color:#1a8c00;margin-top:10px;text-align:center;
945
+ font-style:italic;
946
+ }
947
+ .checkout-pay-btn{
948
+ width:100%;padding:14px;border:none;border-radius:8px;
949
+ background:#33ff00;color:#000;font:inherit;font-size:.9375rem;font-weight:700;
950
+ cursor:pointer;transition:background .15s;
951
+ font-family:'Geist Pixel',monospace;
952
+ }
953
+ .checkout-pay-btn:hover{background:#44ff22;}
954
+ .checkout-cancel{
955
+ text-align:center;margin-top:14px;
956
+ }
957
+ .checkout-cancel a{
958
+ color:#1a8c00;text-decoration:none;font-size:.8125rem;
959
+ transition:color .15s;
960
+ }
961
+ .checkout-cancel a:hover{color:#33ff00;}
962
+ @media(max-width:768px){
963
+ .checkout-layout{flex-direction:column;}
964
+ .checkout-summary{padding:32px 20px;border-right:none;border-bottom:1px solid #0a3300;}
965
+ .checkout-form-side{padding:32px 20px;}
966
+ }
696
967
  `;
697
968
  var POWERED_BY = `<div class="powered-by">Powered by <a href="https://emulate.dev" target="_blank" rel="noopener">emulate</a></div>`;
698
969
  function emuBar(service) {
@@ -712,6 +983,7 @@ function head(title) {
712
983
  <head>
713
984
  <meta charset="utf-8"/>
714
985
  <meta name="viewport" content="width=device-width,initial-scale=1"/>
986
+ <link rel="icon" href="/_emulate/favicon.ico"/>
715
987
  <title>${escapeHtml(title)} | emulate</title>
716
988
  <style>${CSS}</style>
717
989
  </head>`;
@@ -730,6 +1002,72 @@ ${emuBar(service)}
730
1002
  ${POWERED_BY}
731
1003
  </body></html>`;
732
1004
  }
1005
+ function renderCheckoutPage(opts, service) {
1006
+ const fmt = (cents, cur) => `$${(cents / 100).toFixed(2)} ${cur.toUpperCase()}`;
1007
+ const fmtShort = (cents) => `$${(cents / 100).toFixed(2)}`;
1008
+ const itemsHtml = opts.lineItems.length > 0 ? opts.lineItems.map((li) => {
1009
+ const initial = li.name.charAt(0).toUpperCase();
1010
+ const unitNote = li.quantity > 1 ? `<div class="checkout-item-unit">${fmtShort(li.unitPrice)} each</div>` : "";
1011
+ return `<div class="checkout-line-item">
1012
+ <div class="checkout-item-icon">${escapeHtml(initial)}</div>
1013
+ <div class="checkout-item-details">
1014
+ <div class="checkout-item-name">${escapeHtml(li.name)}</div>
1015
+ <div class="checkout-item-qty">Qty ${li.quantity}</div>
1016
+ </div>
1017
+ <div>
1018
+ <div class="checkout-item-price">${fmtShort(li.totalPrice)}</div>
1019
+ ${unitNote}
1020
+ </div>
1021
+ </div>`;
1022
+ }).join("") : '<p class="empty">No line items</p>';
1023
+ const totalsHtml = `<div class="checkout-totals">
1024
+ <div class="checkout-totals-row">
1025
+ <span>Subtotal</span><span>${fmtShort(opts.subtotal)}</span>
1026
+ </div>
1027
+ <div class="checkout-totals-row total">
1028
+ <span>Total due</span><span>${fmt(opts.total, opts.currency)}</span>
1029
+ </div>
1030
+ </div>`;
1031
+ const cancelHtml = opts.cancelUrl ? `<div class="checkout-cancel"><a href="${escapeAttr(opts.cancelUrl)}">Cancel</a></div>` : "";
1032
+ const merchant = opts.merchantName ? escapeHtml(opts.merchantName) : "Checkout";
1033
+ return `${head("Checkout")}
1034
+ <body>
1035
+ ${emuBar(service)}
1036
+ <div class="checkout-layout">
1037
+ <div class="checkout-summary">
1038
+ <div class="checkout-merchant">
1039
+ <span class="checkout-merchant-name">${merchant}</span>
1040
+ <span class="checkout-test-badge">Test Mode</span>
1041
+ </div>
1042
+ <div class="checkout-total">${fmtShort(opts.total)}</div>
1043
+ ${itemsHtml}
1044
+ ${totalsHtml}
1045
+ </div>
1046
+ <div class="checkout-form-side">
1047
+ <form method="post" action="/checkout/${escapeAttr(opts.sessionId)}/complete">
1048
+ <div class="checkout-form-section">
1049
+ <label class="checkout-form-label">Email</label>
1050
+ <input type="email" name="email" class="checkout-input" placeholder="you@example.com"/>
1051
+ </div>
1052
+ <div class="checkout-form-section">
1053
+ <label class="checkout-form-label">Card information</label>
1054
+ <div class="checkout-card-box">
1055
+ <input type="text" class="checkout-input" placeholder="1234 1234 1234 1234" disabled/>
1056
+ <div class="checkout-card-row">
1057
+ <input type="text" class="checkout-input" placeholder="MM / YY" disabled/>
1058
+ <input type="text" class="checkout-input" placeholder="CVC" disabled/>
1059
+ </div>
1060
+ </div>
1061
+ <div class="checkout-sim-note">Card fields are simulated. Payment will be auto-approved.</div>
1062
+ </div>
1063
+ <button type="submit" class="checkout-pay-btn">Pay ${fmtShort(opts.total)}</button>
1064
+ </form>
1065
+ ${cancelHtml}
1066
+ </div>
1067
+ </div>
1068
+ ${POWERED_BY}
1069
+ </body></html>`;
1070
+ }
733
1071
 
734
1072
  // src/routes/checkout-sessions.ts
735
1073
  var SERVICE_LABEL = "Stripe";
@@ -753,9 +1091,17 @@ function checkoutSessionRoutes({ app, store, webhooks, baseUrl }) {
753
1091
  const ss = getStripeStore(store);
754
1092
  app.post("/v1/checkout/sessions", async (c) => {
755
1093
  const body = await parseStripeBody(c);
756
- if (!body.mode) return stripeError(c, 400, "invalid_request_error", "Missing required param: mode.", void 0, "mode");
1094
+ if (!body.mode)
1095
+ return stripeError(c, 400, "invalid_request_error", "Missing required param: mode.", void 0, "mode");
757
1096
  if (body.customer && !ss.customers.findOneBy("stripe_id", body.customer)) {
758
- return stripeError(c, 400, "invalid_request_error", `No such customer: '${body.customer}'`, "resource_missing", "customer");
1097
+ return stripeError(
1098
+ c,
1099
+ 400,
1100
+ "invalid_request_error",
1101
+ `No such customer: '${body.customer}'`,
1102
+ "resource_missing",
1103
+ "customer"
1104
+ );
759
1105
  }
760
1106
  const lineItems = [];
761
1107
  if (body.line_items) {
@@ -765,17 +1111,45 @@ function checkoutSessionRoutes({ app, store, webhooks, baseUrl }) {
765
1111
  for (let i = 0; i < body.line_items.length; i++) {
766
1112
  const li = body.line_items[i];
767
1113
  if (!li || typeof li !== "object") {
768
- return stripeError(c, 400, "invalid_request_error", `Invalid line_items[${i}]: must be an object.`, void 0, `line_items[${i}]`);
1114
+ return stripeError(
1115
+ c,
1116
+ 400,
1117
+ "invalid_request_error",
1118
+ `Invalid line_items[${i}]: must be an object.`,
1119
+ void 0,
1120
+ `line_items[${i}]`
1121
+ );
769
1122
  }
770
1123
  if (!li.price || typeof li.price !== "string") {
771
- return stripeError(c, 400, "invalid_request_error", `Missing required param: line_items[${i}][price].`, void 0, `line_items[${i}][price]`);
1124
+ return stripeError(
1125
+ c,
1126
+ 400,
1127
+ "invalid_request_error",
1128
+ `Missing required param: line_items[${i}][price].`,
1129
+ void 0,
1130
+ `line_items[${i}][price]`
1131
+ );
772
1132
  }
773
1133
  if (!ss.prices.findOneBy("stripe_id", li.price)) {
774
- return stripeError(c, 400, "invalid_request_error", `No such price: '${li.price}'`, "resource_missing", `line_items[${i}][price]`);
1134
+ return stripeError(
1135
+ c,
1136
+ 400,
1137
+ "invalid_request_error",
1138
+ `No such price: '${li.price}'`,
1139
+ "resource_missing",
1140
+ `line_items[${i}][price]`
1141
+ );
775
1142
  }
776
1143
  const qty = typeof li.quantity === "number" ? li.quantity : parseInt(li.quantity, 10);
777
1144
  if (!Number.isFinite(qty) || qty < 1) {
778
- return stripeError(c, 400, "invalid_request_error", `Invalid line_items[${i}][quantity]: must be a positive integer.`, void 0, `line_items[${i}][quantity]`);
1145
+ return stripeError(
1146
+ c,
1147
+ 400,
1148
+ "invalid_request_error",
1149
+ `Invalid line_items[${i}][quantity]: must be a positive integer.`,
1150
+ void 0,
1151
+ `line_items[${i}][quantity]`
1152
+ );
779
1153
  }
780
1154
  lineItems.push({ price: li.price, quantity: qty });
781
1155
  }
@@ -795,14 +1169,34 @@ function checkoutSessionRoutes({ app, store, webhooks, baseUrl }) {
795
1169
  });
796
1170
  app.get("/v1/checkout/sessions/:id", (c) => {
797
1171
  const session = ss.checkoutSessions.findOneBy("stripe_id", c.req.param("id"));
798
- if (!session) return stripeError(c, 404, "invalid_request_error", `No such checkout session: '${c.req.param("id")}'`, "resource_missing");
1172
+ if (!session)
1173
+ return stripeError(
1174
+ c,
1175
+ 404,
1176
+ "invalid_request_error",
1177
+ `No such checkout session: '${c.req.param("id")}'`,
1178
+ "resource_missing"
1179
+ );
799
1180
  return c.json(formatSession(session, baseUrl));
800
1181
  });
801
1182
  app.post("/v1/checkout/sessions/:id/expire", async (c) => {
802
1183
  const session = ss.checkoutSessions.findOneBy("stripe_id", c.req.param("id"));
803
- if (!session) return stripeError(c, 404, "invalid_request_error", `No such checkout session: '${c.req.param("id")}'`, "resource_missing");
1184
+ if (!session)
1185
+ return stripeError(
1186
+ c,
1187
+ 404,
1188
+ "invalid_request_error",
1189
+ `No such checkout session: '${c.req.param("id")}'`,
1190
+ "resource_missing"
1191
+ );
804
1192
  if (session.status !== "open") {
805
- return stripeError(c, 400, "invalid_request_error", "Only open sessions can be expired.", "checkout_session_not_open");
1193
+ return stripeError(
1194
+ c,
1195
+ 400,
1196
+ "invalid_request_error",
1197
+ "Only open sessions can be expired.",
1198
+ "checkout_session_not_open"
1199
+ );
806
1200
  }
807
1201
  const updated = ss.checkoutSessions.update(session.id, { status: "expired" });
808
1202
  await webhooks.dispatch(
@@ -826,35 +1220,53 @@ function checkoutSessionRoutes({ app, store, webhooks, baseUrl }) {
826
1220
  app.get("/checkout/:id", (c) => {
827
1221
  const session = ss.checkoutSessions.findOneBy("stripe_id", c.req.param("id"));
828
1222
  if (!session) {
829
- return c.html(renderCardPage("Session Not Found", "This checkout session does not exist.", '<p class="empty">The session ID is invalid or has been removed.</p>', SERVICE_LABEL), 404);
1223
+ return c.html(
1224
+ renderCardPage(
1225
+ "Session Not Found",
1226
+ "This checkout session does not exist.",
1227
+ '<p class="empty">The session ID is invalid or has been removed.</p>',
1228
+ SERVICE_LABEL
1229
+ ),
1230
+ 404
1231
+ );
830
1232
  }
831
1233
  if (session.status !== "open") {
832
- return c.html(renderCardPage("Session Expired", "This checkout session is no longer available.", `<p class="empty">Status: ${escapeHtml(session.status)}</p>`, SERVICE_LABEL));
1234
+ return c.html(
1235
+ renderCardPage(
1236
+ "Session Expired",
1237
+ "This checkout session is no longer available.",
1238
+ `<p class="empty">Status: ${escapeHtml(session.status)}</p>`,
1239
+ SERVICE_LABEL
1240
+ )
1241
+ );
833
1242
  }
834
- const lineItemsHtml = session.line_items.length > 0 ? session.line_items.map((li) => {
1243
+ const lineItems = session.line_items.map((li) => {
835
1244
  const priceObj = ss.prices.findOneBy("stripe_id", li.price);
836
1245
  const product = priceObj ? ss.products.findOneBy("stripe_id", priceObj.product_id) : null;
837
- const name = product?.name ?? li.price;
838
- const amount = priceObj ? `$${(priceObj.unit_amount / 100).toFixed(2)} ${priceObj.currency.toUpperCase()}` : "";
839
- return `<div class="org-row">
840
- <span class="org-icon">$</span>
841
- <span class="org-name">${escapeHtml(name)}</span>
842
- <span class="emu-bar-service">${escapeHtml(amount)} x ${li.quantity}</span>
843
- </div>`;
844
- }).join("") : '<p class="empty">No line items</p>';
845
- const body = `
846
- ${lineItemsHtml}
847
- <form class="user-form" method="post" action="/checkout/${escapeAttr(session.stripe_id)}/complete">
848
- <button type="submit" class="user-btn">
849
- <span class="avatar">$</span>
850
- <span class="user-text">
851
- <span class="user-login">Pay and Complete</span>
852
- </span>
853
- </button>
854
- </form>
855
- ${session.cancel_url ? `<p class="info-text"><a href="${escapeAttr(session.cancel_url)}" class="btn-revoke">Cancel</a></p>` : ""}
856
- `;
857
- return c.html(renderCardPage("Checkout", `Complete your ${escapeHtml(session.mode)} payment.`, body, SERVICE_LABEL));
1246
+ const unitPrice = priceObj?.unit_amount ?? 0;
1247
+ return {
1248
+ name: product?.name ?? li.price,
1249
+ quantity: li.quantity,
1250
+ unitPrice,
1251
+ totalPrice: unitPrice * li.quantity,
1252
+ currency: priceObj?.currency ?? "usd"
1253
+ };
1254
+ });
1255
+ const subtotal = lineItems.reduce((sum, li) => sum + li.totalPrice, 0);
1256
+ const currency = lineItems.length > 0 ? lineItems[0].currency : "usd";
1257
+ return c.html(
1258
+ renderCheckoutPage(
1259
+ {
1260
+ lineItems,
1261
+ subtotal,
1262
+ total: subtotal,
1263
+ currency,
1264
+ sessionId: session.stripe_id,
1265
+ cancelUrl: session.cancel_url
1266
+ },
1267
+ SERVICE_LABEL
1268
+ )
1269
+ );
858
1270
  });
859
1271
  app.post("/checkout/:id/complete", async (c) => {
860
1272
  const session = ss.checkoutSessions.findOneBy("stripe_id", c.req.param("id"));
@@ -869,9 +1281,49 @@ function checkoutSessionRoutes({ app, store, webhooks, baseUrl }) {
869
1281
  "stripe"
870
1282
  );
871
1283
  if (session.success_url) {
872
- return c.redirect(session.success_url);
1284
+ const url = session.success_url.replace("{CHECKOUT_SESSION_ID}", updated.stripe_id);
1285
+ return c.redirect(url);
873
1286
  }
874
- return c.html(renderCardPage("Payment Complete", "Your payment was successful.", '<p class="empty check">Payment received</p>', SERVICE_LABEL));
1287
+ return c.html(
1288
+ renderCardPage(
1289
+ "Payment Complete",
1290
+ "Your payment was successful.",
1291
+ '<p class="empty check">Payment received</p>',
1292
+ SERVICE_LABEL
1293
+ )
1294
+ );
1295
+ });
1296
+ }
1297
+
1298
+ // src/routes/customer-sessions.ts
1299
+ function customerSessionRoutes({ app, store }) {
1300
+ const ss = getStripeStore(store);
1301
+ app.post("/v1/customer_sessions", async (c) => {
1302
+ const body = await parseStripeBody(c);
1303
+ if (!body.customer)
1304
+ return stripeError(c, 400, "invalid_request_error", "Missing required param: customer.", void 0, "customer");
1305
+ const customer = ss.customers.findOneBy("stripe_id", body.customer);
1306
+ if (!customer)
1307
+ return stripeError(
1308
+ c,
1309
+ 400,
1310
+ "invalid_request_error",
1311
+ `No such customer: '${body.customer}'`,
1312
+ "resource_missing",
1313
+ "customer"
1314
+ );
1315
+ return c.json(
1316
+ {
1317
+ object: "customer_session",
1318
+ client_secret: stripeId("cuss_secret"),
1319
+ components: body.components ?? {},
1320
+ created: Math.floor(Date.now() / 1e3),
1321
+ customer: customer.stripe_id,
1322
+ expires_at: Math.floor(Date.now() / 1e3) + 1800,
1323
+ livemode: false
1324
+ },
1325
+ 200
1326
+ );
875
1327
  });
876
1328
  }
877
1329
 
@@ -886,7 +1338,7 @@ function seedDefaults(store, _baseUrl) {
886
1338
  metadata: {}
887
1339
  });
888
1340
  }
889
- function seedFromConfig(store, _baseUrl, config) {
1341
+ function seedFromConfig(store, _baseUrl, config, webhooks) {
890
1342
  const ss = getStripeStore(store);
891
1343
  if (config.customers) {
892
1344
  for (const c of config.customers) {
@@ -895,7 +1347,7 @@ function seedFromConfig(store, _baseUrl, config) {
895
1347
  if (existing) continue;
896
1348
  }
897
1349
  ss.customers.insert({
898
- stripe_id: stripeId("cus"),
1350
+ stripe_id: c.id ?? stripeId("cus"),
899
1351
  email: c.email ?? null,
900
1352
  name: c.name ?? null,
901
1353
  description: c.description ?? null,
@@ -906,7 +1358,7 @@ function seedFromConfig(store, _baseUrl, config) {
906
1358
  if (config.products) {
907
1359
  for (const p of config.products) {
908
1360
  const product = ss.products.insert({
909
- stripe_id: stripeId("prod"),
1361
+ stripe_id: p.id ?? stripeId("prod"),
910
1362
  name: p.name,
911
1363
  description: p.description ?? null,
912
1364
  active: true,
@@ -915,7 +1367,7 @@ function seedFromConfig(store, _baseUrl, config) {
915
1367
  const matchingPrices = config.prices?.filter((pr) => pr.product_name === p.name) ?? [];
916
1368
  for (const pr of matchingPrices) {
917
1369
  ss.prices.insert({
918
- stripe_id: stripeId("price"),
1370
+ stripe_id: pr.id ?? stripeId("price"),
919
1371
  product_id: product.stripe_id,
920
1372
  currency: pr.currency.toLowerCase(),
921
1373
  unit_amount: pr.unit_amount,
@@ -926,17 +1378,30 @@ function seedFromConfig(store, _baseUrl, config) {
926
1378
  }
927
1379
  }
928
1380
  }
1381
+ if (config.webhooks && webhooks) {
1382
+ for (const wh of config.webhooks) {
1383
+ webhooks.register({
1384
+ url: wh.url,
1385
+ events: wh.events,
1386
+ active: true,
1387
+ secret: wh.secret,
1388
+ owner: "stripe"
1389
+ });
1390
+ }
1391
+ }
929
1392
  }
930
1393
  var stripePlugin = {
931
1394
  name: "stripe",
932
1395
  register(app, store, webhooks, baseUrl, tokenMap) {
933
1396
  const ctx = { app, store, webhooks, baseUrl, tokenMap };
934
1397
  customerRoutes(ctx);
1398
+ paymentMethodRoutes(ctx);
935
1399
  paymentIntentRoutes(ctx);
936
1400
  chargeRoutes(ctx);
937
1401
  productRoutes(ctx);
938
1402
  priceRoutes(ctx);
939
1403
  checkoutSessionRoutes(ctx);
1404
+ customerSessionRoutes(ctx);
940
1405
  },
941
1406
  seed(store, baseUrl) {
942
1407
  seedDefaults(store, baseUrl);