@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.
@@ -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
- 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,28 @@ 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
+ :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.guestEmail.value"
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.paymentMethods.selectedMethod.value"
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 && !co.checkoutResponse.value" class="mg-checkout-sidebar__section mg-checkout-sidebar__actions">
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.termsAccepted.value || co.checkoutLoading.value"
111
+ :disabled="!co.hasItems.value || !co.termUseAccepted.value || co.loading.value"
128
112
  @click="handleCheckout"
129
113
  >
130
- <span v-if="co.checkoutLoading.value" class="spinner"></span>
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
- httpService: AxiosInstance;
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
- function select(name: string) {
44
- 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;
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 = name2key(m.name);
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 name2key(name: string): string {
57
- return (name ?? "").toLowerCase();
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.name.toLowerCase() }"
88
- @click="select(method.name)"
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.name) }}</span>
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
- 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
- // 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 isPix = computed(
50
- () => selectedMethod.value.toLowerCase() === "mercadopago"
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.1.9",
3
+ "version": "1.1.15",
4
4
  "description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",