tonder-web-sdk 1.7.0 → 1.8.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tonder-web-sdk",
3
- "version": "1.7.0",
3
+ "version": "1.8.1",
4
4
  "description": "tonder sdk for integrations",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -1,4 +1,4 @@
1
- import { cardTemplate } from '../helpers/template.js'
1
+ import { cardItemsTemplate, cardTemplate } from '../helpers/template.js'
2
2
  import { cardTemplateSkeleton } from '../helpers/template-skeleton.js'
3
3
  import {
4
4
  getBusiness,
@@ -6,11 +6,16 @@ import {
6
6
  createOrder,
7
7
  createPayment,
8
8
  startCheckoutRouter,
9
- getOpenpayDeviceSessionID
9
+ getOpenpayDeviceSessionID,
10
+ getCustomerCards,
11
+ registerCard,
12
+ deleteCustomerCard
10
13
  } from '../data/api';
11
14
  import {
12
15
  showError,
13
16
  getBrowserInfo,
17
+ mapCards,
18
+ showMessage,
14
19
  } from '../helpers/utils';
15
20
  import { initSkyflow } from '../helpers/skyflow'
16
21
  import { ThreeDSHandler } from './3dsHandler.js';
@@ -18,6 +23,7 @@ import { ThreeDSHandler } from './3dsHandler.js';
18
23
 
19
24
  export class InlineCheckout {
20
25
  static injected = false;
26
+ static cardsInjected = false
21
27
  customer = {}
22
28
  items = []
23
29
  baseUrl = null
@@ -26,6 +32,18 @@ export class InlineCheckout {
26
32
  cartTotal = null
27
33
  metadata = {}
28
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
+ }
29
47
 
30
48
  constructor({
31
49
  mode = "stage",
@@ -34,7 +52,7 @@ export class InlineCheckout {
34
52
  successUrl,
35
53
  renderPaymentButton = false,
36
54
  callBack = () => { },
37
- styles,
55
+ styles
38
56
  }) {
39
57
  this.apiKeyTonder = apiKey;
40
58
  this.returnUrl = returnUrl;
@@ -172,6 +190,9 @@ export class InlineCheckout {
172
190
  this.cartItems = items
173
191
  }
174
192
 
193
+ setCustomerEmail (email) {
194
+ this.email = email
195
+ }
175
196
  setCartTotal(total) {
176
197
  console.log('total: ', total)
177
198
  this.cartTotal = total
@@ -215,13 +236,14 @@ export class InlineCheckout {
215
236
  let checkoutContainer = document.querySelector("#global-loader");
216
237
  if (checkoutContainer) {
217
238
  checkoutContainer.innerHTML = cardTemplateSkeleton;
239
+ checkoutContainer.style.display = 'block';
218
240
  }
219
241
  }
220
242
 
221
243
  #removeGlobalLoader() {
222
244
  const loader = document.querySelector('#global-loader');
223
245
  if (loader) {
224
- loader.remove();
246
+ loader.style.display = 'none';
225
247
  }
226
248
  }
227
249
 
@@ -245,13 +267,25 @@ export class InlineCheckout {
245
267
  return await customerRegister(this.baseUrl, this.apiKeyTonder, customer, signal);
246
268
  }
247
269
 
248
- async #mountTonder() {
270
+ async #mountTonder(getCards = true) {
249
271
  this.#mountPayButton()
250
272
  try{
251
273
  const {
252
274
  vault_id,
253
275
  vault_url,
254
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
+ }
255
289
 
256
290
  this.collectContainer = await initSkyflow(
257
291
  vault_id,
@@ -260,6 +294,7 @@ export class InlineCheckout {
260
294
  this.apiKeyTonder,
261
295
  this.abortController.signal,
262
296
  this.customStyles,
297
+ this.collectorIds
263
298
  );
264
299
  setTimeout(() => {
265
300
  this.#removeGlobalLoader()
@@ -274,6 +309,7 @@ export class InlineCheckout {
274
309
 
275
310
  removeCheckout() {
276
311
  InlineCheckout.injected = false
312
+ InlineCheckout.cardsInjected = false
277
313
  // Cancel all requests
278
314
  this.abortController.abort();
279
315
  this.abortController = new AbortController();
@@ -285,7 +321,7 @@ export class InlineCheckout {
285
321
  async #getCardTokens() {
286
322
  if (this.card?.skyflow_id) return this.card
287
323
  try {
288
- const collectResponse = await this.collectContainer.collect();
324
+ const collectResponse = await this.collectContainer.container.collect();
289
325
  const cardTokens = await collectResponse["records"][0]["fields"];
290
326
  return cardTokens;
291
327
  } catch (error) {
@@ -303,8 +339,14 @@ export class InlineCheckout {
303
339
  const { openpay_keys, reference, business } = this.merchantData
304
340
  const total = Number(this.cartTotal)
305
341
 
306
- const cardTokens = await this.#getCardTokens();
307
-
342
+ let cardTokens = null;
343
+ if(this.radioChecked === "new"){
344
+ cardTokens = await this.#getCardTokens();
345
+ }else{
346
+ cardTokens = {
347
+ skyflow_id: this.radioChecked
348
+ }
349
+ }
308
350
  try {
309
351
  let deviceSessionIdTonder;
310
352
  if (openpay_keys.merchant_id && openpay_keys.public_key) {
@@ -319,7 +361,23 @@ export class InlineCheckout {
319
361
  this.customer,
320
362
  this.abortController.signal
321
363
  )
364
+ if(auth_token && this.email){
365
+ const saveCard = document.getElementById("save-checkout-card");
366
+ if(saveCard && "checked" in saveCard && saveCard.checked){
367
+ await registerCard(this.baseUrl, auth_token, { skyflow_id: cardTokens.skyflow_id });
368
+
369
+ this.cardsInjected = false;
370
+
371
+ const cards = await getCustomerCards(this.baseUrl, auth_token);
372
+ if("cards" in cards) {
373
+ const cardsMapped = cards.cards.map((card) => mapCards(card))
374
+ this.#loadCardsList(cardsMapped, auth_token)
375
+ }
322
376
 
377
+ showMessage("Tarjeta registrada con éxito", this.collectorIds.msgNotification);
378
+
379
+ }
380
+ }
323
381
  var orderItems = {
324
382
  business: this.apiKeyTonder,
325
383
  client: auth_token,
@@ -401,4 +459,81 @@ export class InlineCheckout {
401
459
  throw error;
402
460
  }
403
461
  };
462
+
463
+ #loadCardsList (cards, token) {
464
+ if(this.cardsInjected) return;
465
+ const injectInterval = setInterval(() => {
466
+ const queryElement = document.querySelector(`#${this.collectorIds.cardsListContainer}`);
467
+ if (queryElement && InlineCheckout.injected) {
468
+ queryElement.innerHTML = cardItemsTemplate(cards)
469
+ clearInterval(injectInterval)
470
+ this.#mountRadioButtons(token)
471
+ this.cardsInjected = true
472
+ }
473
+ }, 500);
474
+ }
475
+
476
+ #mountRadioButtons (token) {
477
+ const radioButtons = document.getElementsByName(`card_selected`);
478
+ for (const radio of radioButtons) {
479
+ radio.style.display = "block";
480
+ radio.onclick = async (event) => {
481
+ await this.#handleRadioButtonClick(radio);
482
+ };
483
+ }
484
+ const cardsButtons = document.getElementsByClassName("card-delete-button");
485
+ for (const cardButton of cardsButtons) {
486
+ cardButton.addEventListener("click", (event) => {
487
+ event.preventDefault();
488
+ this.#handleDeleteCardButtonClick(token, cardButton)
489
+ }, false);
490
+ }
491
+ }
492
+
493
+ async #handleRadioButtonClick (radio) {
494
+ if(radio.id === this.radioChecked || ( radio.id === "new" && this.radioChecked === undefined)) return;
495
+ const containerForm = document.querySelector(".container-form");
496
+ if(containerForm) {
497
+ containerForm.style.display = radio.id === "new" ? "block" : "none";
498
+ }
499
+ if(radio.id === "new") {
500
+ if(this.radioChecked !== radio.id) {
501
+ this.#addGlobalLoader()
502
+ this.#mountTonder(false);
503
+ InlineCheckout.injected = true;
504
+ }
505
+ } else {
506
+ this.#unmountForm();
507
+ }
508
+ this.radioChecked = radio.id;
509
+ }
510
+
511
+ async #handleDeleteCardButtonClick (customerToken, button) {
512
+ const id = button.attributes.getNamedItem("id")
513
+ const skyflow_id = id?.value?.split("_")?.[2]
514
+ if(skyflow_id) {
515
+ const cardClicked = document.querySelector(`#card_container-${skyflow_id}`);
516
+ if(cardClicked) {
517
+ cardClicked.style.display = "none"
518
+ }
519
+ await deleteCustomerCard(this.baseUrl, customerToken, skyflow_id)
520
+ this.cardsInjected = false
521
+ const cards = await getCustomerCards(this.baseUrl, customerToken)
522
+ if("cards" in cards) {
523
+ const cardsMapped = cards.cards.map(mapCards)
524
+ this.#loadCardsList(cardsMapped, customerToken)
525
+ }
526
+ }
527
+ }
528
+
529
+ #unmountForm () {
530
+ InlineCheckout.injected = false
531
+ if(this.collectContainer) {
532
+ if("unmount" in this.collectContainer.elements.cardHolderNameElement) this.collectContainer.elements.cardHolderNameElement.unmount()
533
+ if("unmount" in this.collectContainer.elements.cardNumberElement) this.collectContainer.elements.cardNumberElement.unmount()
534
+ if("unmount" in this.collectContainer.elements.expiryYearElement) this.collectContainer.elements.expiryYearElement.unmount()
535
+ if("unmount" in this.collectContainer.elements.expiryMonthElement) this.collectContainer.elements.expiryMonthElement.unmount()
536
+ if("unmount" in this.collectContainer.elements.cvvElement) this.collectContainer.elements.cvvElement.unmount()
537
+ }
538
+ }
404
539
  }
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,
@@ -51,6 +52,15 @@ export async function initSkyflow(
51
52
  max: 70,
52
53
  },
53
54
  };
55
+ const regexEmpty = RegExp("^(?!\s*$).+");
56
+
57
+ const regexMatchRule = {
58
+ type: Skyflow.ValidationRuleType.REGEX_MATCH_RULE,
59
+ params: {
60
+ regex: regexEmpty,
61
+ error: "El campo es requerido" // Optional, default error is 'VALIDATION FAILED'.
62
+ }
63
+ }
54
64
 
55
65
  const cardHolderNameElement = await collectContainer.create({
56
66
  table: "cards",
@@ -59,10 +69,15 @@ export async function initSkyflow(
59
69
  label: collectStylesOptions.labels?.nameLabel,
60
70
  placeholder: collectStylesOptions.placeholders?.namePlaceholder,
61
71
  type: Skyflow.ElementType.CARDHOLDER_NAME,
62
- validations: [lengthMatchRule],
72
+ validations: [lengthMatchRule, regexMatchRule],
63
73
  });
64
74
 
65
- cardHolderNameElement.setError('Inválido')
75
+ handleSkyflowElementEvents(
76
+ cardHolderNameElement,
77
+ collectorIds.holderName,
78
+ "errorCardHolderIdTonder",
79
+ "titular de la tarjeta"
80
+ );
66
81
 
67
82
  // Create collect elements.
68
83
  const cardNumberElement = await collectContainer.create({
@@ -76,9 +91,16 @@ export async function initSkyflow(
76
91
  label: collectStylesOptions.labels?.cardLabel,
77
92
  placeholder: collectStylesOptions.placeholders?.cardPlaceholder,
78
93
  type: Skyflow.ElementType.CARD_NUMBER,
94
+ validations: [regexMatchRule],
79
95
  });
80
96
 
81
- cardNumberElement.setError('Inválido')
97
+ handleSkyflowElementEvents(
98
+ cardNumberElement,
99
+ collectorIds.cardNumber,
100
+ "errorCardNumberIdTonder",
101
+ "número de tarjeta"
102
+ );
103
+
82
104
 
83
105
  const cvvElement = await collectContainer.create({
84
106
  table: "cards",
@@ -87,9 +109,14 @@ export async function initSkyflow(
87
109
  label: collectStylesOptions.labels?.cvvLabel,
88
110
  placeholder: collectStylesOptions.placeholders?.cvvPlaceholder,
89
111
  type: Skyflow.ElementType.CVV,
112
+ validations: [regexMatchRule],
90
113
  });
91
114
 
92
- cvvElement.setError('Inválido')
115
+ handleSkyflowElementEvents(
116
+ cvvElement,
117
+ collectorIds.cvv,
118
+ "errorCvvIdTonder"
119
+ );
93
120
 
94
121
  const expiryMonthElement = await collectContainer.create({
95
122
  table: "cards",
@@ -98,9 +125,14 @@ export async function initSkyflow(
98
125
  label: collectStylesOptions.labels?.expiryDateLabel,
99
126
  placeholder: collectStylesOptions.placeholders?.expiryMonthPlaceholder,
100
127
  type: Skyflow.ElementType.EXPIRATION_MONTH,
128
+ validations: [regexMatchRule],
101
129
  });
102
130
 
103
- expiryMonthElement.setError('Inválido')
131
+ handleSkyflowElementEvents(
132
+ expiryMonthElement,
133
+ collectorIds.expirationMonth,
134
+ "errorExpiryMonthIdTonder"
135
+ );
104
136
 
105
137
  const expiryYearElement = await collectContainer.create({
106
138
  table: "cards",
@@ -109,9 +141,14 @@ export async function initSkyflow(
109
141
  label: "",
110
142
  placeholder: collectStylesOptions.placeholders?.expiryYearPlaceholder,
111
143
  type: Skyflow.ElementType.EXPIRATION_YEAR,
144
+ validations: [regexMatchRule],
112
145
  });
113
146
 
114
- expiryYearElement.setError('Inválido')
147
+ handleSkyflowElementEvents(
148
+ expiryYearElement,
149
+ collectorIds.expirationYear,
150
+ "errorExpiryYearIdTonder"
151
+ );
115
152
 
116
153
  await mountElements(
117
154
  cardNumberElement,
@@ -121,7 +158,16 @@ export async function initSkyflow(
121
158
  cardHolderNameElement,
122
159
  )
123
160
 
124
- return collectContainer
161
+ return {
162
+ container: collectContainer,
163
+ elements: {
164
+ cardHolderNameElement,
165
+ cardNumberElement,
166
+ cvvElement,
167
+ expiryMonthElement,
168
+ expiryYearElement
169
+ }
170
+ }
125
171
  }
126
172
 
127
173
  async function mountElements(
@@ -137,3 +183,30 @@ async function mountElements(
137
183
  expiryYearElement.mount("#collectExpirationYear");
138
184
  cardHolderNameElement.mount("#collectCardholderName");
139
185
  }
186
+
187
+
188
+ function handleSkyflowElementEvents(element, elementId, errorElementId, fieldMessage= "", requiredMessage = "El campo es requerido", invalidMessage= "El campo es inválido") {
189
+ if ("on" in element) {
190
+ element.on(Skyflow.EventName.CHANGE, (state) => {
191
+ let errorElement = document.getElementById(errorElementId);
192
+ if (errorElement && state.isValid && !state.isEmpty) {
193
+ errorElement.remove();
194
+ }
195
+ });
196
+
197
+ element.on(Skyflow.EventName.BLUR, (state) => {
198
+ let container = document.getElementById(elementId);
199
+ let errorElement = document.getElementById(errorElementId);
200
+ if (errorElement) {
201
+ errorElement.remove();
202
+ }
203
+ if (!state.isValid) {
204
+ let errorLabel = document.createElement("div");
205
+ errorLabel.classList.add("error-custom-inputs-tonder");
206
+ errorLabel.id = errorElementId;
207
+ errorLabel.textContent = state.isEmpty ? requiredMessage : fieldMessage != "" ?`El campo ${fieldMessage} es inválido`: invalidMessage;
208
+ container?.appendChild(errorLabel);
209
+ }
210
+ });
211
+ }
212
+ }