@storecraft/payments-stripe 1.0.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 ADDED
@@ -0,0 +1,82 @@
1
+ # Stripe payment gateway for **StoreCraft**
2
+
3
+ [Stripe](https://docs.stripe.com/payments/place-a-hold-on-a-payment-method) integration
4
+
5
+
6
+ ```bash
7
+ npm i @storecraft/payments-stripe
8
+ ```
9
+
10
+ ## Howto
11
+
12
+ ```js
13
+ import { Stripe } from '@storecraft/payments-stripe';
14
+ import { Stripe as StripeCls } from 'stripe';
15
+
16
+ const config = {
17
+ //`stripe` publishable key
18
+ publishable_key: 'pk_....',
19
+
20
+ // `stripe` private secret
21
+ secret: 'sk_.....',
22
+
23
+ // (Optional) `stripe` private `webhook` secret
24
+ webhook_endpoint_secret: 'whsec_.....',
25
+
26
+ // config options for `stripe`
27
+ stripe_config: {
28
+ httpClient: StripeCls.createFetchHttpClient()
29
+ },
30
+
31
+ // configure `intent` creation
32
+ stripe_intent_create_params: {
33
+ currency: 'usd',
34
+ automatic_payment_methods: {
35
+ enabled: true,
36
+ },
37
+ payment_method_options: {
38
+ card: {
39
+ // authorize and capture flow
40
+ capture_method: 'manual',
41
+ },
42
+ },
43
+ }
44
+ }
45
+
46
+ new Stripe(config);
47
+ ```
48
+
49
+ ## Developer info and test
50
+
51
+ First, some resources from `stripe`
52
+
53
+ - [Authorize and Capture Flow](https://docs.stripe.com/payments/place-a-hold-on-a-payment-method)
54
+ - [Webhooks](https://docs.stripe.com/webhooks)
55
+ - [Credit Card Generator](https://developer.paypal.com/tools/sandbox/card-testing/#link-creditcardgenerator)
56
+
57
+ ## Test Webhooks
58
+ First, consult [Stripe Webhooks Docs](https://docs.stripe.com/webhooks)
59
+ Then, Install the `stripe` cli.
60
+
61
+ ```bash
62
+ stripe listen --skip-verify --forward-to localhost:8000/api/payments/gateways/stripe/webhook
63
+ ```
64
+
65
+ This will print the `webhook` **SECRET**
66
+
67
+ ```bash
68
+ Ready! Your webhook signing secret is '{{WEBHOOK_SIGNING_SECRET}}' (^C to quit)
69
+ ```
70
+
71
+ Copy the `WEBHOOK_SIGNING_SECRET` and put it in the `config` of the gateway.
72
+
73
+ Now, start interacting, and test some payments.
74
+
75
+
76
+ ## todo:
77
+ - Add tests
78
+
79
+
80
+ ```text
81
+ Author: Tomer Shalev (tomer.shalev@gmail.com)
82
+ ```
@@ -0,0 +1,311 @@
1
+ /**
2
+ *
3
+ * @description Official `Stripe` UI integration with `storecraft`.
4
+ *
5
+ * Test with dummy data with this generator
6
+ * https://docs.stripe.com/payments/quickstart
7
+ *
8
+ * Or use the following dummy details:
9
+ * https://docs.stripe.com/testing?testing-method=card-numbers
10
+ * - Card number: 4242424242424242
11
+ * - Expiry date: 12/2027
12
+ * - CVC code: 897
13
+ *
14
+ * @param {import("./types.public.js").Config} config
15
+ * @param {Partial<import("@storecraft/core/v-api").OrderData>} order_data
16
+ */
17
+ export default function html_buy_ui(config, order_data) {
18
+
19
+ /** @type {import("./adapter.js").CheckoutCreateResult} */
20
+ const on_checkout_create = order_data?.payment_gateway?.on_checkout_create;
21
+
22
+ return `
23
+ <!DOCTYPE html>
24
+ <html lang="en">
25
+ <head>
26
+ <meta charset="utf-8" />
27
+ <title>Accept a payment</title>
28
+ <meta name="description" content="A demo of a payment on Stripe" />
29
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
30
+ <script src="https://js.stripe.com/v3/"></script>
31
+
32
+ <script defer>
33
+ // This is your test publishable API key.
34
+ const stripe = new Stripe("${config.publishable_key}");
35
+
36
+ let elements;
37
+
38
+ window.onload = function() {
39
+ initialize();
40
+ checkStatus();
41
+
42
+ document
43
+ .getElementById("payment-form")
44
+ .addEventListener("submit", handleSubmit);
45
+ }
46
+
47
+ // Fetches a payment intent and captures the client secret
48
+ async function initialize() {
49
+ const clientSecret = "${on_checkout_create.client_secret}";
50
+
51
+ const appearance = {
52
+ theme: 'stripe',
53
+ };
54
+ elements = stripe.elements({ appearance, clientSecret });
55
+
56
+ const paymentElementOptions = {
57
+ layout: "tabs",
58
+ };
59
+
60
+ const paymentElement = elements.create("payment", paymentElementOptions);
61
+ paymentElement.mount("#payment-element");
62
+ }
63
+
64
+ async function handleSubmit(e) {
65
+ e.preventDefault();
66
+ setLoading(true);
67
+
68
+ const { error } = await stripe.confirmPayment({
69
+ elements,
70
+ confirmParams: {
71
+ // Make sure to change this to your payment completion page
72
+ return_url: "https://storecraft.app/",
73
+ },
74
+ });
75
+
76
+ // This point will only be reached if there is an immediate error when
77
+ // confirming the payment. Otherwise, your customer will be redirected to
78
+ // your \`return_url\`. For some payment methods like iDEAL, your customer will
79
+ // be redirected to an intermediate site first to authorize the payment, then
80
+ // redirected to the \`return_url\`.
81
+ if (error.type === "card_error" || error.type === "validation_error") {
82
+ showMessage(error.message);
83
+ } else {
84
+ showMessage("An unexpected error occurred.");
85
+ }
86
+
87
+ setLoading(false);
88
+ }
89
+
90
+ // Fetches the payment intent status after payment submission
91
+ async function checkStatus() {
92
+ const clientSecret = new URLSearchParams(window.location.search).get(
93
+ "payment_intent_client_secret"
94
+ );
95
+
96
+ if (!clientSecret) {
97
+ return;
98
+ }
99
+
100
+ const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);
101
+
102
+ switch (paymentIntent.status) {
103
+ case "succeeded":
104
+ showMessage("Payment succeeded!");
105
+ break;
106
+ case "processing":
107
+ showMessage("Your payment is processing.");
108
+ break;
109
+ case "requires_payment_method":
110
+ showMessage("Your payment was not successful, please try again.");
111
+ break;
112
+ default:
113
+ showMessage("Something went wrong.");
114
+ break;
115
+ }
116
+ }
117
+
118
+ // ------- UI helpers -------
119
+
120
+ function showMessage(messageText) {
121
+ const messageContainer = document.querySelector("#payment-message");
122
+
123
+ messageContainer.classList.remove("hidden");
124
+ messageContainer.textContent = messageText;
125
+
126
+ setTimeout(function () {
127
+ messageContainer.classList.add("hidden");
128
+ messageContainer.textContent = "";
129
+ }, 4000);
130
+ }
131
+
132
+ // Show a spinner on payment submission
133
+ function setLoading(isLoading) {
134
+ if (isLoading) {
135
+ // Disable the button and show a spinner
136
+ document.querySelector("#submit").disabled = true;
137
+ document.querySelector("#spinner").classList.remove("hidden");
138
+ document.querySelector("#button-text").classList.add("hidden");
139
+ } else {
140
+ document.querySelector("#submit").disabled = false;
141
+ document.querySelector("#spinner").classList.add("hidden");
142
+ document.querySelector("#button-text").classList.remove("hidden");
143
+ }
144
+ }
145
+
146
+ </script>
147
+
148
+ <style>
149
+ /* Variables */
150
+ * {
151
+ box-sizing: border-box;
152
+ }
153
+
154
+ body {
155
+ font-family: -apple-system, BlinkMacSystemFont, sans-serif;
156
+ font-size: 16px;
157
+ -webkit-font-smoothing: antialiased;
158
+ display: flex;
159
+ justify-content: center;
160
+ align-content: center;
161
+ height: 100vh;
162
+ width: 100vw;
163
+ }
164
+
165
+ form {
166
+ width: 30vw;
167
+ min-width: 500px;
168
+ align-self: center;
169
+ box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1),
170
+ 0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07);
171
+ border-radius: 7px;
172
+ padding: 40px;
173
+ }
174
+
175
+ .hidden {
176
+ display: none;
177
+ }
178
+
179
+ #payment-message {
180
+ color: rgb(105, 115, 134);
181
+ font-size: 16px;
182
+ line-height: 20px;
183
+ padding-top: 12px;
184
+ text-align: center;
185
+ }
186
+
187
+ #payment-element {
188
+ margin-bottom: 24px;
189
+ }
190
+
191
+ /* Buttons and links */
192
+ button {
193
+ background: #5469d4;
194
+ font-family: Arial, sans-serif;
195
+ color: #ffffff;
196
+ border-radius: 4px;
197
+ border: 0;
198
+ padding: 12px 16px;
199
+ font-size: 16px;
200
+ font-weight: 600;
201
+ cursor: pointer;
202
+ display: block;
203
+ transition: all 0.2s ease;
204
+ box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07);
205
+ width: 100%;
206
+ }
207
+ button:hover {
208
+ filter: contrast(115%);
209
+ }
210
+ button:disabled {
211
+ opacity: 0.5;
212
+ cursor: default;
213
+ }
214
+
215
+ /* spinner/processing state, errors */
216
+ .spinner,
217
+ .spinner:before,
218
+ .spinner:after {
219
+ border-radius: 50%;
220
+ }
221
+ .spinner {
222
+ color: #ffffff;
223
+ font-size: 22px;
224
+ text-indent: -99999px;
225
+ margin: 0px auto;
226
+ position: relative;
227
+ width: 20px;
228
+ height: 20px;
229
+ box-shadow: inset 0 0 0 2px;
230
+ -webkit-transform: translateZ(0);
231
+ -ms-transform: translateZ(0);
232
+ transform: translateZ(0);
233
+ }
234
+ .spinner:before,
235
+ .spinner:after {
236
+ position: absolute;
237
+ content: "";
238
+ }
239
+ .spinner:before {
240
+ width: 10.4px;
241
+ height: 20.4px;
242
+ background: #5469d4;
243
+ border-radius: 20.4px 0 0 20.4px;
244
+ top: -0.2px;
245
+ left: -0.2px;
246
+ -webkit-transform-origin: 10.4px 10.2px;
247
+ transform-origin: 10.4px 10.2px;
248
+ -webkit-animation: loading 2s infinite ease 1.5s;
249
+ animation: loading 2s infinite ease 1.5s;
250
+ }
251
+ .spinner:after {
252
+ width: 10.4px;
253
+ height: 10.2px;
254
+ background: #5469d4;
255
+ border-radius: 0 10.2px 10.2px 0;
256
+ top: -0.1px;
257
+ left: 10.2px;
258
+ -webkit-transform-origin: 0px 10.2px;
259
+ transform-origin: 0px 10.2px;
260
+ -webkit-animation: loading 2s infinite ease;
261
+ animation: loading 2s infinite ease;
262
+ }
263
+
264
+ @-webkit-keyframes loading {
265
+ 0% {
266
+ -webkit-transform: rotate(0deg);
267
+ transform: rotate(0deg);
268
+ }
269
+ 100% {
270
+ -webkit-transform: rotate(360deg);
271
+ transform: rotate(360deg);
272
+ }
273
+ }
274
+ @keyframes loading {
275
+ 0% {
276
+ -webkit-transform: rotate(0deg);
277
+ transform: rotate(0deg);
278
+ }
279
+ 100% {
280
+ -webkit-transform: rotate(360deg);
281
+ transform: rotate(360deg);
282
+ }
283
+ }
284
+
285
+ @media only screen and (max-width: 600px) {
286
+ form {
287
+ width: 80vw;
288
+ min-width: initial;
289
+ }
290
+ }
291
+
292
+ </style>
293
+ </head>
294
+
295
+ <body>
296
+ <!-- Display a payment form -->
297
+ <form id="payment-form">
298
+ <div id="payment-element">
299
+ <!--Stripe.js injects the Payment Element-->
300
+ </div>
301
+ <button id="submit">
302
+ <div class="spinner hidden" id="spinner"></div>
303
+ <span id="button-text">Pay now</span>
304
+ </button>
305
+ <div id="payment-message" class="hidden"></div>
306
+ </form>
307
+ </body>
308
+
309
+ </html>
310
+ `
311
+ }
package/adapter.js ADDED
@@ -0,0 +1,410 @@
1
+ import { CheckoutStatusEnum, PaymentOptionsEnum } from '@storecraft/core/v-api/types.api.enums.js';
2
+ import { StorecraftError } from '@storecraft/core/v-api/utils.func.js';
3
+ import html_buy_ui from './adapter.html.js';
4
+ import { Stripe as StripeCls } from 'stripe'
5
+
6
+ /**
7
+ * @typedef {StripeCls.Response<StripeCls.PaymentIntent>} CheckoutCreateResult
8
+ * @typedef {import('@storecraft/core/v-api').PaymentGatewayStatus} PaymentGatewayStatus
9
+ * @typedef {import('@storecraft/core/v-api').CheckoutStatusEnum} CheckoutStatusOptions
10
+ * @typedef {import('@storecraft/core/v-api').OrderData} OrderData
11
+ * @typedef {import('./types.public.js').Config} Config
12
+ * @typedef {import('@storecraft/core/v-payments').payment_gateway<Config, CheckoutCreateResult>} payment_gateway
13
+ */
14
+
15
+ /**
16
+ * @description in a {@link StripeCls.PaymentIntent}, is a `metadata` key-value
17
+ * storage where we store the `order_id` of the `storecraft` order.
18
+ */
19
+ export const metadata_storecraft_order_id = 'storecraft_order_id'
20
+
21
+ /**
22
+ * @implements {payment_gateway}
23
+ *
24
+ * @description **Stripe** gateway (https://docs.stripe.com/payments/place-a-hold-on-a-payment-method)
25
+ */
26
+ export class Stripe {
27
+
28
+ /** @type {Config} */ #_config;
29
+
30
+ /**
31
+ *
32
+ * @param {Config} config
33
+ */
34
+ constructor(config) {
35
+ this.#_config = this.#validate_and_resolve_config(config);
36
+ this.stripe = new StripeCls(
37
+ this.#_config.secret_key, this.#_config.stripe_config ?? {}
38
+ );
39
+ }
40
+
41
+ /**
42
+ *
43
+ * @param {Config} config
44
+ */
45
+ #validate_and_resolve_config(config) {
46
+ config = {
47
+ stripe_config: {
48
+ httpClient: StripeCls.createFetchHttpClient()
49
+ },
50
+ stripe_intent_create_params: {
51
+ currency: 'usd',
52
+ automatic_payment_methods: {
53
+ enabled: true,
54
+ },
55
+ payment_method_options: {
56
+ card: {
57
+ capture_method: 'manual',
58
+ },
59
+ },
60
+ },
61
+ ...config,
62
+ }
63
+
64
+ const is_valid = config.publishable_key && config.secret_key;
65
+
66
+ if(!is_valid) {
67
+ throw new StorecraftError(
68
+ `Payment gateway ${this.info.name ?? 'unknown'} has invalid config !!!
69
+ Missing client_id or secret`
70
+ )
71
+ }
72
+
73
+ return config;
74
+ }
75
+
76
+ get info() {
77
+ return {
78
+ name: 'Stripe',
79
+ description: `Stripe powers online and in-person payment processing and financial solutions for businesses of all sizes.`,
80
+ url: 'https://docs.stripe.com/payments/place-a-hold-on-a-payment-method',
81
+ logo_url: 'https://images.ctfassets.net/fzn2n1nzq965/HTTOloNPhisV9P4hlMPNA/cacf1bb88b9fc492dfad34378d844280/Stripe_icon_-_square.svg?q=80&w=256'
82
+ }
83
+ }
84
+
85
+ get config() {
86
+ return this.#_config;
87
+ }
88
+
89
+ get actions() {
90
+ return [
91
+ {
92
+ handle: 'capture',
93
+ name: 'Capture',
94
+ description: 'Capture an authorized payment'
95
+ },
96
+ {
97
+ handle: 'cancel',
98
+ name: 'Cancel',
99
+ description: 'Cancel an a payment'
100
+ },
101
+ {
102
+ handle: 'refund',
103
+ name: 'Refund',
104
+ description: 'Refund a captured payment'
105
+ },
106
+ ]
107
+ }
108
+
109
+ /**
110
+ *
111
+ * @type {payment_gateway["invokeAction"]}
112
+ */
113
+ invokeAction(action_handle) {
114
+ switch (action_handle) {
115
+ case 'capture':
116
+ return this.capture.bind(this);
117
+ case 'cancel':
118
+ return this.cancel.bind(this);
119
+ case 'refund':
120
+ return this.refund.bind(this);
121
+
122
+ default:
123
+ break;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * @description (Optional) buy link ui
129
+ *
130
+ * @param {Partial<OrderData>} order
131
+ *
132
+ * @return {Promise<string>} html
133
+ */
134
+ async onBuyLinkHtml(order) {
135
+
136
+ return html_buy_ui(
137
+ this.config, order
138
+ )
139
+ }
140
+
141
+ /**
142
+ * @description on checkout create `hook`
143
+ *
144
+ * @param {OrderData} order
145
+ *
146
+ * @return {Promise<CheckoutCreateResult>}
147
+ */
148
+ async onCheckoutCreate(order) {
149
+
150
+ const paymentIntent = await this.stripe.paymentIntents.create(
151
+ {
152
+ amount: Math.floor(order.pricing.total * 100),
153
+ ...this.config.stripe_intent_create_params,
154
+ metadata: {
155
+ [metadata_storecraft_order_id]: order.id
156
+ }
157
+ }
158
+ );
159
+
160
+ return paymentIntent;
161
+ }
162
+
163
+ /**
164
+ * @description On checkout complete hook. With stripe, this corresponds
165
+ * to synchronous payments flows, which is discouraged by `stripe`.
166
+ * They advocate async flows where confirmation happens async from
167
+ * client side into their servers, and then you are notified via a
168
+ * webhook.
169
+ *
170
+ * @param {CheckoutCreateResult} create_result
171
+ *
172
+ * @return {ReturnType<payment_gateway["onCheckoutComplete"]>}
173
+ */
174
+ async onCheckoutComplete(create_result) {
175
+
176
+ const intent = await this.stripe.paymentIntents.confirm(
177
+ create_result.id
178
+ );
179
+
180
+ let status;
181
+ switch(intent.status) {
182
+ case 'succeeded':
183
+ status = {
184
+ payment: PaymentOptionsEnum.captured,
185
+ checkout: CheckoutStatusEnum.complete
186
+ }
187
+ break;
188
+ case 'requires_capture':
189
+ status = {
190
+ payment: PaymentOptionsEnum.authorized,
191
+ checkout: CheckoutStatusEnum.complete
192
+ }
193
+ break;
194
+ case 'requires_confirmation':
195
+ case 'requires_action':
196
+ status = {
197
+ checkout: CheckoutStatusEnum.requires_action
198
+ }
199
+ break;
200
+ default:
201
+ status = {
202
+ checkout: CheckoutStatusEnum.failed
203
+ }
204
+ break;
205
+ }
206
+
207
+ return {
208
+ status,
209
+ onCheckoutComplete: intent
210
+ }
211
+ }
212
+
213
+ /**
214
+ * @description Fetch the order and analyze it's status
215
+ *
216
+ *
217
+ * @param {CheckoutCreateResult} create_result
218
+ *
219
+ *
220
+ * @returns {Promise<PaymentGatewayStatus>}
221
+ */
222
+ async status(create_result) {
223
+ const o = await this.retrieve_order(create_result);
224
+ const lc = /** @type {StripeCls.Charge} */ (o.latest_charge);
225
+ /** @param {number} a */
226
+ const fmt = a => (a/100).toFixed(2) + o.currency.toUpperCase();
227
+
228
+ /** @type {PaymentGatewayStatus} */
229
+ const stat = {
230
+ messages: [],
231
+ actions: this.actions
232
+ }
233
+
234
+ if(o) { // just an intent
235
+ const date = new Date(o.created).toUTCString();
236
+ stat.messages = [
237
+ `A payment intent of **${fmt(o.amount)}** was initiated at ${date}`,
238
+ `The status is \`${o.status}\` and the ID is \`${o.id}\``
239
+ ];
240
+ }
241
+
242
+ if(lc?.captured) {
243
+ stat.messages.push(
244
+ `**${fmt(lc.amount_captured)}** was \`CAPTURED\``,
245
+ );
246
+ }
247
+ if(lc?.refunded) {
248
+ const date = lc?.refunds?.data?.[0]?.created ? (new Date(lc?.refunds?.data?.[0]?.created).toUTCString()) : 'unknown time';
249
+ stat.messages.push(
250
+ `**${fmt(lc.amount_refunded)}** was \`REFUNDED\` at \`${date}\``,
251
+ );
252
+ }
253
+
254
+ if(o?.canceled_at) {
255
+ const date = new Date(o.canceled_at).toUTCString();
256
+ stat.messages.push(
257
+ ...[
258
+ `Intent was \`CANCELLED\` at \`${date}\`.`,
259
+ o.cancellation_reason && `Cancellation reason is ${o.cancellation_reason}`
260
+ ].filter(Boolean)
261
+ );
262
+ }
263
+
264
+ return stat;
265
+ }
266
+
267
+ /**
268
+ * @description [https://docs.stripe.com/webhooks](https://docs.stripe.com/webhooks)
269
+ * @param {import('@storecraft/core').ApiRequest} request
270
+ * @param {import('@storecraft/core').ApiResponse} response
271
+ *
272
+ * @type {payment_gateway["webhook"]}
273
+ */
274
+ async webhook(request, response) {
275
+ const sig = request.headers.get('Stripe-Signature');
276
+
277
+ let event;
278
+
279
+ try {
280
+ event = await this.stripe.webhooks.constructEventAsync(
281
+ request.rawBody, sig, this.config.webhook_endpoint_secret, undefined,
282
+ StripeCls.createSubtleCryptoProvider()
283
+ );
284
+ }
285
+ catch (err) {
286
+ response.status = 400;
287
+ response.end();
288
+ console.log(err.message);
289
+ return;
290
+ }
291
+
292
+ let order_id;
293
+ /** @type {StripeCls.PaymentIntent} */
294
+ let payment_intent;
295
+
296
+ let payment_status = PaymentOptionsEnum.unpaid;
297
+
298
+ // Handle the event
299
+ switch (event?.type) {
300
+ case 'payment_intent.succeeded':
301
+ case 'payment_intent.payment_failed':
302
+ case 'payment_intent.requires_action':
303
+ case 'payment_intent.amount_capturable_updated':
304
+ case 'payment_intent.canceled':
305
+ payment_intent = event.data.object;
306
+ order_id = payment_intent.metadata[metadata_storecraft_order_id];
307
+
308
+ if(payment_intent.status==='requires_capture')
309
+ payment_status = PaymentOptionsEnum.authorized;
310
+ else if(payment_intent.status==='canceled')
311
+ payment_status = PaymentOptionsEnum.unpaid;
312
+ else if(payment_intent.status==='processing')
313
+ payment_status = PaymentOptionsEnum.unpaid;
314
+ else if(payment_intent.status==='requires_action')
315
+ payment_status = PaymentOptionsEnum.unpaid;
316
+ else if(payment_intent.status==='succeeded')
317
+ payment_status = PaymentOptionsEnum.captured;
318
+
319
+ break;
320
+ case 'charge.refunded':
321
+ case 'charge.refund.updated':
322
+ payment_status = PaymentOptionsEnum.refunded;
323
+
324
+ default:
325
+ console.log(`Unhandled event type ${event.type}`);
326
+ }
327
+
328
+ console.log('CCCC')
329
+
330
+ // Return a response to acknowledge receipt of the event
331
+ response.sendJson({received: true});
332
+
333
+ console.log('DDDD')
334
+
335
+ return {
336
+ order_id,
337
+ status: {
338
+ payment: payment_status,
339
+ checkout: CheckoutStatusEnum.complete
340
+ }
341
+ }
342
+ }
343
+
344
+ /**
345
+ * @description Retrieve latest order payload
346
+ *
347
+ * @param {CheckoutCreateResult} create_result first create result, holds `Stripe` intent
348
+ *
349
+ */
350
+ retrieve_order = (create_result) => {
351
+ return this.stripe.paymentIntents.retrieve(
352
+ create_result.id,
353
+ {
354
+ expand: ['latest_charge']
355
+ }
356
+ )
357
+ }
358
+
359
+ // actions
360
+
361
+ /**
362
+ * @description todo: logic for if user wanted capture at approval
363
+ *
364
+ * @param {CheckoutCreateResult} create_result
365
+ */
366
+ async cancel(create_result) {
367
+ await this.stripe.paymentIntents.cancel(
368
+ create_result.id,
369
+ {
370
+ cancellation_reason: 'abandoned'
371
+ }
372
+ );
373
+
374
+ return this.status(create_result);
375
+ }
376
+
377
+ /**
378
+ * @description todo: logic for if user wanted capture at approval
379
+ *
380
+ * @param {CheckoutCreateResult} create_result
381
+ */
382
+ async capture(create_result) {
383
+ await this.stripe.paymentIntents.capture(
384
+ create_result.id,
385
+ {
386
+ amount_to_capture: create_result.amount
387
+ }
388
+ );
389
+
390
+ return this.status(create_result);
391
+ }
392
+
393
+ /**
394
+ * @description todo: logic for if user wanted capture at approval
395
+ *
396
+ * @param {CheckoutCreateResult} create_result
397
+ */
398
+ async refund(create_result) {
399
+ const refund = await this.stripe.refunds.create(
400
+ {
401
+ payment_intent: create_result.id,
402
+ amount: create_result.amount,
403
+ }
404
+ );
405
+
406
+ return this.status(create_result);
407
+ }
408
+
409
+ }
410
+
package/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from './adapter.js'
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@storecraft/payments-stripe",
3
+ "version": "1.0.0",
4
+ "description": "Official Storecraft <-> Stripe integration",
5
+ "license": "MIT",
6
+ "author": "Tomer Shalev (https://github.com/store-craft)",
7
+ "homepage": "https://github.com/store-craft/storecraft",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/store-craft/storecraft.git",
11
+ "directory": "packages/payments-stripe"
12
+ },
13
+ "keywords": [
14
+ "commerce",
15
+ "dashboard",
16
+ "code",
17
+ "storecraft"
18
+ ],
19
+ "type": "module",
20
+ "main": "index.js",
21
+ "types": "./types.public.d.ts",
22
+ "scripts": {
23
+ "payments-stripe:test": "uvu -c",
24
+ "payments-stripe:publish": "npm publish --access public"
25
+ },
26
+ "dependencies": {
27
+ "stripe": "^16.6.0"
28
+ },
29
+ "devDependencies": {
30
+ "@storecraft/core": "^1.0.0",
31
+ "@types/node": "^20.11.0",
32
+ "dotenv": "^16.3.1",
33
+ "uvu": "^0.5.6"
34
+ }
35
+ }
@@ -0,0 +1,11 @@
1
+ import 'dotenv/config';
2
+ import { test } from 'uvu';
3
+ import * as assert from 'uvu/assert';
4
+
5
+ test('todo', async () => {
6
+
7
+ assert.ok(true, 'todo')
8
+
9
+ });
10
+
11
+ test.run();
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compileOnSave": false,
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "allowJs": true,
6
+ "checkJs": true,
7
+ "target": "ESNext",
8
+ "resolveJsonModule": true,
9
+ "moduleResolution": "NodeNext",
10
+ "module": "NodeNext",
11
+ "composite": true,
12
+ },
13
+ "include": ["*", "*/*", "src/*"]
14
+ }
@@ -0,0 +1,37 @@
1
+ export * from './index.js';
2
+ import type { Stripe } from 'stripe'
3
+
4
+ /**
5
+ * @description gateway config
6
+ */
7
+ export type Config = {
8
+
9
+ /**
10
+ * @description `stripe` publishable key
11
+ */
12
+ publishable_key: string;
13
+
14
+ /**
15
+ * @description `stripe` private secret
16
+ */
17
+ secret_key: string;
18
+
19
+ /**
20
+ * @description (Optional) `webhook` Endpoint private secret in case
21
+ * you are configuring webhook for async payments
22
+ * [https://docs.stripe.com/webhooks?verify=check-signatures-library](https://docs.stripe.com/webhooks?verify=check-signatures-library)
23
+ */
24
+ webhook_endpoint_secret?: string;
25
+
26
+ /**
27
+ * @description config options for `stripe`
28
+ */
29
+ stripe_config?: Stripe.StripeConfig;
30
+
31
+ /**
32
+ * @description configure `intent` creation
33
+ */
34
+ stripe_intent_create_params?: Omit<Stripe.PaymentIntentCreateParams, 'amount'>;
35
+ }
36
+
37
+