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