@one-payments/web-components 1.7.0 → 1.7.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/dist/index.js +5591 -1583
- package/dist/one-payment.browser.iife.js +671 -54
- package/dist/one-payment.browser.iife.js.map +1 -1
- package/dist/one-payment.d.ts +102 -5
- package/dist/one-payment.d.ts.map +1 -1
- package/dist/one-payment.js +1133 -235
- package/dist/one-payment.js.map +1 -1
- package/package.json +5 -4
package/dist/one-payment.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
import { __decorate } from "tslib";
|
|
6
6
|
import { LitElement, html, css } from 'lit';
|
|
7
7
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
8
|
-
import { PaymentSDK, PAYMENT_METHODS, PAYNOW_ICON_DATA_URL, PROMPTPAY_ICON_DATA_URL, DUITNOW_ICON_DATA_URL, GOPAY_ICON_DATA_URL, BOOST_ICON_DATA_URL, SHOPEEPAY_ICON_DATA_URL, ATOM_ICON_DATA_URL, DANA_ICON_DATA_URL, TNG_ICON_DATA_URL, ALIPAYCN_ICON_DATA_URL, ALIPAYHK_ICON_DATA_URL, GCASH_ICON_DATA_URL, KONBINI_ICON_DATA_URL, PAYEASY_ICON_DATA_URL } from '@one-payments/core';
|
|
8
|
+
import { PaymentSDK, PAYMENT_METHODS, PAYNOW_ICON_DATA_URL, PROMPTPAY_ICON_DATA_URL, DUITNOW_ICON_DATA_URL, GOPAY_ICON_DATA_URL, BOOST_ICON_DATA_URL, SHOPEEPAY_ICON_DATA_URL, ATOM_ICON_DATA_URL, DANA_ICON_DATA_URL, TNG_ICON_DATA_URL, ALIPAYCN_ICON_DATA_URL, ALIPAYHK_ICON_DATA_URL, GCASH_ICON_DATA_URL, KONBINI_ICON_DATA_URL, PAYEASY_ICON_DATA_URL, GRABPAY_ICON_DATA_URL, FPX_ICON_DATA_URL, } from '@one-payments/core';
|
|
9
9
|
import CleaveConstructor from 'cleave.js';
|
|
10
10
|
import QRCode from 'qrcode';
|
|
11
|
+
import { AsYouType, isValidPhoneNumber, parsePhoneNumber, } from 'libphonenumber-js';
|
|
11
12
|
/**
|
|
12
13
|
* Main payment component - framework-agnostic web component with EXACT original design
|
|
13
14
|
*
|
|
@@ -74,6 +75,15 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
74
75
|
this.qrCodeDataUrl = null; // Generated QR image
|
|
75
76
|
this.qrAutoResumeTimer = null;
|
|
76
77
|
this.qrPollingInProgress = false; // Track if QR polling is in progress
|
|
78
|
+
// Bank selection state for FPX and similar methods
|
|
79
|
+
this.availableBanks = [];
|
|
80
|
+
this.selectedBank = null;
|
|
81
|
+
this.banksLoading = false;
|
|
82
|
+
this.bankSearchQuery = '';
|
|
83
|
+
// Phone number input state for methods that require phone entry
|
|
84
|
+
this.phoneInputValue = '';
|
|
85
|
+
this.phoneInputError = '';
|
|
86
|
+
this.selectedCountry = 'SG'; // Default to Singapore
|
|
77
87
|
this.sdk = null;
|
|
78
88
|
this.isInitialized = false;
|
|
79
89
|
this.cardNumberCleave = null;
|
|
@@ -122,6 +132,33 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
122
132
|
}
|
|
123
133
|
}
|
|
124
134
|
};
|
|
135
|
+
// --- Phone Number Helper Methods ---
|
|
136
|
+
/**
|
|
137
|
+
* Minimum phone number length (digits only)
|
|
138
|
+
*/
|
|
139
|
+
this.MIN_PHONE_LENGTH = 8;
|
|
140
|
+
/**
|
|
141
|
+
* Japan-only payment methods - phone must start with +81
|
|
142
|
+
* These methods require the country selector to be locked to Japan
|
|
143
|
+
*/
|
|
144
|
+
this.JAPAN_ONLY_PHONE_METHODS = [
|
|
145
|
+
PAYMENT_METHODS.KONBINI,
|
|
146
|
+
PAYMENT_METHODS.PAYEASY,
|
|
147
|
+
];
|
|
148
|
+
/**
|
|
149
|
+
* Country options for phone number selection
|
|
150
|
+
* Uses ISO 3166-1 alpha-2 country codes (uppercase for libphonenumber-js)
|
|
151
|
+
*/
|
|
152
|
+
this.COUNTRY_OPTIONS = [
|
|
153
|
+
{ code: 'JP', name: 'Japan', flag: '🇯🇵', dialCode: '+81' },
|
|
154
|
+
{ code: 'SG', name: 'Singapore', flag: '🇸🇬', dialCode: '+65' },
|
|
155
|
+
{ code: 'MY', name: 'Malaysia', flag: '🇲🇾', dialCode: '+60' },
|
|
156
|
+
{ code: 'ID', name: 'Indonesia', flag: '🇮🇩', dialCode: '+62' },
|
|
157
|
+
{ code: 'TH', name: 'Thailand', flag: '🇹🇭', dialCode: '+66' },
|
|
158
|
+
{ code: 'PH', name: 'Philippines', flag: '🇵🇭', dialCode: '+63' },
|
|
159
|
+
{ code: 'HK', name: 'Hong Kong', flag: '🇭🇰', dialCode: '+852' },
|
|
160
|
+
{ code: 'CN', name: 'China', flag: '🇨🇳', dialCode: '+86' },
|
|
161
|
+
];
|
|
125
162
|
}
|
|
126
163
|
/**
|
|
127
164
|
* Check if all required props are present and valid.
|
|
@@ -145,7 +182,9 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
145
182
|
if (!this.email || typeof this.email !== 'string' || this.email.trim() === '') {
|
|
146
183
|
return false;
|
|
147
184
|
}
|
|
148
|
-
if (!this.phoneNumber ||
|
|
185
|
+
if (!this.phoneNumber ||
|
|
186
|
+
typeof this.phoneNumber !== 'string' ||
|
|
187
|
+
this.phoneNumber.trim() === '') {
|
|
149
188
|
return false;
|
|
150
189
|
}
|
|
151
190
|
return true;
|
|
@@ -323,11 +362,12 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
323
362
|
}
|
|
324
363
|
// --- Methods ---
|
|
325
364
|
/**
|
|
326
|
-
* Get filtered
|
|
327
|
-
* Applies
|
|
328
|
-
* 1. API availability (from available-methods endpoint)
|
|
365
|
+
* Get filtered payment methods
|
|
366
|
+
* Applies filtering:
|
|
367
|
+
* 1. API availability (from available-methods endpoint with currency filtering)
|
|
329
368
|
* 2. User exclusions (excludePaymentMethods prop)
|
|
330
|
-
*
|
|
369
|
+
*
|
|
370
|
+
* Note: Currency validation is now handled server-side by the available-methods API
|
|
331
371
|
*/
|
|
332
372
|
getFilteredPaymentMethods() {
|
|
333
373
|
// Get available methods from SDK state (from API)
|
|
@@ -342,7 +382,7 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
342
382
|
if (availableMethods.length === 0) {
|
|
343
383
|
return [];
|
|
344
384
|
}
|
|
345
|
-
// Filter by exclusions
|
|
385
|
+
// Filter by user exclusions only - currency filtering is done server-side
|
|
346
386
|
return availableMethods
|
|
347
387
|
.filter((method) => {
|
|
348
388
|
// Apply user exclusions
|
|
@@ -351,180 +391,10 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
351
391
|
}
|
|
352
392
|
return true;
|
|
353
393
|
})
|
|
354
|
-
.map((method) => {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const currency = transactionData.currency;
|
|
359
|
-
if (currency !== 'SGD') {
|
|
360
|
-
return {
|
|
361
|
-
id: method.id,
|
|
362
|
-
enabled: false,
|
|
363
|
-
disabledReason: 'PayNow is only available for SGD currency',
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
// Check currency validation for Prompt Pay
|
|
368
|
-
if (method.id === PAYMENT_METHODS.PROMPTPAY) {
|
|
369
|
-
const transactionData = this.getTransactionData();
|
|
370
|
-
const currency = transactionData.currency;
|
|
371
|
-
if (currency !== 'THB') {
|
|
372
|
-
return {
|
|
373
|
-
id: method.id,
|
|
374
|
-
enabled: false,
|
|
375
|
-
disabledReason: 'PromptPay is only available for THB currency',
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
// Check currency validation for DuitNow
|
|
380
|
-
if (method.id === PAYMENT_METHODS.DUITNOW) {
|
|
381
|
-
const transactionData = this.getTransactionData();
|
|
382
|
-
const currency = transactionData.currency;
|
|
383
|
-
if (currency !== 'MYR') {
|
|
384
|
-
return {
|
|
385
|
-
id: method.id,
|
|
386
|
-
enabled: false,
|
|
387
|
-
disabledReason: 'DuitNow is only available for MYR currency',
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
// Check currency validation for GoPay
|
|
392
|
-
if (method.id === PAYMENT_METHODS.GOPAY) {
|
|
393
|
-
const transactionData = this.getTransactionData();
|
|
394
|
-
const currency = transactionData.currency;
|
|
395
|
-
if (currency !== 'IDR') {
|
|
396
|
-
return {
|
|
397
|
-
id: method.id,
|
|
398
|
-
enabled: false,
|
|
399
|
-
disabledReason: 'GoPay is only available for IDR currency',
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
// Check currency validation for Boost
|
|
404
|
-
if (method.id === PAYMENT_METHODS.BOOST) {
|
|
405
|
-
const transactionData = this.getTransactionData();
|
|
406
|
-
const currency = transactionData.currency;
|
|
407
|
-
if (currency !== 'MYR') {
|
|
408
|
-
return {
|
|
409
|
-
id: method.id,
|
|
410
|
-
enabled: false,
|
|
411
|
-
disabledReason: 'Boost is only available for MYR currency',
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
// Check currency validation for ShopeePay
|
|
416
|
-
if (method.id === PAYMENT_METHODS.SHOPEEPAY) {
|
|
417
|
-
const transactionData = this.getTransactionData();
|
|
418
|
-
const currency = transactionData.currency;
|
|
419
|
-
if (currency !== 'MYR') {
|
|
420
|
-
return {
|
|
421
|
-
id: method.id,
|
|
422
|
-
enabled: false,
|
|
423
|
-
disabledReason: 'ShopeePay is only available for MYR currency',
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
// Check currency validation for Atom (SGD or MYR only)
|
|
428
|
-
if (method.id === PAYMENT_METHODS.ATOM) {
|
|
429
|
-
const transactionData = this.getTransactionData();
|
|
430
|
-
const currency = transactionData.currency;
|
|
431
|
-
if (currency !== 'SGD' && currency !== 'MYR') {
|
|
432
|
-
return {
|
|
433
|
-
id: method.id,
|
|
434
|
-
enabled: false,
|
|
435
|
-
disabledReason: 'Atom is only available for SGD and MYR currencies',
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
// Check currency validation for Dana (IDR only)
|
|
440
|
-
if (method.id === PAYMENT_METHODS.DANA) {
|
|
441
|
-
const transactionData = this.getTransactionData();
|
|
442
|
-
const currency = transactionData.currency;
|
|
443
|
-
if (currency !== 'IDR') {
|
|
444
|
-
return {
|
|
445
|
-
id: method.id,
|
|
446
|
-
enabled: false,
|
|
447
|
-
disabledReason: 'Dana is only available for IDR currency',
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
// Check currency validation for Touch 'n Go (MYR only)
|
|
452
|
-
if (method.id === PAYMENT_METHODS.TNG) {
|
|
453
|
-
const transactionData = this.getTransactionData();
|
|
454
|
-
const currency = transactionData.currency;
|
|
455
|
-
if (currency !== 'MYR') {
|
|
456
|
-
return {
|
|
457
|
-
id: method.id,
|
|
458
|
-
enabled: false,
|
|
459
|
-
disabledReason: "Touch 'n Go is only available for MYR currency",
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
// Check currency validation for Alipay CN (CNY only)
|
|
464
|
-
if (method.id === PAYMENT_METHODS.ALIPAYCN) {
|
|
465
|
-
const transactionData = this.getTransactionData();
|
|
466
|
-
const currency = transactionData.currency;
|
|
467
|
-
if (currency !== 'CNY') {
|
|
468
|
-
return {
|
|
469
|
-
id: method.id,
|
|
470
|
-
enabled: false,
|
|
471
|
-
disabledReason: 'Alipay is only available for CNY currency',
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
// Check currency validation for Alipay HK (HKD only)
|
|
476
|
-
if (method.id === PAYMENT_METHODS.ALIPAYHK) {
|
|
477
|
-
const transactionData = this.getTransactionData();
|
|
478
|
-
const currency = transactionData.currency;
|
|
479
|
-
if (currency !== 'HKD') {
|
|
480
|
-
return {
|
|
481
|
-
id: method.id,
|
|
482
|
-
enabled: false,
|
|
483
|
-
disabledReason: 'AlipayHK is only available for HKD currency',
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
// Check currency validation for GCash (PHP only)
|
|
488
|
-
if (method.id === PAYMENT_METHODS.GCASH) {
|
|
489
|
-
const transactionData = this.getTransactionData();
|
|
490
|
-
const currency = transactionData.currency;
|
|
491
|
-
if (currency !== 'PHP') {
|
|
492
|
-
return {
|
|
493
|
-
id: method.id,
|
|
494
|
-
enabled: false,
|
|
495
|
-
disabledReason: 'GCash is only available for PHP currency',
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
// Check currency validation for Konbini (JPY only)
|
|
500
|
-
if (method.id === PAYMENT_METHODS.KONBINI) {
|
|
501
|
-
const transactionData = this.getTransactionData();
|
|
502
|
-
const currency = transactionData.currency;
|
|
503
|
-
if (currency !== 'JPY') {
|
|
504
|
-
return {
|
|
505
|
-
id: method.id,
|
|
506
|
-
enabled: false,
|
|
507
|
-
disabledReason: 'Konbini is only available for JPY currency',
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
// Check currency validation for Pay-Easy (JPY only)
|
|
512
|
-
if (method.id === PAYMENT_METHODS.PAYEASY) {
|
|
513
|
-
const transactionData = this.getTransactionData();
|
|
514
|
-
const currency = transactionData.currency;
|
|
515
|
-
if (currency !== 'JPY') {
|
|
516
|
-
return {
|
|
517
|
-
id: method.id,
|
|
518
|
-
enabled: false,
|
|
519
|
-
disabledReason: 'Pay-Easy is only available for JPY currency',
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
return {
|
|
524
|
-
id: method.id,
|
|
525
|
-
enabled: true,
|
|
526
|
-
};
|
|
527
|
-
});
|
|
394
|
+
.map((method) => ({
|
|
395
|
+
id: method.id,
|
|
396
|
+
enabled: true,
|
|
397
|
+
}));
|
|
528
398
|
}
|
|
529
399
|
initializeSDK() {
|
|
530
400
|
// Validate required configuration props
|
|
@@ -782,6 +652,22 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
782
652
|
window.location.href = redirectUrl;
|
|
783
653
|
}
|
|
784
654
|
});
|
|
655
|
+
// Handle GrabPay SG redirect
|
|
656
|
+
this.sdk.on('grabpay_sg_redirect', (event) => {
|
|
657
|
+
const { redirectUrl } = event.payload;
|
|
658
|
+
if (redirectUrl) {
|
|
659
|
+
// Redirect the entire page to the GrabPay payment page
|
|
660
|
+
window.location.href = redirectUrl;
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
// Handle FPX redirect
|
|
664
|
+
this.sdk.on('fpx_redirect', (event) => {
|
|
665
|
+
const { redirectUrl } = event.payload;
|
|
666
|
+
if (redirectUrl) {
|
|
667
|
+
// Redirect the entire page to the bank's payment page
|
|
668
|
+
window.location.href = redirectUrl;
|
|
669
|
+
}
|
|
670
|
+
});
|
|
785
671
|
this.sdk.initialize({
|
|
786
672
|
amount: this.amount,
|
|
787
673
|
currency: this.currency,
|
|
@@ -976,13 +862,15 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
976
862
|
const expiryMonth = (expiryParts[0] || '').trim();
|
|
977
863
|
const expiryYearShort = (expiryParts[1] || '').trim().substring(0, 2); // Take only first 2 digits
|
|
978
864
|
const expiryYear = `20${expiryYearShort}`; // Convert to 4-digit year
|
|
865
|
+
// Format names to UPPERCASE for API
|
|
866
|
+
const formattedNames = this.formatNamesUppercase();
|
|
979
867
|
this.sdk.submitPayment(PAYMENT_METHODS.CARD, {
|
|
980
868
|
cardNumber: this.cardFormData.cardNumber.replace(/\s/g, ''),
|
|
981
869
|
expiryMonth,
|
|
982
870
|
expiryYear,
|
|
983
871
|
cvv: this.cardFormData.cvv,
|
|
984
|
-
firstName:
|
|
985
|
-
lastName:
|
|
872
|
+
firstName: formattedNames.firstName,
|
|
873
|
+
lastName: formattedNames.lastName,
|
|
986
874
|
email: this.email,
|
|
987
875
|
phoneNumber: this.phoneNumber,
|
|
988
876
|
});
|
|
@@ -993,10 +881,12 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
993
881
|
}
|
|
994
882
|
// Lock payment methods to prevent switching
|
|
995
883
|
this.paymentLocked = true;
|
|
884
|
+
// Format names to UPPERCASE for API
|
|
885
|
+
const formattedNames = this.formatNamesUppercase();
|
|
996
886
|
// Use customer info from props
|
|
997
887
|
this.sdk.submitPayment(PAYMENT_METHODS.PAYNOW, {
|
|
998
|
-
firstName:
|
|
999
|
-
lastName:
|
|
888
|
+
firstName: formattedNames.firstName,
|
|
889
|
+
lastName: formattedNames.lastName,
|
|
1000
890
|
email: this.email,
|
|
1001
891
|
phoneNumber: this.phoneNumber,
|
|
1002
892
|
});
|
|
@@ -1008,11 +898,13 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
1008
898
|
}
|
|
1009
899
|
// Lock payment methods to prevent switching
|
|
1010
900
|
this.paymentLocked = true;
|
|
901
|
+
// Format names to UPPERCASE for API
|
|
902
|
+
const formattedNames = this.formatNamesUppercase();
|
|
1011
903
|
// Use customer info from props
|
|
1012
904
|
// SDK will redirect to external payment page when nextActionUrl is received
|
|
1013
905
|
this.sdk.submitPayment(PAYMENT_METHODS.PROMPTPAY, {
|
|
1014
|
-
firstName:
|
|
1015
|
-
lastName:
|
|
906
|
+
firstName: formattedNames.firstName,
|
|
907
|
+
lastName: formattedNames.lastName,
|
|
1016
908
|
email: this.email,
|
|
1017
909
|
phoneNumber: this.phoneNumber,
|
|
1018
910
|
});
|
|
@@ -1023,11 +915,13 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
1023
915
|
}
|
|
1024
916
|
// Lock payment methods to prevent switching
|
|
1025
917
|
this.paymentLocked = true;
|
|
918
|
+
// Format names to UPPERCASE for API
|
|
919
|
+
const formattedNames = this.formatNamesUppercase();
|
|
1026
920
|
// Use customer info from props
|
|
1027
921
|
// SDK will redirect to external payment page when nextActionUrl is received
|
|
1028
922
|
this.sdk.submitPayment(PAYMENT_METHODS.DUITNOW, {
|
|
1029
|
-
firstName:
|
|
1030
|
-
lastName:
|
|
923
|
+
firstName: formattedNames.firstName,
|
|
924
|
+
lastName: formattedNames.lastName,
|
|
1031
925
|
email: this.email,
|
|
1032
926
|
phoneNumber: this.phoneNumber,
|
|
1033
927
|
});
|
|
@@ -1038,11 +932,13 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
1038
932
|
}
|
|
1039
933
|
// Lock payment methods to prevent switching
|
|
1040
934
|
this.paymentLocked = true;
|
|
935
|
+
// Format names to UPPERCASE for API
|
|
936
|
+
const formattedNames = this.formatNamesUppercase();
|
|
1041
937
|
// Use customer info from props
|
|
1042
938
|
// SDK will redirect to external payment page when nextActionUrl is received
|
|
1043
939
|
this.sdk.submitPayment(PAYMENT_METHODS.GOPAY, {
|
|
1044
|
-
firstName:
|
|
1045
|
-
lastName:
|
|
940
|
+
firstName: formattedNames.firstName,
|
|
941
|
+
lastName: formattedNames.lastName,
|
|
1046
942
|
email: this.email,
|
|
1047
943
|
phoneNumber: this.phoneNumber,
|
|
1048
944
|
});
|
|
@@ -1051,152 +947,499 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
1051
947
|
if (!this.sdk || this.currentState.status !== 'ready') {
|
|
1052
948
|
return;
|
|
1053
949
|
}
|
|
950
|
+
// Validate phone input if filled, otherwise use prop
|
|
951
|
+
if (this.phoneInputValue && !this.validatePhoneInput()) {
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
1054
954
|
// Lock payment methods to prevent switching
|
|
1055
955
|
this.paymentLocked = true;
|
|
1056
|
-
//
|
|
956
|
+
// Format names to UPPERCASE for API
|
|
957
|
+
const formattedNames = this.formatNamesUppercase();
|
|
958
|
+
// Use phone from input or prop
|
|
959
|
+
const phoneNumber = this.getEffectivePhoneNumber(PAYMENT_METHODS.BOOST);
|
|
1057
960
|
// SDK will redirect to external payment page when nextActionUrl is received
|
|
1058
961
|
this.sdk.submitPayment(PAYMENT_METHODS.BOOST, {
|
|
1059
|
-
firstName:
|
|
1060
|
-
lastName:
|
|
962
|
+
firstName: formattedNames.firstName,
|
|
963
|
+
lastName: formattedNames.lastName,
|
|
1061
964
|
email: this.email,
|
|
1062
|
-
phoneNumber
|
|
965
|
+
phoneNumber,
|
|
1063
966
|
});
|
|
1064
967
|
}
|
|
1065
968
|
handleShopeePayPayment() {
|
|
1066
969
|
if (!this.sdk || this.currentState.status !== 'ready') {
|
|
1067
970
|
return;
|
|
1068
971
|
}
|
|
972
|
+
// Validate phone input if filled, otherwise use prop
|
|
973
|
+
if (this.phoneInputValue && !this.validatePhoneInput()) {
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
1069
976
|
// Lock payment methods to prevent switching
|
|
1070
977
|
this.paymentLocked = true;
|
|
1071
|
-
//
|
|
978
|
+
// Format names to UPPERCASE for API
|
|
979
|
+
const formattedNames = this.formatNamesUppercase();
|
|
980
|
+
// Use phone from input or prop
|
|
981
|
+
const phoneNumber = this.getEffectivePhoneNumber(PAYMENT_METHODS.SHOPEEPAY);
|
|
1072
982
|
// SDK will redirect to external payment page when nextActionUrl is received
|
|
1073
983
|
this.sdk.submitPayment(PAYMENT_METHODS.SHOPEEPAY, {
|
|
1074
|
-
firstName:
|
|
1075
|
-
lastName:
|
|
984
|
+
firstName: formattedNames.firstName,
|
|
985
|
+
lastName: formattedNames.lastName,
|
|
1076
986
|
email: this.email,
|
|
1077
|
-
phoneNumber
|
|
987
|
+
phoneNumber,
|
|
1078
988
|
});
|
|
1079
989
|
}
|
|
1080
990
|
handleAtomPayment() {
|
|
1081
991
|
if (!this.sdk || this.currentState.status !== 'ready') {
|
|
1082
992
|
return;
|
|
1083
993
|
}
|
|
994
|
+
// Validate phone input if filled, otherwise use prop
|
|
995
|
+
if (this.phoneInputValue && !this.validatePhoneInput()) {
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
1084
998
|
// Lock payment methods to prevent switching
|
|
1085
999
|
this.paymentLocked = true;
|
|
1086
|
-
//
|
|
1000
|
+
// Format names to UPPERCASE for API
|
|
1001
|
+
const formattedNames = this.formatNamesUppercase();
|
|
1002
|
+
// Use phone from input or prop
|
|
1003
|
+
const phoneNumber = this.getEffectivePhoneNumber(PAYMENT_METHODS.ATOM);
|
|
1087
1004
|
// SDK will redirect to external payment page when nextActionUrl is received
|
|
1088
1005
|
this.sdk.submitPayment(PAYMENT_METHODS.ATOM, {
|
|
1089
|
-
firstName:
|
|
1090
|
-
lastName:
|
|
1006
|
+
firstName: formattedNames.firstName,
|
|
1007
|
+
lastName: formattedNames.lastName,
|
|
1091
1008
|
email: this.email,
|
|
1092
|
-
phoneNumber
|
|
1009
|
+
phoneNumber,
|
|
1093
1010
|
});
|
|
1094
1011
|
}
|
|
1095
1012
|
handleDanaPayment() {
|
|
1096
1013
|
if (!this.sdk || this.currentState.status !== 'ready') {
|
|
1097
1014
|
return;
|
|
1098
1015
|
}
|
|
1016
|
+
// Validate phone input if filled, otherwise use prop
|
|
1017
|
+
if (this.phoneInputValue && !this.validatePhoneInput()) {
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1099
1020
|
// Lock payment methods to prevent switching
|
|
1100
1021
|
this.paymentLocked = true;
|
|
1101
|
-
//
|
|
1022
|
+
// Format names to UPPERCASE for API
|
|
1023
|
+
const formattedNames = this.formatNamesUppercase();
|
|
1024
|
+
// Use phone from input or prop
|
|
1025
|
+
const phoneNumber = this.getEffectivePhoneNumber(PAYMENT_METHODS.DANA);
|
|
1102
1026
|
// SDK will redirect to external payment page when nextActionUrl is received
|
|
1103
1027
|
this.sdk.submitPayment(PAYMENT_METHODS.DANA, {
|
|
1104
|
-
firstName:
|
|
1105
|
-
lastName:
|
|
1028
|
+
firstName: formattedNames.firstName,
|
|
1029
|
+
lastName: formattedNames.lastName,
|
|
1106
1030
|
email: this.email,
|
|
1107
|
-
phoneNumber
|
|
1031
|
+
phoneNumber,
|
|
1108
1032
|
});
|
|
1109
1033
|
}
|
|
1110
1034
|
handleTngPayment() {
|
|
1111
1035
|
if (!this.sdk || this.currentState.status !== 'ready') {
|
|
1112
1036
|
return;
|
|
1113
1037
|
}
|
|
1038
|
+
// Validate phone input if filled, otherwise use prop
|
|
1039
|
+
if (this.phoneInputValue && !this.validatePhoneInput()) {
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1114
1042
|
// Lock payment methods to prevent switching
|
|
1115
1043
|
this.paymentLocked = true;
|
|
1116
|
-
//
|
|
1044
|
+
// Format names to UPPERCASE for API
|
|
1045
|
+
const formattedNames = this.formatNamesUppercase();
|
|
1046
|
+
// Use phone from input or prop
|
|
1047
|
+
const phoneNumber = this.getEffectivePhoneNumber(PAYMENT_METHODS.TNG);
|
|
1117
1048
|
// SDK will redirect to external payment page when nextActionUrl is received
|
|
1118
1049
|
this.sdk.submitPayment(PAYMENT_METHODS.TNG, {
|
|
1119
|
-
firstName:
|
|
1120
|
-
lastName:
|
|
1050
|
+
firstName: formattedNames.firstName,
|
|
1051
|
+
lastName: formattedNames.lastName,
|
|
1121
1052
|
email: this.email,
|
|
1122
|
-
phoneNumber
|
|
1053
|
+
phoneNumber,
|
|
1123
1054
|
});
|
|
1124
1055
|
}
|
|
1125
1056
|
handleAlipayCNPayment() {
|
|
1126
1057
|
if (!this.sdk || this.currentState.status !== 'ready') {
|
|
1127
1058
|
return;
|
|
1128
1059
|
}
|
|
1060
|
+
// Validate phone input if filled, otherwise use prop
|
|
1061
|
+
if (this.phoneInputValue && !this.validatePhoneInput()) {
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1129
1064
|
// Lock payment methods to prevent switching
|
|
1130
1065
|
this.paymentLocked = true;
|
|
1131
|
-
//
|
|
1066
|
+
// Format names to UPPERCASE for API
|
|
1067
|
+
const formattedNames = this.formatNamesUppercase();
|
|
1068
|
+
// Use phone from input or prop
|
|
1069
|
+
const phoneNumber = this.getEffectivePhoneNumber(PAYMENT_METHODS.ALIPAYCN);
|
|
1132
1070
|
// SDK will redirect to external payment page when nextActionUrl is received
|
|
1133
1071
|
this.sdk.submitPayment(PAYMENT_METHODS.ALIPAYCN, {
|
|
1134
|
-
firstName:
|
|
1135
|
-
lastName:
|
|
1072
|
+
firstName: formattedNames.firstName,
|
|
1073
|
+
lastName: formattedNames.lastName,
|
|
1136
1074
|
email: this.email,
|
|
1137
|
-
phoneNumber
|
|
1075
|
+
phoneNumber,
|
|
1138
1076
|
});
|
|
1139
1077
|
}
|
|
1140
1078
|
handleAlipayHKPayment() {
|
|
1141
1079
|
if (!this.sdk || this.currentState.status !== 'ready') {
|
|
1142
1080
|
return;
|
|
1143
1081
|
}
|
|
1082
|
+
// Validate phone input if filled, otherwise use prop
|
|
1083
|
+
if (this.phoneInputValue && !this.validatePhoneInput()) {
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1144
1086
|
// Lock payment methods to prevent switching
|
|
1145
1087
|
this.paymentLocked = true;
|
|
1146
|
-
//
|
|
1088
|
+
// Format names to UPPERCASE for API
|
|
1089
|
+
const formattedNames = this.formatNamesUppercase();
|
|
1090
|
+
// Use phone from input or prop
|
|
1091
|
+
const phoneNumber = this.getEffectivePhoneNumber(PAYMENT_METHODS.ALIPAYHK);
|
|
1147
1092
|
// SDK will redirect to external payment page when nextActionUrl is received
|
|
1148
1093
|
this.sdk.submitPayment(PAYMENT_METHODS.ALIPAYHK, {
|
|
1149
|
-
firstName:
|
|
1150
|
-
lastName:
|
|
1094
|
+
firstName: formattedNames.firstName,
|
|
1095
|
+
lastName: formattedNames.lastName,
|
|
1151
1096
|
email: this.email,
|
|
1152
|
-
phoneNumber
|
|
1097
|
+
phoneNumber,
|
|
1153
1098
|
});
|
|
1154
1099
|
}
|
|
1155
1100
|
handleGCashPayment() {
|
|
1156
1101
|
if (!this.sdk || this.currentState.status !== 'ready') {
|
|
1157
1102
|
return;
|
|
1158
1103
|
}
|
|
1104
|
+
// Validate phone input if filled, otherwise use prop
|
|
1105
|
+
if (this.phoneInputValue && !this.validatePhoneInput()) {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1159
1108
|
// Lock payment methods to prevent switching
|
|
1160
1109
|
this.paymentLocked = true;
|
|
1161
|
-
//
|
|
1110
|
+
// Format names to UPPERCASE for API
|
|
1111
|
+
const formattedNames = this.formatNamesUppercase();
|
|
1112
|
+
// Use phone from input or prop
|
|
1113
|
+
const phoneNumber = this.getEffectivePhoneNumber(PAYMENT_METHODS.GCASH);
|
|
1162
1114
|
// SDK will redirect to external payment page when nextActionUrl is received
|
|
1163
1115
|
this.sdk.submitPayment(PAYMENT_METHODS.GCASH, {
|
|
1164
|
-
firstName:
|
|
1165
|
-
lastName:
|
|
1116
|
+
firstName: formattedNames.firstName,
|
|
1117
|
+
lastName: formattedNames.lastName,
|
|
1166
1118
|
email: this.email,
|
|
1167
|
-
phoneNumber
|
|
1119
|
+
phoneNumber,
|
|
1168
1120
|
});
|
|
1169
1121
|
}
|
|
1170
1122
|
handleKonbiniPayment() {
|
|
1171
1123
|
if (!this.sdk || this.currentState.status !== 'ready') {
|
|
1172
1124
|
return;
|
|
1173
1125
|
}
|
|
1126
|
+
// Validate phone input - required for Japan method
|
|
1127
|
+
if (this.phoneInputValue && !this.validatePhoneInput()) {
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1174
1130
|
// Lock payment methods to prevent switching
|
|
1175
1131
|
this.paymentLocked = true;
|
|
1176
|
-
//
|
|
1132
|
+
// Format names to UPPERCASE for API
|
|
1133
|
+
const formattedNames = this.formatNamesUppercase();
|
|
1134
|
+
// Use phone from input (formatted for Japan: +81 -> 0) or prop
|
|
1135
|
+
const phoneNumber = this.getEffectivePhoneNumber(PAYMENT_METHODS.KONBINI);
|
|
1177
1136
|
// SDK will redirect to convenience store payment page when nextActionUrl is received
|
|
1178
1137
|
this.sdk.submitPayment(PAYMENT_METHODS.KONBINI, {
|
|
1179
|
-
firstName:
|
|
1180
|
-
lastName:
|
|
1138
|
+
firstName: formattedNames.firstName,
|
|
1139
|
+
lastName: formattedNames.lastName,
|
|
1181
1140
|
email: this.email,
|
|
1182
|
-
phoneNumber
|
|
1141
|
+
phoneNumber,
|
|
1183
1142
|
});
|
|
1184
1143
|
}
|
|
1185
1144
|
handlePayEasyPayment() {
|
|
1186
1145
|
if (!this.sdk || this.currentState.status !== 'ready') {
|
|
1187
1146
|
return;
|
|
1188
1147
|
}
|
|
1148
|
+
// Validate phone input - required for Japan method
|
|
1149
|
+
if (this.phoneInputValue && !this.validatePhoneInput()) {
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1189
1152
|
// Lock payment methods to prevent switching
|
|
1190
1153
|
this.paymentLocked = true;
|
|
1191
|
-
// Use
|
|
1154
|
+
// Use phone from input (formatted for Japan: +81 -> 0) or prop
|
|
1155
|
+
const phoneNumber = this.getEffectivePhoneNumber(PAYMENT_METHODS.PAYEASY);
|
|
1156
|
+
// Format names for Pay-easy (max 9 chars combined)
|
|
1157
|
+
const formattedNames = this.formatNamesForPayEasy(this.firstName, this.lastName);
|
|
1192
1158
|
// SDK will redirect to bank transfer/ATM payment page when nextActionUrl is received
|
|
1193
1159
|
this.sdk.submitPayment(PAYMENT_METHODS.PAYEASY, {
|
|
1194
|
-
firstName:
|
|
1195
|
-
lastName:
|
|
1160
|
+
firstName: formattedNames.firstName,
|
|
1161
|
+
lastName: formattedNames.lastName,
|
|
1162
|
+
email: this.email,
|
|
1163
|
+
phoneNumber,
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
handleGrabPaySGPayment() {
|
|
1167
|
+
if (!this.sdk || this.currentState.status !== 'ready') {
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
// Lock payment methods to prevent switching
|
|
1171
|
+
this.paymentLocked = true;
|
|
1172
|
+
// Format names to UPPERCASE for API
|
|
1173
|
+
const formattedNames = this.formatNamesUppercase();
|
|
1174
|
+
// Use customer info from props
|
|
1175
|
+
// SDK will redirect to GrabPay payment page when nextActionUrl is received
|
|
1176
|
+
this.sdk.submitPayment(PAYMENT_METHODS.GRABPAY_SG, {
|
|
1177
|
+
firstName: formattedNames.firstName,
|
|
1178
|
+
lastName: formattedNames.lastName,
|
|
1196
1179
|
email: this.email,
|
|
1197
1180
|
phoneNumber: this.phoneNumber,
|
|
1198
1181
|
});
|
|
1199
1182
|
}
|
|
1183
|
+
handleFPXPayment() {
|
|
1184
|
+
if (!this.sdk || this.currentState.status !== 'ready') {
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
// Validate bank selection
|
|
1188
|
+
if (!this.selectedBank) {
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
// Lock payment methods to prevent switching
|
|
1192
|
+
this.paymentLocked = true;
|
|
1193
|
+
// Format names to UPPERCASE for API
|
|
1194
|
+
const formattedNames = this.formatNamesUppercase();
|
|
1195
|
+
// Use customer info from props plus selected bank
|
|
1196
|
+
// SDK will redirect to bank's payment page when nextActionUrl is received
|
|
1197
|
+
this.sdk.submitPayment(PAYMENT_METHODS.FPX, {
|
|
1198
|
+
firstName: formattedNames.firstName,
|
|
1199
|
+
lastName: formattedNames.lastName,
|
|
1200
|
+
email: this.email,
|
|
1201
|
+
phoneNumber: this.phoneNumber,
|
|
1202
|
+
bankName: this.selectedBank.bankName,
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Fetch available banks for a payment method
|
|
1207
|
+
*/
|
|
1208
|
+
async fetchBanks(paymentMethodType) {
|
|
1209
|
+
if (!this.sdk)
|
|
1210
|
+
return;
|
|
1211
|
+
this.banksLoading = true;
|
|
1212
|
+
this.availableBanks = [];
|
|
1213
|
+
this.selectedBank = null;
|
|
1214
|
+
this.bankSearchQuery = '';
|
|
1215
|
+
try {
|
|
1216
|
+
const banks = await this.sdk.getBanks(paymentMethodType);
|
|
1217
|
+
this.availableBanks = banks;
|
|
1218
|
+
}
|
|
1219
|
+
catch (error) {
|
|
1220
|
+
console.error('Error fetching banks:', error);
|
|
1221
|
+
this.availableBanks = [];
|
|
1222
|
+
}
|
|
1223
|
+
finally {
|
|
1224
|
+
this.banksLoading = false;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Handle bank selection
|
|
1229
|
+
*/
|
|
1230
|
+
handleBankSelect(bank) {
|
|
1231
|
+
this.selectedBank = bank;
|
|
1232
|
+
this.bankSearchQuery = bank.displayName;
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Get filtered banks based on search query
|
|
1236
|
+
*/
|
|
1237
|
+
getFilteredBanks() {
|
|
1238
|
+
if (!this.bankSearchQuery.trim()) {
|
|
1239
|
+
return this.availableBanks;
|
|
1240
|
+
}
|
|
1241
|
+
const query = this.bankSearchQuery.toLowerCase();
|
|
1242
|
+
return this.availableBanks.filter((bank) => bank.displayName.toLowerCase().includes(query) ||
|
|
1243
|
+
bank.bankName.toLowerCase().includes(query));
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Check if payment method requires Japan-only phone
|
|
1247
|
+
*/
|
|
1248
|
+
isJapanOnlyPhoneMethod(methodId) {
|
|
1249
|
+
return this.JAPAN_ONLY_PHONE_METHODS.includes(methodId);
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Get the full phone number with country code in international format
|
|
1253
|
+
* For display: "+81 90 1234 5678"
|
|
1254
|
+
* For API: depends on payment method
|
|
1255
|
+
*/
|
|
1256
|
+
getFormattedPhoneWithCountryCode() {
|
|
1257
|
+
if (!this.phoneInputValue.trim())
|
|
1258
|
+
return '';
|
|
1259
|
+
try {
|
|
1260
|
+
const parsed = parsePhoneNumber(this.phoneInputValue, this.selectedCountry);
|
|
1261
|
+
if (parsed) {
|
|
1262
|
+
return parsed.formatInternational();
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
catch {
|
|
1266
|
+
// Fallback: prepend dial code
|
|
1267
|
+
}
|
|
1268
|
+
const country = this.COUNTRY_OPTIONS.find((c) => c.code === this.selectedCountry);
|
|
1269
|
+
const dialCode = country?.dialCode || '+65';
|
|
1270
|
+
const digits = this.phoneInputValue.replace(/\D/g, '');
|
|
1271
|
+
return `${dialCode} ${digits}`;
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Get effective phone number for API submission
|
|
1275
|
+
* - Japan methods (konbini, payeasy): Convert +81 to local format (0XXXXXXXXXX)
|
|
1276
|
+
* - Other methods: Keep international format (+XX XXXX XXXX)
|
|
1277
|
+
*/
|
|
1278
|
+
getEffectivePhoneNumber(methodId) {
|
|
1279
|
+
// If phone input is filled, use it
|
|
1280
|
+
if (this.phoneInputValue.trim()) {
|
|
1281
|
+
const internationalPhone = this.getFormattedPhoneWithCountryCode();
|
|
1282
|
+
// For Japan methods, convert +81 to local 0 format
|
|
1283
|
+
if (this.isJapanOnlyPhoneMethod(methodId)) {
|
|
1284
|
+
return this.formatPhoneForJapan(internationalPhone);
|
|
1285
|
+
}
|
|
1286
|
+
// For other methods, keep international format as-is
|
|
1287
|
+
return internationalPhone;
|
|
1288
|
+
}
|
|
1289
|
+
// Otherwise use prop (already formatted)
|
|
1290
|
+
return this.phoneNumber || '';
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Format phone number for Japan (convert +81 to 0)
|
|
1294
|
+
* The Airwallex API for Japanese payment methods expects local format:
|
|
1295
|
+
* - Input: "+81 90 1234 5678" (international format)
|
|
1296
|
+
* - Output: "09012345678" (local format, no spaces)
|
|
1297
|
+
*/
|
|
1298
|
+
formatPhoneForJapan(phoneNumber) {
|
|
1299
|
+
if (!phoneNumber)
|
|
1300
|
+
return '';
|
|
1301
|
+
if (phoneNumber.startsWith('+81')) {
|
|
1302
|
+
// Remove +81 prefix and replace with 0
|
|
1303
|
+
// Also remove all spaces for clean format
|
|
1304
|
+
return '0' + phoneNumber.slice(3).replace(/\s/g, '');
|
|
1305
|
+
}
|
|
1306
|
+
// If already local format or other format, just remove spaces
|
|
1307
|
+
return phoneNumber.replace(/\s/g, '');
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Format names for Pay-Easy (max 9 chars combined)
|
|
1311
|
+
* Pay-easy has a unique requirement: combined first name + last name must be maximum 9 characters
|
|
1312
|
+
*/
|
|
1313
|
+
formatNamesForPayEasy(firstName, lastName) {
|
|
1314
|
+
const MAX_LENGTH = 9;
|
|
1315
|
+
const firstUpper = firstName.toUpperCase();
|
|
1316
|
+
const lastUpper = lastName.toUpperCase();
|
|
1317
|
+
const total = firstUpper.length + lastUpper.length;
|
|
1318
|
+
// Within limit - return as-is (uppercase)
|
|
1319
|
+
if (total <= MAX_LENGTH) {
|
|
1320
|
+
return { firstName: firstUpper, lastName: lastUpper };
|
|
1321
|
+
}
|
|
1322
|
+
// Need to truncate - distribute evenly
|
|
1323
|
+
const half = Math.floor(MAX_LENGTH / 2); // = 4
|
|
1324
|
+
let truncFirst = firstUpper;
|
|
1325
|
+
let truncLast = lastUpper;
|
|
1326
|
+
if (firstUpper.length <= half) {
|
|
1327
|
+
// First name fits in half, give remaining to last name
|
|
1328
|
+
truncLast = lastUpper.slice(0, MAX_LENGTH - firstUpper.length);
|
|
1329
|
+
}
|
|
1330
|
+
else if (lastUpper.length <= half) {
|
|
1331
|
+
// Last name fits in half, give remaining to first name
|
|
1332
|
+
truncFirst = firstUpper.slice(0, MAX_LENGTH - lastUpper.length);
|
|
1333
|
+
}
|
|
1334
|
+
else {
|
|
1335
|
+
// Both longer than half - split evenly
|
|
1336
|
+
truncFirst = firstUpper.slice(0, half); // 4 chars
|
|
1337
|
+
truncLast = lastUpper.slice(0, MAX_LENGTH - half); // 5 chars
|
|
1338
|
+
}
|
|
1339
|
+
return { firstName: truncFirst, lastName: truncLast };
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Format names for API submission (default: uppercase conversion)
|
|
1343
|
+
* All payment methods require names to be UPPERCASE
|
|
1344
|
+
*/
|
|
1345
|
+
formatNamesUppercase() {
|
|
1346
|
+
return {
|
|
1347
|
+
firstName: this.firstName.toUpperCase(),
|
|
1348
|
+
lastName: this.lastName.toUpperCase(),
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Validate phone number input
|
|
1353
|
+
* Rules:
|
|
1354
|
+
* 1. Required - phone number cannot be empty
|
|
1355
|
+
* 2. Min 8 digits - must have at least 8 digits
|
|
1356
|
+
* 3. Japan prefix - for konbini/payeasy, must validate as Japan number
|
|
1357
|
+
* 4. Format - must be valid phone format
|
|
1358
|
+
*/
|
|
1359
|
+
validatePhoneInput() {
|
|
1360
|
+
const phone = this.phoneInputValue.trim();
|
|
1361
|
+
// Rule 1: Required
|
|
1362
|
+
if (!phone) {
|
|
1363
|
+
this.phoneInputError = 'Phone number is required';
|
|
1364
|
+
return false;
|
|
1365
|
+
}
|
|
1366
|
+
// Rule 2: Minimum 8 digits
|
|
1367
|
+
const digitsOnly = phone.replace(/\D/g, '');
|
|
1368
|
+
if (digitsOnly.length < this.MIN_PHONE_LENGTH) {
|
|
1369
|
+
this.phoneInputError = `Phone number must be at least ${this.MIN_PHONE_LENGTH} digits`;
|
|
1370
|
+
return false;
|
|
1371
|
+
}
|
|
1372
|
+
// Rule 3 & 4: Use libphonenumber-js for proper validation
|
|
1373
|
+
try {
|
|
1374
|
+
const isValid = isValidPhoneNumber(phone, this.selectedCountry);
|
|
1375
|
+
if (!isValid) {
|
|
1376
|
+
this.phoneInputError = 'Please enter a valid phone number';
|
|
1377
|
+
return false;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
catch {
|
|
1381
|
+
this.phoneInputError = 'Please enter a valid phone number';
|
|
1382
|
+
return false;
|
|
1383
|
+
}
|
|
1384
|
+
this.phoneInputError = '';
|
|
1385
|
+
return true;
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Handle phone input change with auto-formatting
|
|
1389
|
+
* Uses libphonenumber-js AsYouType formatter to format as user types
|
|
1390
|
+
*/
|
|
1391
|
+
handlePhoneInputChange(e) {
|
|
1392
|
+
const input = e.target;
|
|
1393
|
+
const rawValue = input.value;
|
|
1394
|
+
// Extract only digits from input (reject all non-digits)
|
|
1395
|
+
let digits = rawValue.replace(/\D/g, '');
|
|
1396
|
+
// Country-specific max digits for national numbers
|
|
1397
|
+
const maxDigitsMap = {
|
|
1398
|
+
JP: 11, // 0X0-XXXX-XXXX (11 digits with leading 0)
|
|
1399
|
+
CN: 11, // 1XX-XXXX-XXXX
|
|
1400
|
+
SG: 8, // XXXX XXXX
|
|
1401
|
+
MY: 10, // 1X-XXX XXXX
|
|
1402
|
+
ID: 12, // 8XX-XXXX-XXXX
|
|
1403
|
+
TH: 9, // X XXXX XXXX
|
|
1404
|
+
PH: 10, // 9XX XXX XXXX
|
|
1405
|
+
HK: 8, // XXXX XXXX
|
|
1406
|
+
};
|
|
1407
|
+
const maxDigits = maxDigitsMap[this.selectedCountry] || 15;
|
|
1408
|
+
// Limit digits to max
|
|
1409
|
+
if (digits.length > maxDigits) {
|
|
1410
|
+
digits = digits.slice(0, maxDigits);
|
|
1411
|
+
}
|
|
1412
|
+
// For Japan, auto-prepend 0 if user starts with 9 (common mobile prefix)
|
|
1413
|
+
// Japanese mobile numbers start with 090, 080, 070
|
|
1414
|
+
if (this.selectedCountry === 'JP' && digits.length > 0 && !digits.startsWith('0')) {
|
|
1415
|
+
digits = '0' + digits;
|
|
1416
|
+
if (digits.length > maxDigits) {
|
|
1417
|
+
digits = digits.slice(0, maxDigits);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
// Use AsYouType formatter from libphonenumber-js
|
|
1421
|
+
const formatter = new AsYouType(this.selectedCountry);
|
|
1422
|
+
const formatted = formatter.input(digits);
|
|
1423
|
+
// Update state
|
|
1424
|
+
this.phoneInputValue = formatted;
|
|
1425
|
+
// IMPORTANT: Directly set input value to enforce formatting
|
|
1426
|
+
// This ensures the input field shows the formatted value, not the raw input
|
|
1427
|
+
input.value = formatted;
|
|
1428
|
+
// Clear error while typing
|
|
1429
|
+
if (this.phoneInputError) {
|
|
1430
|
+
this.phoneInputError = '';
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Handle country change
|
|
1435
|
+
*/
|
|
1436
|
+
handleCountryChange(e) {
|
|
1437
|
+
const select = e.target;
|
|
1438
|
+
this.selectedCountry = select.value;
|
|
1439
|
+
// Reset phone input when country changes
|
|
1440
|
+
this.phoneInputValue = '';
|
|
1441
|
+
this.phoneInputError = '';
|
|
1442
|
+
}
|
|
1200
1443
|
// --- Render Methods ---
|
|
1201
1444
|
/**
|
|
1202
1445
|
* Get the formatted pay amount for buttons (net amount including fees)
|
|
@@ -1685,12 +1928,74 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
1685
1928
|
</div>
|
|
1686
1929
|
`;
|
|
1687
1930
|
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Render phone number input for payment methods that require it
|
|
1933
|
+
* Matches react-phone-input-2 behavior:
|
|
1934
|
+
* - Country dropdown with flags and dial codes
|
|
1935
|
+
* - Auto-formatting based on country
|
|
1936
|
+
* - Japan-only lock for konbini/payeasy
|
|
1937
|
+
*/
|
|
1938
|
+
renderPhoneInput(methodId) {
|
|
1939
|
+
const isJapanOnly = this.isJapanOnlyPhoneMethod(methodId);
|
|
1940
|
+
// For Japan-only methods, lock country to Japan
|
|
1941
|
+
if (isJapanOnly && this.selectedCountry !== 'JP') {
|
|
1942
|
+
this.selectedCountry = 'JP';
|
|
1943
|
+
}
|
|
1944
|
+
return html `
|
|
1945
|
+
<div class="phone-input-section">
|
|
1946
|
+
<label class="phone-input-label">
|
|
1947
|
+
${isJapanOnly ? 'Phone number (Japan)' : 'Phone number'}
|
|
1948
|
+
<span style="color: var(--op-color-danger);">*</span>
|
|
1949
|
+
</label>
|
|
1950
|
+
<div class="phone-input-wrapper">
|
|
1951
|
+
<select
|
|
1952
|
+
class="country-code-select"
|
|
1953
|
+
.value=${this.selectedCountry}
|
|
1954
|
+
@change=${this.handleCountryChange}
|
|
1955
|
+
?disabled=${isJapanOnly}
|
|
1956
|
+
>
|
|
1957
|
+
${isJapanOnly
|
|
1958
|
+
? html `<option value="JP">🇯🇵 +81</option>`
|
|
1959
|
+
: this.COUNTRY_OPTIONS.map((country) => html `
|
|
1960
|
+
<option value=${country.code} ?selected=${this.selectedCountry === country.code}>
|
|
1961
|
+
${country.flag} ${country.dialCode}
|
|
1962
|
+
</option>
|
|
1963
|
+
`)}
|
|
1964
|
+
</select>
|
|
1965
|
+
<input
|
|
1966
|
+
type="tel"
|
|
1967
|
+
class="phone-input-field ${this.phoneInputError ? 'error' : ''}"
|
|
1968
|
+
placeholder=${isJapanOnly
|
|
1969
|
+
? 'Enter your Japanese phone number'
|
|
1970
|
+
: 'Enter your phone number'}
|
|
1971
|
+
.value=${this.phoneInputValue}
|
|
1972
|
+
@input=${this.handlePhoneInputChange}
|
|
1973
|
+
@blur=${() => this.validatePhoneInput()}
|
|
1974
|
+
/>
|
|
1975
|
+
</div>
|
|
1976
|
+
${this.phoneInputError
|
|
1977
|
+
? html `<div class="phone-input-error">${this.phoneInputError}</div>`
|
|
1978
|
+
: ''}
|
|
1979
|
+
${isJapanOnly
|
|
1980
|
+
? html `<div class="phone-japan-note">
|
|
1981
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1982
|
+
<circle cx="12" cy="12" r="10"/>
|
|
1983
|
+
<path d="M12 16v-4M12 8h.01"/>
|
|
1984
|
+
</svg>
|
|
1985
|
+
Phone number must be a valid Japanese number (+81)
|
|
1986
|
+
</div>`
|
|
1987
|
+
: ''}
|
|
1988
|
+
</div>
|
|
1989
|
+
`;
|
|
1990
|
+
}
|
|
1688
1991
|
renderBoostContent() {
|
|
1689
1992
|
return html `
|
|
1690
1993
|
<div class="paynow-container">
|
|
1691
1994
|
<div class="paynow-instructions">
|
|
1692
1995
|
<h3 class="instructions-title">How to Pay with Boost:</h3>
|
|
1693
1996
|
|
|
1997
|
+
${this.renderPhoneInput(PAYMENT_METHODS.BOOST)}
|
|
1998
|
+
|
|
1694
1999
|
<div class="instruction-steps">
|
|
1695
2000
|
<div class="instruction-step">
|
|
1696
2001
|
<div class="step-number">1</div>
|
|
@@ -1734,6 +2039,8 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
1734
2039
|
<div class="paynow-instructions">
|
|
1735
2040
|
<h3 class="instructions-title">How to Pay with ShopeePay:</h3>
|
|
1736
2041
|
|
|
2042
|
+
${this.renderPhoneInput(PAYMENT_METHODS.SHOPEEPAY)}
|
|
2043
|
+
|
|
1737
2044
|
<div class="instruction-steps">
|
|
1738
2045
|
<div class="instruction-step">
|
|
1739
2046
|
<div class="step-number">1</div>
|
|
@@ -1777,6 +2084,8 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
1777
2084
|
<div class="paynow-instructions">
|
|
1778
2085
|
<h3 class="instructions-title">How to Pay with Atome:</h3>
|
|
1779
2086
|
|
|
2087
|
+
${this.renderPhoneInput(PAYMENT_METHODS.ATOM)}
|
|
2088
|
+
|
|
1780
2089
|
<div class="instruction-steps">
|
|
1781
2090
|
<div class="instruction-step">
|
|
1782
2091
|
<div class="step-number">1</div>
|
|
@@ -1820,6 +2129,8 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
1820
2129
|
<div class="paynow-instructions">
|
|
1821
2130
|
<h3 class="instructions-title">How to Pay with Dana:</h3>
|
|
1822
2131
|
|
|
2132
|
+
${this.renderPhoneInput(PAYMENT_METHODS.DANA)}
|
|
2133
|
+
|
|
1823
2134
|
<div class="instruction-steps">
|
|
1824
2135
|
<div class="instruction-step">
|
|
1825
2136
|
<div class="step-number">1</div>
|
|
@@ -1863,6 +2174,8 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
1863
2174
|
<div class="paynow-instructions">
|
|
1864
2175
|
<h3 class="instructions-title">How to Pay with Touch 'n Go:</h3>
|
|
1865
2176
|
|
|
2177
|
+
${this.renderPhoneInput(PAYMENT_METHODS.TNG)}
|
|
2178
|
+
|
|
1866
2179
|
<div class="instruction-steps">
|
|
1867
2180
|
<div class="instruction-step">
|
|
1868
2181
|
<div class="step-number">1</div>
|
|
@@ -1906,6 +2219,8 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
1906
2219
|
<div class="paynow-instructions">
|
|
1907
2220
|
<h3 class="instructions-title">How to Pay with Alipay:</h3>
|
|
1908
2221
|
|
|
2222
|
+
${this.renderPhoneInput(PAYMENT_METHODS.ALIPAYCN)}
|
|
2223
|
+
|
|
1909
2224
|
<div class="instruction-steps">
|
|
1910
2225
|
<div class="instruction-step">
|
|
1911
2226
|
<div class="step-number">1</div>
|
|
@@ -1949,6 +2264,8 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
1949
2264
|
<div class="paynow-instructions">
|
|
1950
2265
|
<h3 class="instructions-title">How to Pay with AlipayHK:</h3>
|
|
1951
2266
|
|
|
2267
|
+
${this.renderPhoneInput(PAYMENT_METHODS.ALIPAYHK)}
|
|
2268
|
+
|
|
1952
2269
|
<div class="instruction-steps">
|
|
1953
2270
|
<div class="instruction-step">
|
|
1954
2271
|
<div class="step-number">1</div>
|
|
@@ -1992,6 +2309,8 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
1992
2309
|
<div class="paynow-instructions">
|
|
1993
2310
|
<h3 class="instructions-title">How to Pay with GCash:</h3>
|
|
1994
2311
|
|
|
2312
|
+
${this.renderPhoneInput(PAYMENT_METHODS.GCASH)}
|
|
2313
|
+
|
|
1995
2314
|
<div class="instruction-steps">
|
|
1996
2315
|
<div class="instruction-step">
|
|
1997
2316
|
<div class="step-number">1</div>
|
|
@@ -2035,6 +2354,8 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
2035
2354
|
<div class="paynow-instructions">
|
|
2036
2355
|
<h3 class="instructions-title">How to Pay with Konbini:</h3>
|
|
2037
2356
|
|
|
2357
|
+
${this.renderPhoneInput(PAYMENT_METHODS.KONBINI)}
|
|
2358
|
+
|
|
2038
2359
|
<div class="instruction-steps">
|
|
2039
2360
|
<div class="instruction-step">
|
|
2040
2361
|
<div class="step-number">1</div>
|
|
@@ -2078,6 +2399,8 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
2078
2399
|
<div class="paynow-instructions">
|
|
2079
2400
|
<h3 class="instructions-title">How to Pay with Pay-Easy:</h3>
|
|
2080
2401
|
|
|
2402
|
+
${this.renderPhoneInput(PAYMENT_METHODS.PAYEASY)}
|
|
2403
|
+
|
|
2081
2404
|
<div class="instruction-steps">
|
|
2082
2405
|
<div class="instruction-step">
|
|
2083
2406
|
<div class="step-number">1</div>
|
|
@@ -2086,7 +2409,9 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
2086
2409
|
|
|
2087
2410
|
<div class="instruction-step">
|
|
2088
2411
|
<div class="step-number">2</div>
|
|
2089
|
-
<p class="step-text">
|
|
2412
|
+
<p class="step-text">
|
|
2413
|
+
Use your bank's ATM or online banking to complete the transfer.
|
|
2414
|
+
</p>
|
|
2090
2415
|
</div>
|
|
2091
2416
|
|
|
2092
2417
|
<div class="instruction-step">
|
|
@@ -2115,6 +2440,194 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
2115
2440
|
</div>
|
|
2116
2441
|
`;
|
|
2117
2442
|
}
|
|
2443
|
+
renderGrabPaySGContent() {
|
|
2444
|
+
return html `
|
|
2445
|
+
<div class="paynow-container">
|
|
2446
|
+
<div class="paynow-instructions">
|
|
2447
|
+
<h3 class="instructions-title">How to Pay with GrabPay:</h3>
|
|
2448
|
+
|
|
2449
|
+
<div class="instruction-steps">
|
|
2450
|
+
<div class="instruction-step">
|
|
2451
|
+
<div class="step-number">1</div>
|
|
2452
|
+
<p class="step-text">Click <strong>"Pay"</strong> to open GrabPay.</p>
|
|
2453
|
+
</div>
|
|
2454
|
+
|
|
2455
|
+
<div class="instruction-step">
|
|
2456
|
+
<div class="step-number">2</div>
|
|
2457
|
+
<p class="step-text">Log in to your GrabPay account.</p>
|
|
2458
|
+
</div>
|
|
2459
|
+
|
|
2460
|
+
<div class="instruction-step">
|
|
2461
|
+
<div class="step-number">3</div>
|
|
2462
|
+
<p class="step-text">Confirm and complete your payment.</p>
|
|
2463
|
+
</div>
|
|
2464
|
+
</div>
|
|
2465
|
+
|
|
2466
|
+
<div class="submit-section">
|
|
2467
|
+
<button
|
|
2468
|
+
class="pay-button ${this.currentState.status === 'processing' ||
|
|
2469
|
+
this.currentState.status === 'requires_action'
|
|
2470
|
+
? 'loading'
|
|
2471
|
+
: ''}"
|
|
2472
|
+
@click=${this.handleGrabPaySGPayment}
|
|
2473
|
+
?disabled=${this.currentState.status === 'processing' ||
|
|
2474
|
+
this.currentState.status === 'requires_action'}
|
|
2475
|
+
>
|
|
2476
|
+
${this.currentState.status === 'processing' ||
|
|
2477
|
+
this.currentState.status === 'requires_action'
|
|
2478
|
+
? 'Redirecting...'
|
|
2479
|
+
: `Pay ${this.getPayButtonAmount()}`}
|
|
2480
|
+
</button>
|
|
2481
|
+
</div>
|
|
2482
|
+
</div>
|
|
2483
|
+
</div>
|
|
2484
|
+
`;
|
|
2485
|
+
}
|
|
2486
|
+
renderFPXContent() {
|
|
2487
|
+
// Fetch banks when component is first rendered for FPX
|
|
2488
|
+
if (this.availableBanks.length === 0 && !this.banksLoading) {
|
|
2489
|
+
this.fetchBanks('fpx');
|
|
2490
|
+
}
|
|
2491
|
+
const filteredBanks = this.getFilteredBanks();
|
|
2492
|
+
const isProcessing = this.currentState.status === 'processing' || this.currentState.status === 'requires_action';
|
|
2493
|
+
return html `
|
|
2494
|
+
<div class="fpx-container">
|
|
2495
|
+
<div class="fpx-instructions">
|
|
2496
|
+
<h3 class="instructions-title">Pay with FPX Online Banking</h3>
|
|
2497
|
+
<p class="fpx-description">
|
|
2498
|
+
Select your bank below and you'll be redirected to complete the payment securely.
|
|
2499
|
+
</p>
|
|
2500
|
+
</div>
|
|
2501
|
+
|
|
2502
|
+
<div class="bank-selection-wrapper">
|
|
2503
|
+
<label class="form-label">Select Your Bank</label>
|
|
2504
|
+
<div class="bank-search-container">
|
|
2505
|
+
<input
|
|
2506
|
+
type="text"
|
|
2507
|
+
class="input bank-search-input"
|
|
2508
|
+
placeholder="Search for your bank..."
|
|
2509
|
+
.value=${this.bankSearchQuery}
|
|
2510
|
+
@input=${(e) => {
|
|
2511
|
+
const target = e.target;
|
|
2512
|
+
this.bankSearchQuery = target.value;
|
|
2513
|
+
// Clear selection if user is typing
|
|
2514
|
+
if (this.selectedBank && target.value !== this.selectedBank.displayName) {
|
|
2515
|
+
this.selectedBank = null;
|
|
2516
|
+
}
|
|
2517
|
+
}}
|
|
2518
|
+
@focus=${() => {
|
|
2519
|
+
// Show all banks when focused
|
|
2520
|
+
if (this.selectedBank) {
|
|
2521
|
+
this.bankSearchQuery = '';
|
|
2522
|
+
this.selectedBank = null;
|
|
2523
|
+
}
|
|
2524
|
+
}}
|
|
2525
|
+
/>
|
|
2526
|
+
<div class="bank-search-icon">
|
|
2527
|
+
<svg
|
|
2528
|
+
width="20"
|
|
2529
|
+
height="20"
|
|
2530
|
+
viewBox="0 0 24 24"
|
|
2531
|
+
fill="none"
|
|
2532
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2533
|
+
>
|
|
2534
|
+
<path
|
|
2535
|
+
d="M21 21L15 15M17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10Z"
|
|
2536
|
+
stroke="currentColor"
|
|
2537
|
+
stroke-width="2"
|
|
2538
|
+
stroke-linecap="round"
|
|
2539
|
+
stroke-linejoin="round"
|
|
2540
|
+
/>
|
|
2541
|
+
</svg>
|
|
2542
|
+
</div>
|
|
2543
|
+
</div>
|
|
2544
|
+
|
|
2545
|
+
${this.banksLoading
|
|
2546
|
+
? html `
|
|
2547
|
+
<div class="bank-loading">
|
|
2548
|
+
<div class="bank-loading-spinner"></div>
|
|
2549
|
+
<span>Loading banks...</span>
|
|
2550
|
+
</div>
|
|
2551
|
+
`
|
|
2552
|
+
: html `
|
|
2553
|
+
<div class="bank-grid">
|
|
2554
|
+
${filteredBanks.map((bank) => html `
|
|
2555
|
+
<div
|
|
2556
|
+
class="bank-card ${this.selectedBank?.bankName === bank.bankName
|
|
2557
|
+
? 'selected'
|
|
2558
|
+
: ''}"
|
|
2559
|
+
@click=${() => this.handleBankSelect(bank)}
|
|
2560
|
+
>
|
|
2561
|
+
<div class="bank-logo-wrapper">
|
|
2562
|
+
${bank.logoUrl
|
|
2563
|
+
? html `<img
|
|
2564
|
+
class="bank-logo"
|
|
2565
|
+
src="${bank.logoUrl}"
|
|
2566
|
+
alt="${bank.displayName}"
|
|
2567
|
+
@error=${(e) => {
|
|
2568
|
+
// Hide image and show fallback on error
|
|
2569
|
+
const img = e.target;
|
|
2570
|
+
img.style.display = 'none';
|
|
2571
|
+
const parent = img.parentElement;
|
|
2572
|
+
if (parent) {
|
|
2573
|
+
parent.innerHTML = `<div class="bank-logo-fallback">${bank.displayName.charAt(0)}</div>`;
|
|
2574
|
+
}
|
|
2575
|
+
}}
|
|
2576
|
+
/>`
|
|
2577
|
+
: html `<div class="bank-logo-fallback">
|
|
2578
|
+
${bank.displayName.charAt(0)}
|
|
2579
|
+
</div>`}
|
|
2580
|
+
</div>
|
|
2581
|
+
<span class="bank-name">${bank.displayName}</span>
|
|
2582
|
+
${this.selectedBank?.bankName === bank.bankName
|
|
2583
|
+
? html `<div class="bank-check">
|
|
2584
|
+
<svg
|
|
2585
|
+
width="16"
|
|
2586
|
+
height="16"
|
|
2587
|
+
viewBox="0 0 24 24"
|
|
2588
|
+
fill="none"
|
|
2589
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2590
|
+
>
|
|
2591
|
+
<path
|
|
2592
|
+
d="M20 6L9 17L4 12"
|
|
2593
|
+
stroke="currentColor"
|
|
2594
|
+
stroke-width="2"
|
|
2595
|
+
stroke-linecap="round"
|
|
2596
|
+
stroke-linejoin="round"
|
|
2597
|
+
/>
|
|
2598
|
+
</svg>
|
|
2599
|
+
</div>`
|
|
2600
|
+
: ''}
|
|
2601
|
+
</div>
|
|
2602
|
+
`)}
|
|
2603
|
+
</div>
|
|
2604
|
+
|
|
2605
|
+
${filteredBanks.length === 0 && this.bankSearchQuery
|
|
2606
|
+
? html `
|
|
2607
|
+
<div class="bank-no-results">
|
|
2608
|
+
<p>No banks found matching "${this.bankSearchQuery}"</p>
|
|
2609
|
+
</div>
|
|
2610
|
+
`
|
|
2611
|
+
: ''}
|
|
2612
|
+
`}
|
|
2613
|
+
</div>
|
|
2614
|
+
|
|
2615
|
+
<div class="submit-section">
|
|
2616
|
+
<button
|
|
2617
|
+
class="pay-button ${isProcessing ? 'loading' : ''}"
|
|
2618
|
+
@click=${this.handleFPXPayment}
|
|
2619
|
+
?disabled=${isProcessing || !this.selectedBank}
|
|
2620
|
+
>
|
|
2621
|
+
${isProcessing
|
|
2622
|
+
? 'Redirecting to bank...'
|
|
2623
|
+
: !this.selectedBank
|
|
2624
|
+
? 'Select a bank to continue'
|
|
2625
|
+
: `Pay ${this.getPayButtonAmount()}`}
|
|
2626
|
+
</button>
|
|
2627
|
+
</div>
|
|
2628
|
+
</div>
|
|
2629
|
+
`;
|
|
2630
|
+
}
|
|
2118
2631
|
render3DSModal() {
|
|
2119
2632
|
if (!this.show3DSModal || !this.nextActionUrl) {
|
|
2120
2633
|
return null;
|
|
@@ -2222,6 +2735,10 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
2222
2735
|
return 'Konbini';
|
|
2223
2736
|
case PAYMENT_METHODS.PAYEASY:
|
|
2224
2737
|
return 'Pay-Easy';
|
|
2738
|
+
case PAYMENT_METHODS.GRABPAY_SG:
|
|
2739
|
+
return 'GrabPay';
|
|
2740
|
+
case PAYMENT_METHODS.FPX:
|
|
2741
|
+
return 'FPX Online Banking';
|
|
2225
2742
|
default:
|
|
2226
2743
|
return id;
|
|
2227
2744
|
}
|
|
@@ -2255,7 +2772,11 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
2255
2772
|
else if (id === PAYMENT_METHODS.PROMPTPAY) {
|
|
2256
2773
|
return html `
|
|
2257
2774
|
<div class="method-icon-right">
|
|
2258
|
-
<img
|
|
2775
|
+
<img
|
|
2776
|
+
src="${PROMPTPAY_ICON_DATA_URL}"
|
|
2777
|
+
alt="PromptPay"
|
|
2778
|
+
style="width: 45px; height: auto;"
|
|
2779
|
+
/>
|
|
2259
2780
|
</div>
|
|
2260
2781
|
`;
|
|
2261
2782
|
}
|
|
@@ -2283,7 +2804,11 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
2283
2804
|
else if (id === PAYMENT_METHODS.SHOPEEPAY) {
|
|
2284
2805
|
return html `
|
|
2285
2806
|
<div class="method-icon-right">
|
|
2286
|
-
<img
|
|
2807
|
+
<img
|
|
2808
|
+
src="${SHOPEEPAY_ICON_DATA_URL}"
|
|
2809
|
+
alt="ShopeePay"
|
|
2810
|
+
style="width: 45px; height: auto;"
|
|
2811
|
+
/>
|
|
2287
2812
|
</div>
|
|
2288
2813
|
`;
|
|
2289
2814
|
}
|
|
@@ -2318,7 +2843,11 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
2318
2843
|
else if (id === PAYMENT_METHODS.ALIPAYHK) {
|
|
2319
2844
|
return html `
|
|
2320
2845
|
<div class="method-icon-right">
|
|
2321
|
-
<img
|
|
2846
|
+
<img
|
|
2847
|
+
src="${ALIPAYHK_ICON_DATA_URL}"
|
|
2848
|
+
alt="AlipayHK"
|
|
2849
|
+
style="width: 45px; height: auto;"
|
|
2850
|
+
/>
|
|
2322
2851
|
</div>
|
|
2323
2852
|
`;
|
|
2324
2853
|
}
|
|
@@ -2341,6 +2870,24 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
2341
2870
|
<div class="method-icon-right">
|
|
2342
2871
|
<img src="${PAYEASY_ICON_DATA_URL}" alt="Pay-Easy" style="width: 45px; height: auto;" />
|
|
2343
2872
|
</div>
|
|
2873
|
+
`;
|
|
2874
|
+
}
|
|
2875
|
+
else if (id === PAYMENT_METHODS.GRABPAY_SG) {
|
|
2876
|
+
return html `
|
|
2877
|
+
<div class="method-icon-right">
|
|
2878
|
+
<img src="${GRABPAY_ICON_DATA_URL}" alt="GrabPay" style="width: 45px; height: auto;" />
|
|
2879
|
+
</div>
|
|
2880
|
+
`;
|
|
2881
|
+
}
|
|
2882
|
+
else if (id === PAYMENT_METHODS.FPX) {
|
|
2883
|
+
return html `
|
|
2884
|
+
<div class="method-icon-right">
|
|
2885
|
+
<img
|
|
2886
|
+
src="${FPX_ICON_DATA_URL}"
|
|
2887
|
+
alt="FPX Online Banking"
|
|
2888
|
+
style="width: 45px; height: auto;"
|
|
2889
|
+
/>
|
|
2890
|
+
</div>
|
|
2344
2891
|
`;
|
|
2345
2892
|
}
|
|
2346
2893
|
return null;
|
|
@@ -2421,7 +2968,11 @@ let OnePayment = class OnePayment extends LitElement {
|
|
|
2421
2968
|
? this.renderKonbiniContent()
|
|
2422
2969
|
: method.id === PAYMENT_METHODS.PAYEASY
|
|
2423
2970
|
? this.renderPayEasyContent()
|
|
2424
|
-
:
|
|
2971
|
+
: method.id === PAYMENT_METHODS.GRABPAY_SG
|
|
2972
|
+
? this.renderGrabPaySGContent()
|
|
2973
|
+
: method.id === PAYMENT_METHODS.FPX
|
|
2974
|
+
? this.renderFPXContent()
|
|
2975
|
+
: null}
|
|
2425
2976
|
</div>`
|
|
2426
2977
|
: ''}
|
|
2427
2978
|
</div>
|
|
@@ -3760,6 +4311,332 @@ OnePayment.styles = css `
|
|
|
3760
4311
|
transform: translateX(-50%) translateY(0);
|
|
3761
4312
|
}
|
|
3762
4313
|
}
|
|
4314
|
+
|
|
4315
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
4316
|
+
FPX BANK SELECTION STYLES
|
|
4317
|
+
═══════════════════════════════════════════════════════════════ */
|
|
4318
|
+
|
|
4319
|
+
.fpx-container {
|
|
4320
|
+
display: flex;
|
|
4321
|
+
flex-direction: column;
|
|
4322
|
+
gap: 1.5rem;
|
|
4323
|
+
}
|
|
4324
|
+
|
|
4325
|
+
.fpx-instructions {
|
|
4326
|
+
text-align: left;
|
|
4327
|
+
}
|
|
4328
|
+
|
|
4329
|
+
.fpx-description {
|
|
4330
|
+
color: var(--op-color-text-secondary);
|
|
4331
|
+
font-size: var(--op-font-size-sm);
|
|
4332
|
+
margin: 0.5rem 0 0 0;
|
|
4333
|
+
line-height: 1.5;
|
|
4334
|
+
}
|
|
4335
|
+
|
|
4336
|
+
.bank-selection-wrapper {
|
|
4337
|
+
display: flex;
|
|
4338
|
+
flex-direction: column;
|
|
4339
|
+
gap: 0.75rem;
|
|
4340
|
+
}
|
|
4341
|
+
|
|
4342
|
+
.bank-search-container {
|
|
4343
|
+
position: relative;
|
|
4344
|
+
width: 100%;
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
.bank-search-input {
|
|
4348
|
+
padding-right: 2.75rem;
|
|
4349
|
+
}
|
|
4350
|
+
|
|
4351
|
+
.bank-search-icon {
|
|
4352
|
+
position: absolute;
|
|
4353
|
+
right: 0.75rem;
|
|
4354
|
+
top: 50%;
|
|
4355
|
+
transform: translateY(-50%);
|
|
4356
|
+
color: var(--op-color-text-secondary);
|
|
4357
|
+
pointer-events: none;
|
|
4358
|
+
display: flex;
|
|
4359
|
+
align-items: center;
|
|
4360
|
+
justify-content: center;
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4363
|
+
.bank-loading {
|
|
4364
|
+
display: flex;
|
|
4365
|
+
flex-direction: column;
|
|
4366
|
+
align-items: center;
|
|
4367
|
+
justify-content: center;
|
|
4368
|
+
padding: 2rem;
|
|
4369
|
+
gap: 1rem;
|
|
4370
|
+
color: var(--op-color-text-secondary);
|
|
4371
|
+
font-size: var(--op-font-size-sm);
|
|
4372
|
+
}
|
|
4373
|
+
|
|
4374
|
+
.bank-loading-spinner {
|
|
4375
|
+
width: 32px;
|
|
4376
|
+
height: 32px;
|
|
4377
|
+
border: 3px solid var(--op-color-border);
|
|
4378
|
+
border-top-color: var(--op-color-primary);
|
|
4379
|
+
border-radius: 50%;
|
|
4380
|
+
animation: spin 1s linear infinite;
|
|
4381
|
+
}
|
|
4382
|
+
|
|
4383
|
+
.bank-grid {
|
|
4384
|
+
display: grid;
|
|
4385
|
+
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
4386
|
+
gap: 0.75rem;
|
|
4387
|
+
max-height: 320px;
|
|
4388
|
+
overflow-y: auto;
|
|
4389
|
+
padding: 0.25rem;
|
|
4390
|
+
margin: -0.25rem;
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4393
|
+
.bank-grid::-webkit-scrollbar {
|
|
4394
|
+
width: 6px;
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
.bank-grid::-webkit-scrollbar-track {
|
|
4398
|
+
background: var(--op-color-surface);
|
|
4399
|
+
border-radius: 3px;
|
|
4400
|
+
}
|
|
4401
|
+
|
|
4402
|
+
.bank-grid::-webkit-scrollbar-thumb {
|
|
4403
|
+
background: var(--op-color-border);
|
|
4404
|
+
border-radius: 3px;
|
|
4405
|
+
}
|
|
4406
|
+
|
|
4407
|
+
.bank-grid::-webkit-scrollbar-thumb:hover {
|
|
4408
|
+
background: var(--op-color-text-secondary);
|
|
4409
|
+
}
|
|
4410
|
+
|
|
4411
|
+
.bank-card {
|
|
4412
|
+
position: relative;
|
|
4413
|
+
display: flex;
|
|
4414
|
+
flex-direction: column;
|
|
4415
|
+
align-items: center;
|
|
4416
|
+
padding: 1rem 0.75rem;
|
|
4417
|
+
background: var(--op-color-background);
|
|
4418
|
+
border: 2px solid var(--op-color-border);
|
|
4419
|
+
border-radius: var(--op-border-radius);
|
|
4420
|
+
cursor: pointer;
|
|
4421
|
+
transition: all 0.2s ease;
|
|
4422
|
+
gap: 0.625rem;
|
|
4423
|
+
min-height: 100px;
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4426
|
+
.bank-card:hover {
|
|
4427
|
+
border-color: var(--op-color-text-secondary);
|
|
4428
|
+
background: var(--op-color-surface);
|
|
4429
|
+
transform: translateY(-2px);
|
|
4430
|
+
box-shadow: var(--op-box-shadow);
|
|
4431
|
+
}
|
|
4432
|
+
|
|
4433
|
+
.bank-card.selected {
|
|
4434
|
+
border-color: var(--op-color-primary);
|
|
4435
|
+
background: var(--op-color-surface);
|
|
4436
|
+
box-shadow: 0 0 0 3px rgba(42, 35, 39, 0.1);
|
|
4437
|
+
}
|
|
4438
|
+
|
|
4439
|
+
:host([data-theme='dark']) .bank-card.selected {
|
|
4440
|
+
box-shadow: 0 0 0 3px rgba(255, 190, 50, 0.15);
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
.bank-logo-wrapper {
|
|
4444
|
+
width: 48px;
|
|
4445
|
+
height: 48px;
|
|
4446
|
+
display: flex;
|
|
4447
|
+
align-items: center;
|
|
4448
|
+
justify-content: center;
|
|
4449
|
+
flex-shrink: 0;
|
|
4450
|
+
}
|
|
4451
|
+
|
|
4452
|
+
.bank-logo {
|
|
4453
|
+
max-width: 100%;
|
|
4454
|
+
max-height: 100%;
|
|
4455
|
+
object-fit: contain;
|
|
4456
|
+
border-radius: 4px;
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
.bank-logo-fallback {
|
|
4460
|
+
width: 48px;
|
|
4461
|
+
height: 48px;
|
|
4462
|
+
display: flex;
|
|
4463
|
+
align-items: center;
|
|
4464
|
+
justify-content: center;
|
|
4465
|
+
background: linear-gradient(135deg, var(--op-color-primary) 0%, var(--op-color-primary-hover) 100%);
|
|
4466
|
+
color: var(--op-color-primary-text);
|
|
4467
|
+
font-size: 1.25rem;
|
|
4468
|
+
font-weight: 700;
|
|
4469
|
+
border-radius: var(--op-border-radius);
|
|
4470
|
+
}
|
|
4471
|
+
|
|
4472
|
+
.bank-name {
|
|
4473
|
+
font-size: 0.75rem;
|
|
4474
|
+
font-weight: 500;
|
|
4475
|
+
color: var(--op-color-text);
|
|
4476
|
+
text-align: center;
|
|
4477
|
+
line-height: 1.3;
|
|
4478
|
+
overflow: hidden;
|
|
4479
|
+
text-overflow: ellipsis;
|
|
4480
|
+
display: -webkit-box;
|
|
4481
|
+
-webkit-line-clamp: 2;
|
|
4482
|
+
-webkit-box-orient: vertical;
|
|
4483
|
+
word-break: break-word;
|
|
4484
|
+
}
|
|
4485
|
+
|
|
4486
|
+
.bank-check {
|
|
4487
|
+
position: absolute;
|
|
4488
|
+
top: 0.5rem;
|
|
4489
|
+
right: 0.5rem;
|
|
4490
|
+
width: 20px;
|
|
4491
|
+
height: 20px;
|
|
4492
|
+
background: var(--op-color-primary);
|
|
4493
|
+
color: var(--op-color-primary-text);
|
|
4494
|
+
border-radius: 50%;
|
|
4495
|
+
display: flex;
|
|
4496
|
+
align-items: center;
|
|
4497
|
+
justify-content: center;
|
|
4498
|
+
animation: scaleIn 0.2s ease;
|
|
4499
|
+
}
|
|
4500
|
+
|
|
4501
|
+
.bank-no-results {
|
|
4502
|
+
text-align: center;
|
|
4503
|
+
padding: 2rem;
|
|
4504
|
+
color: var(--op-color-text-secondary);
|
|
4505
|
+
font-size: var(--op-font-size-sm);
|
|
4506
|
+
}
|
|
4507
|
+
|
|
4508
|
+
.bank-no-results p {
|
|
4509
|
+
margin: 0;
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4512
|
+
/* Mobile responsiveness for bank grid */
|
|
4513
|
+
@media (max-width: 480px) {
|
|
4514
|
+
.bank-grid {
|
|
4515
|
+
grid-template-columns: repeat(2, 1fr);
|
|
4516
|
+
max-height: 280px;
|
|
4517
|
+
}
|
|
4518
|
+
|
|
4519
|
+
.bank-card {
|
|
4520
|
+
padding: 0.75rem 0.5rem;
|
|
4521
|
+
min-height: 90px;
|
|
4522
|
+
}
|
|
4523
|
+
|
|
4524
|
+
.bank-logo-wrapper {
|
|
4525
|
+
width: 40px;
|
|
4526
|
+
height: 40px;
|
|
4527
|
+
}
|
|
4528
|
+
|
|
4529
|
+
.bank-logo-fallback {
|
|
4530
|
+
width: 40px;
|
|
4531
|
+
height: 40px;
|
|
4532
|
+
font-size: 1rem;
|
|
4533
|
+
}
|
|
4534
|
+
|
|
4535
|
+
.bank-name {
|
|
4536
|
+
font-size: 0.6875rem;
|
|
4537
|
+
}
|
|
4538
|
+
}
|
|
4539
|
+
|
|
4540
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
4541
|
+
PHONE NUMBER INPUT STYLES
|
|
4542
|
+
═══════════════════════════════════════════════════════════════ */
|
|
4543
|
+
|
|
4544
|
+
.phone-input-section {
|
|
4545
|
+
margin-bottom: 1rem;
|
|
4546
|
+
}
|
|
4547
|
+
|
|
4548
|
+
.phone-input-label {
|
|
4549
|
+
display: block;
|
|
4550
|
+
font-size: var(--op-font-size-sm);
|
|
4551
|
+
font-weight: 500;
|
|
4552
|
+
color: var(--op-color-text);
|
|
4553
|
+
margin-bottom: 0.5rem;
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
.phone-input-wrapper {
|
|
4557
|
+
display: flex;
|
|
4558
|
+
gap: 0.5rem;
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4561
|
+
.country-code-select {
|
|
4562
|
+
flex-shrink: 0;
|
|
4563
|
+
width: 100px;
|
|
4564
|
+
padding: 0.75rem;
|
|
4565
|
+
font-size: var(--op-font-size-base);
|
|
4566
|
+
border: 1px solid var(--op-color-border);
|
|
4567
|
+
border-radius: var(--op-border-radius);
|
|
4568
|
+
background: var(--op-color-background);
|
|
4569
|
+
color: var(--op-color-text);
|
|
4570
|
+
cursor: pointer;
|
|
4571
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
4572
|
+
}
|
|
4573
|
+
|
|
4574
|
+
.country-code-select:focus {
|
|
4575
|
+
outline: none;
|
|
4576
|
+
border-color: var(--op-color-border-focus);
|
|
4577
|
+
box-shadow: 0 0 0 3px rgba(42, 35, 39, 0.1);
|
|
4578
|
+
}
|
|
4579
|
+
|
|
4580
|
+
.country-code-select:disabled {
|
|
4581
|
+
background: var(--op-color-surface);
|
|
4582
|
+
cursor: not-allowed;
|
|
4583
|
+
opacity: 0.7;
|
|
4584
|
+
}
|
|
4585
|
+
|
|
4586
|
+
.phone-input-field {
|
|
4587
|
+
flex: 1;
|
|
4588
|
+
padding: 0.75rem;
|
|
4589
|
+
font-size: var(--op-font-size-base);
|
|
4590
|
+
border: 1px solid var(--op-color-border);
|
|
4591
|
+
border-radius: var(--op-border-radius);
|
|
4592
|
+
background: var(--op-color-background);
|
|
4593
|
+
color: var(--op-color-text);
|
|
4594
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
4595
|
+
}
|
|
4596
|
+
|
|
4597
|
+
.phone-input-field:focus {
|
|
4598
|
+
outline: none;
|
|
4599
|
+
border-color: var(--op-color-border-focus);
|
|
4600
|
+
box-shadow: 0 0 0 3px rgba(42, 35, 39, 0.1);
|
|
4601
|
+
}
|
|
4602
|
+
|
|
4603
|
+
.phone-input-field::placeholder {
|
|
4604
|
+
color: var(--op-color-text-secondary);
|
|
4605
|
+
}
|
|
4606
|
+
|
|
4607
|
+
.phone-input-field.error {
|
|
4608
|
+
border-color: var(--op-color-danger);
|
|
4609
|
+
}
|
|
4610
|
+
|
|
4611
|
+
.phone-input-field.error:focus {
|
|
4612
|
+
box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.1);
|
|
4613
|
+
}
|
|
4614
|
+
|
|
4615
|
+
.phone-input-error {
|
|
4616
|
+
color: var(--op-color-danger);
|
|
4617
|
+
font-size: 0.75rem;
|
|
4618
|
+
margin-top: 0.375rem;
|
|
4619
|
+
}
|
|
4620
|
+
|
|
4621
|
+
.phone-japan-note {
|
|
4622
|
+
font-size: 0.75rem;
|
|
4623
|
+
color: var(--op-color-text-secondary);
|
|
4624
|
+
margin-top: 0.375rem;
|
|
4625
|
+
display: flex;
|
|
4626
|
+
align-items: center;
|
|
4627
|
+
gap: 0.25rem;
|
|
4628
|
+
}
|
|
4629
|
+
|
|
4630
|
+
/* Mobile responsiveness for phone input */
|
|
4631
|
+
@media (max-width: 480px) {
|
|
4632
|
+
.phone-input-wrapper {
|
|
4633
|
+
flex-direction: column;
|
|
4634
|
+
}
|
|
4635
|
+
|
|
4636
|
+
.country-code-select {
|
|
4637
|
+
width: 100%;
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
3763
4640
|
`;
|
|
3764
4641
|
__decorate([
|
|
3765
4642
|
property({ type: Object })
|
|
@@ -3848,6 +4725,27 @@ __decorate([
|
|
|
3848
4725
|
__decorate([
|
|
3849
4726
|
state()
|
|
3850
4727
|
], OnePayment.prototype, "qrPollingInProgress", void 0);
|
|
4728
|
+
__decorate([
|
|
4729
|
+
state()
|
|
4730
|
+
], OnePayment.prototype, "availableBanks", void 0);
|
|
4731
|
+
__decorate([
|
|
4732
|
+
state()
|
|
4733
|
+
], OnePayment.prototype, "selectedBank", void 0);
|
|
4734
|
+
__decorate([
|
|
4735
|
+
state()
|
|
4736
|
+
], OnePayment.prototype, "banksLoading", void 0);
|
|
4737
|
+
__decorate([
|
|
4738
|
+
state()
|
|
4739
|
+
], OnePayment.prototype, "bankSearchQuery", void 0);
|
|
4740
|
+
__decorate([
|
|
4741
|
+
state()
|
|
4742
|
+
], OnePayment.prototype, "phoneInputValue", void 0);
|
|
4743
|
+
__decorate([
|
|
4744
|
+
state()
|
|
4745
|
+
], OnePayment.prototype, "phoneInputError", void 0);
|
|
4746
|
+
__decorate([
|
|
4747
|
+
state()
|
|
4748
|
+
], OnePayment.prototype, "selectedCountry", void 0);
|
|
3851
4749
|
OnePayment = __decorate([
|
|
3852
4750
|
customElement('one-payment')
|
|
3853
4751
|
], OnePayment);
|