@swirepay-developer/swirepay-ach-sdk 2.0.0

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/index.js ADDED
@@ -0,0 +1,16 @@
1
+ export * from "./web-component/swirepay-checkout.js";
2
+
3
+ /**
4
+ * Helper init function (optional usage)
5
+ */
6
+ export const init = (config = {}) => {
7
+ const el = document.createElement("swirepay-ach-checkout");
8
+
9
+ Object.entries(config).forEach(([key, value]) => {
10
+ if (value !== undefined && value !== null) {
11
+ el.setAttribute(key, value);
12
+ }
13
+ });
14
+
15
+ return el;
16
+ };
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@swirepay-developer/swirepay-ach-sdk",
3
+ "version": "2.0.0",
4
+ "description": "Swirepay Ach Payment SDK (Web Component with Modal UI)",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "keywords": [
8
+ "swirepay",
9
+ "payments",
10
+ "sdk",
11
+ "checkout",
12
+ "web-components"
13
+ ],
14
+ "author": "Swirepay",
15
+ "license": "ISC",
16
+ "dependencies": {
17
+ "libphonenumber-js": "^1.10.53",
18
+ "postal-codes-js": "^2.5.2"
19
+ },
20
+ "scripts": {
21
+ "test": "echo \"Error: no test specified\" && exit 1"
22
+ }
23
+ }
@@ -0,0 +1,810 @@
1
+ import postalCodes from "postal-codes-js";
2
+ import { isValidPhoneNumber } from "libphonenumber-js";
3
+
4
+ const SDK_CONFIG = {
5
+ elliptic: "https://cdnjs.cloudflare.com/ajax/libs/elliptic/6.3.1/elliptic.js",
6
+ crypto: "https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"
7
+ };
8
+ class EventEmitter {
9
+ constructor() { this.events = {}; }
10
+ on(e, cb) {
11
+ this.events[e] = this.events[e] || [];
12
+ this.events[e].push(cb);
13
+ }
14
+ emit(e, d) {
15
+ (this.events[e] || []).forEach(cb => cb(d));
16
+ }
17
+ }
18
+ const INDIAN_STATES = [
19
+ { "code": "AN", "name": "Andaman and Nicobar Islands" },
20
+ { "code": "AP", "name": "Andhra Pradesh" },
21
+ { "code": "AR", "name": "Arunachal Pradesh" },
22
+ { "code": "AS", "name": "Assam" },
23
+ { "code": "BR", "name": "Bihar" },
24
+ { "code": "CG", "name": "Chandigarh" },
25
+ { "code": "CH", "name": "Chhattisgarh" },
26
+ { "code": "DH", "name": "Dadra and Nagar Haveli" },
27
+ { "code": "DD", "name": "Daman and Diu" },
28
+ { "code": "DL", "name": "Delhi" },
29
+ { "code": "GA", "name": "Goa" },
30
+ { "code": "GJ", "name": "Gujarat" },
31
+ { "code": "HR", "name": "Haryana" },
32
+ { "code": "HP", "name": "Himachal Pradesh" },
33
+ { "code": "JK", "name": "Jammu and Kashmir" },
34
+ { "code": "JH", "name": "Jharkhand" },
35
+ { "code": "KA", "name": "Karnataka" },
36
+ { "code": "KL", "name": "Kerala" },
37
+ { "code": "LD", "name": "Lakshadweep" },
38
+ { "code": "MP", "name": "Madhya Pradesh" },
39
+ { "code": "MH", "name": "Maharashtra" },
40
+ { "code": "MN", "name": "Manipur" },
41
+ { "code": "ML", "name": "Meghalaya" },
42
+ { "code": "MZ", "name": "Mizoram" },
43
+ { "code": "NL", "name": "Nagaland" },
44
+ { "code": "OR", "name": "Odisha" },
45
+ { "code": "PY", "name": "Puducherry" },
46
+ { "code": "PB", "name": "Punjab" },
47
+ { "code": "RJ", "name": "Rajasthan" },
48
+ { "code": "SK", "name": "Sikkim" },
49
+ { "code": "TN", "name": "Tamil Nadu" },
50
+ { "code": "TS", "name": "Telangana" },
51
+ { "code": "TR", "name": "Tripura" },
52
+ { "code": "UK", "name": "Uttarakhand" },
53
+ { "code": "UP", "name": "Uttar Pradesh" },
54
+ { "code": "WB", "name": "West Bengal" }
55
+ ];
56
+
57
+ const USA_STATES = [
58
+ { "code": "AL", "name": "Alabama" },
59
+ { "code": "AK", "name": "Alaska" },
60
+ { "code": "AZ", "name": "Arizona" },
61
+ { "code": "AR", "name": "Arkansas" },
62
+ { "code": "CA", "name": "California" },
63
+ { "code": "CO", "name": "Colorado" },
64
+ { "code": "CT", "name": "Connecticut" },
65
+ { "code": "DE", "name": "Delaware" },
66
+ { "code": "FL", "name": "Florida" },
67
+ { "code": "GA", "name": "Georgia" },
68
+ { "code": "HI", "name": "Hawaii" },
69
+ { "code": "ID", "name": "Idaho" },
70
+ { "code": "IL", "name": "Illinois" },
71
+ { "code": "IN", "name": "Indiana" },
72
+ { "code": "IA", "name": "Iowa" },
73
+ { "code": "KS", "name": "Kansas" },
74
+ { "code": "KY", "name": "Kentucky" },
75
+ { "code": "LY", "name": "Louisiana" },
76
+ { "code": "ME", "name": "Maine" },
77
+ { "code": "MD", "name": "Maryland" },
78
+ { "code": "MA", "name": "Massachusetts" },
79
+ { "code": "MI", "name": "Michigan" },
80
+ { "code": "MN", "name": "Minnesota" },
81
+ { "code": "MS", "name": "Mississippi" },
82
+ { "code": "MO", "name": "Missouri" },
83
+ { "code": "MT", "name": "Montana" },
84
+ { "code": "NE", "name": "Nebraska" },
85
+ { "code": "NV", "name": "Nevada" },
86
+ { "code": "NH", "name": "New Hampshire" },
87
+ { "code": "NJ", "name": "New Jersey" },
88
+ { "code": "NM", "name": "New Mexico" },
89
+ { "code": "NY", "name": "New York" },
90
+ { "code": "NC", "name": "North Carolina" },
91
+ { "code": "ND", "name": "North Dakota" },
92
+ { "code": "OH", "name": "Ohio" },
93
+ { "code": "OK", "name": "Oklahoma" },
94
+ { "code": "OR", "name": "Oregon" },
95
+ { "code": "PA", "name": "Pennsylvania" },
96
+ { "code": "RI", "name": "Rhode Island" },
97
+ { "code": "SC", "name": "South Carolina" },
98
+ { "code": "SD", "name": "South Dakota" },
99
+ { "code": "TN", "name": "Tennessee" },
100
+ { "code": "TX", "name": "Texas" },
101
+ { "code": "UT", "name": "Utah" },
102
+ { "code": "VT", "name": "Vermont" },
103
+ { "code": "VA", "name": "Virginia" },
104
+ { "code": "WA", "name": "Washington" },
105
+ { "code": "WV", "name": "West Virginia" },
106
+ { "code": "WI", "name": "Wisconsin" },
107
+ { "code": "WY", "name": "Wyoming" },
108
+ ];
109
+
110
+ const COUNTRIES_LIST = [
111
+ { "name": "Afghanistan", "code": "AF" },
112
+ { "name": "Albania", "code": "AL" },
113
+ { "name": "Algeria", "code": "DZ" },
114
+ { "name": "Andorra", "code": "AD" },
115
+ { "name": "Angola", "code": "AO" },
116
+ { "name": "Antigua and Barbuda", "code": "AG" },
117
+ { "name": "Argentina", "code": "AR" },
118
+ { "name": "Armenia", "code": "AM" },
119
+ { "name": "Australia", "code": "AU" },
120
+ { "name": "Austria", "code": "AT" },
121
+ { "name": "Azerbaijan", "code": "AZ" },
122
+ { "name": "Bahamas", "code": "BS" },
123
+ { "name": "Bahrain", "code": "BH" },
124
+ { "name": "Bangladesh", "code": "BD" },
125
+ { "name": "Barbados", "code": "BB" },
126
+ { "name": "Belarus", "code": "BY" },
127
+ { "name": "Belgium", "code": "BE" },
128
+ { "name": "Belize", "code": "BZ" },
129
+ { "name": "Benin", "code": "BJ" },
130
+ { "name": "Bhutan", "code": "BT" },
131
+ { "name": "Bolivia", "code": "BO" },
132
+ { "name": "Bosnia and Herzegovina", "code": "BA" },
133
+ { "name": "Botswana", "code": "BW" },
134
+ { "name": "Brazil", "code": "BR" },
135
+ { "name": "Brunei", "code": "BN" },
136
+ { "name": "Bulgaria", "code": "BG" },
137
+ { "name": "Burkina Faso", "code": "BF" },
138
+ { "name": "Burundi", "code": "BI" },
139
+ { "name": "Cambodia", "code": "KH" },
140
+ { "name": "Cameroon", "code": "CM" },
141
+ { "name": "Canada", "code": "CA" },
142
+ { "name": "Cape Verde", "code": "CV" },
143
+ { "name": "Central African Republic", "code": "CF" },
144
+ { "name": "Chad", "code": "TD" },
145
+ { "name": "Chile", "code": "CL" },
146
+ { "name": "China", "code": "CN" },
147
+ { "name": "Colombia", "code": "CO" },
148
+ { "name": "Comoros", "code": "KM" },
149
+ { "name": "Congo", "code": "CG" },
150
+ { "name": "Costa Rica", "code": "CR" },
151
+ { "name": "Croatia", "code": "HR" },
152
+ { "name": "Cuba", "code": "CU" },
153
+ { "name": "Cyprus", "code": "CY" },
154
+ { "name": "Czech Republic", "code": "CZ" },
155
+ { "name": "Denmark", "code": "DK" },
156
+ { "name": "Djibouti", "code": "DJ" },
157
+ { "name": "Dominica", "code": "DM" },
158
+ { "name": "Dominican Republic", "code": "DO" },
159
+ { "name": "Ecuador", "code": "EC" },
160
+ { "name": "Egypt", "code": "EG" },
161
+ { "name": "El Salvador", "code": "SV" },
162
+ { "name": "Equatorial Guinea", "code": "GQ" },
163
+ { "name": "Eritrea", "code": "ER" },
164
+ { "name": "Estonia", "code": "EE" },
165
+ { "name": "Ethiopia", "code": "ET" },
166
+ { "name": "Fiji", "code": "FJ" },
167
+ { "name": "Finland", "code": "FI" },
168
+ { "name": "France", "code": "FR" },
169
+ { "name": "Gabon", "code": "GA" },
170
+ { "name": "Gambia", "code": "GM" },
171
+ { "name": "Georgia", "code": "GE" },
172
+ { "name": "Germany", "code": "DE" },
173
+ { "name": "Ghana", "code": "GH" },
174
+ { "name": "Greece", "code": "GR" },
175
+ { "name": "Grenada", "code": "GD" },
176
+ { "name": "Guatemala", "code": "GT" },
177
+ { "name": "Guinea", "code": "GN" },
178
+ { "name": "Guyana", "code": "GY" },
179
+ { "name": "Haiti", "code": "HT" },
180
+ { "name": "Honduras", "code": "HN" },
181
+ { "name": "Hungary", "code": "HU" },
182
+ { "name": "Iceland", "code": "IS" },
183
+ { "name": "India", "code": "IN" },
184
+ { "name": "Indonesia", "code": "ID" },
185
+ { "name": "Iran", "code": "IR" },
186
+ { "name": "Iraq", "code": "IQ" },
187
+ { "name": "Ireland", "code": "IE" },
188
+ { "name": "Israel", "code": "IL" },
189
+ { "name": "Italy", "code": "IT" },
190
+ { "name": "Jamaica", "code": "JM" },
191
+ { "name": "Japan", "code": "JP" },
192
+ { "name": "Jordan", "code": "JO" },
193
+ { "name": "Kazakhstan", "code": "KZ" },
194
+ { "name": "Kenya", "code": "KE" },
195
+ { "name": "Korea, South", "code": "KR" },
196
+ { "name": "Kuwait", "code": "KW" },
197
+ { "name": "Laos", "code": "LA" },
198
+ { "name": "Latvia", "code": "LV" },
199
+ { "name": "Lebanon", "code": "LB" },
200
+ { "name": "Libya", "code": "LY" },
201
+ { "name": "Lithuania", "code": "LT" },
202
+ { "name": "Luxembourg", "code": "LU" },
203
+ { "name": "Malaysia", "code": "MY" },
204
+ { "name": "Maldives", "code": "MV" },
205
+ { "name": "Mexico", "code": "MX" },
206
+ { "name": "Monaco", "code": "MC" },
207
+ { "name": "Morocco", "code": "MA" },
208
+ { "name": "Nepal", "code": "NP" },
209
+ { "name": "Netherlands", "code": "NL" },
210
+ { "name": "New Zealand", "code": "NZ" },
211
+ { "name": "Nigeria", "code": "NG" },
212
+ { "name": "Norway", "code": "NO" },
213
+ { "name": "Oman", "code": "OM" },
214
+ { "name": "Pakistan", "code": "PK" },
215
+ { "name": "Panama", "code": "PA" },
216
+ { "name": "Peru", "code": "PE" },
217
+ { "name": "Philippines", "code": "PH" },
218
+ { "name": "Poland", "code": "PL" },
219
+ { "name": "Portugal", "code": "PT" },
220
+ { "name": "Qatar", "code": "QA" },
221
+ { "name": "Romania", "code": "RO" },
222
+ { "name": "Russia", "code": "RU" },
223
+ { "name": "Saudi Arabia", "code": "SA" },
224
+ { "name": "Singapore", "code": "SG" },
225
+ { "name": "South Africa", "code": "ZA" },
226
+ { "name": "Spain", "code": "ES" },
227
+ { "name": "Sri Lanka", "code": "LK" },
228
+ { "name": "Sweden", "code": "SE" },
229
+ { "name": "Switzerland", "code": "CH" },
230
+ { "name": "Thailand", "code": "TH" },
231
+ { "name": "Turkey", "code": "TR" },
232
+ { "name": "Ukraine", "code": "UA" },
233
+ { "name": "United Arab Emirates", "code": "AE" },
234
+ { "name": "United Kingdom", "code": "GB" },
235
+ { "name": "United States", "code": "US" },
236
+ { "name": "Vietnam", "code": "VN" },
237
+ { "name": "Zimbabwe", "code": "ZW" }
238
+ ];
239
+
240
+ const DEFAULT_THEME = {
241
+ bg: "#ffffff",
242
+ text: "#111827",
243
+ primary: "#2563eb",
244
+ border: "#e5e7eb",
245
+ inputBg: "#ffffff",
246
+ placeholder: "#9ca3af"
247
+ };
248
+
249
+ const COUNTRY_PHONE_MAP = {
250
+ US: "+1",
251
+ IN: "+91"
252
+ };
253
+
254
+ function getStates(country) {
255
+ if (country === "IN") return INDIAN_STATES;
256
+ if (country === "US") return USA_STATES;
257
+ return [];
258
+ }
259
+
260
+ function loadScript(url) {
261
+ return new Promise((resolve, reject) => {
262
+ if (document.querySelector(`script[src="${url}"]`)) {
263
+ resolve();
264
+ return;
265
+ }
266
+ const script = document.createElement('script');
267
+ script.src = url;
268
+ script.onload = resolve;
269
+ script.onerror = reject;
270
+ document.head.appendChild(script);
271
+ });
272
+ }
273
+
274
+ export class SwirepayCheckout extends HTMLElement {
275
+ constructor() {
276
+ super();
277
+ this.shadow = this.attachShadow({ mode: "open" });
278
+ this.events = new EventEmitter();
279
+ this.theme = DEFAULT_THEME;
280
+ }
281
+
282
+ async connectedCallback() {
283
+ this.amount = parseInt(this.getAttribute("amount")) || 0;
284
+ this.test = this.getAttribute("mode") === "test";
285
+ this.amount = parseInt(this.getAttribute("amount")) || 0;
286
+ this.currencyCode = this.getAttribute("currencyCode");
287
+ this.apiKey = this.getAttribute("api-key");
288
+ await Promise.all([
289
+ loadScript(SDK_CONFIG.elliptic),
290
+ loadScript(SDK_CONFIG.crypto)
291
+ ]);
292
+
293
+ this.render();
294
+ }
295
+
296
+ async encryptData(data, serverPublicKey, clientPrivateKey, keyId) {
297
+ try {
298
+ const EC = window.elliptic.ec;
299
+ const ec = new EC('p256');
300
+ const publicKeyBytes = atob(serverPublicKey);
301
+ let keyDataStart = -1;
302
+ for (let i = 0; i < publicKeyBytes.length - 4; i++) {
303
+ if (publicKeyBytes.charCodeAt(i) === 0x03 &&
304
+ publicKeyBytes.charCodeAt(i + 1) === 0x42 &&
305
+ publicKeyBytes.charCodeAt(i + 2) === 0x00 &&
306
+ publicKeyBytes.charCodeAt(i + 3) === 0x04) {
307
+ keyDataStart = i + 4; // Skip the 03 42 00 04 header
308
+ break;
309
+ }
310
+ }
311
+
312
+ if (keyDataStart === -1) {
313
+ throw new Error('Could not find public key data in DER format');
314
+ }
315
+ const keyData = publicKeyBytes.slice(keyDataStart, keyDataStart + 64);
316
+
317
+ if (keyData.length !== 64) {
318
+ throw new Error(`Invalid key data length: ${keyData.length}, expected 64`);
319
+ }
320
+ const publicKeyHex = '04' + Array.from(keyData)
321
+ .map(byte => byte.charCodeAt(0).toString(16).padStart(2, '0'))
322
+ .join('');
323
+ const serverPubKey = ec.keyFromPublic(publicKeyHex, 'hex');
324
+ const sharedSecret = clientPrivateKey.derive(serverPubKey.getPublic());
325
+ const sharedSecretBytes = sharedSecret.toArray();
326
+ const sharedSecretHex = sharedSecretBytes.map(byte =>
327
+ (byte < 0 ? byte + 256 : byte).toString(16).padStart(2, '0')
328
+ ).join('');
329
+ const aesKeyHash = CryptoJS.SHA256(CryptoJS.enc.Hex.parse(sharedSecretHex));
330
+ const aesKey = aesKeyHash.toString(CryptoJS.enc.Hex);
331
+ const iv = CryptoJS.lib.WordArray.random(16);
332
+ const key = CryptoJS.enc.Hex.parse(aesKey);
333
+ const encrypted = CryptoJS.AES.encrypt(data, key, {
334
+ iv: iv,
335
+ mode: CryptoJS.mode.CBC,
336
+ padding: CryptoJS.pad.Pkcs7,
337
+ });
338
+
339
+ return {
340
+ encryptedData: encrypted.toString(),
341
+ keyId: keyId,
342
+ iv: iv.toString(CryptoJS.enc.Base64),
343
+ };
344
+ } catch (error) {
345
+ console.error('Error encrypting data:', error);
346
+ throw new Error('Failed to encrypt data');
347
+ }
348
+ }
349
+
350
+ getEndpoints() {
351
+ if (this.test) {
352
+ return { gateway: 'https://staging-backend.swirepay.com' };
353
+ }
354
+ return { gateway: 'https://api.swirepay.com' };
355
+ }
356
+
357
+ async createCustomer(payload) {
358
+ const gateway = this.getEndpoints().gateway;
359
+ let url = `${gateway}/v3/customer`;
360
+ const response = await fetch(url, {
361
+ method: 'POST',
362
+ headers: {
363
+ 'x-enc-public-key': payload?.publicKey,
364
+ 'x-enc-key-id': payload?.keyId,
365
+ 'x-enc-iv': payload?.iv,
366
+ 'Content-Type': 'application/json',
367
+ 'x-api-key': this.apiKey,
368
+ },
369
+ body: JSON.stringify({ data: payload?.data })
370
+ })
371
+ return await response.json();
372
+ }
373
+
374
+ async addPaymentMethod(payload) {
375
+ const gateway = this.getEndpoints().gateway;
376
+ let url = `${gateway}/v3/payment-method`;
377
+ const response = await fetch(url, {
378
+ method: 'POST',
379
+ headers: {
380
+ 'x-enc-public-key': payload?.publicKey,
381
+ 'x-enc-key-id': payload?.keyId,
382
+ 'x-enc-iv': payload?.iv,
383
+ 'Content-Type': 'application/json',
384
+ 'x-api-key': this.apiKey,
385
+ },
386
+ body: JSON.stringify({ data: payload?.data })
387
+ })
388
+ return await response.json();
389
+ }
390
+
391
+ async getServerEncription() {
392
+ try {
393
+ const gateway = this.getEndpoints().gateway;
394
+ const response = await fetch(`${gateway}/v1/encryption/session`, {
395
+ method: 'POST',
396
+ });
397
+ const responseData = await response.json();
398
+ return responseData?.entity;
399
+ } catch (err) {
400
+ console.error("Failed to get encryption session:", err);
401
+ throw err;
402
+ }
403
+ }
404
+
405
+ async generateClientKeyPair() {
406
+ try {
407
+ if (!window.elliptic) {
408
+ throw new Error("Elliptic failed to load");
409
+ }
410
+ const EC = window.elliptic.ec;
411
+ const ec = new EC("p256");
412
+ const keyPair = ec.genKeyPair();
413
+ const publicKey = keyPair.getPublic('hex');
414
+ const privateKey = keyPair;
415
+ return { privateKey, publicKey };
416
+ } catch (err) {
417
+ console.error("Elliptic load error:", err);
418
+ throw err;
419
+ }
420
+ }
421
+
422
+ onSuccess(cb) { this.events.on("success", cb); }
423
+ onError(cb) { this.events.on("error", cb); }
424
+
425
+ open(options = {}) {
426
+ this.theme = { ...DEFAULT_THEME, ...(options.theme || {}) };
427
+ this.render();
428
+ this.shadow.getElementById("overlay").style.display = "flex";
429
+ }
430
+
431
+ close() {
432
+ this.shadow.getElementById("overlay").style.display = "none";
433
+ }
434
+ formatCard(v) {
435
+ return v.replace(/\D/g, "").replace(/(.{4})/g, "$1 ").trim();
436
+ }
437
+
438
+ formatExpiry(v) {
439
+ return v.replace(/\D/g, "").replace(/(\d{2})(\d{1,2})/, "$1/$2");
440
+ }
441
+
442
+ formatPhone(v) {
443
+ return v.replace(/\D/g, "").slice(0, 10);
444
+ }
445
+ handleCountryChange() {
446
+ const country = this.shadow.getElementById("country").value;
447
+ const phoneCodeEl = this.shadow.getElementById("phone-code");
448
+ const stateEl = this.shadow.getElementById("state");
449
+ phoneCodeEl.value = COUNTRY_PHONE_MAP[country] || "";
450
+ const states = getStates(country);
451
+ stateEl.innerHTML = states.map(
452
+ s => `<option value="${s.code}">${s.name}</option>`
453
+ ).join("");
454
+ }
455
+
456
+ validate() {
457
+ const get = id => this.shadow.getElementById(id)?.value?.trim();
458
+
459
+ const name = get("name");
460
+ const email = get("email");
461
+ const phone = get("phone");
462
+ const code = get("phone-code");
463
+ const zip = get("zip");
464
+ const country = get("country");
465
+
466
+ const accountName = get("account-name");
467
+ const accountNumber = get("account-number");
468
+ const routingNumber = get("routing-number");
469
+ const accountType = get("account-type");
470
+ const bankName = get("bank-name");
471
+ const businessType = get("business-type");
472
+ const street = get("street");
473
+ if (!name) return "Name required";
474
+ if (!email) return "Email required";
475
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return "Invalid email address";
476
+ if (!accountName) return "Account holder name required";
477
+
478
+ if (!accountNumber || !/^\d{6,18}$/.test(accountNumber)) {
479
+ return "Invalid account number";
480
+ }
481
+
482
+ if (!routingNumber || !/^\d{9}$/.test(routingNumber)) {
483
+ return "Invalid routing number (must be 9 digits)";
484
+ }
485
+
486
+ if (!accountType) return "Select account type";
487
+
488
+ if (!bankName) return "Bank name required";
489
+
490
+ if (!businessType) return "Select business type";
491
+ if (!street) return "Street address required";
492
+ if (phone && !isValidPhoneNumber(code + phone)) {
493
+ return "Invalid phone";
494
+ }
495
+ if (zip && postalCodes.validate(country, zip) !== true) {
496
+ return "Invalid ZIP";
497
+ }
498
+
499
+ return null;
500
+ }
501
+
502
+ formatAccountNumber(v) {
503
+ return v.replace(/\D/g, "").slice(0, 18);
504
+ }
505
+
506
+ formatRouting(v) {
507
+ return v.replace(/\D/g, "").slice(0, 9);
508
+ }
509
+
510
+ async handleSubmit(e) {
511
+ e.preventDefault();
512
+
513
+ const payButton = this.shadow.getElementById("form").querySelector(".btn");
514
+ payButton.disabled = true;
515
+ payButton.textContent = "Processing...";
516
+
517
+ const err = this.validate();
518
+ if (err) {
519
+ alert(err);
520
+ payButton.disabled = false;
521
+ payButton.textContent = `Pay $${(this.amount / 100).toFixed(2)}`;
522
+ return;
523
+ }
524
+
525
+ try {
526
+ const values = await this.getServerEncription();
527
+ if (values?.public_key) {
528
+ const keys = await this.generateClientKeyPair();
529
+ if (keys?.privateKey) {
530
+ const getVal = id => this.shadow.getElementById(id)?.value?.trim();
531
+
532
+ const phoneCode = getVal("phone-code") || "";
533
+ const phone = getVal("phone");
534
+
535
+ const bankDetails = {
536
+ name: getVal("account-name"),
537
+ accountNumber: getVal("account-number"),
538
+ routingNumber: getVal("routing-number"),
539
+ accountType: getVal("account-type"),
540
+ bankName: getVal("bank-name"),
541
+ businessType: getVal("business-type"),
542
+ };
543
+
544
+ const customer = {
545
+ name: getVal("name"),
546
+ email: getVal("email"),
547
+ phone: phone ? `${phoneCode}${phone}` : null
548
+ };
549
+
550
+ const address = {
551
+ street: getVal("street"),
552
+ city: getVal("city"),
553
+ state: getVal("state"),
554
+ countryCode: getVal("country"),
555
+ postalCode: getVal("zip")
556
+ };
557
+
558
+ const encriptedData = await this.encryptData(JSON.stringify(customer), values.public_key, keys.privateKey, values.key_id);
559
+ const payload = {
560
+ publicKey: keys?.publicKey,
561
+ keyId: encriptedData?.keyId,
562
+ iv: encriptedData?.iv,
563
+ data: encriptedData?.encryptedData,
564
+ }
565
+ const newCustomer = await this.createCustomer(payload);
566
+ const customerGid = newCustomer.entity?.gid;
567
+
568
+ const pmPayload = {
569
+ type: 'ACH_LEGACY',
570
+ bank: bankDetails,
571
+ postalCode: address?.zip,
572
+ paymentMethodBillingAddress: address,
573
+ customerGid: customerGid
574
+ };
575
+
576
+ const encriptedData1 = await this.encryptData(JSON.stringify(pmPayload), values.public_key, keys.privateKey, values.key_id);
577
+ const payload1 = {
578
+ publicKey: keys?.publicKey,
579
+ keyId: encriptedData1?.keyId,
580
+ iv: encriptedData1?.iv,
581
+ data: encriptedData1?.encryptedData,
582
+ }
583
+
584
+ const result = await this.addPaymentMethod(payload1);
585
+ const gid = result?.entity?.gid;
586
+
587
+ const sessionData = {
588
+ amount: this.amount,
589
+ currencyCode: this.currencyCode,
590
+ paymentMethodGid: gid,
591
+ paymentMethodType: ['ACH_LEGACY'],
592
+ statementDescriptor: 'ACH Payment',
593
+ confirmMethod: 'AUTOMATIC',
594
+ captureMethod: 'AUTOMATIC',
595
+ };
596
+ const encriptedData4 = await this.encryptData(JSON.stringify(sessionData), values.public_key, keys.privateKey, values.key_id);
597
+ const payload2 = {
598
+ publicKey: keys?.publicKey,
599
+ keyId: encriptedData4?.keyId,
600
+ iv: encriptedData4?.iv,
601
+ data: encriptedData4?.encryptedData,
602
+ }
603
+ const gateway = this.getEndpoints().gateway;
604
+ const url = `${gateway}/v3/payment-session`;
605
+ const res = await fetch(url, {
606
+ method: 'POST',
607
+ headers: {
608
+ 'x-enc-public-key': payload2?.publicKey,
609
+ 'x-enc-key-id': payload2?.keyId,
610
+ 'x-enc-iv': payload2?.iv,
611
+ 'Content-Type': 'application/json',
612
+ 'x-api-key': this.apiKey,
613
+ },
614
+ body: JSON.stringify({ data: payload2?.data })
615
+ });
616
+ const resData = await res.json();
617
+ if (resData?.entity?.errorCode === null){
618
+ this.events.emit("success", resData);
619
+ this.close();
620
+ } else if (resData?.entity?.errorCode !== null){
621
+ console.error("Payment Error:", resData?.entity?.errorDescription);
622
+ this.events.emit("error", resData?.entity?.errorDescription);
623
+ alert(resData?.entity?.errorDescription || "Payment failed");
624
+ }
625
+ }
626
+ }
627
+ } catch (error) {
628
+ console.error("Payment Error:", error);
629
+ this.events.emit("error", error);
630
+ alert(error.message || "Payment failed");
631
+ } finally {
632
+ payButton.disabled = false;
633
+ payButton.textContent = `Pay $${(this.amount / 100).toFixed(2)}`;
634
+ }
635
+ }
636
+ render() {
637
+ const t = this.theme;
638
+ const amountText = (this.amount / 100).toFixed(2);
639
+ this.shadow.innerHTML = `
640
+ <style>
641
+ :host {
642
+ --bg:${t.bg};
643
+ --text:${t.text};
644
+ --primary:${t.primary};
645
+ --border:${t.border};
646
+ --input-bg:${t.inputBg};
647
+ --placeholder:${t.placeholder};
648
+ color: var(--text);
649
+ font-family: sans-serif;
650
+ }
651
+
652
+ .overlay {
653
+ display:none;
654
+ position:fixed;
655
+ inset:0;
656
+ background:rgba(0,0,0,0.6);
657
+ justify-content:center;
658
+ align-items:center;
659
+ }
660
+
661
+ .modal {
662
+ width:420px;
663
+ background:var(--bg);
664
+ color:var(--text);
665
+ border-radius:16px;
666
+ padding:20px;
667
+ font-family:sans-serif;
668
+ }
669
+
670
+ input, select {
671
+ width:100%;
672
+ padding:10px;
673
+ margin:6px 0;
674
+ border:1px solid var(--border);
675
+ background: white;
676
+ color: black;
677
+ border-radius:8px;
678
+ outline:none;
679
+ }
680
+
681
+ input::placeholder {
682
+ color: var(--placeholder);
683
+ }
684
+ select {
685
+ color: var(--text);
686
+ }
687
+
688
+ input {
689
+ background: #ffffff;
690
+ color: #000000;
691
+ }
692
+
693
+ input::placeholder {
694
+ color: #9ca3af !important;
695
+ opacity: 1 !important;
696
+ }
697
+
698
+ select {
699
+ width:100%;
700
+ padding:10px;
701
+ margin:6px 0;
702
+ border:1px solid var(--border);
703
+ border-radius:8px;
704
+
705
+ background-color: #ffffff !important;
706
+ color: #000000 !important;
707
+
708
+ -webkit-text-fill-color: #000000; /* Chrome fix */
709
+ appearance: none; /* removes default weird styling */
710
+ }
711
+
712
+ select option {
713
+ background: #ffffff;
714
+ color: #000000;
715
+ }
716
+
717
+ .row { display:flex; gap:10px; }
718
+
719
+ .phone-code { width:70px; }
720
+
721
+ .btn {
722
+ background:var(--primary);
723
+ color:#fff;
724
+ padding:12px;
725
+ border:none;
726
+ border-radius:10px;
727
+ cursor:pointer;
728
+ width:100%;
729
+ }
730
+
731
+ .close {
732
+ float:right;
733
+ cursor:pointer;
734
+ }
735
+ </style>
736
+
737
+ <div class="overlay" id="overlay">
738
+ <div class="modal">
739
+ <span class="close" id="close">✕</span>
740
+
741
+ <h3>Pay by Bank</h3>
742
+
743
+ <form id="form">
744
+ <input id="name" placeholder="Name"/>
745
+ <input id="email" placeholder="Email"/>
746
+
747
+ <div class="row">
748
+ <input class="phone-code" id="phone-code" value="+1"/>
749
+ <input id="phone" placeholder="Phone"/>
750
+ </div>
751
+
752
+ <input id="account-name" placeholder="Account Holder Name"/>
753
+ <input id="account-number" placeholder="Account Number"/>
754
+ <input id="routing-number" placeholder="Routing Number"/>
755
+
756
+ <select id="account-type">
757
+ <option value="">Account Type</option>
758
+ <option value="CURRENT">Current</option>
759
+ <option value="SAVINGS">Savings</option>
760
+ </select>
761
+
762
+ <input id="bank-name" placeholder="Bank Name"/>
763
+
764
+ <select id="business-type">
765
+ <option value="">Business Type</option>
766
+ <option value="INDIVIDUAL">Individual</option>
767
+ <option value="BUSINESS">Business</option>
768
+ </select>
769
+
770
+ <input id="street" placeholder="Street Address"/>
771
+ <input id="city" placeholder="City"/>
772
+
773
+ <!-- STATE FIRST -->
774
+ <select id="state"></select>
775
+
776
+ <!-- COUNTRY AFTER -->
777
+ <select id="country">
778
+ ${COUNTRIES_LIST.map(c =>
779
+ `<option value="${c.code}" ${c.code === "US" ? "selected" : ""}>
780
+ ${c.name}
781
+ </option>`
782
+ ).join("")}
783
+ </select>
784
+
785
+ <input id="zip" placeholder="ZIP Code"/>
786
+
787
+ <button class="btn">Pay $${amountText}</button>
788
+ </form>
789
+ </div>
790
+ </div>
791
+ `;
792
+
793
+ const $ = id => this.shadow.getElementById(id);
794
+
795
+ $("close").onclick = () => this.close();
796
+ $("form").onsubmit = e => this.handleSubmit(e);
797
+
798
+ $("country").onchange = () => this.handleCountryChange();
799
+
800
+ const countryEl = $("country");
801
+ countryEl.value = "US";
802
+ this.handleCountryChange();
803
+ $("account-number").oninput = e => e.target.value = this.formatAccountNumber(e.target.value);
804
+ $("routing-number").oninput = e => e.target.value = this.formatRouting(e.target.value);
805
+ $("phone").oninput = e => e.target.value = this.formatPhone(e.target.value);
806
+ }
807
+ }
808
+ if (!customElements.get("swirepay-ach-checkout")) {
809
+ customElements.define("swirepay-ach-checkout", SwirepayCheckout);
810
+ }