@tonder.io/ionic-lite-sdk 0.0.63-beta.DEV-1975.3 → 0.0.63-beta.DEV-1975.5

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.
@@ -35,6 +35,7 @@ import {GetSecureTokenResponse} from "../types/responses";
35
35
  import {getSecureToken} from "../data/tokenApi";
36
36
  import {MESSAGES} from "../shared/constants/messages";
37
37
  import { IMPConfigRequest } from "../types/mercadoPago";
38
+ import { CardOnFile } from "../helpers/card_on_file";
38
39
  export class BaseInlineCheckout<T extends CustomizationOptions = CustomizationOptions> {
39
40
  baseUrl = "";
40
41
  cartTotal: string | number = "0";
@@ -58,6 +59,7 @@ export class BaseInlineCheckout<T extends CustomizationOptions = CustomizationOp
58
59
  order_reference?: string | null = null;
59
60
  card? = {};
60
61
  currency?: string = "";
62
+ protected cardOnFileInstance: CardOnFile | null = null;
61
63
  #apm_config?:IMPConfigRequest | Record<string, any>
62
64
  #customerData?: Record<string, any>;
63
65
 
@@ -153,6 +155,10 @@ export class BaseInlineCheckout<T extends CustomizationOptions = CustomizationOp
153
155
  ) {
154
156
  injectMercadoPagoSecurity();
155
157
  }
158
+
159
+ if (this._hasCardOnFileKeys()) {
160
+ await this._initializeCardOnFile();
161
+ }
156
162
  }
157
163
 
158
164
  async _checkout(data: any): Promise<any> {
@@ -184,16 +190,16 @@ export class BaseInlineCheckout<T extends CustomizationOptions = CustomizationOp
184
190
  payment_method,
185
191
  customer,
186
192
  isSandbox,
187
- card_on_file_id,
193
+ enable_card_on_file,
188
194
  // TODO: DEPRECATED
189
195
  returnUrl: returnUrlData
190
196
  }: {
191
- card?: string;
197
+ card?: Record<string, any>;
192
198
  payment_method?: string;
193
199
  customer: Record<string, any>;
194
200
  isSandbox?: boolean;
195
201
  returnUrl?: string;
196
- card_on_file_id?: string;
202
+ enable_card_on_file?: boolean;
197
203
  }) {
198
204
  const { openpay_keys, reference, business } = this.merchantData!;
199
205
  const total = Number(this.cartTotal);
@@ -286,7 +292,7 @@ export class BaseInlineCheckout<T extends CustomizationOptions = CustomizationOp
286
292
  ...(!!payment_method ? { payment_method } : { card }),
287
293
  apm_config: this.#apm_config,
288
294
  ...(this.customer && "identification" in this.customer ? { identification: this.customer.identification } : {}),
289
- ...(!!card_on_file_id ? { card_on_file_id, enable_card_on_file: true } : {}),
295
+ ...(enable_card_on_file !== undefined ? { enable_card_on_file } : {}),
290
296
  };
291
297
 
292
298
  const jsonResponseRouter = await startCheckoutRouter(
@@ -372,6 +378,23 @@ export class BaseInlineCheckout<T extends CustomizationOptions = CustomizationOp
372
378
  return await fetchCustomerPaymentMethods(this.baseUrl, this.apiKeyTonder);
373
379
  }
374
380
 
381
+ protected _hasCardOnFileKeys(): boolean {
382
+ return !!this.merchantData?.cardonfile_keys?.public_key;
383
+ }
384
+
385
+ protected async _initializeCardOnFile(): Promise<CardOnFile> {
386
+ if (!this.cardOnFileInstance) {
387
+ this.cardOnFileInstance = new CardOnFile({
388
+ merchantId: this.merchantData?.cardonfile_keys!.public_key!,
389
+ apiKey: this.apiKeyTonder,
390
+ isTestEnvironment: this.mode !== "production",
391
+ });
392
+ await this.cardOnFileInstance.initialize();
393
+ }
394
+
395
+ return this.cardOnFileInstance;
396
+ }
397
+
375
398
  #handleCustomer(customer: ICustomer | { email: string }) {
