tonder-web-sdk 1.4.0 → 1.8.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 CHANGED
@@ -160,6 +160,7 @@ const response = await inlineCheckout.payment(checkoutData);
160
160
  ## Configuration
161
161
  | Property | Type | Description |
162
162
  |:---------------:|:-------------:|:---------------------------------------------------:|
163
+ | mode | string | 'stage' 'production' 'sandbox', default 'stage' |
163
164
  | apiKey | string | You can take this from you Tonder Dashboard |
164
165
  | backgroundColor | string | Hex color #000000 |
165
166
  | returnUrl | string | |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tonder-web-sdk",
3
- "version": "1.4.0",
3
+ "version": "1.8.0",
4
4
  "description": "tonder sdk for integrations",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -1,15 +1,21 @@
1
- import { cardTemplate } from '../helpers/template.js'
1
+ import { cardItemsTemplate, cardTemplate } from '../helpers/template.js'
2
+ import { cardTemplateSkeleton } from '../helpers/template-skeleton.js'
2
3
  import {
3
4
  getBusiness,
4
5
  customerRegister,
5
6
  createOrder,
6
7
  createPayment,
7
8
  startCheckoutRouter,
8
- getOpenpayDeviceSessionID
9
+ getOpenpayDeviceSessionID,
10
+ getCustomerCards,
11
+ registerCard,
12
+ deleteCustomerCard
9
13
  } from '../data/api';
10
14
  import {
11
15
  showError,
12
16
  getBrowserInfo,
17
+ mapCards,
18
+ showMessage,
13
19
  } from '../helpers/utils';
14
20
  import { initSkyflow } from '../helpers/skyflow'
15
21
  import { ThreeDSHandler } from './3dsHandler.js';
@@ -17,22 +23,36 @@ import { ThreeDSHandler } from './3dsHandler.js';
17
23
 
