@mundogamernetwork/shared-ui 1.1.9 → 1.1.15
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/MgCartSummary.vue +3 -0
- package/components/checkout/MgCheckoutSidebar.vue +20 -36
- package/components/checkout/MgCouponInput.vue +21 -1
- package/components/checkout/MgPaymentMethodSelector.vue +21 -20
- package/composables/usePaymentMethods.ts +31 -45
- package/package.json +1 -1
|
@@ -32,6 +32,8 @@ const props = defineProps<{
|
|
|
32
32
|
autoWire?: boolean;
|
|
33
33
|
/** Show coupon input section */
|
|
34
34
|
showCoupon?: boolean;
|
|
35
|
+
/** Coupons unavailable for this flow (e.g. subscriptions) — shows disabled + notice */
|
|
36
|
+
couponUnavailable?: boolean;
|
|
35
37
|
}>();
|
|
36
38
|
|
|
37
39
|
const emit = defineEmits<{
|
|
@@ -101,6 +103,7 @@ const showCoupon = computed(() => props.showCoupon !== false);
|
|
|
101
103
|
:error="couponError"
|
|
102
104
|
:disabled="disabled"
|
|
103
105
|
:auto-wire="autoWire"
|
|
106
|
+
:unavailable="couponUnavailable"
|
|
104
107
|
@apply-coupon="(code) => emit('apply-coupon', code)"
|
|
105
108
|
@remove-coupon="emit('remove-coupon')"
|
|
106
109
|
/>
|
|
@@ -6,7 +6,6 @@ import MgCartItemList from "./MgCartItemList.vue";
|
|
|
6
6
|
import MgCartSummary from "./MgCartSummary.vue";
|
|
7
7
|
import MgPaymentMethodSelector from "./MgPaymentMethodSelector.vue";
|
|
8
8
|
import MgGuestEmailForm from "./MgGuestEmailForm.vue";
|
|
9
|
-
import MgPixQRCode from "./MgPixQRCode.vue";
|
|
10
9
|
|
|
11
10
|
const props = defineProps<{
|
|
12
11
|
checkout?: MgCheckoutInstance;
|
|
@@ -24,7 +23,7 @@ const emit = defineEmits<{
|
|
|
24
23
|
}>();
|
|
25
24
|
|
|
26
25
|
// Use injected checkout or prop
|
|
27
|
-
const co = props.checkout ?? injectMgCheckout()
|
|
26
|
+
const co = (props.checkout ?? injectMgCheckout())!;
|
|
28
27
|
if (!co) {
|
|
29
28
|
throw new Error("MgCheckoutSidebar: No checkout instance provided. Use provideMgCheckout() or pass checkout prop.");
|
|
30
29
|
}
|
|
@@ -33,11 +32,10 @@ onMounted(() => {
|
|
|
33
32
|
co.fetchCart();
|
|
34
33
|
});
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
35
|
+
function handleCheckout() {
|
|
36
|
+
// handlePurchase validates (email/gateway/terms) then opens the confirm modal;
|
|
37
|
+
// startValidation performs the actual submit. Consumers can also call directly.
|
|
38
|
+
co.handlePurchase();
|
|
41
39
|
}
|
|
42
40
|
</script>
|
|
43
41
|
|
|
@@ -49,28 +47,28 @@ async function handleCheckout() {
|
|
|
49
47
|
{{ $t?.("checkout.sidebar.cart") ?? "Your Cart" }}
|
|
50
48
|
</h3>
|
|
51
49
|
<MgCartItemList
|
|
52
|
-
:items="co.
|
|
50
|
+
:items="co.orderCart.value"
|
|
53
51
|
:loading="co.cartLoading.value"
|
|
54
52
|
:editable="editable ?? true"
|
|
55
53
|
:currency-symbol="currencySymbol"
|
|
56
54
|
@add-item="(id, type) => co.addItem(id, type)"
|
|
57
55
|
@remove-item="(id, type) => co.removeItem(id, type)"
|
|
58
|
-
@clear-all="co.
|
|
56
|
+
@clear-all="co.clearCartItems()"
|
|
59
57
|
/>
|
|
60
58
|
</div>
|
|
61
59
|
|
|
62
|
-
<!-- Cart Summary -->
|
|
60
|
+
<!-- Cart Summary (coupon auto-disabled when a plan is in cart) -->
|
|
63
61
|
<div v-if="co.hasItems.value" class="mg-checkout-sidebar__section">
|
|
64
62
|
<MgCartSummary
|
|
65
|
-
:total="co.
|
|
66
|
-
:original-total="co.
|
|
67
|
-
:has-discount="co.
|
|
63
|
+
:formatted-total="String(co.orderCartTotal.value)"
|
|
64
|
+
:formatted-original-total="String(co.orderCartDiscount.value)"
|
|
65
|
+
:has-discount="co.hasDiscountFlag.value"
|
|
68
66
|
:currency-symbol="currencySymbol"
|
|
69
67
|
:coupon-info="co.couponInfo.value"
|
|
70
68
|
:coupon-loading="co.couponLoading.value"
|
|
71
69
|
:coupon-error="co.couponError.value"
|
|
72
|
-
:disabled="co.
|
|
73
|
-
@apply-coupon="(code) => co.applyCoupon(code)"
|
|
70
|
+
:disabled="co.loading.value"
|
|
71
|
+
@apply-coupon="(code: string) => co.applyCoupon(code)"
|
|
74
72
|
@remove-coupon="co.removeCoupon()"
|
|
75
73
|
/>
|
|
76
74
|
</div>
|
|
@@ -78,9 +76,9 @@ async function handleCheckout() {
|
|
|
78
76
|
<!-- Guest Email -->
|
|
79
77
|
<div v-if="co.hasItems.value" class="mg-checkout-sidebar__section">
|
|
80
78
|
<MgGuestEmailForm
|
|
81
|
-
v-model="co.
|
|
79
|
+
v-model="co.offlineEmail.value"
|
|
82
80
|
:error="co.emailError.value"
|
|
83
|
-
:is-authenticated="isAuthenticated"
|
|
81
|
+
:is-authenticated="isAuthenticated ?? co.signedIn.value"
|
|
84
82
|
/>
|
|
85
83
|
</div>
|
|
86
84
|
|
|
@@ -90,29 +88,15 @@ async function handleCheckout() {
|
|
|
90
88
|
{{ $t?.("checkout.sidebar.payment") ?? "Payment Method" }}
|
|
91
89
|
</h3>
|
|
92
90
|
<MgPaymentMethodSelector
|
|
93
|
-
v-model="co.
|
|
94
|
-
:http-service="(co as any).paymentMethods.methods.value.length ? undefined : undefined"
|
|
91
|
+
v-model="co.selectedPaymentGateway.value"
|
|
95
92
|
:context="paymentContext ?? 'checkout'"
|
|
96
93
|
/>
|
|
97
94
|
</div>
|
|
98
95
|
|
|
99
|
-
<!-- Pix QR Code (shown after checkout if Pix) -->
|
|
100
|
-
<div v-if="co.checkoutResponse.value?.payment_type === 'pix'" class="mg-checkout-sidebar__section">
|
|
101
|
-
<MgPixQRCode
|
|
102
|
-
:qr-code="co.checkoutResponse.value.pix_data!.qr_code"
|
|
103
|
-
:qr-code-base64="co.checkoutResponse.value.pix_data!.qr_code_base64"
|
|
104
|
-
:expires-at="co.checkoutResponse.value.pix_data!.expires_at"
|
|
105
|
-
@paid="emit('checkout-complete', co.checkoutResponse.value)"
|
|
106
|
-
/>
|
|
107
|
-
</div>
|
|
108
|
-
|
|
109
96
|
<!-- Terms & Checkout Button -->
|
|
110
|
-
<div v-if="co.hasItems.value
|
|
97
|
+
<div v-if="co.hasItems.value" class="mg-checkout-sidebar__section mg-checkout-sidebar__actions">
|
|
111
98
|
<label class="terms-check">
|
|
112
|
-
<input
|
|
113
|
-
type="checkbox"
|
|
114
|
-
v-model="co.termsAccepted.value"
|
|
115
|
-
/>
|
|
99
|
+
<input type="checkbox" v-model="co.termUseAccepted.value" />
|
|
116
100
|
<span>
|
|
117
101
|
{{ $t?.("checkout.terms.accept") ?? "I accept the" }}
|
|
118
102
|
<a v-if="termsUrl" :href="termsUrl" target="_blank" class="terms-link">
|
|
@@ -124,10 +108,10 @@ async function handleCheckout() {
|
|
|
124
108
|
|
|
125
109
|
<button
|
|
126
110
|
class="checkout-btn"
|
|
127
|
-
:disabled="!co.hasItems.value || !co.
|
|
111
|
+
:disabled="!co.hasItems.value || !co.termUseAccepted.value || co.loading.value"
|
|
128
112
|
@click="handleCheckout"
|
|
129
113
|
>
|
|
130
|
-
<span v-if="co.
|
|
114
|
+
<span v-if="co.loading.value" class="spinner"></span>
|
|
131
115
|
<template v-else>
|
|
132
116
|
{{ $t?.("checkout.sidebar.buy") ?? "Complete Purchase" }}
|
|
133
117
|
</template>
|
|
@@ -18,6 +18,14 @@ const props = defineProps<{
|
|
|
18
18
|
disabled?: boolean;
|
|
19
19
|
/** If true, the component manages its own state via injectMgCheckout */
|
|
20
20
|
autoWire?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* When true, coupons are not available for this flow (e.g. subscriptions —
|
|
23
|
+
* the backend does not apply coupon discounts to plans yet). Shows the input
|
|
24
|
+
* disabled with an informational notice instead of accepting a code.
|
|
25
|
+
*/
|
|
26
|
+
unavailable?: boolean;
|
|
27
|
+
/** Override the default "coupons unavailable" notice text. */
|
|
28
|
+
unavailableText?: string;
|
|
21
29
|
}>();
|
|
22
30
|
|
|
23
31
|
const emit = defineEmits<{
|
|
@@ -41,7 +49,7 @@ const isLoading = computed(() =>
|
|
|
41
49
|
const couponError = computed(() =>
|
|
42
50
|
props.autoWire ? checkout?.couponError.value : (props.error ?? _localError.value)
|
|
43
51
|
);
|
|
44
|
-
const isDisabled = computed(() => props.disabled || isLoading.value);
|
|
52
|
+
const isDisabled = computed(() => props.disabled || props.unavailable || isLoading.value);
|
|
45
53
|
|
|
46
54
|
async function handleApply() {
|
|
47
55
|
const code = couponInput.value.trim().toUpperCase();
|
|
@@ -112,6 +120,11 @@ watch(
|
|
|
112
120
|
</button>
|
|
113
121
|
</div>
|
|
114
122
|
|
|
123
|
+
<!-- Unavailable notice (e.g. subscriptions: backend doesn't apply coupons yet) -->
|
|
124
|
+
<p v-if="unavailable" class="mg-coupon__notice">
|
|
125
|
+
{{ unavailableText ?? $t?.('checkout.coupon.unavailable_subscription') ?? 'Coupons are not available for subscriptions yet.' }}
|
|
126
|
+
</p>
|
|
127
|
+
|
|
115
128
|
<p v-if="couponError" class="mg-coupon__error">{{ couponError }}</p>
|
|
116
129
|
</div>
|
|
117
130
|
</template>
|
|
@@ -196,6 +209,13 @@ watch(
|
|
|
196
209
|
margin-top: 4px;
|
|
197
210
|
}
|
|
198
211
|
|
|
212
|
+
&__notice {
|
|
213
|
+
color: var(--secondary-info-fg, #9ca3af);
|
|
214
|
+
font-size: 12px;
|
|
215
|
+
margin-top: 6px;
|
|
216
|
+
font-style: italic;
|
|
217
|
+
}
|
|
218
|
+
|
|
199
219
|
&__spinner {
|
|
200
220
|
display: inline-block;
|
|
201
221
|
width: 14px;
|
|
@@ -5,7 +5,8 @@ import MgPaymentMethods from "../ui/MgPaymentMethods.vue";
|
|
|
5
5
|
import type { AxiosInstance } from "axios";
|
|
6
6
|
|
|
7
7
|
const props = defineProps<{
|
|
8
|
-
|
|
8
|
+
// Optional now — gateways are fixed (PayPal/Stripe), no HTTP fetch needed.
|
|
9
|
+
httpService?: AxiosInstance;
|
|
9
10
|
context?: "checkout" | "subscription";
|
|
10
11
|
modelValue?: string;
|
|
11
12
|
theme?: string;
|
|
@@ -40,31 +41,31 @@ watch(selectedMethod, (val) => {
|
|
|
40
41
|
emit("update:modelValue", val);
|
|
41
42
|
});
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
// Resolve the canonical gateway key (paypal|stripe) from a method.
|
|
45
|
+
// `name` is translatable on the backend, so fall back to the canonical id
|
|
46
|
+
// (PayPal = 2, Stripe = 3) when the name doesn't match.
|
|
47
|
+
function gatewayKey(m: { id: number; name: string }): string {
|
|
48
|
+
const n = (m.name ?? "").trim().toLowerCase();
|
|
49
|
+
if (n === "paypal" || m.id === 2) return "paypal";
|
|
50
|
+
if (n === "stripe" || m.id === 3) return "stripe";
|
|
51
|
+
return n;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function select(m: { id: number; name: string }) {
|
|
55
|
+
selectedMethod.value = gatewayKey(m);
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
// Canonical numeric ids consumed by <MgPaymentMethods> inline SVG logos.
|
|
48
|
-
// PayPal = 2, Stripe = 3 (matches api-main + useConfirmation).
|
|
49
59
|
function getMethodId(m: { id: number; name: string }): number {
|
|
50
|
-
const key =
|
|
60
|
+
const key = gatewayKey(m);
|
|
51
61
|
if (key === "paypal") return 2;
|
|
52
62
|
if (key === "stripe") return 3;
|
|
53
63
|
return m.id;
|
|
54
64
|
}
|
|
55
65
|
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
function getLabel(name: string): string {
|
|
61
|
-
const key = name2key(name);
|
|
62
|
-
const labels: Record<string, string> = {
|
|
63
|
-
paypal: "PayPal",
|
|
64
|
-
stripe: "Stripe",
|
|
65
|
-
mercadopago: "Pix",
|
|
66
|
-
};
|
|
67
|
-
return labels[key] ?? name;
|
|
66
|
+
function getLabel(m: { id: number; name: string }): string {
|
|
67
|
+
const labels: Record<string, string> = { paypal: "PayPal", stripe: "Stripe" };
|
|
68
|
+
return labels[gatewayKey(m)] ?? m.name;
|
|
68
69
|
}
|
|
69
70
|
</script>
|
|
70
71
|
|
|
@@ -84,13 +85,13 @@ function getLabel(name: string): string {
|
|
|
84
85
|
:key="method.id"
|
|
85
86
|
type="button"
|
|
86
87
|
class="mg-payment-method"
|
|
87
|
-
:class="{ 'mg-payment-method--active': selectedMethod === method
|
|
88
|
-
@click="select(method
|
|
88
|
+
:class="{ 'mg-payment-method--active': selectedMethod === gatewayKey(method) }"
|
|
89
|
+
@click="select(method)"
|
|
89
90
|
>
|
|
90
91
|
<span class="mg-payment-method__logo">
|
|
91
92
|
<MgPaymentMethods :method="getMethodId(method)" :theme="theme" />
|
|
92
93
|
</span>
|
|
93
|
-
<span class="mg-payment-method__label">{{ getLabel(method
|
|
94
|
+
<span class="mg-payment-method__label">{{ getLabel(method) }}</span>
|
|
94
95
|
</button>
|
|
95
96
|
</div>
|
|
96
97
|
</div>
|
|
@@ -6,57 +6,43 @@ export interface PaymentMethod {
|
|
|
6
6
|
name: string;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Payment gateways shown in the checkout UI.
|
|
11
|
+
*
|
|
12
|
+
* These are FIXED to PayPal + Stripe — the only gateways live across the
|
|
13
|
+
* ecosystem. We do NOT read the `payment_methods` table: that table holds
|
|
14
|
+
* generic legacy methods (Cash, Credit Card, Transfer, Bitcoin...) and has no
|
|
15
|
+
* notion of PayPal/Stripe.
|
|
16
|
+
*
|
|
17
|
+
* The actual gateway *account* (which api_credential to charge) is resolved by
|
|
18
|
+
* the backend at checkout time, honouring the existing active / default /
|
|
19
|
+
* production flags on `api_credentials`. That lets admins switch accounts
|
|
20
|
+
* without any frontend change. The frontend only sends
|
|
21
|
+
* `payment_gateway: 'paypal' | 'stripe'`.
|
|
22
|
+
*
|
|
23
|
+
* The numeric `id` here (PayPal = 2, Stripe = 3) is the convention used by
|
|
24
|
+
* <MgPaymentMethods> to pick which inline SVG logo to render. It is unrelated
|
|
25
|
+
* to the payment_methods table ids and to api ids (PayPal = 4, Stripe = 5).
|
|
26
|
+
*/
|
|
27
|
+
const GATEWAYS: PaymentMethod[] = [
|
|
28
|
+
{ id: 2, name: "paypal" },
|
|
29
|
+
{ id: 3, name: "stripe" },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export function usePaymentMethods(_httpService?: AxiosInstance) {
|
|
33
|
+
const methods = ref<PaymentMethod[]>([...GATEWAYS]);
|
|
11
34
|
const selectedMethod = ref<string>("");
|
|
12
35
|
const loading = ref(false);
|
|
13
36
|
const error = ref<string | null>(null);
|
|
14
37
|
|
|
15
|
-
async
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
loading.value = true;
|
|
19
|
-
error.value = null;
|
|
20
|
-
try {
|
|
21
|
-
const endpoint =
|
|
22
|
-
context === "subscription"
|
|
23
|
-
? "/payment-methods/subscriptions"
|
|
24
|
-
: "/payment-methods";
|
|
25
|
-
const res = await httpService.get(endpoint);
|
|
26
|
-
methods.value = res.data?.data ?? [];
|
|
27
|
-
|
|
28
|
-
// Auto-select if only one method available
|
|
29
|
-
if (methods.value.length === 1) {
|
|
30
|
-
selectedMethod.value = methods.value[0].name.toLowerCase();
|
|
31
|
-
}
|
|
32
|
-
} catch (e: any) {
|
|
33
|
-
// Fallback: if endpoint doesn't exist yet (404), provide default methods
|
|
34
|
-
console.warn("[usePaymentMethods] Endpoint not available, using fallback:", e?.response?.status);
|
|
35
|
-
// Canonical IDs (api-main): PayPal = 2, Stripe = 3
|
|
36
|
-
methods.value = [
|
|
37
|
-
{ id: 2, name: "paypal" },
|
|
38
|
-
{ id: 3, name: "stripe" },
|
|
39
|
-
];
|
|
40
|
-
if (methods.value.length > 0 && !selectedMethod.value) {
|
|
41
|
-
selectedMethod.value = methods.value[0].name.toLowerCase();
|
|
42
|
-
}
|
|
43
|
-
error.value = null;
|
|
44
|
-
} finally {
|
|
45
|
-
loading.value = false;
|
|
46
|
-
}
|
|
38
|
+
// No HTTP call. Kept async + the context arg for call-site compatibility.
|
|
39
|
+
async function fetchMethods(_context: "checkout" | "subscription" = "checkout") {
|
|
40
|
+
methods.value = [...GATEWAYS];
|
|
47
41
|
}
|
|
48
42
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
const isStripe = computed(
|
|
54
|
-
() => selectedMethod.value.toLowerCase() === "stripe"
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
const isPaypal = computed(
|
|
58
|
-
() => selectedMethod.value.toLowerCase() === "paypal"
|
|
59
|
-
);
|
|
43
|
+
const isStripe = computed(() => selectedMethod.value.toLowerCase() === "stripe");
|
|
44
|
+
const isPaypal = computed(() => selectedMethod.value.toLowerCase() === "paypal");
|
|
45
|
+
const isPix = computed(() => false); // Pix not enabled yet
|
|
60
46
|
|
|
61
47
|
function selectMethod(name: string) {
|
|
62
48
|
selectedMethod.value = name.toLowerCase();
|