@emulators/stripe 0.4.1 → 0.6.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);
@@ -522,8 +666,6 @@ function priceRoutes({ app, store, webhooks }) {
522
666
  }
523
667
 
524
668
  // ../core/dist/index.js
525
- import { Hono } from "hono";
526
- import { cors } from "hono/cors";
527
669
  import { readFileSync } from "fs";
528
670
  import { fileURLToPath } from "url";
529
671
  import { dirname, join } from "path";
@@ -542,6 +684,7 @@ var FONTS = {
542
684
  "geist-sans.woff2": readFileSync(join(__dirname, "fonts", "geist-sans.woff2")),
543
685
  "GeistPixel-Square.woff2": readFileSync(join(__dirname, "fonts", "GeistPixel-Square.woff2"))
544
686
  };
687
+ var FAVICON = readFileSync(join(__dirname, "fonts", "favicon.ico"));
545
688
  function escapeHtml(s) {
546
689
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
547
690
  }
@@ -693,6 +836,132 @@ body{
693
836
  .app-link-name{font-weight:600;font-size:.875rem;color:#33ff00;}
694
837
  .app-link-scopes{font-size:.6875rem;color:#1a8c00;margin-top:1px;}
695
838
  .empty{color:#1a8c00;text-align:center;padding:28px 0;font-size:.875rem;}
839
+
840
+ .inspector-layout{max-width:960px;margin:0 auto;padding:28px 20px;}
841
+ .inspector-tabs{display:flex;gap:4px;margin-bottom:20px;}
842
+ .inspector-tabs a{
843
+ padding:7px 16px;border-radius:6px;text-decoration:none;
844
+ font-size:.8125rem;color:#1a8c00;border:1px solid transparent;
845
+ transition:color .15s,border-color .15s;
846
+ }
847
+ .inspector-tabs a:hover{color:#33ff00;}
848
+ .inspector-tabs a.active{color:#33ff00;font-weight:600;border-color:#0a3300;background:#0a3300;}
849
+ .inspector-section{margin-bottom:24px;}
850
+ .inspector-section h2{
851
+ font-family:'Geist Pixel',monospace;
852
+ font-size:1rem;font-weight:600;color:#33ff00;margin-bottom:10px;
853
+ }
854
+ .inspector-section h3{
855
+ font-family:'Geist Pixel',monospace;
856
+ font-size:.875rem;font-weight:600;color:#1a8c00;margin:16px 0 8px;
857
+ }
858
+ .inspector-table{width:100%;border-collapse:collapse;margin-bottom:12px;}
859
+ .inspector-table th,.inspector-table td{
860
+ text-align:left;padding:8px 12px;border-bottom:1px solid #0a3300;
861
+ font-size:.8125rem;
862
+ }
863
+ .inspector-table th{color:#1a8c00;font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;}
864
+ .inspector-table td{color:#33ff00;}
865
+ .inspector-table tbody tr{transition:background .1s;}
866
+ .inspector-table tbody tr:hover{background:#0a3300;}
867
+ .inspector-empty{color:#1a8c00;text-align:center;padding:20px 0;font-size:.8125rem;}
868
+
869
+ .checkout-layout{
870
+ display:flex;min-height:calc(100vh - 42px);
871
+ }
872
+ .checkout-summary{
873
+ flex:1;background:#020;padding:48px 40px 48px 10%;
874
+ display:flex;flex-direction:column;justify-content:center;
875
+ border-right:1px solid #0a3300;
876
+ }
877
+ .checkout-form-side{
878
+ flex:1;background:#000;padding:48px 10% 48px 40px;
879
+ display:flex;flex-direction:column;justify-content:center;
880
+ }
881
+ .checkout-merchant{
882
+ display:flex;align-items:center;gap:10px;margin-bottom:6px;
883
+ }
884
+ .checkout-merchant-name{
885
+ font-family:'Geist Pixel',monospace;
886
+ font-size:.9375rem;font-weight:600;color:#33ff00;
887
+ }
888
+ .checkout-test-badge{
889
+ font-size:.625rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
890
+ background:#0a3300;color:#1a8c00;padding:2px 8px;border-radius:4px;
891
+ }
892
+ .checkout-total{
893
+ font-family:'Geist Pixel',monospace;
894
+ font-size:2rem;font-weight:700;color:#33ff00;margin:8px 0 28px;
895
+ }
896
+ .checkout-line-item{
897
+ display:flex;align-items:center;gap:14px;padding:14px 0;
898
+ border-bottom:1px solid #0a3300;
899
+ }
900
+ .checkout-line-item:first-child{border-top:1px solid #0a3300;}
901
+ .checkout-item-icon{
902
+ width:42px;height:42px;border-radius:6px;background:#0a3300;
903
+ display:flex;align-items:center;justify-content:center;flex-shrink:0;
904
+ font-family:'Geist Pixel',monospace;font-size:.875rem;font-weight:700;color:#116600;
905
+ }
906
+ .checkout-item-details{flex:1;min-width:0;}
907
+ .checkout-item-name{font-size:.875rem;font-weight:600;color:#33ff00;}
908
+ .checkout-item-qty{font-size:.75rem;color:#1a8c00;margin-top:2px;}
909
+ .checkout-item-price{
910
+ font-size:.875rem;font-weight:600;color:#33ff00;text-align:right;white-space:nowrap;
911
+ }
912
+ .checkout-item-unit{font-size:.6875rem;color:#1a8c00;text-align:right;margin-top:2px;}
913
+ .checkout-totals{margin-top:20px;}
914
+ .checkout-totals-row{
915
+ display:flex;justify-content:space-between;padding:6px 0;
916
+ font-size:.8125rem;color:#1a8c00;
917
+ }
918
+ .checkout-totals-row.total{
919
+ border-top:1px solid #0a3300;margin-top:8px;padding-top:14px;
920
+ font-size:.9375rem;font-weight:600;color:#33ff00;
921
+ }
922
+ .checkout-form-section{margin-bottom:24px;}
923
+ .checkout-form-label{
924
+ font-size:.8125rem;font-weight:600;color:#33ff00;margin-bottom:8px;display:block;
925
+ }
926
+ .checkout-input{
927
+ width:100%;padding:10px 12px;border:1px solid #0a3300;border-radius:6px;
928
+ background:#020;color:#33ff00;font:inherit;font-size:.875rem;
929
+ transition:border-color .15s;outline:none;
930
+ }
931
+ .checkout-input:focus{border-color:#33ff00;}
932
+ .checkout-input::placeholder{color:#116600;}
933
+ .checkout-card-box{
934
+ border:1px solid #0a3300;border-radius:6px;padding:14px;
935
+ background:#020;
936
+ }
937
+ .checkout-card-row{
938
+ display:flex;gap:12px;margin-top:10px;
939
+ }
940
+ .checkout-card-row .checkout-input{flex:1;}
941
+ .checkout-sim-note{
942
+ font-size:.6875rem;color:#1a8c00;margin-top:10px;text-align:center;
943
+ font-style:italic;
944
+ }
945
+ .checkout-pay-btn{
946
+ width:100%;padding:14px;border:none;border-radius:8px;
947
+ background:#33ff00;color:#000;font:inherit;font-size:.9375rem;font-weight:700;
948
+ cursor:pointer;transition:background .15s;
949
+ font-family:'Geist Pixel',monospace;
950
+ }
951
+ .checkout-pay-btn:hover{background:#44ff22;}
952
+ .checkout-cancel{
953
+ text-align:center;margin-top:14px;
954
+ }
955
+ .checkout-cancel a{
956
+ color:#1a8c00;text-decoration:none;font-size:.8125rem;
957
+ transition:color .15s;
958
+ }
959
+ .checkout-cancel a:hover{color:#33ff00;}
960
+ @media(max-width:768px){
961
+ .checkout-layout{flex-direction:column;}
962
+ .checkout-summary{padding:32px 20px;border-right:none;border-bottom:1px solid #0a3300;}
963
+ .checkout-form-side{padding:32px 20px;}
964
+ }
696
965
  `;
697
966
  var POWERED_BY = `<div class="powered-by">Powered by <a href="https://emulate.dev" target="_blank" rel="noopener">emulate</a></div>`;
698
967
  function emuBar(service) {
@@ -712,6 +981,7 @@ function head(title) {
712
981
  <head>
713
982
  <meta charset="utf-8"/>
714
983
  <meta name="viewport" content="width=device-width,initial-scale=1"/>
984
+ <link rel="icon" href="/_emulate/favicon.ico"/>
715
985
  <title>${escapeHtml(title)} | emulate</title>
716
986
  <style>${CSS}</style>
717
987
  </head>`;
@@ -730,6 +1000,72 @@ ${emuBar(service)}
730
1000
  ${POWERED_BY}
731
1001
  </body></html>`;
732
1002
  }
1003
+ function renderCheckoutPage(opts, service) {
1004
+ const fmt = (cents, cur) => `$${(cents / 100).toFixed(2)} ${cur.toUpperCase()}`;
1005
+ const fmtShort = (cents) => `$${(cents / 100).toFixed(2)}`;
1006
+ const itemsHtml = opts.lineItems.length > 0 ? opts.lineItems.map((li) => {
1007
+ const initial = li.name.charAt(0).toUpperCase();
1008
+ const unitNote = li.quantity > 1 ? `<div class="checkout-item-unit">${fmtShort(li.unitPrice)} each</div>` : "";
1009
+ return `<div class="checkout-line-item">
1010
+ <div class="checkout-item-icon">${escapeHtml(initial)}</div>
1011
+ <div class="checkout-item-details">
1012
+ <div class="checkout-item-name">${escapeHtml(li.name)}</div>
1013
+ <div class="checkout-item-qty">Qty ${li.quantity}</div>
1014
+ </div>
1015
+ <div>
1016
+ <div class="checkout-item-price">${fmtShort(li.totalPrice)}</div>
1017
+ ${unitNote}
1018
+ </div>
1019
+ </div>`;
1020
+ }).join("") : '<p class="empty">No line items</p>';
1021
+ const totalsHtml = `<div class="checkout-totals">
1022
+ <div class="checkout-totals-row">
1023
+ <span>Subtotal</span><span>${fmtShort(opts.subtotal)}</span>
1024
+ </div>
1025
+ <div class="checkout-totals-row total">
1026
+ <span>Total due</span><span>${fmt(opts.total, opts.currency)}</span>
1027
+ </div>
1028
+ </div>`;
1029
+ const cancelHtml = opts.cancelUrl ? `<div class="checkout-cancel"><a href="${escapeAttr(opts.cancelUrl)}">Cancel</a></div>` : "";
1030
+ const merchant = opts.merchantName ? escapeHtml(opts.merchantName) : "Checkout";
1031
+ return `${head("Checkout")}
1032
+ <body>
1033
+ ${emuBar(service)}
1034
+ <div class="checkout-layout">
1035
+ <div class="checkout-summary">
1036
+ <div class="checkout-merchant">
1037
+ <span class="checkout-merchant-name">${merchant}</span>
1038
+ <span class="checkout-test-badge">Test Mode</span>
1039
+ </div>
1040
+ <div class="checkout-total">${fmtShort(opts.total)}</div>
1041
+ ${itemsHtml}
1042
+ ${totalsHtml}
1043
+ </div>
1044
+ <div class="checkout-form-side">
1045
+ <form method="post" action="/checkout/${escapeAttr(opts.sessionId)}/complete">
1046
+ <div class="checkout-form-section">
1047
+ <label class="checkout-form-label">Email</label>
1048
+ <input type="email" name="email" class="checkout-input" placeholder="you@example.com"/>
1049
+ </div>
1050
+ <div class="checkout-form-section">
1051
+ <label class="checkout-form-label">Card information</label>
1052
+ <div class="checkout-card-box">
1053
+ <input type="text" class="checkout-input" placeholder="1234 1234 1234 1234" disabled/>
1054
+ <div class="checkout-card-row">
1055
+ <input type="text" class="checkout-input" placeholder="MM / YY" disabled/>
1056
+ <input type="text" class="checkout-input" placeholder="CVC" disabled/>
1057
+ </div>
1058
+ </div>
1059
+ <div class="checkout-sim-note">Card fields are simulated. Payment will be auto-approved.</div>
1060
+ </div>
1061
+ <button type="submit" class="checkout-pay-btn">Pay ${fmtShort(opts.total)}</button>
1062
+ </form>
1063
+ ${cancelHtml}
1064
+ </div>
1065
+ </div>
1066
+ ${POWERED_BY}
1067
+ </body></html>`;
1068
+ }
733
1069
 
734
1070
  // src/routes/checkout-sessions.ts
735
1071
  var SERVICE_LABEL = "Stripe";
@@ -753,9 +1089,17 @@ function checkoutSessionRoutes({ app, store, webhooks, baseUrl }) {
753
1089
  const ss = getStripeStore(store);
754
1090
  app.post("/v1/checkout/sessions", async (c) => {
755
1091
  const body = await parseStripeBody(c);
756
- if (!body.mode) return stripeError(c, 400, "invalid_request_error", "Missing required param: mode.", void 0, "mode");
1092
+ if (!body.mode)
1093
+ return stripeError(c, 400, "invalid_request_error", "Missing required param: mode.", void 0, "mode");
757
1094
  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");
1095
+ return stripeError(
1096
+ c,
1097
+ 400,
1098
+ "invalid_request_error",
1099
+ `No such customer: '${body.customer}'`,
1100
+ "resource_missing",
1101
+ "customer"
1102
+ );
759
1103
  }
760
1104
  const lineItems = [];
761
1105
  if (body.line_items) {
@@ -765,17 +1109,45 @@ function checkoutSessionRoutes({ app, store, webhooks, baseUrl }) {
765
1109
  for (let i = 0; i < body.line_items.length; i++) {
766
1110
  const li = body.line_items[i];
767
1111
  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}]`);
1112
+ return stripeError(
1113
+ c,
1114
+ 400,
1115
+ "invalid_request_error",
1116
+ `Invalid line_items[${i}]: must be an object.`,
1117
+ void 0,
1118
+ `line_items[${i}]`
1119
+ );
769
1120
  }
770
1121
  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]`);
1122
+ return stripeError(
1123
+ c,
1124
+ 400,
1125
+ "invalid_request_error",
1126
+ `Missing required param: line_items[${i}][price].`,
1127
+ void 0,
1128
+ `line_items[${i}][price]`
1129
+ );
772
1130
  }
773
1131
  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]`);
1132
+ return stripeError(
1133
+ c,
1134
+ 400,
1135
+ "invalid_request_error",
1136
+ `No such price: '${li.price}'`,
1137
+ "resource_missing",
1138
+ `line_items[${i}][price]`
1139
+ );
775
1140
  }
776
1141
  const qty = typeof li.quantity === "number" ? li.quantity : parseInt(li.quantity, 10);
777
1142
  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]`);
1143
+ return stripeError(
1144
+ c,
1145
+ 400,
1146
+ "invalid_request_error",
1147
+ `Invalid line_items[${i}][quantity]: must be a positive integer.`,
1148
+ void 0,
1149
+ `line_items[${i}][quantity]`
1150
+ );
779
1151
  }
780
1152
  lineItems.push({ price: li.price, quantity: qty });
781
1153
  }
@@ -795,14 +1167,34 @@ function checkoutSessionRoutes({ app, store, webhooks, baseUrl }) {
795
1167
  });
796
1168
  app.get("/v1/checkout/sessions/:id", (c) => {
797
1169
  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");
1170
+ if (!session)
1171
+ return stripeError(
1172
+ c,
1173
+ 404,
1174
+ "invalid_request_error",
1175
+ `No such checkout session: '${c.req.param("id")}'`,
1176
+ "resource_missing"
1177
+ );
799
1178
  return c.json(formatSession(session, baseUrl));
800
1179
  });
801
1180
  app.post("/v1/checkout/sessions/:id/expire", async (c) => {
802
1181
  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");
1182
+ if (!session)
1183
+ return stripeError(
1184
+ c,
1185
+ 404,
1186
+ "invalid_request_error",
1187
+ `No such checkout session: '${c.req.param("id")}'`,
1188
+ "resource_missing"
1189
+ );
804
1190
  if (session.status !== "open") {
805
- return stripeError(c, 400, "invalid_request_error", "Only open sessions can be expired.", "checkout_session_not_open");
1191
+ return stripeError(
1192
+ c,
1193
+ 400,
1194
+ "invalid_request_error",
1195
+ "Only open sessions can be expired.",
1196
+ "checkout_session_not_open"
1197
+ );
806
1198
  }
807
1199
  const updated = ss.checkoutSessions.update(session.id, { status: "expired" });
808
1200
  await webhooks.dispatch(
@@ -826,35 +1218,53 @@ function checkoutSessionRoutes({ app, store, webhooks, baseUrl }) {
826
1218
  app.get("/checkout/:id", (c) => {
827
1219
  const session = ss.checkoutSessions.findOneBy("stripe_id", c.req.param("id"));
828
1220
  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);
1221
+ return c.html(
1222
+ renderCardPage(
1223
+ "Session Not Found",
1224
+ "This checkout session does not exist.",
1225
+ '<p class="empty">The session ID is invalid or has been removed.</p>',
1226
+ SERVICE_LABEL
1227
+ ),
1228
+ 404
1229
+ );
830
1230
  }
831
1231
  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));
1232
+ return c.html(
1233
+ renderCardPage(
1234
+ "Session Expired",
1235
+ "This checkout session is no longer available.",
1236
+ `<p class="empty">Status: ${escapeHtml(session.status)}</p>`,
1237
+ SERVICE_LABEL
1238
+ )
1239
+ );
833
1240
  }
834
- const lineItemsHtml = session.line_items.length > 0 ? session.line_items.map((li) => {
1241
+ const lineItems = session.line_items.map((li) => {
835
1242
  const priceObj = ss.prices.findOneBy("stripe_id", li.price);
836
1243
  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));
1244
+ const unitPrice = priceObj?.unit_amount ?? 0;
1245
+ return {
1246
+ name: product?.name ?? li.price,
1247
+ quantity: li.quantity,
1248
+ unitPrice,
1249
+ totalPrice: unitPrice * li.quantity,
1250
+ currency: priceObj?.currency ?? "usd"
1251
+ };
1252
+ });
1253
+ const subtotal = lineItems.reduce((sum, li) => sum + li.totalPrice, 0);
1254
+ const currency = lineItems.length > 0 ? lineItems[0].currency : "usd";
1255
+ return c.html(
1256
+ renderCheckoutPage(
1257
+ {
1258
+ lineItems,
1259
+ subtotal,
1260
+ total: subtotal,
1261
+ currency,
1262
+ sessionId: session.stripe_id,
1263
+ cancelUrl: session.cancel_url
1264
+ },
1265
+ SERVICE_LABEL
1266
+ )
1267
+ );
858
1268
  });
859
1269
  app.post("/checkout/:id/complete", async (c) => {
860
1270
  const session = ss.checkoutSessions.findOneBy("stripe_id", c.req.param("id"));
@@ -869,9 +1279,49 @@ function checkoutSessionRoutes({ app, store, webhooks, baseUrl }) {
869
1279
  "stripe"
870
1280
  );
871
1281
  if (session.success_url) {
872
- return c.redirect(session.success_url);
1282
+ const url = session.success_url.replace("{CHECKOUT_SESSION_ID}", updated.stripe_id);
1283
+ return c.redirect(url);
873
1284
  }
874
- return c.html(renderCardPage("Payment Complete", "Your payment was successful.", '<p class="empty check">Payment received</p>', SERVICE_LABEL));
1285
+ return c.html(
1286
+ renderCardPage(
1287
+ "Payment Complete",
1288
+ "Your payment was successful.",
1289
+ '<p class="empty check">Payment received</p>',
1290
+ SERVICE_LABEL
1291
+ )
1292
+ );
1293
+ });
1294
+ }
1295
+
1296
+ // src/routes/customer-sessions.ts
1297
+ function customerSessionRoutes({ app, store }) {
1298
+ const ss = getStripeStore(store);
1299
+ app.post("/v1/customer_sessions", async (c) => {
1300
+ const body = await parseStripeBody(c);
1301
+ if (!body.customer)
1302
+ return stripeError(c, 400, "invalid_request_error", "Missing required param: customer.", void 0, "customer");
1303
+ const customer = ss.customers.findOneBy("stripe_id", body.customer);
1304
+ if (!customer)
1305
+ return stripeError(
1306
+ c,
1307
+ 400,
1308
+ "invalid_request_error",
1309
+ `No such customer: '${body.customer}'`,
1310
+ "resource_missing",
1311
+ "customer"
1312
+ );
1313
+ return c.json(
1314
+ {
1315
+ object: "customer_session",
1316
+ client_secret: stripeId("cuss_secret"),
1317
+ components: body.components ?? {},
1318
+ created: Math.floor(Date.now() / 1e3),
1319
+ customer: customer.stripe_id,
1320
+ expires_at: Math.floor(Date.now() / 1e3) + 1800,
1321
+ livemode: false
1322
+ },
1323
+ 200
1324
+ );
875
1325
  });
876
1326
  }
877
1327
 
@@ -886,7 +1336,7 @@ function seedDefaults(store, _baseUrl) {
886
1336
  metadata: {}
887
1337
  });
888
1338
  }
889
- function seedFromConfig(store, _baseUrl, config) {
1339
+ function seedFromConfig(store, _baseUrl, config, webhooks) {
890
1340
  const ss = getStripeStore(store);
891
1341
  if (config.customers) {
892
1342
  for (const c of config.customers) {
@@ -895,7 +1345,7 @@ function seedFromConfig(store, _baseUrl, config) {
895
1345
  if (existing) continue;
896
1346
  }
897
1347
  ss.customers.insert({
898
- stripe_id: stripeId("cus"),
1348
+ stripe_id: c.id ?? stripeId("cus"),
899
1349
  email: c.email ?? null,
900
1350
  name: c.name ?? null,
901
1351
  description: c.description ?? null,
@@ -906,7 +1356,7 @@ function seedFromConfig(store, _baseUrl, config) {
906
1356
  if (config.products) {
907
1357
  for (const p of config.products) {
908
1358
  const product = ss.products.insert({
909
- stripe_id: stripeId("prod"),
1359
+ stripe_id: p.id ?? stripeId("prod"),
910
1360
  name: p.name,
911
1361
  description: p.description ?? null,
912
1362
  active: true,
@@ -915,7 +1365,7 @@ function seedFromConfig(store, _baseUrl, config) {
915
1365
  const matchingPrices = config.prices?.filter((pr) => pr.product_name === p.name) ?? [];
916
1366
  for (const pr of matchingPrices) {
917
1367
  ss.prices.insert({
918
- stripe_id: stripeId("price"),
1368
+ stripe_id: pr.id ?? stripeId("price"),
919
1369
  product_id: product.stripe_id,
920
1370
  currency: pr.currency.toLowerCase(),
921
1371
  unit_amount: pr.unit_amount,
@@ -926,17 +1376,30 @@ function seedFromConfig(store, _baseUrl, config) {
926
1376
  }
927
1377
  }
928
1378
  }
1379
+ if (config.webhooks && webhooks) {
1380
+ for (const wh of config.webhooks) {
1381
+ webhooks.register({
1382
+ url: wh.url,
1383
+ events: wh.events,
1384
+ active: true,
1385
+ secret: wh.secret,
1386
+ owner: "stripe"
1387
+ });
1388
+ }
1389
+ }
929
1390
  }
930
1391
  var stripePlugin = {
931
1392
  name: "stripe",
932
1393
  register(app, store, webhooks, baseUrl, tokenMap) {
933
1394
  const ctx = { app, store, webhooks, baseUrl, tokenMap };
934
1395
  customerRoutes(ctx);
1396
+ paymentMethodRoutes(ctx);
935
1397
  paymentIntentRoutes(ctx);
936
1398
  chargeRoutes(ctx);
937
1399
  productRoutes(ctx);
938
1400
  priceRoutes(ctx);
939
1401
  checkoutSessionRoutes(ctx);
1402
+ customerSessionRoutes(ctx);
940
1403
  },
941
1404
  seed(store, baseUrl) {
942
1405
  seedDefaults(store, baseUrl);