@storecraft/payments-paypal 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 +43 -0
- package/adapter.html.js +136 -0
- package/adapter.js +377 -0
- package/adapter.utils.js +76 -0
- package/index.js +1 -0
- package/package.json +34 -0
- package/tests/storage.test.js +11 -0
- package/tsconfig.json +14 -0
- package/types.checkout_orders_v2.d.ts +6960 -0
- package/types.private.d.ts +6 -0
- package/types.public.d.ts +33 -0
package/README.md
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# Paypal payment gateway for **StoreCraft**
|
2
|
+
|
3
|
+
[paypal standard](https://developer.paypal.com/docs/checkout/standard/) integration
|
4
|
+
|
5
|
+
## Features
|
6
|
+
- Create checkouts with `AUTHORIZE` or `CAPTURE` intents
|
7
|
+
- `capture`, `void`, `refund` actions
|
8
|
+
- Get a readable and explainable `status`
|
9
|
+
- Supports both `prod` and `test` endpoints
|
10
|
+
|
11
|
+
```bash
|
12
|
+
npm i @storecraft/payments-paypal-standard
|
13
|
+
```
|
14
|
+
|
15
|
+
## Howto
|
16
|
+
|
17
|
+
```js
|
18
|
+
const config = {
|
19
|
+
env: 'prod',
|
20
|
+
client_id: '<get-from-your-paypal-dashboard>',
|
21
|
+
secret: '<get-from-your-paypal-dashboard>',
|
22
|
+
currency_code: 'USD',
|
23
|
+
intent_on_checkout: 'AUTHORIZE'
|
24
|
+
}
|
25
|
+
|
26
|
+
new PaypalStandard(config);
|
27
|
+
```
|
28
|
+
|
29
|
+
## Developer info and test
|
30
|
+
|
31
|
+
Integration examples
|
32
|
+
- https://developer.paypal.com/studio/checkout/standard/integrate
|
33
|
+
|
34
|
+
Credit Card Generator
|
35
|
+
- https://developer.paypal.com/tools/sandbox/card-testing/#link-creditcardgenerator
|
36
|
+
|
37
|
+
## todo:
|
38
|
+
- Add tests
|
39
|
+
- Think about adding more dynamic config
|
40
|
+
|
41
|
+
```text
|
42
|
+
Author: Tomer Shalev (tomer.shalev@gmail.com)
|
43
|
+
```
|
package/adapter.html.js
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
/**
|
2
|
+
*
|
3
|
+
* @description Official PayPal UI integration with `storecraft`.
|
4
|
+
*
|
5
|
+
* Test with dummy data with this generator
|
6
|
+
* https://developer.paypal.com/tools/sandbox/card-testing/#link-creditcardgenerator.
|
7
|
+
*
|
8
|
+
* Or use the following dummy details:
|
9
|
+
* - Card number: 4032033785933750
|
10
|
+
* - Expiry date: 12/2027
|
11
|
+
* - CVC code: 897
|
12
|
+
*
|
13
|
+
* @param {import("./types.public.js").Config} config
|
14
|
+
* @param {Partial<import("@storecraft/core/v-api").OrderData>} order_data
|
15
|
+
*/
|
16
|
+
export default function html_buy_ui(config, order_data) {
|
17
|
+
const orderData = order_data?.payment_gateway?.on_checkout_create;
|
18
|
+
|
19
|
+
return `
|
20
|
+
<!DOCTYPE html>
|
21
|
+
<html style="height: 100%; width: 100%">
|
22
|
+
<head>
|
23
|
+
<meta charset="UTF-8" />
|
24
|
+
<meta name="viewport" content="width=device-width; height=device-height, initial-scale=1.0" />
|
25
|
+
<title>PayPal JS SDK Standard Integration</title>
|
26
|
+
</head>
|
27
|
+
<body style='display: flex; flex-direction: column; justify-content: start; height: 100%; align-items: center;'>
|
28
|
+
<div id="paypal-button-container" style='width: 100%' ></div>
|
29
|
+
<p id="result-message"></p>
|
30
|
+
<!-- Initialize the JS-SDK -->
|
31
|
+
<script src="https://www.paypal.com/sdk/js?currency=${config.default_currency_code}&client-id=${config.client_id}&intent=${config.intent_on_checkout.toLowerCase()}"></script>
|
32
|
+
|
33
|
+
<!-- code -->
|
34
|
+
<script>
|
35
|
+
const resultMessage = (msg) => {
|
36
|
+
document.getElementById('result-message').innerHTML = msg
|
37
|
+
console.log(msg);
|
38
|
+
|
39
|
+
}
|
40
|
+
|
41
|
+
window.paypal
|
42
|
+
.Buttons(
|
43
|
+
{
|
44
|
+
style: {
|
45
|
+
shape: "rect",
|
46
|
+
layout: "vertical",
|
47
|
+
color: "gold",
|
48
|
+
label: "paypal",
|
49
|
+
disableMaxWidth: true
|
50
|
+
},
|
51
|
+
|
52
|
+
message: {
|
53
|
+
amount: ${order_data.pricing.total},
|
54
|
+
},
|
55
|
+
|
56
|
+
async createOrder() {
|
57
|
+
try {
|
58
|
+
if ('${orderData.id}') {
|
59
|
+
return '${orderData.id}';
|
60
|
+
}
|
61
|
+
const errorDetail = ${orderData?.details?.[0]};
|
62
|
+
const errorMessage = errorDetail
|
63
|
+
? (errorDetail.issue + errorDetail.description + "(" + '${orderData.debug_id}' + ")")
|
64
|
+
: JSON.stringify(orderData);
|
65
|
+
|
66
|
+
throw new Error(errorMessage);
|
67
|
+
} catch (error) {
|
68
|
+
console.error(error);
|
69
|
+
resultMessage("Could not initiate PayPal Checkout...<br><br> " + error);
|
70
|
+
}
|
71
|
+
},
|
72
|
+
|
73
|
+
async onApprove(data, actions) {
|
74
|
+
try {
|
75
|
+
const response = await fetch("/api/checkout/${order_data.id}/complete", {
|
76
|
+
method: "POST",
|
77
|
+
headers: {
|
78
|
+
"Content-Type": "application/json",
|
79
|
+
},
|
80
|
+
});
|
81
|
+
|
82
|
+
const orderData_main = await response.json();
|
83
|
+
const orderData = orderData_main.payment_gateway.on_checkout_complete;
|
84
|
+
|
85
|
+
// Three cases to handle:
|
86
|
+
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
|
87
|
+
// (2) Other non-recoverable errors -> Show a failure message
|
88
|
+
// (3) Successful transaction -> Show confirmation or thank you message
|
89
|
+
|
90
|
+
const errorDetail = orderData?.details?.[0];
|
91
|
+
|
92
|
+
if (errorDetail?.issue === "INSTRUMENT_DECLINED") {
|
93
|
+
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
|
94
|
+
// recoverable state, per
|
95
|
+
// https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/
|
96
|
+
return actions.restart();
|
97
|
+
} else if (errorDetail) {
|
98
|
+
// (2) Other non-recoverable errors -> Show a failure message
|
99
|
+
throw new Error(errorDetail.description + "(" + orderData.debug_id + ")");
|
100
|
+
}
|
101
|
+
// else if (!orderData.purchase_units) {
|
102
|
+
// throw new Error(JSON.stringify(orderData));
|
103
|
+
// }
|
104
|
+
else {
|
105
|
+
// (3) Successful transaction -> Show confirmation or thank you message
|
106
|
+
// Or go to another URL: actions.redirect('thank_you.html');
|
107
|
+
const transaction =
|
108
|
+
orderData?.purchase_units?.[0]?.payments?.captures?.[0] ||
|
109
|
+
orderData?.purchase_units?.[0]?.payments?.authorizations?.[0];
|
110
|
+
resultMessage(
|
111
|
+
"Transaction " + transaction?.status + ":" + transaction?.id +
|
112
|
+
" See console for all available details"
|
113
|
+
);
|
114
|
+
console.log(
|
115
|
+
"Capture result",
|
116
|
+
orderData,
|
117
|
+
JSON.stringify(orderData, null, 2)
|
118
|
+
);
|
119
|
+
}
|
120
|
+
} catch (error) {
|
121
|
+
console.error(error);
|
122
|
+
resultMessage(
|
123
|
+
"Sorry, your transaction could not be processed... " + error
|
124
|
+
);
|
125
|
+
}
|
126
|
+
}
|
127
|
+
|
128
|
+
}
|
129
|
+
).render("#paypal-button-container");
|
130
|
+
|
131
|
+
</script>
|
132
|
+
|
133
|
+
</body>
|
134
|
+
</html>
|
135
|
+
`
|
136
|
+
}
|
package/adapter.js
ADDED
@@ -0,0 +1,377 @@
|
|
1
|
+
import { CheckoutStatusEnum, PaymentOptionsEnum } from '@storecraft/core/v-api/types.api.enums.js';
|
2
|
+
import { fetch_with_auth, throw_bad_response } from './adapter.utils.js';
|
3
|
+
import { StorecraftError } from '@storecraft/core/v-api/utils.func.js';
|
4
|
+
import html_buy_ui from './adapter.html.js';
|
5
|
+
|
6
|
+
/**
|
7
|
+
* @typedef {import('./types.private.js').paypal_order} CreateResult
|
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, CreateResult>} payment_gateway
|
13
|
+
*/
|
14
|
+
|
15
|
+
/**
|
16
|
+
* @implements {payment_gateway}
|
17
|
+
*
|
18
|
+
* @description **Paypal Payment** gateway (https://developer.paypal.com/docs/checkout/)
|
19
|
+
*/
|
20
|
+
export class Paypal {
|
21
|
+
|
22
|
+
/** @type {Config} */ #_config;
|
23
|
+
|
24
|
+
/**
|
25
|
+
*
|
26
|
+
* @param {Config} config
|
27
|
+
*/
|
28
|
+
constructor(config) {
|
29
|
+
this.#_config = this.#validate_and_resolve_config(config);
|
30
|
+
}
|
31
|
+
|
32
|
+
/**
|
33
|
+
*
|
34
|
+
* @param {Config} config
|
35
|
+
*/
|
36
|
+
#validate_and_resolve_config(config) {
|
37
|
+
config = {
|
38
|
+
default_currency_code: 'USD',
|
39
|
+
env: 'prod',
|
40
|
+
intent_on_checkout: 'AUTHORIZE',
|
41
|
+
...config,
|
42
|
+
}
|
43
|
+
|
44
|
+
const is_valid = config.client_id && config.secret;
|
45
|
+
|
46
|
+
if(!is_valid) {
|
47
|
+
throw new StorecraftError(
|
48
|
+
`Payment gateway ${this.info.name ?? 'unknown'} has invalid config !!!
|
49
|
+
Missing client_id or secret`
|
50
|
+
)
|
51
|
+
}
|
52
|
+
|
53
|
+
return config;
|
54
|
+
}
|
55
|
+
|
56
|
+
get info() {
|
57
|
+
return {
|
58
|
+
name: 'Paypal payments',
|
59
|
+
description: `Set up standard and advanced payments to present payment buttons to your payers so they can pay with PayPal, debit and credit cards, Pay Later options, Venmo, and alternative payment methods.
|
60
|
+
You can get started quickly with this 15-minute copy-and-paste integration. If you have an older Checkout integration, you can upgrade your Checkout integration.`,
|
61
|
+
url: 'https://developer.paypal.com/docs/checkout/',
|
62
|
+
logo_url: 'https://www.paypalobjects.com/webstatic/mktg/logo/pp_cc_mark_37x23.jpg'
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
get config() {
|
67
|
+
return this.#_config;
|
68
|
+
}
|
69
|
+
|
70
|
+
get actions() {
|
71
|
+
return [
|
72
|
+
{
|
73
|
+
handle: 'capture',
|
74
|
+
name: 'Capture',
|
75
|
+
description: 'Capture an authorized payment'
|
76
|
+
},
|
77
|
+
{
|
78
|
+
handle: 'void',
|
79
|
+
name: 'Void',
|
80
|
+
description: 'Cancel an authorized payment'
|
81
|
+
},
|
82
|
+
{
|
83
|
+
handle: 'refund',
|
84
|
+
name: 'Refund',
|
85
|
+
description: 'Refund a captured payment'
|
86
|
+
},
|
87
|
+
]
|
88
|
+
}
|
89
|
+
|
90
|
+
/**
|
91
|
+
*
|
92
|
+
* @type {payment_gateway["invokeAction"]}
|
93
|
+
*/
|
94
|
+
invokeAction(action_handle) {
|
95
|
+
switch (action_handle) {
|
96
|
+
case 'capture':
|
97
|
+
return this.capture.bind(this);
|
98
|
+
case 'void':
|
99
|
+
return this.void.bind(this);
|
100
|
+
case 'refund':
|
101
|
+
return this.refund.bind(this);
|
102
|
+
|
103
|
+
default:
|
104
|
+
break;
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
/**
|
109
|
+
* @description (Optional) buy link ui
|
110
|
+
*
|
111
|
+
* @param {Partial<OrderData>} order
|
112
|
+
*
|
113
|
+
* @return {Promise<string>} html
|
114
|
+
*/
|
115
|
+
async onBuyLinkHtml(order) {
|
116
|
+
|
117
|
+
return html_buy_ui(
|
118
|
+
this.config, order
|
119
|
+
)
|
120
|
+
}
|
121
|
+
|
122
|
+
/**
|
123
|
+
* @description TODO: the user prefers to capture intent instead
|
124
|
+
*
|
125
|
+
* @param {OrderData} order
|
126
|
+
*
|
127
|
+
* @return {Promise<CreateResult>}
|
128
|
+
*/
|
129
|
+
async onCheckoutCreate(order) {
|
130
|
+
const { default_currency_code: currency_code, intent_on_checkout } = this.config;
|
131
|
+
|
132
|
+
/** @type {import('./types.private.js').paypal_order_request} */
|
133
|
+
const body = {
|
134
|
+
intent: intent_on_checkout==='AUTHORIZE' ? 'AUTHORIZE' : 'CAPTURE',
|
135
|
+
purchase_units: [
|
136
|
+
{
|
137
|
+
custom_id: order.id,
|
138
|
+
amount: {
|
139
|
+
currency_code: currency_code,
|
140
|
+
value: order.pricing.total.toFixed(2),
|
141
|
+
},
|
142
|
+
invoice_id: `${order.id}_${Date.now()}`
|
143
|
+
},
|
144
|
+
],
|
145
|
+
}
|
146
|
+
|
147
|
+
const response = await fetch_with_auth(
|
148
|
+
this.config, 'v2/checkout/orders', {
|
149
|
+
method: 'post',
|
150
|
+
body: JSON.stringify(body),
|
151
|
+
}
|
152
|
+
);
|
153
|
+
|
154
|
+
await throw_bad_response(response);
|
155
|
+
|
156
|
+
/** @type {CreateResult} */
|
157
|
+
const json = await response.json();
|
158
|
+
return json;
|
159
|
+
}
|
160
|
+
|
161
|
+
/**
|
162
|
+
* @description todo: logic for if user wanted capture at approval
|
163
|
+
*
|
164
|
+
* @param {CreateResult} create_result
|
165
|
+
*
|
166
|
+
* @return {ReturnType<payment_gateway["onCheckoutComplete"]>}
|
167
|
+
*/
|
168
|
+
async onCheckoutComplete(create_result) {
|
169
|
+
// the url based on authorize or capture intent
|
170
|
+
const url = this.config.intent_on_checkout==='AUTHORIZE' ?
|
171
|
+
`v2/checkout/orders/${create_result.id}/authorize` :
|
172
|
+
`v2/checkout/orders/${create_result.id}/capture`;
|
173
|
+
const response = await fetch_with_auth(
|
174
|
+
this.config, url,
|
175
|
+
{ method: 'post' }
|
176
|
+
);
|
177
|
+
|
178
|
+
await throw_bad_response(response);
|
179
|
+
|
180
|
+
/** @type {import('./types.private.js').paypal_order} */
|
181
|
+
const payload = await response.json();
|
182
|
+
|
183
|
+
let status;
|
184
|
+
switch(payload.status) {
|
185
|
+
case 'COMPLETED':
|
186
|
+
status = {
|
187
|
+
payment: PaymentOptionsEnum.authorized,
|
188
|
+
checkout: CheckoutStatusEnum.complete
|
189
|
+
}
|
190
|
+
break;
|
191
|
+
case 'PAYER_ACTION_REQUIRED':
|
192
|
+
status = {
|
193
|
+
checkout: CheckoutStatusEnum.requires_action
|
194
|
+
}
|
195
|
+
break;
|
196
|
+
default:
|
197
|
+
status = {
|
198
|
+
checkout: CheckoutStatusEnum.failed
|
199
|
+
}
|
200
|
+
break;
|
201
|
+
}
|
202
|
+
|
203
|
+
return {
|
204
|
+
status,
|
205
|
+
onCheckoutComplete: payload
|
206
|
+
}
|
207
|
+
}
|
208
|
+
|
209
|
+
/**
|
210
|
+
* @description Fetch the order and analyze it's status
|
211
|
+
*
|
212
|
+
*
|
213
|
+
* @param {CreateResult} create_result
|
214
|
+
*
|
215
|
+
*
|
216
|
+
* @returns {Promise<PaymentGatewayStatus>}
|
217
|
+
*/
|
218
|
+
async status(create_result) {
|
219
|
+
const o = await this.retrieve_order(create_result);
|
220
|
+
const purchase_unit = o.purchase_units?.[0];
|
221
|
+
const authorization = purchase_unit?.payments?.authorizations?.[0];
|
222
|
+
const capture = purchase_unit?.payments?.captures?.[0];
|
223
|
+
const refund = purchase_unit?.payments?.refunds?.[0];
|
224
|
+
|
225
|
+
/** @type {PaymentGatewayStatus} */
|
226
|
+
const stat = {
|
227
|
+
messages: [],
|
228
|
+
actions: this.actions
|
229
|
+
}
|
230
|
+
|
231
|
+
/** @param {typeof authorization | typeof capture | typeof refund} u */
|
232
|
+
const get_values = u => {
|
233
|
+
return {
|
234
|
+
currency_code: u.amount.currency_code,
|
235
|
+
price: u.amount.value,
|
236
|
+
reason: u?.status_details?.reason,
|
237
|
+
create_time: new Date(u?.create_time).toUTCString(),
|
238
|
+
update_time: new Date(u?.update_time).toUTCString(),
|
239
|
+
id: u?.id, status: u.status
|
240
|
+
}
|
241
|
+
}
|
242
|
+
|
243
|
+
if(refund) {
|
244
|
+
const v = get_values(refund);
|
245
|
+
stat.messages = [
|
246
|
+
`**${v.price}${v.currency_code}** were tried to be \`REFUNDED\` at \`${v.create_time}\``,
|
247
|
+
`The status is \`${v.status}\`, updated at \`${v.update_time}\``,
|
248
|
+
v.reason && `The reason for this status is \`${v.reason}\``,
|
249
|
+
`Refund ID is \`${v.id}\`.`
|
250
|
+
].filter(Boolean);
|
251
|
+
|
252
|
+
} else if(capture) {
|
253
|
+
const v = get_values(capture);
|
254
|
+
stat.messages = [
|
255
|
+
`**${v.price}${v.currency_code}** were tried to be \`CAPTURED\` at \`${v.create_time}\``,
|
256
|
+
`The status is \`${v.status}\`, updated at \`${v.update_time}\``,
|
257
|
+
v.reason && `The reason for this status is \`${v.reason}\``,
|
258
|
+
`Capture ID is \`${v.id}\`.`
|
259
|
+
].filter(Boolean);
|
260
|
+
|
261
|
+
} else if (authorization) {
|
262
|
+
const v = get_values(authorization);
|
263
|
+
const expiration_time = new Date(authorization?.expiration_time).toUTCString();
|
264
|
+
stat.messages = [
|
265
|
+
`**${v.price}${v.currency_code}** were tried to be \`AUTHORIZED\` at \`${v.create_time}\``,
|
266
|
+
`The status is \`${authorization.status}\`, updated at \`${v.update_time}\``,
|
267
|
+
`The authorization will expire at \`${expiration_time}\``,
|
268
|
+
v.reason && `The reason for this status is \`${v.reason}\``,
|
269
|
+
`Authorization ID is \`${v.id}\`.`
|
270
|
+
].filter(Boolean);
|
271
|
+
|
272
|
+
} else { // just an intent
|
273
|
+
const currency_code = purchase_unit.amount.currency_code;
|
274
|
+
const price = purchase_unit.amount.value;
|
275
|
+
stat.messages = [
|
276
|
+
`An intent to **${o.intent}** of **${price}${currency_code}** was initiated`,
|
277
|
+
`The status is \`${o.status}\``
|
278
|
+
];
|
279
|
+
}
|
280
|
+
|
281
|
+
return stat;
|
282
|
+
}
|
283
|
+
|
284
|
+
/**
|
285
|
+
* @description [https://developer.paypal.com/api/rest/webhooks/rest/](https://developer.paypal.com/api/rest/webhooks/rest/)
|
286
|
+
*
|
287
|
+
* @param {Request} request
|
288
|
+
*/
|
289
|
+
async webhook(request) {
|
290
|
+
return null;
|
291
|
+
}
|
292
|
+
|
293
|
+
/**
|
294
|
+
* @description Retrieve latest order payload
|
295
|
+
*
|
296
|
+
* @param {CreateResult} create_result first create result, holds paypal id
|
297
|
+
*
|
298
|
+
* @return {Promise<import('./types.private.js').paypal_order>}
|
299
|
+
*/
|
300
|
+
retrieve_order = async (create_result) => {
|
301
|
+
const response = await fetch_with_auth(
|
302
|
+
this.config,
|
303
|
+
`v2/checkout/orders/${create_result.id}`,
|
304
|
+
{ method: 'get' }
|
305
|
+
);
|
306
|
+
|
307
|
+
await throw_bad_response(response);
|
308
|
+
|
309
|
+
/** @type {import('./types.private.js').paypal_order} */
|
310
|
+
const jsonData = await response.json();
|
311
|
+
return jsonData;
|
312
|
+
}
|
313
|
+
|
314
|
+
// actions
|
315
|
+
|
316
|
+
/**
|
317
|
+
* @description todo: logic for if user wanted capture at approval
|
318
|
+
*
|
319
|
+
* @param {CreateResult} create_result
|
320
|
+
*/
|
321
|
+
async void(create_result) {
|
322
|
+
const paypal_order = await this.retrieve_order(create_result);
|
323
|
+
const authorization_id = paypal_order?.purchase_units?.[0]?.payments?.authorizations?.[0]?.id
|
324
|
+
|
325
|
+
const response = await fetch_with_auth(
|
326
|
+
this.config,
|
327
|
+
`v2/payments/authorizations/${authorization_id}/void`,
|
328
|
+
{ method: 'post' }
|
329
|
+
);
|
330
|
+
|
331
|
+
await throw_bad_response(response);
|
332
|
+
|
333
|
+
return this.status(create_result);
|
334
|
+
}
|
335
|
+
|
336
|
+
/**
|
337
|
+
* @description todo: logic for if user wanted capture at approval
|
338
|
+
*
|
339
|
+
* @param {CreateResult} create_result
|
340
|
+
*/
|
341
|
+
async capture(create_result) {
|
342
|
+
const paypal_order = await this.retrieve_order(create_result);
|
343
|
+
const authorization_id = paypal_order?.purchase_units?.[0]?.payments?.authorizations?.[0]?.id
|
344
|
+
|
345
|
+
const response = await fetch_with_auth(
|
346
|
+
this.config,
|
347
|
+
`v2/payments/authorizations/${authorization_id}/capture`,
|
348
|
+
{ method: 'post' }
|
349
|
+
);
|
350
|
+
|
351
|
+
await throw_bad_response(response);
|
352
|
+
|
353
|
+
return this.status(create_result);
|
354
|
+
}
|
355
|
+
|
356
|
+
/**
|
357
|
+
* @description todo: logic for if user wanted capture at approval
|
358
|
+
*
|
359
|
+
* @param {CreateResult} create_result
|
360
|
+
*/
|
361
|
+
async refund(create_result) {
|
362
|
+
const paypal_order = await this.retrieve_order(create_result);
|
363
|
+
const capture_id = paypal_order?.purchase_units?.[0]?.payments?.captures?.[0]?.id;
|
364
|
+
|
365
|
+
const response = await fetch_with_auth(
|
366
|
+
this.config,
|
367
|
+
`v2/payments/captures/${capture_id}/refund`,
|
368
|
+
{ method: 'post' }
|
369
|
+
);
|
370
|
+
|
371
|
+
await throw_bad_response(response);
|
372
|
+
|
373
|
+
return this.status(create_result);
|
374
|
+
}
|
375
|
+
|
376
|
+
}
|
377
|
+
|
package/adapter.utils.js
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
|
2
|
+
export const endpoints = {
|
3
|
+
test: 'https://api-m.sandbox.paypal.com',
|
4
|
+
prod: 'https://api-m.paypal.com'
|
5
|
+
}
|
6
|
+
|
7
|
+
/**
|
8
|
+
* @typedef {import("./types.public.js").Config} Config
|
9
|
+
*/
|
10
|
+
|
11
|
+
/**
|
12
|
+
* @param {Config} config
|
13
|
+
*/
|
14
|
+
export const get_endpoint = (config) => endpoints[config.env==='test' ? 'test': 'prod'];
|
15
|
+
|
16
|
+
/** @param {Response} response */
|
17
|
+
export const throw_bad_response = async (response) => {
|
18
|
+
if(!response.ok) throw new Error(await response.text());
|
19
|
+
}
|
20
|
+
|
21
|
+
/**
|
22
|
+
* get access token if it has expired
|
23
|
+
*
|
24
|
+
* @param {Config} config
|
25
|
+
* @returns {Promise<{ access_token: string, endpoint: string }>}
|
26
|
+
*/
|
27
|
+
export const getAccessToken = async (config) => {
|
28
|
+
|
29
|
+
const { client_id, secret } = config;
|
30
|
+
const endpoint = get_endpoint(config);
|
31
|
+
|
32
|
+
const auth = btoa(client_id + ':' + secret);
|
33
|
+
// const expired = current_auth.expires_at - Date.now() <= 10*1000
|
34
|
+
// if(!expired)
|
35
|
+
// return current_auth.latest_auth_response.access_token
|
36
|
+
|
37
|
+
const response = await fetch(
|
38
|
+
`${endpoint}/v1/oauth2/token`,
|
39
|
+
{
|
40
|
+
method: 'post',
|
41
|
+
body: 'grant_type=client_credentials',
|
42
|
+
headers: {
|
43
|
+
Authorization: `Basic ${auth}`,
|
44
|
+
},
|
45
|
+
}
|
46
|
+
);
|
47
|
+
|
48
|
+
await throw_bad_response(response);
|
49
|
+
|
50
|
+
const jsonData = await response.json()
|
51
|
+
// current_auth.latest_auth_response = jsonData
|
52
|
+
return {
|
53
|
+
endpoint,
|
54
|
+
access_token: jsonData.access_token
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
/**
|
59
|
+
* make a request, taking into account auth and endpoints for test and prod
|
60
|
+
* @param {Config} config
|
61
|
+
* @param {string} path relative path, i.e `v2/checkout/orders/..`
|
62
|
+
* @param {RequestInit} init
|
63
|
+
*/
|
64
|
+
export const fetch_with_auth = async (config, path, init={}) => {
|
65
|
+
const { access_token, endpoint } = await getAccessToken(config);
|
66
|
+
|
67
|
+
return fetch(
|
68
|
+
`${endpoint}/${path}`, {
|
69
|
+
...init,
|
70
|
+
headers: {
|
71
|
+
'Content-Type': 'application/json',
|
72
|
+
Authorization: `Bearer ${access_token}`,
|
73
|
+
},
|
74
|
+
}
|
75
|
+
);
|
76
|
+
}
|
package/index.js
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export * from './adapter.js'
|
package/package.json
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
{
|
2
|
+
"name": "@storecraft/payments-paypal",
|
3
|
+
"version": "1.0.0",
|
4
|
+
"description": "Official Paypal integration with Storecraft",
|
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-paypal"
|
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-paypal:test": "uvu -c",
|
24
|
+
"payments-paypal:publish": "npm publish --access public"
|
25
|
+
},
|
26
|
+
"dependencies": {
|
27
|
+
},
|
28
|
+
"devDependencies": {
|
29
|
+
"@storecraft/core": "^1.0.0",
|
30
|
+
"@types/node": "^20.11.0",
|
31
|
+
"uvu": "^0.5.6",
|
32
|
+
"dotenv": "^16.3.1"
|
33
|
+
}
|
34
|
+
}
|
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
|
+
}
|