@swirepay-developer/swirepay-card-sdk 2.0.0 → 2.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swirepay-developer/swirepay-card-sdk",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Swirepay Card Payment SDK (Web Component with Modal UI)",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -108,133 +108,8 @@ const USA_STATES = [
108
108
  ];
109
109
 
110
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
111
  { "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
112
  { "name": "United States", "code": "US" },
236
- { "name": "Vietnam", "code": "VN" },
237
- { "name": "Zimbabwe", "code": "ZW" }
238
113
  ];
239
114
 
240
115
  const DEFAULT_THEME = {
@@ -279,20 +154,120 @@ export class SwirepayCheckout extends HTMLElement {
279
154
  this.theme = DEFAULT_THEME;
280
155
  }
281
156
 
157
+ getEndpoints() {
158
+ if (this.test) {
159
+ return { gateway: 'https://staging-backend.swirepay.com' };
160
+ }
161
+ return { gateway: 'https://api.swirepay.com' };
162
+ }
163
+
164
+ async getSplitUpDetails(inventoryOrder) {
165
+ const payload = {
166
+ shopGid: inventoryOrder?.shopGid,
167
+ items: inventoryOrder?.inventoryOrderLineItems,
168
+ tipType: "AMOUNT",
169
+ tip: inventoryOrder?.totalTip
170
+ }
171
+ const gateway = this.getEndpoints().gateway;
172
+ let url = `${gateway}/v1/inventory/order/inventory-order-split-up`;
173
+ const response = await fetch(url, {
174
+ method: 'POST',
175
+ headers: {
176
+ 'Content-Type': 'application/json',
177
+ 'x-api-key': this.apiKey,
178
+ },
179
+ body: JSON.stringify(payload)
180
+ })
181
+ return await response.json();
182
+ }
183
+
184
+ async updateFromAttributes() {
185
+ try {
186
+ this.amount = parseInt(this.getAttribute("amount")) || 0;
187
+ this.test = this.getAttribute("mode") === "test";
188
+ this.currencyCode = this.getAttribute("currencycode");
189
+ this.apiKey = this.getAttribute("api-key");
190
+ this.isAddressRequired = this.getAttribute("isaddressrequired") === "true";
191
+ this.inventory = this.getAttribute("inventory") === "true";
192
+ this.planGid = this.getAttribute("plangid");
193
+ this.productName = this.getAttribute("productname");
194
+ this.frequency = this.getAttribute("frequency");
195
+ this.description = this.getAttribute("description");
196
+ this.totalPayments = this.getAttribute("totalpayments");
197
+ const customerAttr = this.getAttribute("customer");
198
+ this.customer = customerAttr ? JSON.parse(customerAttr) : {};
199
+
200
+ const inventoryOrders = this.getAttribute("inventoryorders");
201
+ this.inventoryOrder = inventoryOrders ? JSON.parse(inventoryOrders) : {};
202
+ if (this.inventory && this.inventoryOrder) {
203
+ const splitUpInfo = await this.getSplitUpDetails(this.inventoryOrder);
204
+ this.splitUp = splitUpInfo?.entity;
205
+ }
206
+
207
+ } catch (e) {
208
+ console.error("Attribute parsing error:", e);
209
+ }
210
+ }
211
+
282
212
  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");
213
+ await this.updateFromAttributes();
214
+
288
215
  await Promise.all([
289
216
  loadScript(SDK_CONFIG.elliptic),
290
- loadScript(SDK_CONFIG.crypto)
217
+ loadScript(SDK_CONFIG.crypto),
218
+ loadScript(SDK_CONFIG.plaid)
291
219
  ]);
292
220
 
293
221
  this.render();
294
222
  }
295
223
 
