@tonder.io/ionic-lite-sdk 0.0.68 → 0.0.69-beta.TEC-192.ee4f7cd

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
@@ -1,840 +1,1320 @@
1
- # Tonder SDK
2
-
3
- Tonder SDK helps to integrate the services Tonder offers in your own mobile app
1
+ # @tonder.io/ionic-lite-sdk
4
2
 
3
+ PCI DSS–compliant payment SDK for Ionic, Angular, and React.
4
+ Card data is collected through **Skyflow secure iframes** — raw card values never touch your application code.
5
5
 
6
6
  ## Table of Contents
7
7
 
8
- 1. [Installation](#installation)
9
- 2. [Usage](#usage)
10
- 3. [Configuration Options](#configuration-options)
11
- 4. [Card On File](#card-on-file)
12
- 5. [Mobile Settings](#mobile-settings)
13
- 6. [Payment Data Structure](#payment-data-structure)
14
- 7. [Field Validation Functions](#field-validation-functions)
15
- 8. [API Reference](#api-reference)
16
- 9. [Error Handling](#error-handling)
17
- 10. [Examples](#examples)
18
- 11. [Deprecated Fields](#deprecated-fields)
19
- 12. [Deprecated Functions](#deprecated-functions)
20
- 13. [License](#license)
8
+ 1. [Quick Start](#1-quick-start)
9
+ 2. [Installation](#2-installation)
10
+ 3. [Constructor & Configuration](#3-constructor--configuration)
11
+ - [3.1 Options reference](#31-options-reference)
12
+ - [3.2 Customization options](#32-customization-options)
13
+ - [3.3 Form events](#33-form-events)
14
+ 4. [Initialization Sequence](#4-initialization-sequence)
15
+ 5. [Collecting Card Data — `mountCardFields`](#5-collecting-card-data--mountcardfields)
16
+ - [5.1 New-card form (all 5 fields)](#51-new-card-form-all-5-fields)
17
+ - [5.2 Saved-card CVV only](#52-saved-card-cvv-only)
18
+ - [5.3 Unmounting fields](#53-unmounting-fields)
19
+ 6. [Processing Payments](#6-processing-payments)
20
+ - [6.1 New card payment](#61-new-card-payment)
21
+ - [6.2 Saved card payment](#62-saved-card-payment)
22
+ - [6.3 Alternative Payment Method (APM)](#63-alternative-payment-method-apm)
23
+ - [6.4 Payment response reference](#64-payment-response-reference)
24
+ 7. [3DS Handling](#7-3ds-handling)
25
+ 8. [Managing Saved Cards](#8-managing-saved-cards)
26
+ - [8.1 List saved cards](#81-list-saved-cards)
27
+ - [8.2 Save a new card](#82-save-a-new-card)
28
+ - [8.3 Remove a card](#83-remove-a-card)
29
+ - [8.4 Card On File (subscription_id)](#84-card-on-file-subscription_id)
30
+ 9. [Revealing Card Data — `revealCardFields`](#9-revealing-card-data--revealcardfields)
31
+ 10. [Error Handling](#10-error-handling)
32
+ - [10.1 Error structure](#101-error-structure)
33
+ - [10.2 Error code reference](#102-error-code-reference)
34
+ 11. [Customization & Styling](#11-customization--styling)
35
+ - [11.1 Global form styles](#111-global-form-styles)
36
+ - [11.2 Per-field styles](#112-per-field-styles)
37
+ - [11.3 Labels & placeholders](#113-labels--placeholders)
38
+ 12. [Deprecated API](#12-deprecated-api)
39
+
40
+ ---
41
+
42
+ ## 1. Quick Start
43
+
44
+ Get a working payment form in under 5 minutes. This example uses the minimum required setup. See [Section 4](#4-initialization-sequence) for the full step-by-step explanation.
45
+
46
+ **Angular template:**
47
+
48
+ ```html
49
+ <!-- 3DS iframe — only add when using redirectOnComplete: false (see Section 7).
50
+ Remove this element if you are using the default redirectOnComplete: true. -->
51
+ <iframe id="tdsIframe" allowtransparency="true" class="tds-iframe"></iframe>
52
+
53
+ <!-- Secure iframes — card values never touch your code -->
54
+ <div id="collect_cardholder_name"></div>
55
+ <div id="collect_card_number"></div>
56
+ <div id="collect_expiration_month"></div>
57
+ <div id="collect_expiration_year"></div>
58
+ <div id="collect_cvv"></div>
59
+
60
+ <button (click)="pay()">Pay</button>
61
+ ```
62
+
63
+ **Angular component:**
64
+
65
+ ```typescript
66
+ import { Component, OnInit } from '@angular/core';
67
+ import { LiteCheckout, AppError } from '@tonder.io/ionic-lite-sdk';
68
+
69
+ @Component({ selector: 'app-checkout', templateUrl: './checkout.component.html' })
70
+ export class CheckoutComponent implements OnInit {
71
+ private liteCheckout!: LiteCheckout;
72
+ loading = false;
73
+
74
+ async ngOnInit() {
75
+ // Step 1 — Create instance
76
+ this.liteCheckout = new LiteCheckout({
77
+ apiKey: 'YOUR_PUBLIC_API_KEY',
78
+ mode: 'stage',
79
+ returnUrl: `${window.location.origin}/checkout`,
80
+ });
81
+
82
+ // Step 2 — Fetch secure token from YOUR backend (never expose YOUR_SECRET_API_KEY on the frontend)
83
+ // Your backend calls POST https://stage.tonder.io/api/secure-token/ with the secret key
84
+ // and returns { access: string } to the client.
85
+ const { access } = await fetch('/api/tonder-secure-token', {
86
+ method: 'POST',
87
+ }).then(r => r.json());
88
+
89
+ // Step 3 — Configure with customer + token
90
+ this.liteCheckout.configureCheckout({
91
+ customer: { email: 'user@example.com' },
92
+ secureToken: access,
93
+ // cart: { total: 100, items: [...] },
94
+ // currency: 'MXN',
95
+ // order_reference: 'ORD-001', // your internal order ID — shows in Tonder dashboard & exports
96
+ // metadata: { order_id: 'ORD-001' }, // reporting metadata — see Section 6.1
97
+ });
98
+
99
+ // Step 4 — Initialize checkout (must be awaited)
100
+ await this.liteCheckout.injectCheckout();
101
+
102
+ // Step 5 — Check if returning from a 3DS redirect
103
+ // Only needed when redirectOnComplete: true (default). In iframe mode
104
+ // (redirectOnComplete: false) the payment() promise resolves directly — skip this step.
105
+ // If this page load is a return from a 3DS challenge, the SDK verifies the
106
+ // transaction and, if routing is configured, may automatically retry with
107
+ // the next payment route. Await the result before deciding what to do next.
108
+ const tdsResult = await this.liteCheckout.verify3dsTransaction();
109
+ if (tdsResult) {
110
+ const status = (tdsResult as any).transaction_status;
111
+ if (status === 'Success' ) {
112
+ // navigate to order confirmation
113
+ } else {
114
+ // show error to the user
115
+ }
116
+ return; // do not mount card fields — the payment flow already completed
117
+ }
118
+
119
+ // Step 6 — Normal page load: mount secure card input iframes
120
+ await this.liteCheckout.mountCardFields({
121
+ fields: ['cardholder_name', 'card_number', 'expiration_month', 'expiration_year', 'cvv'],
122
+ });
123
+ }
124
+
125
+ async pay() {
126
+ this.loading = true;
127
+ try {
128
+ const response = await this.liteCheckout.payment({
129
+ customer: { email: 'user@example.com' },
130
+ cart: {
131
+ total: 100,
132
+ items: [{
133
+ name: 'Product A', description: 'Product description',
134
+ quantity: 1, price_unit: 100, discount: 0, taxes: 0,
135
+ product_reference: 'SKU-001', amount_total: 100,
136
+ }],
137
+ },
138
+ currency: 'MXN',
139
+ // order_reference: 'ORD-001', // your internal order ID — shows in Tonder dashboard & exports
140
+ // metadata: { order_id: 'ORD-001' }, // reporting metadata — see Section 6.1
141
+ });
142
+ console.log('Transaction status:', response.transaction_status);
143
+ } catch (error) {
144
+ if (error instanceof AppError) {
145
+ console.error(`[${error.code}] ${error.message}`);
146
+ }
147
+ }
148
+ }
149
+ }
150
+ ```
151
+
152
+ > **Security note:** In this example `YOUR_SECRET_API_KEY` is hardcoded for brevity. In production, move this `fetch` call to your own backend and return only the `access` token to the frontend. See [Section 4](#4-initialization-sequence).
21
153
 
154
+ ---
22
155
 
23
- ## Installation
156
+ ## 2. Installation
24
157
 
25
- You can install using NPM
26
158
  ```bash
27
- npm i @tonder.io/ionic-lite-sdk
159
+ npm install @tonder.io/ionic-lite-sdk
160
+ # or
161
+ yarn add @tonder.io/ionic-lite-sdk
28
162
  ```
29
163
 
30
- Add dependencies to the root of the app (index.html) only if you are going to use Openpay as the payment processor.
31
- ```html
32
- <script src=https://openpay.s3.amazonaws.com/openpay.v1.min.js></script>
33
- <script src=https://openpay.s3.amazonaws.com/openpay-data.v1.min.js></script>
164
+ ---
165
+
166
+ ## 3. Constructor & Configuration
167
+
168
+ ### 3.1 Options reference
169
+
170
+ ```typescript
171
+ import { LiteCheckout } from '@tonder.io/ionic-lite-sdk';
172
+
173
+ const liteCheckout = new LiteCheckout(options);
34
174
  ```
35
175
 
36
- ## Usage
37
- LiteCheckout allows you to build a custom checkout interface using Tonder's core functionality
38
- ### Import LiteCheckout class
39
- ```javascript
40
- import { LiteCheckout } from "@tonder.io/ionic-lite-sdk"
176
+ | Property | Type | Required | Default | Description |
177
+ |----------|------|----------|---------|-------------|
178
+ | `apiKey` | `string` | **Required** | — | Public API key from the Tonder Dashboard |
179
+ | `mode` | `'stage' \| 'production'` | **Required** | `'stage'` | Target environment |
180
+ | `returnUrl` | `string` | **Required for 3DS** | — | URL to which 3DS redirects return after authentication |
181
+ | `callBack` | `(response) => void` | Optional | `undefined` | Called after a successful payment or card enrollment |
182
+ | `customization` | `ILiteCustomizationOptions` | Optional | `undefined` | Styles, labels, placeholders, and redirect behavior |
183
+ | `events` | `ICardFormEvents` | Optional | `undefined` | `onChange` / `onFocus` / `onBlur` callbacks per field |
184
+ | `tdsIframeId` | `string` | Optional | `'tdsIframe'` | DOM `id` of the 3DS `<iframe>` element |
185
+
186
+ **Full example:**
187
+
188
+ ```typescript
189
+ const liteCheckout = new LiteCheckout({
190
+ apiKey: 'YOUR_PUBLIC_API_KEY',
191
+ mode: 'production',
192
+ returnUrl: 'https://myapp.com/checkout',
193
+ callBack: (response) => console.log('Payment done', response),
194
+ tdsIframeId: 'myCustomTdsFrame',
195
+ customization: {
196
+ redirectOnComplete: false, // render 3DS challenge inside #tdsIframe instead of full-page redirect
197
+ styles: { enableCardIcon: true },
198
+ },
199
+ events: {
200
+ cardNumberEvents: {
201
+ onChange: (e) => console.log('Card number valid:', e.isValid),
202
+ },
203
+ },
204
+ });
41
205
  ```
42
- ### Create instance
43
206
 
44
- ```javascript
45
- const liteCheckout = new LiteCheckout({
46
- signal,
47
- baseUrlTonder,
48
- apiKeyTonder
49
- })
207
+ ---
50
208
 
51
- // The configureCheckout function allows you to set initial information,
52
- // such as the customer's email, which is used to retrieve a list of saved cards, save new card, etc.
53
- inlineCheckout.configureCheckout({ customer: { email: "example@email.com" } });
209
+ ### 3.2 Customization options
54
210
 
55
- // Initialize the checkout
56
- await liteCheckout.injectCheckout();
211
+ ```typescript
212
+ interface ILiteCustomizationOptions {
213
+ styles?: IStyles; // Field and form visual styles (see Section 11)
214
+ labels?: IFormLabels; // Label text per field
215
+ placeholders?: IFormPlaceholder; // Placeholder text per field
216
+ redirectOnComplete?: boolean; // default: true
217
+ }
218
+ ```
219
+
220
+ **`redirectOnComplete`** controls how 3DS challenges are displayed when the payment processor requires authentication:
221
+
222
+ | Value | Behavior |
223
+ |-------|----------|
224
+ | `true` (default) | SDK performs a **full-page redirect** to the 3DS challenge URL. The user leaves your app, completes authentication on the bank's page, and is sent back to `returnUrl`. |
225
+ | `false` | The 3DS challenge is rendered **inside the `#tdsIframe` element**. The user stays in your app until the challenge resolves. |
226
+
227
+ > Use `redirectOnComplete: false` when you want to keep the user inside the app (e.g., in a mobile WebView). Make sure the `#tdsIframe` is styled to cover the screen when active — see [Section 7](#7-3ds-handling).
228
+
229
+ ---
57
230
 
58
- // To verify a 3ds transaction you can use the following method
59
- // It should be called after the injectCheckout method
60
- // The response status will be one of the following
61
- // ['Declined', 'Cancelled', 'Failed', 'Success', 'Pending', 'Authorized']
231
+ ### 3.3 Form events
232
+
233
+ Register callbacks to react to field state changes (validation, focus, etc.):
234
+
235
+ ```typescript
236
+ interface ICardFormEvents {
237
+ cardHolderEvents?: IInputEvents;
238
+ cardNumberEvents?: IInputEvents;
239
+ cvvEvents?: IInputEvents;
240
+ monthEvents?: IInputEvents;
241
+ yearEvents?: IInputEvents;
242
+ }
243
+
244
+ interface IInputEvents {
245
+ onChange?: (event: IEventSecureInput) => void;
246
+ onFocus?: (event: IEventSecureInput) => void;
247
+ onBlur?: (event: IEventSecureInput) => void;
248
+ }
62
249
 
63
- inlineCheckout.verify3dsTransaction().then(response => {
64
- console.log('Verify 3ds response', response)
65
- })
250
+ interface IEventSecureInput {
251
+ elementType: string; // e.g. 'CARD_NUMBER', 'CVV', 'CARDHOLDER_NAME'
252
+ isEmpty: boolean;
253
+ isFocused: boolean;
254
+ isValid: boolean;
255
+ value?: string; // See PCI note below
256
+ }
66
257
  ```
67
258
 
68
- ```javascript
69
- // Retrieve customer's saved cards
70
- const cards = await liteCheckout.getCustomerCards();
259
+ > **PCI note:** `value` is only populated in `development` mode. In `production`, `value` is always `''` for sensitive fields (`card_number`, `cvv`); non-sensitive fields (`cardholder_name`, `expiration_month`, `expiration_year`) may still return their value. Use `isValid` and `isEmpty` for UI state logic — never depend on `value` in production.
260
+
261
+ **Example live card form validation:**
262
+
263
+ ```typescript
264
+ events: {
265
+ cardNumberEvents: {
266
+ onChange: (e) => { this.cardNumberValid = e.isValid; },
267
+ onBlur: (e) => { this.showCardNumberError = !e.isValid && !e.isEmpty; },
268
+ },
269
+ cvvEvents: {
270
+ onChange: (e) => { this.cvvValid = e.isValid; },
271
+ },
272
+ }
71
273
  ```
72
274
 
73
- ```javascript
74
- // Save a new card
75
- const newCard = await liteCheckout.saveCustomerCard(cardData);
275
+ ---
276
+
277
+ ## 4. Initialization Sequence
278
+
279
+ The SDK must be initialized in this exact order before any other method is called:
280
+
281
+ ```
282
+ 1. new LiteCheckout(options)
283
+
284
+ 2. fetch baseUrl/api/secure-token/ → { access: string }
285
+
286
+ 3. configureCheckout({ customer, secureToken, ...optional })
287
+
288
+ 4. await injectCheckout() (must be awaited)
289
+
290
+ 5. result = await verify3dsTransaction() (redirectOnComplete: true only — void on normal loads)
291
+ ↓ if result → handle and return early; if void → continue ↓
292
+ 6. await mountCardFields(...) (mounts secure iframes into your divs)
76
293
  ```
77
294
 
78
- ```javascript
79
- // Remove a saved card
80
- await liteCheckout.removeCustomerCard(cardId);
295
+ **Base URL by environment:**
296
+
297
+ | `mode` | Base URL |
298
+ |--------|---------|
299
+ | `'stage'` | `https://stage.tonder.io` |
300
+ | `'production'` | `https://app.tonder.io` |
301
+
302
+ ---
303
+
304
+ ### Fetching the secure token
305
+
306
+ The secure token is a short-lived credential required for `configureCheckout`. Fetch it from Tonder's API using your **secret API key** in the `Authorization` header:
307
+
308
+ ```typescript
309
+ const { access } = await fetch(`${baseUrl}/api/secure-token/`, {
310
+ method: 'POST',
311
+ headers: {
312
+ 'Authorization': 'Token YOUR_SECRET_API_KEY',
313
+ 'Content-Type': 'application/json',
314
+ },
315
+ }).then(r => r.json());
316
+ ```
317
+
318
+ > **Security note:** `YOUR_SECRET_API_KEY` is a server-side credential. In production, make this request from your own backend and return only the `access` token to the frontend. Never expose your secret key in client-side code.
319
+
320
+ ---
321
+
322
+ ### `injectCheckout()`
323
+
324
+ Initializes the checkout session. Must be **awaited** before calling `mountCardFields`.
325
+
326
+ ```typescript
327
+ await liteCheckout.injectCheckout();
81
328
  ```
82
329
 
83
- ```javascript
84
- // Get available payment methods
85
- const paymentMethods = await liteCheckout.getCustomerPaymentMethods();
330
+ > **Important:** Calling `mountCardFields()` before `injectCheckout()` resolves will throw `SKYFLOW_NOT_INITIALIZED`.
331
+
332
+ ---
333
+
334
+ ### `configureCheckout(data)`
335
+
336
+ Sets the customer identity and secure token for the current session. Fields like `cart`, `currency`, `metadata`, and `order_reference` can also be passed here as defaults — any field provided again in `payment()` will override them.
337
+
338
+ ```typescript
339
+ interface IConfigureCheckout {
340
+ customer: { email: string } | ICustomer; // Required — minimum: { email }
341
+ secureToken: string; // Required — from the token fetch
342
+ cart?: { total: number | string; items: IItem[] };
343
+ currency?: string;
344
+ order_reference?: string; // your internal order ID — shown in Tonder dashboard & exports
345
+ metadata?: Record<string, any>; // reporting fields — see Section 6.1
346
+ card?: string; // skyflow_id — pre-selects a saved card for payment()
347
+ payment_method?: string; // APM identifier — pre-selects an APM for payment()
348
+ }
86
349
  ```
87
350
 
88
- ```javascript
89
- // Process a payment
90
- const paymentResponse = await liteCheckout.payment(paymentData);
351
+ ```typescript
352
+ liteCheckout.configureCheckout({
353
+ customer: { email: 'user@example.com' },
354
+ secureToken: access,
355
+ });
91
356
  ```
92
357
 
93
- ## Configuration Options
358
+ ---
94
359
 
95
- | Property | Type | Description |
96
- |:---------:|:--------:|:--------------------------------------------------------------------------------------------:|
97
- | mode | string | Environment mode. Options: 'stage', 'production', 'sandbox', 'development'. Default: 'stage' |
98
- | apiKey | string | Your API key from the Tonder Dashboard |
99
- | returnrl | string | URL where the checkout form is mounted (used for 3DS) |
100
- | callBack | function | Callback function to be invoked after the payment process ends successfully. |
360
+ ### `verify3dsTransaction()`
101
361
 
102
- ## Card On File
362
+ Only relevant when `redirectOnComplete: true` (the default). When using `redirectOnComplete: false` (iframe mode), `payment()` resolves the promise directly after the challenge completes — skip this call entirely.
103
363
 
104
- Card On File is applied automatically when enabled for your merchant account. No extra SDK configuration is required. For saved-card UIs, you must handle CVV collection based on the card data returned by `getCustomerCards()`:
364
+ When using the default mode, call this on **every page load**. If the page was loaded as a return from a 3DS redirect, it verifies the transaction and if the merchant has routing configured and the transaction was declined — **automatically retries with the next payment route**. Resolves with the final transaction result, or `void` on a normal (non-3DS) page load.
105
365
 
106
- - If a saved card has `subscription_id`, CVV is not required.
107
- - If a saved card does not have `subscription_id`, you must collect CVV and pass the card id to `payment()`, or the SDK will error.
108
- - Only call `mountCardFields()` when the selected card does not have `subscription_id`.
366
+ ```typescript
367
+ const result = await liteCheckout.verify3dsTransaction();
368
+
369
+ if (result) {
370
+ // Returning from 3DS — routing may have been applied automatically
371
+ const status = (result as any).transaction_status;
372
+ if (status === 'Success') {
373
+ // navigate to confirmation
374
+ } else {
375
+ // show error
376
+ }
377
+ return; // do not proceed to mountCardFields
378
+ }
379
+
380
+ // Normal page load — continue with checkout initialization
381
+ await liteCheckout.mountCardFields({ ... });
382
+ ```
383
+
384
+ ---
109
385
 
110
- Example (conditional CVV mount):
111
- ```ts
112
- const selectedCard = cardsResponse.cards.find(
113
- (card) => card.fields.skyflow_id === selectedCardId
114
- );
115
- const needsCvv = !selectedCard?.fields?.subscription_id;
386
+ ## 5. Collecting Card Data — `mountCardFields`
116
387
 
117
- if (needsCvv) {
118
- liteCheckout.mountCardFields({ fields: ['cvv'], card_id: selectedCardId });
388
+ Mounts secure iframes into your `<div>` containers. Card values are captured directly inside the iframe and **never pass through your application code**.
389
+
390
+ > **Prerequisite:** The container `<div>` elements must exist in the DOM before calling `mountCardFields()`.
391
+
392
+ ```typescript
393
+ interface IMountCardFieldsRequest {
394
+ fields: (CardField | { field: CardField; container_id?: string })[];
395
+ card_id?: string; // Omit for new card; provide skyflow_id for saved-card CVV
396
+ unmount_context?: 'all' | 'current' | 'create' | string; // default: 'all'
119
397
  }
398
+
399
+ type CardField =
400
+ | 'cardholder_name'
401
+ | 'card_number'
402
+ | 'expiration_month'
403
+ | 'expiration_year'
404
+ | 'cvv';
120
405
  ```
121
406
 
122
- ### Existing saved cards without subscription_id
123
- Cards saved before Card On File was enabled may not have `subscription_id`. You have three options:
124
- 1) Remove the card using `removeCustomerCard()` and let the user add it again.
125
- 2) Run a payment flow that shows the full card form; the SDK will create a subscription and update the card. Note: this can generate a new `skyflow_id`, so update any references in your app.
126
- 3) Ask Tonder support to remove a specific card or all saved cards for a user.
407
+ ---
408
+
409
+ ### 5.1 New-card form (all 5 fields)
410
+
411
+ **Default container IDs** (used when no custom `container_id` is provided):
412
+
413
+ | Field | Default Container ID |
414
+ |-------|---------------------|
415
+ | `cardholder_name` | `#collect_cardholder_name` |
416
+ | `card_number` | `#collect_card_number` |
417
+ | `expiration_month` | `#collect_expiration_month` |
418
+ | `expiration_year` | `#collect_expiration_year` |
419
+ | `cvv` | `#collect_cvv` |
420
+
421
+ **HTML:**
422
+ ```html
423
+ <div id="collect_cardholder_name"></div>
424
+ <div id="collect_card_number"></div>
425
+ <div id="collect_expiration_month"></div>
426
+ <div id="collect_expiration_year"></div>
427
+ <div id="collect_cvv"></div>
428
+ ```
429
+
430
+ **TypeScript — shorthand (string array):**
431
+ ```typescript
432
+ await liteCheckout.mountCardFields({
433
+ fields: ['cardholder_name', 'card_number', 'expiration_month', 'expiration_year', 'cvv'],
434
+ });
435
+ ```
436
+
437
+ **TypeScript — custom container IDs:**
438
+ ```typescript
439
+ await liteCheckout.mountCardFields({
440
+ fields: [
441
+ { field: 'cardholder_name', container_id: '#my-name' },
442
+ { field: 'card_number', container_id: '#my-card-number' },
443
+ { field: 'expiration_month', container_id: '#my-month' },
444
+ { field: 'expiration_year', container_id: '#my-year' },
445
+ { field: 'cvv', container_id: '#my-cvv' },
446
+ ],
447
+ });
448
+ ```
449
+
450
+ ---
451
+
452
+ ### 5.2 Saved-card CVV only
127
453
 
128
- ## Mobile settings
454
+ For saved-card payments, mount only the CVV field for the selected card. The default container ID is `#collect_cvv_<skyflow_id>`.
129
455
 
130
- <font size="3">If you are deploying to Android, edit your AndroidManifest.xml file to add the Internet permission.</font>
456
+ ```html
457
+ <!-- Use the card's skyflow_id as part of the container id -->
458
+ <div id="collect_cvv_abc123"></div>
459
+ ```
131
460
 
132
- ```xml
133
- <!-- Required to fetch data from the internet. -->
134
- <uses-permission android:name="android.permission.INTERNET" />
461
+ ```typescript
462
+ liteCheckout.mountCardFields({
463
+ fields: ['cvv'],
464
+ card_id: 'abc123', // card.fields.skyflow_id
465
+ });
135
466
  ```
136
467
 
137
- <font size="3">Likewise, if you are deploying to macOS, edit your macos/Runner/DebugProfile.entitlements and macos/Runner/Release.entitlements files to include the network client entitlement.</font>
468
+ > **Note:** Cards with `subscription_id` do not require CVV entry. See [Section 8.4](#84-card-on-file-subscription_id).
138
469
 
139
- ```xml
140
- <!-- Required to fetch data from the internet. -->
141
- <key>com.apple.security.network.client</key>
142
- <true>
470
+ **Conditional CVV mount pattern:**
471
+ ```typescript
472
+ handleSelectCard(card: ICard) {
473
+ if (this.selectedCard?.fields?.skyflow_id === card.fields.skyflow_id) return;
474
+ this.selectedCard = card;
475
+
476
+ // Only mount CVV for cards that don't have a Card On File subscription
477
+ if (!card.fields.subscription_id) {
478
+ this.liteCheckout.mountCardFields({
479
+ fields: ['cvv'],
480
+ card_id: card.fields.skyflow_id,
481
+ });
482
+ }
483
+ }
143
484
  ```
144
485
 
145
- ## Payment Data Structure
486
+ ---
146
487
 
147
- When calling the `payment` method, use the following data structure:
488
+ ### 5.3 Unmounting fields
148
489
 
149
- ### Field Descriptions
490
+ `mountCardFields()` automatically unmounts previously mounted fields before mounting new ones — you don't need to call `unmountCardFields()` manually when switching between cards or modes.
150
491
 
151
- - **customer**: Object containing the customer's personal information to be registered in the transaction.
492
+ The `unmount_context` parameter on `mountCardFields` controls what gets cleared before the new fields are mounted:
152
493
 
153
- - **cart**: Object containing the total amount and an array of items to be registered in the Tonder order.
494
+ | `unmount_context` | What gets unmounted before mounting |
495
+ |-------------------|-------------------------------------|
496
+ | `'all'` (default) | All mounted fields across all contexts |
497
+ | `'current'` | Only the current context (new-card or the active saved-card CVV) |
498
+ | `'create'` | New-card form fields only |
499
+ | `'update:skyflow_id'` | CVV field for a specific saved card |
154
500
 
155
- - **total**: The total amount of the transaction.
156
- - **items**: An array of objects, each representing a product or service in the order.
157
- - name: name of the product
158
- - price_unit: valid float string with the price of the product
159
- - quantity: valid integer string with the quantity of this product
501
+ **The only case where you call `unmountCardFields()` directly** is when navigating away from the checkout screen without remounting:
160
502
 
161
- - **currency**: String representing the currency code for the transaction (e.g., "MXN" for Mexican Peso).
503
+ ```typescript
504
+ ngOnDestroy() {
505
+ this.liteCheckout.unmountCardFields();
506
+ }
507
+ ```
162
508
 
163
- - **metadata**: Object for including any additional information about the transaction. This can be used for internal references or tracking.
509
+ > **Important notes:**
510
+ > 1. Never show the new-card form (all 5 fields) and a saved-card CVV field simultaneously.
511
+ > 2. Only one saved-card CVV input should be active at a time.
512
+ > 3. Always `mountCardFields()` and let the user fill in the fields **before** calling `payment()` or `saveCustomerCard()`.
164
513
 
165
- - **card**: (for LiteCheckout) Object containing card information. This is used differently depending on whether it's a new card or a saved card:
514
+ ---
166
515
 
167
- - For a new card: Include `card_number`, `cvv`, `expiration_month`, `expiration_year`, and `cardholder_name`.
168
- - For a saved card: Include only the `skyflow_id` of the saved card.
169
- - This is only used when not paying with a payment_method.
516
+ ## 6. Processing Payments
170
517
 
171
- - **payment_method**: (for LiteCheckout) String indicating the alternative payment method to be used (e.g., "Spei"). This is only used when not paying with a card.
172
- - **order_reference**: Unique order reference from the merchant. Used to visually identify/filter the order in dashboard.
173
- - **apm_config**: (Optional) Configuration object for APM-specific options. Only applicable when using alternative payment methods like Mercado Pago.
174
- <details>
175
- <summary>APM Config Fields - Mercado Pago</summary>
176
-
177
- | **Field** | **Type** | **Description** |
178
- |-------------------------------------|--------------------------------------------|---------------------------------------------------------------------------|
179
- | `binary_mode` | `boolean` | If `true`, payment must be approved or rejected immediately (no pending). |
180
- | `additional_info` | `string` | Extra info shown during checkout and in payment details. |
181
- | `back_urls` | `object` | URLs to redirect the user after payment. |
182
- | └─ `success` | `string` | Redirect URL after successful payment. |
183
- | └─ `pending` | `string` | Redirect URL after pending payment. |
184
- | └─ `failure` | `string` | Redirect URL after failed/canceled payment. |
185
- | `auto_return` | `"approved"` \| `"all"` | Enables auto redirection after payment completion. |
186
- | `payment_methods` | `object` | Payment method restrictions and preferences. |
187
- | └─ `excluded_payment_methods[]` | `array` | List of payment methods to exclude. |
188
- | └─ `excluded_payment_methods[].id` | `string` | ID of payment method to exclude (e.g., "visa"). |
189
- | └─ `excluded_payment_types[]` | `array` | List of payment types to exclude. |
190
- | └─ `excluded_payment_types[].id` | `string` | ID of payment type to exclude (e.g., "ticket"). |
191
- | └─ `default_payment_method_id` | `string` | Default payment method (e.g., "master"). |
192
- | └─ `installments` | `number` | Max number of installments allowed. |
193
- | └─ `default_installments` | `number` | Default number of installments suggested. |
194
- | `expires` | `boolean` | Whether the preference has expiration. |
195
- | `expiration_date_from` | `string` (ISO 8601) | Start of validity period (e.g. `"2025-01-01T12:00:00-05:00"`). |
196
- | `expiration_date_to` | `string` (ISO 8601) | End of validity period. |
197
- | `differential_pricing` | `object` | Configuration for differential pricing. |
198
- | └─ `id` | `number` | ID of the differential pricing strategy. |
199
- | `marketplace` | `string` | Marketplace identifier (default: "NONE"). |
200
- | `marketplace_fee` | `number` | Fee to collect as marketplace commission. |
201
- | `tracks[]` | `array` | Ad tracking configurations. |
202
- | └─ `type` | `"google_ad"` \| `"facebook_ad"` | Type of tracker. |
203
- | └─ `values.conversion_id` | `string` | Google Ads conversion ID. |
204
- | └─ `values.conversion_label` | `string` | Google Ads label. |
205
- | └─ `values.pixel_id` | `string` | Facebook Pixel ID. |
206
- | `statement_descriptor` | `string` | Text on payer’s card statement (max 16 characters). |
207
- | `shipments` | `object` | Shipping configuration. |
208
- | └─ `mode` | `"custom"` \| `"me2"` \| `"not_specified"` | Type of shipping mode. |
209
- | └─ `local_pickup` | `boolean` | Enable pickup at local branch (for `me2`). |
210
- | └─ `dimensions` | `string` | Package dimensions (e.g. `10x10x10,500`). |
211
- | └─ `default_shipping_method` | `number` | Default shipping method (for `me2`). |
212
- | └─ `free_methods[]` | `array` | Shipping methods offered for free (for `me2`). |
213
- | └─ `free_methods[].id` | `number` | ID of free shipping method. |
214
- | └─ `cost` | `number` | Shipping cost (only for `custom` mode). |
215
- | └─ `free_shipping` | `boolean` | If `true`, shipping is free (`custom` only). |
216
- | └─ `receiver_address` | `object` | Shipping address. |
217
- | └─ `receiver_address.zip_code` | `string` | ZIP or postal code. |
218
- | └─ `receiver_address.street_name` | `string` | Street name. |
219
- | └─ `receiver_address.street_number` | `number` | Street number. |
220
- | └─ `receiver_address.city_name` | `string` | City name. |
221
- | └─ `receiver_address.state_name` | `string` | State name. |
222
- | └─ `receiver_address.country_name` | `string` | Country name. |
223
- | └─ `receiver_address.floor` | `string` | Floor (optional). |
224
- | └─ `receiver_address.apartment` | `string` | Apartment or unit (optional). |
225
- </details>
518
+ ### 6.1 New card payment
226
519
 
227
- ```javascript
228
- const paymentData = {
229
- customer: {
230
- firstName: "John",
231
- lastName: "Doe",
232
- country: "USA",
233
- address: "123 Main St",
234
- city: "Anytown",
235
- state: "CA",
236
- postCode: "12345",
237
- email: "john.doe@example.com",
238
- phone: "1234567890",
239
- identification:{
240
- type: "CPF",
241
- number: "19119119100"
242
- }
243
- },
244
- cart: {
245
- total: "100.00",
246
- items: [
247
- {
248
- description: "Product description",
249
- quantity: 1,
250
- price_unit: "100.00",
251
- discount: "0.00",
252
- taxes: "0.00",
253
- product_reference: "PROD123",
254
- name: "Product Name",
255
- amount_total: "100.00",
256
- },
257
- ],
258
- },
259
- currency: "MXN",
260
- metadata: {
261
- order_id: "ORDER123",
262
- },
263
- // For a new card:
264
- card: {
265
- card_number: "4111111111111111",
266
- cvv: "123",
267
- expiration_month: "12",
268
- expiration_year: "25",
269
- cardholder_name: "John Doe",
270
- },
271
- // card: "skyflow_id" // for a selected saved card.
272
- // payment_method: "Spei", // For the selected payment method.
273
- // apm_config: {} // Optional, only for APMs like Mercado Pago, Oxxo Pay
520
+ **Prerequisites:** [Section 5.1](#51-new-card-form-all-5-fields) — all 5 card fields must be mounted and filled by the user.
521
+
522
+ ```typescript
523
+ interface IProcessPaymentRequest {
524
+ customer: ICustomer | { email: string }; // Required
525
+ cart: { total: string | number; items: IItem[] }; // Required
526
+ currency?: string; // Optional — ISO code e.g. 'MXN'
527
+ order_reference?: string | null; // Recommended — your internal order ID; shown in Tonder dashboard, filters, and exports
528
+ metadata?: Record<string, any>; // Recommended — fields shown in Tonder transaction exports (see table below)
529
+ isSandbox?: boolean; // Optional — Openpay sandbox mode
530
+ apm_config?: Record<string, any>; // Optional — APM-specific config (Mercado Pago, etc.)
531
+ // card — OMIT for new-card payment
532
+ // payment_method — OMIT for card payment
533
+ }
534
+ ```
535
+
536
+ #### Metadata for Reporting
537
+
538
+ To ensure proper visibility in Tonder's **transaction and dispute reports**, pass the following fields inside `metadata`. They are included in exported reports and help link transactions to customer activity, business users, and external systems.
539
+
540
+ | Field | Type | Report column | Description |
541
+ |-------|------|---------------|-------------|
542
+ | `order_reference` | `string` | Business Transaction ID | Merchant's internal order ID. Shown in Tonder dashboard filters and exports. Recommended on every payment. |
543
+ | `metadata.order_id` | `string` | Business Transaction ID | Takes precedence over `order_reference` when both are provided. |
544
+ | `metadata.operation_date` | `Date \| string` | Customer ID (metadata) | Business operation date for reporting and reconciliation. |
545
+ | `metadata.customer_email` | `string` | Customer Email | Overrides the email shown in reports. Falls back to `customer.email` if omitted. |
546
+ | `metadata.business_user` | `string` | Business User (metadata) | Internal user or system that initiated the payment (e.g. POS terminal ID, cashier ID). |
547
+ | `metadata.customer_id` | `string` | Customer ID (metadata) | Your internal customer identifier — correlates payments with customer records. |
548
+
549
+ > **Tip:** At minimum, pass `order_reference` on every payment so your orders appear correctly in Tonder's dashboard and exports.
550
+
551
+ **`ICustomer`:**
552
+ ```typescript
553
+ type ICustomer = {
554
+ firstName: string; // Required
555
+ lastName: string; // Required
556
+ email: string; // Required
557
+ phone?: string;
558
+ country?: string;
559
+ street?: string;
560
+ city?: string;
561
+ state?: string;
562
+ postCode?: string;
563
+ address?: string;
564
+ identification?: { type: string; number: string };
274
565
  };
275
566
  ```
276
567
 
277
- ## Field Validation Functions
278
-
279
- For LiteCheckout implementations, the SDK provides validation functions to ensure the integrity of card data before submitting:
280
-
281
- - `validateCardNumber(cardNumber)`: Validates the card number using the Luhn algorithm.
282
- - `validateCardholderName(name)`: Checks if the cardholder name is valid.
283
- - `validateCVV(cvv)`: Ensures the CVV is in the correct format.
284
- - `validateExpirationDate(expirationDate)`: Validates the expiration date in MM/YY format.
285
- - `validateExpirationMonth(month)`: Checks if the expiration month is valid.
286
- - `validateExpirationYear(year)`: Validates the expiration year.
287
-
288
- Example usage:
289
-
290
- ```javascript
291
- import {
292
- validateCardNumber,
293
- validateCardholderName,
294
- validateCVV,
295
- validateExpirationDate,
296
- } from "@tonder.io/ionic-lite-sdk";
297
-
298
- const cardNumber = "4111111111111111";
299
- const cardholderName = "John Doe";
300
- const cvv = "123";
301
- const expirationDate = "12/25";
302
-
303
- if (
304
- validateCardNumber(cardNumber) &&
305
- validateCardholderName(cardholderName) &&
306
- validateCVV(cvv) &&
307
- validateExpirationDate(expirationDate)
308
- ) {
309
- // Proceed with payment
310
- } else {
311
- // Show error message
568
+ **`IItem`:**
569
+ ```typescript
570
+ interface IItem {
571
+ name: string;
572
+ description: string;
573
+ quantity: number;
574
+ price_unit: number;
575
+ amount_total: number;
576
+ discount: number;
577
+ taxes: number;
578
+ product_reference: string | number;
312
579
  }
313
580
  ```
314
581
 
582
+ **Example:**
583
+ ```typescript
584
+ async pay() {
585
+ this.loading = true;
586
+ try {
587
+ const response = await this.liteCheckout.payment({
588
+ customer: {
589
+ firstName: 'John',
590
+ lastName: 'Doe',
591
+ email: 'john@example.com',
592
+ phone: '+1 555 0100',
593
+ country: 'MX',
594
+ city: 'CDMX',
595
+ street: '123 Main St',
596
+ state: 'CMX',
597
+ postCode: '06600',
598
+ },
599
+ cart: {
600
+ total: 150,
601
+ items: [{
602
+ name: 'Product A',
603
+ description: 'Product description',
604
+ quantity: 1,
605
+ price_unit: 150,
606
+ discount: 0,
607
+ taxes: 0,
608
+ product_reference: 'SKU-001',
609
+ amount_total: 150,
610
+ }],
611
+ },
612
+ currency: 'MXN',
613
+ metadata: { order_id: 'ORD-789' },
614
+ order_reference: 'ORD-789',
615
+ });
616
+ console.log('Transaction status:', response.transaction_status);
617
+ } catch (error) {
618
+ if (error instanceof AppError) {
619
+ console.error(error.code, error.message);
620
+ this.errorMessage = error.message;
621
+ }
622
+ } finally {
623
+ this.loading = false;
624
+ }
625
+ }
626
+ ```
315
627
 
316
- ## API Reference
628
+ ---
317
629
 
318
- ### LiteCheckout Methods
630
+ ### 6.2 Saved card payment
319
631
 
320
- - `configureCheckout(data)`: Set initial checkout data
321
- - `injectCheckout()`: Initialize the checkout
322
- - `getCustomerCards()`: Retrieve saved cards
323
- - `saveCustomerCard(cardData)`: Save a new card
324
- - `removeCustomerCard(cardId)`: Remove a saved card
325
- - `getCustomerPaymentMethods()`: Get available payment methods
326
- - `payment(data)`: Process a payment
327
- - `verify3dsTransaction()`: Verify a 3DS transaction
328
- - `mountCardFields({ fields, card_id })`: Mounts card input fields (e.g., CVV) for a saved card. Useful for requesting CVV when listing saved cards.
632
+ **Prerequisites:** Fetch saved cards ([Section 8.1](#81-list-saved-cards)). Conditionally mount the CVV field ([Section 5.2](#52-saved-card-cvv-only)).
329
633
 
330
- #### mountCardFields
634
+ ```typescript
635
+ const response = await this.liteCheckout.payment({
636
+ customer: { email: 'user@example.com' },
637
+ cart: { total: 100, items: [...] },
638
+ currency: 'MXN',
639
+ card: selectedCard.fields.skyflow_id, // the only addition vs. new card
640
+ });
641
+ ```
331
642
 
332
- Mounts card input fields (currently CVV) for a saved card. When a `card_id` is provided, the CVV input will be associated with that specific card, allowing you to update its CVV. This is useful for workflows where you need to request CVV for saved cards before payment.
643
+ ---
333
644
 
334
- **Parameters:**
645
+ ### 6.3 Alternative Payment Method (APM)
335
646
 
336
- | Name | Type | Required | Description |
337
- |---------|----------|----------|---------------------------------------------------------------------|
338
- | fields | string[] | Yes | Array of fields to mount (currently supports `["cvv"]`). |
339
- | card_id | string | No | Card ID of the saved card. Associates the CVV input with this card. |
647
+ No `mountCardFields()` call is needed for APM payments.
340
648
 
341
- **Important Notes:**
342
- 1. **Single Card Selection Only:** The CVV input for a saved card must only be displayed when a specific card is selected.
343
- 2. **One CVV Input at a Time:** You cannot display multiple CVV inputs for different cards simultaneously. Only one CVV update operation should be active at any given time.
344
- 3. **Mutually Exclusive with Card Form:** The CVV input for a saved card cannot be shown at the same time as the full card enrollment form. These are two separate workflows:
345
- - **Save New Card:** Use the complete card form without `card_id`.
346
- - **Update CVV for Saved Card:** Use CVV input with `card_id` only.
649
+ ```typescript
650
+ // 1. Fetch available APMs
651
+ const apms = await this.liteCheckout.getCustomerPaymentMethods();
652
+ // IPaymentMethod = { id, payment_method, priority, category, icon, label }
653
+
654
+ // 2. User selects an APM
655
+
656
+ // 3. Pay
657
+ const response = await this.liteCheckout.payment({
658
+ customer: { email: 'user@example.com' },
659
+ cart: { total: 100, items: [...] },
660
+ currency: 'MXN',
661
+ payment_method: selectedApm.payment_method, // e.g. 'Spei'
662
+ });
663
+ ```
347
664
 
348
- **Example:**
349
- ```tsx
350
- // Update CVV for a saved card
665
+ **Mercado Pago — `apm_config`:**
351
666
 
352
- // 1. Place the div in your component where the CVV field will be mounted
353
- <div id={`collect_cvv_saved-card-id`}></div>
667
+ Pass Mercado Pago-specific preferences via `apm_config`:
354
668
 
355
- // 2. Call mountCardFields and pass the card_id of the selected card
356
- liteCheckout.mountCardFields({ fields: ["cvv"], card_id: "saved-card-id" });
669
+ ```typescript
670
+ const response = await this.liteCheckout.payment({
671
+ ...paymentData,
672
+ payment_method: 'MercadoPago',
673
+ apm_config: {
674
+ back_urls: {
675
+ success: 'https://myapp.com/success',
676
+ pending: 'https://myapp.com/pending',
677
+ failure: 'https://myapp.com/failure',
678
+ },
679
+ auto_return: 'approved',
680
+ },
681
+ });
682
+ ```
357
683
 
684
+ <details>
685
+ <summary>Full Mercado Pago <code>apm_config</code> fields</summary>
686
+
687
+ | Field | Type | Description |
688
+ |-------|------|-------------|
689
+ | `binary_mode` | `boolean` | If `true`, payment must be approved or rejected immediately (no pending state) |
690
+ | `additional_info` | `string` | Extra info shown during checkout |
691
+ | `back_urls.success` | `string` | Redirect URL after successful payment |
692
+ | `back_urls.pending` | `string` | Redirect URL after pending payment |
693
+ | `back_urls.failure` | `string` | Redirect URL after failed/canceled payment |
694
+ | `auto_return` | `'approved' \| 'all'` | Enable auto-redirect after payment completion |
695
+ | `payment_methods.excluded_payment_methods[].id` | `string` | Payment method to exclude (e.g. `'visa'`) |
696
+ | `payment_methods.excluded_payment_types[].id` | `string` | Payment type to exclude (e.g. `'ticket'`) |
697
+ | `payment_methods.default_payment_method_id` | `string` | Default payment method (e.g. `'master'`) |
698
+ | `payment_methods.installments` | `number` | Max installments allowed |
699
+ | `payment_methods.default_installments` | `number` | Default installments suggested |
700
+ | `expires` | `boolean` | Whether the preference has an expiration |
701
+ | `expiration_date_from` | `string` (ISO 8601) | Start of validity period |
702
+ | `expiration_date_to` | `string` (ISO 8601) | End of validity period |
703
+ | `statement_descriptor` | `string` | Text on payer's card statement (max 16 chars) |
704
+ | `marketplace` | `string` | Marketplace identifier (default: `'NONE'`) |
705
+ | `marketplace_fee` | `number` | Fee to collect as marketplace commission |
706
+ | `differential_pricing.id` | `number` | Differential pricing strategy ID |
707
+ | `shipments.mode` | `'custom' \| 'me2' \| 'not_specified'` | Shipping mode |
708
+ | `shipments.local_pickup` | `boolean` | Enable local branch pickup |
709
+ | `shipments.cost` | `number` | Shipping cost (custom mode only) |
710
+ | `shipments.free_shipping` | `boolean` | Free shipping flag (custom mode only) |
711
+ | `tracks[].type` | `'google_ad' \| 'facebook_ad'` | Ad tracker type |
712
+ | `tracks[].values.conversion_id` | `string` | Google Ads conversion ID |
713
+ | `tracks[].values.pixel_id` | `string` | Facebook Pixel ID |
358
714
 
715
+ </details>
716
+
717
+ ---
718
+
719
+ ### 6.4 Payment response reference
720
+
721
+ ```typescript
722
+ interface IStartCheckoutResponse {
723
+ status: string;
724
+ message: string;
725
+ transaction_status: string; // 'Success' | 'Pending' | 'Declined' | 'Failed'
726
+ transaction_id: number;
727
+ payment_id: number;
728
+ checkout_id: string;
729
+ is_route_finished: boolean;
730
+ provider: string;
731
+ psp_response: Record<string, any>; // Raw response from the payment processor
732
+ }
359
733
  ```
360
734
 
361
- ## Error Handling
735
+ > **Note:** 3DS authentication is handled automatically by the SDK. You do not need to handle redirection yourself.
362
736
 
363
- Public SDK methods that fail due to API/SDK execution return an `AppError` (with `name: "TonderError"`).
737
+ ---
364
738
 
365
- ### Error structure
739
+ ## 7. 3DS Handling
366
740
 
367
- ```json
368
- {
369
- "status": "error",
370
- "name": "TonderError",
371
- "code": "PAYMENT_PROCESS_ERROR",
372
- "message": "There was an issue processing the payment.",
373
- "statusCode": 500,
374
- "details": {
375
- "code": "PAYMENT_PROCESS_ERROR",
376
- "statusCode": 500,
377
- "systemError": "APP_INTERNAL_001"
378
- }
741
+ When a payment requires 3DS authentication, the SDK handles the challenge automatically. There are two display modes, controlled by [`redirectOnComplete`](#32-customization-options):
742
+
743
+ | `redirectOnComplete` | Challenge display | User experience |
744
+ |----------------------|-------------------|-----------------|
745
+ | `true` (default) | Full-page redirect to bank's 3DS page | User leaves the app; returns to `returnUrl` after completing auth |
746
+ | `false` | Rendered inside `#tdsIframe` in your app | User stays in app until the challenge resolves |
747
+
748
+ ### `redirectOnComplete: true` (default — full-page redirect)
749
+
750
+ No additional template changes required. Set `returnUrl` in the constructor and call `verify3dsTransaction()` on every page load — see [Section 4 — `verify3dsTransaction()`](#verify3dstransaction) for the full implementation pattern.
751
+
752
+ **How it works:** `payment()` redirects the browser to the bank's authentication page. After authentication, the bank sends the user back to `returnUrl`. On that page load, `verify3dsTransaction()` completes the verification — if routing is configured and the transaction was declined, it automatically retries with the next route.
753
+
754
+ ---
755
+
756
+ ### `redirectOnComplete: false` (iframe mode — user stays in app)
757
+
758
+ Recommended for **Ionic / mobile WebViews** where a full-page redirect would break the app flow.
759
+
760
+ **1. Add the iframe to your template:**
761
+ ```html
762
+ <iframe id="tdsIframe" allowtransparency="true" class="tds-iframe"></iframe>
763
+ ```
764
+
765
+ **2. Add CSS — hidden by default, shown full-screen when the challenge activates:**
766
+ ```css
767
+ .tds-iframe {
768
+ display: none;
769
+ border: none;
770
+ position: fixed;
771
+ inset: 0;
772
+ width: 100vw;
773
+ height: 100vh;
774
+ z-index: 100;
775
+ background: white;
379
776
  }
380
777
  ```
381
778
 
382
- Notes:
383
- - `statusCode` comes from HTTP response when available; otherwise defaults to `500`.
384
- - `details.systemError` comes from backend error code when available; otherwise defaults to `APP_INTERNAL_001`.
385
- - In card-on-file flow failures, the SDK returns `CARD_ON_FILE_DECLINED`.
779
+ In this mode the SDK resolves the original `payment()` promise directly when the challenge completes — `verify3dsTransaction()` is not needed.
386
780
 
387
- ### Public method error mapping
781
+ ---
388
782
 
389
- | Method | Returned `error.code` |
390
- |---|---|
391
- | `payment(data)` | `PAYMENT_PROCESS_ERROR` or `CARD_ON_FILE_DECLINED` |
392
- | `getCustomerCards()` | `FETCH_CARDS_ERROR` |
393
- | `saveCustomerCard(cardData)` | `SAVE_CARD_ERROR` or `CARD_ON_FILE_DECLINED` |
394
- | `removeCustomerCard(cardId)` | `REMOVE_CARD_ERROR` |
395
- | `getCustomerPaymentMethods()` | `FETCH_PAYMENT_METHODS_ERROR` |
783
+ ## 8. Managing Saved Cards
396
784
 
785
+ ### 8.1 List saved cards
397
786
 
398
- ## Examples
787
+ ```typescript
788
+ getCustomerCards(): Promise<ICustomerCardsResponse>
789
+ ```
399
790
 
400
- Here are examples of how to implement Tonder Lite SDK:
791
+ ```typescript
792
+ interface ICustomerCardsResponse {
793
+ user_id: number;
794
+ cards: ICard[];
795
+ }
401
796
 
402
- ### Angular
797
+ interface ICard {
798
+ fields: ICardSkyflowFields;
799
+ icon?: string; // URL to card brand image
800
+ }
403
801
 
404
- For Angular, we recommend using a service to manage the Tonder instance:
802
+ interface ICardSkyflowFields {
803
+ skyflow_id: string;
804
+ card_number: string; // Masked — e.g. 'XXXX-XXXX-XXXX-4242'
805
+ cardholder_name: string;
806
+ expiration_month: string; // e.g. '12'
807
+ expiration_year: string; // e.g. '25'
808
+ card_scheme: string; // e.g. 'VISA', 'MASTERCARD'
809
+ subscription_id?: string; // Present when Card On File is activated on your merchant account — see Section 8.4
810
+ }
811
+ ```
405
812
 
813
+ **Example:**
406
814
  ```typescript
407
- // tonder.service.ts
408
- import { Injectable } from "@angular/core";
409
- import { LiteCheckout } from "@tonder.io/ionic-lite-sdk";
410
- import {ILiteCheckout} from "@tonder.io/ionic-lite-sdk/dist/types/liteInlineCheckout";
815
+ const { cards } = await this.liteCheckout.getCustomerCards();
411
816
 
412
- @Injectable({
413
- providedIn: "root",
414
- })
415
- export class TonderService {
416
- private liteCheckout!: ILiteCheckout;
817
+ cards.forEach((card) => {
818
+ const lastFour = card.fields.card_number.slice(-4);
819
+ const expiry = `${card.fields.expiration_month}/${card.fields.expiration_year}`;
820
+ console.log(`${card.fields.card_scheme} •••• ${lastFour} — expires ${expiry}`);
821
+ });
822
+ ```
417
823
 
418
- constructor(@Inject(Object) private sdkParameters: IInlineLiteCheckoutOptions) {
419
- this.initializeInlineCheckout();
420
- }
824
+ ---
421
825
 
422
- private initializeInlineCheckout(): void {
423
- this.liteCheckout = new LiteCheckout({ ...this.sdkParameters });
424
- }
826
+ ### 8.2 Save a new card
425
827
 
426
- configureCheckout(customerData: IConfigureCheckout): void {
427
- return this.liteCheckout.configureCheckout({ ...customerData });
428
- }
828
+ **Prerequisites:** All 5 card fields must be mounted via `mountCardFields()` with no `card_id`, and the user must have filled them in.
429
829
 
430
- async injectCheckout(): Promise<void> {
431
- return await this.liteCheckout.injectCheckout();
432
- }
830
+ ```typescript
831
+ saveCustomerCard(): Promise<ISaveCardResponse>
832
+ // Returns: { skyflow_id: string; user_id: number }
833
+ ```
433
834
 
434
- verify3dsTransaction(): Promise<ITransaction | IStartCheckoutResponse | void> {
435
- return this.liteCheckout.verify3dsTransaction();
436
- }
835
+ **Complete enrollment flow:**
836
+ ```typescript
837
+ // 1. Mount all 5 fields (see Section 5.1)
838
+ await this.liteCheckout.mountCardFields({
839
+ fields: ['cardholder_name', 'card_number', 'expiration_month', 'expiration_year', 'cvv'],
840
+ });
437
841
 
438
- payment(
439
- checkoutData: IProcessPaymentRequest,
440
- ): Promise<IStartCheckoutResponse> {
441
- return this.inlineCheckout.payment(checkoutData);
442
- }
842
+ // 2. User fills in the card form...
443
843
 
444
- // Add more functions, for example for lite sdk: get payment methods
844
+ // 3. Save the card
845
+ const saved = await this.liteCheckout.saveCustomerCard();
846
+ console.log('Saved card skyflow_id:', saved.skyflow_id);
445
847
 
446
- // getCustomerPaymentMethods(): Promise<IPaymentMethod[]> {
447
- // return this.liteCheckout.getCustomerPaymentMethods();
448
- // }
449
- }
848
+ // 4. Optionally reveal the saved card data (see Section 9)
849
+ await this.liteCheckout.revealCardFields({
850
+ fields: ['card_number', 'cardholder_name', 'expiration_month', 'expiration_year'],
851
+ });
852
+ ```
450
853
 
451
- // checkout.component.ts
452
- import { Component, OnInit, OnDestroy } from "@angular/core";
453
- import { TonderService } from "./tonder.service";
454
-
455
- @Component({
456
- selector: "app-tonder-checkout",
457
- template: `
458
- <div id="container">
459
- <form [formGroup]="paymentForm">
460
- <div class="lite-container-tonder">
461
- <div id="id-name" class="empty-div">
462
- <label for="name">Namess: </label>
463
- <input id="name" type="text" formControlName="name">
464
- </div>
465
- <div id="id-cardNumber" class="empty-div">
466
- <label for="cardNumber">Card number: </label>
467
- <input id="cardNumber" type="text" formControlName="cardNumber">
468
- </div>
469
- <div class="collect-row">
470
- <div class="empty-div">
471
- <label for="month">Month: </label>
472
- <input id="month" type="text" formControlName="month">
473
- </div>
474
- <div class="expiration-year">
475
- <label for="expirationYear">Year: </label>
476
- <input id="expirationYear" type="text" formControlName="expirationYear">
477
- </div>
478
- <div class="empty-div">
479
- <label for="cvv">CVV: </label>
480
- <input id="cvv" type="text" formControlName="cvv">
481
- </div>
482
- </div>
483
- <div id="msgError">{{ errorMessage }}</div>
484
- <div id="msgNotification"></div>
485
- <div class="container-pay-button">
486
- <button class="lite-pay-button" (click)="onPayment($event)">Pay</button>
487
- </div>
488
- </div>
489
-
490
- </form>
491
- </div>
492
- `,
493
- providers: [
494
- {
495
- provide: TonderInlineService,
496
- // Initialization of the Tonder Lite SDK.
497
- // Note: Replace these credentials with your own in development/production.
498
- useFactory: () =>
499
- new TonderInlineService({
500
- apiKey: "11e3d3c3e95e0eaabbcae61ebad34ee5f93c3d27",
501
- returnUrl: "http://localhost:8100/tabs/tab5",
502
- mode: "stage",
503
- }),
504
- },
505
- ],
506
- })
507
- export class TonderCheckoutComponent implements OnInit, OnDestroy {
508
- loading = false;
509
- checkoutData: IProcessPaymentRequest;
510
- paymentForm = new FormGroup({
511
- name: new FormControl('Pedro Paramo'),
512
- cardNumber: new FormControl('4242424242424242'),
513
- month: new FormControl('12'),
514
- expirationYear: new FormControl('28'),
515
- cvv: new FormControl('123')
516
- });
517
-
518
- constructor(private tonderService: TonderService) {
519
- this.checkoutData = {
520
- customer: {
521
- firstName: "Jhon",
522
- lastName: "Doe",
523
- email: "john.c.calhoun@examplepetstore.com",
524
- phone: "+58452258525"
525
- },
526
- cart: {
527
- total: 25,
528
- items: [
529
- {
530
- description: "Test product description",
531
- quantity: 1,
532
- price_unit: 25,
533
- discount: 1,
534
- taxes: 12,
535
- product_reference: 89456123,
536
- name: "Test product",
537
- amount_total: 25
538
- }
539
- ]
540
- },
541
- metadata: {},
542
- currency: "MXN"
543
- }
544
- }
854
+ ---
545
855
 
546
- ngOnInit() {
547
- this.initCheckout();
548
- }
856
+ ### 8.3 Remove a card
549
857
 
550
- async initCheckout() {
551
- this.tonderService.configureCheckout({
552
- customer: { email: "example@email.com" },
553
- });
554
- await this.tonderService.injectCheckout();
555
- this.tonderService.verify3dsTransaction().then((response) => {
556
- console.log("Verify 3ds response", response);
557
- });
558
-
559
- // Calls more functions to get payment methods, saved cards, etc.
560
- }
858
+ ```typescript
859
+ removeCustomerCard(skyflowId: string): Promise<string>
860
+ // skyflowId = card.fields.skyflow_id from getCustomerCards()
861
+ ```
561
862
 
562
- async pay() {
563
- this.loading = true;
564
- try {
565
- const response = await this.tonderService.payment({
566
- ...this.checkoutData,
567
- card: { // Card details, if not using a payment method.
568
- card_number: this.paymentForm.value.cardNumber || "",
569
- cvv: this.paymentForm.value.cvv || "",
570
- expiration_month: this.paymentForm.value.month || "",
571
- expiration_year: this.paymentForm.value.expirationYear || "",
572
- cardholder_name: this.paymentForm.value.name || ""
573
- },
574
- // card: "skyflow_id" // In case a saved card is selected.
575
- // payment_method: "" // Payment method if not using the card form
576
- });
577
- console.log("Payment successful:", response);
578
- alert("Payment successful");
579
- } catch (error) {
580
- console.error("Payment failed:", error);
581
- alert("Payment failed");
582
- } finally {
583
- this.loading = false;
584
- }
585
- }
863
+ ```typescript
864
+ await this.liteCheckout.removeCustomerCard(card.fields.skyflow_id);
865
+ // Refresh the card list after removal
866
+ const { cards } = await this.liteCheckout.getCustomerCards();
867
+ ```
868
+
869
+ ---
870
+
871
+ ### 8.4 Card On File (`subscription_id`)
872
+
873
+ Card On File is a feature that Tonder activates on your merchant account. When active, saved cards receive a `subscription_id` — these cards do **not** require CVV entry on subsequent payments.
874
+
875
+ **Conditional CVV rule:**
876
+
877
+ | `subscription_id` present | CVV required? | Action |
878
+ |--------------------------|--------------|--------|
879
+ | Yes | No | Call `payment()` directly with `card: skyflow_id` |
880
+ | No | Yes | Mount CVV with `mountCardFields({ fields: ['cvv'], card_id })` first |
881
+
882
+ ```typescript
883
+ const selectedCard = cards.find(c => c.fields.skyflow_id === selectedId);
884
+
885
+ if (!selectedCard.fields.subscription_id) {
886
+ // Need CVV — mount the field
887
+ await this.liteCheckout.mountCardFields({
888
+ fields: ['cvv'],
889
+ card_id: selectedCard.fields.skyflow_id,
890
+ });
586
891
  }
892
+
893
+ // Then pay
894
+ await this.liteCheckout.payment({
895
+ ...customerCartData,
896
+ card: selectedCard.fields.skyflow_id,
897
+ });
587
898
  ```
588
899
 
589
- ### React: Request CVV for saved card
900
+ **Legacy cards without `subscription_id`**
590
901
 
591
- ```tsx
592
- import { LiteCheckout } from '@tonder.io/ionic-lite-sdk';
593
- import { useEffect, useState } from 'react';
594
-
595
- const checkoutData = {
596
- customer: {
597
- firstName: "Adrian",
598
- lastName: "Martinez",
599
- country: "Mexico",
600
- address: "Pinos 507, Col El Tecuan",
601
- city: "Durango",
602
- state: "Durango",
603
- postCode: "34105",
604
- email: "test@example.com",
605
- phone: "8161234567",
606
- },
607
- cart: {
608
- total: 120,
609
- items: [
610
- {
611
- description: "Test product description",
612
- quantity: 1,
613
- price_unit: 120,
614
- discount: 25,
615
- taxes: 12,
616
- product_reference: 12,
617
- name: "Test product",
618
- amount_total: 120
619
- }
620
- ]
621
- },
622
- currency: "MXN",
623
- // Reference from the merchant
624
- order_reference: "ORD-123456",
625
- metadata: {
626
- business_user: "123456-test"
627
- },
628
- };
902
+ Cards saved before Card On File was enabled may not have `subscription_id`. You have three options:
629
903
 
630
- const ExploreContainer = () => {
631
- const [liteCheckout, setLiteCheckout] = useState<any>(null);
632
- const [cards, setCards] = useState<any[]>([]);
633
- const [selectedCardId, setSelectedCardId] = useState<string | null>(null);
634
- const [loading, setLoading] = useState<boolean>(false);
904
+ 1. Remove the card with `removeCustomerCard()` and let the user re-enroll.
905
+ 2. Run a full new-card payment flow — the SDK creates a subscription and updates the card. Note: this may generate a new `skyflow_id`; update any stored references in your app.
906
+ 3. Contact Tonder support to migrate specific cards.
635
907
 
636
- useEffect(() => {
637
- if (!liteCheckout) {
638
- setLoading(true);
639
- initializeTonderSDK();
640
- }
641
- }, []);
908
+ ---
642
909
 
643
- useEffect(() => {
644
- if (liteCheckout) {
645
- fetchCards();
646
- }
647
- }, [liteCheckout]);
648
-
649
- const initializeTonderSDK = async () => {
650
- const sdk = new LiteCheckout({
651
- mode: "stage",
652
- apiKey: "YOUR_API_KEY",
653
- returnUrl: window.location.href,
654
- customization: { redirectOnComplete: false },
655
- events: {
656
- cvvEvents: {
657
- onChange: (data) => {
658
- console.log("CVV onChange event data:", data);
659
- }
660
- }
661
- }
662
- });
663
- setLiteCheckout(sdk);
910
+ ## 9. Revealing Card Data — `revealCardFields`
664
911
 
665
- // Get secure token from your backend
666
- const token = "123"
912
+ After a successful `saveCustomerCard()` or `payment()` with a **new card**, use `revealCardFields()` to display the card data in your UI through secure iframes raw card values are never exposed to your application.
667
913
 
668
- sdk.configureCheckout({ ...checkoutData, secureToken: token });
669
- await sdk.injectCheckout();
670
- sdk.verify3dsTransaction().then((response: any) => {
671
- console.log('Verify 3ds response', response);
672
- });
673
- setLoading(false);
674
- };
914
+ > **When to call:** Only after a successful `saveCustomerCard()` or new-card `payment()`. Calling it without a prior successful card operation throws `MOUNT_COLLECT_ERROR`.
675
915
 
676
- const fetchCards = async () => {
677
- const response = await liteCheckout.getCustomerCards();
678
- setCards(response.cards || []);
679
- };
916
+ **Default container IDs and fixed redaction:**
680
917
 
681
- const handleSelectCard = (cardId: string) => {
682
- if (cardId === selectedCardId) return;
683
- setSelectedCardId(cardId);
684
- liteCheckout.mountCardFields({ fields: ["cvv"], card_id: cardId });
685
- };
918
+ | Field | Default Container ID | Redaction Applied |
919
+ |-------|---------------------|-------------------|
920
+ | `card_number` | `#reveal_card_number` | `MASKED` — e.g. `4111 11•• •••• 1234` |
921
+ | `cardholder_name` | `#reveal_cardholder_name` | `PLAIN_TEXT` |
922
+ | `expiration_month` | `#reveal_expiration_month` | `PLAIN_TEXT` |
923
+ | `expiration_year` | `#reveal_expiration_year` | `PLAIN_TEXT` |
686
924
 
687
- const handlePayment = async () => {
688
- if (!selectedCardId) return;
689
- try {
690
- const response = await liteCheckout.payment({ ...checkoutData, card: selectedCardId });
691
- } catch (err) {
692
- console.error("Payment error:", err);
693
- }
925
+ > **PCI note:** `cvv` cannot be revealed — PCI DSS Requirement 3.2.1 prohibits storing or displaying CVV post-authorization. Redaction levels are fixed by the SDK and **cannot be overridden**.
926
+
927
+ > **Note:** Reveal elements only support `base`, `copyIcon`, and `global` style variants (unlike Collect elements which also support `focus`, `complete`, `invalid`, etc.).
928
+
929
+ **Types:**
930
+
931
+ ```typescript
932
+ type RevealableCardField = 'card_number' | 'cardholder_name' | 'expiration_month' | 'expiration_year';
933
+
934
+ interface IRevealCardFieldsRequest {
935
+ fields: (RevealableCardField | IRevealCardField)[];
936
+ styles?: IRevealElementStyles; // Applied to all fields unless overridden per-field
937
+ }
938
+
939
+ interface IRevealCardField {
940
+ field: RevealableCardField;
941
+ container_id?: string; // default: #reveal_<field>
942
+ altText?: string; // Placeholder text shown before reveal() resolves
943
+ label?: string; // Label rendered above the field
944
+ styles?: IRevealElementStyles; // Per-field override; takes priority over request.styles
945
+ }
946
+
947
+ interface IRevealElementStyles {
948
+ inputStyles?: {
949
+ base?: Record<string, any>;
950
+ copyIcon?: Record<string, any>;
951
+ global?: Record<string, any>;
694
952
  };
953
+ labelStyles?: { base?: Record<string, any>; global?: Record<string, any> };
954
+ errorTextStyles?: { base?: Record<string, any>; global?: Record<string, any> };
955
+ }
956
+ ```
695
957
 
696
- return (
697
- <div className="container">
698
- <iframe className="tds-iframe" allowTransparency={true} id="tdsIframe"></iframe>
699
- {loading ? (
700
- <div style={{ textAlign: 'center', padding: '40px 0', color: '#007AFF' }}>
701
- <div style={{ fontWeight: 600, fontSize: 18 }}>Loading checkout...</div>
702
- </div>
703
- ) : (
704
- <>
705
- <p>Saved cards:</p>
706
- <div style={{ marginBottom: 24 }}>
707
- {cards.length > 0 ? (
708
- cards.map((card: any) => (
709
- <div
710
- key={card.fields.skyflow_id}
711
- style={{
712
- background: selectedCardId === card.fields.skyflow_id ? '#E3F2FD' : '#f9f9f9',
713
- borderRadius: 12,
714
- border: selectedCardId === card.fields.skyflow_id ? '2px solid #007AFF' : '2px solid transparent',
715
- marginBottom: 12,
716
- padding: 0,
717
- cursor: 'pointer',
718
- textAlign: 'left',
719
- }}
720
- onClick={() => handleSelectCard(card.fields.skyflow_id)}
721
- >
722
- <div style={{ display: 'flex', alignItems: 'center', padding: 16 }}>
723
- {card.icon && (
724
- <img src={card.icon} alt="card" style={{ width: 50, height: 32, marginRight: 16, objectFit: 'contain' }} />
725
- )}
726
- <div style={{ flex: 1, textAlign: 'left' }}>
727
- <div style={{ fontWeight: 'bold', color: '#333', marginBottom: 4 }}>{card.fields.cardholder_name}</div>
728
- <div style={{ color: '#666', marginBottom: 4 }}>•••• •••• •••• {card.fields.card_number.slice(-4)}</div>
729
- <div style={{ color: '#999', fontSize: 12 }}>Expires: {card.fields.expiration_month}/{card.fields.expiration_year}</div>
730
- </div>
731
- {selectedCardId === card.fields.skyflow_id && (
732
- <div
733
- style={{ marginLeft: 12, width: 120, background: '#fff', borderRadius: 8, boxShadow: '0 2px 8px #eee', padding: 8, textAlign: 'left', display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}
734
- onClick={e => e.stopPropagation()}
735
- >
736
- <div style={{ maxHeight: '90px' }} id={`collect_cvv_${card.fields.skyflow_id}`}></div>
737
- </div>
738
- )}
739
- </div>
740
- </div>
741
- ))
742
- ) : (
743
- <div style={{ textAlign: 'center', padding: '40px 0', color: '#999' }}>
744
- <div style={{ fontWeight: 600, fontSize: 16 }}>No saved cards</div>
745
- <div style={{ fontSize: 14, color: '#bbb' }}>Add a card to use this method</div>
746
- </div>
747
- )}
748
- </div>
749
- <button style={{ padding: '10px', background: '#ddd' }} onClick={handlePayment}>
750
- Pay with saved card
751
- </button>
752
- </>
753
- )}
754
- </div>
755
- );
756
- };
958
+ **Example 1 — Basic (shorthand):**
959
+
960
+ ```html
961
+ <div id="reveal_cardholder_name"></div>
962
+ <div id="reveal_card_number"></div>
963
+ <div id="reveal_expiration_month"></div>
964
+ <div id="reveal_expiration_year"></div>
965
+ ```
966
+
967
+ ```typescript
968
+ await this.liteCheckout.saveCustomerCard();
969
+
970
+ // Reveal immediately after saving
971
+ await this.liteCheckout.revealCardFields({
972
+ fields: ['cardholder_name', 'card_number', 'expiration_month', 'expiration_year'],
973
+ });
974
+ // #reveal_card_number shows "4111 11•• •••• 1234"
975
+ // #reveal_cardholder_name shows "John Doe"
757
976
  ```
758
977
 
978
+ **Example 2 — With `altText` and `label` per field:**
979
+
980
+ ```typescript
981
+ await this.liteCheckout.revealCardFields({
982
+ fields: [
983
+ { field: 'card_number', altText: '•••• •••• •••• ••••', label: 'Card Number' },
984
+ { field: 'cardholder_name', altText: 'Loading…', label: 'Cardholder' },
985
+ { field: 'expiration_month', label: 'Month' },
986
+ { field: 'expiration_year', label: 'Year' },
987
+ ],
988
+ });
989
+ ```
759
990
 
760
- ## Return secure token
991
+ **Example 3 Custom styles:**
761
992
 
762
993
  ```typescript
994
+ await this.liteCheckout.revealCardFields({
995
+ fields: ['card_number', 'cardholder_name', 'expiration_month', 'expiration_year'],
996
+ styles: {
997
+ inputStyles: {
998
+ base: {
999
+ color: '#ffffff',
1000
+ fontFamily: '"Courier New", monospace',
1001
+ fontSize: '16px',
1002
+ background: 'transparent',
1003
+ letterSpacing: '2px',
1004
+ },
1005
+ },
1006
+ },
1007
+ });
1008
+ ```
1009
+
1010
+ **Example 4 — Custom container IDs:**
1011
+
1012
+ ```typescript
1013
+ await this.liteCheckout.revealCardFields({
1014
+ fields: [
1015
+ { field: 'card_number', container_id: '#my-card-display' },
1016
+ { field: 'cardholder_name', container_id: '#my-name-display' },
1017
+ ],
1018
+ });
1019
+ ```
1020
+
1021
+ ---
1022
+
1023
+ ## 10. Error Handling
1024
+
1025
+ ### 10.1 Error structure
1026
+
1027
+ When a public SDK method fails, it throws an `AppError` instance with the following shape:
1028
+
1029
+ ```json
763
1030
  {
764
- access: string;
1031
+ "name": "TonderError",
1032
+ "status": "error",
1033
+ "code": "PAYMENT_PROCESS_ERROR",
1034
+ "message": "There was an issue processing the payment.",
1035
+ "statusCode": 500,
1036
+ "details": { ... }
1037
+ }
1038
+ ```
1039
+
1040
+ **Notes:**
1041
+ - `statusCode` reflects the HTTP status when available; defaults to `500` for non-HTTP errors.
1042
+ - `details` contains additional context about the error when available.
1043
+
1044
+ **TypeScript catch pattern:**
1045
+
1046
+ ```typescript
1047
+ import { AppError } from '@tonder.io/ionic-lite-sdk';
1048
+
1049
+ try {
1050
+ const response = await this.liteCheckout.payment(data);
1051
+ } catch (error) {
1052
+ if (error instanceof AppError) {
1053
+ console.error(`[${error.code}] ${error.message} (HTTP ${error.statusCode})`);
1054
+ // Use error.code to show a user-friendly message
1055
+ }
765
1056
  }
766
1057
  ```
767
1058
 
768
- ## Deprecated Fields
1059
+ ---
1060
+
1061
+ ### 10.2 Error code reference
1062
+
1063
+ | Code | Thrown by | When |
1064
+ |------|-----------|------|
1065
+ | `PAYMENT_PROCESS_ERROR` | `payment()` | Any payment failure |
1066
+ | `CARD_ON_FILE_DECLINED` | `payment()`, `saveCustomerCard()` | Card On File authorization declined (only when Card On File is active on your account) |
1067
+ | `MOUNT_COLLECT_ERROR` | `mountCardFields()`, `revealCardFields()` | Secure fields fail to mount, or `revealCardFields()` called without a prior successful card operation |
1068
+ | `SAVE_CARD_ERROR` | `saveCustomerCard()` | Any error during card save |
1069
+ | `FETCH_CARDS_ERROR` | `getCustomerCards()` | Request to fetch saved cards fails |
1070
+ | `REMOVE_CARD_ERROR` | `removeCustomerCard()` | Request to remove a card fails |
1071
+ | `FETCH_PAYMENT_METHODS_ERROR` | `getCustomerPaymentMethods()` | Request to fetch APMs fails |
1072
+
1073
+ ---
1074
+
1075
+ ## 11. Customization & Styling
1076
+
1077
+ ### 11.1 Global form styles
1078
+
1079
+ Apply styles to all card input fields at once via `customization.styles.cardForm`. Per-field overrides (Section 11.2) take priority.
1080
+
1081
+ `cardForm` uses wrapper keys (`inputStyles`, `labelStyles`, `errorStyles`) defined by `ILiteCardFormStyles`. Per-field keys like `cardholderName`, `cardNumber`, `cvv`, etc. are directly `CollectInputStylesVariant` — no wrapper.
1082
+
1083
+ ```typescript
1084
+ new LiteCheckout({
1085
+ apiKey: 'YOUR_KEY',
1086
+ mode: 'production',
1087
+ returnUrl: 'https://myapp.com/checkout',
1088
+ customization: {
1089
+ styles: {
1090
+ // Show card brand icon inside the card_number field (default: true)
1091
+ enableCardIcon: true,
1092
+
1093
+ cardForm: {
1094
+ // Base typography applied to all fields
1095
+ base: {
1096
+ fontFamily: 'Inter, sans-serif',
1097
+ fontSize: '14px',
1098
+ color: '#1d1d1f',
1099
+ },
1100
+ // Styles applied to the <input> element inside each iframe
1101
+ inputStyles: {
1102
+ base: {
1103
+ border: '1px solid #d1d1d6',
1104
+ borderRadius: '8px',
1105
+ padding: '10px 12px',
1106
+ backgroundColor: '#ffffff',
1107
+ },
1108
+ focus: {
1109
+ borderColor: '#6200ee',
1110
+ boxShadow: '0 0 0 3px rgba(98, 0, 238, 0.15)',
1111
+ outline: 'none',
1112
+ },
1113
+ complete: {
1114
+ borderColor: '#34c759',
1115
+ },
1116
+ invalid: {
1117
+ borderColor: '#ff3b30',
1118
+ color: '#ff3b30',
1119
+ },
1120
+ empty: {
1121
+ borderColor: '#d1d1d6',
1122
+ },
1123
+ },
1124
+ // Styles applied to the field label
1125
+ labelStyles: {
1126
+ base: {
1127
+ fontSize: '12px',
1128
+ fontWeight: '500',
1129
+ color: '#6e6e73',
1130
+ marginBottom: '4px',
1131
+ },
1132
+ },
1133
+ // Styles applied to the validation error message
1134
+ errorStyles: {
1135
+ base: {
1136
+ color: '#ff3b30',
1137
+ fontSize: '11px',
1138
+ marginTop: '4px',
1139
+ },
1140
+ },
1141
+ },
1142
+ },
1143
+ },
1144
+ });
1145
+ ```
1146
+
1147
+ ---
769
1148
 
770
- The following fields have been deprecated and should no longer be used. Consider using the recommended alternatives:
1149
+ ### 11.2 Per-field styles
771
1150
 
772
- ## Register customer card
773
- ### `apiKeyTonder` Property
1151
+ Override styles for individual fields using the field keys in `IStyles`. These take priority over `cardForm` styles.
774
1152
 
775
- - **Deprecated Reason:** The `apiKeyTonder` property in the constructor and `IInlineLiteCheckoutOptions` interface is no longer required.
776
- - **Alternative:** Use the `apiKey` field.
1153
+ > **Key difference from `cardForm`:** Per-field keys are directly `CollectInputStylesVariant` variants like `base`, `focus`, `invalid` go at the top level. There is **no** `inputStyles` wrapper.
777
1154
 
778
- ### `baseUrlTonder` Property
1155
+ **Available style variants for `CollectInputStylesVariant`:**
779
1156
 
780
- - **Deprecated Reason:** The `baseUrlTonder` property in the constructor and `IInlineLiteCheckoutOptions` interface is no longer required.
781
- - **Alternative:** Use the `mode` field with `stage` | `development` | `sandbox` | `production` options.
1157
+ ```typescript
1158
+ interface CollectInputStylesVariant {
1159
+ base?: Record<string, any>; // Default state
1160
+ focus?: Record<string, any>; // When field has focus
1161
+ complete?: Record<string, any>; // When field has a valid value
1162
+ invalid?: Record<string, any>; // When value fails validation
1163
+ empty?: Record<string, any>; // When field is empty (unfocused)
1164
+ cardIcon?: Record<string, any>; // Card brand icon (card_number only)
1165
+ dropdownIcon?: Record<string, any>; // Dropdown arrow icon
1166
+ dropdown?: Record<string, any>; // Dropdown container
1167
+ dropdownListItem?: Record<string, any>; // Each dropdown option
1168
+ global?: Record<string, any>; // Applied to the iframe root element
1169
+ }
1170
+ ```
782
1171
 
783
- ### `signal` Property
1172
+ **Per-field keys in `IStyles`:**
784
1173
 
785
- - **Deprecated Reason:** The `signal` property in the constructor and `IInlineLiteCheckoutOptions` interface is no longer required.
1174
+ | Key | Field |
1175
+ |-----|-------|
1176
+ | `cardholderName` | Cardholder name field |
1177
+ | `cardNumber` | Card number field |
1178
+ | `cvv` | CVV field |
1179
+ | `expirationMonth` | Expiration month field |
1180
+ | `expirationYear` | Expiration year field |
786
1181
 
1182
+ **Example — highlight CVV with a different color scheme:**
787
1183
 
788
- ## Deprecated Functions
1184
+ ```typescript
1185
+ customization: {
1186
+ styles: {
1187
+ cvv: {
1188
+ base: { borderColor: '#8e44ad', backgroundColor: '#faf5ff' },
1189
+ focus: { borderColor: '#6c3483', boxShadow: '0 0 0 3px rgba(108,52,131,0.2)' },
1190
+ invalid: { borderColor: '#e74c3c', color: '#e74c3c' },
1191
+ complete: { borderColor: '#27ae60' },
1192
+ },
1193
+ },
1194
+ }
1195
+ ```
789
1196
 
790
- The following functions have been deprecated and should no longer be used. Consider using the recommended alternatives:
1197
+ **Example full per-field override:**
791
1198
 
792
- ### `customerRegister`
1199
+ ```typescript
1200
+ customization: {
1201
+ styles: {
1202
+ cardNumber: {
1203
+ base: { letterSpacing: '2px', fontFamily: '"Courier New", monospace' },
1204
+ cardIcon: { width: '32px', height: '20px' },
1205
+ },
1206
+ expirationMonth: {
1207
+ base: { textAlign: 'center' },
1208
+ },
1209
+ expirationYear: {
1210
+ base: { textAlign: 'center' },
1211
+ },
1212
+ },
1213
+ }
1214
+ ```
793
1215
 
794
- - **Deprecated Reason:** This function is no longer necessary as registration is now automatically handled during payment processing or when using card management methods.
1216
+ ---
795
1217
 
796
- ### `createOrder` and `createPayment`
1218
+ ### 11.3 Labels & placeholders
797
1219
 
798
- - **Deprecated Reason:** These functions have been replaced by the `payment` function, which now automatically handles order creation and payment processing.
799
- - **Alternative:** Use the `payment` function.
1220
+ ```typescript
1221
+ interface IFormLabels {
1222
+ name?: string; // Label for cardholder name field
1223
+ card_number?: string; // Label for card number field
1224
+ cvv?: string; // Label for CVV field
1225
+ expiry_date?: string; // Shared expiry label (if shown as one field)
1226
+ expiration_month?: string; // Label for expiration month field
1227
+ expiration_year?: string; // Label for expiration year field
1228
+ }
1229
+
1230
+ interface IFormPlaceholder {
1231
+ name?: string; // Placeholder for cardholder name
1232
+ card_number?: string; // Placeholder for card number
1233
+ cvv?: string; // Placeholder for CVV
1234
+ expiration_month?: string; // Placeholder for expiration month
1235
+ expiration_year?: string; // Placeholder for expiration year
1236
+ }
1237
+ ```
1238
+
1239
+ **Example:**
800
1240
 
801
- ### `startCheckoutRouter` and `startCheckoutRouterFull`
1241
+ ```typescript
1242
+ customization: {
1243
+ labels: {
1244
+ name: 'Cardholder Name',
1245
+ card_number: 'Card Number',
1246
+ cvv: 'Security Code (CVV)',
1247
+ expiration_month: 'Month',
1248
+ expiration_year: 'Year',
1249
+ },
1250
+ placeholders: {
1251
+ name: 'John Doe',
1252
+ card_number: '4111 1111 1111 1111',
1253
+ cvv: '•••',
1254
+ expiration_month: 'MM',
1255
+ expiration_year: 'YY',
1256
+ },
1257
+ }
1258
+ ```
802
1259
 
803
- - **Deprecated Reason:** These functions have been replaced by the `payment` function.
804
- - **Alternative:** Use the `payment` function.
1260
+ ---
805
1261
 
806
- ### `registerCustomerCard`
1262
+ ## 12. Deprecated API
807
1263
 
808
- - **Deprecated Reason:** This function has been renamed to `saveCustomerCard` to better align with its purpose. The method's usage has also been updated.
809
- - **Alternative:** Use the `saveCustomerCard` method and update your implementation to reflect the changes.
1264
+ <details>
1265
+ <summary>Deprecated API click to expand</summary>
810
1266
 
811
- ### `deleteCustomerCard`
1267
+ ### Deprecated constructor properties
812
1268
 
813
- - **Deprecated Reason:** This function has been renamed to `removeCustomerCard` to better align with its purpose. The method's usage has also been updated.
814
- - **Alternative:** Use the `removeCustomerCard` method and update your implementation to reflect the changes.
1269
+ | Property | Replacement | Notes |
1270
+ |----------|-------------|-------|
1271
+ | `apiKeyTonder` | `apiKey` | Renamed for clarity |
1272
+ | `baseUrlTonder` | `mode` | Replaced by environment enum (`'stage'` \| `'production'` \| ...) |
1273
+ | `signal` | (removed) | AbortController signal is no longer needed |
815
1274
 
816
- ### `getActiveAPMs`
1275
+ ### Deprecated methods
817
1276
 
818
- - **Deprecated Reason:** This function has been renamed to `getCustomerPaymentMethods` to better align with its purpose. The method's usage has also been updated.
819
- - **Alternative:** Use the `getCustomerPaymentMethods` method and update your implementation to reflect the changes.
1277
+ | Deprecated Method | Use Instead | Notes |
1278
+ |------------------|-------------|-------|
1279
+ | `getBusiness()` | (auto-handled) | No longer needed |
1280
+ | `customerRegister(email)` | (auto-handled) | No longer needed |
1281
+ | `createOrder(items)` | `payment()` | Replaced by unified `payment()` |
1282
+ | `createPayment(items)` | `payment()` | Replaced by unified `payment()` |
1283
+ | `startCheckoutRouter(data)` | `payment()` | Replaced by unified `payment()` |
1284
+ | `startCheckoutRouterFull(data)` | `payment()` | Replaced by unified `payment()` |
1285
+ | `registerCustomerCard(secureToken, customerToken, data)` | `saveCustomerCard()` | Signature changed; call `mountCardFields()` first |
1286
+ | `deleteCustomerCard(customerToken, skyflowId)` | `removeCustomerCard(skyflowId)` | Signature simplified |
1287
+ | `getActiveAPMs()` | `getCustomerPaymentMethods()` | Renamed |
1288
+ | `getSkyflowTokens(...)` | (auto-handled) | No longer needed |
1289
+ | `getOpenpayDeviceSessionID(...)` | (auto-handled) | No longer needed |
820
1290
 
821
- ### `getSkyflowTokens`
1291
+ ### Deprecated data patterns
822
1292
 
823
- - **Deprecated Reason:** Card registration and checkout are now automatically handled during the payment process or through card management methods, making this method unnecessary.
1293
+ **Raw card fields in `payment()` / `saveCustomerCard()`**
824
1294
 
825
- ### `getOpenpayDeviceSessionID`
1295
+ Passing raw card values (card number, CVV, etc.) directly to these methods is no longer supported. Card data must be collected via `mountCardFields()` first:
826
1296
 
827
- - **Deprecated Reason:** It is no longer necessary to use this method is now automatically handled during the payment process.
1297
+ ```typescript
1298
+ // ❌ Deprecated
1299
+ await liteCheckout.payment({ ..., card: { card_number: '4111...', cvv: '123', ... } });
828
1300
 
1301
+ // ✅ Current
1302
+ await liteCheckout.mountCardFields({ fields: ['cardholder_name', 'card_number', 'expiration_month', 'expiration_year', 'cvv'] });
1303
+ // user fills in the form
1304
+ await liteCheckout.payment({ customer, cart, currency });
1305
+ ```
829
1306
 
830
- ## Notes
1307
+ **`returnUrl` in `IProcessPaymentRequest`**
831
1308
 
832
- ### General
1309
+ Set `returnUrl` on the constructor instead of per-payment:
833
1310
 
834
- - Replace `apiKey`, `mode`, `returnUrl` with your actual values.
835
- - Remember to use the `configureCheckout` function after creating an instance of `LiteCheckout`. This ensures that functions such as payment processing, saving cards, deleting cards, and others work correctly.
1311
+ ```typescript
1312
+ // Deprecated
1313
+ await liteCheckout.payment({ ..., returnUrl: 'https://myapp.com/done' });
836
1314
 
1315
+ // ✅ Current — set on constructor
1316
+ new LiteCheckout({ apiKey, mode, returnUrl: 'https://myapp.com/done' });
1317
+ ```
837
1318
 
838
- ## License
1319
+ </details>
839
1320
 
840
- [MIT](https://choosealicense.com/licenses/mit/)