@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 +1157 -677
- package/dist/classes/liteCheckout.d.ts +24 -4
- package/dist/helpers/skyflow.d.ts +9 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/types/card.d.ts +95 -0
- package/dist/types/checkout.d.ts +10 -1
- package/dist/types/commons.d.ts +25 -1
- package/dist/types/liteInlineCheckout.d.ts +49 -8
- package/dist/types/requests.d.ts +1 -0
- package/package.json +1 -1
- package/src/classes/liteCheckout.ts +88 -39
- package/src/helpers/skyflow.ts +144 -41
- package/src/index.ts +8 -0
- package/src/types/card.ts +96 -0
- package/src/types/checkout.ts +10 -1
- package/src/types/commons.ts +25 -1
- package/src/types/liteInlineCheckout.ts +50 -8
- package/src/types/requests.ts +1 -0
package/README.md
CHANGED
|
@@ -1,840 +1,1320 @@
|
|
|
1
|
-
#
|
|
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. [
|
|
9
|
-
2. [
|
|
10
|
-
3. [Configuration
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
159
|
+
npm install @tonder.io/ionic-lite-sdk
|
|
160
|
+
# or
|
|
161
|
+
yarn add @tonder.io/ionic-lite-sdk
|
|
28
162
|
```
|
|
29
163
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
45
|
-
const liteCheckout = new LiteCheckout({
|
|
46
|
-
signal,
|
|
47
|
-
baseUrlTonder,
|
|
48
|
-
apiKeyTonder
|
|
49
|
-
})
|
|
207
|
+
---
|
|
50
208
|
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
```
|
|
89
|
-
|
|
90
|
-
|
|
351
|
+
```typescript
|
|
352
|
+
liteCheckout.configureCheckout({
|
|
353
|
+
customer: { email: 'user@example.com' },
|
|
354
|
+
secureToken: access,
|
|
355
|
+
});
|
|
91
356
|
```
|
|
92
357
|
|
|
93
|
-
|
|
358
|
+
---
|
|
94
359
|
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
1
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
133
|
-
|
|
134
|
-
|
|
461
|
+
```typescript
|
|
462
|
+
liteCheckout.mountCardFields({
|
|
463
|
+
fields: ['cvv'],
|
|
464
|
+
card_id: 'abc123', // card.fields.skyflow_id
|
|
465
|
+
});
|
|
135
466
|
```
|
|
136
467
|
|
|
137
|
-
|
|
468
|
+
> **Note:** Cards with `subscription_id` do not require CVV entry. See [Section 8.4](#84-card-on-file-subscription_id).
|
|
138
469
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
486
|
+
---
|
|
146
487
|
|
|
147
|
-
|
|
488
|
+
### 5.3 Unmounting fields
|
|
148
489
|
|
|
149
|
-
|
|
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
|
-
|
|
492
|
+
The `unmount_context` parameter on `mountCardFields` controls what gets cleared before the new fields are mounted:
|
|
152
493
|
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
503
|
+
```typescript
|
|
504
|
+
ngOnDestroy() {
|
|
505
|
+
this.liteCheckout.unmountCardFields();
|
|
506
|
+
}
|
|
507
|
+
```
|
|
162
508
|
|
|
163
|
-
|
|
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
|
-
|
|
514
|
+
---
|
|
166
515
|
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
628
|
+
---
|
|
317
629
|
|
|
318
|
-
###
|
|
630
|
+
### 6.2 Saved card payment
|
|
319
631
|
|
|
320
|
-
|
|
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
|
-
|
|
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
|
-
|
|
643
|
+
---
|
|
333
644
|
|
|
334
|
-
|
|
645
|
+
### 6.3 Alternative Payment Method (APM)
|
|
335
646
|
|
|
336
|
-
|
|
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
|
-
|
|
342
|
-
1.
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
**
|
|
349
|
-
```tsx
|
|
350
|
-
// Update CVV for a saved card
|
|
665
|
+
**Mercado Pago — `apm_config`:**
|
|
351
666
|
|
|
352
|
-
|
|
353
|
-
<div id={`collect_cvv_saved-card-id`}></div>
|
|
667
|
+
Pass Mercado Pago-specific preferences via `apm_config`:
|
|
354
668
|
|
|
355
|
-
|
|
356
|
-
liteCheckout.
|
|
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
|
-
|
|
735
|
+
> **Note:** 3DS authentication is handled automatically by the SDK. You do not need to handle redirection yourself.
|
|
362
736
|
|
|
363
|
-
|
|
737
|
+
---
|
|
364
738
|
|
|
365
|
-
|
|
739
|
+
## 7. 3DS Handling
|
|
366
740
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
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
|
-
|
|
781
|
+
---
|
|
388
782
|
|
|
389
|
-
|
|
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
|
-
|
|
787
|
+
```typescript
|
|
788
|
+
getCustomerCards(): Promise<ICustomerCardsResponse>
|
|
789
|
+
```
|
|
399
790
|
|
|
400
|
-
|
|
791
|
+
```typescript
|
|
792
|
+
interface ICustomerCardsResponse {
|
|
793
|
+
user_id: number;
|
|
794
|
+
cards: ICard[];
|
|
795
|
+
}
|
|
401
796
|
|
|
402
|
-
|
|
797
|
+
interface ICard {
|
|
798
|
+
fields: ICardSkyflowFields;
|
|
799
|
+
icon?: string; // URL to card brand image
|
|
800
|
+
}
|
|
403
801
|
|
|
404
|
-
|
|
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
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
419
|
-
this.initializeInlineCheckout();
|
|
420
|
-
}
|
|
824
|
+
---
|
|
421
825
|
|
|
422
|
-
|
|
423
|
-
this.liteCheckout = new LiteCheckout({ ...this.sdkParameters });
|
|
424
|
-
}
|
|
826
|
+
### 8.2 Save a new card
|
|
425
827
|
|
|
426
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
830
|
+
```typescript
|
|
831
|
+
saveCustomerCard(): Promise<ISaveCardResponse>
|
|
832
|
+
// Returns: { skyflow_id: string; user_id: number }
|
|
833
|
+
```
|
|
433
834
|
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
439
|
-
checkoutData: IProcessPaymentRequest,
|
|
440
|
-
): Promise<IStartCheckoutResponse> {
|
|
441
|
-
return this.inlineCheckout.payment(checkoutData);
|
|
442
|
-
}
|
|
842
|
+
// 2. User fills in the card form...
|
|
443
843
|
|
|
444
|
-
|
|
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
|
-
|
|
447
|
-
|
|
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
|
-
|
|
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
|
-
|
|
547
|
-
this.initCheckout();
|
|
548
|
-
}
|
|
856
|
+
### 8.3 Remove a card
|
|
549
857
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
900
|
+
**Legacy cards without `subscription_id`**
|
|
590
901
|
|
|
591
|
-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
637
|
-
if (!liteCheckout) {
|
|
638
|
-
setLoading(true);
|
|
639
|
-
initializeTonderSDK();
|
|
640
|
-
}
|
|
641
|
-
}, []);
|
|
908
|
+
---
|
|
642
909
|
|
|
643
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
677
|
-
const response = await liteCheckout.getCustomerCards();
|
|
678
|
-
setCards(response.cards || []);
|
|
679
|
-
};
|
|
916
|
+
**Default container IDs and fixed redaction:**
|
|
680
917
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1149
|
+
### 11.2 Per-field styles
|
|
771
1150
|
|
|
772
|
-
|
|
773
|
-
### `apiKeyTonder` Property
|
|
1151
|
+
Override styles for individual fields using the field keys in `IStyles`. These take priority over `cardForm` styles.
|
|
774
1152
|
|
|
775
|
-
|
|
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
|
-
|
|
1155
|
+
**Available style variants for `CollectInputStylesVariant`:**
|
|
779
1156
|
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
1172
|
+
**Per-field keys in `IStyles`:**
|
|
784
1173
|
|
|
785
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1197
|
+
**Example — full per-field override:**
|
|
791
1198
|
|
|
792
|
-
|
|
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
|
-
|
|
1216
|
+
---
|
|
795
1217
|
|
|
796
|
-
###
|
|
1218
|
+
### 11.3 Labels & placeholders
|
|
797
1219
|
|
|
798
|
-
|
|
799
|
-
|
|
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
|
-
|
|
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
|
-
|
|
804
|
-
- **Alternative:** Use the `payment` function.
|
|
1260
|
+
---
|
|
805
1261
|
|
|
806
|
-
|
|
1262
|
+
## 12. Deprecated API
|
|
807
1263
|
|
|
808
|
-
|
|
809
|
-
|
|
1264
|
+
<details>
|
|
1265
|
+
<summary>Deprecated API — click to expand</summary>
|
|
810
1266
|
|
|
811
|
-
###
|
|
1267
|
+
### Deprecated constructor properties
|
|
812
1268
|
|
|
813
|
-
|
|
814
|
-
|
|
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
|
-
###
|
|
1275
|
+
### Deprecated methods
|
|
817
1276
|
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
###
|
|
1291
|
+
### Deprecated data patterns
|
|
822
1292
|
|
|
823
|
-
|
|
1293
|
+
**Raw card fields in `payment()` / `saveCustomerCard()`**
|
|
824
1294
|
|
|
825
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1307
|
+
**`returnUrl` in `IProcessPaymentRequest`**
|
|
831
1308
|
|
|
832
|
-
|
|
1309
|
+
Set `returnUrl` on the constructor instead of per-payment:
|
|
833
1310
|
|
|
834
|
-
|
|
835
|
-
|
|
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
|
-
|
|
1319
|
+
</details>
|
|
839
1320
|
|
|
840
|
-
[MIT](https://choosealicense.com/licenses/mit/)
|