@mundogamernetwork/shared-ui 1.0.0

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.
Files changed (87) hide show
  1. package/README.md +283 -0
  2. package/components/PressKit/AssetGallery.vue +349 -0
  3. package/components/PressKit/Awards.vue +100 -0
  4. package/components/PressKit/Credits.vue +78 -0
  5. package/components/PressKit/FactSheet.vue +204 -0
  6. package/components/PressKit/Hero.vue +143 -0
  7. package/components/PressKit/Quotes.vue +80 -0
  8. package/components/PressKit/VideoPlayer.vue +134 -0
  9. package/components/checkout/MgCartItemList.vue +214 -0
  10. package/components/checkout/MgCartSummary.vue +204 -0
  11. package/components/checkout/MgCheckoutSidebar.vue +230 -0
  12. package/components/checkout/MgGuestEmailForm.vue +97 -0
  13. package/components/checkout/MgPaymentMethodSelector.vue +162 -0
  14. package/components/checkout/MgPixQRCode.vue +222 -0
  15. package/components/indie-wall/IndieWallLeaderboard.vue +208 -0
  16. package/components/indie-wall/MuralCanvas.vue +481 -0
  17. package/components/indie-wall/StepBlock.vue +314 -0
  18. package/components/indie-wall/StepCustomize.vue +530 -0
  19. package/components/indie-wall/StepGoal.vue +169 -0
  20. package/components/indie-wall/StepPackage.vue +145 -0
  21. package/components/indie-wall/StepPay.vue +209 -0
  22. package/components/indie-wall/SupportStepper.vue +372 -0
  23. package/components/invoices/MgInvoiceDownload.vue +50 -0
  24. package/components/pricing/MgBillingToggle.vue +74 -0
  25. package/components/pricing/MgPricingCard.vue +245 -0
  26. package/components/ui/Header/MgMessageCard.vue +147 -0
  27. package/components/ui/Header/MgMessageModal.vue +414 -0
  28. package/components/ui/Header/MgNotificationCard.vue +200 -0
  29. package/components/ui/Header/MgNotificationsModal.vue +125 -0
  30. package/components/ui/MgAnnouncementBanner.vue +147 -0
  31. package/components/ui/MgBanners.vue +23 -0
  32. package/components/ui/MgHeaderComponent.vue +283 -0
  33. package/components/ui/MgHeaderUIConfig.vue +225 -0
  34. package/components/ui/MgHeaderUIUser.vue +301 -0
  35. package/components/ui/MgLoginModal.vue +156 -0
  36. package/components/ui/MgPromotionBanner.vue +185 -0
  37. package/composables/useLogout.ts +42 -0
  38. package/composables/useMgCheckout.ts +287 -0
  39. package/composables/useMgUserNotifications.ts +122 -0
  40. package/composables/usePaymentMethods.ts +75 -0
  41. package/composables/useSubscription.ts +163 -0
  42. package/middleware/auth.global.ts +40 -0
  43. package/nuxt.config.ts +31 -0
  44. package/package.json +40 -0
  45. package/pages/[slug]/index.vue +112 -0
  46. package/pages/about.vue +133 -0
  47. package/pages/blog.vue +430 -0
  48. package/pages/careers.vue +329 -0
  49. package/pages/contact.vue +339 -0
  50. package/pages/faq.vue +317 -0
  51. package/pages/health-check.vue +20 -0
  52. package/pages/icons.vue +58 -0
  53. package/pages/magazine/[slug].vue +209 -0
  54. package/pages/magazine/index.vue +267 -0
  55. package/pages/media-kit/[slug].vue +625 -0
  56. package/pages/mural/[slug].vue +1058 -0
  57. package/pages/partners.vue +290 -0
  58. package/pages/press.vue +237 -0
  59. package/pages/presskit/[slug].vue +191 -0
  60. package/pages/roadmap.vue +355 -0
  61. package/pages/status.vue +199 -0
  62. package/pages/team.vue +266 -0
  63. package/pages/wall/[slug].vue +11 -0
  64. package/plugins/auth.client.ts +17 -0
  65. package/plugins/echo.client.ts +132 -0
  66. package/services/authService.ts +95 -0
  67. package/services/chatService.ts +53 -0
  68. package/services/contactService.ts +35 -0
  69. package/services/documentService.ts +16 -0
  70. package/services/httpService.ts +95 -0
  71. package/services/indieWallService.ts +174 -0
  72. package/services/institutionalService.ts +248 -0
  73. package/services/mediaKitService.ts +51 -0
  74. package/services/notificationsService.ts +20 -0
  75. package/services/pressKitService.ts +55 -0
  76. package/stores/announcement.ts +129 -0
  77. package/stores/auth.ts +86 -0
  78. package/stores/chat.ts +150 -0
  79. package/stores/contact.ts +28 -0
  80. package/stores/document.ts +27 -0
  81. package/stores/index.ts +34 -0
  82. package/stores/institutional.ts +231 -0
  83. package/stores/login.ts +27 -0
  84. package/stores/notifications.ts +133 -0
  85. package/stores/promotion.ts +154 -0
  86. package/types/index.ts +135 -0
  87. package/utils/serialize.ts +29 -0
