@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/README.md +7 -1
- package/dist/fonts/favicon.ico +0 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +523 -60
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
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)
|
|
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)
|
|
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)
|
|
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(
|
|
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(
|
|
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)
|
|
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)
|
|
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)
|
|
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(
|
|
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
|
-
{
|
|
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)
|
|
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(
|
|
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)
|
|
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)
|
|
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)
|
|
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 {
|
|
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(
|
|
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(
|
|
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)
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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)
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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)
|
|
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)
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
-
|
|
1282
|
+
const url = session.success_url.replace("{CHECKOUT_SESSION_ID}", updated.stripe_id);
|
|
1283
|
+
return c.redirect(url);
|
|
873
1284
|
}
|
|
874
|
-
return c.html(
|
|
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);
|