tonder-web-sdk 1.11.11 → 1.12.0-beta.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.
@@ -1,30 +1,22 @@
1
- import { apmItemsTemplate, cardItemsTemplate, cardTemplate } from '../helpers/template.js'
2
- import { cardTemplateSkeleton } from '../helpers/template-skeleton.js'
1
+ import {apmItemsTemplate, cardItemsTemplate, cardTemplate} from '../helpers/template.js'
3
2
  import {
4
- getBusiness,
5
- customerRegister,
6
- createOrder,
7
- createPayment,
8
- startCheckoutRouter,
9
- getOpenpayDeviceSessionID,
10
- getCustomerCards,
11
- registerCard,
12
- deleteCustomerCard,
13
- getCustomerAPMs
14
- } from '../data/api';
15
- import {
16
- showError,
17
- getBrowserInfo,
3
+ clearSpace,
4
+ injectMercadoPagoSecurity,
18
5
  mapCards,
19
- showMessage,
20
- clearSpace
6
+ showError,
7
+ showMessage
21
8
  } from '../helpers/utils';
22
- import { initSkyflow } from '../helpers/skyflow'
23
- import { ThreeDSHandler } from './3dsHandler.js';
24
- import { globalLoader } from './globalLoader.js';
25
-
9
+ import {initSkyflow} from '../helpers/skyflow'
10
+ import {globalLoader} from './globalLoader.js';
11
+ import {BaseInlineCheckout} from "./BaseInlineCheckout";
12
+ import {
13
+ fetchCustomerCards,
14
+ removeCustomerCard,
15
+ saveCustomerCard,
16
+ fetchCustomerAPMs
17
+ } from "../data";
26
18
 
27
- export class InlineCheckout {
19
+ export class InlineCheckout extends BaseInlineCheckout{
28
20
  static injected = false;
29
21
  static cardsInjected = false
30
22
  static apmsInjected = false
@@ -32,7 +24,6 @@ export class InlineCheckout {
32
24
  deletingCards = [];
33
25
  customer = {}
34
26
  items = []
35
- baseUrl = null
36
27
  collectContainer = null
37
28
  merchantData = {}
38
29
  cartTotal = null
@@ -55,36 +46,14 @@ export class InlineCheckout {
55
46
  mode = "stage",
56
47
  apiKey,
57
48
  returnUrl,
58
- successUrl,
59
49
  renderPaymentButton = false,
60
50
  callBack = () => { },
61
51
  styles
62
52
  }) {
63
- this.apiKeyTonder = apiKey;
64
- this.returnUrl = returnUrl;
65
- this.successUrl = successUrl;
53
+ super({ mode, apiKey, returnUrl, callBack });
66
54
  this.renderPaymentButton = renderPaymentButton;
67
- this.callBack = callBack;
68
55
  this.customStyles = styles
69
- this.mode = mode
70
- this.baseUrl = this.#getBaseUrl()
71
-
72
- this.abortController = new AbortController()
73
56
  this.abortRefreshCardsController = new AbortController()
74
- this.process3ds = new ThreeDSHandler(
75
- { apiKey: apiKey, baseUrl: this.baseUrl, successUrl: successUrl }
76
- )
77
- }
78
-
79
- #getBaseUrl() {
80
- const modeUrls = {
81
- 'production': 'https://app.tonder.io',
82
- 'sandbox': 'https://sandbox.tonder.io',
83
- 'stage': 'https://stage.tonder.io',
84
- 'development': 'http://localhost:8000',
85
- };
86
-
87
- return modeUrls[this.mode] || modeUrls['stage']
88
57
  }
89
58
 
90
59
  #mountPayButton() {
@@ -117,90 +86,7 @@ export class InlineCheckout {
117
86
  }
118
87
  }
119
88
 
