@gait-financial/react 0.1.6 → 0.1.7
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/wc/button.js +1769 -102
- package/package.json +3 -1
- package/wc/button.js +1769 -102
package/dist/wc/button.js
CHANGED
|
@@ -1,25 +1,128 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(exports) {
|
|
2
|
+
"use strict";
|
|
3
|
+
function darkenColor(hex, percent) {
|
|
4
|
+
const color = hex.replace("#", "");
|
|
5
|
+
const num = parseInt(color, 16);
|
|
6
|
+
const r = num >> 16 & 255;
|
|
7
|
+
const g = num >> 8 & 255;
|
|
8
|
+
const b = num & 255;
|
|
9
|
+
const newR = Math.max(0, Math.floor(r * (1 - percent)));
|
|
10
|
+
const newG = Math.max(0, Math.floor(g * (1 - percent)));
|
|
11
|
+
const newB = Math.max(0, Math.floor(b * (1 - percent)));
|
|
12
|
+
return `#${(newR << 16 | newG << 8 | newB).toString(16).padStart(6, "0")}`;
|
|
13
|
+
}
|
|
14
|
+
function isColorDark(hex) {
|
|
15
|
+
const color = hex.replace("#", "");
|
|
16
|
+
const num = parseInt(color, 16);
|
|
17
|
+
const r = num >> 16 & 255;
|
|
18
|
+
const g = num >> 8 & 255;
|
|
19
|
+
const b = num & 255;
|
|
20
|
+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
21
|
+
return luminance < 0.5;
|
|
22
|
+
}
|
|
23
|
+
const colors = {
|
|
24
|
+
// Primary colors (CTA button - dark)
|
|
25
|
+
primary: {
|
|
26
|
+
main: "rgb(47, 44, 37)",
|
|
27
|
+
dark: "rgb(35, 33, 28)",
|
|
28
|
+
contrastText: "#ffffff"
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const spacing = {
|
|
32
|
+
sm: "8px",
|
|
33
|
+
md: "16px",
|
|
34
|
+
lg: "24px"
|
|
35
|
+
};
|
|
36
|
+
const borderRadius = {
|
|
37
|
+
md: "8px"
|
|
38
|
+
};
|
|
39
|
+
const transitions = {
|
|
40
|
+
normal: "250ms ease-in-out"
|
|
41
|
+
};
|
|
42
|
+
const zIndex = {
|
|
43
|
+
modalBackdrop: 1040
|
|
44
|
+
};
|
|
45
|
+
const typography = {
|
|
46
|
+
fontFamily: {
|
|
47
|
+
primary: '"Inter", "Helvetica", "Arial", sans-serif'
|
|
48
|
+
},
|
|
49
|
+
fontSize: {
|
|
50
|
+
// 12px
|
|
51
|
+
sm: "0.875rem",
|
|
52
|
+
// 14px
|
|
53
|
+
md: "1rem",
|
|
54
|
+
// 16px
|
|
55
|
+
lg: "1.125rem"
|
|
56
|
+
},
|
|
57
|
+
fontWeight: {
|
|
58
|
+
medium: 500
|
|
59
|
+
},
|
|
60
|
+
lineHeight: {
|
|
61
|
+
normal: 1.5
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const GAIT_BRAND_COLOR = "#4147BF";
|
|
65
|
+
const GAIT_SECONDARY_COLOR = "#F3F2FF";
|
|
66
|
+
const splitPaymentConfig = {
|
|
67
|
+
/**
|
|
68
|
+
* Split payment fee amount
|
|
69
|
+
* This fee is added to the total cost when using split payment
|
|
70
|
+
*/
|
|
71
|
+
fee: 10
|
|
72
|
+
};
|
|
73
|
+
function getThemeStyles(darkenColor2, isColorDark2) {
|
|
74
|
+
return {
|
|
75
|
+
bg: GAIT_BRAND_COLOR,
|
|
76
|
+
bgHover: darkenColor2(GAIT_BRAND_COLOR, 0.15),
|
|
77
|
+
bgActive: darkenColor2(GAIT_BRAND_COLOR, 0.25),
|
|
78
|
+
color: isColorDark2(GAIT_BRAND_COLOR) ? "#ffffff" : "#000000"
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function getSizeStyles(size) {
|
|
82
|
+
const sizeStyles = {
|
|
83
|
+
small: {
|
|
84
|
+
padding: "8px 20px",
|
|
85
|
+
fontSize: typography.fontSize.sm,
|
|
86
|
+
minHeight: "36px"
|
|
87
|
+
},
|
|
88
|
+
medium: {
|
|
89
|
+
padding: "12px 28px",
|
|
90
|
+
fontSize: typography.fontSize.md,
|
|
91
|
+
minHeight: "44px"
|
|
92
|
+
},
|
|
93
|
+
large: {
|
|
94
|
+
padding: "16px 36px",
|
|
95
|
+
fontSize: typography.fontSize.lg,
|
|
96
|
+
minHeight: "52px"
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
return sizeStyles[size];
|
|
100
|
+
}
|
|
101
|
+
function generateButtonStyles(size, darkenColorFn, isColorDarkFn) {
|
|
102
|
+
const themeStyles = getThemeStyles(darkenColorFn, isColorDarkFn);
|
|
103
|
+
const currentSize = getSizeStyles(size);
|
|
104
|
+
return `
|
|
2
105
|
<style>
|
|
3
106
|
:host {
|
|
4
107
|
display: inline-block;
|
|
5
|
-
--gait-button-bg: ${
|
|
6
|
-
--gait-button-bg-hover: ${
|
|
7
|
-
--gait-button-bg-active: ${
|
|
8
|
-
--gait-button-color: ${
|
|
9
|
-
--gait-button-padding: ${
|
|
10
|
-
--gait-button-font-size: ${
|
|
11
|
-
--gait-button-min-height: ${
|
|
108
|
+
--gait-button-bg: ${themeStyles.bg};
|
|
109
|
+
--gait-button-bg-hover: ${themeStyles.bgHover};
|
|
110
|
+
--gait-button-bg-active: ${themeStyles.bgActive};
|
|
111
|
+
--gait-button-color: ${themeStyles.color};
|
|
112
|
+
--gait-button-padding: ${currentSize.padding};
|
|
113
|
+
--gait-button-font-size: ${currentSize.fontSize};
|
|
114
|
+
--gait-button-min-height: ${currentSize.minHeight};
|
|
12
115
|
}
|
|
13
116
|
|
|
14
117
|
button {
|
|
15
118
|
display: inline-flex;
|
|
16
119
|
align-items: center;
|
|
17
120
|
justify-content: center;
|
|
18
|
-
gap: ${
|
|
121
|
+
gap: ${spacing.sm};
|
|
19
122
|
|
|
20
|
-
font-family: ${
|
|
21
|
-
font-weight: ${
|
|
22
|
-
line-height: ${
|
|
123
|
+
font-family: ${typography.fontFamily.primary};
|
|
124
|
+
font-weight: ${typography.fontWeight.medium};
|
|
125
|
+
line-height: ${typography.lineHeight.normal};
|
|
23
126
|
font-size: var(--gait-button-font-size);
|
|
24
127
|
|
|
25
128
|
padding: var(--gait-button-padding);
|
|
@@ -29,10 +132,10 @@
|
|
|
29
132
|
color: var(--gait-button-color);
|
|
30
133
|
|
|
31
134
|
border: none;
|
|
32
|
-
border-radius: ${
|
|
135
|
+
border-radius: ${borderRadius.md};
|
|
33
136
|
cursor: pointer;
|
|
34
137
|
|
|
35
|
-
transition: all ${
|
|
138
|
+
transition: all ${transitions.normal};
|
|
36
139
|
|
|
37
140
|
/* Remove default button styles */
|
|
38
141
|
outline: none;
|
|
@@ -81,7 +184,15 @@
|
|
|
81
184
|
width: 100%;
|
|
82
185
|
}
|
|
83
186
|
</style>
|
|
84
|
-
|
|
187
|
+
`;
|
|
188
|
+
}
|
|
189
|
+
function injectModalStyles() {
|
|
190
|
+
if (document.getElementById("gait-modal-styles")) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const style = document.createElement("style");
|
|
194
|
+
style.id = "gait-modal-styles";
|
|
195
|
+
style.textContent = `
|
|
85
196
|
/* Modal Backdrop - The overlay behind the modal */
|
|
86
197
|
.gait-modal-backdrop {
|
|
87
198
|
position: fixed;
|
|
@@ -93,9 +204,9 @@
|
|
|
93
204
|
display: flex;
|
|
94
205
|
align-items: center;
|
|
95
206
|
justify-content: center;
|
|
96
|
-
z-index: ${
|
|
207
|
+
z-index: ${zIndex.modalBackdrop};
|
|
97
208
|
opacity: 0;
|
|
98
|
-
transition: opacity ${
|
|
209
|
+
transition: opacity ${transitions.normal};
|
|
99
210
|
overflow: hidden;
|
|
100
211
|
}
|
|
101
212
|
|
|
@@ -396,7 +507,7 @@
|
|
|
396
507
|
border: 1px solid #e5e7eb;
|
|
397
508
|
border-radius: 8px;
|
|
398
509
|
cursor: pointer;
|
|
399
|
-
transition: all ${
|
|
510
|
+
transition: all ${transitions.normal};
|
|
400
511
|
color: #6b7280;
|
|
401
512
|
}
|
|
402
513
|
|
|
@@ -407,9 +518,9 @@
|
|
|
407
518
|
}
|
|
408
519
|
|
|
409
520
|
.gait-modal-split-option.selected {
|
|
410
|
-
background: ${
|
|
411
|
-
border: 1px solid ${
|
|
412
|
-
color: ${
|
|
521
|
+
background: ${GAIT_SECONDARY_COLOR};
|
|
522
|
+
border: 1px solid ${GAIT_BRAND_COLOR};
|
|
523
|
+
color: ${GAIT_BRAND_COLOR};
|
|
413
524
|
}
|
|
414
525
|
|
|
415
526
|
.gait-modal-email-inputs {
|
|
@@ -428,11 +539,11 @@
|
|
|
428
539
|
background: #fff;
|
|
429
540
|
color: #111827;
|
|
430
541
|
outline: none;
|
|
431
|
-
transition: all ${
|
|
542
|
+
transition: all ${transitions.normal};
|
|
432
543
|
}
|
|
433
544
|
|
|
434
545
|
.gait-modal-email-input:focus {
|
|
435
|
-
border-color: ${
|
|
546
|
+
border-color: ${colors.primary.main};
|
|
436
547
|
box-shadow: 0 0 0 2px rgba(47, 44, 37, 0.1);
|
|
437
548
|
}
|
|
438
549
|
|
|
@@ -495,7 +606,7 @@
|
|
|
495
606
|
|
|
496
607
|
.gait-modal-email-item-amount {
|
|
497
608
|
font-weight: 500;
|
|
498
|
-
color: ${
|
|
609
|
+
color: ${colors.primary.main};
|
|
499
610
|
font-size: 14px;
|
|
500
611
|
}
|
|
501
612
|
|
|
@@ -514,12 +625,12 @@
|
|
|
514
625
|
border: none;
|
|
515
626
|
cursor: pointer;
|
|
516
627
|
padding: 2px 4px;
|
|
517
|
-
transition: color ${
|
|
628
|
+
transition: color ${transitions.normal};
|
|
518
629
|
}
|
|
519
630
|
|
|
520
631
|
.gait-modal-email-item-edit:hover,
|
|
521
632
|
.gait-modal-email-item-remove:hover {
|
|
522
|
-
color: ${
|
|
633
|
+
color: ${colors.primary.main};
|
|
523
634
|
}
|
|
524
635
|
|
|
525
636
|
/* For "By amount" mode - two input fields side by side */
|
|
@@ -542,18 +653,18 @@
|
|
|
542
653
|
padding: 8px 16px;
|
|
543
654
|
font-size: 13px;
|
|
544
655
|
font-weight: 500;
|
|
545
|
-
border: 1px solid ${
|
|
656
|
+
border: 1px solid ${colors.primary.main};
|
|
546
657
|
border-radius: 6px;
|
|
547
|
-
background: ${
|
|
548
|
-
color: ${
|
|
658
|
+
background: ${colors.primary.main};
|
|
659
|
+
color: ${colors.primary.contrastText};
|
|
549
660
|
cursor: pointer;
|
|
550
|
-
transition: all ${
|
|
661
|
+
transition: all ${transitions.normal};
|
|
551
662
|
white-space: nowrap;
|
|
552
663
|
}
|
|
553
664
|
|
|
554
665
|
.gait-modal-done-button:hover {
|
|
555
|
-
background: ${
|
|
556
|
-
border-color: ${
|
|
666
|
+
background: ${colors.primary.dark};
|
|
667
|
+
border-color: ${colors.primary.dark};
|
|
557
668
|
}
|
|
558
669
|
|
|
559
670
|
.gait-modal-email-amount-input {
|
|
@@ -569,7 +680,7 @@
|
|
|
569
680
|
}
|
|
570
681
|
|
|
571
682
|
.gait-modal-email-amount-input:focus {
|
|
572
|
-
border-color: ${
|
|
683
|
+
border-color: ${colors.primary.main};
|
|
573
684
|
box-shadow: 0 0 0 2px rgba(47, 44, 37, 0.1);
|
|
574
685
|
}
|
|
575
686
|
|
|
@@ -581,11 +692,11 @@
|
|
|
581
692
|
border: none;
|
|
582
693
|
cursor: pointer;
|
|
583
694
|
text-align: left;
|
|
584
|
-
transition: color ${
|
|
695
|
+
transition: color ${transitions.normal};
|
|
585
696
|
}
|
|
586
697
|
|
|
587
698
|
.gait-modal-add-email-button:hover {
|
|
588
|
-
color: ${
|
|
699
|
+
color: ${colors.primary.main};
|
|
589
700
|
text-decoration: underline;
|
|
590
701
|
}
|
|
591
702
|
|
|
@@ -605,7 +716,7 @@
|
|
|
605
716
|
.gait-modal-split-amount {
|
|
606
717
|
font-size: 18px;
|
|
607
718
|
font-weight: 600;
|
|
608
|
-
color: ${
|
|
719
|
+
color: ${GAIT_BRAND_COLOR};
|
|
609
720
|
}
|
|
610
721
|
|
|
611
722
|
.gait-modal-send-split-button {
|
|
@@ -614,16 +725,16 @@
|
|
|
614
725
|
font-size: 14px;
|
|
615
726
|
font-weight: 500;
|
|
616
727
|
border-radius: 8px;
|
|
617
|
-
border: 1px solid ${
|
|
618
|
-
background: ${
|
|
619
|
-
color: ${
|
|
728
|
+
border: 1px solid ${GAIT_BRAND_COLOR};
|
|
729
|
+
background: ${GAIT_SECONDARY_COLOR};
|
|
730
|
+
color: ${GAIT_BRAND_COLOR};
|
|
620
731
|
cursor: pointer;
|
|
621
|
-
transition: all ${
|
|
732
|
+
transition: all ${transitions.normal};
|
|
622
733
|
margin-top: 16px;
|
|
623
734
|
}
|
|
624
735
|
|
|
625
736
|
.gait-modal-send-split-button:hover {
|
|
626
|
-
background: ${
|
|
737
|
+
background: ${GAIT_SECONDARY_COLOR};
|
|
627
738
|
filter: brightness(0.98);
|
|
628
739
|
}
|
|
629
740
|
|
|
@@ -643,10 +754,10 @@
|
|
|
643
754
|
font-weight: 600;
|
|
644
755
|
border-radius: 10px;
|
|
645
756
|
border: none;
|
|
646
|
-
background: var(--gait-modal-button-bg, ${
|
|
647
|
-
color: var(--gait-modal-button-color, ${
|
|
757
|
+
background: var(--gait-modal-button-bg, ${colors.primary.main});
|
|
758
|
+
color: var(--gait-modal-button-color, ${colors.primary.contrastText});
|
|
648
759
|
cursor: pointer;
|
|
649
|
-
transition: all ${
|
|
760
|
+
transition: all ${transitions.normal};
|
|
650
761
|
margin-top: 12px;
|
|
651
762
|
display: flex;
|
|
652
763
|
flex-direction: row;
|
|
@@ -657,7 +768,7 @@
|
|
|
657
768
|
}
|
|
658
769
|
|
|
659
770
|
.gait-modal-confirm-button-with-amount:hover {
|
|
660
|
-
background-color: var(--gait-modal-button-bg-hover, ${
|
|
771
|
+
background-color: var(--gait-modal-button-bg-hover, ${colors.primary.dark});
|
|
661
772
|
}
|
|
662
773
|
|
|
663
774
|
.gait-modal-confirm-button-with-amount:active {
|
|
@@ -704,7 +815,7 @@
|
|
|
704
815
|
background: var(--gait-modal-button-bg, #e91e63);
|
|
705
816
|
color: var(--gait-modal-button-color, #fff);
|
|
706
817
|
cursor: pointer;
|
|
707
|
-
transition: all ${
|
|
818
|
+
transition: all ${transitions.normal};
|
|
708
819
|
}
|
|
709
820
|
|
|
710
821
|
.gait-modal-confirm-button:hover {
|
|
@@ -717,29 +828,29 @@
|
|
|
717
828
|
|
|
718
829
|
/* Modal Footer - The button section */
|
|
719
830
|
.gait-modal-footer {
|
|
720
|
-
padding: ${
|
|
831
|
+
padding: ${spacing.lg};
|
|
721
832
|
border-top: 1px solid #e5e7eb;
|
|
722
833
|
display: flex;
|
|
723
834
|
justify-content: flex-end;
|
|
724
|
-
gap: ${
|
|
835
|
+
gap: ${spacing.md};
|
|
725
836
|
}
|
|
726
837
|
|
|
727
838
|
/* Modal Button - The close button */
|
|
728
839
|
.gait-modal-button {
|
|
729
|
-
padding: ${
|
|
730
|
-
font-family: ${
|
|
731
|
-
font-size: ${
|
|
732
|
-
font-weight: ${
|
|
840
|
+
padding: ${spacing.sm} ${spacing.lg};
|
|
841
|
+
font-family: ${typography.fontFamily.primary};
|
|
842
|
+
font-size: ${typography.fontSize.md};
|
|
843
|
+
font-weight: ${typography.fontWeight.medium};
|
|
733
844
|
border: none;
|
|
734
|
-
border-radius: ${
|
|
845
|
+
border-radius: ${borderRadius.md};
|
|
735
846
|
cursor: pointer;
|
|
736
|
-
transition: all ${
|
|
737
|
-
background-color: ${
|
|
738
|
-
color: ${
|
|
847
|
+
transition: all ${transitions.normal};
|
|
848
|
+
background-color: ${colors.primary.main};
|
|
849
|
+
color: ${colors.primary.contrastText};
|
|
739
850
|
}
|
|
740
851
|
|
|
741
852
|
.gait-modal-button:hover {
|
|
742
|
-
background-color: ${
|
|
853
|
+
background-color: ${colors.primary.dark};
|
|
743
854
|
}
|
|
744
855
|
|
|
745
856
|
.gait-modal-button:active {
|
|
@@ -834,55 +945,73 @@
|
|
|
834
945
|
background-color: #ef4444;
|
|
835
946
|
color: #fff;
|
|
836
947
|
}
|
|
837
|
-
|
|
948
|
+
`;
|
|
949
|
+
document.head.appendChild(style);
|
|
950
|
+
}
|
|
951
|
+
const GAIT_LOGO_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 311.96 313.95" width="20" height="20" aria-hidden="true"><path fill="currentColor" d="M168.07,139.33c6.81-15.69,22.45-26.66,40.65-26.66s33.83,10.97,40.66,26.66h62.58C303.19,60.94,236.71,0,155.98,0S8.77,60.95,0,139.33h168.07Z"/><path fill="currentColor" d="M249.37,174.62c-6.81,15.68-22.46,26.65-40.65,26.65s-33.82-10.97-40.64-26.65H0c8.77,78.39,75.25,139.33,155.98,139.33s147.21-60.94,155.98-139.33h-62.59Z"/></svg>`;
|
|
952
|
+
function generateButtonTemplate(disabled, customStyle) {
|
|
953
|
+
return `
|
|
838
954
|
<button
|
|
839
955
|
type="button"
|
|
840
|
-
${
|
|
841
|
-
style="${
|
|
956
|
+
${disabled ? "disabled" : ""}
|
|
957
|
+
style="${customStyle}"
|
|
842
958
|
>
|
|
843
|
-
<span class="gait-button-icon">${
|
|
959
|
+
<span class="gait-button-icon">${GAIT_LOGO_ICON}</span>
|
|
844
960
|
<span class="gait-button-label">Gait</span>
|
|
845
961
|
</button>
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
</svg
|
|
852
|
-
|
|
853
|
-
<path d="M18 6L6 18M6 6l12 12"
|
|
854
|
-
</svg
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
962
|
+
`;
|
|
963
|
+
}
|
|
964
|
+
function formatCurrency(amount) {
|
|
965
|
+
return `R ${amount.toFixed(2)}`;
|
|
966
|
+
}
|
|
967
|
+
const closeIcon = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">\n <path d="M18 6L6 18M6 6l12 12" />\n</svg>\n';
|
|
968
|
+
const checkIcon = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">\n <path d="M20 6L9 17l-5-5" />\n</svg>\n';
|
|
969
|
+
const crossIcon = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">\n <path d="M18 6L6 18M6 6l12 12" />\n</svg>\n';
|
|
970
|
+
const spinnerIcon = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">\n <path d="M21 12a9 9 0 11-6.22-8.56" />\n</svg>\n';
|
|
971
|
+
function renderItems(items) {
|
|
972
|
+
if (!items || items.length === 0) {
|
|
973
|
+
return "";
|
|
974
|
+
}
|
|
975
|
+
return items.map((item) => `
|
|
859
976
|
<div class="gait-modal-item">
|
|
860
977
|
<img
|
|
861
|
-
src="${
|
|
862
|
-
alt="${
|
|
978
|
+
src="${item.image || ""}"
|
|
979
|
+
alt="${item.name || "Product"}"
|
|
863
980
|
class="gait-modal-item-image"
|
|
864
981
|
/>
|
|
865
982
|
<div class="gait-modal-item-meta">
|
|
866
983
|
<h3 class="gait-modal-item-title">
|
|
867
|
-
${
|
|
984
|
+
${item.name || ""}
|
|
868
985
|
</h3>
|
|
869
986
|
<div class="gait-modal-item-quantity">
|
|
870
|
-
Qty: ${
|
|
871
|
-
<span class="gait-modal-item-price">${
|
|
987
|
+
Qty: ${item.quantity || 0}
|
|
988
|
+
<span class="gait-modal-item-price">${formatCurrency(item.price || 0)}</span>
|
|
872
989
|
</div>
|
|
873
990
|
</div>
|
|
874
991
|
</div>
|
|
875
|
-
`).join("")
|
|
992
|
+
`).join("");
|
|
993
|
+
}
|
|
994
|
+
function renderPriceBreakdown(breakdown) {
|
|
995
|
+
if (!breakdown || breakdown.length === 0) {
|
|
996
|
+
return "";
|
|
997
|
+
}
|
|
998
|
+
return breakdown.map((item) => {
|
|
999
|
+
const price = typeof item.price === "number" ? item.price : parseFloat(String(item.price)) || 0;
|
|
1000
|
+
return `
|
|
876
1001
|
<div class="gait-modal-price-item">
|
|
877
|
-
<span>${
|
|
878
|
-
<span>${
|
|
1002
|
+
<span>${item.name || ""}</span>
|
|
1003
|
+
<span>${formatCurrency(price)}</span>
|
|
879
1004
|
</div>
|
|
880
|
-
|
|
1005
|
+
`;
|
|
1006
|
+
}).join("");
|
|
1007
|
+
}
|
|
1008
|
+
function generateModalTemplate(items, priceBreakdown, totalCost) {
|
|
1009
|
+
return `
|
|
881
1010
|
<div class="gait-modal-container">
|
|
882
1011
|
<div class="gait-modal-header">
|
|
883
1012
|
<h2 class="gait-modal-header-title">Split Payment</h2>
|
|
884
1013
|
<button class="gait-modal-close-icon" data-gait-modal-close aria-label="Close">
|
|
885
|
-
${
|
|
1014
|
+
${closeIcon}
|
|
886
1015
|
</button>
|
|
887
1016
|
</div>
|
|
888
1017
|
|
|
@@ -890,20 +1019,20 @@
|
|
|
890
1019
|
<!-- LEFT -->
|
|
891
1020
|
<div class="gait-modal-left-panel">
|
|
892
1021
|
<div class="gait-modal-items-container">
|
|
893
|
-
${
|
|
1022
|
+
${renderItems(items)}
|
|
894
1023
|
</div>
|
|
895
1024
|
|
|
896
1025
|
<div class="gait-modal-price-section">
|
|
897
1026
|
<h4 class="gait-modal-price-title">Price details</h4>
|
|
898
1027
|
|
|
899
|
-
${
|
|
1028
|
+
${renderPriceBreakdown(priceBreakdown)}
|
|
900
1029
|
|
|
901
|
-
${
|
|
1030
|
+
${totalCost > 0 ? `
|
|
902
1031
|
<div class="gait-modal-price-total">
|
|
903
1032
|
<span>Total</span>
|
|
904
|
-
<span>${
|
|
1033
|
+
<span>${formatCurrency(totalCost)}</span>
|
|
905
1034
|
</div>
|
|
906
|
-
|
|
1035
|
+
` : ""}
|
|
907
1036
|
</div>
|
|
908
1037
|
</div>
|
|
909
1038
|
|
|
@@ -937,22 +1066,1560 @@
|
|
|
937
1066
|
</div>
|
|
938
1067
|
</div>
|
|
939
1068
|
</div>
|
|
940
|
-
|
|
941
|
-
|
|
1069
|
+
`;
|
|
1070
|
+
}
|
|
1071
|
+
function getDataAttributes(element) {
|
|
1072
|
+
const dataAttrs = {};
|
|
1073
|
+
Array.from(element.attributes).forEach((attr) => {
|
|
1074
|
+
if (attr.name.startsWith("data-")) {
|
|
1075
|
+
dataAttrs[attr.name] = attr.value;
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
return dataAttrs;
|
|
1079
|
+
}
|
|
1080
|
+
function getAllAttributes(element) {
|
|
1081
|
+
const attrs = {};
|
|
1082
|
+
Array.from(element.attributes).forEach((attr) => {
|
|
1083
|
+
attrs[attr.name] = attr.value;
|
|
1084
|
+
});
|
|
1085
|
+
return attrs;
|
|
1086
|
+
}
|
|
1087
|
+
function createAttributeGetters(element) {
|
|
1088
|
+
return {
|
|
1089
|
+
get dataId() {
|
|
1090
|
+
return element.getAttribute("data-id") || null;
|
|
1091
|
+
},
|
|
1092
|
+
get disabled() {
|
|
1093
|
+
return element.hasAttribute("disabled") && element.getAttribute("disabled") !== "false";
|
|
1094
|
+
},
|
|
1095
|
+
get size() {
|
|
1096
|
+
return element.getAttribute("size") || "medium";
|
|
1097
|
+
},
|
|
1098
|
+
get items() {
|
|
1099
|
+
const itemsAttr = element.getAttribute("items");
|
|
1100
|
+
if (!itemsAttr) {
|
|
1101
|
+
return [];
|
|
1102
|
+
}
|
|
1103
|
+
try {
|
|
1104
|
+
return JSON.parse(itemsAttr);
|
|
1105
|
+
} catch (e) {
|
|
1106
|
+
console.warn("[GaitButton] Failed to parse items attribute:", e);
|
|
1107
|
+
return [];
|
|
1108
|
+
}
|
|
1109
|
+
},
|
|
1110
|
+
get totalCost() {
|
|
1111
|
+
const totalCostAttr = element.getAttribute("total-cost");
|
|
1112
|
+
if (!totalCostAttr) {
|
|
1113
|
+
return 0;
|
|
1114
|
+
}
|
|
1115
|
+
const parsed = parseFloat(totalCostAttr);
|
|
1116
|
+
const baseTotal = isNaN(parsed) ? 0 : parsed;
|
|
1117
|
+
return baseTotal + splitPaymentConfig.fee;
|
|
1118
|
+
},
|
|
1119
|
+
get priceBreakdown() {
|
|
1120
|
+
const totalCostAttr = element.getAttribute("total-cost");
|
|
1121
|
+
const parsed = parseFloat(totalCostAttr || "0");
|
|
1122
|
+
const baseTotal = isNaN(parsed) ? 0 : parsed;
|
|
1123
|
+
const allPriceBreakdown = [
|
|
1124
|
+
{
|
|
1125
|
+
name: "Subtotal",
|
|
1126
|
+
price: baseTotal
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
name: "Split Payment Fee",
|
|
1130
|
+
price: splitPaymentConfig.fee
|
|
1131
|
+
}
|
|
1132
|
+
];
|
|
1133
|
+
return allPriceBreakdown;
|
|
1134
|
+
},
|
|
1135
|
+
get customer() {
|
|
1136
|
+
const customerAttr = element.getAttribute("customer");
|
|
1137
|
+
if (!customerAttr) {
|
|
1138
|
+
return null;
|
|
1139
|
+
}
|
|
1140
|
+
try {
|
|
1141
|
+
return JSON.parse(customerAttr);
|
|
1142
|
+
} catch (e) {
|
|
1143
|
+
console.warn("[GaitButton] Failed to parse customer attribute:", e);
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
},
|
|
1147
|
+
get webhook() {
|
|
1148
|
+
const webhookAttr = element.getAttribute("webhook");
|
|
1149
|
+
if (!webhookAttr) {
|
|
1150
|
+
return null;
|
|
1151
|
+
}
|
|
1152
|
+
try {
|
|
1153
|
+
return JSON.parse(webhookAttr);
|
|
1154
|
+
} catch (e) {
|
|
1155
|
+
console.warn("[GaitButton] Failed to parse webhook attribute:", e);
|
|
1156
|
+
return null;
|
|
1157
|
+
}
|
|
1158
|
+
},
|
|
1159
|
+
get merchantStoreUrl() {
|
|
1160
|
+
const url = element.getAttribute("merchant-store-url");
|
|
1161
|
+
if (url && url.trim() !== "") {
|
|
1162
|
+
return url.trim();
|
|
1163
|
+
}
|
|
1164
|
+
if (typeof window !== "undefined" && window.location) {
|
|
1165
|
+
return window.location.origin;
|
|
1166
|
+
}
|
|
1167
|
+
return "";
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
function isValidEmail(email) {
|
|
1172
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1173
|
+
return emailRegex.test(email);
|
|
1174
|
+
}
|
|
1175
|
+
function calculateSplit(totalCost, splitMethod, emailData) {
|
|
1176
|
+
if (emailData.length === 0) {
|
|
1177
|
+
return totalCost;
|
|
1178
|
+
}
|
|
1179
|
+
if (splitMethod === "equally") {
|
|
1180
|
+
const participantCount = emailData.length + 1;
|
|
1181
|
+
const perPerson = totalCost / participantCount;
|
|
1182
|
+
return Math.max(0, totalCost - perPerson * emailData.length);
|
|
1183
|
+
} else if (splitMethod === "by-amount") {
|
|
1184
|
+
let totalSplit = 0;
|
|
1185
|
+
emailData.forEach((item) => {
|
|
1186
|
+
const amount = item.amount || 0;
|
|
1187
|
+
totalSplit += amount;
|
|
1188
|
+
});
|
|
1189
|
+
return Math.max(0, totalCost - totalSplit);
|
|
1190
|
+
}
|
|
1191
|
+
return totalCost;
|
|
1192
|
+
}
|
|
1193
|
+
function calculateEqualSplit(totalCost, participantCount) {
|
|
1194
|
+
return totalCost / participantCount;
|
|
1195
|
+
}
|
|
1196
|
+
function getEmailData(container, splitMethod, totalCost) {
|
|
1197
|
+
const data = [];
|
|
1198
|
+
const items = container.querySelectorAll("[data-email-item]");
|
|
1199
|
+
items.forEach((item) => {
|
|
1200
|
+
const email = item.getAttribute("data-email-item") || "";
|
|
1201
|
+
if (email) {
|
|
1202
|
+
if (splitMethod === "by-amount") {
|
|
1203
|
+
const amountSpan = item.querySelector(".gait-modal-email-item-amount");
|
|
1204
|
+
const amount = amountSpan ? parseFloat(amountSpan.textContent?.replace(/[^\d.]/g, "") || "0") || 0 : 0;
|
|
1205
|
+
data.push({ email, amount });
|
|
1206
|
+
} else {
|
|
1207
|
+
const participantCount = items.length + 1;
|
|
1208
|
+
const amount = calculateEqualSplit(totalCost, participantCount);
|
|
1209
|
+
data.push({ email, amount });
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
return data;
|
|
1214
|
+
}
|
|
1215
|
+
function getEmailDataCount(container) {
|
|
1216
|
+
const items = container.querySelectorAll("[data-email-item]");
|
|
1217
|
+
const inputs = container.querySelectorAll("[data-email-input]");
|
|
1218
|
+
return items.length + inputs.length;
|
|
1219
|
+
}
|
|
1220
|
+
class SplitPaymentManager {
|
|
1221
|
+
modalElement;
|
|
1222
|
+
totalCost;
|
|
1223
|
+
currentSplitMethod = "equally";
|
|
1224
|
+
dispatchEventFn;
|
|
1225
|
+
isSplitSent = false;
|
|
1226
|
+
isSplitLoading = false;
|
|
1227
|
+
splitFeedbackElement = null;
|
|
1228
|
+
feedbackHandler = null;
|
|
1229
|
+
constructor(modalElement, totalCost, dispatchEventFn) {
|
|
1230
|
+
this.modalElement = modalElement;
|
|
1231
|
+
this.totalCost = totalCost;
|
|
1232
|
+
this.dispatchEventFn = dispatchEventFn;
|
|
1233
|
+
this.setup();
|
|
1234
|
+
this.setupFeedbackListener();
|
|
1235
|
+
}
|
|
1236
|
+
setup() {
|
|
1237
|
+
const splitOptions = this.modalElement.querySelectorAll("[data-split-method]");
|
|
1238
|
+
const emailInputsContainer = this.modalElement.querySelector("[data-email-inputs]");
|
|
1239
|
+
const addEmailButton = this.modalElement.querySelector("[data-add-email]");
|
|
1240
|
+
const sendSplitButton = this.modalElement.querySelector("[data-send-split]");
|
|
1241
|
+
if (!emailInputsContainer || !addEmailButton || !sendSplitButton) {
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
splitOptions.forEach((option) => {
|
|
1245
|
+
option.addEventListener("click", () => {
|
|
1246
|
+
if (this.isSplitSent) {
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
splitOptions.forEach((opt) => opt.classList.remove("selected"));
|
|
1250
|
+
option.classList.add("selected");
|
|
1251
|
+
const newSplitMethod = option.getAttribute("data-split-method") || "equally";
|
|
1252
|
+
const oldSplitMethod = this.currentSplitMethod;
|
|
1253
|
+
this.currentSplitMethod = newSplitMethod;
|
|
1254
|
+
if (oldSplitMethod !== newSplitMethod) {
|
|
1255
|
+
this.updateActiveInputsForSplitMethod(oldSplitMethod, newSplitMethod);
|
|
1256
|
+
}
|
|
1257
|
+
this.updateEmailItemsForSplitMethod();
|
|
1258
|
+
this.updateSplitCalculation();
|
|
1259
|
+
});
|
|
1260
|
+
});
|
|
1261
|
+
addEmailButton.addEventListener("click", () => this.addEmailField());
|
|
1262
|
+
emailInputsContainer.addEventListener("input", (e) => {
|
|
1263
|
+
const target = e.target;
|
|
1264
|
+
if (target.hasAttribute("data-email-input")) {
|
|
1265
|
+
this.updateSplitCalculation();
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
sendSplitButton.addEventListener("click", () => this.handleSendSplit());
|
|
1269
|
+
this.updateSplitCalculation();
|
|
1270
|
+
}
|
|
1271
|
+
addEmailField() {
|
|
1272
|
+
if (this.isSplitSent) {
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
const emailInputsContainer = this.modalElement.querySelector("[data-email-inputs]");
|
|
1276
|
+
if (!emailInputsContainer) return;
|
|
1277
|
+
const existingEmptyInput = this.findEmptyInput(emailInputsContainer);
|
|
1278
|
+
if (existingEmptyInput) {
|
|
1279
|
+
if (existingEmptyInput instanceof HTMLInputElement) {
|
|
1280
|
+
existingEmptyInput.focus();
|
|
1281
|
+
} else {
|
|
1282
|
+
const emailInput = existingEmptyInput.querySelector("[data-email-input]");
|
|
1283
|
+
if (emailInput) {
|
|
1284
|
+
emailInput.focus();
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
if (this.currentSplitMethod === "by-amount") {
|
|
1290
|
+
this.addByAmountInput(emailInputsContainer);
|
|
1291
|
+
} else {
|
|
1292
|
+
this.addEquallyInput(emailInputsContainer);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
findEmptyInput(container) {
|
|
1296
|
+
if (this.currentSplitMethod === "by-amount") {
|
|
1297
|
+
const rows = container.querySelectorAll(".gait-modal-email-amount-row");
|
|
1298
|
+
for (const row of Array.from(rows)) {
|
|
1299
|
+
const emailInput = row.querySelector("[data-email-input]");
|
|
1300
|
+
if (emailInput && !emailInput.value.trim()) {
|
|
1301
|
+
return row;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
} else {
|
|
1305
|
+
const inputs = container.querySelectorAll("[data-email-input]");
|
|
1306
|
+
for (const input of Array.from(inputs)) {
|
|
1307
|
+
if (!input.parentElement?.classList.contains("gait-modal-email-amount-row")) {
|
|
1308
|
+
if (!input.value.trim()) {
|
|
1309
|
+
return input;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
return null;
|
|
1315
|
+
}
|
|
1316
|
+
removeEmptyInputs(container) {
|
|
1317
|
+
const rows = container.querySelectorAll(".gait-modal-email-amount-row");
|
|
1318
|
+
rows.forEach((row) => {
|
|
1319
|
+
const emailInput = row.querySelector("[data-email-input]");
|
|
1320
|
+
if (emailInput && !emailInput.value.trim()) {
|
|
1321
|
+
container.removeChild(row);
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
const inputs = container.querySelectorAll("[data-email-input]");
|
|
1325
|
+
inputs.forEach((input) => {
|
|
1326
|
+
if (!input.parentElement?.classList.contains("gait-modal-email-amount-row")) {
|
|
1327
|
+
if (!input.value.trim()) {
|
|
1328
|
+
container.removeChild(input);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Calculate the remaining amount available for a new split
|
|
1335
|
+
* This is totalCost minus all amounts already assigned to other people
|
|
1336
|
+
*/
|
|
1337
|
+
calculateRemainingAmount(excludeRow) {
|
|
1338
|
+
const emailInputsContainer = this.modalElement.querySelector("[data-email-inputs]");
|
|
1339
|
+
if (!emailInputsContainer) return this.totalCost;
|
|
1340
|
+
let totalAssigned = 0;
|
|
1341
|
+
const items = emailInputsContainer.querySelectorAll("[data-email-item]");
|
|
1342
|
+
items.forEach((item) => {
|
|
1343
|
+
const amountSpan = item.querySelector(".gait-modal-email-item-amount");
|
|
1344
|
+
if (amountSpan) {
|
|
1345
|
+
const amountText = amountSpan.textContent || "0.00";
|
|
1346
|
+
const amount = parseFloat(amountText.replace(/[^\d.]/g, "")) || 0;
|
|
1347
|
+
totalAssigned += amount;
|
|
1348
|
+
}
|
|
1349
|
+
});
|
|
1350
|
+
const rows = emailInputsContainer.querySelectorAll(".gait-modal-email-amount-row");
|
|
1351
|
+
rows.forEach((row) => {
|
|
1352
|
+
if (row === excludeRow) return;
|
|
1353
|
+
const amountInput = row.querySelector("[data-email-amount-input]");
|
|
1354
|
+
if (amountInput && amountInput.value) {
|
|
1355
|
+
const amount = parseFloat(amountInput.value) || 0;
|
|
1356
|
+
totalAssigned += amount;
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
const remaining = Math.max(0, this.totalCost - totalAssigned);
|
|
1360
|
+
return remaining;
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Update max attribute on all amount inputs
|
|
1364
|
+
*/
|
|
1365
|
+
updateAmountInputMaxValues(excludeRow) {
|
|
1366
|
+
const emailInputsContainer = this.modalElement.querySelector("[data-email-inputs]");
|
|
1367
|
+
if (!emailInputsContainer) return;
|
|
1368
|
+
const remaining = this.calculateRemainingAmount(excludeRow);
|
|
1369
|
+
const maxValue = remaining.toFixed(2);
|
|
1370
|
+
const amountInputs = emailInputsContainer.querySelectorAll("[data-email-amount-input]");
|
|
1371
|
+
amountInputs.forEach((input) => {
|
|
1372
|
+
const row = input.closest(".gait-modal-email-amount-row");
|
|
1373
|
+
if (row !== excludeRow) {
|
|
1374
|
+
input.max = maxValue;
|
|
1375
|
+
const currentValue = parseFloat(input.value) || 0;
|
|
1376
|
+
if (currentValue > remaining) {
|
|
1377
|
+
input.value = maxValue;
|
|
1378
|
+
input.style.borderColor = "#ef4444";
|
|
1379
|
+
setTimeout(() => {
|
|
1380
|
+
input.style.borderColor = "";
|
|
1381
|
+
}, 2e3);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
addByAmountInput(container) {
|
|
1387
|
+
const row = document.createElement("div");
|
|
1388
|
+
row.className = "gait-modal-email-amount-row";
|
|
1389
|
+
const emailInput = document.createElement("input");
|
|
1390
|
+
emailInput.type = "email";
|
|
1391
|
+
emailInput.className = "gait-modal-email-input";
|
|
1392
|
+
emailInput.placeholder = "Email address";
|
|
1393
|
+
emailInput.setAttribute("data-email-input", "");
|
|
1394
|
+
const amountInput = document.createElement("input");
|
|
1395
|
+
amountInput.type = "number";
|
|
1396
|
+
amountInput.className = "gait-modal-email-amount-input";
|
|
1397
|
+
amountInput.placeholder = "0.00";
|
|
1398
|
+
amountInput.step = "0.01";
|
|
1399
|
+
amountInput.min = "0";
|
|
1400
|
+
amountInput.setAttribute("data-email-amount-input", "");
|
|
1401
|
+
const remaining = this.calculateRemainingAmount();
|
|
1402
|
+
amountInput.max = remaining.toFixed(2);
|
|
1403
|
+
const doneButton = document.createElement("button");
|
|
1404
|
+
doneButton.type = "button";
|
|
1405
|
+
doneButton.className = "gait-modal-done-button";
|
|
1406
|
+
doneButton.textContent = "Done";
|
|
1407
|
+
doneButton.setAttribute("data-done-email", "");
|
|
1408
|
+
const handleDone = () => {
|
|
1409
|
+
const email = emailInput.value.trim();
|
|
1410
|
+
let amount = parseFloat(amountInput.value) || 0;
|
|
1411
|
+
const remaining2 = this.calculateRemainingAmount(row);
|
|
1412
|
+
if (amount > remaining2) {
|
|
1413
|
+
amount = remaining2;
|
|
1414
|
+
amountInput.value = remaining2.toFixed(2);
|
|
1415
|
+
amountInput.style.borderColor = "#ef4444";
|
|
1416
|
+
setTimeout(() => {
|
|
1417
|
+
amountInput.style.borderColor = "";
|
|
1418
|
+
}, 2e3);
|
|
1419
|
+
}
|
|
1420
|
+
if (email && isValidEmail(email)) {
|
|
1421
|
+
const item = this.createEmailItem(email, amount);
|
|
1422
|
+
container.replaceChild(item, row);
|
|
1423
|
+
this.updateSplitCalculation();
|
|
1424
|
+
this.updateAmountInputMaxValues();
|
|
1425
|
+
} else if (!email) {
|
|
1426
|
+
container.removeChild(row);
|
|
1427
|
+
this.updateAmountInputMaxValues();
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
amountInput.addEventListener("input", () => {
|
|
1431
|
+
const remaining2 = this.calculateRemainingAmount(row);
|
|
1432
|
+
const currentValue = parseFloat(amountInput.value) || 0;
|
|
1433
|
+
if (currentValue > remaining2) {
|
|
1434
|
+
amountInput.value = remaining2.toFixed(2);
|
|
1435
|
+
amountInput.style.borderColor = "#ef4444";
|
|
1436
|
+
setTimeout(() => {
|
|
1437
|
+
amountInput.style.borderColor = "";
|
|
1438
|
+
}, 2e3);
|
|
1439
|
+
} else {
|
|
1440
|
+
amountInput.style.borderColor = "";
|
|
1441
|
+
}
|
|
1442
|
+
this.updateAmountInputMaxValues(row);
|
|
1443
|
+
});
|
|
1444
|
+
doneButton.addEventListener("click", handleDone);
|
|
1445
|
+
emailInput.addEventListener("blur", () => {
|
|
1446
|
+
if (!emailInput.value.trim() && !amountInput.value) {
|
|
1447
|
+
setTimeout(() => {
|
|
1448
|
+
if (row.parentElement && !emailInput.value.trim()) {
|
|
1449
|
+
container.removeChild(row);
|
|
1450
|
+
}
|
|
1451
|
+
}, 100);
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
row.appendChild(emailInput);
|
|
1455
|
+
row.appendChild(amountInput);
|
|
1456
|
+
row.appendChild(doneButton);
|
|
1457
|
+
container.appendChild(row);
|
|
1458
|
+
emailInput.focus();
|
|
1459
|
+
}
|
|
1460
|
+
addEquallyInput(container) {
|
|
1461
|
+
const input = document.createElement("input");
|
|
1462
|
+
input.type = "email";
|
|
1463
|
+
input.className = "gait-modal-email-input";
|
|
1464
|
+
input.placeholder = "Email address";
|
|
1465
|
+
input.setAttribute("data-email-input", "");
|
|
1466
|
+
const handleEmailDone = () => {
|
|
1467
|
+
const email = input.value.trim();
|
|
1468
|
+
if (email && isValidEmail(email)) {
|
|
1469
|
+
this.convertInputToEmailItem(input);
|
|
1470
|
+
this.updateSplitCalculation();
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
input.addEventListener("blur", () => {
|
|
1474
|
+
const email = input.value.trim();
|
|
1475
|
+
if (email && isValidEmail(email)) {
|
|
1476
|
+
handleEmailDone();
|
|
1477
|
+
} else if (!email) {
|
|
1478
|
+
setTimeout(() => {
|
|
1479
|
+
if (input.parentElement && !input.value.trim()) {
|
|
1480
|
+
container.removeChild(input);
|
|
1481
|
+
}
|
|
1482
|
+
}, 100);
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
input.addEventListener("keydown", (e) => {
|
|
1486
|
+
if (e.key === "Enter") {
|
|
1487
|
+
e.preventDefault();
|
|
1488
|
+
handleEmailDone();
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
container.appendChild(input);
|
|
1492
|
+
input.focus();
|
|
1493
|
+
}
|
|
1494
|
+
createEmailItem(email, amount = 0, isSent = false) {
|
|
1495
|
+
const item = document.createElement("div");
|
|
1496
|
+
item.className = `gait-modal-email-item${isSent ? " sent" : ""}`;
|
|
1497
|
+
item.setAttribute("data-email-item", email);
|
|
1498
|
+
let displayAmount = amount;
|
|
1499
|
+
if (this.currentSplitMethod === "equally") {
|
|
1500
|
+
const container = this.modalElement.querySelector("[data-email-inputs]");
|
|
1501
|
+
const participantCount = container ? getEmailDataCount(container) + 1 : 2;
|
|
1502
|
+
displayAmount = calculateEqualSplit(this.totalCost, participantCount);
|
|
1503
|
+
}
|
|
1504
|
+
const checkmark = isSent ? `<div class="gait-modal-email-item-sent-check">${checkIcon}</div>` : "";
|
|
1505
|
+
item.innerHTML = `
|
|
1506
|
+
<span class="gait-modal-email-item-email">${email}</span>
|
|
942
1507
|
<div class="gait-modal-email-item-right">
|
|
943
1508
|
<div class="gait-modal-email-item-amount-container">
|
|
944
|
-
<span class="gait-modal-email-item-amount">R ${
|
|
945
|
-
${
|
|
1509
|
+
<span class="gait-modal-email-item-amount">R ${displayAmount.toFixed(2)}</span>
|
|
1510
|
+
${checkmark}
|
|
946
1511
|
</div>
|
|
947
1512
|
<div class="gait-modal-email-item-actions">
|
|
948
|
-
<button class="gait-modal-email-item-edit" data-edit-email="${
|
|
949
|
-
<button class="gait-modal-email-item-remove" data-remove-email="${
|
|
1513
|
+
<button class="gait-modal-email-item-edit" data-edit-email="${email}">Edit</button>
|
|
1514
|
+
<button class="gait-modal-email-item-remove" data-remove-email="${email}">Remove</button>
|
|
950
1515
|
</div>
|
|
951
1516
|
</div>
|
|
952
|
-
`;
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
`;const n=this.modalElement.querySelector(".gait-modal-split-section");n&&(n.insertBefore(i,n.firstChild),this.splitFeedbackElement=i,e!=="loading"&&setTimeout(()=>{this.splitFeedbackElement===i&&(i.style.opacity="0",setTimeout(()=>{i.parentElement&&i.remove(),this.splitFeedbackElement===i&&(this.splitFeedbackElement=null)},300))},5e3))}disableSplitOptions(){this.modalElement.querySelectorAll("[data-split-method]").forEach(i=>{const a=i;a.style.pointerEvents="none",a.style.opacity="0.5",a.style.cursor="not-allowed",a.setAttribute("aria-disabled","true")});const e=this.modalElement.querySelector("[data-add-email]");e&&(e.style.pointerEvents="none",e.style.opacity="0.5",e.style.cursor="not-allowed",e.setAttribute("aria-disabled","true"))}}async function et(r,t=2e3){console.log("[SplitPaymentAPI] Sending split payment request:",r);const e="https://api.usegait.com/checkout";try{const a=await(await fetch(`${e}/v1/payments`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r),signal:AbortSignal.timeout(t)})).json().catch(()=>({success:!1,error:"Failed to parse API response"}));if(a.success){const n={success:!0,message:a.message,data:a.data};return console.log("[SplitPaymentAPI] Split payment sent successfully:",n),n}else{const n={success:!1,message:a.message,error:a.error};return console.error("[SplitPaymentAPI] Split payment failed:",n),n}}catch(i){const a={success:!1,message:"Failed to send split payment",error:i instanceof Error?i.message:"Network error or server unavailable"};return console.error("[SplitPaymentAPI] Split payment failed:",a,i),a}}let at=r=>crypto.getRandomValues(new Uint8Array(r)),it=(r,t,e)=>{let i=(2<<Math.log2(r.length-1))-1,a=-~(1.6*i*t/r.length);return(n=t)=>{let o="";for(;;){let d=e(a),l=a|0;for(;l--;)if(o+=r[d[l]&i]||"",o.length>=n)return o}}},ot=(r,t=21)=>it(r,t|0,at);const M={apiGateway:{MERCHANT_SERVICE_API:"https://api.usegait.com/merchant",CHECKOUT_SERVICE_API:"https://api.usegait.com/checkout"},paylink:{DOMAIN:"https://checkout.usegait.com"}},nt=M.apiGateway.MERCHANT_SERVICE_API;class rt{static async getMerchant(t){try{return await(await fetch(`${nt}/v1/merchant/identity/${t}`,{method:"GET",headers:{"Content-Type":"application/json"}})).json()}catch(e){return e}}}const k=M.apiGateway.CHECKOUT_SERVICE_API;class lt{static async getPayment(t){try{return await(await fetch(`${k}/v1/payments/${t}`,{method:"GET",headers:{"Content-Type":"application/json"}})).json()}catch(e){return e}}static async updatePaymentStatus(t,e,i){try{return await(await fetch(`${k}/v1/payments/merchants/${t}/${e}/status`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({status:i})})).json()}catch(a){return a}}static async createWebhook(t,e,i){try{return await(await fetch(`${k}/v1/webhooks/${t}/${e}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)})).json()}catch(a){return a}}static async updatePayment(t,e,i){try{return await(await fetch(`${k}/v1/payments/merchants/${t}/${i}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({paymentId:e})})).json()}catch(a){return a}}}const S=(typeof process<"u"&&process.env?"0.1.0":void 0)||"0.0.0";class x extends HTMLElement{buttonElement=null;hasRendered=!1;mutationObserver=null;modalElement=null;modalId;splitPaymentManager=null;attributeGetters;storedSplitPaymentRequest=null;merchantId=null;paymentProvider="stripe";static version=S;version=S;static get observedAttributes(){return["data-id","disabled","size","style","items","price-breakdown","total-cost","webhook","merchant-store-url"]}constructor(){super(),this.attachShadow({mode:"open"}),this.modalId=`gait-modal-${Math.random().toString(36).substr(2,9)}`,this.attributeGetters=X(this)}connectedCallback(){!this.hasRendered&&this.shadowRoot&&this.render(),this.setupEventListeners(),this.setupMutationObserver(),this.setupApiListeners(),setTimeout(()=>this.processData(),0)}disconnectedCallback(){this.buttonElement&&this.buttonElement.removeEventListener("click",this.handleClick),this.mutationObserver&&(this.mutationObserver.disconnect(),this.mutationObserver=null),this.closeModal(),this.splitPaymentManager&&(this.splitPaymentManager=null),this.removeEventListener("gait-split-sent",this.handleSplitSent)}attributeChangedCallback(t,e,i){if(e!==i&&!(!this.isConnected||!this.shadowRoot)){if(t==="style"&&this.buttonElement&&this.hasRendered){const a=(i||"").replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,"");this.buttonElement.setAttribute("style",a);return}this.hasRendered&&(this.render(),this.setupEventListeners()),(t==="data-id"||t.startsWith("data-"))&&(t==="data-id"&&(this.merchantId=null),setTimeout(()=>this.processData(),0))}}get dataId(){return this.attributeGetters.dataId}get disabled(){return this.attributeGetters.disabled}get size(){return this.attributeGetters.size}get items(){return this.attributeGetters.items}get priceBreakdown(){return this.attributeGetters.priceBreakdown}get totalCost(){return this.attributeGetters.totalCost}get customer(){return this.attributeGetters.customer}get webhook(){return this.attributeGetters.webhook}get merchantStoreUrl(){return this.attributeGetters.merchantStoreUrl}getDataAttributes(){return Z(this)}async fetchMerchantId(){const t=this.dataId;if(!t)return console.warn("[GaitButton] No checkoutId (data-id) provided, cannot fetch merchantId"),this.showMerchantIdError(),!1;try{const e=await rt.getMerchant(t);return e.success&&e.data?.merchantId?(this.merchantId=e.data.merchantId,this.paymentProvider=e.data.paymentProvider,!0):(console.error("[GaitButton] Failed to fetch merchantId:",e),this.showMerchantIdError(),!1)}catch(e){return console.error("[GaitButton] Error fetching merchantId:",e),this.showMerchantIdError(),!1}}showMerchantIdError(){const t="Unable to identify your account. Please go to your dashboard and copy your checkout identity.";alert(t),this.dispatchEvent(new CustomEvent("gait-merchant-id-error",{detail:{message:t,timestamp:new Date().toISOString()},bubbles:!0,composed:!0}))}async processData(){const t=this.dataId,e=this.getDataAttributes();if(t&&!this.merchantId&&await this.fetchMerchantId(),t||Object.keys(e).length>0){const i={id:t,...e,timestamp:new Date().toISOString()};this.dispatchEvent(new CustomEvent("gait-data-processed",{detail:i,bubbles:!0,composed:!0}))}}handleClick=async t=>{if(this.disabled){t.preventDefault(),t.stopPropagation();return}const e={label:"Gait",dataId:this.dataId,dataAttributes:this.getDataAttributes(),timestamp:new Date().toISOString()};this.dispatchEvent(new CustomEvent("gait-click",{detail:e,bubbles:!0,composed:!0})),this.dispatchEvent(new CustomEvent("click",{detail:e,bubbles:!0,composed:!0})),!(!this.merchantId&&(!await this.fetchMerchantId()||!this.merchantId))&&this.openModal()};setupEventListeners(){this.shadowRoot&&(this.buttonElement&&this.buttonElement.removeEventListener("click",this.handleClick),this.buttonElement=this.shadowRoot.querySelector("button"),this.buttonElement&&this.buttonElement.addEventListener("click",this.handleClick))}setupMutationObserver(){this.mutationObserver&&this.mutationObserver.disconnect(),this.mutationObserver=new MutationObserver(t=>{let e=!1;for(const i of t)if(i.type==="attributes"){const a=i.attributeName;if(a&&a.startsWith("data-")&&!x.observedAttributes.includes(a)){e=!0;break}}e&&this.isConnected&&this.processData()}),this.mutationObserver.observe(this,{attributes:!0,attributeFilter:void 0})}setupApiListeners(){this.addEventListener("gait-split-sent",this.handleSplitSent)}handleConfirmPayment=async()=>{if(console.log("[GaitButton] Pay remaining button clicked"),!this.storedSplitPaymentRequest){console.error("[GaitButton] Split payment must be sent first before paying remaining amount");return}if(!this.merchantId&&(!await this.fetchMerchantId()||!this.merchantId))return;const t={merchantId:this.merchantId,splitId:this.storedSplitPaymentRequest.splitId,paymentId:this.storedSplitPaymentRequest.paymentSplits.find(a=>a.initiator)?.paymentId},i=`${M.paylink.DOMAIN}/payment/${t.paymentId}`;window.open(i,"_blank"),this.dispatchEvent(new CustomEvent("gait-confirm",{detail:t,bubbles:!0,composed:!0})),this.closeModal()};handleSplitSent=async t=>{const i=t.detail;if(!this.merchantId&&(!await this.fetchMerchantId()||!this.merchantId))return;const a=this.merchantId,n=this.customer;if(!n||!n.email){console.error("[GaitButton] Customer email is required for split payment");return}const o=this.webhook;if(!o||!o.url||o.body==null){console.error("[GaitButton] Webhook with url and body is required");return}const l=ot("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",6),s=l(),m=[];m.push({paymentId:l(),email:n.email,amount:i.yourAmount,isPaid:!1,datePaid:null,initiator:!0}),i.emailData.forEach(c=>{m.push({paymentId:l(),email:c.email,amount:c.amount||0,isPaid:!1,datePaid:null,initiator:!1})});const u={merchantId:a,splitId:s,paymentSplits:m,splitMethod:i.splitMethod,timestamp:i.timestamp,totalCost:i.totalCost,items:this.items,priceBreakdown:this.priceBreakdown,brandColor:g,merchantStoreUrl:this.merchantStoreUrl,subTotal:this.priceBreakdown.find(c=>c.name==="Subtotal")?.price||0,paymentProvider:this.paymentProvider};console.log("[GaitButton] Split payment request body:",JSON.stringify(u,null,2));try{const c=await et(u,2e3);if(c.success){const p=c.data?.splitId||s;this.storedSplitPaymentRequest={...u,splitId:p};try{const b=await lt.createWebhook(a,p,{url:o.url,body:o.body});console.log("[GaitButton] Webhook created:",b)}catch(b){console.error("[GaitButton] Failed to create webhook:",b)}}if(this.modalElement){const p=this.modalElement.querySelector("[data-gait-modal-confirm]");p&&(p.disabled=!1,p.style.opacity="1",p.style.cursor="pointer")}this.dispatchEvent(new CustomEvent("gait-split-feedback",{detail:{success:c.success,message:c.message,data:c.success?this.storedSplitPaymentRequest:c.data,error:c.error,timestamp:new Date().toISOString()},bubbles:!0,composed:!0})),console.log("[GaitButton] Split payment API response:",c)}catch(c){console.error("[GaitButton] Split payment API error:",c),this.dispatchEvent(new CustomEvent("gait-split-feedback",{detail:{success:!1,message:"Failed to send split payment",error:c instanceof Error?c.message:"Unknown error",timestamp:new Date().toISOString()},bubbles:!0,composed:!0}))}};openModal(){V(),this.closeModal(),this.modalElement=document.createElement("div"),this.modalElement.className="gait-modal-backdrop",this.modalElement.id=this.modalId,this.modalElement&&(this.modalElement.style.setProperty("--gait-modal-brand-color",g),this.modalElement.style.setProperty("--gait-modal-button-bg",g),this.modalElement.style.setProperty("--gait-modal-button-bg-hover",F(g,.15)),this.modalElement.style.setProperty("--gait-modal-button-color",q(g)?"#ffffff":"#000000")),this.modalElement.innerHTML=K(this.items,this.priceBreakdown,this.totalCost);const t=this.modalElement.querySelector("[data-gait-modal-close]");t&&t.addEventListener("click",this.closeModal);const e=this.modalElement.querySelector("[data-gait-modal-confirm]");e&&(e.disabled=!0,e.style.opacity="0.5",e.style.cursor="not-allowed",e.addEventListener("click",this.handleConfirmPayment)),this.splitPaymentManager=new tt(this.modalElement,this.totalCost,a=>this.dispatchEvent(a)),this.modalElement._splitPaymentManager=this.splitPaymentManager,this.modalElement.addEventListener("click",a=>{a.target===this.modalElement&&this.closeModal()});const i=a=>{a.key==="Escape"&&this.modalElement&&(this.closeModal(),document.removeEventListener("keydown",i))};document.addEventListener("keydown",i),document.body.appendChild(this.modalElement),requestAnimationFrame(()=>{this.modalElement&&this.modalElement.classList.add("gait-modal-open")})}closeModal=()=>{this.modalElement&&(this.splitPaymentManager&&this.splitPaymentManager.cleanup(),this.modalElement.classList.remove("gait-modal-open"),setTimeout(()=>{this.modalElement&&this.modalElement.parentNode&&this.modalElement.parentNode.removeChild(this.modalElement),this.modalElement=null,this.splitPaymentManager=null},250))};render(){const t=this.shadowRoot;if(!t)return;const e=!this.hasRendered,i=D(this.size,F,q),n=(this.getAttribute("style")||"").replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,"");if(t.innerHTML=`
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1517
|
+
`;
|
|
1518
|
+
const editButton = item.querySelector(`[data-edit-email="${email}"]`);
|
|
1519
|
+
if (editButton) {
|
|
1520
|
+
editButton.addEventListener("click", (e) => {
|
|
1521
|
+
e.preventDefault();
|
|
1522
|
+
e.stopPropagation();
|
|
1523
|
+
this.convertEmailItemToInput(item);
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
const removeButton = item.querySelector(`[data-remove-email="${email}"]`);
|
|
1527
|
+
if (removeButton) {
|
|
1528
|
+
removeButton.addEventListener("click", (e) => {
|
|
1529
|
+
e.preventDefault();
|
|
1530
|
+
e.stopPropagation();
|
|
1531
|
+
this.removeEmailItem(item);
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
return item;
|
|
1535
|
+
}
|
|
1536
|
+
convertEmailItemToInput(item) {
|
|
1537
|
+
const container = item.parentElement;
|
|
1538
|
+
if (!container) return;
|
|
1539
|
+
const email = item.getAttribute("data-email-item") || "";
|
|
1540
|
+
if (this.currentSplitMethod === "by-amount") {
|
|
1541
|
+
const row = document.createElement("div");
|
|
1542
|
+
row.className = "gait-modal-email-amount-row";
|
|
1543
|
+
const emailInput = document.createElement("input");
|
|
1544
|
+
emailInput.type = "email";
|
|
1545
|
+
emailInput.className = "gait-modal-email-input";
|
|
1546
|
+
emailInput.placeholder = "Email address";
|
|
1547
|
+
emailInput.setAttribute("data-email-input", "");
|
|
1548
|
+
emailInput.value = email;
|
|
1549
|
+
const amountInput = document.createElement("input");
|
|
1550
|
+
amountInput.type = "number";
|
|
1551
|
+
amountInput.className = "gait-modal-email-amount-input";
|
|
1552
|
+
amountInput.placeholder = "0.00";
|
|
1553
|
+
amountInput.step = "0.01";
|
|
1554
|
+
amountInput.min = "0";
|
|
1555
|
+
amountInput.setAttribute("data-email-amount-input", "");
|
|
1556
|
+
const amountSpan = item.querySelector(".gait-modal-email-item-amount");
|
|
1557
|
+
let initialAmount = 0;
|
|
1558
|
+
if (amountSpan) {
|
|
1559
|
+
const amountText = amountSpan.textContent || "0.00";
|
|
1560
|
+
initialAmount = parseFloat(amountText.replace(/[^\d.]/g, "")) || 0;
|
|
1561
|
+
amountInput.value = initialAmount.toFixed(2);
|
|
1562
|
+
}
|
|
1563
|
+
const remaining = this.calculateRemainingAmount(row) + initialAmount;
|
|
1564
|
+
amountInput.max = remaining.toFixed(2);
|
|
1565
|
+
const doneButton = document.createElement("button");
|
|
1566
|
+
doneButton.type = "button";
|
|
1567
|
+
doneButton.className = "gait-modal-done-button";
|
|
1568
|
+
doneButton.textContent = "Done";
|
|
1569
|
+
doneButton.setAttribute("data-done-email", "");
|
|
1570
|
+
doneButton.addEventListener("click", () => {
|
|
1571
|
+
const emailVal = emailInput.value.trim();
|
|
1572
|
+
let amountVal = parseFloat(amountInput.value) || 0;
|
|
1573
|
+
const remaining2 = this.calculateRemainingAmount(row) + initialAmount;
|
|
1574
|
+
if (amountVal > remaining2) {
|
|
1575
|
+
amountVal = remaining2;
|
|
1576
|
+
amountInput.value = remaining2.toFixed(2);
|
|
1577
|
+
amountInput.style.borderColor = "#ef4444";
|
|
1578
|
+
setTimeout(() => {
|
|
1579
|
+
amountInput.style.borderColor = "";
|
|
1580
|
+
}, 2e3);
|
|
1581
|
+
}
|
|
1582
|
+
if (emailVal && isValidEmail(emailVal)) {
|
|
1583
|
+
const newItem = this.createEmailItem(emailVal, amountVal);
|
|
1584
|
+
if (row.parentElement) {
|
|
1585
|
+
row.parentElement.replaceChild(newItem, row);
|
|
1586
|
+
}
|
|
1587
|
+
this.updateSplitCalculation();
|
|
1588
|
+
this.updateAmountInputMaxValues();
|
|
1589
|
+
} else if (!emailVal) {
|
|
1590
|
+
if (row.parentElement) {
|
|
1591
|
+
row.parentElement.removeChild(row);
|
|
1592
|
+
}
|
|
1593
|
+
this.updateAmountInputMaxValues();
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
amountInput.addEventListener("input", () => {
|
|
1597
|
+
const remaining2 = this.calculateRemainingAmount(row) + initialAmount;
|
|
1598
|
+
const currentValue = parseFloat(amountInput.value) || 0;
|
|
1599
|
+
if (currentValue > remaining2) {
|
|
1600
|
+
amountInput.value = remaining2.toFixed(2);
|
|
1601
|
+
amountInput.style.borderColor = "#ef4444";
|
|
1602
|
+
setTimeout(() => {
|
|
1603
|
+
amountInput.style.borderColor = "";
|
|
1604
|
+
}, 2e3);
|
|
1605
|
+
} else {
|
|
1606
|
+
amountInput.style.borderColor = "";
|
|
1607
|
+
}
|
|
1608
|
+
this.updateAmountInputMaxValues(row);
|
|
1609
|
+
});
|
|
1610
|
+
row.appendChild(emailInput);
|
|
1611
|
+
row.appendChild(amountInput);
|
|
1612
|
+
row.appendChild(doneButton);
|
|
1613
|
+
if (item.parentElement) {
|
|
1614
|
+
item.parentElement.replaceChild(row, item);
|
|
1615
|
+
}
|
|
1616
|
+
emailInput.focus();
|
|
1617
|
+
emailInput.select();
|
|
1618
|
+
} else {
|
|
1619
|
+
const input = document.createElement("input");
|
|
1620
|
+
input.type = "email";
|
|
1621
|
+
input.className = "gait-modal-email-input";
|
|
1622
|
+
input.placeholder = "Email address";
|
|
1623
|
+
input.setAttribute("data-email-input", "");
|
|
1624
|
+
input.value = email;
|
|
1625
|
+
const handleEmailDone = () => {
|
|
1626
|
+
const emailVal = input.value.trim();
|
|
1627
|
+
if (emailVal && isValidEmail(emailVal)) {
|
|
1628
|
+
this.convertInputToEmailItem(input);
|
|
1629
|
+
this.updateSplitCalculation();
|
|
1630
|
+
} else if (!emailVal) {
|
|
1631
|
+
setTimeout(() => {
|
|
1632
|
+
if (input.parentElement && !input.value.trim()) {
|
|
1633
|
+
input.parentElement.removeChild(input);
|
|
1634
|
+
}
|
|
1635
|
+
}, 100);
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
input.addEventListener("blur", handleEmailDone);
|
|
1639
|
+
input.addEventListener("keydown", (e) => {
|
|
1640
|
+
if (e.key === "Enter") {
|
|
1641
|
+
e.preventDefault();
|
|
1642
|
+
handleEmailDone();
|
|
1643
|
+
}
|
|
1644
|
+
});
|
|
1645
|
+
if (item.parentElement) {
|
|
1646
|
+
item.parentElement.replaceChild(input, item);
|
|
1647
|
+
}
|
|
1648
|
+
input.focus();
|
|
1649
|
+
input.select();
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
convertInputToEmailItem(input) {
|
|
1653
|
+
const email = input.value.trim();
|
|
1654
|
+
if (!email || !isValidEmail(email)) {
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
const container = input.parentElement;
|
|
1658
|
+
if (!container) return;
|
|
1659
|
+
const participantCount = getEmailDataCount(container) + 1;
|
|
1660
|
+
const perPersonAmount = this.currentSplitMethod === "equally" ? calculateEqualSplit(this.totalCost, participantCount) : 0;
|
|
1661
|
+
const item = this.createEmailItem(email, perPersonAmount);
|
|
1662
|
+
container.replaceChild(item, input);
|
|
1663
|
+
}
|
|
1664
|
+
removeEmailItem(item) {
|
|
1665
|
+
const container = item.parentElement;
|
|
1666
|
+
if (container) {
|
|
1667
|
+
container.removeChild(item);
|
|
1668
|
+
this.updateSplitCalculation();
|
|
1669
|
+
this.updateAmountInputMaxValues();
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
updateActiveInputsForSplitMethod(oldMethod, newMethod) {
|
|
1673
|
+
const emailInputsContainer = this.modalElement.querySelector("[data-email-inputs]");
|
|
1674
|
+
if (!emailInputsContainer) return;
|
|
1675
|
+
this.removeEmptyInputs(emailInputsContainer);
|
|
1676
|
+
if (oldMethod === "equally" && newMethod === "by-amount") {
|
|
1677
|
+
const emailInputs = Array.from(emailInputsContainer.querySelectorAll("[data-email-input]")).filter((input) => !input.parentElement?.classList.contains("gait-modal-email-amount-row"));
|
|
1678
|
+
emailInputs.forEach((input) => {
|
|
1679
|
+
const email = input.value.trim();
|
|
1680
|
+
const container = input.parentElement;
|
|
1681
|
+
if (!container) return;
|
|
1682
|
+
const row = document.createElement("div");
|
|
1683
|
+
row.className = "gait-modal-email-amount-row";
|
|
1684
|
+
const emailInput = document.createElement("input");
|
|
1685
|
+
emailInput.type = "email";
|
|
1686
|
+
emailInput.className = "gait-modal-email-input";
|
|
1687
|
+
emailInput.placeholder = "Email address";
|
|
1688
|
+
emailInput.setAttribute("data-email-input", "");
|
|
1689
|
+
emailInput.value = email;
|
|
1690
|
+
const amountInput = document.createElement("input");
|
|
1691
|
+
amountInput.type = "number";
|
|
1692
|
+
amountInput.className = "gait-modal-email-amount-input";
|
|
1693
|
+
amountInput.placeholder = "0.00";
|
|
1694
|
+
amountInput.step = "0.01";
|
|
1695
|
+
amountInput.min = "0";
|
|
1696
|
+
amountInput.setAttribute("data-email-amount-input", "");
|
|
1697
|
+
const remaining = this.calculateRemainingAmount(row);
|
|
1698
|
+
amountInput.max = remaining.toFixed(2);
|
|
1699
|
+
const doneButton = document.createElement("button");
|
|
1700
|
+
doneButton.type = "button";
|
|
1701
|
+
doneButton.className = "gait-modal-done-button";
|
|
1702
|
+
doneButton.textContent = "Done";
|
|
1703
|
+
doneButton.setAttribute("data-done-email", "");
|
|
1704
|
+
doneButton.addEventListener("click", () => {
|
|
1705
|
+
const emailVal = emailInput.value.trim();
|
|
1706
|
+
let amountVal = parseFloat(amountInput.value) || 0;
|
|
1707
|
+
const remaining2 = this.calculateRemainingAmount(row);
|
|
1708
|
+
if (amountVal > remaining2) {
|
|
1709
|
+
amountVal = remaining2;
|
|
1710
|
+
amountInput.value = remaining2.toFixed(2);
|
|
1711
|
+
amountInput.style.borderColor = "#ef4444";
|
|
1712
|
+
setTimeout(() => {
|
|
1713
|
+
amountInput.style.borderColor = "";
|
|
1714
|
+
}, 2e3);
|
|
1715
|
+
}
|
|
1716
|
+
if (emailVal && isValidEmail(emailVal)) {
|
|
1717
|
+
const item = this.createEmailItem(emailVal, amountVal);
|
|
1718
|
+
container.replaceChild(item, row);
|
|
1719
|
+
this.updateSplitCalculation();
|
|
1720
|
+
this.updateAmountInputMaxValues();
|
|
1721
|
+
}
|
|
1722
|
+
});
|
|
1723
|
+
amountInput.addEventListener("input", () => {
|
|
1724
|
+
const remaining2 = this.calculateRemainingAmount(row);
|
|
1725
|
+
const currentValue = parseFloat(amountInput.value) || 0;
|
|
1726
|
+
if (currentValue > remaining2) {
|
|
1727
|
+
amountInput.value = remaining2.toFixed(2);
|
|
1728
|
+
amountInput.style.borderColor = "#ef4444";
|
|
1729
|
+
setTimeout(() => {
|
|
1730
|
+
amountInput.style.borderColor = "";
|
|
1731
|
+
}, 2e3);
|
|
1732
|
+
} else {
|
|
1733
|
+
amountInput.style.borderColor = "";
|
|
1734
|
+
}
|
|
1735
|
+
this.updateAmountInputMaxValues(row);
|
|
1736
|
+
});
|
|
1737
|
+
row.appendChild(emailInput);
|
|
1738
|
+
row.appendChild(amountInput);
|
|
1739
|
+
row.appendChild(doneButton);
|
|
1740
|
+
container.replaceChild(row, input);
|
|
1741
|
+
emailInput.focus();
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
if (oldMethod === "by-amount" && newMethod === "equally") {
|
|
1745
|
+
const rows = emailInputsContainer.querySelectorAll(".gait-modal-email-amount-row");
|
|
1746
|
+
rows.forEach((row) => {
|
|
1747
|
+
const emailInput = row.querySelector("[data-email-input]");
|
|
1748
|
+
if (!emailInput) return;
|
|
1749
|
+
const email = emailInput.value.trim();
|
|
1750
|
+
const container = row.parentElement;
|
|
1751
|
+
if (!container) return;
|
|
1752
|
+
const input = document.createElement("input");
|
|
1753
|
+
input.type = "email";
|
|
1754
|
+
input.className = "gait-modal-email-input";
|
|
1755
|
+
input.placeholder = "Email address";
|
|
1756
|
+
input.setAttribute("data-email-input", "");
|
|
1757
|
+
input.value = email;
|
|
1758
|
+
const handleEmailDone = () => {
|
|
1759
|
+
const emailVal = input.value.trim();
|
|
1760
|
+
if (emailVal && isValidEmail(emailVal)) {
|
|
1761
|
+
this.convertInputToEmailItem(input);
|
|
1762
|
+
this.updateSplitCalculation();
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
input.addEventListener("blur", handleEmailDone);
|
|
1766
|
+
input.addEventListener("keydown", (e) => {
|
|
1767
|
+
if (e.key === "Enter") {
|
|
1768
|
+
e.preventDefault();
|
|
1769
|
+
handleEmailDone();
|
|
1770
|
+
}
|
|
1771
|
+
});
|
|
1772
|
+
container.replaceChild(input, row);
|
|
1773
|
+
input.focus();
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
updateEmailItemsForSplitMethod() {
|
|
1778
|
+
const emailInputsContainer = this.modalElement.querySelector("[data-email-inputs]");
|
|
1779
|
+
if (!emailInputsContainer) return;
|
|
1780
|
+
const items = emailInputsContainer.querySelectorAll("[data-email-item]");
|
|
1781
|
+
const participantCount = items.length + 1;
|
|
1782
|
+
items.forEach((item) => {
|
|
1783
|
+
const rightColumn = item.querySelector(".gait-modal-email-item-right");
|
|
1784
|
+
if (!rightColumn) return;
|
|
1785
|
+
const amountSpan = rightColumn.querySelector(".gait-modal-email-item-amount");
|
|
1786
|
+
if (!amountSpan) return;
|
|
1787
|
+
if (this.currentSplitMethod === "equally") {
|
|
1788
|
+
const perPerson = calculateEqualSplit(this.totalCost, participantCount);
|
|
1789
|
+
amountSpan.textContent = `R ${perPerson.toFixed(2)}`;
|
|
1790
|
+
} else if (this.currentSplitMethod === "by-amount") {
|
|
1791
|
+
const currentAmountText = amountSpan.textContent || "R 0.00";
|
|
1792
|
+
const currentAmount = parseFloat(currentAmountText.replace(/[^\d.]/g, "")) || 0;
|
|
1793
|
+
amountSpan.textContent = `R ${currentAmount.toFixed(2)}`;
|
|
1794
|
+
}
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
updateSplitCalculation() {
|
|
1798
|
+
const emailInputsContainer = this.modalElement.querySelector("[data-email-inputs]");
|
|
1799
|
+
const confirmAmountDisplay = this.modalElement.querySelector("[data-confirm-amount]");
|
|
1800
|
+
if (!emailInputsContainer || !confirmAmountDisplay) {
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
const emailData = getEmailData(emailInputsContainer, this.currentSplitMethod, this.totalCost);
|
|
1804
|
+
const emailInputs = emailInputsContainer.querySelectorAll("[data-email-input]");
|
|
1805
|
+
emailInputs.forEach((input) => {
|
|
1806
|
+
const email = input.value.trim();
|
|
1807
|
+
if (email && isValidEmail(email)) {
|
|
1808
|
+
const participantCount = emailData.length + emailInputs.length + 1;
|
|
1809
|
+
const amount = this.currentSplitMethod === "equally" ? calculateEqualSplit(this.totalCost, participantCount) : 0;
|
|
1810
|
+
emailData.push({ email, amount });
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
const yourAmount = emailData.length > 0 ? calculateSplit(this.totalCost, this.currentSplitMethod, emailData) : this.totalCost;
|
|
1814
|
+
confirmAmountDisplay.textContent = `R ${yourAmount.toFixed(2)}`;
|
|
1815
|
+
if (this.currentSplitMethod === "by-amount") {
|
|
1816
|
+
this.updateAmountInputMaxValues();
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
handleSendSplit() {
|
|
1820
|
+
const emailInputsContainer = this.modalElement.querySelector("[data-email-inputs]");
|
|
1821
|
+
const sendSplitButton = this.modalElement.querySelector("[data-send-split]");
|
|
1822
|
+
if (!emailInputsContainer || !sendSplitButton) return;
|
|
1823
|
+
const emailData = getEmailData(emailInputsContainer, this.currentSplitMethod, this.totalCost);
|
|
1824
|
+
if (emailData.length === 0) {
|
|
1825
|
+
this.showFeedback("Please add at least one email address", "error");
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
if (this.isSplitSent || this.isSplitLoading) {
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
this.isSplitLoading = true;
|
|
1832
|
+
sendSplitButton.disabled = true;
|
|
1833
|
+
sendSplitButton.textContent = "Sending...";
|
|
1834
|
+
this.showFeedback("Sending split payment...", "loading");
|
|
1835
|
+
const yourAmount = calculateSplit(this.totalCost, this.currentSplitMethod, emailData);
|
|
1836
|
+
this.dispatchEventFn(new CustomEvent("gait-split-sent", {
|
|
1837
|
+
detail: {
|
|
1838
|
+
splitMethod: this.currentSplitMethod,
|
|
1839
|
+
emails: emailData.map((d) => d.email),
|
|
1840
|
+
emailData,
|
|
1841
|
+
totalCost: this.totalCost,
|
|
1842
|
+
yourAmount,
|
|
1843
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1844
|
+
},
|
|
1845
|
+
bubbles: true,
|
|
1846
|
+
composed: true
|
|
1847
|
+
}));
|
|
1848
|
+
}
|
|
1849
|
+
setupFeedbackListener() {
|
|
1850
|
+
this.feedbackHandler = (event) => {
|
|
1851
|
+
const customEvent = event;
|
|
1852
|
+
if (customEvent.detail) {
|
|
1853
|
+
this.handleSplitFeedback(customEvent.detail);
|
|
1854
|
+
}
|
|
1855
|
+
};
|
|
1856
|
+
document.addEventListener("gait-split-feedback", this.feedbackHandler);
|
|
1857
|
+
}
|
|
1858
|
+
// Cleanup method (can be called when modal is closed)
|
|
1859
|
+
cleanup() {
|
|
1860
|
+
if (this.feedbackHandler) {
|
|
1861
|
+
document.removeEventListener("gait-split-feedback", this.feedbackHandler);
|
|
1862
|
+
this.feedbackHandler = null;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
handleSplitFeedback(feedback) {
|
|
1866
|
+
this.isSplitLoading = false;
|
|
1867
|
+
const sendSplitButton = this.modalElement.querySelector("[data-send-split]");
|
|
1868
|
+
if (feedback.success) {
|
|
1869
|
+
this.isSplitSent = true;
|
|
1870
|
+
this.markEmailsAsSent();
|
|
1871
|
+
this.disableSplitOptions();
|
|
1872
|
+
if (sendSplitButton) {
|
|
1873
|
+
sendSplitButton.disabled = true;
|
|
1874
|
+
sendSplitButton.textContent = "Split sent!";
|
|
1875
|
+
}
|
|
1876
|
+
this.showFeedback(feedback.message || "Split payment sent successfully", "success");
|
|
1877
|
+
} else {
|
|
1878
|
+
if (sendSplitButton) {
|
|
1879
|
+
sendSplitButton.disabled = false;
|
|
1880
|
+
sendSplitButton.textContent = "Send split";
|
|
1881
|
+
}
|
|
1882
|
+
this.showFeedback(feedback.message || feedback.error || "Failed to send split payment", "error");
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
markEmailsAsSent() {
|
|
1886
|
+
const emailInputsContainer = this.modalElement.querySelector("[data-email-inputs]");
|
|
1887
|
+
if (!emailInputsContainer) return;
|
|
1888
|
+
const emailItems = emailInputsContainer.querySelectorAll("[data-email-item]");
|
|
1889
|
+
emailItems.forEach((item) => {
|
|
1890
|
+
if (!item.classList.contains("sent")) {
|
|
1891
|
+
item.classList.add("sent");
|
|
1892
|
+
const rightColumn = item.querySelector(".gait-modal-email-item-right");
|
|
1893
|
+
if (rightColumn) {
|
|
1894
|
+
let amountContainer = rightColumn.querySelector(".gait-modal-email-item-amount-container");
|
|
1895
|
+
const amountSpan = rightColumn.querySelector(".gait-modal-email-item-amount");
|
|
1896
|
+
if (!amountContainer && amountSpan) {
|
|
1897
|
+
amountContainer = document.createElement("div");
|
|
1898
|
+
amountContainer.className = "gait-modal-email-item-amount-container";
|
|
1899
|
+
amountSpan.parentElement?.replaceChild(amountContainer, amountSpan);
|
|
1900
|
+
amountContainer.appendChild(amountSpan);
|
|
1901
|
+
}
|
|
1902
|
+
if (amountContainer && !amountContainer.querySelector(".gait-modal-email-item-sent-check")) {
|
|
1903
|
+
const checkmark = document.createElement("div");
|
|
1904
|
+
checkmark.className = "gait-modal-email-item-sent-check";
|
|
1905
|
+
checkmark.innerHTML = checkIcon;
|
|
1906
|
+
amountContainer.appendChild(checkmark);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
const actions = item.querySelector(".gait-modal-email-item-actions");
|
|
1910
|
+
if (actions) {
|
|
1911
|
+
actions.style.display = "none";
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
showFeedback(message, type) {
|
|
1917
|
+
if (this.splitFeedbackElement) {
|
|
1918
|
+
this.splitFeedbackElement.remove();
|
|
1919
|
+
this.splitFeedbackElement = null;
|
|
1920
|
+
}
|
|
1921
|
+
const feedback = document.createElement("div");
|
|
1922
|
+
feedback.className = `gait-modal-feedback gait-modal-feedback-${type}`;
|
|
1923
|
+
feedback.setAttribute("data-split-feedback", "");
|
|
1924
|
+
const icon = type === "success" ? checkIcon : type === "error" ? crossIcon : spinnerIcon;
|
|
1925
|
+
feedback.innerHTML = `
|
|
1926
|
+
<span class="gait-modal-feedback-icon">${icon}</span>
|
|
1927
|
+
<span class="gait-modal-feedback-message">${message}</span>
|
|
1928
|
+
`;
|
|
1929
|
+
const splitSection = this.modalElement.querySelector(".gait-modal-split-section");
|
|
1930
|
+
if (splitSection) {
|
|
1931
|
+
splitSection.insertBefore(feedback, splitSection.firstChild);
|
|
1932
|
+
this.splitFeedbackElement = feedback;
|
|
1933
|
+
if (type !== "loading") {
|
|
1934
|
+
setTimeout(() => {
|
|
1935
|
+
if (this.splitFeedbackElement === feedback) {
|
|
1936
|
+
feedback.style.opacity = "0";
|
|
1937
|
+
setTimeout(() => {
|
|
1938
|
+
if (feedback.parentElement) {
|
|
1939
|
+
feedback.remove();
|
|
1940
|
+
}
|
|
1941
|
+
if (this.splitFeedbackElement === feedback) {
|
|
1942
|
+
this.splitFeedbackElement = null;
|
|
1943
|
+
}
|
|
1944
|
+
}, 300);
|
|
1945
|
+
}
|
|
1946
|
+
}, 5e3);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
disableSplitOptions() {
|
|
1951
|
+
const splitOptions = this.modalElement.querySelectorAll("[data-split-method]");
|
|
1952
|
+
splitOptions.forEach((option) => {
|
|
1953
|
+
const optionElement = option;
|
|
1954
|
+
optionElement.style.pointerEvents = "none";
|
|
1955
|
+
optionElement.style.opacity = "0.5";
|
|
1956
|
+
optionElement.style.cursor = "not-allowed";
|
|
1957
|
+
optionElement.setAttribute("aria-disabled", "true");
|
|
1958
|
+
});
|
|
1959
|
+
const addEmailButton = this.modalElement.querySelector("[data-add-email]");
|
|
1960
|
+
if (addEmailButton) {
|
|
1961
|
+
addEmailButton.style.pointerEvents = "none";
|
|
1962
|
+
addEmailButton.style.opacity = "0.5";
|
|
1963
|
+
addEmailButton.style.cursor = "not-allowed";
|
|
1964
|
+
addEmailButton.setAttribute("aria-disabled", "true");
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
async function sendSplitPayment(request, timeout = 2e3) {
|
|
1969
|
+
console.log("[SplitPaymentAPI] Sending split payment request:", request);
|
|
1970
|
+
const apiUrl = "https://api.usegait.cloud/checkout";
|
|
1971
|
+
try {
|
|
1972
|
+
const response = await fetch(`${apiUrl}/v1/payments`, {
|
|
1973
|
+
method: "POST",
|
|
1974
|
+
headers: {
|
|
1975
|
+
"Content-Type": "application/json"
|
|
1976
|
+
},
|
|
1977
|
+
body: JSON.stringify(request),
|
|
1978
|
+
signal: AbortSignal.timeout(timeout)
|
|
1979
|
+
});
|
|
1980
|
+
const apiResponse = await response.json().catch(() => ({
|
|
1981
|
+
success: false,
|
|
1982
|
+
error: "Failed to parse API response"
|
|
1983
|
+
}));
|
|
1984
|
+
if (apiResponse.success) {
|
|
1985
|
+
const successResponse = {
|
|
1986
|
+
success: true,
|
|
1987
|
+
message: apiResponse.message,
|
|
1988
|
+
data: apiResponse.data
|
|
1989
|
+
};
|
|
1990
|
+
console.log("[SplitPaymentAPI] Split payment sent successfully:", successResponse);
|
|
1991
|
+
return successResponse;
|
|
1992
|
+
} else {
|
|
1993
|
+
const errorResponse = {
|
|
1994
|
+
success: false,
|
|
1995
|
+
message: apiResponse.message,
|
|
1996
|
+
error: apiResponse.error
|
|
1997
|
+
};
|
|
1998
|
+
console.error("[SplitPaymentAPI] Split payment failed:", errorResponse);
|
|
1999
|
+
return errorResponse;
|
|
2000
|
+
}
|
|
2001
|
+
} catch (error) {
|
|
2002
|
+
const errorResponse = {
|
|
2003
|
+
success: false,
|
|
2004
|
+
message: "Failed to send split payment",
|
|
2005
|
+
error: error instanceof Error ? error.message : "Network error or server unavailable"
|
|
2006
|
+
};
|
|
2007
|
+
console.error("[SplitPaymentAPI] Split payment failed:", errorResponse, error);
|
|
2008
|
+
return errorResponse;
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
let random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes));
|
|
2012
|
+
let customRandom = (alphabet, defaultSize, getRandom) => {
|
|
2013
|
+
let mask = (2 << Math.log2(alphabet.length - 1)) - 1;
|
|
2014
|
+
let step = -~(1.6 * mask * defaultSize / alphabet.length);
|
|
2015
|
+
return (size = defaultSize) => {
|
|
2016
|
+
let id = "";
|
|
2017
|
+
while (true) {
|
|
2018
|
+
let bytes = getRandom(step);
|
|
2019
|
+
let j = step | 0;
|
|
2020
|
+
while (j--) {
|
|
2021
|
+
id += alphabet[bytes[j] & mask] || "";
|
|
2022
|
+
if (id.length >= size) return id;
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
};
|
|
2027
|
+
let customAlphabet = (alphabet, size = 21) => customRandom(alphabet, size | 0, random);
|
|
2028
|
+
const config = {
|
|
2029
|
+
apiGateway: {
|
|
2030
|
+
MERCHANT_SERVICE_API: "https://api.usegait.cloud/merchant",
|
|
2031
|
+
CHECKOUT_SERVICE_API: "https://api.usegait.cloud/checkout"
|
|
2032
|
+
},
|
|
2033
|
+
paylink: {
|
|
2034
|
+
DOMAIN: "https://checkout.usegait.cloud"
|
|
2035
|
+
}
|
|
2036
|
+
};
|
|
2037
|
+
const domain$1 = config.apiGateway.MERCHANT_SERVICE_API;
|
|
2038
|
+
class MerchantService {
|
|
2039
|
+
static async getMerchant(checkoutId) {
|
|
2040
|
+
try {
|
|
2041
|
+
const resp = await fetch(`${domain$1}/v1/merchant/identity/${checkoutId}`, {
|
|
2042
|
+
"method": "GET",
|
|
2043
|
+
headers: {
|
|
2044
|
+
"Content-Type": "application/json"
|
|
2045
|
+
}
|
|
2046
|
+
});
|
|
2047
|
+
return await resp.json();
|
|
2048
|
+
} catch (error) {
|
|
2049
|
+
return error;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
const domain = config.apiGateway.CHECKOUT_SERVICE_API;
|
|
2054
|
+
class PaymentsService {
|
|
2055
|
+
static async getPayment(paymentId) {
|
|
2056
|
+
try {
|
|
2057
|
+
const resp = await fetch(`${domain}/v1/payments/${paymentId}`, {
|
|
2058
|
+
"method": "GET",
|
|
2059
|
+
headers: {
|
|
2060
|
+
"Content-Type": "application/json"
|
|
2061
|
+
}
|
|
2062
|
+
});
|
|
2063
|
+
return await resp.json();
|
|
2064
|
+
} catch (error) {
|
|
2065
|
+
return error;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
static async updatePaymentStatus(merchantId, splitId, status) {
|
|
2069
|
+
try {
|
|
2070
|
+
const resp = await fetch(`${domain}/v1/payments/merchants/${merchantId}/${splitId}/status`, {
|
|
2071
|
+
"method": "PUT",
|
|
2072
|
+
headers: {
|
|
2073
|
+
"Content-Type": "application/json"
|
|
2074
|
+
},
|
|
2075
|
+
body: JSON.stringify({
|
|
2076
|
+
status
|
|
2077
|
+
})
|
|
2078
|
+
});
|
|
2079
|
+
return await resp.json();
|
|
2080
|
+
} catch (error) {
|
|
2081
|
+
return error;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
static async createWebhook(merchantId, splitId, webhook) {
|
|
2085
|
+
try {
|
|
2086
|
+
const resp = await fetch(`${domain}/v1/webhooks/${merchantId}/${splitId}`, {
|
|
2087
|
+
"method": "POST",
|
|
2088
|
+
headers: {
|
|
2089
|
+
"Content-Type": "application/json"
|
|
2090
|
+
},
|
|
2091
|
+
body: JSON.stringify(webhook)
|
|
2092
|
+
});
|
|
2093
|
+
return await resp.json();
|
|
2094
|
+
} catch (error) {
|
|
2095
|
+
return error;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
static async updatePayment(merchantId, paymentId, splitId) {
|
|
2099
|
+
try {
|
|
2100
|
+
const resp = await fetch(`${domain}/v1/payments/merchants/${merchantId}/${splitId}`, {
|
|
2101
|
+
"method": "PUT",
|
|
2102
|
+
headers: {
|
|
2103
|
+
"Content-Type": "application/json"
|
|
2104
|
+
},
|
|
2105
|
+
body: JSON.stringify({
|
|
2106
|
+
paymentId
|
|
2107
|
+
})
|
|
2108
|
+
});
|
|
2109
|
+
return await resp.json();
|
|
2110
|
+
} catch (error) {
|
|
2111
|
+
return error;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
function devLog(...args) {
|
|
2116
|
+
{
|
|
2117
|
+
console.log(...args);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
const _GAIT_BUTTON_VERSION_ENV = typeof process !== "undefined" && process.env ? "0.1.0" : void 0;
|
|
2121
|
+
const GAIT_BUTTON_VERSION = _GAIT_BUTTON_VERSION_ENV || "0.1.4";
|
|
2122
|
+
class GaitButton extends HTMLElement {
|
|
2123
|
+
buttonElement = null;
|
|
2124
|
+
hasRendered = false;
|
|
2125
|
+
mutationObserver = null;
|
|
2126
|
+
modalElement = null;
|
|
2127
|
+
modalId;
|
|
2128
|
+
splitPaymentManager = null;
|
|
2129
|
+
attributeGetters;
|
|
2130
|
+
storedSplitPaymentRequest = null;
|
|
2131
|
+
merchantId = null;
|
|
2132
|
+
paymentProvider = "stripe";
|
|
2133
|
+
// Expose version as static and instance properties for easy access
|
|
2134
|
+
static version = GAIT_BUTTON_VERSION;
|
|
2135
|
+
version = GAIT_BUTTON_VERSION;
|
|
2136
|
+
// Observed attributes for reactivity (label, theme, brand-color removed - fixed to Gait / #4147BF)
|
|
2137
|
+
static get observedAttributes() {
|
|
2138
|
+
return ["data-id", "disabled", "size", "style", "items", "price-breakdown", "total-cost", "webhook", "merchant-store-url"];
|
|
2139
|
+
}
|
|
2140
|
+
constructor() {
|
|
2141
|
+
super();
|
|
2142
|
+
this.attachShadow({ mode: "open" });
|
|
2143
|
+
this.modalId = `gait-modal-${Math.random().toString(36).substr(2, 9)}`;
|
|
2144
|
+
this.attributeGetters = createAttributeGetters(this);
|
|
2145
|
+
}
|
|
2146
|
+
// Lifecycle: Called when element is connected to DOM
|
|
2147
|
+
connectedCallback() {
|
|
2148
|
+
if (!this.hasRendered && this.shadowRoot) {
|
|
2149
|
+
this.render();
|
|
2150
|
+
}
|
|
2151
|
+
this.setupEventListeners();
|
|
2152
|
+
this.setupMutationObserver();
|
|
2153
|
+
this.setupApiListeners();
|
|
2154
|
+
setTimeout(() => this.processData(), 0);
|
|
2155
|
+
}
|
|
2156
|
+
// Lifecycle: Called when element is disconnected from DOM
|
|
2157
|
+
disconnectedCallback() {
|
|
2158
|
+
if (this.buttonElement) {
|
|
2159
|
+
this.buttonElement.removeEventListener("click", this.handleClick);
|
|
2160
|
+
}
|
|
2161
|
+
if (this.mutationObserver) {
|
|
2162
|
+
this.mutationObserver.disconnect();
|
|
2163
|
+
this.mutationObserver = null;
|
|
2164
|
+
}
|
|
2165
|
+
this.closeModal();
|
|
2166
|
+
if (this.splitPaymentManager) {
|
|
2167
|
+
this.splitPaymentManager = null;
|
|
2168
|
+
}
|
|
2169
|
+
this.removeEventListener("gait-split-sent", this.handleSplitSent);
|
|
2170
|
+
}
|
|
2171
|
+
// Lifecycle: Called when observed attributes change
|
|
2172
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
2173
|
+
if (oldValue === newValue) {
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
if (!this.isConnected || !this.shadowRoot) {
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
if (name === "style" && this.buttonElement && this.hasRendered) {
|
|
2180
|
+
const sanitizedStyle = (newValue || "").replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "");
|
|
2181
|
+
this.buttonElement.setAttribute("style", sanitizedStyle);
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
if (this.hasRendered) {
|
|
2185
|
+
this.render();
|
|
2186
|
+
this.setupEventListeners();
|
|
2187
|
+
}
|
|
2188
|
+
if (name === "data-id" || name.startsWith("data-")) {
|
|
2189
|
+
if (name === "data-id") {
|
|
2190
|
+
this.merchantId = null;
|
|
2191
|
+
}
|
|
2192
|
+
setTimeout(() => this.processData(), 0);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
// Getters for attributes with defaults (using attribute getters)
|
|
2196
|
+
get dataId() {
|
|
2197
|
+
return this.attributeGetters.dataId;
|
|
2198
|
+
}
|
|
2199
|
+
get disabled() {
|
|
2200
|
+
return this.attributeGetters.disabled;
|
|
2201
|
+
}
|
|
2202
|
+
get size() {
|
|
2203
|
+
return this.attributeGetters.size;
|
|
2204
|
+
}
|
|
2205
|
+
get items() {
|
|
2206
|
+
return this.attributeGetters.items;
|
|
2207
|
+
}
|
|
2208
|
+
get priceBreakdown() {
|
|
2209
|
+
return this.attributeGetters.priceBreakdown;
|
|
2210
|
+
}
|
|
2211
|
+
get totalCost() {
|
|
2212
|
+
return this.attributeGetters.totalCost;
|
|
2213
|
+
}
|
|
2214
|
+
get customer() {
|
|
2215
|
+
return this.attributeGetters.customer;
|
|
2216
|
+
}
|
|
2217
|
+
get webhook() {
|
|
2218
|
+
return this.attributeGetters.webhook;
|
|
2219
|
+
}
|
|
2220
|
+
get merchantStoreUrl() {
|
|
2221
|
+
return this.attributeGetters.merchantStoreUrl;
|
|
2222
|
+
}
|
|
2223
|
+
// Get all data-* attributes
|
|
2224
|
+
getDataAttributes() {
|
|
2225
|
+
return getDataAttributes(this);
|
|
2226
|
+
}
|
|
2227
|
+
// Fetch merchantId from API using checkoutId (data-id)
|
|
2228
|
+
async fetchMerchantId() {
|
|
2229
|
+
const checkoutId = this.dataId;
|
|
2230
|
+
if (!checkoutId) {
|
|
2231
|
+
console.warn("[GaitButton] No checkoutId (data-id) provided, cannot fetch merchantId");
|
|
2232
|
+
this.showMerchantIdError();
|
|
2233
|
+
return false;
|
|
2234
|
+
}
|
|
2235
|
+
try {
|
|
2236
|
+
const response = await MerchantService.getMerchant(checkoutId);
|
|
2237
|
+
if (response.success && response.data?.merchantId) {
|
|
2238
|
+
this.merchantId = response.data.merchantId;
|
|
2239
|
+
this.paymentProvider = response.data.paymentProvider;
|
|
2240
|
+
return true;
|
|
2241
|
+
} else {
|
|
2242
|
+
console.error("[GaitButton] Failed to fetch merchantId:", response);
|
|
2243
|
+
this.showMerchantIdError();
|
|
2244
|
+
return false;
|
|
2245
|
+
}
|
|
2246
|
+
} catch (error) {
|
|
2247
|
+
console.error("[GaitButton] Error fetching merchantId:", error);
|
|
2248
|
+
this.showMerchantIdError();
|
|
2249
|
+
return false;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
// Show error message when merchantId cannot be identified
|
|
2253
|
+
showMerchantIdError() {
|
|
2254
|
+
const errorMessage = "Unable to identify your account. Please go to your dashboard and copy your checkout identity.";
|
|
2255
|
+
alert(errorMessage);
|
|
2256
|
+
this.dispatchEvent(new CustomEvent("gait-merchant-id-error", {
|
|
2257
|
+
detail: {
|
|
2258
|
+
message: errorMessage,
|
|
2259
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2260
|
+
},
|
|
2261
|
+
bubbles: true,
|
|
2262
|
+
composed: true
|
|
2263
|
+
}));
|
|
2264
|
+
}
|
|
2265
|
+
// Process data from attributes and do something (like console.log)
|
|
2266
|
+
async processData() {
|
|
2267
|
+
const dataId = this.dataId;
|
|
2268
|
+
const allDataAttrs = this.getDataAttributes();
|
|
2269
|
+
if (dataId && !this.merchantId) {
|
|
2270
|
+
await this.fetchMerchantId();
|
|
2271
|
+
}
|
|
2272
|
+
if (dataId || Object.keys(allDataAttrs).length > 0) {
|
|
2273
|
+
const data = {
|
|
2274
|
+
id: dataId,
|
|
2275
|
+
...allDataAttrs,
|
|
2276
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2277
|
+
};
|
|
2278
|
+
devLog("[GaitButton] Processing data v0.0.16:", data);
|
|
2279
|
+
this.dispatchEvent(new CustomEvent("gait-data-processed", {
|
|
2280
|
+
detail: data,
|
|
2281
|
+
bubbles: true,
|
|
2282
|
+
composed: true
|
|
2283
|
+
}));
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
// Handle click events
|
|
2287
|
+
handleClick = async (event) => {
|
|
2288
|
+
if (this.disabled) {
|
|
2289
|
+
event.preventDefault();
|
|
2290
|
+
event.stopPropagation();
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
const clickData = {
|
|
2294
|
+
label: "Gait",
|
|
2295
|
+
dataId: this.dataId,
|
|
2296
|
+
dataAttributes: this.getDataAttributes(),
|
|
2297
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2298
|
+
};
|
|
2299
|
+
this.dispatchEvent(new CustomEvent("gait-click", {
|
|
2300
|
+
detail: clickData,
|
|
2301
|
+
bubbles: true,
|
|
2302
|
+
composed: true
|
|
2303
|
+
}));
|
|
2304
|
+
this.dispatchEvent(new CustomEvent("click", {
|
|
2305
|
+
detail: clickData,
|
|
2306
|
+
bubbles: true,
|
|
2307
|
+
composed: true
|
|
2308
|
+
}));
|
|
2309
|
+
if (!this.merchantId) {
|
|
2310
|
+
const fetched = await this.fetchMerchantId();
|
|
2311
|
+
if (!fetched || !this.merchantId) {
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
this.openModal();
|
|
2316
|
+
};
|
|
2317
|
+
// Setup event listeners
|
|
2318
|
+
setupEventListeners() {
|
|
2319
|
+
if (!this.shadowRoot) {
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
if (this.buttonElement) {
|
|
2323
|
+
this.buttonElement.removeEventListener("click", this.handleClick);
|
|
2324
|
+
}
|
|
2325
|
+
this.buttonElement = this.shadowRoot.querySelector("button");
|
|
2326
|
+
if (this.buttonElement) {
|
|
2327
|
+
this.buttonElement.addEventListener("click", this.handleClick);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
// Setup MutationObserver to watch for data-* attribute changes
|
|
2331
|
+
setupMutationObserver() {
|
|
2332
|
+
if (this.mutationObserver) {
|
|
2333
|
+
this.mutationObserver.disconnect();
|
|
2334
|
+
}
|
|
2335
|
+
this.mutationObserver = new MutationObserver((mutations) => {
|
|
2336
|
+
let shouldProcessData = false;
|
|
2337
|
+
for (const mutation of mutations) {
|
|
2338
|
+
if (mutation.type === "attributes") {
|
|
2339
|
+
const attrName = mutation.attributeName;
|
|
2340
|
+
if (attrName && attrName.startsWith("data-") && !GaitButton.observedAttributes.includes(attrName)) {
|
|
2341
|
+
shouldProcessData = true;
|
|
2342
|
+
break;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
if (shouldProcessData && this.isConnected) {
|
|
2347
|
+
this.processData();
|
|
2348
|
+
}
|
|
2349
|
+
});
|
|
2350
|
+
this.mutationObserver.observe(this, {
|
|
2351
|
+
attributes: true,
|
|
2352
|
+
attributeFilter: void 0
|
|
2353
|
+
// Observe all attributes
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
// Setup API event listeners
|
|
2357
|
+
setupApiListeners() {
|
|
2358
|
+
this.addEventListener("gait-split-sent", this.handleSplitSent);
|
|
2359
|
+
}
|
|
2360
|
+
// Handle confirm payment (Pay remaining button)
|
|
2361
|
+
handleConfirmPayment = async () => {
|
|
2362
|
+
console.log("[GaitButton] Pay remaining button clicked");
|
|
2363
|
+
if (!this.storedSplitPaymentRequest) {
|
|
2364
|
+
console.error("[GaitButton] Split payment must be sent first before paying remaining amount");
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
if (!this.merchantId) {
|
|
2368
|
+
const fetched = await this.fetchMerchantId();
|
|
2369
|
+
if (!fetched || !this.merchantId) {
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
const initiatorObject = {
|
|
2374
|
+
merchantId: this.merchantId,
|
|
2375
|
+
splitId: this.storedSplitPaymentRequest.splitId,
|
|
2376
|
+
paymentId: this.storedSplitPaymentRequest.paymentSplits.find((payment) => payment.initiator)?.paymentId
|
|
2377
|
+
};
|
|
2378
|
+
const paylinkDomain = config.paylink.DOMAIN;
|
|
2379
|
+
const paylink = `${paylinkDomain}/payment/${initiatorObject.paymentId}`;
|
|
2380
|
+
window.open(paylink, "_blank");
|
|
2381
|
+
this.dispatchEvent(new CustomEvent("gait-confirm", {
|
|
2382
|
+
detail: initiatorObject,
|
|
2383
|
+
bubbles: true,
|
|
2384
|
+
composed: true
|
|
2385
|
+
}));
|
|
2386
|
+
this.closeModal();
|
|
2387
|
+
};
|
|
2388
|
+
// Handle split payment sent event and make API call
|
|
2389
|
+
handleSplitSent = async (event) => {
|
|
2390
|
+
const customEvent = event;
|
|
2391
|
+
const splitData = customEvent.detail;
|
|
2392
|
+
if (!this.merchantId) {
|
|
2393
|
+
const fetched = await this.fetchMerchantId();
|
|
2394
|
+
if (!fetched || !this.merchantId) {
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
const merchantId = this.merchantId;
|
|
2399
|
+
const customer = this.customer;
|
|
2400
|
+
if (!customer || !customer.email) {
|
|
2401
|
+
console.error("[GaitButton] Customer email is required for split payment");
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
const webhook = this.webhook;
|
|
2405
|
+
if (!webhook || !webhook.url || webhook.body == null) {
|
|
2406
|
+
console.error("[GaitButton] Webhook with url and body is required");
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
2410
|
+
const generateId = customAlphabet(alphabet, 6);
|
|
2411
|
+
const splitId = generateId();
|
|
2412
|
+
const paymentSplits = [];
|
|
2413
|
+
paymentSplits.push({
|
|
2414
|
+
paymentId: generateId(),
|
|
2415
|
+
email: customer.email,
|
|
2416
|
+
amount: splitData.yourAmount,
|
|
2417
|
+
isPaid: false,
|
|
2418
|
+
datePaid: null,
|
|
2419
|
+
initiator: true
|
|
2420
|
+
});
|
|
2421
|
+
splitData.emailData.forEach((emailData) => {
|
|
2422
|
+
paymentSplits.push({
|
|
2423
|
+
paymentId: generateId(),
|
|
2424
|
+
email: emailData.email,
|
|
2425
|
+
amount: emailData.amount || 0,
|
|
2426
|
+
isPaid: false,
|
|
2427
|
+
datePaid: null,
|
|
2428
|
+
initiator: false
|
|
2429
|
+
});
|
|
2430
|
+
});
|
|
2431
|
+
const id = {
|
|
2432
|
+
merchantId,
|
|
2433
|
+
splitId,
|
|
2434
|
+
paymentSplits,
|
|
2435
|
+
splitMethod: splitData.splitMethod,
|
|
2436
|
+
timestamp: splitData.timestamp,
|
|
2437
|
+
totalCost: splitData.totalCost,
|
|
2438
|
+
items: this.items,
|
|
2439
|
+
priceBreakdown: this.priceBreakdown,
|
|
2440
|
+
brandColor: GAIT_BRAND_COLOR,
|
|
2441
|
+
merchantStoreUrl: this.merchantStoreUrl,
|
|
2442
|
+
subTotal: this.priceBreakdown.find((item) => item.name === "Subtotal")?.price || 0,
|
|
2443
|
+
paymentProvider: this.paymentProvider
|
|
2444
|
+
};
|
|
2445
|
+
console.log("[GaitButton] Split payment request body:", JSON.stringify(id, null, 2));
|
|
2446
|
+
try {
|
|
2447
|
+
const response = await sendSplitPayment(id, 2e3);
|
|
2448
|
+
if (response.success) {
|
|
2449
|
+
const finalSplitId = response.data?.splitId || splitId;
|
|
2450
|
+
this.storedSplitPaymentRequest = {
|
|
2451
|
+
...id,
|
|
2452
|
+
splitId: finalSplitId
|
|
2453
|
+
};
|
|
2454
|
+
try {
|
|
2455
|
+
const webhookResponse = await PaymentsService.createWebhook(
|
|
2456
|
+
merchantId,
|
|
2457
|
+
finalSplitId,
|
|
2458
|
+
{
|
|
2459
|
+
url: webhook.url,
|
|
2460
|
+
body: webhook.body
|
|
2461
|
+
}
|
|
2462
|
+
);
|
|
2463
|
+
console.log("[GaitButton] Webhook created:", webhookResponse);
|
|
2464
|
+
} catch (webhookError) {
|
|
2465
|
+
console.error("[GaitButton] Failed to create webhook:", webhookError);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
if (this.modalElement) {
|
|
2469
|
+
const confirmButton = this.modalElement.querySelector("[data-gait-modal-confirm]");
|
|
2470
|
+
if (confirmButton) {
|
|
2471
|
+
confirmButton.disabled = false;
|
|
2472
|
+
confirmButton.style.opacity = "1";
|
|
2473
|
+
confirmButton.style.cursor = "pointer";
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
this.dispatchEvent(new CustomEvent("gait-split-feedback", {
|
|
2477
|
+
detail: {
|
|
2478
|
+
success: response.success,
|
|
2479
|
+
message: response.message,
|
|
2480
|
+
data: response.success ? this.storedSplitPaymentRequest : response.data,
|
|
2481
|
+
error: response.error,
|
|
2482
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2483
|
+
},
|
|
2484
|
+
bubbles: true,
|
|
2485
|
+
composed: true
|
|
2486
|
+
}));
|
|
2487
|
+
console.log("[GaitButton] Split payment API response:", response);
|
|
2488
|
+
} catch (error) {
|
|
2489
|
+
console.error("[GaitButton] Split payment API error:", error);
|
|
2490
|
+
this.dispatchEvent(new CustomEvent("gait-split-feedback", {
|
|
2491
|
+
detail: {
|
|
2492
|
+
success: false,
|
|
2493
|
+
message: "Failed to send split payment",
|
|
2494
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
2495
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2496
|
+
},
|
|
2497
|
+
bubbles: true,
|
|
2498
|
+
composed: true
|
|
2499
|
+
}));
|
|
2500
|
+
}
|
|
2501
|
+
};
|
|
2502
|
+
// Open modal
|
|
2503
|
+
openModal() {
|
|
2504
|
+
injectModalStyles();
|
|
2505
|
+
this.closeModal();
|
|
2506
|
+
this.modalElement = document.createElement("div");
|
|
2507
|
+
this.modalElement.className = "gait-modal-backdrop";
|
|
2508
|
+
this.modalElement.id = this.modalId;
|
|
2509
|
+
if (this.modalElement) {
|
|
2510
|
+
this.modalElement.style.setProperty("--gait-modal-brand-color", GAIT_BRAND_COLOR);
|
|
2511
|
+
this.modalElement.style.setProperty("--gait-modal-button-bg", GAIT_BRAND_COLOR);
|
|
2512
|
+
this.modalElement.style.setProperty("--gait-modal-button-bg-hover", darkenColor(GAIT_BRAND_COLOR, 0.15));
|
|
2513
|
+
this.modalElement.style.setProperty("--gait-modal-button-color", isColorDark(GAIT_BRAND_COLOR) ? "#ffffff" : "#000000");
|
|
2514
|
+
}
|
|
2515
|
+
this.modalElement.innerHTML = generateModalTemplate(
|
|
2516
|
+
this.items,
|
|
2517
|
+
this.priceBreakdown,
|
|
2518
|
+
this.totalCost
|
|
2519
|
+
);
|
|
2520
|
+
const closeButton = this.modalElement.querySelector("[data-gait-modal-close]");
|
|
2521
|
+
if (closeButton) {
|
|
2522
|
+
closeButton.addEventListener("click", this.closeModal);
|
|
2523
|
+
}
|
|
2524
|
+
const confirmButton = this.modalElement.querySelector("[data-gait-modal-confirm]");
|
|
2525
|
+
if (confirmButton) {
|
|
2526
|
+
confirmButton.disabled = true;
|
|
2527
|
+
confirmButton.style.opacity = "0.5";
|
|
2528
|
+
confirmButton.style.cursor = "not-allowed";
|
|
2529
|
+
confirmButton.addEventListener("click", this.handleConfirmPayment);
|
|
2530
|
+
}
|
|
2531
|
+
this.splitPaymentManager = new SplitPaymentManager(
|
|
2532
|
+
this.modalElement,
|
|
2533
|
+
this.totalCost,
|
|
2534
|
+
(event) => this.dispatchEvent(event)
|
|
2535
|
+
);
|
|
2536
|
+
this.modalElement._splitPaymentManager = this.splitPaymentManager;
|
|
2537
|
+
this.modalElement.addEventListener("click", (e) => {
|
|
2538
|
+
if (e.target === this.modalElement) {
|
|
2539
|
+
this.closeModal();
|
|
2540
|
+
}
|
|
2541
|
+
});
|
|
2542
|
+
const handleEscape = (e) => {
|
|
2543
|
+
if (e.key === "Escape" && this.modalElement) {
|
|
2544
|
+
this.closeModal();
|
|
2545
|
+
document.removeEventListener("keydown", handleEscape);
|
|
2546
|
+
}
|
|
2547
|
+
};
|
|
2548
|
+
document.addEventListener("keydown", handleEscape);
|
|
2549
|
+
document.body.appendChild(this.modalElement);
|
|
2550
|
+
requestAnimationFrame(() => {
|
|
2551
|
+
if (this.modalElement) {
|
|
2552
|
+
this.modalElement.classList.add("gait-modal-open");
|
|
2553
|
+
}
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
// Close modal
|
|
2557
|
+
closeModal = () => {
|
|
2558
|
+
if (this.modalElement) {
|
|
2559
|
+
if (this.splitPaymentManager) {
|
|
2560
|
+
this.splitPaymentManager.cleanup();
|
|
2561
|
+
}
|
|
2562
|
+
this.modalElement.classList.remove("gait-modal-open");
|
|
2563
|
+
setTimeout(() => {
|
|
2564
|
+
if (this.modalElement && this.modalElement.parentNode) {
|
|
2565
|
+
this.modalElement.parentNode.removeChild(this.modalElement);
|
|
2566
|
+
}
|
|
2567
|
+
this.modalElement = null;
|
|
2568
|
+
this.splitPaymentManager = null;
|
|
2569
|
+
}, 250);
|
|
2570
|
+
}
|
|
2571
|
+
};
|
|
2572
|
+
// Render the component
|
|
2573
|
+
render() {
|
|
2574
|
+
const shadow = this.shadowRoot;
|
|
2575
|
+
if (!shadow) {
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
const wasFirstRender = !this.hasRendered;
|
|
2579
|
+
const styles = generateButtonStyles(
|
|
2580
|
+
this.size,
|
|
2581
|
+
darkenColor,
|
|
2582
|
+
isColorDark
|
|
2583
|
+
);
|
|
2584
|
+
const hostStyle = this.getAttribute("style") || "";
|
|
2585
|
+
const sanitizedStyle = hostStyle.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "");
|
|
2586
|
+
shadow.innerHTML = `
|
|
2587
|
+
${styles}
|
|
2588
|
+
${generateButtonTemplate(this.disabled, sanitizedStyle)}
|
|
2589
|
+
`;
|
|
2590
|
+
this.hasRendered = true;
|
|
2591
|
+
this.setupEventListeners();
|
|
2592
|
+
if (wasFirstRender) {
|
|
2593
|
+
const allAttributes = getAllAttributes(this);
|
|
2594
|
+
console.log("[GaitButton] rendered", allAttributes);
|
|
2595
|
+
this.dispatchEvent(new CustomEvent("gait-loaded", {
|
|
2596
|
+
detail: {
|
|
2597
|
+
attributes: allAttributes,
|
|
2598
|
+
dataId: this.dataId,
|
|
2599
|
+
dataAttributes: this.getDataAttributes(),
|
|
2600
|
+
label: "Gait",
|
|
2601
|
+
size: this.size,
|
|
2602
|
+
disabled: this.disabled,
|
|
2603
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2604
|
+
},
|
|
2605
|
+
bubbles: true,
|
|
2606
|
+
composed: true
|
|
2607
|
+
}));
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
if (!customElements.get("gait-button")) {
|
|
2612
|
+
customElements.define("gait-button", GaitButton);
|
|
2613
|
+
}
|
|
2614
|
+
if (!customElements.get("gait-button")) {
|
|
2615
|
+
customElements.define("gait-button", GaitButton);
|
|
2616
|
+
}
|
|
2617
|
+
if (typeof window !== "undefined") {
|
|
2618
|
+
window.GaitButton = GaitButton;
|
|
2619
|
+
window.GaitButtonVersion = GAIT_BUTTON_VERSION;
|
|
2620
|
+
console.log(`[GaitButton] Version: ${GAIT_BUTTON_VERSION}`);
|
|
2621
|
+
}
|
|
2622
|
+
exports.GAIT_BUTTON_VERSION = GAIT_BUTTON_VERSION;
|
|
2623
|
+
exports.GaitButton = GaitButton;
|
|
2624
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2625
|
+
})(this.GaitButton = this.GaitButton || {});
|