@eeplatform/nuxt-layer-common 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.editorconfig +12 -0
  4. package/.github/workflows/main.yml +17 -0
  5. package/.github/workflows/publish.yml +39 -0
  6. package/.nuxtrc +1 -0
  7. package/.playground/app.vue +37 -0
  8. package/.playground/nuxt.config.ts +20 -0
  9. package/CHANGELOG.md +7 -0
  10. package/README.md +73 -0
  11. package/app.vue +3 -0
  12. package/components/AddPaymentMethod.vue +585 -0
  13. package/components/BtnUploadFile.vue +139 -0
  14. package/components/ConfirmDialog.vue +66 -0
  15. package/components/Container/Standard.vue +33 -0
  16. package/components/Input/Date.vue +177 -0
  17. package/components/Input/ListGroupSelection.vue +93 -0
  18. package/components/Input/NewDate.vue +123 -0
  19. package/components/Input/Number.vue +124 -0
  20. package/components/Input/Password.vue +35 -0
  21. package/components/InputLabel.vue +18 -0
  22. package/components/InvitationMain.vue +195 -0
  23. package/components/Layout/Header.vue +285 -0
  24. package/components/Layout/NavigationDrawer.vue +52 -0
  25. package/components/LinkHome.vue +9 -0
  26. package/components/ListItem.vue +35 -0
  27. package/components/LocalPagination.vue +41 -0
  28. package/components/MemberMain.vue +452 -0
  29. package/components/NavigationItem.vue +73 -0
  30. package/components/PlaceholderComponent.vue +34 -0
  31. package/components/RolePermissionFormCreate.vue +179 -0
  32. package/components/RolePermissionFormPreviewUpdate.vue +184 -0
  33. package/components/RolePermissionMain.vue +376 -0
  34. package/components/Snackbar.vue +23 -0
  35. package/components/SpecificAttr.vue +57 -0
  36. package/components/Std/Pagination.vue +52 -0
  37. package/components/SwitchContext.vue +109 -0
  38. package/components/SwitchOrg.vue +159 -0
  39. package/components/TableList.vue +130 -0
  40. package/composables/useAddress.ts +144 -0
  41. package/composables/useChartOfAccount.ts +62 -0
  42. package/composables/useCommonPermission.ts +130 -0
  43. package/composables/useFile.ts +29 -0
  44. package/composables/useInvoice.ts +42 -0
  45. package/composables/useLocal.ts +63 -0
  46. package/composables/useLocalAuth.ts +157 -0
  47. package/composables/useLocalSetup.ts +46 -0
  48. package/composables/useMember.ts +107 -0
  49. package/composables/useOrder.ts +22 -0
  50. package/composables/useOrg.ts +106 -0
  51. package/composables/useOrgPermission.ts +27 -0
  52. package/composables/usePayment.ts +22 -0
  53. package/composables/usePaymentMethod.ts +347 -0
  54. package/composables/usePermission.ts +54 -0
  55. package/composables/usePrice.ts +15 -0
  56. package/composables/usePromoCode.ts +43 -0
  57. package/composables/useRecapPermission.ts +26 -0
  58. package/composables/useRole.ts +89 -0
  59. package/composables/useSchoolPermission.ts +13 -0
  60. package/composables/useSubscription.ts +264 -0
  61. package/composables/useUser.ts +102 -0
  62. package/composables/useUtils.ts +294 -0
  63. package/composables/useVerification.ts +19 -0
  64. package/error.vue +41 -0
  65. package/eslint.config.js +3 -0
  66. package/layouts/plain.vue +7 -0
  67. package/middleware/01.auth.ts +14 -0
  68. package/middleware/org.ts +16 -0
  69. package/nuxt.config.ts +48 -0
  70. package/package.json +35 -0
  71. package/pages/index.vue +3 -0
  72. package/pages/payment-method-cancel-link.vue +31 -0
  73. package/pages/payment-method-failed-link.vue +31 -0
  74. package/pages/payment-method-linked.vue +31 -0
  75. package/pages/require-organization-membership.vue +47 -0
  76. package/pages/unauthorized.vue +29 -0
  77. package/plugins/API.ts +58 -0
  78. package/plugins/iconify.client.ts +5 -0
  79. package/plugins/vuetify.ts +55 -0
  80. package/public/bdo-logo.svg +4 -0
  81. package/public/bpi-logo.svg +74 -0
  82. package/public/chinabank-logo.svg +120 -0
  83. package/public/gcash-logo.png +0 -0
  84. package/public/gcash-logo.svg +65 -0
  85. package/public/grabpay-logo.svg +99 -0
  86. package/public/paymaya-logo.jpg +0 -0
  87. package/public/paymaya-logo.png +0 -0
  88. package/public/paymaya-logo.svg +25 -0
  89. package/public/qrph-c567ff0f-ab6d-4662-86bf-24c6c731d8a8-logo.svg +20 -0
  90. package/public/rcbc-logo.svg +15 -0
  91. package/public/shopeepay-logo.svg +89 -0
  92. package/public/ubp-logo.svg +88 -0
  93. package/tsconfig.json +3 -0
  94. package/types/address.d.ts +13 -0
  95. package/types/invoice.d.ts +28 -0
  96. package/types/local.d.ts +25 -0
  97. package/types/member.d.ts +12 -0
  98. package/types/org.d.ts +13 -0
  99. package/types/payment-method.d.ts +11 -0
  100. package/types/payment.d.ts +18 -0
  101. package/types/permission.d.ts +14 -0
  102. package/types/price.d.ts +17 -0
  103. package/types/promo-code.d.ts +19 -0
  104. package/types/role.d.ts +13 -0
  105. package/types/subscription.d.ts +29 -0
  106. package/types/user.d.ts +21 -0
  107. package/types/verification.d.ts +15 -0
  108. package/types/xendit.d.ts +3 -0
