@mundogamernetwork/shared-ui 1.1.7 → 1.1.14

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.
@@ -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 || (autoWire && checkout?.hasPlanInCart?.value)"
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
- async function handleCheckout() {
37
- const res = await co.checkout(props.successUrl, props.cancelUrl);
38
- if (res) {
39
- emit("checkout-complete", res);
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,29 @@ async function handleCheckout() {
49
47
  {{ $t?.("checkout.sidebar.cart") ?? "Your Cart" }}
50
48
  </h3>
51
49
  <MgCartItemList
52
- :items="co.cart.value?.items ?? []"
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.clearCart()"
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.cartTotal.value"
66
- :original-total="co.cartOriginalTotal.value"
67
- :has-discount="co.hasDiscount.value"
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.checkoutLoading.value"
73
- @apply-coupon="(code) => co.applyCoupon(code)"
70
+ :coupon-unavailable="co.hasPlanInCart.value"
71
+ :disabled="co.loading.value"
72
+ @apply-coupon="(code: string) => co.applyCoupon(code)"
74
73
  @remove-coupon="co.removeCoupon()"
75
74
  />
76
75
  </div>
@@ -78,9 +77,9 @@ async function handleCheckout() {
78
77
  <!-- Guest Email -->
79
78
  <div v-if="co.hasItems.value" class="mg-checkout-sidebar__section">
80
79
  <MgGuestEmailForm
81
- v-model="co.guestEmail.value"
80
+ v-model="co.offlineEmail.value"
82
81
  :error="co.emailError.value"
83
- :is-authenticated="isAuthenticated"
82
+ :is-authenticated="isAuthenticated ?? co.signedIn.value"
84
83
  />
85
84
  </div>
86
85
 
@@ -90,29 +89,15 @@ async function handleCheckout() {
90
89
  {{ $t?.("checkout.sidebar.payment") ?? "Payment Method" }}
91
90
  </h3>
92
91
  <MgPaymentMethodSelector
93
- v-model="co.paymentMethods.selectedMethod.value"
94
- :http-service="(co as any).paymentMethods.methods.value.length ? undefined : undefined"
92
+ v-model="co.selectedPaymentGateway.value"
95
93
  :context="paymentContext ?? 'checkout'"
96
94
  />
97
95
  </div>
98
96
 
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
97
  <!-- Terms & Checkout Button -->
110
- <div v-if="co.hasItems.value && !co.checkoutResponse.value" class="mg-checkout-sidebar__section mg-checkout-sidebar__actions">
98
+ <div v-if="co.hasItems.value" class="mg-checkout-sidebar__section mg-checkout-sidebar__actions">
111
99
  <label class="terms-check">
112
- <input
113
- type="checkbox"
114
- v-model="co.termsAccepted.value"
115
- />
100
+ <input type="checkbox" v-model="co.termUseAccepted.value" />
116
101
  <span>
117
102
  {{ $t?.("checkout.terms.accept") ?? "I accept the" }}
118
103
  <a v-if="termsUrl" :href="termsUrl" target="_blank" class="terms-link">
@@ -124,10 +109,10 @@ async function handleCheckout() {
124
109
 
125
110
  <button
126
111
  class="checkout-btn"
127
- :disabled="!co.hasItems.value || !co.termsAccepted.value || co.checkoutLoading.value"
112
+ :disabled="!co.hasItems.value || !co.termUseAccepted.value || co.loading.value"
128
113
  @click="handleCheckout"
129
114
  >
130
- <span v-if="co.checkoutLoading.value" class="spinner"></span>
115
+ <span v-if="co.loading.value" class="spinner"></span>
131
116
  <template v-else>
132
117
  {{ $t?.("checkout.sidebar.buy") ?? "Complete Purchase" }}
133
118
  </template>
@@ -154,7 +139,6 @@ async function handleCheckout() {
154
139
  padding: 16px;
155
140
  background: var(--body-bg-card, #fff);
156
141
  border: 1px solid var(--inactive, #f3f4f6);
157
- border-radius: 8px;
158
142
  }
159
143
 
160
144
  &__actions {
@@ -198,7 +182,6 @@ async function handleCheckout() {
198
182
  width: 100%;
199
183
  height: 48px;
200
184
  border: none;
201
- border-radius: 8px;
202
185
  background: #4f46e5;
203
186
  color: #fff;
204
187
  font-size: 15px;
@@ -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;
@@ -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,15 @@
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
- httpService: AxiosInstance;
8
+ // Optional now — gateways are fixed (PayPal/Stripe), no HTTP fetch needed.
9
+ httpService?: AxiosInstance;
8
10
  context?: "checkout" | "subscription";
9
11
  modelValue?: string;
12
+ theme?: string;
10
13
  }>();
11
14
 
12
15
  const emit = defineEmits<{
@@ -38,28 +41,31 @@ watch(selectedMethod, (val) => {
38
41
  emit("update:modelValue", val);
39
42
  });
40
43
 
41
- function select(name: string) {
42
- selectedMethod.value = name.toLowerCase();
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;
43
52
  }
44
53
 
45
- function getLogoSrc(name: string): string {
46
- const key = name.toLowerCase();
47
- const logos: Record<string, string> = {
48
- paypal: "/imgs/payments/paypal.svg",
49
- stripe: "/imgs/payments/stripe.svg",
50
- mercadopago: "/imgs/payments/pix.svg",
51
- };
52
- return logos[key] ?? "/imgs/payments/default.svg";
54
+ function select(m: { id: number; name: string }) {
55
+ selectedMethod.value = gatewayKey(m);
53
56
  }
54
57
 
55
- function getLabel(name: string): string {
56
- const key = name.toLowerCase();
57
- const labels: Record<string, string> = {
58
- paypal: "PayPal",
59
- stripe: "Stripe",
60
- mercadopago: "Pix",
61
- };
62
- return labels[key] ?? name;
58
+ // Canonical numeric ids consumed by <MgPaymentMethods> inline SVG logos.
59
+ function getMethodId(m: { id: number; name: string }): number {
60
+ const key = gatewayKey(m);
61
+ if (key === "paypal") return 2;
62
+ if (key === "stripe") return 3;
63
+ return m.id;
64
+ }
65
+
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;
63
69
  }
64
70
  </script>
65
71
 
@@ -79,15 +85,13 @@ function getLabel(name: string): string {
79
85
  :key="method.id"
80
86
  type="button"
81
87
  class="mg-payment-method"
82
- :class="{ 'mg-payment-method--active': selectedMethod === method.name.toLowerCase() }"
83
- @click="select(method.name)"
88
+ :class="{ 'mg-payment-method--active': selectedMethod === gatewayKey(method) }"
89
+ @click="select(method)"
84
90
  >
85
- <img
86
- :src="getLogoSrc(method.name)"
87
- :alt="getLabel(method.name)"
88
- class="mg-payment-method__logo"
89
- />
90
- <span class="mg-payment-method__label">{{ getLabel(method.name) }}</span>
91
+ <span class="mg-payment-method__logo">
92
+ <MgPaymentMethods :method="getMethodId(method)" :theme="theme" />
93
+ </span>
94
+ <span class="mg-payment-method__label">{{ getLabel(method) }}</span>
91
95
  </button>
92
96
  </div>
93
97
  </div>
@@ -119,7 +123,6 @@ function getLabel(name: string): string {
119
123
  gap: 8px;
120
124
  padding: 12px 16px;
121
125
  border: 2px solid var(--inactive, #e5e7eb);
122
- border-radius: 8px;
123
126
  background: transparent;
124
127
  cursor: pointer;
125
128
  transition: all 0.2s ease;
@@ -151,7 +154,6 @@ function getLabel(name: string): string {
151
154
 
152
155
  .skeleton-block {
153
156
  background: var(--inactive, #e5e7eb);
154
- border-radius: 8px;
155
157
  animation: pulse 1.5s ease-in-out infinite;
156
158
  }
157
159
 
@@ -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;
@@ -6,56 +6,43 @@ export interface PaymentMethod {
6
6
  name: string;
7
7
  }
8
8
 
9
- export function usePaymentMethods(httpService: AxiosInstance) {
10
- const methods = ref<PaymentMethod[]>([]);
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 function fetchMethods(
16
- context: "checkout" | "subscription" = "checkout"
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
- methods.value = [
36
- { id: 0, name: "paypal" },
37
- { id: 1, name: "Stripe" },
38
- ];
39
- if (methods.value.length > 0 && !selectedMethod.value) {
40
- selectedMethod.value = methods.value[0].name.toLowerCase();
41
- }
42
- error.value = null;
43
- } finally {
44
- loading.value = false;
45
- }
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];
46
41
  }
47
42
 
48
- const isPix = computed(
49
- () => selectedMethod.value.toLowerCase() === "mercadopago"
50
- );
51
-
52
- const isStripe = computed(
53
- () => selectedMethod.value.toLowerCase() === "stripe"
54
- );
55
-
56
- const isPaypal = computed(
57
- () => selectedMethod.value.toLowerCase() === "paypal"
58
- );
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
59
46
 
60
47
  function selectMethod(name: string) {
61
48
  selectedMethod.value = name.toLowerCase();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.1.7",
3
+ "version": "1.1.14",
4
4
  "description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",