@@ -0,0 +1,230 @@
1
+ <script setup lang="ts">
2
+ import { onMounted } from "vue";
3
+ import { injectMgCheckout } from "../../composables/useMgCheckout";
4
+ import type { MgCheckoutInstance } from "../../composables/useMgCheckout";
5
+ import MgCartItemList from "./MgCartItemList.vue";
6
+ import MgCartSummary from "./MgCartSummary.vue";
7
+ import MgPaymentMethodSelector from "./MgPaymentMethodSelector.vue";
8
+ import MgGuestEmailForm from "./MgGuestEmailForm.vue";
9
+ import MgPixQRCode from "./MgPixQRCode.vue";
10
+
11
+ const props = defineProps<{
12
+ checkout?: MgCheckoutInstance;
13
+ successUrl: string;
14
+ cancelUrl: string;
15
+ termsUrl?: string;
16
+ isAuthenticated?: boolean;
17
+ currencySymbol?: string;
18
+ editable?: boolean;
19
+ paymentContext?: "checkout" | "subscription";
20
+ }>();
21
+
22
+ const emit = defineEmits<{
23
+ "checkout-complete": [response: any];
24
+ }>();
25
+
26
+ // Use injected checkout or prop
27
+ const co = props.checkout ?? injectMgCheckout();
28
+ if (!co) {
29
+ throw new Error("MgCheckoutSidebar: No checkout instance provided. Use provideMgCheckout() or pass checkout prop.");
30
+ }
31
+
32
+ onMounted(() => {
33
+ co.fetchCart();
34
+ });
35
+
36
+ async function handleCheckout() {
37
+ const res = await co.checkout(props.successUrl, props.cancelUrl);
38
+ if (res) {
39
+ emit("checkout-complete", res);
40
+ }
41
+ }
42
+ </script>
43
+
44
+ <template>
45
+ <div class="mg-checkout-sidebar">
46
+ <!-- Cart Items -->
47
+ <div class="mg-checkout-sidebar__section">
48
+ <h3 class="section-title">
49
+ {{ $t?.("checkout.sidebar.cart") ?? "Your Cart" }}
50
+ </h3>
51
+ <MgCartItemList
52
+ :items="co.cart.value?.items ?? []"
53
+ :loading="co.cartLoading.value"
54
+ :editable="editable ?? true"
55
+ :currency-symbol="currencySymbol"
56
+ @add-item="(id, type) => co.addItem(id, type)"
57
+ @remove-item="(id, type) => co.removeItem(id, type)"
58
+ @clear-all="co.clearCart()"
59
+ />
60
+ </div>
61
+
62
+ <!-- Cart Summary -->
63
+ <div v-if="co.hasItems.value" class="mg-checkout-sidebar__section">
64
+ <MgCartSummary
65
+ :total="co.cartTotal.value"
66
+ :original-total="co.cartOriginalTotal.value"
67
+ :has-discount="co.hasDiscount.value"
68
+ :currency-symbol="currencySymbol"
69
+ :coupon-info="co.couponInfo.value"
70
+ :coupon-loading="co.couponLoading.value"
71
+ :coupon-error="co.couponError.value"
72
+ :disabled="co.checkoutLoading.value"
73
+ @apply-coupon="(code) => co.applyCoupon(code)"
74
+ @remove-coupon="co.removeCoupon()"
75
+ />
76
+ </div>
77
+
78
+ <!-- Guest Email -->
79
+ <div v-if="co.hasItems.value" class="mg-checkout-sidebar__section">
80
+ <MgGuestEmailForm
81
+ v-model="co.guestEmail.value"
82
+ :error="co.emailError.value"
83
+ :is-authenticated="isAuthenticated"
84
+ />
85
+ </div>
86
+
87
+ <!-- Payment Method -->
88
+ <div v-if="co.hasItems.value" class="mg-checkout-sidebar__section">
89
+ <h3 class="section-title">
90
+ {{ $t?.("checkout.sidebar.payment") ?? "Payment Method" }}
91
+ </h3>
92
+ <MgPaymentMethodSelector
93
+ v-model="co.paymentMethods.selectedMethod.value"
94
+ :http-service="(co as any).paymentMethods.methods.value.length ? undefined : undefined"
95
+ :context="paymentContext ?? 'checkout'"
96
+ />
97
+ </div>
98
+
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
+ <!-- Terms & Checkout Button -->
110
+ <div v-if="co.hasItems.value && !co.checkoutResponse.value" class="mg-checkout-sidebar__section mg-checkout-sidebar__actions">
111
+ <label class="terms-check">
112
+ <input
113
+ type="checkbox"
114
+ v-model="co.termsAccepted.value"
115
+ />
116
+ <span>
117
+ {{ $t?.("checkout.terms.accept") ?? "I accept the" }}
118
+ <a v-if="termsUrl" :href="termsUrl" target="_blank" class="terms-link">
119
+ {{ $t?.("checkout.terms.link") ?? "Terms of Use" }}
120
+ </a>
121
+ <template v-else>{{ $t?.("checkout.terms.link") ?? "Terms of Use" }}</template>
122
+ </span>
123
+ </label>
124
+
125
+ <button
126
+ class="checkout-btn"
127
+ :disabled="!co.hasItems.value || !co.termsAccepted.value || co.checkoutLoading.value"
128
+ @click="handleCheckout"
129
+ >
130
+ <span v-if="co.checkoutLoading.value" class="spinner"></span>
131
+ <template v-else>
132
+ {{ $t?.("checkout.sidebar.buy") ?? "Complete Purchase" }}
133
+ </template>
134
+ </button>
135
+
136
+ <p v-if="co.checkoutError.value" class="checkout-error">
137
+ {{ co.checkoutError.value }}
138
+ </p>
139
+ </div>
140
+
141
+ <slot name="footer" />
142
+ </div>
143
+ </template>
144
+
145
+ <style lang="scss" scoped>
146
+ .mg-checkout-sidebar {
147
+ display: flex;
148
+ flex-direction: column;
149
+ gap: 4px;
150
+ position: sticky;
151
+ top: 80px;
152
+
153
+ &__section {
154
+ padding: 16px;
155
+ background: var(--body-bg-card, #fff);
156
+ border: 1px solid var(--inactive, #f3f4f6);
157
+ border-radius: 8px;
158
+ }
159
+
160
+ &__actions {
161
+ display: flex;
162
+ flex-direction: column;
163
+ gap: 12px;
164
+ }
165
+ }
166
+
167
+ .section-title {
168
+ font-size: 14px;
169
+ font-weight: 600;
170
+ color: var(--active, #111827);
171
+ margin: 0 0 12px;
172
+ }
173
+
174
+ .terms-check {
175
+ display: flex;
176
+ align-items: flex-start;
177
+ gap: 8px;
178
+ cursor: pointer;
179
+ font-size: 13px;
180
+ color: var(--active, #111827);
181
+
182
+ input[type="checkbox"] {
183
+ margin-top: 2px;
184
+ accent-color: #4f46e5;
185
+ }
186
+ }
187
+
188
+ .terms-link {
189
+ color: #4f46e5;
190
+ text-decoration: underline;
191
+ }
192
+
193
+ .checkout-btn {
194
+ display: flex;
195
+ align-items: center;
196
+ justify-content: center;
197
+ gap: 8px;
198
+ width: 100%;
199
+ height: 48px;
200
+ border: none;
201
+ border-radius: 8px;
202
+ background: #4f46e5;
203
+ color: #fff;
204
+ font-size: 15px;
205
+ font-weight: 600;
206
+ cursor: pointer;
207
+ transition: background 0.2s;
208
+
209
+ &:hover:not(:disabled) { background: #4338ca; }
210
+ &:disabled { opacity: 0.5; cursor: not-allowed; }
211
+ }
212
+
213
+ .checkout-error {
214
+ color: #ee3831;
215
+ font-size: 13px;
216
+ text-align: center;
217
+ margin: 0;
218
+ }
219
+
220
+ .spinner {
221
+ width: 18px;
222
+ height: 18px;
223
+ border: 2px solid rgba(255, 255, 255, 0.3);
224
+ border-top-color: #fff;
225
+ border-radius: 50%;
226
+ animation: spin 0.6s linear infinite;
227
+ }
228
+
229
+ @keyframes spin { to { transform: rotate(360deg); } }
230
+ </style>
@@ -0,0 +1,97 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{
3
+ modelValue: string;
4
+ error?: string;
5
+ isAuthenticated?: boolean;
6
+ }>();
7
+
8
+ const emit = defineEmits<{
9
+ "update:modelValue": [value: string];
10
+ }>();
11
+
12
+ function handleInput(e: Event) {
13
+ emit("update:modelValue", (e.target as HTMLInputElement).value);
14
+ }
15
+ </script>
16
+
17
+ <template>
18
+ <div v-if="!isAuthenticated" class="mg-guest-email">
19
+ <div class="mg-guest-email__info">
20
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
21
+ <circle cx="12" cy="12" r="10" /><line x1="12" y1="16" x2="12" y2="12" /><line x1="12" y1="8" x2="12.01" y2="8" />
22
+ </svg>
23
+ <p>{{ $t?.("checkout.guest.info") ?? "You are purchasing as a guest. Enter your email to receive your invoice and product access." }}</p>
24
+ </div>
25
+ <div class="mg-guest-email__field">
26
+ <label>{{ $t?.("checkout.guest.email_label") ?? "Email" }} *</label>
27
+ <input
28
+ type="email"
29
+ :value="modelValue"
30
+ :placeholder="$t?.('checkout.guest.email_placeholder') ?? 'your@email.com'"
31
+ :class="{ 'has-error': error }"
32
+ @input="handleInput"
33
+ />
34
+ <small v-if="error" class="error-text">{{ error }}</small>
35
+ </div>
36
+ </div>
37
+ </template>
38
+
39
+ <style lang="scss" scoped>
40
+ .mg-guest-email {
41
+ width: 100%;
42
+ padding: 16px;
43
+ border: 1px solid #f59e0b;
44
+ border-radius: 8px;
45
+ background: rgba(245, 158, 11, 0.05);
46
+
47
+ &__info {
48
+ display: flex;
49
+ gap: 8px;
50
+ margin-bottom: 12px;
51
+
52
+ svg {
53
+ flex-shrink: 0;
54
+ color: #f59e0b;
55
+ margin-top: 2px;
56
+ }
57
+
58
+ p {
59
+ font-size: 13px;
60
+ color: var(--active, #111827);
61
+ line-height: 1.5;
62
+ margin: 0;
63
+ }
64
+ }
65
+
66
+ &__field {
67
+ label {
68
+ display: block;
69
+ font-size: 12px;
70
+ font-weight: 600;
71
+ color: var(--active, #111827);
72
+ margin-bottom: 4px;
73
+ }
74
+
75
+ input {
76
+ width: 100%;
77
+ padding: 10px 12px;
78
+ border: 1px solid var(--inactive, #d1d5db);
79
+ border-radius: 6px;
80
+ font-size: 14px;
81
+ background: var(--body-bg-card, #fff);
82
+ color: var(--active, #111827);
83
+
84
+ &::placeholder { color: var(--inactive, #9ca3af); }
85
+ &:focus { outline: none; border-color: #4f46e5; }
86
+ &.has-error { border-color: #ee3831; }
87
+ }
88
+
89
+ .error-text {
90
+ display: block;
91
+ color: #ee3831;
92
+ font-size: 12px;
93
+ margin-top: 4px;
94
+ }
95
+ }
96
+ }
97
+ </style>
@@ -0,0 +1,162 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, watch } from "vue";
3
+ import { usePaymentMethods } from "../../composables/usePaymentMethods";
4
+ import type { AxiosInstance } from "axios";
5
+
6
+ const props = defineProps<{
7
+ httpService: AxiosInstance;
8
+ context?: "checkout" | "subscription";
9
+ modelValue?: string;
10
+ }>();
11
+
12
+ const emit = defineEmits<{
13
+ "update:modelValue": [value: string];
14
+ }>();
15
+
16
+ const {
17
+ methods,
18
+ selectedMethod,
19
+ loading,
20
+ fetchMethods,
21
+ } = usePaymentMethods(props.httpService);
22
+
23
+ onMounted(() => {
24
+ fetchMethods(props.context ?? "checkout");
25
+ });
26
+
27
+ watch(
28
+ () => props.modelValue,
29
+ (val) => {
30
+ if (val && val !== selectedMethod.value) {
31
+ selectedMethod.value = val.toLowerCase();
32
+ }
33
+ },
34
+ { immediate: true }
35
+ );
36
+
37
+ watch(selectedMethod, (val) => {
38
+ emit("update:modelValue", val);
39
+ });
40
+
41
+ function select(name: string) {
42
+ selectedMethod.value = name.toLowerCase();
43
+ }
44
+
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";
53
+ }
54
+
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;
63
+ }
64
+ </script>
65
+
66
+ <template>
67
+ <div class="mg-payment-methods">
68
+ <div v-if="loading" class="mg-payment-methods__loading">
69
+ <div class="skeleton-block" style="width: 100%; height: 48px"></div>
70
+ </div>
71
+
72
+ <div v-else-if="methods.length === 0" class="mg-payment-methods__empty">
73
+ <small>{{ $t?.("checkout.no_payment_methods") ?? "No payment methods available" }}</small>
74
+ </div>
75
+
76
+ <div v-else class="mg-payment-methods__list">
77
+ <button
78
+ v-for="method in methods"
79
+ :key="method.id"
80
+ type="button"
81
+ class="mg-payment-method"
82
+ :class="{ 'mg-payment-method--active': selectedMethod === method.name.toLowerCase() }"
83
+ @click="select(method.name)"
84
+ >
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
+ </button>
92
+ </div>
93
+ </div>
94
+ </template>
95
+
96
+ <style lang="scss" scoped>
97
+ .mg-payment-methods {
98
+ width: 100%;
99
+
100
+ &__list {
101
+ display: flex;
102
+ gap: 8px;
103
+ flex-wrap: wrap;
104
+ }
105
+
106
+ &__loading,
107
+ &__empty {
108
+ display: flex;
109
+ align-items: center;
110
+ justify-content: center;
111
+ min-height: 48px;
112
+ color: var(--inactive, #6b7280);
113
+ }
114
+ }
115
+
116
+ .mg-payment-method {
117
+ display: flex;
118
+ align-items: center;
119
+ gap: 8px;
120
+ padding: 12px 16px;
121
+ border: 2px solid var(--inactive, #e5e7eb);
122
+ border-radius: 8px;
123
+ background: transparent;
124
+ cursor: pointer;
125
+ transition: all 0.2s ease;
126
+ flex: 1;
127
+ min-width: 120px;
128
+ justify-content: center;
129
+
130
+ &:hover {
131
+ border-color: var(--active, #9ca3af);
132
+ }
133
+
134
+ &--active {
135
+ border-color: #4f46e5;
136
+ background-color: rgba(79, 70, 229, 0.05);
137
+ }
138
+
139
+ &__logo {
140
+ height: 20px;
141
+ width: auto;
142
+ object-fit: contain;
143
+ }
144
+
145
+ &__label {
146
+ font-size: 14px;
147
+ font-weight: 500;
148
+ color: var(--active, #111827);
149
+ }
150
+ }
151
+
152
+ .skeleton-block {
153
+ background: var(--inactive, #e5e7eb);
154
+ border-radius: 8px;
155
+ animation: pulse 1.5s ease-in-out infinite;
156
+ }
157
+
158
+ @keyframes pulse {
159
+ 0%, 100% { opacity: 1; }
160
+ 50% { opacity: 0.5; }
161
+ }
162
+ </style>
@@ -0,0 +1,222 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted, onUnmounted } from "vue";
3
+
4
+ const props = defineProps<{
5
+ qrCode: string;
6
+ qrCodeBase64: string;
7
+ expiresAt: string;
8
+ pollingUrl?: string;
9
+ pollingInterval?: number;
10
+ httpService?: any;
11
+ }>();
12
+
13
+ const emit = defineEmits<{
14
+ paid: [];
15
+ expired: [];
16
+ }>();
17
+
18
+ const copied = ref(false);
19
+ const remainingSeconds = ref(0);
20
+ let countdownTimer: ReturnType<typeof setInterval> | null = null;
21
+ let pollingTimer: ReturnType<typeof setInterval> | null = null;
22
+
23
+ const formattedTime = computed(() => {
24
+ const mins = Math.floor(remainingSeconds.value / 60);
25
+ const secs = remainingSeconds.value % 60;
26
+ return `${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
27
+ });
28
+
29
+ const isExpired = computed(() => remainingSeconds.value <= 0);
30
+
31
+ async function copyCode() {
32
+ try {
33
+ await navigator.clipboard.writeText(props.qrCode);
34
+ copied.value = true;
35
+ setTimeout(() => (copied.value = false), 3000);
36
+ } catch {
37
+ // Fallback
38
+ const input = document.createElement("input");
39
+ input.value = props.qrCode;
40
+ document.body.appendChild(input);
41
+ input.select();
42
+ document.execCommand("copy");
43
+ document.body.removeChild(input);
44
+ copied.value = true;
45
+ setTimeout(() => (copied.value = false), 3000);
46
+ }
47
+ }
48
+
49
+ function startCountdown() {
50
+ const expiresDate = new Date(props.expiresAt).getTime();
51
+ const updateRemaining = () => {
52
+ const now = Date.now();
53
+ const diff = Math.max(0, Math.floor((expiresDate - now) / 1000));
54
+ remainingSeconds.value = diff;
55
+ if (diff <= 0) {
56
+ if (countdownTimer) clearInterval(countdownTimer);
57
+ if (pollingTimer) clearInterval(pollingTimer);
58
+ emit("expired");
59
+ }
60
+ };
61
+ updateRemaining();
62
+ countdownTimer = setInterval(updateRemaining, 1000);
63
+ }
64
+
65
+ function startPolling() {
66
+ if (!props.pollingUrl || !props.httpService) return;
67
+ const interval = props.pollingInterval ?? 5000;
68
+ pollingTimer = setInterval(async () => {
69
+ try {
70
+ const res = await props.httpService.get(props.pollingUrl);
71
+ const status = res.data?.data?.status ?? res.data?.status;
72
+ if (status === "paid" || status === "approved") {
73
+ if (pollingTimer) clearInterval(pollingTimer);
74
+ if (countdownTimer) clearInterval(countdownTimer);
75
+ emit("paid");
76
+ }
77
+ } catch {
78
+ // Silent retry
79
+ }
80
+ }, interval);
81
+ }
82
+
83
+ onMounted(() => {
84
+ startCountdown();
85
+ startPolling();
86
+ });
87
+
88
+ onUnmounted(() => {
89
+ if (countdownTimer) clearInterval(countdownTimer);
90
+ if (pollingTimer) clearInterval(pollingTimer);
91
+ });
92
+ </script>
93
+
94
+ <template>
95
+ <div class="mg-pix">
96
+ <div class="mg-pix__header">
97
+ <img src="/imgs/payments/pix.svg" alt="Pix" class="mg-pix__logo" />
98
+ <span class="mg-pix__title">{{ $t?.("checkout.pix.title") ?? "Pay with Pix" }}</span>
99
+ </div>
100
+
101
+ <div v-if="!isExpired" class="mg-pix__body">
102
+ <div class="mg-pix__qr">
103
+ <img :src="`data:image/png;base64,${qrCodeBase64}`" alt="QR Code Pix" />
104
+ </div>
105
+
106
+ <div class="mg-pix__timer">
107
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
108
+ <circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
109
+ </svg>
110
+ <span>{{ formattedTime }}</span>
111
+ </div>
112
+
113
+ <button class="mg-pix__copy" @click="copyCode">
114
+ <template v-if="copied">
115
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2">
116
+ <polyline points="20 6 9 17 4 12" />
117
+ </svg>
118
+ {{ $t?.("checkout.pix.copied") ?? "Copied!" }}
119
+ </template>
120
+ <template v-else>
121
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
122
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
123
+ </svg>
124
+ {{ $t?.("checkout.pix.copy_code") ?? "Copy Pix code" }}
125
+ </template>
126
+ </button>
127
+
128
+ <p class="mg-pix__instructions">
129
+ {{ $t?.("checkout.pix.instructions") ?? "Open your banking app, choose Pix, and scan the QR code or paste the code above." }}
130
+ </p>
131
+ </div>
132
+
133
+ <div v-else class="mg-pix__expired">
134
+ <p>{{ $t?.("checkout.pix.expired") ?? "This Pix code has expired. Please try again." }}</p>
135
+ </div>
136
+ </div>
137
+ </template>
138
+
139
+ <style lang="scss" scoped>
140
+ .mg-pix {
141
+ display: flex;
142
+ flex-direction: column;
143
+ align-items: center;
144
+ padding: 24px;
145
+ border: 1px solid var(--inactive, #e5e7eb);
146
+ border-radius: 12px;
147
+ background: var(--body-bg-card, #fff);
148
+
149
+ &__header {
150
+ display: flex;
151
+ align-items: center;
152
+ gap: 8px;
153
+ margin-bottom: 20px;
154
+ }
155
+
156
+ &__logo {
157
+ height: 24px;
158
+ }
159
+
160
+ &__title {
161
+ font-size: 16px;
162
+ font-weight: 600;
163
+ color: var(--active, #111827);
164
+ }
165
+
166
+ &__qr {
167
+ width: 200px;
168
+ height: 200px;
169
+ margin-bottom: 16px;
170
+
171
+ img {
172
+ width: 100%;
173
+ height: 100%;
174
+ object-fit: contain;
175
+ }
176
+ }
177
+
178
+ &__timer {
179
+ display: flex;
180
+ align-items: center;
181
+ gap: 6px;
182
+ font-size: 14px;
183
+ font-weight: 600;
184
+ color: var(--active, #111827);
185
+ margin-bottom: 16px;
186
+ }
187
+
188
+ &__copy {
189
+ display: flex;
190
+ align-items: center;
191
+ gap: 8px;
192
+ padding: 10px 20px;
193
+ border: 1px solid #4f46e5;
194
+ border-radius: 8px;
195
+ background: transparent;
196
+ color: #4f46e5;
197
+ font-size: 14px;
198
+ font-weight: 500;
199
+ cursor: pointer;
200
+ transition: all 0.2s;
201
+ margin-bottom: 16px;
202
+
203
+ &:hover {
204
+ background: rgba(79, 70, 229, 0.05);
205
+ }
206
+ }
207
+
208
+ &__instructions {
209
+ font-size: 12px;
210
+ color: var(--inactive, #6b7280);
211
+ text-align: center;
212
+ max-width: 280px;
213
+ line-height: 1.5;
214
+ }
215
+
216
+ &__expired {
217
+ text-align: center;
218
+ color: #ee3831;
219
+ font-size: 14px;
220
+ }
221
+ }
222
+ </style>