@@ -0,0 +1,585 @@
1
+ <template>
2
+ <v-row no-gutters>
3
+ <v-col cols="12">
4
+ <v-card variant="outlined" border="thin" rounded="md">
5
+ <v-expansion-panels
6
+ v-model="expansionPanel"
7
+ elevation="0"
8
+ variant="accordion"
9
+ focusable
10
+ >
11
+ <v-expansion-panel>
12
+ <template #title>
13
+ <span class="font-weight-bold py-2">E-Wallet</span>
14
+ </template>
15
+
16
+ <template #text>
17
+ <v-item-group
18
+ v-model.number="selectedPaymentMethod"
19
+ selected-class="bg-cyan-accent-1"
20
+ mandatory
21
+ >
22
+ <v-row class="pt-4 pb-3">
23
+ <template v-for="eWalletItem in supportedEwallets">
24
+ <v-col cols="12" lg="3" md="4" sm="4">
25
+ <v-item v-slot="{ toggle, selectedClass }">
26
+ <v-card
27
+ variant="outlined"
28
+ border="thin"
29
+ @click="toggle"
30
+ :class="['px-4', selectedClass]"
31
+ >
32
+ <v-img
33
+ :src="eWalletItem.logo"
34
+ width="100%"
35
+ height="60px"
36
+ >
37
+ </v-img>
38
+ </v-card>
39
+ </v-item>
40
+ </v-col>
41
+ </template>
42
+
43
+ <slot name="ewallet-action"></slot>
44
+ </v-row>
45
+ </v-item-group>
46
+ </template>
47
+ </v-expansion-panel>
48
+
49
+ <v-expansion-panel disabled>
50
+ <template #title>
51
+ <span class="font-weight-bold py-2">Credit/Debit Card</span>
52
+ </template>
53
+
54
+ <template #text>
55
+ <v-row no-gutters>
56
+ <v-col cols="12" lg="8" md="9" class="mt-2">
57
+ <v-row no-gutters>
58
+ <v-col cols="12">
59
+ <v-row no-gutters>
60
+ <v-col cols="12">
61
+ <InputLabel title="Card Number" />
62
+ <v-text-field
63
+ v-model="cardNumber"
64
+ @input="formatCardNumber"
65
+ :rules="[validateCardNumber, validateAcceptedCard]"
66
+ >
67
+ <template #prepend-inner>
68
+ <component :is="cardLogo" />
69
+ </template>
70
+ </v-text-field>
71
+ </v-col>
72
+ </v-row>
73
+ </v-col>
74
+
75
+ <v-col cols="12" class="mt-2">
76
+ <v-row no-gutters>
77
+ <v-sheet width="110px" class="mr-4">
78
+ <InputLabel title="Card Validity" />
79
+ <v-text-field
80
+ v-model="cardExpiration"
81
+ @input="formatExpiry"
82
+ :rules="[validateExpiry]"
83
+ >
84
+ <template #prepend-inner>
85
+ <v-img
86
+ :src="`${CARD_LOGO}#calendar`"
87
+ width="30"
88
+ height="30"
89
+ />
90
+ </template>
91
+ </v-text-field>
92
+ </v-sheet>
93
+
94
+ <v-sheet width="100px">
95
+ <InputLabel title="Card Validity" />
96
+ <v-text-field
97
+ v-model="cardSecurityCode"
98
+ @input="formatCardSecurityCode"
99
+ :rules="[validateCVV]"
100
+ >
101
+ <template #prepend-inner>
102
+ <v-img
103
+ :src="`${CARD_LOGO}#protected-registration`"
104
+ width="30"
105
+ height="30"
106
+ />
107
+ </template>
108
+ </v-text-field>
109
+ </v-sheet>
110
+ </v-row>
111
+ </v-col>
112
+
113
+ <v-col cols="12">
114
+ <v-row no-gutters>
115
+ <v-col cols="12">
116
+ <InputLabel title="Cardholder Name" />
117
+ <v-text-field
118
+ v-model="cardholderName"
119
+ :rules="[requiredRule]"
120
+ >
121
+ <template #prepend-inner>
122
+ <v-img
123
+ :src="`${CARD_LOGO}#discount-domain-club`"
124
+ width="30"
125
+ height="30"
126
+ />
127
+ </template>
128
+ </v-text-field>
129
+ </v-col>
130
+ </v-row>
131
+ </v-col>
132
+
133
+ <v-col cols="12" class="mt-2">
134
+ <v-row no-gutters>
135
+ <v-col cols="12" lg="9">
136
+ <InputLabel title="Email" />
137
+ <v-text-field
138
+ v-model="cardholderEmail"
139
+ :rules="[emailRule, requiredRule]"
140
+ >
141
+ </v-text-field>
142
+ </v-col>
143
+ </v-row>
144
+ </v-col>
145
+
146
+ <v-col cols="12" class="mt-2">
147
+ <v-row no-gutters>
148
+ <v-col cols="12" lg="9">
149
+ <InputLabel title="Mobile Number" />
150
+ <v-text-field
151
+ v-model="cardholderMobileNumber"
152
+ @input="formatContactNumber"
153
+ :rules="[requiredRule, validateContactNumber]"
154
+ placeholder="+XXX XXXX XXX XXX"
155
+ ></v-text-field>
156
+ </v-col>
157
+ </v-row>
158
+ </v-col>
159
+ </v-row>
160
+ </v-col>
161
+
162
+ <slot name="card-action"></slot>
163
+ </v-row>
164
+ </template>
165
+ </v-expansion-panel>
166
+
167
+ <v-expansion-panel disabled>
168
+ <template #title>
169
+ <span class="font-weight-bold py-2">Direct Debit</span>
170
+ </template>
171
+
172
+ <template #text>
173
+ <v-item-group
174
+ v-model.number="selectedPaymentMethod"
175
+ selected-class="bg-cyan-accent-1"
176
+ mandatory
177
+ >
178
+ <v-row class="pt-4 pb-3">
179
+ <template v-for="directDebit in supportedDirectDebit">
180
+ <v-col cols="12" lg="3" md="4" sm="4">
181
+ <v-item v-slot="{ toggle, selectedClass }">
182
+ <v-card
183
+ variant="outlined"
184
+ border="thin"
185
+ @click="toggle"
186
+ :class="['px-4', selectedClass]"
187
+ >
188
+ <v-img
189
+ :src="directDebit.logo"
190
+ width="100%"
191
+ height="60px"
192
+ >
193
+ </v-img>
194
+ </v-card>
195
+ </v-item>
196
+ </v-col>
197
+ </template>
198
+
199
+ <slot name="ewallet-action"></slot>
200
+ </v-row>
201
+ </v-item-group>
202
+ </template>
203
+ </v-expansion-panel>
204
+ </v-expansion-panels>
205
+ </v-card>
206
+ </v-col>
207
+ </v-row>
208
+ </template>
209
+
210
+ <script setup lang="ts">
211
+ const selectedPaymentMethod = ref<number | null>(null);
212
+ const channel = defineModel<string | null>("channel", {
213
+ default: null,
214
+ });
215
+ const type = defineModel<string | null>("type", {
216
+ default: null,
217
+ });
218
+
219
+ const expansionPanel = ref();
220
+
221
+ watch(expansionPanel, (value) => {
222
+ channel.value = null;
223
+ type.value = null;
224
+ if (value === 0) {
225
+ type.value = "EWALLET";
226
+ } else if (value === 1) {
227
+ type.value = "CARD";
228
+ } else if (value === 2) {
229
+ type.value = "DIRECT_DEBIT";
230
+ }
231
+ });
232
+
233
+ const { requiredRule, emailRule } = useUtils();
234
+
235
+ const {
236
+ supportedPaymentMethods,
237
+ supportedEwallets,
238
+ supportedDirectDebit,
239
+ cardNumber,
240
+ cardExpiration,
241
+ cardSecurityCode,
242
+ cardholderName,
243
+ cardholderMobileNumber,
244
+ cardholderEmail,
245
+ } = usePaymentMethod();
246
+
247
+ watch(selectedPaymentMethod, (value: number | null) => {
248
+ if (value !== null) {
249
+ const paymentMethods =
250
+ type.value === "DIRECT_DEBIT"
251
+ ? supportedDirectDebit
252
+ : type.value === "EWALLET"
253
+ ? supportedEwallets
254
+ : [];
255
+ const selectedPayment = paymentMethods[value];
256
+ channel.value = selectedPayment.channel;
257
+ } else {
258
+ channel.value = null;
259
+ }
260
+ });
261
+
262
+ // Extended card providers with regex patterns
263
+ const cardTypes = [
264
+ { name: "Visa", regex: /^4/ },
265
+ {
266
+ name: "MasterCard",
267
+ regex: /^(5[1-5]|222[1-9]|22[3-9]\d|2[3-6]\d{2}|27[01]\d|2720)/,
268
+ },
269
+ { name: "American Express", regex: /^3[47]/ },
270
+ {
271
+ name: "Discover",
272
+ regex: /^(6011|622(12[6-9]|1[3-9]\d|[2-8]\d\d|92[0-5])|64[4-9]|65)/,
273
+ },
274
+ { name: "Diners Club", regex: /^3(0[0-5]|[68])/ },
275
+ { name: "JCB", regex: /^35/ },
276
+ { name: "UnionPay", regex: /^62/ },
277
+ { name: "Maestro", regex: /^(50|5[6-9]|6\d)/ },
278
+ ];
279
+
280
+ function detectCard(value: string) {
281
+ if (!value) return "Unknown";
282
+ return cardTypes.find((card) => card.regex.test(value))?.name || "Unknown";
283
+ }
284
+
285
+ function validateAcceptedCard(value: string): boolean | string {
286
+ const acceptedCards = ["JCB", "Visa", "MasterCard"];
287
+ if (!acceptedCards.includes(detectCard(value))) {
288
+ return "Card type not accepted";
289
+ }
290
+ return true;
291
+ }
292
+
293
+ const CARD_LOGO = useRuntimeConfig().public.CARD_LOGO;
294
+
295
+ const cardLogo = computed(() =>
296
+ getPaymentMethodLogo(detectCard(cardNumber.value))
297
+ );
298
+
299
+ import { h } from "vue";
300
+ import { VImg } from "vuetify/components";
301
+
302
+ function getPaymentMethodLogo(type: string) {
303
+ const width = "30";
304
+ const height = "30";
305
+ switch (type) {
306
+ case "Visa":
307
+ return h(VImg, {
308
+ src: `${CARD_LOGO}#visa`,
309
+ width,
310
+ height,
311
+ });
312
+ case "MasterCard":
313
+ return h(VImg, {
314
+ src: `${CARD_LOGO}#mastercard`,
315
+ width,
316
+ height,
317
+ });
318
+ case "American Express":
319
+ return h(VImg, {
320
+ src: `${CARD_LOGO}#amex`,
321
+ width,
322
+ height,
323
+ });
324
+ case "Discover":
325
+ return h(VImg, {
326
+ src: `${CARD_LOGO}#discover`,
327
+ width,
328
+ height,
329
+ });
330
+ case "Diners Club":
331
+ return h(VImg, {
332
+ src: `${CARD_LOGO}#diners`,
333
+ width,
334
+ height,
335
+ });
336
+ case "JCB":
337
+ return h(VImg, {
338
+ src: `${CARD_LOGO}#jcb`,
339
+ width,
340
+ height,
341
+ });
342
+ case "UnionPay":
343
+ return h(VImg, {
344
+ src: `${CARD_LOGO}#unionpay`,
345
+ width,
346
+ height,
347
+ });
348
+ case "Maestro":
349
+ return h(VImg, {
350
+ src: `${CARD_LOGO}#maestro`,
351
+ width,
352
+ height,
353
+ });
354
+ case "GCASH":
355
+ return h(VImg, {
356
+ src: `${CARD_LOGO}#gcash`,
357
+ width,
358
+ height,
359
+ });
360
+ case "PAYMAYA":
361
+ return h(VImg, {
362
+ src: "/public/paymaya-logo.png",
363
+ width,
364
+ height,
365
+ });
366
+ default:
367
+ return h(VImg, {
368
+ src: `${CARD_LOGO}#guide_code`,
369
+ width,
370
+ height,
371
+ });
372
+ }
373
+ }
374
+
375
+ const formatExpiry = (event: Event) => {
376
+ const input = event.target as HTMLInputElement;
377
+ let value = input.value.replace(/\D/g, ""); // Remove non-numeric characters
378
+
379
+ if (value.length > 2) {
380
+ value = value.replace(/(\d{2})(\d{0,2})/, "$1/$2"); // Insert slash after MM
381
+ }
382
+
383
+ value = value.substring(0, 5); // ✨ Limit to 5 characters
384
+
385
+ cardExpiration.value = value;
386
+ };
387
+
388
+ // Expiry date validation
389
+ const validateExpiry = (value: string): boolean | string => {
390
+ if (!value.match(/^\d{2}\/\d{2}$/)) return "Invalid"; // Check format
391
+
392
+ const [monthStr, yearStr] = value.split("/");
393
+ const month = parseInt(monthStr, 10);
394
+ const year = parseInt("20" + yearStr, 10); // Convert YY to YYYY
395
+
396
+ const now = new Date();
397
+ const currentMonth = now.getMonth() + 1; // JavaScript months are 0-based
398
+ const currentYear = now.getFullYear();
399
+
400
+ if (month < 1 || month > 12) return "Invalid";
401
+ if (year < currentYear || (year === currentYear && month < currentMonth))
402
+ return "Expired";
403
+
404
+ return true;
405
+ };
406
+
407
+ // Format card number as "#### #### #### ####"
408
+ const formatCardNumber = (event: Event) => {
409
+ const input = event.target as HTMLInputElement;
410
+ let value = input.value.replace(/\D/g, ""); // Remove non-numeric characters
411
+
412
+ // Insert spaces after every 4 digits
413
+ let formattedValue = value
414
+ .slice(0, 19)
415
+ .replace(/(\d{4})/g, "$1 ")
416
+ .trim(); // Ensures no extra spaces at the end
417
+
418
+ // Preserve cursor position while formatting
419
+ const cursorPosition = input.selectionStart ?? 0;
420
+ const spaceCountBefore = (cardNumber.value.match(/ /g) || []).length;
421
+ const spaceCountAfter = (formattedValue.match(/ /g) || []).length;
422
+ const cursorOffset = spaceCountAfter - spaceCountBefore;
423
+
424
+ cardNumber.value = formattedValue;
425
+
426
+ // Restore correct cursor position
427
+ setTimeout(() => {
428
+ input.setSelectionRange(
429
+ cursorPosition + cursorOffset,
430
+ cursorPosition + cursorOffset
431
+ );
432
+ });
433
+ };
434
+
435
+ // Validate card number using Luhn Algorithm
436
+ const validateCardNumber = (value: string): boolean | string => {
437
+ const sanitizedValue = value.replace(/\s/g, ""); // Remove spaces
438
+
439
+ if (!/^\d{15,19}$/.test(sanitizedValue)) return "Invalid card number";
440
+
441
+ if (!luhnCheck(sanitizedValue)) return "Invalid card";
442
+
443
+ return true;
444
+ };
445
+
446
+ // Luhn Algorithm for card validation
447
+ const luhnCheck = (num: string): boolean => {
448
+ let sum = 0;
449
+ let shouldDouble = false;
450
+
451
+ for (let i = num.length - 1; i >= 0; i--) {
452
+ let digit = parseInt(num.charAt(i), 10);
453
+
454
+ if (shouldDouble) {
455
+ digit *= 2;
456
+ if (digit > 9) digit -= 9;
457
+ }
458
+
459
+ sum += digit;
460
+ shouldDouble = !shouldDouble;
461
+ }
462
+
463
+ return sum % 10 === 0;
464
+ };
465
+
466
+ function validateCVV() {
467
+ const type = detectCard(cardNumber.value);
468
+ const cvvLength = type === "American Express" ? 4 : 3;
469
+ const cvvRegex = new RegExp(`^\\d{${cvvLength}}$`);
470
+ if (!cvvRegex.test(cardSecurityCode.value)) {
471
+ return "Invalid";
472
+ }
473
+ return true;
474
+ }
475
+
476
+ function formatCardSecurityCode(event: Event) {
477
+ const input = event.target as HTMLInputElement;
478
+ let value = input.value.replace(/\D/g, ""); // Remove non-numeric characters
479
+
480
+ const maxLength = detectCard(cardNumber.value) === "American Express" ? 4 : 3;
481
+ value = value.substring(0, maxLength);
482
+
483
+ input.value = value; // ✨ update input field
484
+ cardSecurityCode.value = value; // ✨ update bound variable
485
+
486
+ // Preserve cursor position while formatting
487
+ const cursorPosition = input.selectionStart ?? 0;
488
+ const cursorOffset = value.length - input.value.length;
489
+ setTimeout(() => {
490
+ input.setSelectionRange(
491
+ cursorPosition + cursorOffset,
492
+ cursorPosition + cursorOffset
493
+ );
494
+ });
495
+ }
496
+
497
+ const countryRules: { [key: string]: number } = {
498
+ "1": 11, // 🇺🇸 USA, 🇨🇦 Canada (1 XXX XXX XXXX)
499
+ "44": 12, // 🇬🇧 UK (44 XXXX XXXXXX)
500
+ "971": 12, // 🇦🇪 UAE (971 XX XXX XXXX)
501
+ "234": 13, // 🇳🇬 Nigeria (234 XXX XXX XXXX)
502
+ "63": 12, // 🇵🇭 Philippines (63 XXX XXX XXXX)
503
+ "91": 12, // 🇮🇳 India (91 XXXXX XXXXX)
504
+ "61": 12, // 🇦🇺 Australia (61 XXXX XXX XXX)
505
+ "81": 12, // 🇯🇵 Japan (81 XX XXXX XXXX)
506
+ "65": 11, // 🇸🇬 Singapore (65 XXXX XXXX)
507
+ "62": 12, // 🇮🇩 Indonesia (62 XX XXX XXXX)
508
+ "60": 12, // 🇲🇾 Malaysia (60 XX XXX XXXX)
509
+ "49": 13, // 🇩🇪 Germany (49 XXXX XXXXXX)
510
+ "33": 12, // 🇫🇷 France (33 X XX XX XX XX)
511
+ "39": 13, // 🇮🇹 Italy (39 XXX XXX XXXX)
512
+ };
513
+
514
+ const formatContactNumber = (event: Event) => {
515
+ const input = event.target as HTMLInputElement;
516
+ let rawValue = input.value.replace(/\D/g, ""); // Remove all non-numeric characters
517
+
518
+ if (!rawValue) {
519
+ cardholderMobileNumber.value = "";
520
+ return;
521
+ }
522
+
523
+ let countryCode = "";
524
+ let localNumber = "";
525
+ let maxLength = 15; // Default max length
526
+ let cursorPosition = input.selectionStart || 0;
527
+ let convertedToPH = false;
528
+
529
+ // Detect if the user types a Philippine number (09...)
530
+ if (rawValue.startsWith("09")) {
531
+ rawValue = "63" + rawValue.slice(1); // Convert 09xxxxxxx to +63 9xxxxxxx
532
+ convertedToPH = true; // Mark that conversion happened
533
+ }
534
+
535
+ // Detect country code
536
+ for (let code in countryRules) {
537
+ if (rawValue.startsWith(code)) {
538
+ countryCode = code;
539
+ maxLength = countryRules[code];
540
+ break;
541
+ }
542
+ }
543
+
544
+ // If no valid country code is detected, allow free typing
545
+ if (!countryCode) {
546
+ cardholderMobileNumber.value = `+${rawValue}`;
547
+ return;
548
+ }
549
+
550
+ // Extract local number
551
+ localNumber = rawValue.slice(countryCode.length);
552
+ rawValue = rawValue.slice(0, maxLength); // Enforce max length
553
+
554
+ // Format number
555
+ let formattedValue = `+${countryCode} ${localNumber}`;
556
+
557
+ // Set formatted value
558
+ cardholderMobileNumber.value = formattedValue;
559
+
560
+ // Move cursor to the end if `09` was converted to `+63 9`
561
+ setTimeout(() => {
562
+ if (convertedToPH) {
563
+ input.setSelectionRange(formattedValue.length, formattedValue.length);
564
+ } else {
565
+ input.setSelectionRange(cursorPosition, cursorPosition);
566
+ }
567
+ }, 0);
568
+ };
569
+
570
+ // ✅ **Validation Function**
571
+ const validateContactNumber = (value: string): boolean | string => {
572
+ const cleanValue = value.replace(/\D/g, ""); // Remove spaces and non-numeric characters
573
+
574
+ for (let code in countryRules) {
575
+ if (
576
+ cleanValue.startsWith(code) &&
577
+ cleanValue.length === countryRules[code]
578
+ ) {
579
+ return true; // ✅ Valid if country code matches and length is correct
580
+ }
581
+ }
582
+
583
+ return "Invalid contact number"; // ❌ Fallback error
584
+ };
585
+ </script>