224
+ static get observedAttributes() {
225
+ return [
226
+ "amount",
227
+ "mode",
228
+ "currencycode",
229
+ "api-key",
230
+ "isaddressrequired",
231
+ "inventory",
232
+ "plangid",
233
+ "productname",
234
+ "frequency",
235
+ "description",
236
+ "totalpayments",
237
+ "customer",
238
+ "inventoryorders"
239
+ ];
240
+ }
241
+
242
+ async attributeChangedCallback(name, oldValue, newValue) {
243
+ if (oldValue === newValue) return;
244
+
245
+ await this.updateFromAttributes();
246
+
247
+ if (this.shadowRoot) {
248
+ this.render();
249
+ }
250
+ }
251
+
252
+ setError(id, message) {
253
+ const input = this.shadow.getElementById(id);
254
+ const errorEl = this.shadow.getElementById(`error-${id}`);
255
+
256
+ if (errorEl) errorEl.textContent = message || "";
257
+
258
+ if (input) {
259
+ if (message) input.classList.add("input-error");
260
+ else input.classList.remove("input-error");
261
+ }
262
+ }
263
+
264
+ clearErrors() {
265
+ this.shadow.querySelectorAll(".error").forEach(e => e.textContent = "");
266
+ this.shadow.querySelectorAll("input, select").forEach(el => {
267
+ el.classList.remove("input-error");
268
+ });
269
+ }
270
+
296
271
  async encryptData(data, serverPublicKey, clientPrivateKey, keyId) {
297
272
  try {
298
273
  const EC = window.elliptic.ec;
@@ -388,6 +363,108 @@ export class SwirepayCheckout extends HTMLElement {
388
363
  return await response.json();
389
364
  }
390
365
 
366
+ async createOrder() {
367
+ const gateway = this.getEndpoints().gateway;
368
+ let url = `${gateway}/v1/inventory/order`;
369
+ const response = await fetch(url, {
370
+ method: 'POST',
371
+ headers: {
372
+ 'Content-Type': 'application/json',
373
+ 'x-api-key': this.apiKey,
374
+ },
375
+ body: JSON.stringify(this.inventoryOrder)
376
+ })
377
+ return await response.json();
378
+ }
379
+
380
+ async getSubScription(payload) {
381
+ const gateway = this.getEndpoints().gateway;
382
+ let url = `${gateway}/v2/subscription`;
383
+ const response = await fetch(url, {
384
+ method: 'POST',
385
+ headers: {
386
+ 'Content-Type': 'application/json',
387
+ 'x-api-key': this.apiKey,
388
+ },
389
+ body: JSON.stringify(payload)
390
+ })
391
+ return await response.json();
392
+ }
393
+
394
+ async getPlan() {
395
+ const gateway = this.getEndpoints().gateway;
396
+ let url = `${gateway}/v2/plan?isInternal.EQ=false&name.EQ=Recurring%20Plan`;
397
+ const response = await fetch(url, {
398
+ method: 'GET',
399
+ headers: {
400
+ 'Content-Type': 'application/json',
401
+ 'x-api-key': this.apiKey,
402
+ }
403
+ })
404
+ return await response.json();
405
+ }
406
+
407
+ async addedPlanInfo() {
408
+ const payload = {
409
+ "currencyCode": this.currencyCode,
410
+ "name": "Recurring Plan",
411
+ "description": "Recurring Plan",
412
+ "note": "Recurring Plan",
413
+ "billingFrequency": this.frequency,
414
+ "billingPeriod": 1
415
+ }
416
+ const gateway = this.getEndpoints().gateway;
417
+ let url = `${gateway}/v2/plan`;
418
+ const response = await fetch(url, {
419
+ method: 'POST',
420
+ headers: {
421
+ 'Content-Type': 'application/json',
422
+ 'x-api-key': this.apiKey,
423
+ },
424
+ body: JSON.stringify(payload)
425
+ })
426
+ return await response.json();
427
+ }
428
+
429
+ async getItemInfo() {
430
+ const gateway = this.getEndpoints().gateway;
431
+ let url = `${gateway}/v1/inventory/item?name.EQ=Recurring%20item&priceType.EQ=VARIABLE&isRecurring.EQ=true`;
432
+ const response = await fetch(url, {
433
+ method: 'GET',
434
+ headers: {
435
+ 'Content-Type': 'application/json',
436
+ 'x-api-key': this.apiKey,
437
+ },
438
+ })
439
+ return await response.json();
440
+ }
441
+
442
+ async addedItemInfo() {
443
+ const payload = {
444
+ "name": "Recurring item",
445
+ "alternateName": "Recurring item",
446
+ "onlineName": "Recurring item",
447
+ "onlineEnabled": true,
448
+ "price": 100,
449
+ "priceType": "VARIABLE",
450
+ "includeTax": true,
451
+ "available": true,
452
+ "recurring": true,
453
+ "onKiosk": true
454
+ }
455
+ const gateway = this.getEndpoints().gateway;
456
+ let url = `${gateway}/v1/inventory/item`;
457
+ const response = await fetch(url, {
458
+ method: 'POST',
459
+ headers: {
460
+ 'Content-Type': 'application/json',
461
+ 'x-api-key': this.apiKey,
462
+ },
463
+ body: JSON.stringify(payload)
464
+ })
465
+ return await response.json();
466
+ }
467
+
391
468
  async getServerEncription() {
392
469
  try {
393
470
  const gateway = this.getEndpoints().gateway;
@@ -443,9 +520,11 @@ export class SwirepayCheckout extends HTMLElement {
443
520
  return v.replace(/\D/g, "").slice(0, 10);
444
521
  }
445
522
  handleCountryChange() {
446
- const country = this.shadow.getElementById("country").value;
523
+ const countryEl = this.shadow.getElementById("country");
447
524
  const phoneCodeEl = this.shadow.getElementById("phone-code");
448
525
  const stateEl = this.shadow.getElementById("state");
526
+ if (!countryEl || !phoneCodeEl || !stateEl) return;
527
+ const country = countryEl.value;
449
528
  phoneCodeEl.value = COUNTRY_PHONE_MAP[country] || "";
450
529
  const states = getStates(country);
451
530
  stateEl.innerHTML = states.map(
@@ -453,77 +532,173 @@ export class SwirepayCheckout extends HTMLElement {
453
532
  ).join("");
454
533
  }
455
534
 
535
+ detectCardType(number) {
536
+ const n = number.replace(/\s/g, "");
537
+
538
+ if (/^4/.test(n)) return "VISA";
539
+ if (/^5[1-5]/.test(n)) return "MASTERCARD";
540
+ if (/^3[47]/.test(n)) return "AMEX";
541
+ if (/^6(?:011|5)/.test(n)) return "DISCOVER";
542
+
543
+ return "";
544
+ }
545
+
546
+ getAddressFields() {
547
+ if (!this.isAddressRequired) return "";
548
+
549
+ return `
550
+ <div class="section">
551
+ <div class="section-title">Address</div>
552
+
553
+ <div class="field">
554
+ <input id="street" required placeholder=" " />
555
+ <label>Street *</label>
556
+ </div>
557
+ <div class="error" id="error-street"></div>
558
+
559
+ <div class="field">
560
+ <input id="city" required placeholder=" " />
561
+ <label>City *</label>
562
+ </div>
563
+ <div class="error" id="error-city"></div>
564
+
565
+ <div class="field">
566
+ <select id="state"></select>
567
+ <label class="floating">State *</label>
568
+ </div>
569
+ <div class="error" id="error-state"></div>
570
+
571
+ <div class="field">
572
+ <select id="country" required>
573
+ ${COUNTRIES_LIST.map(c =>
574
+ `<option value="${c.code}" ${c.code === "US" ? "selected" : ""}>${c.name}</option>`
575
+ ).join("")}
576
+ </select>
577
+ <label class="floating">Country *</label>
578
+ </div>
579
+ <div class="error" id="error-country"></div>
580
+
581
+ <div class="field">
582
+ <input id="zip" required placeholder=" " />
583
+ <label>ZIP Code *</label>
584
+ </div>
585
+ <div class="error" id="error-zip"></div>
586
+
587
+ </div>
588
+ `;
589
+ }
590
+
456
591
  validate() {
592
+ this.clearErrors();
593
+ let isValid = true;
594
+
457
595
  const get = id => this.shadow.getElementById(id)?.value?.trim();
458
596
 
459
- const name = get("name");
597
+ if (!get("name")) {
598
+ this.setError("name", "Name required");
599
+ isValid = false;
600
+ }
601
+
460
602
  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");
603
+ if (!email) {
604
+ this.setError("email", "Email required");
605
+ isValid = false;
606
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
607
+ this.setError("email", "Invalid email");
608
+ isValid = false;
609
+ }
610
+
611
+ if (!get("card-name")) {
612
+ this.setError("card-name", "Card holder name required");
613
+ isValid = false;
614
+ }
465
615
 
466
- const cardName = get("card-name");
467
616
  const cardNumber = get("card")?.replace(/\s/g, "");
468
- const expiry = get("expiry");
469
- const cvv = get("cvv");
470
- const street = get("street");
471
- if (!name) return "Name required";
472
- if (!email) return "Email required";
473
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return "Invalid email address";
474
- if (!cardName) return "Card holder name required";
475
617
  if (!cardNumber || !/^\d{12,19}$/.test(cardNumber)) {
476
- return "Invalid card number";
618
+ this.setError("card", "Invalid card number");
619
+ isValid = false;
477
620
  }
621
+
622
+ const expiry = get("expiry");
478
623
  if (!expiry || !/^\d{2}\/\d{2}$/.test(expiry)) {
479
- return "Invalid expiry format (MM/YY)";
624
+ this.setError("expiry", "Invalid expiry");
625
+ isValid = false;
480
626
  } else {
481
627
  const [mm, yy] = expiry.split("/").map(Number);
482
- if (mm < 1 || mm > 12) return "Invalid expiry month";
483
628
  const now = new Date();
484
629
  const currentYear = now.getFullYear() % 100;
485
630
  const currentMonth = now.getMonth() + 1;
486
631
 
487
- if (yy < currentYear || (yy === currentYear && mm < currentMonth)) {
488
- return "Card expired";
632
+ if (mm < 1 || mm > 12) {
633
+ this.setError("expiry", "Invalid month");
634
+ isValid = false;
635
+ } else if (yy < currentYear || (yy === currentYear && mm < currentMonth)) {
636
+ this.setError("expiry", "Card expired");
637
+ isValid = false;
489
638
  }
490
639
  }
640
+
641
+ const cvv = get("cvv");
491
642
  if (!cvv || !/^\d{3,4}$/.test(cvv)) {
492
- return "Invalid CVV";
493
- }
494
- if (!street) return "Street address required";
495
- if (phone && !isValidPhoneNumber(code + phone)) {
496
- return "Invalid phone";
643
+ this.setError("cvv", "Invalid CVV");
644
+ isValid = false;
497
645
  }
498
- if (zip && postalCodes.validate(country, zip) !== true) {
499
- return "Invalid ZIP";
646
+
647
+ const phone = get("phone")?.replace(/\D/g, "");
648
+ const code = get("phone-code");
649
+ if (!phone || !code) {
650
+ this.setError("phone", "Phone number required");
651
+ isValid = false;
652
+ } else {
653
+ const fullNumber = `${code}${phone}`;
654
+ if (!isValidPhoneNumber(fullNumber)) {
655
+ this.setError("phone", "Invalid phone number");
656
+ isValid = false;
657
+ }
500
658
  }
501
659
 
502
- return null;
503
- }
660
+ if (this.isAddressRequired) {
661
+ if (!get("street")) {
662
+ this.setError("street", "Street required");
663
+ isValid = false;
664
+ }
504
665
 
505
- async handleSubmit(e) {
506
- e.preventDefault();
666
+ if (!get("city")) {
667
+ this.setError("city", "City required");
668
+ isValid = false;
669
+ }
507
670
 
508
- const payButton = this.shadow.getElementById("form").querySelector(".btn");
509
- payButton.disabled = true;
510
- payButton.textContent = "Processing...";
671
+ if (!get("state")) {
672
+ this.setError("state", "State required");
673
+ isValid = false;
674
+ }
511
675
 
512
- const err = this.validate();
513
- if (err) {
514
- alert(err);
515
- payButton.disabled = false;
516
- payButton.textContent = `Pay $${(this.amount / 100).toFixed(2)}`;
517
- return;
676
+ if (!get("country")) {
677
+ this.setError("country", "Country required");
678
+ isValid = false;
679
+ }
680
+
681
+ const zip = get("zip");
682
+
683
+ if (!zip) {
684
+ this.setError("zip", "ZIP required");
685
+ isValid = false;
686
+ } else if (postalCodes.validate(get("country"), zip) !== true) {
687
+ this.setError("zip", "Invalid ZIP code");
688
+ isValid = false;
689
+ }
518
690
  }
519
691
 
692
+ return isValid;
693
+ }
694
+
695
+ async oneTimePayment() {
520
696
  try {
521
697
  const values = await this.getServerEncription();
522
698
  if (values?.public_key) {
523
699
  const keys = await this.generateClientKeyPair();
524
700
  if (keys?.privateKey) {
525
701
  const getVal = id => this.shadow.getElementById(id)?.value?.trim();
526
-
527
702
  const phoneCode = getVal("phone-code") || "";
528
703
  const phone = getVal("phone");
529
704
 
@@ -538,13 +713,17 @@ export class SwirepayCheckout extends HTMLElement {
538
713
  phone: phone ? `${phoneCode}${phone}` : null
539
714
  };
540
715
 
541
- const address = {
542
- street: getVal("street"),
543
- city: getVal("city"),
544
- state: getVal("state"),
545
- countryCode: getVal("country"),
546
- postalCode: getVal("zip")
547
- };
716
+ let address = null;
717
+
718
+ if (this.isAddressRequired) {
719
+ address = {
720
+ street: getVal("street"),
721
+ city: getVal("city"),
722
+ state: getVal("state"),
723
+ countryCode: getVal("country"),
724
+ postalCode: getVal("zip")
725
+ };
726
+ }
548
727
 
549
728
  const encriptedData = await this.encryptData(JSON.stringify(customer), values.public_key, keys.privateKey, values.key_id);
550
729
  const payload = {
@@ -565,7 +744,7 @@ export class SwirepayCheckout extends HTMLElement {
565
744
  cvv: cvv,
566
745
  name: cardHolderName
567
746
  },
568
- postalCode: address?.zip,
747
+ postalCode: address?.postalCode,
569
748
  paymentMethodBillingAddress: address,
570
749
  customerGid: customerGid
571
750
  };
@@ -611,20 +790,282 @@ export class SwirepayCheckout extends HTMLElement {
611
790
  body: JSON.stringify({ data: payload2?.data })
612
791
  });
613
792
  const resData = await res.json();
614
- if (resData?.entity?.errorCode === null){
615
- this.events.emit("success", resData);
616
- this.close();
617
- } else if (resData?.entity?.errorCode !== null){
618
- console.error("Payment Error:", resData?.entity?.errorDescription);
619
- this.events.emit("error", resData?.entity?.errorDescription);
620
- alert(resData?.entity?.errorDescription || "Payment failed");
621
- }
793
+ return resData;
794
+ }
795
+ }
796
+ } catch (error) {
797
+ console.error("Payment Error:", error);
798
+ this.events.emit("error", error);
799
+ }
800
+ }
801
+
802
+ async recurringPayment() {
803
+ try {
804
+ const values = await this.getServerEncription();
805
+ if (values?.public_key) {
806
+ const keys = await this.generateClientKeyPair();
807
+ if (keys?.privateKey) {
808
+ const getVal = id => this.shadow.getElementById(id)?.value?.trim();
809
+ const phoneCode = getVal("phone-code") || "";
810
+ const phone = getVal("phone");
811
+
812
+ const cardNumber = getVal("card")?.replace(/\s/g, "");
813
+ const expiry = getVal("expiry")?.split("/") || [];
814
+ const cvv = getVal("cvv");
815
+ const cardHolderName = getVal("card-name");
816
+
817
+ const customer = {
818
+ name: getVal("name"),
819
+ email: getVal("email"),
820
+ phone: phone ? `${phoneCode}${phone}` : null
821
+ };
822
+
823
+ let address = null;
824
+
825
+ if (this.isAddressRequired) {
826
+ address = {
827
+ street: getVal("street"),
828
+ city: getVal("city"),
829
+ state: getVal("state"),
830
+ countryCode: getVal("country"),
831
+ postalCode: getVal("zip")
832
+ };
833
+ }
834
+
835
+ const encriptedData = await this.encryptData(JSON.stringify(customer), values.public_key, keys.privateKey, values.key_id);
836
+ const payload = {
837
+ publicKey: keys?.publicKey,
838
+ keyId: encriptedData?.keyId,
839
+ iv: encriptedData?.iv,
840
+ data: encriptedData?.encryptedData,
841
+ }
842
+ const newCustomer = await this.createCustomer(payload);
843
+ const customerGid = newCustomer.entity?.gid;
844
+ let product;
845
+ const itemInfo = await this.getItemInfo();
846
+ if (itemInfo?.entity?.content[0]?.gid) {
847
+ product = itemInfo?.entity?.content[0];
848
+ } else {
849
+ const newItemInfo = await this.addedItemInfo();
850
+ product = newItemInfo?.entity;
851
+ }
852
+ let plan;
853
+ const planInfo = await this.getPlan();
854
+ if (planInfo?.entity?.content[0]?.gid) {
855
+ plan = planInfo?.entity?.content[0];
856
+ } else {
857
+ const newPlanInfo = await this.addedPlanInfo();
858
+ plan = newPlanInfo?.entity;
859
+ }
860
+
861
+ const subscriptionData = {
862
+ currencyCode: this.currencyCode,
863
+ customerGid: customerGid,
864
+ plan2Gid: plan?.gid,
865
+ plan2BillingPeriod: 1,
866
+ plan2BillingFrequency: this.frequency,
867
+ plan2StartDate: new Date().toISOString(),
868
+ plan2TotalPayments: this.totalPayments,
869
+ description: this.description,
870
+ notificationType: "ALL",
871
+ emailRecipientList: [
872
+ customer.email
873
+ ],
874
+ canCustomerCancelOrPause: true,
875
+ subscription2LineItems: [
876
+ {
877
+ itemGid: product?.gid,
878
+ quantity: 1,
879
+ upfront: false,
880
+ currencyCode: this.currencyCode,
881
+ amount: this.amount
882
+ }
883
+ ]
884
+ }
885
+ const subscriptionDetails = await this.getSubScription(subscriptionData);
886
+ const subscription = subscriptionDetails?.entity;
887
+ const sessionData = {
888
+ link: subscription?.subscription2Links[0]?.link,
889
+ customer: subscription?.customer,
890
+ type: 'CARD',
891
+ card: {
892
+ number: cardNumber,
893
+ expiryMonth: parseInt(expiry[0]),
894
+ expiryYear: 2000 + parseInt(expiry[1]),
895
+ cvv: cvv,
896
+ name: cardHolderName
897
+ },
898
+ postalCode: address?.postalCode,
899
+ paymentMethodBillingAddress: address
900
+ }
901
+ const encriptedData4 = await this.encryptData(JSON.stringify(sessionData), values.public_key, keys.privateKey, values.key_id);
902
+ const payload2 = {
903
+ publicKey: keys?.publicKey,
904
+ keyId: encriptedData4?.keyId,
905
+ iv: encriptedData4?.iv,
906
+ data: encriptedData4?.encryptedData,
907
+ }
908
+ const gateway = this.getEndpoints().gateway;
909
+ const url = `${gateway}/v2/subscription-link/${subscription?.subscription2Links[0]?.gid}/payment-method`;
910
+ const res = await fetch(url, {
911
+ method: 'POST',
912
+ headers: {
913
+ 'x-enc-public-key': payload2?.publicKey,
914
+ 'x-enc-key-id': payload2?.keyId,
915
+ 'x-enc-iv': payload2?.iv,
916
+ 'Content-Type': 'application/json',
917
+ 'x-api-key': this.apiKey,
918
+ },
919
+ body: JSON.stringify({ data: payload2?.data })
920
+ });
921
+ const resData = await res.json();
922
+ return resData;
923
+ }
924
+ }
925
+ } catch (error) {
926
+ console.error("Payment Error:", error);
927
+ this.events.emit("error", error);
928
+ }
929
+ }
930
+
931
+ async inventoryCharge() {
932
+ try {
933
+ const values = await this.getServerEncription();
934
+ if (values?.public_key) {
935
+ const keys = await this.generateClientKeyPair();
936
+ if (keys?.privateKey) {
937
+ const getVal = id => this.shadow.getElementById(id)?.value?.trim();
938
+ const phoneCode = getVal("phone-code") || "";
939
+ const phone = getVal("phone");
940
+
941
+ const cardNumber = getVal("card")?.replace(/\s/g, "");
942
+ const expiry = getVal("expiry")?.split("/") || [];
943
+ const cvv = getVal("cvv");
944
+ const cardHolderName = getVal("card-name");
945
+
946
+ const customer = {
947
+ name: getVal("name"),
948
+ email: getVal("email"),
949
+ phone: phone ? `${phoneCode}${phone}` : null
950
+ };
951
+
952
+ let address = null;
953
+
954
+ if (this.isAddressRequired) {
955
+ address = {
956
+ street: getVal("street"),
957
+ city: getVal("city"),
958
+ state: getVal("state"),
959
+ countryCode: getVal("country"),
960
+ postalCode: getVal("zip")
961
+ };
962
+ }
963
+
964
+ const encriptedData = await this.encryptData(JSON.stringify(customer), values.public_key, keys.privateKey, values.key_id);
965
+ const payload = {
966
+ publicKey: keys?.publicKey,
967
+ keyId: encriptedData?.keyId,
968
+ iv: encriptedData?.iv,
969
+ data: encriptedData?.encryptedData,
970
+ }
971
+ const newCustomer = await this.createCustomer(payload);
972
+ const customerGid = newCustomer.entity?.gid;
973
+ const orderDetails = await this.createOrder();
974
+ const order = orderDetails?.entity;
975
+ const sessionData = {
976
+ customer: customer,
977
+ customerGid: customerGid,
978
+ paymentMethod: {
979
+ card: {
980
+ number: cardNumber,
981
+ expiryMonth: parseInt(expiry[0]),
982
+ expiryYear: 2000 + parseInt(expiry[1]),
983
+ cvv: cvv,
984
+ name: cardHolderName
985
+ },
986
+ postalCode: address?.postalCode,
987
+ type: "CARD",
988
+ paymentMethodBillingAddress: address,
989
+ }
990
+ }
991
+ const encriptedData1 = await this.encryptData(JSON.stringify(sessionData), values.public_key, keys.privateKey, values.key_id);
992
+ const payload2 = {
993
+ publicKey: keys?.publicKey,
994
+ keyId: encriptedData1?.keyId,
995
+ iv: encriptedData1?.iv,
996
+ data: encriptedData1?.encryptedData,
997
+ }
998
+ const gateway = this.getEndpoints().gateway;
999
+ const url = `${gateway}/v1/inventory/order-link/${order?.inventoryOrderLink?.gid}/charge`;
1000
+ const res = await fetch(url, {
1001
+ method: 'POST',
1002
+ headers: {
1003
+ 'x-enc-public-key': payload2?.publicKey,
1004
+ 'x-enc-key-id': payload2?.keyId,
1005
+ 'x-enc-iv': payload2?.iv,
1006
+ 'Content-Type': 'application/json',
1007
+ 'x-api-key': this.apiKey,
1008
+ },
1009
+ body: JSON.stringify({ data: payload2?.data })
1010
+ });
1011
+ const resData = await res.json();
1012
+ return resData;
1013
+ }
1014
+ }
1015
+
1016
+ } catch (error) {
1017
+ console.error("Payment Error:", error);
1018
+ this.events.emit("error", error);
1019
+ }
1020
+ }
1021
+
1022
+ async handleSubmit(e) {
1023
+ e.preventDefault();
1024
+
1025
+ const payButton = this.shadow.getElementById("form").querySelector(".btn");
1026
+ payButton.disabled = true;
1027
+ payButton.textContent = "Processing...";
1028
+
1029
+ const isValid = this.validate();
1030
+ if (!isValid) {
1031
+ payButton.disabled = false;
1032
+ payButton.textContent = `Pay $${(this.amount / 100).toFixed(2)}`;
1033
+ return;
1034
+ }
1035
+ try {
1036
+ if (this.frequency === "ONE-TIME" && !this.inventory) {
1037
+ const resData = await this.oneTimePayment();
1038
+ if (resData?.entity?.errorCode === null) {
1039
+ this.events.emit("success", resData);
1040
+ this.close();
1041
+ } else {
1042
+ const errorMsg = resData?.entity?.errorDescription || "Payment failed";
1043
+ console.error("Payment Error:", errorMsg);
1044
+ this.events.emit("error", errorMsg);
1045
+ }
1046
+ } else if (this.frequency !== "ONE-TIME" && !this.inventory) {
1047
+ const resData = await this.recurringPayment();
1048
+ if (resData?.entity?.setupSession?.errorCode === null) {
1049
+ this.events.emit("success", resData);
1050
+ this.close();
1051
+ } else {
1052
+ const errorMsg = resData?.entity?.setupSession?.errorDescription || "Payment failed";
1053
+ console.error("Payment Error:", errorMsg);
1054
+ this.events.emit("error", errorMsg);
1055
+ }
1056
+ } else if (this.inventory) {
1057
+ const resData = await this.inventoryCharge();
1058
+ if (resData?.responseCode === 200) {
1059
+ this.events.emit("success", resData);
1060
+ this.close();
1061
+ } else {
1062
+ console.error("Payment Error:", resData?.message || 'Payment failed');
1063
+ this.events.emit("error", resData?.message);
622
1064
  }
623
1065
  }
624
1066
  } catch (error) {
625
1067
  console.error("Payment Error:", error);
626
1068
  this.events.emit("error", error);
627
- alert(error.message || "Payment failed");
628
1069
  } finally {
629
1070
  payButton.disabled = false;
630
1071
  payButton.textContent = `Pay $${(this.amount / 100).toFixed(2)}`;
@@ -633,6 +1074,7 @@ export class SwirepayCheckout extends HTMLElement {
633
1074
  render() {
634
1075
  const t = this.theme;
635
1076
  const amountText = (this.amount / 100).toFixed(2);
1077
+ const inventoryAmountText = ((this.splitUp?.netPayable) / 100).toFixed(2);
636
1078
 
637
1079
  this.shadow.innerHTML = `
638
1080
  <style>
@@ -654,26 +1096,45 @@ export class SwirepayCheckout extends HTMLElement {
654
1096
  background:rgba(0,0,0,0.6);
655
1097
  justify-content:center;
656
1098
  align-items:center;
1099
+ z-index:9999;
657
1100
  }
658
1101
 
659
1102
  .modal {
660
- width:420px;
661
- background:var(--bg);
662
- color:var(--text);
663
- border-radius:16px;
664
- padding:20px;
665
- font-family:sans-serif;
1103
+ width: 520px;
1104
+ height: auto;
1105
+ max-height: 650px;
1106
+ background: var(--bg);
1107
+ color: var(--text);
1108
+ border-radius: 16px;
1109
+ padding: 28px;
1110
+ font-family: sans-serif;
1111
+ }
1112
+
1113
+ .modal-body {
1114
+ overflow-y: auto;
1115
+ flex: 1;
1116
+ height: auto;
1117
+ max-height: 530px;
1118
+ }
1119
+
1120
+ form {
1121
+ display: flex;
1122
+ flex-direction: column;
1123
+ gap: 16px;
666
1124
  }
667
1125
 
668
1126
  input, select {
669
- width:100%;
670
- padding:10px;
671
- margin:6px 0;
672
- border:1px solid var(--border);
673
- background: white;
674
- color: black;
675
- border-radius:8px;
676
- outline:none;
1127
+ width: 100%;
1128
+ padding: 12px;
1129
+ border: 1px solid var(--border);
1130
+ background: #ffffff;
1131
+ color: #000000;
1132
+ border-radius: 8px;
1133
+ outline: none;
1134
+ height: 44px;
1135
+ font-size: 14px;
1136
+ box-sizing: border-box;
1137
+ margin: 0 !important;
677
1138
  }
678
1139
 
679
1140
  input::placeholder {
@@ -696,7 +1157,7 @@ input::placeholder {
696
1157
  select {
697
1158
  width:100%;
698
1159
  padding:10px;
699
- margin:6px 0;
1160
+ margin: 0;
700
1161
  border:1px solid var(--border);
701
1162
  border-radius:8px;
702
1163
 
@@ -712,67 +1173,199 @@ select option {
712
1173
  color: #000000;
713
1174
  }
714
1175
 
715
- .row { display:flex; gap:10px; }
1176
+ .row {
1177
+ display: flex;
1178
+ gap: 16px;
1179
+ align-items: center;
1180
+ }
1181
+ .phone-code {
1182
+ flex: 0 0 70px !important;
1183
+ }
1184
+
1185
+ .row input,
1186
+ .row select {
1187
+ flex: 1;
1188
+ }
716
1189
 
717
- .phone-code { width:70px; }
1190
+ input, select {
1191
+ box-sizing: border-box;
1192
+ height: 40px;
1193
+ padding: 10px;
1194
+ font-size: 14px;
1195
+ }
718
1196
 
719
1197
  .btn {
720
- background:var(--primary);
721
- color:#fff;
722
- padding:12px;
723
- border:none;
724
- border-radius:10px;
725
- cursor:pointer;
726
- width:100%;
1198
+ background: var(--primary);
1199
+ color: #fff;
1200
+ padding: 14px;
1201
+ border: none;
1202
+ border-radius: 10px;
1203
+ cursor: pointer;
1204
+ width: 100%;
1205
+ height: 48px;
1206
+ font-size: 15px;
727
1207
  }
728
1208
 
729
1209
  .close {
730
1210
  float:right;
731
1211
  cursor:pointer;
732
1212
  }
733
- </style>
734
1213
 
735
- <div class="overlay" id="overlay">
736
- <div class="modal">
737
- <span class="close" id="close">✕</span>
1214
+ h3 {
1215
+ margin-bottom: 16px;
1216
+ }
738
1217
 
739
- <h3>Pay with Card</h3>
1218
+ .section {
1219
+ display: flex;
1220
+ flex-direction: column;
1221
+ gap: 16px;
1222
+ }
740
1223
 
741
- <form id="form">
742
- <input id="name" placeholder="Name"/>
743
- <input id="email" placeholder="Email"/>
1224
+ .section-title {
1225
+ font-size: 14px;
1226
+ font-weight: 600;
1227
+ margin: 0;
1228
+ }
744
1229
 
745
- <div class="row">
746
- <input class="phone-code" id="phone-code" value="+1"/>
747
- <input id="phone" placeholder="Phone"/>
748
- </div>
1230
+ .card-wrapper {
1231
+ position: relative;
1232
+ }
749
1233
 
750
- <input id="card-name" placeholder="Card Holder Name"/>
751
- <input id="card" placeholder="Card Number"/>
1234
+ #card-icon {
1235
+ position: absolute;
1236
+ right: 10px;
1237
+ top: 50%;
1238
+ transform: translateY(-50%);
1239
+ height: 24px;
1240
+ }
1241
+ .error {
1242
+ font-size: 12px;
1243
+ color: #dc2626;
1244
+ margin-top: 0px;
1245
+ margin-bottom: 0px;
1246
+ }
752
1247
 
753
- <div class="row">
754
- <input id="expiry" placeholder="MM/YY"/>
755
- <input id="cvv" type="password" placeholder="CVV"/>
756
- </div>
1248
+ .field {
1249
+ position: relative;
1250
+ width: 100%;
1251
+ }
757
1252
 
758
- <input id="street" placeholder="Street Address"/>
759
- <input id="city" placeholder="City"/>
1253
+ .field input,
1254
+ .field select {
1255
+ width: 100%;
1256
+ padding: 14px 12px;
1257
+ border: 1px solid var(--border);
1258
+ border-radius: 8px;
1259
+ font-size: 14px;
1260
+ background: #fff;
1261
+ outline: none;
1262
+ }
1263
+ .field label {
1264
+ position: absolute;
1265
+ left: 0px;
1266
+ top: 50%;
1267
+ transform: translateY(-50%);
1268
+ padding: 0 4px;
1269
+ color: #6b7280;
1270
+ font-size: 14px;
1271
+ pointer-events: none;
1272
+ transition: 0.2s ease;
1273
+ }
1274
+ .field input:focus + label,
1275
+ .field input:not(:placeholder-shown) + label {
1276
+ top: -8px;
1277
+ font-size: 12px;
1278
+ color: var(--primary);
1279
+ }
1280
+ .field select {
1281
+ width: 100%;
1282
+ height: 44px;
1283
+ padding: 0 12px;
1284
+ border: 1px solid var(--border);
1285
+ border-radius: 8px;
1286
+ background: #fff;
1287
+ color: #000;
1288
+ font-size: 14px;
1289
+ outline: none;
1290
+ appearance: none;
1291
+ }
1292
+ .input-error {
1293
+ border-color: #dc2626 !important;
1294
+ }
760
1295
 
761
- <!-- STATE FIRST -->
762
- <select id="state"></select>
1296
+ .field input.input-error + label {
1297
+ color: #dc2626;
1298
+ }
763
1299
 
764
- <!-- COUNTRY AFTER -->
765
- <select id="country">
766
- ${COUNTRIES_LIST.map(c =>
767
- `<option value="${c.code}" ${c.code === "US" ? "selected" : ""}>
768
- ${c.name}
769
- </option>`
770
- ).join("")}
771
- </select>
1300
+ .field label.floating {
1301
+ top: -8px;
1302
+ font-size: 12px;
1303
+ color: var(--primary);
1304
+ }
1305
+ </style>
772
1306
 
773
- <input id="zip" placeholder="ZIP Code"/>
1307
+ <div class="overlay" id="overlay">
1308
+ <div class="modal">
1309
+ <div class="modal-header">
1310
+ <span class="close" id="close">✕</span>
1311
+ <h3>Pay with Card</h3>
1312
+ </div>
1313
+ <div class="modal-body">
1314
+ <form id="form">
1315
+ <div class="section">
1316
+ <div class="section-title">Customer Details</div>
1317
+ <div class="field">
1318
+ <input id="name" required placeholder=" " />
1319
+ <label>Name *</label>
1320
+ </div>
1321
+ <div class="error" id="error-name"></div>
1322
+ <div class="field">
1323
+ <input id="email" required placeholder=" " />
1324
+ <label>Email *</label>
1325
+ </div>
1326
+ <div class="error" id="error-email"></div>
1327
+ <div class="row">
1328
+ <div class="field phone-code">
1329
+ <input id="phone-code" placeholder=" " />
1330
+ <label>Code</label>
1331
+ </div>
1332
+ <div class="field">
1333
+ <input id="phone" required placeholder=" " />
1334
+ <label>Phone *</label>
1335
+ </div>
1336
+ </div>
1337
+ <div class="error" id="error-phone"></div>
1338
+ </div>
1339
+ <div class="section">
1340
+ <div class="section-title">Card Details</div>
1341
+ <div class="field">
1342
+ <input id="card" required placeholder=" " />
1343
+ <label>Card Number *</label>
1344
+ </div>
1345
+ <div class="error" id="error-card"></div>
774
1346
 
775
- <button class="btn">Pay $${amountText}</button>
1347
+ <div class="row">
1348
+ <div class="field">
1349
+ <input id="expiry" required placeholder=" " />
1350
+ <label>MM/YY *</label>
1351
+ </div>
1352
+
1353
+ <div class="field">
1354
+ <input id="cvv" type="password" required placeholder=" " />
1355
+ <label>CVV *</label>
1356
+ </div>
1357
+ </div>
1358
+ <div class="error" id="error-expiry"></div>
1359
+ <div class="error" id="error-cvv"></div>
1360
+ <div class="field">
1361
+ <input id="card-name" required placeholder=" " />
1362
+ <label>Card Holder Name *</label>
1363
+ </div>
1364
+ <div class="error" id="error-card-name"></div>
1365
+ </div>
1366
+ ${this.getAddressFields()}
1367
+ <button class="btn">Pay $${this.inventory ? inventoryAmountText : amountText}</button>
1368
+ </div>
776
1369
  </form>
777
1370
  </div>
778
1371
  </div>
@@ -780,16 +1373,64 @@ select option {
780
1373
 
781
1374
  const $ = id => this.shadow.getElementById(id);
782
1375
 
1376
+ if (this.customer) {
1377
+ if (this.customer.name) $("name").value = this.customer.name;
1378
+ if (this.customer.email) $("email").value = this.customer.email;
1379
+
1380
+ if (this.customer.phone) {
1381
+ let phone = this.customer.phone;
1382
+ let code = "";
1383
+ let number = phone;
1384
+
1385
+ if (phone.startsWith("+")) {
1386
+ if (phone.startsWith("+1")) {
1387
+ code = "+1";
1388
+ number = phone.slice(2);
1389
+ } else if (phone.startsWith("+91")) {
1390
+ code = "+91";
1391
+ number = phone.slice(3);
1392
+ }
1393
+ }
1394
+
1395
+ if ($("phone-code")) $("phone-code").value = code;
1396
+ if ($("phone")) $("phone").value = number;
1397
+ }
1398
+ }
1399
+
783
1400
  $("close").onclick = () => this.close();
784
1401
  $("form").onsubmit = e => this.handleSubmit(e);
785
1402
 
786
- $("country").onchange = () => this.handleCountryChange();
787
-
788
1403
  const countryEl = $("country");
789
- countryEl.value = "US";
790
- this.handleCountryChange();
1404
+ if (countryEl) {
1405
+ countryEl.onchange = () => this.handleCountryChange();
1406
+ if (!this.customer?.countryCode) {
1407
+ countryEl.value = "US";
1408
+ }
1409
+ this.handleCountryChange();
1410
+ }
1411
+ const phoneCodeEl = $("phone-code");
1412
+ if (phoneCodeEl) {
1413
+ phoneCodeEl.dataset.auto = "true";
1414
+ }
791
1415
 
792
- $("card").oninput = e => e.target.value = this.formatCard(e.target.value);
1416
+ const icons = {
1417
+ VISA: `<svg width="40" viewBox="0 0 48 16"><text x="0" y="14">VISA</text></svg>`,
1418
+ MASTERCARD: `<svg width="40"><text x="0" y="14">MC</text></svg>`,
1419
+ AMEX: `<svg width="40"><text x="0" y="14">AMEX</text></svg>`,
1420
+ DISCOVER: `<svg width="40"><text x="0" y="14">DISC</text></svg>`
1421
+ };
1422
+ $("card").oninput = e => {
1423
+ const formatted = this.formatCard(e.target.value);
1424
+ e.target.value = formatted;
1425
+
1426
+ const type = this.detectCardType(formatted);
1427
+
1428
+ const iconEl = $("card-icon");
1429
+ if (iconEl) {
1430
+ iconEl.src = icons[type] || "";
1431
+ iconEl.style.display = type ? "block" : "none";
1432
+ }
1433
+ };
793
1434
  $("expiry").oninput = e => e.target.value = this.formatExpiry(e.target.value);
794
1435
  $("phone").oninput = e => e.target.value = this.formatPhone(e.target.value);
795
1436
  }