18
24
  export class InlineCheckout {
19
25
  static injected = false;
26
+ static cardsInjected = false
20
27
  customer = {}
21
28
  items = []
22
- baseUrl = process.env.BASE_URL || "http://localhost:8000";
29
+ baseUrl = null
23
30
  collectContainer = null
24
31
  merchantData = {}
25
32
  cartTotal = null
26
33
  metadata = {}
27
34
  card = {}
35
+ collectorIds = {
36
+ cardsListContainer: "cardsListContainer",
37
+ holderName: "collectCardholderName",
38
+ cardNumber: "collectCardNumber",
39
+ expirationMonth: "collectExpirationMonth",
40
+ expirationYear: "collectExpirationYear",
41
+ cvv: "collectCvv",
42
+ tonderPayButton: "tonderPayButton",
43
+ msgError: "msgError",
44
+ msgNotification: "msgNotification"
45
+
46
+ }
28
47
 
29
48
  constructor({
49
+ mode = "stage",
30
50
  apiKey,
31
51
  returnUrl,
32
52
  successUrl,
33
53
  renderPaymentButton = false,
34
54
  callBack = () => { },
35
- styles,
55
+ styles
36
56
  }) {
37
57
  this.apiKeyTonder = apiKey;
38
58
  this.returnUrl = returnUrl;
@@ -40,6 +60,8 @@ export class InlineCheckout {
40
60
  this.renderPaymentButton = renderPaymentButton;
41
61
  this.callBack = callBack;
42
62
  this.customStyles = styles
63
+ this.mode = mode
64
+ this.baseUrl = this.#getBaseUrl()
43
65
 
44
66
  this.abortController = new AbortController()
45
67
  this.process3ds = new ThreeDSHandler(
@@ -47,6 +69,17 @@ export class InlineCheckout {
47
69
  )
48
70
  }
49
71
 
72
+ #getBaseUrl() {
73
+ const modeUrls = {
74
+ 'production': 'https://app.tonder.io',
75
+ 'sandbox': 'https://sandbox.tonder.io',
76
+ 'stage': 'https://stage.tonder.io',
77
+ 'development': 'http://localhost:8000',
78
+ };
79
+
80
+ return modeUrls[this.mode] || modeUrls['stage']
81
+ }
82
+
50
83
  #mountPayButton() {
51
84
  if (!this.renderPaymentButton) return;
52
85
 
@@ -157,6 +190,9 @@ export class InlineCheckout {
157
190
  this.cartItems = items
158
191
  }
159
192
 
193
+ setCustomerEmail (email) {
194
+ this.email = email
195
+ }
160
196
  setCartTotal(total) {
161
197
  console.log('total: ', total)
162
198
  this.cartTotal = total
@@ -176,14 +212,46 @@ export class InlineCheckout {
176
212
  injectCheckout() {
177
213
  if (InlineCheckout.injected) return
178
214
  this.process3ds.verifyTransactionStatus()
179
- const injectInterval = setInterval(() => {
180
- if (document.querySelector("#tonder-checkout")) {
181
- document.querySelector("#tonder-checkout").innerHTML = cardTemplate;
182
- this.#mountTonder();
183
- clearInterval(injectInterval);
184
- InlineCheckout.injected = true
215
+ const containerTonderCheckout = document.querySelector("#tonder-checkout");
216
+ if (containerTonderCheckout) {
217
+ this.#mount(containerTonderCheckout)
218
+ return;
219
+ }
220
+ const observer = new MutationObserver((mutations, obs) => {
221
+ const containerTonderCheckout = document.querySelector("#tonder-checkout");
222
+ if (containerTonderCheckout) {
223
+ this.#mount(containerTonderCheckout)
224
+ obs.disconnect();
185
225
  }
186
- }, 500);
226
+ });
227
+ observer.observe(document.body, {
228
+ childList: true,
229
+ subtree: true,
230
+ attributeFilter: ['id']
231
+ });
232
+ }
233
+
234
+
235
+ #addGlobalLoader() {
236
+ let checkoutContainer = document.querySelector("#global-loader");
237
+ if (checkoutContainer) {
238
+ checkoutContainer.innerHTML = cardTemplateSkeleton;
239
+ checkoutContainer.style.display = 'block';
240
+ }
241
+ }
242
+
243
+ #removeGlobalLoader() {
244
+ const loader = document.querySelector('#global-loader');
245
+ if (loader) {
246
+ loader.style.display = 'none';
247
+ }
248
+ }
249
+
250
+ #mount(containerTonderCheckout){
251
+ containerTonderCheckout.innerHTML = cardTemplate;
252
+ this.#addGlobalLoader();
253
+ this.#mountTonder();
254
+ InlineCheckout.injected = true;
187
255
  }
188
256
 
189
257
  async #fetchMerchantData() {
@@ -199,26 +267,48 @@ export class InlineCheckout {
199
267
  return await customerRegister(this.baseUrl, this.apiKeyTonder, customer, signal);
200
268
  }
201
269
 
202
- async #mountTonder() {
270
+ async #mountTonder(getCards = true) {
203
271
  this.#mountPayButton()
272
+ try{
273
+ const {
274
+ vault_id,
275
+ vault_url,
276
+ } = await this.#fetchMerchantData();
277
+ if(this.email && getCards){
278
+ const customerResponse = await this.getCustomer({email: this.email});
279
+ if("auth_token" in customerResponse) {
280
+ const { auth_token } = customerResponse
281
+ const cards = await getCustomerCards(this.baseUrl, auth_token);
282
+
283
+ if("cards" in cards) {
284
+ const cardsMapped = cards.cards.map(mapCards)
285
+ this.#loadCardsList(cardsMapped, auth_token)
286
+ }
287
+ }
288
+ }
204
289
 
205
- const {
206
- vault_id,
207
- vault_url,
208
- } = await this.#fetchMerchantData();
209
-
210
- this.collectContainer = await initSkyflow(
211
- vault_id,
212
- vault_url,
213
- this.baseUrl,
214
- this.apiKeyTonder,
215
- this.abortController.signal,
216
- this.customStyles,
217
- );
290
+ this.collectContainer = await initSkyflow(
291
+ vault_id,
292
+ vault_url,
293
+ this.baseUrl,
294
+ this.apiKeyTonder,
295
+ this.abortController.signal,
296
+ this.customStyles,
297
+ );
298
+ setTimeout(() => {
299
+ this.#removeGlobalLoader()
300
+ }, 800)
301
+ }catch(e){
302
+ if (e && e.name !== 'AbortError') {
303
+ this.#removeGlobalLoader()
304
+ showError("No se pudieron cargar los datos del comercio.")
305
+ }
306
+ }
218
307
  }
219
308
 
220
309
  removeCheckout() {
221
310
  InlineCheckout.injected = false
311
+ InlineCheckout.cardsInjected = false
222
312
  // Cancel all requests
223
313
  this.abortController.abort();
224
314
  this.abortController = new AbortController();
@@ -230,7 +320,7 @@ export class InlineCheckout {
230
320
  async #getCardTokens() {
231
321
  if (this.card?.skyflow_id) return this.card
232
322
  try {
233
- const collectResponse = await this.collectContainer.collect();
323
+ const collectResponse = await this.collectContainer.container.collect();
234
324
  const cardTokens = await collectResponse["records"][0]["fields"];
235
325
  return cardTokens;
236
326
  } catch (error) {
@@ -264,7 +354,23 @@ export class InlineCheckout {
264
354
  this.customer,
265
355
  this.abortController.signal
266
356
  )
357
+ if(auth_token && this.email){
358
+ const saveCard = document.getElementById("save-checkout-card");
359
+ if(saveCard && "checked" in saveCard && saveCard.checked){
360
+ await registerCard(this.baseUrl, auth_token, { skyflow_id: cardTokens.skyflow_id });
361
+
362
+ this.cardsInjected = false;
363
+
364
+ const cards = await getCustomerCards(this.baseUrl, auth_token);
365
+ if("cards" in cards) {
366
+ const cardsMapped = cards.cards.map((card) => mapCards(card))
367
+ this.#loadCardsList(cardsMapped, auth_token)
368
+ }
267
369
 
370
+ showMessage("Tarjeta registrada con éxito", this.collectorIds.msgNotification);
371
+
372
+ }
373
+ }
268
374
  var orderItems = {
269
375
  business: this.apiKeyTonder,
270
376
  client: auth_token,
@@ -346,4 +452,81 @@ export class InlineCheckout {
346
452
  throw error;
347
453
  }
348
454
  };
455
+
456
+ #loadCardsList (cards, token) {
457
+ if(this.cardsInjected) return;
458
+ const injectInterval = setInterval(() => {
459
+ const queryElement = document.querySelector(`#${this.collectorIds.cardsListContainer}`);
460
+ if (queryElement && InlineCheckout.injected) {
461
+ queryElement.innerHTML = cardItemsTemplate(cards)
462
+ clearInterval(injectInterval)
463
+ this.#mountRadioButtons(token)
464
+ this.cardsInjected = true
465
+ }
466
+ }, 500);
467
+ }
468
+
469
+ #mountRadioButtons (token) {
470
+ const radioButtons = document.getElementsByName(`card_selected`);
471
+ for (const radio of radioButtons) {
472
+ radio.style.display = "block";
473
+ radio.onclick = async (event) => {
474
+ await this.#handleRadioButtonClick(radio);
475
+ };
476
+ }
477
+ const cardsButtons = document.getElementsByClassName("card-delete-button");
478
+ for (const cardButton of cardsButtons) {
479
+ cardButton.addEventListener("click", (event) => {
480
+ event.preventDefault();
481
+ this.#handleDeleteCardButtonClick(token, cardButton)
482
+ }, false);
483
+ }
484
+ }
485
+
486
+ async #handleRadioButtonClick (radio) {
487
+ if(radio.id === this.radioChecked || ( radio.id === "new" && this.radioChecked === undefined)) return;
488
+ const containerForm = document.querySelector(".container-form");
489
+ if(containerForm) {
490
+ containerForm.style.display = radio.id === "new" ? "block" : "none";
491
+ }
492
+ if(radio.id === "new") {
493
+ if(this.radioChecked !== radio.id) {
494
+ this.#addGlobalLoader()
495
+ this.#mountTonder(false);
496
+ InlineCheckout.injected = true;
497
+ }
498
+ } else {
499
+ this.#unmountForm();
500
+ }
501
+ this.radioChecked = radio.id;
502
+ }
503
+
504
+ async #handleDeleteCardButtonClick (customerToken, button) {
505
+ const id = button.attributes.getNamedItem("id")
506
+ const skyflow_id = id?.value?.split("_")?.[2]
507
+ if(skyflow_id) {
508
+ const cardClicked = document.querySelector(`#card_container-${skyflow_id}`);
509
+ if(cardClicked) {
510
+ cardClicked.style.display = "none"
511
+ }
512
+ await deleteCustomerCard(this.baseUrl, customerToken, skyflow_id)
513
+ this.cardsInjected = false
514
+ const cards = await getCustomerCards(this.baseUrl, customerToken)
515
+ if("cards" in cards) {
516
+ const cardsMapped = cards.cards.map(mapCards)
517
+ this.#loadCardsList(cardsMapped, customerToken)
518
+ }
519
+ }
520
+ }
521
+
522
+ #unmountForm () {
523
+ InlineCheckout.injected = false
524
+ if(this.collectContainer) {
525
+ if("unmount" in this.collectContainer.elements.cardHolderNameElement) this.collectContainer.elements.cardHolderNameElement.unmount()
526
+ if("unmount" in this.collectContainer.elements.cardNumberElement) this.collectContainer.elements.cardNumberElement.unmount()
527
+ if("unmount" in this.collectContainer.elements.expiryYearElement) this.collectContainer.elements.expiryYearElement.unmount()
528
+ if("unmount" in this.collectContainer.elements.expiryMonthElement) this.collectContainer.elements.expiryMonthElement.unmount()
529
+ if("unmount" in this.collectContainer.elements.cvvElement) this.collectContainer.elements.cvvElement.unmount()
530
+ }
531
+ }
349
532
  }
