@tonder.io/ionic-full-sdk 0.0.14-beta → 0.0.16-beta

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.io/ionic-full-sdk",
3
- "version": "0.0.14-beta",
3
+ "version": "0.0.16-beta",
4
4
  "description": "Tonder ionic full SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.js",
@@ -10,7 +10,7 @@
10
10
  "author": "",
11
11
  "license": "ISC",
12
12
  "dependencies": {
13
- "@tonder.io/ionic-lite-sdk": "^0.0.22-beta",
13
+ "@tonder.io/ionic-lite-sdk": "^0.0.25-beta",
14
14
  "crypto-js": "^4.1.1",
15
15
  "skyflow-js": "^1.34.1"
16
16
  },
@@ -27,7 +27,45 @@ export class ThreeDSHandler {
27
27
  saveVerifyTransactionUrl() {
28
28
  const url = this.payload?.next_action?.redirect_to_url?.verify_transaction_status_url
29
29
  if (url) {
30
- localStorage.setItem("verify_transaction_status_url", url)
30
+ this.saveUrlWithExpiration(url)
31
+ } else {
32
+ const url = this.payload?.next_action?.iframe_resources?.verify_transaction_status_url
33
+ if (url) {
34
+ this.saveUrlWithExpiration(url)
35
+ } else {
36
+ console.log('No verify_transaction_status_url found');
37
+ }
38
+ }
39
+ }
40
+
41
+ saveUrlWithExpiration(url: string) {
42
+ try {
43
+ const now = new Date()
44
+ const item = {
45
+ url: url,
46
+ // Expires after 20 minutes
47
+ expires: now.getTime() + 20 * 60 * 1000
48
+ }
49
+ localStorage.setItem('verify_transaction_status', JSON.stringify(item))
50
+ } catch (error) {
51
+ console.log('error: ', error)
52
+ }
53
+ }
54
+
55
+ getUrlWithExpiration() {
56
+ const status = localStorage.getItem("verify_transaction_status");
57
+ if(status) {
58
+ const item = JSON.parse(status)
59
+ if (!item) return
60
+ const now = new Date()
61
+ if (now.getTime() > item.expires) {
62
+ this.removeVerifyTransactionUrl()
63
+ return null
64
+ } else {
65
+ return item.url
66
+ }
67
+ } else {
68
+ return null
31
69
  }
32
70
  }
33
71
 
@@ -39,17 +77,54 @@ export class ThreeDSHandler {
39
77
  return localStorage.getItem("verify_transaction_status_url")
40
78
  }
41
79
 
42
- redirectTo3DS() {
43
- const url = this.payload?.next_action?.redirect_to_url?.url
80
+ loadIframe() {
81
+ const iframe = this.payload?.next_action?.iframe_resources?.iframe
82
+
83
+ if (iframe) {
84
+ return new Promise((resolve, reject) => {
85
+ const iframe = this.payload?.next_action?.iframe_resources?.iframe
86
+
87
+ if (iframe) {
88
+ this.saveVerifyTransactionUrl()
89
+ const container = document.createElement('div')
90
+ container.innerHTML = iframe
91
+ document.body.appendChild(container)
92
+
93
+ // Create and append the script tag manually
94
+ const script = document.createElement('script')
95
+ script.textContent = 'document.getElementById("tdsMmethodForm").submit();'
96
+ container.appendChild(script)
97
+
98
+ // Resolve the promise when the iframe is loaded
99
+ const iframeElement = document.getElementById('tdsMmethodTgtFrame')
100
+ if(iframeElement) {
101
+ iframeElement.onload = () => resolve(true)
102
+ } else {
103
+ console.log('No redirection found');
104
+ reject(false)
105
+ }
106
+ } else {
107
+ console.log('No redirection found');
108
+ reject(false)
109
+ }
110
+ })
111
+ }
112
+ }
113
+
114
+ getRedirectUrl() {
115
+ return this.payload?.next_action?.redirect_to_url?.url
116
+ }
117
+
118
+ redirectToChallenge() {
119
+ const url = this.getRedirectUrl()
44
120
  if (url) {
45
- this.saveVerifyTransactionUrl();
46
- return true
121
+ this.saveVerifyTransactionUrl()
122
+ window.location = url;
47
123
  } else {
48
124
  console.log('No redirection found');
49
- return false
50
125
  }
51
126
  }
52
-
127
+
53
128
  // Returns an object
54
129
  // https://example.com/?name=John&age=30&city=NewYork
55
130
  // { name: "John", age: "30", city: "NewYork" }
@@ -64,8 +139,63 @@ export class ThreeDSHandler {
64
139
  return parameters;
65
140
  }
66
141
 
142
+ handleSuccessTransaction(response: any) {
143
+ this.removeVerifyTransactionUrl();
144
+ if(this.successUrl) {
145
+ window.location = this.successUrl as Location;
146
+ }
147
+ console.log('Transacción autorizada exitosamente.');
148
+ return response;
149
+ }
150
+
151
+ handleDeclinedTransaction(response: any) {
152
+ this.removeVerifyTransactionUrl();
153
+ console.log('Transacción rechazada.');
154
+ throw new Error("Transacción rechazada.");
155
+ }
156
+
157
+ // TODO: the method below needs to be tested with a real 3DS challenge
158
+ // since we couldn't get a test card that works with this feature
159
+ async handle3dsChallenge(response_json: any) {
160
+ // Create the form element:
161
+ const form = document.createElement('form');
162
+ form.name = 'frm';
163
+ form.method = 'POST';
164
+ form.action = response_json.redirect_post_url;
165
+
166
+ // Add hidden fields:
167
+ const creqInput = document.createElement('input');
168
+ creqInput.type = 'hidden';
169
+ creqInput.name = response_json.creq;
170
+ creqInput.value = response_json.creq;
171
+ form.appendChild(creqInput);
172
+
173
+ const termUrlInput = document.createElement('input');
174
+ termUrlInput.type = 'hidden';
175
+ termUrlInput.name = response_json.term_url;
176
+ termUrlInput.value = response_json.TermUrl;
177
+ form.appendChild(termUrlInput);
178
+
179
+ // Append the form to the body:
180
+ document.body.appendChild(form);
181
+ form.submit();
182
+
183
+ await this.verifyTransactionStatus();
184
+ }
185
+
186
+ async handleTransactionResponse(response: any) {
187
+ const response_json = await response.json();
188
+ if (response_json.status === "Pending") {
189
+ return await this.handle3dsChallenge(response_json);
190
+ } else if (["Success", "Authorized"].includes(response_json.status)) {
191
+ return this.handleSuccessTransaction(response);
192
+ } else {
193
+ return this.handleDeclinedTransaction(response);
194
+ }
195
+ }
196
+
67
197
  async verifyTransactionStatus() {
68
- const verifyUrl = this.getVerifyTransactionUrl();
198
+ const verifyUrl = this.getUrlWithExpiration();
69
199
 
70
200
  if (verifyUrl) {
71
201
  const url = `${this.baseUrl}${verifyUrl}`;
@@ -79,23 +209,19 @@ export class ThreeDSHandler {
79
209
  // body: JSON.stringify(data),
80
210
  });
81
211
 
82
- if (response.status === 200) {
83
- this.removeVerifyTransactionUrl();
84
- //@ts-ignore
85
- window.location = this.successUrl
86
- console.log('La transacción se verificó con éxito.');
87
- return response;
88
- } else {
212
+ if (response.status !== 200) {
89
213
  console.error('La verificación de la transacción falló.');
90
- return null;
214
+ return
91
215
  }
216
+
217
+ return await this.handleTransactionResponse(response);
92
218
  } catch (error) {
93
219
  console.error('Error al verificar la transacción:', error);
94
220
  return error;
95
221
  }
96
222
  } else {
97
223
  console.log('No verify_transaction_status_url found');
98
- return null;
99
224
  }
100
225
  }
226
+
101
227
  }
