@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.
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.editorconfig +12 -0
- package/.github/workflows/main.yml +17 -0
- package/.github/workflows/publish.yml +39 -0
- package/.nuxtrc +1 -0
- package/.playground/app.vue +37 -0
- package/.playground/nuxt.config.ts +20 -0
- package/CHANGELOG.md +7 -0
- package/README.md +73 -0
- package/app.vue +3 -0
- package/components/AddPaymentMethod.vue +585 -0
- package/components/BtnUploadFile.vue +139 -0
- package/components/ConfirmDialog.vue +66 -0
- package/components/Container/Standard.vue +33 -0
- package/components/Input/Date.vue +177 -0
- package/components/Input/ListGroupSelection.vue +93 -0
- package/components/Input/NewDate.vue +123 -0
- package/components/Input/Number.vue +124 -0
- package/components/Input/Password.vue +35 -0
- package/components/InputLabel.vue +18 -0
- package/components/InvitationMain.vue +195 -0
- package/components/Layout/Header.vue +285 -0
- package/components/Layout/NavigationDrawer.vue +52 -0
- package/components/LinkHome.vue +9 -0
- package/components/ListItem.vue +35 -0
- package/components/LocalPagination.vue +41 -0
- package/components/MemberMain.vue +452 -0
- package/components/NavigationItem.vue +73 -0
- package/components/PlaceholderComponent.vue +34 -0
- package/components/RolePermissionFormCreate.vue +179 -0
- package/components/RolePermissionFormPreviewUpdate.vue +184 -0
- package/components/RolePermissionMain.vue +376 -0
- package/components/Snackbar.vue +23 -0
- package/components/SpecificAttr.vue +57 -0
- package/components/Std/Pagination.vue +52 -0
- package/components/SwitchContext.vue +109 -0
- package/components/SwitchOrg.vue +159 -0
- package/components/TableList.vue +130 -0
- package/composables/useAddress.ts +144 -0
- package/composables/useChartOfAccount.ts +62 -0
- package/composables/useCommonPermission.ts +130 -0
- package/composables/useFile.ts +29 -0
- package/composables/useInvoice.ts +42 -0
- package/composables/useLocal.ts +63 -0
- package/composables/useLocalAuth.ts +157 -0
- package/composables/useLocalSetup.ts +46 -0
- package/composables/useMember.ts +107 -0
- package/composables/useOrder.ts +22 -0
- package/composables/useOrg.ts +106 -0
- package/composables/useOrgPermission.ts +27 -0
- package/composables/usePayment.ts +22 -0
- package/composables/usePaymentMethod.ts +347 -0
- package/composables/usePermission.ts +54 -0
- package/composables/usePrice.ts +15 -0
- package/composables/usePromoCode.ts +43 -0
- package/composables/useRecapPermission.ts +26 -0
- package/composables/useRole.ts +89 -0
- package/composables/useSchoolPermission.ts +13 -0
- package/composables/useSubscription.ts +264 -0
- package/composables/useUser.ts +102 -0
- package/composables/useUtils.ts +294 -0
- package/composables/useVerification.ts +19 -0
- package/error.vue +41 -0
- package/eslint.config.js +3 -0
- package/layouts/plain.vue +7 -0
- package/middleware/01.auth.ts +14 -0
- package/middleware/org.ts +16 -0
- package/nuxt.config.ts +48 -0
- package/package.json +35 -0
- package/pages/index.vue +3 -0
- package/pages/payment-method-cancel-link.vue +31 -0
- package/pages/payment-method-failed-link.vue +31 -0
- package/pages/payment-method-linked.vue +31 -0
- package/pages/require-organization-membership.vue +47 -0
- package/pages/unauthorized.vue +29 -0
- package/plugins/API.ts +58 -0
- package/plugins/iconify.client.ts +5 -0
- package/plugins/vuetify.ts +55 -0
- package/public/bdo-logo.svg +4 -0
- package/public/bpi-logo.svg +74 -0
- package/public/chinabank-logo.svg +120 -0
- package/public/gcash-logo.png +0 -0
- package/public/gcash-logo.svg +65 -0
- package/public/grabpay-logo.svg +99 -0
- package/public/paymaya-logo.jpg +0 -0
- package/public/paymaya-logo.png +0 -0
- package/public/paymaya-logo.svg +25 -0
- package/public/qrph-c567ff0f-ab6d-4662-86bf-24c6c731d8a8-logo.svg +20 -0
- package/public/rcbc-logo.svg +15 -0
- package/public/shopeepay-logo.svg +89 -0
- package/public/ubp-logo.svg +88 -0
- package/tsconfig.json +3 -0
- package/types/address.d.ts +13 -0
- package/types/invoice.d.ts +28 -0
- package/types/local.d.ts +25 -0
- package/types/member.d.ts +12 -0
- package/types/org.d.ts +13 -0
- package/types/payment-method.d.ts +11 -0
- package/types/payment.d.ts +18 -0
- package/types/permission.d.ts +14 -0
- package/types/price.d.ts +17 -0
- package/types/promo-code.d.ts +19 -0
- package/types/role.d.ts +13 -0
- package/types/subscription.d.ts +29 -0
- package/types/user.d.ts +21 -0
- package/types/verification.d.ts +15 -0
- 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>
|