tonder-web-sdk 1.9.3-beta.1 → 1.10.2

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 CHANGED
@@ -139,6 +139,7 @@ const checkoutData = {
139
139
  const apiKey = "4c87c36e697e65ddfe288be0afbe7967ea0ab865";
140
140
  const returnUrl = "http://my-website:8080/checkout"
141
141
  const successUrl = "http://my-website:8080/success"
142
+
142
143
  // if using script tag, it should be initialized like this
143
144
  // new TonderSdk.InlineCheckout
144
145
  const inlineCheckout = new InlineCheckout({
@@ -148,8 +149,22 @@ const inlineCheckout = new InlineCheckout({
148
149
  styles: customStyles
149
150
  });
150
151
 
152
+ // The configureCheckout function allows you to set initial information,
153
+ // such as the customer's email, which is used to retrieve a list of saved cards.
154
+ inlineCheckout.configureCheckout({customer: {email: "example@email.com"}});
155
+
156
+
151
157
  inlineCheckout.injectCheckout();
152
158
 
159
+ // To verify a 3ds transaction you can use the following method
160
+ // It should be called after the injectCheckout method
161
+ // The response status will be one of the following
162
+ // ['Declined', 'Cancelled', 'Failed', 'Success', 'Pending', 'Authorized']
163
+
164
+ inlineCheckout.verify3dsTransaction().then(response => {
165
+ console.log('Verify 3ds response', response)
166
+ })
167
+
153
168
  const response = await inlineCheckout.payment(checkoutData);
154
169
  ```
155
170
 
@@ -163,7 +178,7 @@ const response = await inlineCheckout.payment(checkoutData);
163
178
  | mode | string | 'stage' 'production' 'sandbox', default 'stage' |
164
179
  | apiKey | string | You can take this from you Tonder Dashboard |
165
180
  | backgroundColor | string | Hex color #000000 |
166
- | returnUrl | string | |
181
+ | returnUrl | string | url where the checkout form is mounted (3ds) |
167
182
  | successUrl | string | |
168
183
  | backgroundColor | string | |
169
184
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tonder-web-sdk",
3
- "version": "1.9.3-beta.1",
3
+ "version": "1.10.2",
4
4
  "description": "tonder sdk for integrations",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -39,6 +39,18 @@ export class ThreeDSHandler {
39
39
  }
40
40
  }
41
41
 
42
+ saveCheckoutId(checkoutId) {
43
+ localStorage.setItem('checkout_id', JSON.stringify(checkoutId))
44
+ }
45
+
46
+ removeCheckoutId() {
47
+ localStorage.removeItem("checkout_id")
48
+ }
49
+
50
+ getCurrentCheckoutId() {
51
+ return JSON.parse(localStorage.getItem("checkout_id"));
52
+ }
53
+
42
54
  getUrlWithExpiration() {
43
55
  const item = JSON.parse(localStorage.getItem("verify_transaction_status"))
44
56
  if (!item) return
@@ -117,17 +129,17 @@ export class ThreeDSHandler {
117
129
  return parameters;
118
130
  }
119
131
 
132
+ // TODO: Remove this duplication
120
133
  handleSuccessTransaction(response) {
121
134
  this.removeVerifyTransactionUrl();
122
- window.location = this.successUrl
123
- console.log('Transacción autorizada exitosamente.');
135
+ // window.location = this.successUrl
136
+ console.log('Transacción autorizada');
124
137
  return response;
125
138
  }
126
139
 
127
140
  handleDeclinedTransaction(response) {
128
141
  this.removeVerifyTransactionUrl();
129
- console.log('Transacción rechazada.');
130
- throw new Error("Transacción rechazada.");
142
+ return response;
131
143
  }
132
144
 
133
145
  // TODO: the method below needs to be tested with a real 3DS challenge
@@ -159,15 +171,18 @@ export class ThreeDSHandler {
159
171
  await this.verifyTransactionStatus();
160
172
  }
161
173
 
174
+ // TODO: This method could be removed
162
175
  async handleTransactionResponse(response) {
163
176
  const response_json = await response.json();
164
177
 
165
- if (response_json.status === "Pending") {
178
+ // Azul property
179
+ if (response_json.status === "Pending" && response_json.redirect_post_url) {
166
180
  return await this.handle3dsChallenge(response_json);
167
181
  } else if (["Success", "Authorized"].includes(response_json.status)) {
168
- return this.handleSuccessTransaction(response);
182
+ return this.handleSuccessTransaction(response_json);
169
183
  } else {
170
- return this.handleDeclinedTransaction(response);
184
+ this.handleDeclinedTransaction();
185
+ return response_json
171
186
  }
172
187
  }
173
188
 
@@ -185,19 +200,23 @@ export class ThreeDSHandler {
185
200
  },
186
201
  // body: JSON.stringify(data),
187
202
  });
188
-
189
203
  if (response.status !== 200) {
190
204
  console.error('La verificación de la transacción falló.');
191
- return
205
+ this.removeVerifyTransactionUrl();
206
+ return response
192
207
  }
193
208
 
194
209
  return await this.handleTransactionResponse(response);
195
210
  } catch (error) {
196
211
  console.error('Error al verificar la transacción:', error);
197
- return error;
212
+ this.removeVerifyTransactionUrl();
198
213
  }
199
214
  } else {
200
215
  console.log('No verify_transaction_status_url found');
201
216
  }
202
217
  }
218
+
219
+ setPayload = (payload) => {
220
+ this.payload = payload
221
+ }
203
222
  }
@@ -43,7 +43,6 @@ export class InlineCheckout {
43
43
  tonderPayButton: "tonderPayButton",
44
44
  msgError: "msgError",
45
45
  msgNotification: "msgNotification"
46
-
47
46
  }
48
47
 
49
48
  constructor({
@@ -78,7 +77,7 @@ export class InlineCheckout {
78
77
  'stage': 'https://stage.tonder.io',
79
78
  'development': 'http://localhost:8000',
80
79
  };
81
-
80
+
82
81
  return modeUrls[this.mode] || modeUrls['stage']
83
82
  }
84
83
 
@@ -112,6 +111,31 @@ export class InlineCheckout {
112
111
  }
113
112
  }
114
113
 
114
+ async handle3dsRedirect(response) {
115
+ const iframe = response?.next_action?.iframe_resources?.iframe
116
+
117
+ if (iframe) {
118
+ this.process3ds.loadIframe().then(() => {
119
+ //TODO: Check if this will be necessary on the frontend side
120
+ // after some the tests in production, since the 3DS process
121
+ // doesn't works properly on the sandbox environment
122
+ // setTimeout(() => {
123
+ // process3ds.verifyTransactionStatus();
124
+ // }, 10000);
125
+ this.process3ds.verifyTransactionStatus();
126
+ }).catch((error) => {
127
+ console.log('Error loading iframe:', error)
128
+ })
129
+ } else {
130
+ const redirectUrl = this.process3ds.getRedirectUrl()
131
+ if (redirectUrl) {
132
+ this.process3ds.redirectToChallenge()
133
+ } else {
134
+ return response;
135
+ }
136
+ }
137
+ }
138
+
115
139
  payment(data) {
116
140
  return new Promise(async (resolve, reject) => {
117
141
  try {
@@ -122,36 +146,12 @@ export class InlineCheckout {
122
146
  this.#handleCurrency(data)
123
147
  this.#handleCard(data)
124
148
  const response = await this.#checkout()
125
- if (response) {
126
- const process3ds = new ThreeDSHandler({
127
- baseUrl: this.baseUrl,
128
- apiKey: this.apiKeyTonder,
129
- payload: response,
130
- });
131
- this.callBack(response);
132
-
133
- const iframe = response?.next_action?.iframe_resources?.iframe
134
-
135
- if (iframe) {
136
- process3ds.loadIframe().then(() => {
137
- //TODO: Check if this will be necessary on the frontend side
138
- // after some the tests in production, since the 3DS process
139
- // doesn't works properly on the sandbox environment
140
- // setTimeout(() => {
141
- // process3ds.verifyTransactionStatus();
142
- // }, 10000);
143
- process3ds.verifyTransactionStatus();
144
- }).catch((error) => {
145
- console.log('Error loading iframe:', error)
146
- })
147
- } else {
148
- const redirectUrl = process3ds.getRedirectUrl()
149
- if (redirectUrl) {
150
- process3ds.redirectToChallenge()
151
- } else {
152
- resolve(response);
153
- }
154
- }
149
+ this.process3ds.setPayload(response)
150
+ this.process3ds.saveCheckoutId(response.checkout_id)
151
+ this.callBack(response);
152
+ const payload = await this.handle3dsRedirect(response)
153
+ if (payload) {
154
+ resolve(response);
155
155
  }
156
156
  } catch (error) {
157
157
  reject(error);
@@ -160,7 +160,6 @@ export class InlineCheckout {
160
160
  }
161
161
 
162
162
  #handleCustomer(customer) {
163
- console.log('customer: ', customer)
164
163
  if (!customer) return
165
164
 
166
165
  this.firstName = customer?.firstName
@@ -188,15 +187,15 @@ export class InlineCheckout {
188
187
  }
189
188
 
190
189
  setCartItems(items) {
191
- console.log('items: ', items)
192
190
  this.cartItems = items
193
191
  }
194
192
 
195
- setCustomerEmail (email) {
196
- this.email = email
193
+ configureCheckout(data) {
194
+ if ('customer' in data)
195
+ this.#handleCustomer(data['customer'])
197
196
  }
197
+
198
198
  setCartTotal(total) {
199
- console.log('total: ', total)
200
199
  this.cartTotal = total
201
200
  this.#updatePayButton()
202
201
  }
@@ -213,7 +212,6 @@ export class InlineCheckout {
213
212
 
214
213
  injectCheckout() {
215
214
  if (InlineCheckout.injected) return
216
- this.process3ds.verifyTransactionStatus()
217
215
  const containerTonderCheckout = document.querySelector("#tonder-checkout");
218
216
  if (containerTonderCheckout) {
219
217
  this.#mount(containerTonderCheckout)
@@ -230,15 +228,40 @@ export class InlineCheckout {
230
228
  childList: true,
231
229
  subtree: true,
232
230
  attributeFilter: ['id']
233
- });
231
+ });
232
+ }
233
+
234
+ async verify3dsTransaction () {
235
+ const result3ds = await this.process3ds.verifyTransactionStatus()
236
+ const resultCheckout = await this.resumeCheckout(result3ds)
237
+ this.process3ds.setPayload(resultCheckout)
238
+ if (resultCheckout?.is_route_finished && resultCheckout?.provider === 'tonder') {
239
+ return resultCheckout
240
+ }
241
+ return this.handle3dsRedirect(resultCheckout)
234
242
  }
235
243
 
244
+ async resumeCheckout(response) {
245
+ if (["Failed", "Declined", "Cancelled"].includes(response?.status)) {
246
+ const routerItems = {
247
+ // TODO: Replace this with reponse.checkout_id
248
+ checkout_id: this.process3ds.getCurrentCheckoutId(),
249
+ };
250
+ const routerResponse = await startCheckoutRouter(
251
+ this.baseUrl,
252
+ this.apiKeyTonder,
253
+ routerItems
254
+ );
255
+ return routerResponse
256
+ }
257
+ return response
258
+ }
236
259
 
237
260
  #addGlobalLoader() {
238
261
  let checkoutContainer = document.querySelector("#global-loader");
239
262
  if (checkoutContainer) {
240
- checkoutContainer.innerHTML = cardTemplateSkeleton;
241
- checkoutContainer.style.display = 'block';
263
+ checkoutContainer.innerHTML = cardTemplateSkeleton;
264
+ checkoutContainer.style.display = 'block';
242
265
  }
243
266
  }
244
267
 
@@ -249,7 +272,7 @@ export class InlineCheckout {
249
272
  }
250
273
  }
251
274
 
252
- #mount(containerTonderCheckout){
275
+ #mount(containerTonderCheckout) {
253
276
  containerTonderCheckout.innerHTML = cardTemplate;
254
277
  this.#addGlobalLoader();
255
278
  this.#mountTonder();
@@ -271,20 +294,28 @@ export class InlineCheckout {
271
294
 
272
295
  async #mountTonder(getCards = true) {
273
296
  this.#mountPayButton()
274
- try{
297
+ try {
275
298
  const {
276
299
  vault_id,
277
300
  vault_url,
278
301
  } = await this.#fetchMerchantData();
279
- if(this.email && getCards){
280
- const customerResponse = await this.getCustomer({email: this.email});
281
- if("auth_token" in customerResponse) {
302
+ console.log("this.email : ", this.email )
303
+ if (this.email && getCards) {
304
+ const customerResponse = await this.getCustomer({ email: this.email });
305
+ if ("auth_token" in customerResponse) {
282
306
  const { auth_token } = customerResponse
283
- const cards = await getCustomerCards(this.baseUrl, auth_token);
307
+ const saveCardCheckbox = document.getElementById('save-card-container');
284
308
 
285
- if("cards" in cards) {
286
- const cardsMapped = cards.cards.map(mapCards)
287
- this.#loadCardsList(cardsMapped, auth_token)
309
+ saveCardCheckbox.style.display = 'none';
310
+ console.log("mode: ", this.mode)
311
+ if (this.mode !== 'production') {
312
+ const cards = await getCustomerCards(this.baseUrl, auth_token);
313
+ saveCardCheckbox.style.display = '';
314
+
315
+ if ("cards" in cards) {
316
+ const cardsMapped = cards.cards.map(mapCards)
317
+ this.#loadCardsList(cardsMapped, auth_token)
318
+ }
288
319
  }
289
320
  }
290
321
  }
@@ -301,7 +332,7 @@ export class InlineCheckout {
301
332
  setTimeout(() => {
302
333
  this.#removeGlobalLoader()
303
334
  }, 800)
304
- }catch(e){
335
+ } catch (e) {
305
336
  if (e && e.name !== 'AbortError') {
306
337
  this.#removeGlobalLoader()
307
338
  showError("No se pudieron cargar los datos del comercio.")
@@ -342,9 +373,9 @@ export class InlineCheckout {
342
373
  const total = Number(this.cartTotal)
343
374
 
344
375
  let cardTokens = null;
345
- if(this.radioChecked === "new" || this.radioChecked === undefined){
376
+ if (this.radioChecked === "new" || this.radioChecked === undefined) {
346
377
  cardTokens = await this.#getCardTokens();
347
- }else{
378
+ } else {
348
379
  cardTokens = {
349
380
  skyflow_id: this.radioChecked
350
381
  }
@@ -360,24 +391,24 @@ export class InlineCheckout {
360
391
  }
361
392
 
362
393
  const { id, auth_token } = await this.getCustomer(
363
- this.customer,
394
+ this.customer,
364
395
  this.abortController.signal
365
396
  )
366
- if(auth_token && this.email){
397
+ if (auth_token && this.email) {
367
398
  const saveCard = document.getElementById("save-checkout-card");
368
- if(saveCard && "checked" in saveCard && saveCard.checked){
399
+ if (saveCard && "checked" in saveCard && saveCard.checked) {
369
400
  await registerCard(this.baseUrl, auth_token, { skyflow_id: cardTokens.skyflow_id });
370
-
401
+
371
402
  this.cardsInjected = false;
372
403
 
373
404
  const cards = await getCustomerCards(this.baseUrl, auth_token);
374
- if("cards" in cards) {
405
+ if ("cards" in cards) {
375
406
  const cardsMapped = cards.cards.map((card) => mapCards(card))
376
407
  this.#loadCardsList(cardsMapped, auth_token)
377
408
  }
378
409
 
379
410
  showMessage("Tarjeta registrada con éxito", this.collectorIds.msgNotification);
380
-
411
+
381
412
  }
382
413
  }
383
414
  var orderItems = {
@@ -391,7 +422,6 @@ export class InlineCheckout {
391
422
  is_oneclick: true,
392
423
  items: this.cartItems,
393
424
  };
394
- console.log('orderItems: ', orderItems)
395
425
  const jsonResponseOrder = await createOrder(
396
426
  this.baseUrl,
397
427
  this.apiKeyTonder,
@@ -462,8 +492,8 @@ export class InlineCheckout {
462
492
  }
463
493
  };
464
494
 
465
- #loadCardsList (cards, token) {
466
- if(this.cardsInjected) return;
495
+ #loadCardsList(cards, token) {
496
+ if (this.cardsInjected) return;
467
497
  const injectInterval = setInterval(() => {
468
498
  const queryElement = document.querySelector(`#${this.collectorIds.cardsListContainer}`);
469
499
  if (queryElement && InlineCheckout.injected) {
@@ -475,7 +505,7 @@ export class InlineCheckout {
475
505
  }, 500);
476
506
  }
477
507
 
478
- #mountRadioButtons (token) {
508
+ #mountRadioButtons(token) {
479
509
  const radioButtons = document.getElementsByName(`card_selected`);
480
510
  for (const radio of radioButtons) {
481
511
  radio.style.display = "block";
@@ -492,14 +522,14 @@ export class InlineCheckout {
492
522
  }
493
523
  }
494
524
 
495
- async #handleRadioButtonClick (radio) {
496
- if(radio.id === this.radioChecked || ( radio.id === "new" && this.radioChecked === undefined)) return;
525
+ async #handleRadioButtonClick(radio) {
526
+ if (radio.id === this.radioChecked || (radio.id === "new" && this.radioChecked === undefined)) return;
497
527
  const containerForm = document.querySelector(".container-form");
498
- if(containerForm) {
528
+ if (containerForm) {
499
529
  containerForm.style.display = radio.id === "new" ? "block" : "none";
500
530
  }
501
- if(radio.id === "new") {
502
- if(this.radioChecked !== radio.id) {
531
+ if (radio.id === "new") {
532
+ if (this.radioChecked !== radio.id) {
503
533
  this.#addGlobalLoader()
504
534
  this.#mountTonder(false);
505
535
  InlineCheckout.injected = true;
@@ -510,45 +540,45 @@ export class InlineCheckout {
510
540
  this.radioChecked = radio.id;
511
541
  }
512
542
 
513
- async #handleDeleteCardButtonClick (customerToken, button) {
543
+ async #handleDeleteCardButtonClick(customerToken, button) {
514
544
  const id = button.attributes.getNamedItem("id")
515
545
  const skyflow_id = id?.value?.split("_")?.[2]
516
- if(skyflow_id) {
546
+ if (skyflow_id) {
517
547
  const cardClicked = document.querySelector(`#card_container-${skyflow_id}`);
518
- if(cardClicked) {
548
+ if (cardClicked) {
519
549
  cardClicked.style.display = "none"
520
550
  }
521
- try{
522
- this.deletingCards.push(skyflow_id);
523
- if (this.abortRefreshCardsController) {
524
- this.abortRefreshCardsController.abort();
525
- this.abortRefreshCardsController = new AbortController();
551
+ try {
552
+ this.deletingCards.push(skyflow_id);
553
+ if (this.abortRefreshCardsController) {
554
+ this.abortRefreshCardsController.abort();
555
+ this.abortRefreshCardsController = new AbortController();
556
+ }
557
+ await deleteCustomerCard(this.baseUrl, customerToken, skyflow_id)
558
+ } catch {
559
+ } finally {
560
+ this.deletingCards = this.deletingCards.filter(id => id !== skyflow_id);
561
+ this.#refreshCardOnDelete(customerToken)
526
562
  }
527
- await deleteCustomerCard(this.baseUrl, customerToken, skyflow_id)
528
- }catch{
529
- } finally {
530
- this.deletingCards = this.deletingCards.filter(id => id !== skyflow_id);
531
- this.#refreshCardOnDelete(customerToken)
532
- }
533
563
  }
534
564
  }
535
- async #refreshCardOnDelete(customerToken){
536
- if(this.deletingCards.length > 0) return;
565
+ async #refreshCardOnDelete(customerToken) {
566
+ if (this.deletingCards.length > 0) return;
537
567
  this.cardsInjected = false
538
568
  const cards = await getCustomerCards(this.baseUrl, customerToken, "", this.abortRefreshCardsController.signal)
539
- if("cards" in cards) {
569
+ if ("cards" in cards) {
540
570
  const cardsMapped = cards.cards.map(mapCards)
541
571
  this.#loadCardsList(cardsMapped, customerToken)
542
572
  }
543
573
  }
544
- #unmountForm () {
574
+ #unmountForm() {
545
575
  InlineCheckout.injected = false
546
- if(this.collectContainer) {
547
- if("unmount" in this.collectContainer.elements.cardHolderNameElement) this.collectContainer.elements.cardHolderNameElement.unmount()
548
- if("unmount" in this.collectContainer.elements.cardNumberElement) this.collectContainer.elements.cardNumberElement.unmount()
549
- if("unmount" in this.collectContainer.elements.expiryYearElement) this.collectContainer.elements.expiryYearElement.unmount()
550
- if("unmount" in this.collectContainer.elements.expiryMonthElement) this.collectContainer.elements.expiryMonthElement.unmount()
551
- if("unmount" in this.collectContainer.elements.cvvElement) this.collectContainer.elements.cvvElement.unmount()
576
+ if (this.collectContainer) {
577
+ if ("unmount" in this.collectContainer.elements.cardHolderNameElement) this.collectContainer.elements.cardHolderNameElement.unmount()
578
+ if ("unmount" in this.collectContainer.elements.cardNumberElement) this.collectContainer.elements.cardNumberElement.unmount()
579
+ if ("unmount" in this.collectContainer.elements.expiryYearElement) this.collectContainer.elements.expiryYearElement.unmount()
580
+ if ("unmount" in this.collectContainer.elements.expiryMonthElement) this.collectContainer.elements.expiryMonthElement.unmount()
581
+ if ("unmount" in this.collectContainer.elements.cvvElement) this.collectContainer.elements.cvvElement.unmount()
552
582
  }
553
583
  }
554
584
  }
@@ -19,7 +19,7 @@ export const cardTemplate = `
19
19
  <div id="collectExpirationYear" class="expiration-year"></div>
20
20
  <div id="collectCvv" class="empty-div"></div>
21
21
  </div>
22
- <div class="checkbox">
22
+ <div class="checkbox" id="save-card-container">
23
23
  <input id="save-checkout-card" type="checkbox">
24
24
  <label for="save-checkout-card">
25
25
  Guardar tarjeta para futuros pagos
package/src/index-dev.js CHANGED
@@ -97,21 +97,26 @@ const checkoutData = {
97
97
  };
98
98
 
99
99
  // localhost
100
- const apiKey = "4c87c36e697e65ddfe288be0afbe7967ea0ab865";
100
+ const apiKey = "11e3d3c3e95e0eaabbcae61ebad34ee5f93c3d27";
101
101
  const returnUrl = "http://127.0.0.1:8080/"
102
102
  const successUrl = "http://127.0.0.1:8080/success"
103
103
  // stage
104
104
  // const apiKey = "8365683bdc33dd6d50fe2397188d79f1a6765852";
105
105
 
106
106
  const inlineCheckout = new InlineCheckout({
107
- mode: 'development',
107
+ mode: 'stage',
108
108
  apiKey,
109
109
  returnUrl,
110
110
  successUrl,
111
111
  styles: customStyles
112
112
  });
113
- inlineCheckout.setCustomerEmail(checkoutData.customer.email)
113
+ inlineCheckout.configureCheckout({customer: checkoutData.customer})
114
114
  inlineCheckout.injectCheckout();
115
+ //
116
+ // ['Declined', 'Cancelled', 'Failed', 'Success', 'Pending', 'Authorized']
117
+ inlineCheckout.verify3dsTransaction().then(response => {
118
+ console.log('Verify 3ds response', response)
119
+ })
115
120
 
116
121
  document.addEventListener('DOMContentLoaded', function() {
117
122
  const payButton = document.getElementById('pay-button');