376
399
  if (!customer) return;
377
400
 
@@ -3,12 +3,12 @@ import {ErrorResponse} from "./errorResponse";
3
3
  import TonderError from "../shared/utils/errors";
4
4
  import {ErrorKeyEnum} from "../shared/enum/ErrorKeyEnum";
5
5
  import {
6
- buildErrorResponse,
7
- buildErrorResponseFromCatch,
8
- formatPublicErrorResponse,
9
- getBrowserInfo,
10
- getBusinessId,
11
- getCardType,
6
+ buildErrorResponse,
7
+ buildErrorResponseFromCatch,
8
+ formatPublicErrorResponse,
9
+ getBrowserInfo,
10
+ getBusinessId,
11
+ getCardType,
12
12
  } from "../helpers/utils";
13
13
  import {getCustomerAPMs} from "../data/api";
14
14
  import {BaseInlineCheckout} from "./BaseInlineCheckout";
@@ -17,38 +17,32 @@ import {getSkyflowTokens, initSkyflowInstance, mountSkyflowFields} from "../help
17
17
  import {startCheckoutRouter} from "../data/checkoutApi";
18
18
  import {getOpenpayDeviceSessionID} from "../data/openPayApi";
19
19
  import {getPaymentMethodDetails} from "../shared/catalog/paymentMethodsCatalog";
20
+ import {APM, IEvents, IInlineLiteCheckoutOptions, InCollectorContainer, TonderAPM} from "../types/commons";
20
21
  import {
21
- APM, IEvents,
22
- IInlineLiteCheckoutOptions,
23
- ILiteCustomizationOptions,
24
- InCollectorContainer,
25
- TonderAPM
26
- } from "../types/commons";
27
- import {
28
- ICustomerCardsResponse,
29
- IMountCardFieldsRequest,
30
- ISaveCardRequest,
31
- ISaveCardResponse,
32
- ISaveCardSkyflowRequest
22
+ ICustomerCardsResponse,
23
+ IMountCardFieldsRequest,
24
+ ISaveCardRequest,
25
+ ISaveCardResponse,
26
+ ISaveCardSkyflowRequest
33
27
  } from "../types/card";
34
28
  import {IPaymentMethod} from "../types/paymentMethod";
35
29
  import {
36
- CreateOrderResponse,
37
- CreatePaymentResponse,
38
- CustomerRegisterResponse,
39
- GetBusinessResponse,
40
- IErrorResponse,
41
- RegisterCustomerCardResponse,
42
- StartCheckoutResponse
30
+ CreateOrderResponse,
31
+ CreatePaymentResponse,
32
+ CustomerRegisterResponse,
33
+ GetBusinessResponse,
34
+ IErrorResponse,
35
+ RegisterCustomerCardResponse,
36
+ StartCheckoutResponse
43
37
  } from "../types/responses";
44
38
  import {
45
- CreateOrderRequest,
46
- CreatePaymentRequest,
47
- RegisterCustomerCardRequest,
48
- StartCheckoutFullRequest,
49
- StartCheckoutIdRequest,
50
- StartCheckoutRequest,
51
- TokensRequest
39
+ CreateOrderRequest,
40
+ CreatePaymentRequest,
41
+ RegisterCustomerCardRequest,
42
+ StartCheckoutFullRequest,
43
+ StartCheckoutIdRequest,
44
+ StartCheckoutRequest,
45
+ TokensRequest
52
46
  } from "../types/requests";
53
47
  import {ICardFields, IStartCheckoutResponse} from "../types/checkout";
54
48
  import {ILiteCheckout} from "../types/liteInlineCheckout";
