@tonder.io/ionic-lite-sdk 0.0.63-beta.DEV-1975.3 → 0.0.63-beta.DEV-1975.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/classes/BaseInlineCheckout.d.ts +7 -3
- package/dist/classes/liteCheckout.d.ts +2 -0
- package/dist/helpers/card_on_file.d.ts +45 -0
- package/dist/index.js +1 -1
- package/dist/types/card.d.ts +2 -0
- package/dist/types/cardOnFile.d.ts +133 -0
- package/dist/types/commons.d.ts +5 -0
- package/dist/types/responses.d.ts +2 -0
- package/package.json +1 -1
- package/src/classes/BaseInlineCheckout.ts +27 -4
- package/src/classes/liteCheckout.ts +207 -67
- package/src/helpers/card_on_file.ts +267 -0
- package/src/types/card.ts +2 -0
- package/src/types/cardOnFile.ts +155 -0
- package/src/types/commons.ts +5 -0
- package/src/types/responses.ts +2 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AcquirerInstance,
|
|
3
|
+
CardOnFileSubscriptionRequest,
|
|
4
|
+
CardOnFileSubscriptionResponse,
|
|
5
|
+
CardOnFileTokenRequest,
|
|
6
|
+
CardOnFileTokenResponse,
|
|
7
|
+
ProcessParams,
|
|
8
|
+
SecureInitResponse,
|
|
9
|
+
SecurityInfo,
|
|
10
|
+
Validate3DSResponse,
|
|
11
|
+
} from "../types/cardOnFile";
|
|
12
|
+
|
|
13
|
+
declare global {
|
|
14
|
+
interface Window {
|
|
15
|
+
// External acquirer SDK (Kushki)
|
|
16
|
+
Kushki: new (config: { merchantId: string; inTestEnvironment: boolean }) => AcquirerInstance;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ============ Helper Functions ============
|
|
21
|
+
|
|
22
|
+
const ACQUIRER_SCRIPT_URL = "https://cdn.kushkipagos.com/kushki.min.js";
|
|
23
|
+
|
|
24
|
+
let acquirerScriptLoaded = false;
|
|
25
|
+
let acquirerScriptPromise: Promise<void> | null = null;
|
|
26
|
+
|
|
27
|
+
async function loadAcquirerScript(): Promise<void> {
|
|
28
|
+
if (acquirerScriptLoaded) {
|
|
29
|
+
return Promise.resolve();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (acquirerScriptPromise) {
|
|
33
|
+
return acquirerScriptPromise;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
acquirerScriptPromise = new Promise((resolve, reject) => {
|
|
37
|
+
const existingScript = document.querySelector(`script[src="${ACQUIRER_SCRIPT_URL}"]`);
|
|
38
|
+
|
|
39
|
+
if (existingScript) {
|
|
40
|
+
acquirerScriptLoaded = true;
|
|
41
|
+
resolve();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const script = document.createElement("script");
|
|
46
|
+
script.src = ACQUIRER_SCRIPT_URL;
|
|
47
|
+
script.async = true;
|
|
48
|
+
|
|
49
|
+
script.onload = () => {
|
|
50
|
+
acquirerScriptLoaded = true;
|
|
51
|
+
resolve();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
script.onerror = () => {
|
|
55
|
+
acquirerScriptPromise = null;
|
|
56
|
+
reject(new Error("Failed to load acquirer script"));
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
document.head.appendChild(script);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return acquirerScriptPromise;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createAcquirerInstance(
|
|
66
|
+
merchantId: string,
|
|
67
|
+
isTestEnvironment: boolean
|
|
68
|
+
): AcquirerInstance {
|
|
69
|
+
if (!window.Kushki) {
|
|
70
|
+
throw new Error("Acquirer script not loaded. Call initialize() first.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return new window.Kushki({
|
|
74
|
+
merchantId,
|
|
75
|
+
inTestEnvironment: isTestEnvironment,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ============ Constants ============
|
|
80
|
+
|
|
81
|
+
const ACQ_API_URL_STAGE = "https://api-stage.tonder.io";
|
|
82
|
+
const ACQ_API_URL_PROD = "https://api.tonder.io";
|
|
83
|
+
|
|
84
|
+
// ============ CardOnFile Class ============
|
|
85
|
+
|
|
86
|
+
export class CardOnFile {
|
|
87
|
+
private readonly apiUrl: string;
|
|
88
|
+
private readonly merchantId: string;
|
|
89
|
+
private readonly apiKey: string;
|
|
90
|
+
private readonly isTestEnvironment: boolean;
|
|
91
|
+
private acquirerInstance: AcquirerInstance | null = null;
|
|
92
|
+
|
|
93
|
+
constructor(config: {
|
|
94
|
+
merchantId: string;
|
|
95
|
+
apiKey: string;
|
|
96
|
+
isTestEnvironment?: boolean;
|
|
97
|
+
}) {
|
|
98
|
+
this.isTestEnvironment = config.isTestEnvironment ?? true;
|
|
99
|
+
this.apiUrl = this.isTestEnvironment ? ACQ_API_URL_STAGE : ACQ_API_URL_PROD;
|
|
100
|
+
this.merchantId = config.merchantId;
|
|
101
|
+
this.apiKey = config.apiKey;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async initialize(): Promise<void> {
|
|
105
|
+
await loadAcquirerScript();
|
|
106
|
+
this.acquirerInstance = createAcquirerInstance(
|
|
107
|
+
this.merchantId,
|
|
108
|
+
this.isTestEnvironment
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private getAcquirerInstance(): AcquirerInstance {
|
|
113
|
+
if (!this.acquirerInstance) {
|
|
114
|
+
throw new Error("CardOnFile not initialized. Call initialize() first.");
|
|
115
|
+
}
|
|
116
|
+
return this.acquirerInstance;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get JWT for 3DS authentication
|
|
121
|
+
* @param cardBin - First 8 digits of the card number
|
|
122
|
+
*/
|
|
123
|
+
async getJwt(cardBin: string): Promise<string> {
|
|
124
|
+
const acquirer = this.getAcquirerInstance();
|
|
125
|
+
|
|
126
|
+
return new Promise<string>((resolve, reject) => {
|
|
127
|
+
acquirer.requestSecureInit(
|
|
128
|
+
{
|
|
129
|
+
card: {
|
|
130
|
+
number: cardBin,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
(response) => {
|
|
134
|
+
if ("code" in response && response.code) {
|
|
135
|
+
reject(new Error(`Error getting JWT: ${response.message}`));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const successResponse = response as SecureInitResponse;
|
|
140
|
+
if (!successResponse.jwt) {
|
|
141
|
+
reject(new Error("No JWT returned from acquirer"));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
resolve(successResponse.jwt);
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Generate a recurring charge token
|
|
153
|
+
*/
|
|
154
|
+
async generateToken(request: CardOnFileTokenRequest): Promise<CardOnFileTokenResponse> {
|
|
155
|
+
const response = await fetch(
|
|
156
|
+
`${this.apiUrl}/acq-kushki/subscription/token`,
|
|
157
|
+
{
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers: {
|
|
160
|
+
"Content-Type": "application/json",
|
|
161
|
+
Authorization: `Token ${this.apiKey}`,
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify(request),
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
const errorText = await response.text();
|
|
169
|
+
throw new Error(`Failed to generate token: ${response.status} - ${errorText}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return response.json() as Promise<CardOnFileTokenResponse>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Create a subscription with the generated token
|
|
177
|
+
*/
|
|
178
|
+
async createSubscription(request: CardOnFileSubscriptionRequest): Promise<CardOnFileSubscriptionResponse> {
|
|
179
|
+
const response = await fetch(
|
|
180
|
+
`${this.apiUrl}/acq-kushki/subscription/create`,
|
|
181
|
+
{
|
|
182
|
+
method: "POST",
|
|
183
|
+
headers: {
|
|
184
|
+
"Content-Type": "application/json",
|
|
185
|
+
Authorization: `Token ${this.apiKey}`,
|
|
186
|
+
},
|
|
187
|
+
body: JSON.stringify(request),
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
const errorText = await response.text();
|
|
193
|
+
throw new Error(`Failed to create subscription: ${response.status} - ${errorText}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return response.json() as Promise<CardOnFileSubscriptionResponse>;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Validate 3DS challenge
|
|
201
|
+
* @returns true if validation passed, throws error otherwise
|
|
202
|
+
*/
|
|
203
|
+
async validate3DS(
|
|
204
|
+
secureId: string,
|
|
205
|
+
security: SecurityInfo
|
|
206
|
+
): Promise<boolean> {
|
|
207
|
+
const acquirer = this.getAcquirerInstance();
|
|
208
|
+
|
|
209
|
+
return new Promise<boolean>((resolve, reject) => {
|
|
210
|
+
acquirer.requestValidate3DS(
|
|
211
|
+
{
|
|
212
|
+
secureId,
|
|
213
|
+
security,
|
|
214
|
+
},
|
|
215
|
+
(response) => {
|
|
216
|
+
const validResponse = response as Validate3DSResponse;
|
|
217
|
+
// Check for error code
|
|
218
|
+
if (validResponse.code && validResponse.code !== "3DS000") {
|
|
219
|
+
reject(new Error(`3DS validation failed}`));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check isValid flag if present
|
|
224
|
+
if (validResponse.isValid === false) {
|
|
225
|
+
reject(new Error("3DS validation failed"));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
resolve(true);
|
|
230
|
+
}
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Complete flow: JWT → Token → 3DS validation → Subscription
|
|
237
|
+
*/
|
|
238
|
+
async process(params: ProcessParams): Promise<CardOnFileSubscriptionResponse> {
|
|
239
|
+
const jwt = await this.getJwt(params.cardBin);
|
|
240
|
+
const tokenResponse = await this.generateToken({
|
|
241
|
+
card: params.cardTokens,
|
|
242
|
+
currency: params.currency,
|
|
243
|
+
jwt,
|
|
244
|
+
});
|
|
245
|
+
// Handle both response structures: root level or nested in details
|
|
246
|
+
const secureId = tokenResponse.secureId || tokenResponse.details?.secureId;
|
|
247
|
+
const security = tokenResponse.security || tokenResponse.details?.security;
|
|
248
|
+
|
|
249
|
+
// Validate 3DS is required
|
|
250
|
+
if (!secureId || !security) {
|
|
251
|
+
throw new Error("Missing secureId or security in token response");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Validate 3DS - throws error if validation fails
|
|
255
|
+
await this.validate3DS(secureId, security);
|
|
256
|
+
|
|
257
|
+
// Only continue to subscription if 3DS validation passed
|
|
258
|
+
return this.createSubscription({
|
|
259
|
+
token: tokenResponse.token,
|
|
260
|
+
contactDetails: params.contactDetails,
|
|
261
|
+
metadata: {
|
|
262
|
+
customerId: params.customerId,
|
|
263
|
+
},
|
|
264
|
+
currency: params.currency,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
package/src/types/card.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface ICardSkyflowFields {
|
|
|
10
10
|
skyflow_id: string;
|
|
11
11
|
card_scheme: string;
|
|
12
12
|
cardholder_name: string;
|
|
13
|
+
subscription_id?: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export interface ICustomerCardsResponse {
|
|
@@ -30,6 +31,7 @@ export interface ISaveCardInternalResponse {
|
|
|
30
31
|
|
|
31
32
|
export interface ISaveCardSkyflowRequest {
|
|
32
33
|
skyflow_id: string;
|
|
34
|
+
subscription_id?: string;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export interface ISaveCardRequest {
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
export interface AcquirerInstance {
|
|
2
|
+
requestSecureInit: (
|
|
3
|
+
params: SecureInitParams,
|
|
4
|
+
callback: (response: SecureInitResponse | AcquirerErrorResponse) => void
|
|
5
|
+
) => void;
|
|
6
|
+
requestValidate3DS: (
|
|
7
|
+
params: Validate3DSParams,
|
|
8
|
+
callback: (response: Validate3DSResponse | AcquirerErrorResponse) => void
|
|
9
|
+
) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SecureInitParams {
|
|
13
|
+
card: {
|
|
14
|
+
number: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SecureInitResponse {
|
|
19
|
+
jwt: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Validate3DSParams {
|
|
23
|
+
secureId: string;
|
|
24
|
+
security: SecurityInfo;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Validate3DSResponse {
|
|
28
|
+
code?: string;
|
|
29
|
+
message?: string;
|
|
30
|
+
isValid?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AcquirerErrorResponse {
|
|
34
|
+
code: string;
|
|
35
|
+
message: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SecurityInfo {
|
|
39
|
+
acsURL: string;
|
|
40
|
+
authenticationTransactionId: string;
|
|
41
|
+
authRequired: boolean;
|
|
42
|
+
paReq: string;
|
|
43
|
+
specificationVersion: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CardOnFileTokenRequest {
|
|
47
|
+
card: {
|
|
48
|
+
name: string;
|
|
49
|
+
number: string;
|
|
50
|
+
expiryMonth: string;
|
|
51
|
+
expiryYear: string;
|
|
52
|
+
cvv: string;
|
|
53
|
+
};
|
|
54
|
+
currency: string;
|
|
55
|
+
jwt: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface CardOnFileTokenResponse {
|
|
59
|
+
token: string;
|
|
60
|
+
secureId?: string;
|
|
61
|
+
secureService?: string;
|
|
62
|
+
security?: SecurityInfo;
|
|
63
|
+
details?: {
|
|
64
|
+
secureId?: string;
|
|
65
|
+
security?: SecurityInfo;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface CardOnFileSubscriptionRequest {
|
|
70
|
+
token: string;
|
|
71
|
+
contactDetails: {
|
|
72
|
+
firstName: string;
|
|
73
|
+
lastName: string;
|
|
74
|
+
email: string;
|
|
75
|
+
};
|
|
76
|
+
metadata: {
|
|
77
|
+
customerId: string;
|
|
78
|
+
notes?: string;
|
|
79
|
+
};
|
|
80
|
+
currency: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface CardOnFileSubscriptionResponse {
|
|
84
|
+
details: {
|
|
85
|
+
amount: {
|
|
86
|
+
subtotalIva: number;
|
|
87
|
+
subtotalIva0: number;
|
|
88
|
+
ice: number;
|
|
89
|
+
iva: number;
|
|
90
|
+
currency: string;
|
|
91
|
+
};
|
|
92
|
+
binCard: string;
|
|
93
|
+
binInfo: {
|
|
94
|
+
bank: string;
|
|
95
|
+
type: string;
|
|
96
|
+
};
|
|
97
|
+
cardHolderName: string;
|
|
98
|
+
contactDetails: {
|
|
99
|
+
firstName: string;
|
|
100
|
+
lastName: string;
|
|
101
|
+
email: string;
|
|
102
|
+
};
|
|
103
|
+
created: string;
|
|
104
|
+
lastFourDigits: string;
|
|
105
|
+
maskedCreditCard: string;
|
|
106
|
+
merchantId: string;
|
|
107
|
+
merchantName: string;
|
|
108
|
+
paymentBrand: string;
|
|
109
|
+
periodicity: string;
|
|
110
|
+
planName: string;
|
|
111
|
+
processorBankName: string;
|
|
112
|
+
startDate: string;
|
|
113
|
+
};
|
|
114
|
+
subscriptionId: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface SkyflowCollectFields {
|
|
118
|
+
card_number: string;
|
|
119
|
+
cvv: string;
|
|
120
|
+
expiration_month: string;
|
|
121
|
+
expiration_year: string;
|
|
122
|
+
cardholder_name: string;
|
|
123
|
+
skyflow_id: string;
|
|
124
|
+
[key: string]: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface SkyflowCollectRecord {
|
|
128
|
+
fields: SkyflowCollectFields;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface SkyflowCollectResponse {
|
|
132
|
+
records?: SkyflowCollectRecord[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface CardTokens {
|
|
136
|
+
name: string;
|
|
137
|
+
number: string;
|
|
138
|
+
expiryMonth: string;
|
|
139
|
+
expiryYear: string;
|
|
140
|
+
cvv: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface ContactDetails {
|
|
144
|
+
firstName: string;
|
|
145
|
+
lastName: string;
|
|
146
|
+
email: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface ProcessParams {
|
|
150
|
+
cardTokens: CardTokens;
|
|
151
|
+
cardBin: string;
|
|
152
|
+
contactDetails: ContactDetails;
|
|
153
|
+
customerId: string;
|
|
154
|
+
currency: string;
|
|
155
|
+
}
|
package/src/types/commons.ts
CHANGED
|
@@ -8,6 +8,10 @@ import ComposableElement from "skyflow-js/types/core/external/collect/compose-co
|
|
|
8
8
|
import RevealElement from "skyflow-js/types/core/external/reveal/reveal-element";
|
|
9
9
|
import {LabelStyles} from "skyflow-js/types/utils/common";
|
|
10
10
|
|
|
11
|
+
export type CardOnFileKeys = {
|
|
12
|
+
merchant_id: string;
|
|
13
|
+
public_key: string;
|
|
14
|
+
}
|
|
11
15
|
|
|
12
16
|
export type Business = {
|
|
13
17
|
business: {
|
|
@@ -41,6 +45,7 @@ export type Business = {
|
|
|
41
45
|
vault_url: string;
|
|
42
46
|
reference: number;
|
|
43
47
|
is_installments_available: boolean;
|
|
48
|
+
cardonfile_keys?: CardOnFileKeys;
|
|
44
49
|
};
|
|
45
50
|
|
|
46
51
|
export type Customer = {
|