package/src/data/api.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { buildErrorResponse, buildErrorResponseFromCatch } from "../helpers/utils";
2
+
1
3
  export async function getOpenpayDeviceSessionID(merchant_id, public_key, signal) {
2
4
  let openpay = await window.OpenPay;
3
5
  openpay.setId(merchant_id);
@@ -107,3 +109,64 @@ export async function startCheckoutRouter(baseUrlTonder, apiKeyTonder, routerIte
107
109
  throw error
108
110
  }
109
111
  }
112
+
113
+ export async function registerCard(baseUrlTonder, customerToken, data) {
114
+ try {
115
+ const response = await fetch(`${baseUrlTonder}/api/v1/cards/`, {
116
+ method: 'POST',
117
+ headers: {
118
+ 'Authorization': `Token ${customerToken}`,
119
+ 'Content-Type': 'application/json'
120
+ },
121
+ body: JSON.stringify(data)
122
+ });
123
+
124
+ if (response.ok) return await response.json();
125
+ if (response.status === 409){
126
+ const res_json = await response.json()
127
+ if(res_json.error = 'Card number already exists.'){
128
+ return {
129
+ code: 200,
130
+ body: res_json,
131
+ name: '',
132
+ message: res_json.error,
133
+ }
134
+ }
135
+ }
136
+ throw await buildErrorResponse(response);
137
+ } catch (error) {
138
+ throw buildErrorResponseFromCatch(error);
139
+ }
140
+ }
141
+ export async function deleteCustomerCard(baseUrlTonder, customerToken, skyflowId = "") {
142
+ try {
143
+ const response = await fetch(`${baseUrlTonder}/api/v1/cards/${skyflowId}`, {
144
+ method: 'DELETE',
145
+ headers: {
146
+ 'Authorization': `Token ${customerToken}`,
147
+ 'Content-Type': 'application/json'
148
+ },
149
+ });
150
+
151
+ if (response.ok) return true;
152
+ throw await buildErrorResponse(response);
153
+ } catch (error) {
154
+ throw buildErrorResponseFromCatch(error);
155
+ }
156
+ }
157
+ export async function getCustomerCards(baseUrlTonder, customerToken, query = "") {
158
+ try {
159
+ const response = await fetch(`${baseUrlTonder}/api/v1/cards/${query}`, {
160
+ method: 'GET',
161
+ headers: {
162
+ 'Authorization': `Token ${customerToken}`,
163
+ 'Content-Type': 'application/json'
164
+ },
165
+ });
166
+
167
+ if (response.ok) return await response.json();
168
+ throw await buildErrorResponse(response);
169
+ } catch (error) {
170
+ throw buildErrorResponseFromCatch(error);
171
+ }
172
+ }
@@ -6,7 +6,8 @@ export async function initSkyflow(
6
6
  baseUrl,
7
7
  apiKey,
8
8
  signal,
9
- customStyles = {}
9
+ customStyles = {},
10
+ collectorIds,
10
11
  ) {
11
12
  const skyflow = await Skyflow.init({
12
13
  vaultID: vaultId,
@@ -121,7 +122,16 @@ export async function initSkyflow(
121
122
  cardHolderNameElement,
122
123
  )
123
124
 
124
- return collectContainer
125
+ return {
126
+ container: collectContainer,
127
+ elements: {
128
+ cardHolderNameElement,
129
+ cardNumberElement,
130
+ cvvElement,
131
+ expiryMonthElement,
132
+ expiryYearElement
133
+ }
134
+ }
125
135
  }
126
136
 
127
137
  async function mountElements(
@@ -0,0 +1,59 @@
1
+ export const cardTemplateSkeleton = `
2
+ <div class="container-tonder-skeleton">
3
+ <div class="skeleton-loader"></div>
4
+ <div class="skeleton-loader"></div>
5
+ <div class="collect-row-skeleton">
6
+ <div class="skeleton-loader skeleton-loader-item"></div>
7
+ <div class="skeleton-loader skeleton-loader-item"></div>
8
+ <div class="skeleton-loader skeleton-loader-item"></div>
9
+ </div>
10
+ </div>
11
+
12
+ <style>
13
+ .container-tonder-skeleton {
14
+ background-color: #F9F9F9;
15
+ margin: 0 auto !important;
16
+ padding: 30px 10px 30px 10px;
17
+ overflow: hidden;
18
+ transition: max-height 0.5s ease-out;
19
+ max-width: 600px;
20
+ height: 100%;
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: 45px;
24
+ }
25
+
26
+ .collect-row-skeleton {
27
+ display: flex !important;
28
+ justify-content: space-between !important;
29
+ margin-left: 10px !important;
30
+ margin-right: 10px !important;
31
+ gap: 10px;
32
+ }
33
+ .skeleton-loader {
34
+ height: 45px !important;
35
+ border-radius: 8px;
36
+ margin-top: 2px;
37
+ margin-bottom: 4px;
38
+ margin-left: 10px !important;
39
+ margin-right: 10px !important;
40
+ background-color: #e0e0e0;
41
+ animation: pulse 1.5s infinite ease-in-out;
42
+ }
43
+ .skeleton-loader-item{
44
+ width: 35%;
45
+ margin: 0 !important;
46
+ }
47
+ @keyframes pulse {
48
+ 0% {
49
+ background-color: #e0e0e0;
50
+ }
51
+ 50% {
52
+ background-color: #f0f0f0;
53
+ }
54
+ 100% {
55
+ background-color: #e0e0e0;
56
+ }
57
+ }
58
+ </style>
59
+ `