@@ -69,6 +63,7 @@ export class LiteCheckout extends BaseInlineCheckout implements ILiteCheckout{
69
63
  private readonly events: IEvents
70
64
  // Store mounted elements by context: 'create' or 'update:card_id'
71
65
  private mountedElementsByContext: Map<string, { elements: any[], container: InCollectorContainer | null }> = new Map();
66
+ private customerCardsCache: ICustomerCardsResponse | null = null;
72
67
 
73
68
  constructor({ apiKey, mode, returnUrl, callBack, apiKeyTonder, baseUrlTonder, customization, collectorIds, events }: IInlineLiteCheckoutOptions) {
74
69
  super({ mode, apiKey, returnUrl, callBack, apiKeyTonder, baseUrlTonder, customization, tdsIframeId: collectorIds && 'tdsIframe' in collectorIds ? collectorIds?.tdsIframe : "tdsIframe"});
@@ -88,13 +83,14 @@ export class LiteCheckout extends BaseInlineCheckout implements ILiteCheckout{
88
83
  auth_token,
89
84
  this.merchantData!.business.pk,
90
85
  );
86
+ this.customerCardsCache = response;
91
87
 
92
88
  return {
93
- ...response,
94
- cards: response.cards.map((ic) => ({
95
- ...ic,
96
- icon: getCardType(ic.fields.card_scheme),
97
- })),
89
+ ...response,
90
+ cards: response.cards.map((ic) => ({
91
+ ...ic,
92
+ icon: getCardType(ic.fields.card_scheme),
93
+ })),
98
94
  };
99
95
  } catch (error) {
100
96
  throw formatPublicErrorResponse(
@@ -111,28 +107,71 @@ export class LiteCheckout extends BaseInlineCheckout implements ILiteCheckout{
111
107
  ): Promise<ISaveCardResponse> {
112
108
  try {
113
109
  await this._fetchMerchantData();
114
- const { auth_token } = await this._getCustomer();
110
+ const customerResponse = await this._getCustomer() as CustomerRegisterResponse;
111
+ const { auth_token, first_name = "", last_name = "", email = "" } = customerResponse;
115
112
  const { vault_id, vault_url, business } = this.merchantData!;
113
+ const cardOnFileEnabled = this._hasCardOnFileKeys();
114
+
115
+ const sanitizedCard = {
116
+ card_number: card.card_number.replace(/\s+/g, ""),
117
+ expiration_month: card.expiration_month.replace(/\s+/g, ""),
118
+ expiration_year: card.expiration_year.replace(/\s+/g, ""),
119
+ cvv: card.cvv.replace(/\s+/g, ""),
120
+ cardholder_name: card.cardholder_name.replace(/\s+/g, ""),
121
+ };
116
122
 
117
123
  const skyflowTokens: ISaveCardSkyflowRequest = await getSkyflowTokens({
118
124
  vault_id: vault_id,
119
125
  vault_url: vault_url,
120
- data: {...card,
121
- card_number: card.card_number.replace(/\s+/g, ""),
122
- expiration_month: card.expiration_month.replace(/\s+/g, ""),
123
- expiration_year: card.expiration_year.replace(/\s+/g, ""),
124
- cvv: card.cvv.replace(/\s+/g, ""),
125
- cardholder_name: card.cardholder_name.replace(/\s+/g, ""),
126
- },
126
+ data: sanitizedCard,
127
127
  baseUrl: this.baseUrl,
128
128
  apiKey: this.apiKeyTonder,
129
129
  });
130
130
 
131
- return await this._saveCustomerCard(
131
+ const saveResponse = await this._saveCustomerCard(
132
132
  auth_token,
133
133
  business?.pk,
134
134
  skyflowTokens,
135
+ cardOnFileEnabled,
135
136
  );
137
+
138
+ if (cardOnFileEnabled) {
139
+ const cardBin = saveResponse.card_bin;
140
+ if (!cardBin) {
141
+ throw new Error("Card BIN not returned from save card");
142
+ }
143
+
144
+ const cardOnFile = await this._initializeCardOnFile();
145
+ const result = await cardOnFile.process({
146
+ cardTokens: {
147
+ name: card.cardholder_name.trim(),
148
+ number: sanitizedCard.card_number,
149
+ expiryMonth: sanitizedCard.expiration_month,
150
+ expiryYear: sanitizedCard.expiration_year,
151
+ cvv: sanitizedCard.cvv,
152
+ },
153
+ cardBin,
154
+ contactDetails: {
155
+ firstName: first_name || "",
156
+ lastName: last_name || "",
157
+ email: email || "",
158
+ },
159
+ customerId: auth_token,
160
+ currency: this.currency || "MXN",
161
+ });
162
+
163
+ const updatePayload: ISaveCardSkyflowRequest = {
164
+ skyflow_id: skyflowTokens.skyflow_id,
165
+ subscription_id: result.subscriptionId,
166
+ };
167
+ await this._saveCustomerCard(
168
+ auth_token,
169
+ business?.pk,
170
+ updatePayload,
171
+ );
172
+ }
173
+
174
+ return saveResponse;
136
175
  } catch (error) {
137
176
  throw formatPublicErrorResponse(
138
177
  {
@@ -233,6 +272,26 @@ export class LiteCheckout extends BaseInlineCheckout implements ILiteCheckout{
233
272
  return (contextData?.container?.container as CollectorContainer) || null;
234
273
  }
235
274
 
275
+ private async collectCardTokens(cardId: string): Promise<Record<string, any> | null> {
276
+ const container = this.getContainerByCardId(cardId);
277
+ if (!container) {
278
+ return null;
279
+ }
280
+
281
+ try {
282
+ const collectResponse: any = await container.collect();
283
+ return collectResponse?.records?.[0]?.fields || null;
284
+ } catch (e: any) {
285
+ const errorDescription = e?.error?.description;
286
+ throw new TonderError({
287
+ code: ErrorKeyEnum.MOUNT_COLLECT_ERROR,
288
+ details: {
289
+ message: errorDescription
290
+ }
291
+ });
292
+ }
293
+ }
294
+
236
295
  public unmountCardFields(context: string = 'all'): void {
237
296
  if (context === 'all') {
238
297
  this.mountedElementsByContext.forEach((contextData, ctx) => {
@@ -346,40 +405,120 @@ export class LiteCheckout extends BaseInlineCheckout implements ILiteCheckout{
346
405
  returnUrl?: string;
347
406
  }) {
348
407
  await this._fetchMerchantData();
349
- const customer = await this._getCustomer(this.abortController.signal);
350
- const { vault_id, vault_url } = this.merchantData!;
408
+ const customer = await this._getCustomer(this.abortController.signal) as CustomerRegisterResponse;
409
+ const { vault_id, vault_url, business } = this.merchantData!;
410
+ const { auth_token, first_name = "", last_name = "", email = "" } = customer;
411
+ const cardOnFileEnabled = this._hasCardOnFileKeys();
351
412
  let skyflowTokens;
413
+ let subscriptionCard: { subscriptionId: string } | null = null;
414
+ let skyflowId: string | null = null;
415
+ let cardTokensForCardOnFile: { name: string; number: string; expiryMonth: string; expiryYear: string; cvv: string } | null = null;
416
+ let shouldProcessCardOnFile = false;
417
+
418
+ const ensureCustomerCards = async () => {
419
+ if (!this.customerCardsCache?.cards?.length) {
420
+ this.customerCardsCache = await this._getCustomerCards(
421
+ auth_token,
422
+ this.merchantData!.business.pk,
423
+ );
424
+ }
425
+ };
426
+
352
427
  if (!payment_method) {
353
428
  if (typeof card === "string") {
354
- // Get the container from the specific context for this card
355
- const container = this.getContainerByCardId(card);
429
+ skyflowId = card;
430
+ skyflowTokens = {
431
+ skyflow_id: card,
432
+ };
433
+
434
+ if (cardOnFileEnabled) {
435
+ await ensureCustomerCards();
436
+ const selectedCard = this.customerCardsCache?.cards?.find(
437
+ (item) => item.fields.skyflow_id === card,
438
+ );
439
+ if (!selectedCard) {
440
+ throw new Error("Card not found for card-on-file processing");
441
+ }
442
+ const hasSubscriptionId = !!selectedCard.fields.subscription_id;
356
443
 
357
- try{
358
- if (container) {
359
- await container.collect();
444
+ if (!hasSubscriptionId) {
445
+ await this.collectCardTokens(card);
360
446
  }
361
- }catch (e: any){
362
- if (container) {
363
- const errorDescription = e?.error?.description;
364
- throw new TonderError({
365
- code: ErrorKeyEnum.MOUNT_COLLECT_ERROR,
366
- details: {
367
- message: errorDescription
368
- }
369
- });
447
+
448
+ if (hasSubscriptionId) {
449
+ subscriptionCard = { subscriptionId: selectedCard.fields.subscription_id! };
370
450
  }
451
+ } else {
452
+ await this.collectCardTokens(card);
371
453
  }
372
- skyflowTokens = {
373
- skyflow_id: card,
374
- };
375
454
  } else {
455
+ const sanitizedCard = {
456
+ ...card!,
457
+ card_number: card!.card_number.replace(/\s+/g, ""),
458
+ expiration_month: card!.expiration_month.replace(/\s+/g, ""),
459
+ expiration_year: card!.expiration_year.replace(/\s+/g, ""),
460
+ cvv: card!.cvv.replace(/\s+/g, ""),
461
+ cardholder_name: card!.cardholder_name.replace(/\s+/g, ""),
462
+ };
376
463
  skyflowTokens = await getSkyflowTokens({
377
464
  vault_id: vault_id,
378
465
  vault_url: vault_url,
379
- data: { ...card, card_number: card!.card_number.replace(/\s+/g, "") },
466
+ data: sanitizedCard,
380
467
  baseUrl: this.baseUrl,
381
468
  apiKey: this.apiKeyTonder,
382
469
  });
470
+ skyflowId = skyflowTokens.skyflow_id;
471
+
472
+ if (cardOnFileEnabled) {
473
+ cardTokensForCardOnFile = {
474
+ name: skyflowTokens.cardholder_name,
475
+ number: skyflowTokens.card_number,
476
+ expiryMonth: skyflowTokens.expiration_month,
477
+ expiryYear: skyflowTokens.expiration_year,
478
+ cvv: skyflowTokens.cvv,
479
+ };
480
+ shouldProcessCardOnFile = true;
481
+ }
482
+ }
483
+
484
+ if (shouldProcessCardOnFile) {
485
+ if (!skyflowId || !cardTokensForCardOnFile) {
486
+ throw new Error("Missing card data for card-on-file processing");
487
+ }
488
+
489
+ const saveResponse = await this._saveCustomerCard(
490
+ auth_token,
491
+ business?.pk,
492
+ { skyflow_id: skyflowId },
493
+ true,
494
+ );
495
+ const cardBin = saveResponse.card_bin;
496
+ if (!cardBin) {
497
+ throw new Error("Card BIN not returned from save card");
498
+ }
499
+
500
+ const cardOnFile = await this._initializeCardOnFile();
501
+ const result = await cardOnFile.process({
502
+ cardTokens: cardTokensForCardOnFile,
503
+ cardBin,
504
+ contactDetails: {
505
+ firstName: first_name || "",
506
+ lastName: last_name || "",
507
+ email: email || "",
508
+ },
509
+ customerId: auth_token,
510
+ currency: this.currency || "MXN",
511
+ });
512
+ subscriptionCard = { subscriptionId: result.subscriptionId };
513
+
514
+ await this._saveCustomerCard(
515
+ auth_token,
516
+ business?.pk,
517
+ {
518
+ skyflow_id: skyflowId,
519
+ subscription_id: subscriptionCard.subscriptionId,
520
+ },
521
+ );
383
522
  }
384
523
  }
385
524
 
@@ -388,7 +527,8 @@ export class LiteCheckout extends BaseInlineCheckout implements ILiteCheckout{
388
527
  payment_method,
389
528
  customer,
390
529
  isSandbox,
391
- returnUrl: returnUrlData
530
+ returnUrl: returnUrlData,
531
+ enable_card_on_file: !!subscriptionCard?.subscriptionId,
392
532
  });
393
533
  }
394
534