120
- async handle3dsRedirect(response) {
121
- const iframe = response?.next_action?.iframe_resources?.iframe
122
-
123
- if (iframe) {
124
- this.process3ds.loadIframe().then(() => {
125
- //TODO: Check if this will be necessary on the frontend side
126
- // after some the tests in production, since the 3DS process
127
- // doesn't works properly on the sandbox environment
128
- // setTimeout(() => {
129
- // process3ds.verifyTransactionStatus();
130
- // }, 10000);
131
- this.process3ds.verifyTransactionStatus();
132
- }).catch((error) => {
133
- console.log('Error loading iframe:', error)
134
- })
135
- } else {
136
- const redirectUrl = this.process3ds.getRedirectUrl()
137
- if (redirectUrl) {
138
- this.process3ds.redirectToChallenge()
139
- } else {
140
- return response;
141
- }
142
- }
143
- }
144
-
145
- payment(data) {
146
- return new Promise(async (resolve, reject) => {
147
- try {
148
- this.#handleCustomer(data.customer)
149
- this.setCartTotal(data.cart?.total)
150
- this.setCartItems(data.cart?.items)
151
- this.#handleMetadata(data)
152
- this.#handleCurrency(data)
153
- this.#handleCard(data)
154
- const response = await this.#checkout()
155
- this.process3ds.setPayload(response)
156
- this.callBack(response);
157
- const payload = await this.handle3dsRedirect(response)
158
- if (payload) {
159
- resolve(response);
160
- }
161
- } catch (error) {
162
- reject(error);
163
- }
164
- });
165
- }
166
-
167
- #handleCustomer(customer) {
168
- if (!customer) return
169
-
170
- this.firstName = customer?.firstName
171
- this.lastName = customer?.lastName
172
- this.country = customer?.country
173
- this.address = customer?.street
174
- this.city = customer?.city
175
- this.state = customer?.state
176
- this.postCode = customer?.postCode
177
- this.email = customer?.email
178
- this.phone = customer?.phone
179
- this.customer = customer
180
- }
181
-
182
- #handleMetadata(data) {
183
- this.metadata = data?.metadata
184
- }
185
-
186
- #handleCurrency(data) {
187
- this.currency = data?.currency
188
- }
189
-
190
- #handleCard(data) {
191
- this.card = data?.card
192
- }
193
-
194
- setCartItems(items) {
195
- this.cartItems = items
196
- }
197
-
198
- configureCheckout(data) {
199
- if ('customer' in data)
200
- this.#handleCustomer(data['customer'])
201
- }
202
-
203
- setCartTotal(total) {
89
+ _setCartTotal(total) {
204
90
  this.cartTotal = total
205
91
  this.#updatePayButton()
206
92
  }
@@ -236,47 +122,6 @@ export class InlineCheckout {
236
122
  });
237
123
  }
238
124
 
239
- async verify3dsTransaction () {
240
- globalLoader.show()
241
- const result3ds = await this.process3ds.verifyTransactionStatus()
242
- const resultCheckout = await this.resumeCheckout(result3ds)
243
- this.process3ds.setPayload(resultCheckout)
244
- globalLoader.remove()
245
- return this.handle3dsRedirect(resultCheckout)
246
- }
247
-
248
-
249
- async resumeCheckout(response) {
250
- // Stop the routing process if the transaction is either hard declined or successful
251
- if (response?.decline?.error_type === "Hard") {
252
- return response
253
- }
254
-
255
- if (["Success", "Authorized"].includes(response?.transaction_status)) {
256
- return response;
257
- }
258
-
259
- if (response) {
260
- globalLoader.show()
261
- const routerItems = {
262
- checkout_id: response?.checkout?.id,
263
- };
264
- try {
265
- const routerResponse = await startCheckoutRouter(
266
- this.baseUrl,
267
- this.apiKeyTonder,
268
- routerItems
269
- );
270
- return routerResponse
271
- } catch (error) {
272
- // throw error
273
- } finally {
274
- globalLoader.remove()
275
- }
276
- return response
277
- }
278
- }
279
-
280
125
  #mount(containerTonderCheckout) {
