@mundogamernetwork/shared-ui 1.1.4 → 1.1.9
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/components/checkout/MgCartItemList.vue +94 -43
- package/components/checkout/MgCartSummary.vue +100 -137
- package/components/checkout/MgCheckoutSidebar.vue +0 -2
- package/components/checkout/MgCouponInput.vue +211 -0
- package/components/checkout/MgGuestEmailForm.vue +0 -2
- package/components/checkout/MgPaymentMethodSelector.vue +17 -16
- package/components/checkout/MgPixQRCode.vue +0 -2
- package/components/ui/MgConfirmationPage.vue +415 -0
- package/composables/useConfirmation.ts +34 -5
- package/composables/useMgCheckout.ts +398 -166
- package/composables/usePaymentMethods.ts +3 -2
- package/package.json +2 -2
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* MgCouponInput — standalone coupon input component.
|
|
4
|
+
*
|
|
5
|
+
* Can be used in two modes:
|
|
6
|
+
* 1. Wired mode: inject useMgCheckout context (call provideMgCheckout in parent)
|
|
7
|
+
* 2. Prop/event mode: pass couponInfo, couponLoading, couponError as props
|
|
8
|
+
* and listen to apply-coupon / remove-coupon events.
|
|
9
|
+
*/
|
|
10
|
+
import { ref, computed } from "vue";
|
|
11
|
+
import { injectMgCheckout } from "../../composables/useMgCheckout";
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
/** Applied coupon object (prop mode). If null, shows the input. */
|
|
15
|
+
couponInfo?: { code: string; discount?: number; name?: string } | null;
|
|
16
|
+
loading?: boolean;
|
|
17
|
+
error?: string | null;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
/** If true, the component manages its own state via injectMgCheckout */
|
|
20
|
+
autoWire?: boolean;
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
const emit = defineEmits<{
|
|
24
|
+
"apply-coupon": [code: string];
|
|
25
|
+
"remove-coupon": [];
|
|
26
|
+
}>();
|
|
27
|
+
|
|
28
|
+
// Try to auto-wire to parent checkout context
|
|
29
|
+
const checkout = injectMgCheckout();
|
|
30
|
+
|
|
31
|
+
const couponInput = ref("");
|
|
32
|
+
const _localLoading = ref(false);
|
|
33
|
+
const _localError = ref<string | null>(null);
|
|
34
|
+
|
|
35
|
+
const activeCoupon = computed(() =>
|
|
36
|
+
props.autoWire ? checkout?.couponInfo.value : props.couponInfo
|
|
37
|
+
);
|
|
38
|
+
const isLoading = computed(() =>
|
|
39
|
+
props.autoWire ? checkout?.couponLoading.value : (props.loading ?? _localLoading.value)
|
|
40
|
+
);
|
|
41
|
+
const couponError = computed(() =>
|
|
42
|
+
props.autoWire ? checkout?.couponError.value : (props.error ?? _localError.value)
|
|
43
|
+
);
|
|
44
|
+
const isDisabled = computed(() => props.disabled || isLoading.value);
|
|
45
|
+
|
|
46
|
+
async function handleApply() {
|
|
47
|
+
const code = couponInput.value.trim().toUpperCase();
|
|
48
|
+
if (!code || isDisabled.value) return;
|
|
49
|
+
|
|
50
|
+
if (props.autoWire && checkout) {
|
|
51
|
+
checkout.couponCode.value = code;
|
|
52
|
+
await checkout.applyCoupon(code);
|
|
53
|
+
} else {
|
|
54
|
+
emit("apply-coupon", code);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function handleRemove() {
|
|
59
|
+
if (props.autoWire && checkout) {
|
|
60
|
+
await checkout.removeCoupon();
|
|
61
|
+
couponInput.value = "";
|
|
62
|
+
} else {
|
|
63
|
+
couponInput.value = "";
|
|
64
|
+
emit("remove-coupon");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Clear input after coupon applied successfully
|
|
69
|
+
watch(
|
|
70
|
+
() => activeCoupon.value,
|
|
71
|
+
(v) => { if (v) couponInput.value = ""; }
|
|
72
|
+
);
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<div class="mg-coupon">
|
|
77
|
+
<!-- Applied state -->
|
|
78
|
+
<div v-if="activeCoupon" class="mg-coupon__applied">
|
|
79
|
+
<span class="mg-coupon__badge">
|
|
80
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
81
|
+
<polyline points="20 6 9 17 4 12" />
|
|
82
|
+
</svg>
|
|
83
|
+
{{ activeCoupon.code }}
|
|
84
|
+
<span v-if="activeCoupon.discount">(-{{ activeCoupon.discount }}%)</span>
|
|
85
|
+
</span>
|
|
86
|
+
<button class="mg-coupon__remove" :disabled="isLoading" @click="handleRemove" :title="$t?.('checkout.coupon.remove') ?? 'Remove coupon'">
|
|
87
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
88
|
+
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
|
|
89
|
+
</svg>
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<!-- Input state -->
|
|
94
|
+
<div v-else class="mg-coupon__form">
|
|
95
|
+
<input
|
|
96
|
+
v-model="couponInput"
|
|
97
|
+
type="text"
|
|
98
|
+
class="mg-coupon__input"
|
|
99
|
+
:class="{ 'mg-coupon__input--error': couponError }"
|
|
100
|
+
:placeholder="$t?.('checkout.coupon.placeholder') ?? 'Discount code'"
|
|
101
|
+
:disabled="isDisabled"
|
|
102
|
+
style="text-transform: uppercase"
|
|
103
|
+
@keyup.enter="handleApply"
|
|
104
|
+
/>
|
|
105
|
+
<button
|
|
106
|
+
class="mg-coupon__btn"
|
|
107
|
+
:disabled="!couponInput.trim() || isDisabled"
|
|
108
|
+
@click="handleApply"
|
|
109
|
+
>
|
|
110
|
+
<span v-if="isLoading" class="mg-coupon__spinner" />
|
|
111
|
+
<template v-else>{{ $t?.('checkout.coupon.apply') ?? 'Apply' }}</template>
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<p v-if="couponError" class="mg-coupon__error">{{ couponError }}</p>
|
|
116
|
+
</div>
|
|
117
|
+
</template>
|
|
118
|
+
|
|
119
|
+
<style lang="scss" scoped>
|
|
120
|
+
.mg-coupon {
|
|
121
|
+
width: 100%;
|
|
122
|
+
|
|
123
|
+
&__form {
|
|
124
|
+
display: flex;
|
|
125
|
+
gap: 8px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
&__input {
|
|
129
|
+
flex: 1;
|
|
130
|
+
padding: 10px 12px;
|
|
131
|
+
border: 1px solid var(--inactive, #808080);
|
|
132
|
+
background: transparent;
|
|
133
|
+
color: var(--active, #fff);
|
|
134
|
+
font-size: 14px;
|
|
135
|
+
outline: none;
|
|
136
|
+
transition: border-color 0.15s;
|
|
137
|
+
|
|
138
|
+
&::placeholder { color: var(--inactive, #9ca3af); text-transform: none; }
|
|
139
|
+
&:focus { border-color: var(--highlight-color, #4f46e5); }
|
|
140
|
+
&:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
141
|
+
&--error { border-color: #ee3831; }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
&__btn {
|
|
145
|
+
padding: 10px 18px;
|
|
146
|
+
background: var(--highlight-color, #4f46e5);
|
|
147
|
+
color: #fff;
|
|
148
|
+
border: none;
|
|
149
|
+
font-size: 14px;
|
|
150
|
+
font-weight: 600;
|
|
151
|
+
cursor: pointer;
|
|
152
|
+
white-space: nowrap;
|
|
153
|
+
transition: opacity 0.15s;
|
|
154
|
+
|
|
155
|
+
&:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
156
|
+
&:hover:not(:disabled) { opacity: 0.85; }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
&__applied {
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
gap: 8px;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
&__badge {
|
|
166
|
+
display: inline-flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
gap: 5px;
|
|
169
|
+
padding: 6px 12px;
|
|
170
|
+
background: rgba(34, 197, 94, 0.12);
|
|
171
|
+
color: #22c55e;
|
|
172
|
+
font-size: 13px;
|
|
173
|
+
font-weight: 600;
|
|
174
|
+
border: 1px solid rgba(34, 197, 94, 0.3);
|
|
175
|
+
|
|
176
|
+
svg { flex-shrink: 0; }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
&__remove {
|
|
180
|
+
background: none;
|
|
181
|
+
border: none;
|
|
182
|
+
color: var(--inactive, #6b7280);
|
|
183
|
+
cursor: pointer;
|
|
184
|
+
padding: 4px;
|
|
185
|
+
display: flex;
|
|
186
|
+
align-items: center;
|
|
187
|
+
transition: color 0.15s;
|
|
188
|
+
|
|
189
|
+
&:hover { color: #ee3831; }
|
|
190
|
+
&:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
&__error {
|
|
194
|
+
color: #ee3831;
|
|
195
|
+
font-size: 12px;
|
|
196
|
+
margin-top: 4px;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
&__spinner {
|
|
200
|
+
display: inline-block;
|
|
201
|
+
width: 14px;
|
|
202
|
+
height: 14px;
|
|
203
|
+
border: 2px solid rgba(255,255,255,0.3);
|
|
204
|
+
border-top-color: #fff;
|
|
205
|
+
border-radius: 50%;
|
|
206
|
+
animation: mg-coupon-spin 0.6s linear infinite;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@keyframes mg-coupon-spin { to { transform: rotate(360deg); } }
|
|
211
|
+
</style>
|
|
@@ -41,7 +41,6 @@ function handleInput(e: Event) {
|
|
|
41
41
|
width: 100%;
|
|
42
42
|
padding: 16px;
|
|
43
43
|
border: 1px solid #f59e0b;
|
|
44
|
-
border-radius: 8px;
|
|
45
44
|
background: rgba(245, 158, 11, 0.05);
|
|
46
45
|
|
|
47
46
|
&__info {
|
|
@@ -76,7 +75,6 @@ function handleInput(e: Event) {
|
|
|
76
75
|
width: 100%;
|
|
77
76
|
padding: 10px 12px;
|
|
78
77
|
border: 1px solid var(--inactive, #d1d5db);
|
|
79
|
-
border-radius: 6px;
|
|
80
78
|
font-size: 14px;
|
|
81
79
|
background: var(--body-bg-card, #fff);
|
|
82
80
|
color: var(--active, #111827);
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { onMounted, watch } from "vue";
|
|
3
3
|
import { usePaymentMethods } from "../../composables/usePaymentMethods";
|
|
4
|
+
import MgPaymentMethods from "../ui/MgPaymentMethods.vue";
|
|
4
5
|
import type { AxiosInstance } from "axios";
|
|
5
6
|
|
|
6
7
|
const props = defineProps<{
|
|
7
8
|
httpService: AxiosInstance;
|
|
8
9
|
context?: "checkout" | "subscription";
|
|
9
10
|
modelValue?: string;
|
|
11
|
+
theme?: string;
|
|
10
12
|
}>();
|
|
11
13
|
|
|
12
14
|
const emit = defineEmits<{
|
|
@@ -42,18 +44,21 @@ function select(name: string) {
|
|
|
42
44
|
selectedMethod.value = name.toLowerCase();
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
// Canonical numeric ids consumed by <MgPaymentMethods> inline SVG logos.
|
|
48
|
+
// PayPal = 2, Stripe = 3 (matches api-main + useConfirmation).
|
|
49
|
+
function getMethodId(m: { id: number; name: string }): number {
|
|
50
|
+
const key = name2key(m.name);
|
|
51
|
+
if (key === "paypal") return 2;
|
|
52
|
+
if (key === "stripe") return 3;
|
|
53
|
+
return m.id;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function name2key(name: string): string {
|
|
57
|
+
return (name ?? "").toLowerCase();
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
function getLabel(name: string): string {
|
|
56
|
-
const key = name
|
|
61
|
+
const key = name2key(name);
|
|
57
62
|
const labels: Record<string, string> = {
|
|
58
63
|
paypal: "PayPal",
|
|
59
64
|
stripe: "Stripe",
|
|
@@ -82,11 +87,9 @@ function getLabel(name: string): string {
|
|
|
82
87
|
:class="{ 'mg-payment-method--active': selectedMethod === method.name.toLowerCase() }"
|
|
83
88
|
@click="select(method.name)"
|
|
84
89
|
>
|
|
85
|
-
<
|
|
86
|
-
:
|
|
87
|
-
|
|
88
|
-
class="mg-payment-method__logo"
|
|
89
|
-
/>
|
|
90
|
+
<span class="mg-payment-method__logo">
|
|
91
|
+
<MgPaymentMethods :method="getMethodId(method)" :theme="theme" />
|
|
92
|
+
</span>
|
|
90
93
|
<span class="mg-payment-method__label">{{ getLabel(method.name) }}</span>
|
|
91
94
|
</button>
|
|
92
95
|
</div>
|
|
@@ -119,7 +122,6 @@ function getLabel(name: string): string {
|
|
|
119
122
|
gap: 8px;
|
|
120
123
|
padding: 12px 16px;
|
|
121
124
|
border: 2px solid var(--inactive, #e5e7eb);
|
|
122
|
-
border-radius: 8px;
|
|
123
125
|
background: transparent;
|
|
124
126
|
cursor: pointer;
|
|
125
127
|
transition: all 0.2s ease;
|
|
@@ -151,7 +153,6 @@ function getLabel(name: string): string {
|
|
|
151
153
|
|
|
152
154
|
.skeleton-block {
|
|
153
155
|
background: var(--inactive, #e5e7eb);
|
|
154
|
-
border-radius: 8px;
|
|
155
156
|
animation: pulse 1.5s ease-in-out infinite;
|
|
156
157
|
}
|
|
157
158
|
|
|
@@ -143,7 +143,6 @@ onUnmounted(() => {
|
|
|
143
143
|
align-items: center;
|
|
144
144
|
padding: 24px;
|
|
145
145
|
border: 1px solid var(--inactive, #e5e7eb);
|
|
146
|
-
border-radius: 12px;
|
|
147
146
|
background: var(--body-bg-card, #fff);
|
|
148
147
|
|
|
149
148
|
&__header {
|
|
@@ -191,7 +190,6 @@ onUnmounted(() => {
|
|
|
191
190
|
gap: 8px;
|
|
192
191
|
padding: 10px 20px;
|
|
193
192
|
border: 1px solid #4f46e5;
|
|
194
|
-
border-radius: 8px;
|
|
195
193
|
background: transparent;
|
|
196
194
|
color: #4f46e5;
|
|
197
195
|
font-size: 14px;
|