@@ -3,7 +3,8 @@ import { LiteCheckout } from '@tonder.io/ionic-lite-sdk';
3
3
  import {
4
4
  showError,
5
5
  showMessage,
6
- mapCards
6
+ mapCards,
7
+ getBrowserInfo
7
8
  } from '../helpers/utils';
8
9
  import { initSkyflow } from '../helpers/skyflow'
9
10
  import { ThreeDSHandler } from './3dsHandler';
@@ -62,6 +63,9 @@ export class InlineCheckout {
62
63
  radioChecked: string | null
63
64
  fetchingPayment: boolean
64
65
  isOpenPaySandbox: boolean = true
66
+ metadata = {}
67
+ card = {}
68
+ currency: string = ""
65
69
 
66
70
  constructor ({
67
71
  apiKey,
@@ -155,12 +159,27 @@ export class InlineCheckout {
155
159
  }
156
160
  }
157
161
 
162
+ #handleMetadata(data: { metadata: any }) {
163
+ this.metadata = data?.metadata
164
+ }
165
+
166
+ #handleCurrency(data: { currency: string }) {
167
+ this.currency = data?.currency
168
+ }
169
+
170
+ #handleCard(data: { card: string }) {
171
+ this.card = data?.card
172
+ }
173
+
158
174
  payment(data: any) {
159
175
  return new Promise(async (resolve, reject) => {
160
176
  try {
161
177
  this.#handleCustomer(data.customer)
162
178
  this.setCartTotal(data.cart?.total)
163
179
  this.setCartItems(data.cart?.items)
180
+ this.#handleMetadata(data)
181
+ this.#handleCurrency(data)
182
+ this.#handleCard(data)
164
183
  const response: ErrorResponse | StartCheckoutResponse | false | undefined = await this.#checkout()
165
184
  if (response) {
166
185
  const process3ds = new ThreeDSHandler({
@@ -169,11 +188,30 @@ export class InlineCheckout {
169
188
  apiKey: this.apiKeyTonder,
170
189
  successUrl: this.successUrl
171
190
  });
191
+ console.log("response", response);
172
192
  this.callBack(response);
173
- if (!process3ds.redirectTo3DS()) {
174
- resolve(response);
175
- } else {
176
- resolve(response);
193
+ if("next_action" in response) {
194
+ const iframe = response?.next_action?.iframe_resources?.iframe
195
+ if (iframe) {
196
+ process3ds.loadIframe()?.then(() => {
197
+ //TODO: Check if this will be necessary on the frontend side
198
+ // after some the tests in production, since the 3DS process
199
+ // doesn't works properly on the sandbox environment
200
+ // setTimeout(() => {
201
+ // process3ds.verifyTransactionStatus();
202
+ // }, 10000);
203
+ process3ds.verifyTransactionStatus();
204
+ }).catch((error) => {
205
+ console.log('Error loading iframe:', error)
206
+ })
207
+ } else {
208
+ const redirectUrl = process3ds.getRedirectUrl()
209
+ if (redirectUrl) {
210
+ process3ds.redirectToChallenge()
211
+ } else {
212
+ resolve(response);
213
+ }
214
+ }
177
215
  }
178
216
  }
179
217
  } catch (error) {
@@ -547,11 +585,15 @@ export class InlineCheckout {
547
585
  business_id: business.pk,
548
586
  payment_id: ("pk" in jsonResponsePayment) && jsonResponsePayment.pk,
549
587
  source: 'sdk',
588
+ metadata: this.metadata,
589
+ browser_info: getBrowserInfo(),
590
+ currency: this.currency
550
591
  };
551
592
 
552
593
  const jsonResponseRouter = await this.liteCheckout.startCheckoutRouter(
553
594
  routerItems
554
595
  );
596
+
555
597
  if (jsonResponseRouter) {
556
598
  try {
557
599
  const selector: any = document.querySelector(`#${this.collectorIds.tonderPayButton}`);
@@ -63,9 +63,20 @@ export async function initSkyflow(
63
63
  type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE,
64
64
  params: {
65
65
  max: 70,
66
+ error: "El campo nombre del titular debe tener menos de 70 caracteres"
66
67
  },
67
68
  };
68
69
 
70
+ const regexEmpty = RegExp("^(?!\s*$).+");
71
+
72
+ const regexMatchRule = {
73
+ type: Skyflow.ValidationRuleType.REGEX_MATCH_RULE,
74
+ params: {
75
+ regex: regexEmpty,
76
+ error: "El campo es requerido" // Optional, default error is 'VALIDATION FAILED'.
77
+ }
78
+ }
79
+
69
80
  const cardHolderNameElement: CollectElement | RevealElement | ComposableElement = await collectContainer.create({
70
81
  table: "cards",
71
82
  column: "cardholder_name",
@@ -73,13 +84,9 @@ export async function initSkyflow(
73
84
  label: "Titular de la tarjeta",
74
85
  placeholder: "Nombre como aparece en la tarjeta",
75
86
  type: Skyflow.ElementType.CARDHOLDER_NAME,
76
- validations: [lengthMatchRule],
87
+ validations: [regexMatchRule, lengthMatchRule],
77
88
  });
78
89
 
79
- if("setError" in cardHolderNameElement) {
80
- cardHolderNameElement.setError('Inválido')
81
- }
82
-
83
90
  // Create collect elements.
84
91
  const cardNumberElement: CollectElement | RevealElement | ComposableElement = await collectContainer.create({
85
92
  table: "cards",
@@ -92,12 +99,9 @@ export async function initSkyflow(
92
99
  label: "Número de tarjeta",
93
100
  placeholder: "1234 1234 1234 1234",
94
101
  type: Skyflow.ElementType.CARD_NUMBER,
102
+ validations: [regexMatchRule],
95
103
  });
96
104
 
97
- if("setError" in cardNumberElement) {
98
- cardNumberElement.setError('Inválido')
99
- }
100
-
101
105
  const cvvElement: CollectElement | RevealElement | ComposableElement = await collectContainer.create({
102
106
  table: "cards",
103
107
  column: "cvv",
@@ -105,11 +109,8 @@ export async function initSkyflow(
105
109
  label: "CVC/CVV",
106
110
  placeholder: "3-4 dígitos",
107
111
  type: Skyflow.ElementType.CVV,
112
+ validations: [regexMatchRule],
108
113
  });
109
-
110
- if("setError" in cvvElement) {
111
- cvvElement.setError('Inválido')
112
- }
113
114
 
114
115
  const expiryMonthElement: CollectElement | RevealElement | ComposableElement = await collectContainer.create({
115
116
  table: "cards",
@@ -118,12 +119,9 @@ export async function initSkyflow(
118
119
  label: "Fecha de expiración",
119
120
  placeholder: "MM",
120
121
  type: Skyflow.ElementType.EXPIRATION_MONTH,
122
+ validations: [regexMatchRule],
121
123
  });
122
124
 
123
- if("setError" in expiryMonthElement) {
124
- expiryMonthElement.setError('Inválido')
125
- }
126
-
127
125
  const expiryYearElement: CollectElement | RevealElement | ComposableElement = await collectContainer.create({
128
126
  table: "cards",
129
127
  column: "expiration_year",
@@ -131,12 +129,9 @@ export async function initSkyflow(
131
129
  label: "",
132
130
  placeholder: "AA",
133
131
  type: Skyflow.ElementType.EXPIRATION_YEAR,
132
+ validations: [regexMatchRule],
134
133
  });
135
134
 
136
- if("setError" in expiryYearElement) {
137
- expiryYearElement.setError('Inválido')
138
- }
139
-
140
135
  await mountElements(
141
136
  { id: collectorIds.cardNumber, element: cardNumberElement },
142
137
  { id: collectorIds.cvv, element: cvvElement },
@@ -133,4 +133,17 @@ export const mapCards = (card: CardResponse) => {
133
133
  const last = carArr[carArr.length - 1];
134
134
  newCard.card_number = `••••${last}`;
135
135
  return newCard;
136
+ }
137
+
138
+ export const getBrowserInfo = () => {
139
+ const browserInfo = {
140
+ javascript_enabled: true, // Assumed since JavaScript is running
141
+ time_zone: new Date().getTimezoneOffset(),
142
+ language: navigator.language || 'en-US', // Fallback to 'en-US'
143
+ color_depth: window.screen ? window.screen.colorDepth : null,
144
+ screen_width: window.screen ? window.screen.width * window.devicePixelRatio || window.screen.width : null,
145
+ screen_height: window.screen ? window.screen.height * window.devicePixelRatio || window.screen.height : null,
146
+ user_agent: navigator.userAgent,
147
+ };
148
+ return browserInfo;
136
149
  }