281
126
  containerTonderCheckout.innerHTML = cardTemplate({renderPaymentButton: this.renderPaymentButton, customStyles: this.customStyles});
282
127
  globalLoader.show()
@@ -284,22 +129,9 @@ export class InlineCheckout {
284
129
  InlineCheckout.injected = true;
285
130
  }
286
131
 
287
- async #fetchMerchantData() {
288
- this.merchantData = await getBusiness(
289
- this.baseUrl,
290
- this.apiKeyTonder,
291
- this.abortController.signal
292
- );
293
- return this.merchantData
294
- }
295
-
296
- async getCustomer(customer, signal) {
297
- return await customerRegister(this.baseUrl, this.apiKeyTonder, customer, signal);
298
- }
299
-
300
132
  async #mountAPMs() {
301
133
  try{
302
- const apms = await getCustomerAPMs(this.baseUrl, this.apiKeyTonder, "?status=active&page_size=10000");
134
+ const apms = await fetchCustomerAPMs(this.baseUrl, this.apiKeyTonder);
303
135
  if(apms && apms['results'] && apms['results'].length > 0){
304
136
  this.apmsData = apms['results']
305
137
  this.#loadAPMList(apms['results'])
@@ -309,18 +141,23 @@ export class InlineCheckout {
309
141
  }
310
142
  }
311
143
 
312
- async #mountTonder(getCards = false) {
144
+ async #mountTonder(getCards = true) {
313
145
  this.#mountPayButton()
146
+ await this._initializeCheckout()
314
147
  try {
315
148
  const {
316
149
  vault_id,
317
- vault_url,
318
- } = await this.#fetchMerchantData();
150
+ vault_url
151
+ } = this.merchantData;
319
152
  if (this.email && getCards) {
320
- const customerResponse = await this.getCustomer({ email: this.email });
153
+ const customerResponse = await this._getCustomer({ email: this.email });
321
154
  if ("auth_token" in customerResponse) {
322
155
  const { auth_token } = customerResponse
323
- const cards = await getCustomerCards(this.baseUrl, auth_token);
156
+ const cards = await fetchCustomerCards(
157
+ this.baseUrl,
158
+ auth_token,
159
+ this.merchantData.business.pk
160
+ );
324
161
 
325
162
  if ("cards" in cards) {
326
163
  const cardsMapped = cards.cards.map(mapCards)
@@ -328,7 +165,7 @@ export class InlineCheckout {
328
165
  }
329
166
  }
330
167
  }
331
-
168
+
332
169
  await this.#mountAPMs();
333
170
 
334
171
  this.collectContainer = await initSkyflow(
@@ -375,16 +212,14 @@ export class InlineCheckout {
375
212
  }
376
213
  }
377
214
 
378
- async #checkout() {
215
+ async _checkout() {
379
216
  try {
380
217
  document.querySelector("#tonderPayButton").disabled = true;
381
218
  } catch (error) {
382
219
  }
220
+ const { business } = this.merchantData
221
+ let cardTokens;
383
222
 
384
- const { openpay_keys, reference, business } = this.merchantData
385
- const total = Number(this.cartTotal)
386
-
387
- let cardTokens = null;
388
223
  if (this.radioChecked === "new" || this.radioChecked === undefined) {
389
224
  cardTokens = await this.#getCardTokens();
390
225
  } else {
@@ -393,105 +228,47 @@ export class InlineCheckout {
393
228
  }
394
229
  }
395
230
  try {
396
- let deviceSessionIdTonder;
397
- if (openpay_keys.merchant_id && openpay_keys.public_key) {
398
- deviceSessionIdTonder = await getOpenpayDeviceSessionID(
399
- openpay_keys.merchant_id,
400
- openpay_keys.public_key,
401
- this.abortController.signal
402
- );
403
- }
404
-
405
- const { id, auth_token } = await this.getCustomer(
231
+ const customerData = await this._getCustomer(
406
232
  this.customer,
407
233
  this.abortController.signal
408
234
  )
235
+ const { auth_token } = customerData;
409
236
  if (auth_token && this.email) {
410
237
  const saveCard = document.getElementById("save-checkout-card");
411
238
  if (saveCard && "checked" in saveCard && saveCard.checked) {
412
- await registerCard(this.baseUrl, auth_token, { skyflow_id: cardTokens.skyflow_id });
239
+ try {
240
+ await saveCustomerCard(this.baseUrl, auth_token, business.pk, {
241
+ skyflow_id: cardTokens.skyflow_id,
242
+ });
243
+ showMessage("Tarjeta registrada con éxito", this.collectorIds.msgNotification);
244
+ } catch (error) {
245
+ if (error?.message) {
246
+ showError(error.message)
247
+ }
248
+ }
413
249
 
414
250
  this.cardsInjected = false;
415
251
 
416
- const cards = await getCustomerCards(this.baseUrl, auth_token);
252
+ const cards = await fetchCustomerCards(
253
+ this.baseUrl,
254
+ auth_token,
255
+ this.merchantData.business.pk,
256
+ );
417
257
  if ("cards" in cards) {
418
258
  const cardsMapped = cards.cards.map((card) => mapCards(card))
419
259
  this.#loadCardsList(cardsMapped, auth_token)
420
260
  }
421
-
422
- showMessage("Tarjeta registrada con éxito", this.collectorIds.msgNotification);
423
-
424
261
  }
425
262
  }
426
- var orderItems = {
427
- business: this.apiKeyTonder,
428
- client: auth_token,
429
- billing_address_id: null,
430
- shipping_address_id: null,
431
- amount: total,
432
- status: "A",
433
- reference: reference,
434
- is_oneclick: true,
435
- items: this.cartItems,
436
- };
437
- const jsonResponseOrder = await createOrder(
438
- this.baseUrl,
439
- this.apiKeyTonder,
440
- orderItems
441
- );
442
-
443
- // Create payment
444
- const now = new Date();
445
- const dateString = now.toISOString();
446
-
447
- var paymentItems = {
448
- business_pk: business.pk,
449
- client_id: id,
450
- amount: total,
451
- date: dateString,
452
- order_id: jsonResponseOrder.id,
453
- };
454
- const jsonResponsePayment = await createPayment(
455
- this.baseUrl,
456
- this.apiKeyTonder,
457
- paymentItems
458
- );
459
263
 
460
264
  const selected_apm = this.apmsData ? this.apmsData.find((iapm) => iapm.pk === this.radioChecked):{};
461
-
462
- // Checkout router
463
- const routerItems = {
464
- name: this.firstName || "",
465
- last_name: this.lastName || "",
466
- email_client: this.email,
467
- phone_number: this.phone,
468
- return_url: this.returnUrl,
469
- id_product: "no_id",
470
- quantity_product: 1,
471
- id_ship: "0",
472
- instance_id_ship: "0",
473
- amount: total,
474
- title_ship: "shipping",
475
- description: "transaction",
476
- device_session_id: deviceSessionIdTonder ? deviceSessionIdTonder : null,
477
- token_id: "",
478
- order_id: jsonResponseOrder.id,
479
- business_id: business.pk,
480
- payment_id: jsonResponsePayment.pk,
481
- source: 'sdk',
482
- metadata: this.metadata,
483
- browser_info: getBrowserInfo(),
484
- currency: this.currency,
265
+
266
+ const jsonResponseRouter = await this._handleCheckout({
485
267
  ...( selected_apm && Object.keys(selected_apm).length > 0
486
- ? {payment_method: selected_apm.payment_method}
487
- : {card: cardTokens}
488
- )
489
- };
490
- const jsonResponseRouter = await startCheckoutRouter(
491
- this.baseUrl,
492
- this.apiKeyTonder,
493
- routerItems
494
- );
268
+ ? {payment_method: selected_apm.payment_method}
269
+ : {card: cardTokens}),
270
+ customer: customerData
271
+ });
495
272
 
496
273
  if (jsonResponseRouter) {
497
274
  try {
@@ -552,6 +329,7 @@ export class InlineCheckout {
552
329
  for (const cardButton of cardsButtons) {
553
330
  cardButton.addEventListener("click", (event) => {
554
331
  event.preventDefault();
332
+ event.stopImmediatePropagation();
555
333
  this.#handleDeleteCardButtonClick(token, cardButton)
556
334
  }, false);
557
335
  }
@@ -589,7 +367,8 @@ export class InlineCheckout {
589
367
  this.abortRefreshCardsController.abort();
590
368
  this.abortRefreshCardsController = new AbortController();
591
369
  }
592
- await deleteCustomerCard(this.baseUrl, customerToken, skyflow_id)
370
+ const businessId = this.merchantData.business.pk
371
+ await removeCustomerCard(this.baseUrl, customerToken, skyflow_id, businessId)
593
372
  } catch {
594
373
  } finally {
595
374
  this.deletingCards = this.deletingCards.filter(id => id !== skyflow_id);
@@ -600,7 +379,12 @@ export class InlineCheckout {
600
379
  async #refreshCardOnDelete(customerToken) {
601
380
  if (this.deletingCards.length > 0) return;
602
381
  this.cardsInjected = false
603
- const cards = await getCustomerCards(this.baseUrl, customerToken, "", this.abortRefreshCardsController.signal)
382
+ const cards = await fetchCustomerCards(
383
+ this.baseUrl,
384
+ customerToken,
385
+ this.merchantData.business.pk,
386
+ this.abortRefreshCardsController.signal
387
+ )
604
388
  if ("cards" in cards) {
605
389
  const cardsMapped = cards.cards.map(mapCards)
606
390
  this.#loadCardsList(cardsMapped, customerToken)
@@ -0,0 +1,44 @@
1
+ import {
2
+ buildErrorResponse,
3
+ buildErrorResponseFromCatch,
4
+ } from "../helpers/utils";
5
+
6
+ /**
7
+ * Fetches Alternative Payment Methods (APMs) of a customer.
8
+ * @param {string} baseUrl - The base URL of the API.
9
+ * @param {string} apiKey - The API key for authentication.
10
+ * @param params - The query params to filter APMs
11
+ * @param {AbortSignal} signal - The abort signal to cancel the request.
12
+ * @returns {Promise<Object>} The available APMs.
13
+ */
14
+ export async function fetchCustomerAPMs(
15
+ baseUrl,
16
+ apiKey,
17
+ params = {
18
+ status: "active",
19
+ pagesize: "10000",
20
+ },
21
+ signal = null,
22
+ ) {
23
+ try {
24
+ const queryString = new URLSearchParams(params).toString();
25
+
26
+ const response = await fetch(
27
+ `${baseUrl}/api/v1/payment_methods?${queryString}`,
28
+ {
29
+ method: "GET",
30
+ headers: {
31
+ Authorization: `Token ${apiKey}`,
32
+ "Content-Type": "application/json",
33
+ },
34
+ signal,
35
+ },
36
+ );
37
+
38
+ if (response.ok) return await response.json();
39
+ const res_json = await response.json();
40
+ throw await buildErrorResponse(response, res_json);
41
+ } catch (error) {
42
+ throw buildErrorResponseFromCatch(error);
43
+ }
44
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Fetches business information.
3
+ * @param {string} baseUrl - The base URL of the Tonder API.
4
+ * @param {string} apiKey - The API key for authentication.
5
+ * @param {AbortSignal} signal - The abort signal to cancel the request.
6
+ * @returns {Promise<Object>} The business information.
7
+ */
8
+ export async function fetchBusiness(baseUrl, apiKey, signal) {
9
+ const getBusiness = await fetch(
10
+ `${baseUrl}/api/v1/payments/business/${apiKey}`,
11
+ {
12
+ headers: {
13
+ Authorization: `Token ${apiKey}`,
14
+ },
15
+ signal: signal,
16
+ },
17
+ );
18
+ return await getBusiness.json();
19
+ }
@@ -0,0 +1,116 @@
1
+ import {
2
+ buildErrorResponse,
3
+ buildErrorResponseFromCatch,
4
+ } from "../helpers/utils";
5
+
6
+ /**
7
+ * Saves/Update a customer's card information.
8
+ * @param {string} baseUrl - The base URL of the API.
9
+ * @param {string} customerToken - The customer's authentication token.
10
+ * @param {string} businessId - The business ID.
11
+ * @param {Object} data - The card information to be saved.
12
+ * @returns {Promise<Object>} The saved card data.
13
+ */
14
+ export async function saveCustomerCard(
15
+ baseUrl,
16
+ customerToken,
17
+ businessId,
18
+ data,
19
+ ) {
20
+ try {
21
+ const url = `${baseUrl}/api/v1/business/${businessId}/cards/`;
22
+ const response = await fetch(url, {
23
+ method: "POST",
24
+ headers: {
25
+ Authorization: `Token ${customerToken}`,
26
+ "Content-Type": "application/json",
27
+ },
28
+ body: JSON.stringify(data),
29
+ });
30
+
31
+ if (response.ok) return await response.json();
32
+
33
+ const res_json = await response.json();
34
+
35
+ if (response.status === 409) {
36
+ if ((res_json.error = "Card number already exists.")) {
37
+ return {
38
+ code: 200,
39
+ body: res_json,
40
+ name: "",
41
+ message: res_json.error,
42
+ };
43
+ }
44
+ }
45
+ throw await buildErrorResponse(response, res_json);
46
+ } catch (error) {
47
+ throw buildErrorResponseFromCatch(error);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Removes a customer's card.
53
+ * @param {string} baseUrl - The base URL of the API.
54
+ * @param {string} customerToken - The customer's authentication token.
55
+ * @param {string} skyflowId - The Skyflow ID of the card to be removed.
56
+ * @param {string} businessId - The business ID.
57
+ * @returns {Promise<Object>} The result of the card removal operation.
58
+ */
59
+ export async function removeCustomerCard(
60
+ baseUrl,
61
+ customerToken,
62
+ skyflowId = "",
63
+ businessId,
64
+ ) {
65
+ try {
66
+ const url = `${baseUrl}/api/v1/business/${businessId}/cards/${skyflowId}`;
67
+
68
+ const response = await fetch(url, {
69
+ method: "DELETE",
70
+ headers: {
71
+ Authorization: `Token ${customerToken}`,
72
+ "Content-Type": "application/json",
73
+ },
74
+ });
75
+
76
+ if (response.ok) return await response.json();
77
+ const res_json = await response.json();
78
+
79
+ throw await buildErrorResponse(response, res_json);
80
+ } catch (error) {
81
+ throw buildErrorResponseFromCatch(error);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Fetches a customer's saved cards.
87
+ * @param {string} baseUrl - The base URL of the API.
88
+ * @param {string} customerToken - The customer's authentication token.
89
+ * @param {string} businessId - The business ID.
90
+ * @param {AbortSignal} signal - The abort signal to cancel the request.
91
+ * @returns {Promise<Object>} The customer's saved cards.
92
+ */
93
+ export async function fetchCustomerCards(
94
+ baseUrl,
95
+ customerToken,
96
+ businessId,
97
+ signal= null,
98
+ ) {
99
+ try {
100
+ const url = `${baseUrl}/api/v1/business/${businessId}/cards/`;
101
+ const response = await fetch(url, {
102
+ method: "GET",
103
+ headers: {
104
+ Authorization: `Token ${customerToken}`,
105
+ "Content-Type": "application/json",
106
+ },
107
+ signal,
108
+ });
109
+
110
+ if (response.ok) return await response.json();
111
+ const res_json = await response.json();
112
+ throw await buildErrorResponse(response, res_json);
113
+ } catch (error) {
114
+ throw buildErrorResponseFromCatch(error);
115
+ }
116
+ }