@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.
@@ -0,0 +1,415 @@
1
+ <template>
2
+ <div class="mg-confirmation">
3
+ <!-- Section 1: Status -->
4
+ <div class="mg-confirmation__status-card">
5
+ <h2>
6
+ <slot v-if="displayStatus === 'paid'" name="title-paid">{{ $t("confirmation_page.section_1.title") }}</slot>
7
+ <slot v-else-if="displayStatus === 'pending'" name="title-pending">{{ $t("confirmation_page.section_1.title_pending") }}</slot>
8
+ <slot v-else name="title-failed">{{ $t("confirmation_page.section_1.title_failed") }}</slot>
9
+ </h2>
10
+
11
+ <div class="mg-confirmation__status-card__desc">
12
+ <template v-if="displayStatus === 'paid'">
13
+ <slot name="desc-paid">
14
+ {{ $t("confirmation_page.section_1.description_1") }}<br />
15
+ {{ $t("confirmation_page.section_1.description_2") }}
16
+ {{ cartData.user?.email ?? cartData.user_email }}
17
+ {{ $t("confirmation_page.section_1.description_3") }}
18
+ </slot>
19
+ </template>
20
+ <template v-else-if="displayStatus === 'pending'">
21
+ <slot name="desc-pending">
22
+ {{ $t("confirmation_page.section_1.description_pending") }}
23
+ <p v-if="processingTooLong" class="mg-confirmation__too-long">
24
+ {{ $t("confirmation_page.section_1.processing_too_long") }}
25
+ </p>
26
+ </slot>
27
+ </template>
28
+ <template v-else>
29
+ <slot name="desc-failed">{{ $t("confirmation_page.section_1.description_failed") }}</slot>
30
+ </template>
31
+ </div>
32
+
33
+ <!-- Status badge -->
34
+ <div class="mg-confirmation__status-badge" :class="displayStatus">
35
+ <span v-if="displayStatus === 'pending'" class="mg-confirmation__loader" />
36
+ <svg v-else-if="displayStatus === 'paid'" xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none">
37
+ <path d="M15.1405 26.0394L8.81875 19.7176L6.66602 21.8552L15.1405 30.3297L33.3327 12.1376L31.1951 10L15.1405 26.0394Z" fill="var(--text-primary-color, #fff)" />
38
+ </svg>
39
+ <svg v-else width="40" height="41" viewBox="0 0 40 41" fill="none" xmlns="http://www.w3.org/2000/svg">
40
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M17.3696 20.4707L8.82244 11.9395L11.1043 9.65332L19.6696 18.2026L28.2008 9.65545L30.487 11.9373L21.9376 20.5026L30.4848 29.0338L28.203 31.32L19.6377 22.7707L11.1065 31.3179L8.82031 29.036L17.3696 20.4707Z" fill="#fff" />
41
+ </svg>
42
+ <span>{{ statusLabel }}</span>
43
+ </div>
44
+
45
+ <!-- Actions -->
46
+ <div class="mg-confirmation__actions">
47
+ <template v-if="displayStatus === 'pending'">
48
+ <span class="mg-confirmation__actions__hint">
49
+ {{ $t("confirmation_page.section_1.obs") }}
50
+ <a v-if="cartData.payment_link" :href="cartData.payment_link" target="_blank" class="mg-confirmation__link">
51
+ {{ $t("confirmation_page.section_1.retry_payment") }}
52
+ </a>
53
+ </span>
54
+ </template>
55
+ <template v-if="displayStatus === 'failed' || displayStatus === 'canceled'">
56
+ <a v-if="cartData.payment_link" :href="cartData.payment_link" target="_blank" class="mg-confirmation__btn-retry">
57
+ {{ $t("confirmation_page.section_1.retry_payment") }}
58
+ </a>
59
+ <button class="mg-confirmation__btn-back" @click="$emit('back-to-pricing')">
60
+ {{ $t("confirmation_page.back_to_cart") }}
61
+ </button>
62
+ </template>
63
+ </div>
64
+ </div>
65
+
66
+ <!-- Section 2: CTA / Guest slot -->
67
+ <slot name="cta" />
68
+
69
+ <!-- Section 2 default: guest create account -->
70
+ <div v-if="!signedIn && !$slots.cta" class="mg-confirmation__guest-card">
71
+ <div>
72
+ <h4>
73
+ {{ $t("confirmation_page.section_2.offline.title1") }}
74
+ <span>{{ $t("confirmation_page.section_2.offline.title2") }}</span>
75
+ </h4>
76
+ <p>{{ $t("confirmation_page.section_2.offline.text") }}</p>
77
+ </div>
78
+ <button class="btn primary" @click="$emit('create-account')">
79
+ {{ $t("confirmation_page.section_2.offline.create_account") }}
80
+ </button>
81
+ </div>
82
+
83
+ <!-- Section 3: Order summary -->
84
+ <div v-if="showOrderSummary" class="mg-confirmation__summary">
85
+ <h4>{{ $t("confirmation_page.section_3.title") }}</h4>
86
+ <div class="mg-confirmation__summary__cards">
87
+ <!-- Items card -->
88
+ <div class="mg-confirmation__summary__card">
89
+ <div v-if="cartItems.length" class="mg-confirmation__summary__items">
90
+ <div v-for="(item, i) in cartItems" :key="i" class="mg-confirmation__summary__item">
91
+ <span>{{ item.item?.localized_name ?? item.item?.name ?? $t("confirmation_page.section_3.item") }}</span>
92
+ <span>{{ cartData.currency?.data?.symbol }}{{ item.price }}</span>
93
+ </div>
94
+ </div>
95
+ <!-- Coupon / totals -->
96
+ <div v-if="cartData.discount_coupon" class="mg-confirmation__summary__coupon">
97
+ {{ $t("confirmation_page.section_3.text_2") }} {{ cartData.discount_coupon.code }}
98
+ </div>
99
+ <div v-if="cartData.discount_coupon" class="mg-confirmation__summary__discount">
100
+ <s>{{ cartData.formatted_original_total }}</s>
101
+ - {{ cartData.formatted_total_discount }}
102
+ </div>
103
+ <div class="mg-confirmation__summary__total">
104
+ <span>{{ $t("confirmation_page.section_3.text_3") }} {{ cartData.formatted_total ?? cartData.formatted_total_with_discount }}</span>
105
+ </div>
106
+ </div>
107
+
108
+ <!-- Meta card -->
109
+ <div class="mg-confirmation__summary__card">
110
+ <div v-if="cartData.reference" class="mg-confirmation__summary__meta-row">
111
+ {{ $t("confirmation_page.section_3.text_4") }}
112
+ <span>{{ cartData.reference }}</span>
113
+ </div>
114
+ <div v-if="displayStatus === 'paid' && (cartData.paid_at || cartData.created_at)" class="mg-confirmation__summary__meta-row">
115
+ {{ $t("confirmation_page.section_3.text_5") }}
116
+ <span>{{ formatDate(cartData.paid_at ?? cartData.created_at) }}</span>
117
+ </div>
118
+ <div v-if="hasKnownPaymentMethod" class="mg-confirmation__summary__meta-row">
119
+ {{ $t("confirmation_page.section_3.text_6") }}:
120
+ <span>
121
+ <slot name="payment-method-icon" :method-id="paymentMethodId">
122
+ <!-- Fallback: show gateway name -->
123
+ {{ paymentMethodId === 3 ? 'Stripe' : 'PayPal' }}
124
+ </slot>
125
+ </span>
126
+ </div>
127
+ <slot name="meta-extra" />
128
+ </div>
129
+ </div>
130
+ </div>
131
+
132
+ <!-- Custom extra sections -->
133
+ <slot name="extra" />
134
+ </div>
135
+ </template>
136
+
137
+ <script setup lang="ts">
138
+ import type { PropType } from "vue";
139
+ import type { ConfirmationInvoice, OrderStatus } from "../../composables/useConfirmation";
140
+
141
+ const props = defineProps({
142
+ displayStatus: {
143
+ type: String as PropType<OrderStatus>,
144
+ default: "",
145
+ },
146
+ cartData: {
147
+ type: Object as PropType<ConfirmationInvoice>,
148
+ default: () => ({}),
149
+ },
150
+ processingTooLong: {
151
+ type: Boolean,
152
+ default: false,
153
+ },
154
+ paymentMethodId: {
155
+ type: Number,
156
+ default: 2,
157
+ },
158
+ hasKnownPaymentMethod: {
159
+ type: Boolean,
160
+ default: false,
161
+ },
162
+ signedIn: {
163
+ type: Boolean,
164
+ default: false,
165
+ },
166
+ showOrderSummary: {
167
+ type: Boolean,
168
+ default: true,
169
+ },
170
+ });
171
+
172
+ defineEmits<{
173
+ "back-to-pricing": [];
174
+ "create-account": [];
175
+ }>();
176
+
177
+ const { $i18n } = useNuxtApp();
178
+ const $t = $i18n.t;
179
+
180
+ const cartItems = computed(() => {
181
+ const items = props.cartData?.items;
182
+ if (!items) return [];
183
+ if (Array.isArray(items)) return items;
184
+ return (items as any).data ?? [];
185
+ });
186
+
187
+ const statusLabel = computed(() => {
188
+ const map: Record<string, string> = {
189
+ paid: $t("confirmation_page.section_1.confirmation"),
190
+ pending: $t("confirmation_page.section_1.processing"),
191
+ failed: $t("confirmation_page.section_1.failed"),
192
+ canceled: $t("confirmation_page.section_1.canceled"),
193
+ };
194
+ return map[props.displayStatus] ?? "";
195
+ });
196
+
197
+ function formatDate(raw?: string): string {
198
+ if (!raw) return "";
199
+ try {
200
+ return new Date(raw).toLocaleString($i18n.locale.value, {
201
+ year: "numeric", month: "short", day: "numeric",
202
+ hour: "2-digit", minute: "2-digit",
203
+ });
204
+ } catch { return raw; }
205
+ }
206
+ </script>
207
+
208
+ <style lang="scss" scoped>
209
+ .mg-confirmation {
210
+ display: flex;
211
+ flex-direction: column;
212
+ gap: 2rem;
213
+
214
+ &__status-card {
215
+ display: flex;
216
+ flex-direction: column;
217
+ gap: 1.5rem;
218
+ padding: 2.5rem;
219
+ background: var(--body-bg-card, var(--card-article-bg, #1a1a2e));
220
+
221
+ h2 {
222
+ font-size: 1.75rem;
223
+ font-weight: 600;
224
+ color: var(--active, var(--text-primary-color, #fff));
225
+ }
226
+ }
227
+
228
+ &__status-card__desc {
229
+ font-size: 1rem;
230
+ line-height: 1.5;
231
+ color: var(--active, var(--text-primary-color, #ccc));
232
+ }
233
+
234
+ &__too-long {
235
+ font-size: 0.75rem;
236
+ color: var(--secondary-info-fg, #999);
237
+ margin-top: 0.5rem;
238
+ }
239
+
240
+ &__status-badge {
241
+ display: flex;
242
+ align-items: center;
243
+ gap: 1rem;
244
+ padding: 1.5rem;
245
+ font-size: 1.125rem;
246
+ font-weight: 600;
247
+ color: #fff;
248
+
249
+ &.paid { background: #1f7700; }
250
+ &.pending { background: #ffd600; color: #000; }
251
+ &.failed,
252
+ &.canceled { background: #a22a17; }
253
+ }
254
+
255
+ &__loader {
256
+ display: inline-block;
257
+ width: 20px;
258
+ height: 20px;
259
+ border-radius: 50%;
260
+ animation: mg-rotate 1s linear infinite;
261
+ position: relative;
262
+
263
+ &::before {
264
+ content: "";
265
+ box-sizing: border-box;
266
+ position: absolute;
267
+ inset: 0;
268
+ border-radius: 50%;
269
+ border: 2px solid currentColor;
270
+ animation: mg-clip 2s linear infinite;
271
+ }
272
+ }
273
+
274
+ &__actions {
275
+ display: flex;
276
+ flex-direction: column;
277
+ align-items: center;
278
+ gap: 0.75rem;
279
+
280
+ &__hint {
281
+ font-size: 0.8125rem;
282
+ color: var(--secondary-info-fg, #999);
283
+ text-align: center;
284
+ }
285
+ }
286
+
287
+ &__link {
288
+ color: var(--highlight-color, #1c5d6f);
289
+ text-decoration: underline;
290
+ cursor: pointer;
291
+ }
292
+
293
+ &__btn-retry {
294
+ display: inline-flex;
295
+ align-items: center;
296
+ justify-content: center;
297
+ padding: 10px 24px;
298
+ background: var(--highlight-color, #1c5d6f);
299
+ color: #fff;
300
+ border: 1px solid var(--highlight-color, #1c5d6f);
301
+ font-size: 0.875rem;
302
+ cursor: pointer;
303
+ text-decoration: none;
304
+ transition: opacity 0.2s;
305
+
306
+ &:hover { opacity: 0.85; }
307
+ }
308
+
309
+ &__btn-back {
310
+ display: inline-flex;
311
+ align-items: center;
312
+ justify-content: center;
313
+ padding: 10px 24px;
314
+ background: transparent;
315
+ color: var(--highlight-color, #1c5d6f);
316
+ border: 1px solid var(--highlight-color, #1c5d6f);
317
+ font-size: 0.875rem;
318
+ cursor: pointer;
319
+ transition: all 0.2s;
320
+
321
+ &:hover { background: var(--highlight-color, #1c5d6f); color: #fff; }
322
+ }
323
+
324
+ &__guest-card {
325
+ display: flex;
326
+ justify-content: space-between;
327
+ align-items: center;
328
+ gap: 1.5rem;
329
+ padding: 1.5rem 2.5rem;
330
+ background: var(--body-bg-card, #1a1a2e);
331
+ border-bottom: 2px solid var(--highlight-color, #1c5d6f);
332
+
333
+ h4 { font-size: 1.125rem; font-weight: 600; color: var(--active, #fff); span { color: var(--highlight-color); } }
334
+ p { font-size: 0.75rem; color: var(--secondary-info-fg, #999); margin: 0.25rem 0 0; }
335
+ }
336
+
337
+ &__summary {
338
+ display: flex;
339
+ flex-direction: column;
340
+ gap: 1.5rem;
341
+
342
+ h4 {
343
+ font-size: 1.125rem;
344
+ font-weight: 600;
345
+ color: var(--active, #fff);
346
+ padding-left: 1rem;
347
+ border-left: 2px solid var(--highlight-color, #1c5d6f);
348
+ }
349
+
350
+ &__cards {
351
+ display: flex;
352
+ gap: 0.5rem;
353
+ align-items: flex-start;
354
+
355
+ @media (max-width: 768px) { flex-direction: column; }
356
+ }
357
+
358
+ &__card {
359
+ flex: 1;
360
+ padding: 2.5rem;
361
+ background: var(--body-bg-card, #1a1a2e);
362
+ display: flex;
363
+ flex-direction: column;
364
+ gap: 1rem;
365
+ }
366
+
367
+ &__item {
368
+ display: flex;
369
+ justify-content: space-between;
370
+ font-size: 1rem;
371
+ color: var(--active, #fff);
372
+ padding-bottom: 1rem;
373
+ border-bottom: 1px solid #808080;
374
+ }
375
+
376
+ &__coupon,
377
+ &__discount {
378
+ font-size: 0.875rem;
379
+ color: var(--secondary-info-fg, #999);
380
+ text-align: right;
381
+ }
382
+
383
+ &__total {
384
+ text-align: right;
385
+ span {
386
+ font-size: 1.5rem;
387
+ font-weight: 600;
388
+ color: var(--highlight-color, #1c5d6f);
389
+ }
390
+ }
391
+
392
+ &__meta-row {
393
+ font-size: 1rem;
394
+ font-weight: 600;
395
+ color: var(--active, #fff);
396
+ display: flex;
397
+ align-items: center;
398
+ gap: 0.25rem;
399
+ padding-bottom: 1.25rem;
400
+ border-bottom: 1px solid #808080;
401
+
402
+ span { font-size: 0.875rem; font-weight: 400; color: var(--secondary-info-fg, #999); }
403
+ }
404
+ }
405
+ }
406
+
407
+ @keyframes mg-rotate { 100% { transform: rotate(360deg); } }
408
+ @keyframes mg-clip {
409
+ 0% { clip-path: polygon(50% 50%, 0 0, 0 0, 0 0, 0 0, 0 0); }
410
+ 25% { clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 0, 100% 0, 100% 0); }
411
+ 50% { clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 100% 100%, 100% 100%); }
412
+ 75% { clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 100%); }
413
+ 100% { clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 0); }
414
+ }
415
+ </style>
@@ -22,7 +22,19 @@ export interface ConfirmationInvoice {
22
22
  [key: string]: any;
23
23
  }
24
24
 
25
- export function useConfirmation(fetchInvoice: (id: string) => Promise<ConfirmationInvoice>) {
25
+ export interface UseConfirmationOptions {
26
+ /** Called once when displayStatus transitions to "paid". Use to refresh subscription/auth. */
27
+ onPaid?: () => Promise<void> | void;
28
+ /** Called once when displayStatus transitions to "failed". */
29
+ onFailed?: () => Promise<void> | void;
30
+ /** Called once when displayStatus transitions to "canceled". */
31
+ onCanceled?: () => Promise<void> | void;
32
+ }
33
+
34
+ export function useConfirmation(
35
+ fetchInvoice: (id: string) => Promise<ConfirmationInvoice>,
36
+ options?: UseConfirmationOptions,
37
+ ) {
26
38
  const invoiceId = ref<string>("");
27
39
  const cartData = ref<ConfirmationInvoice>({});
28
40
  const displayStatus = ref<OrderStatus>("");
@@ -30,6 +42,16 @@ export function useConfirmation(fetchInvoice: (id: string) => Promise<Confirmati
30
42
 
31
43
  const LONG_PENDING_MS = 60_000;
32
44
  let longPendingTimer: ReturnType<typeof setTimeout> | null = null;
45
+ let _paidFired = false;
46
+
47
+ async function _firePaid() {
48
+ if (_paidFired) return;
49
+ _paidFired = true;
50
+ displayStatus.value = "paid";
51
+ if (options?.onPaid) {
52
+ try { await options.onPaid(); } catch { /* silent — don't block UI */ }
53
+ }
54
+ }
33
55
 
34
56
  // PayPal = 2, Stripe = 3
35
57
  const paymentMethodId = computed(() => {
@@ -62,8 +84,13 @@ export function useConfirmation(fetchInvoice: (id: string) => Promise<Confirmati
62
84
  try {
63
85
  const data = await fetchInvoice(invoiceId.value);
64
86
  cartData.value = data;
65
- displayStatus.value = (data.status as OrderStatus) ?? "";
66
- return data.status !== "pending";
87
+ const status = (data.status as OrderStatus) ?? "";
88
+ if (status === "paid") {
89
+ await _firePaid();
90
+ } else {
91
+ displayStatus.value = status;
92
+ }
93
+ return status !== "pending" && status !== "";
67
94
  } catch {
68
95
  return false;
69
96
  }
@@ -79,15 +106,17 @@ export function useConfirmation(fetchInvoice: (id: string) => Promise<Confirmati
79
106
  const { cancelSafetyNets } = usePaymentListener(
80
107
  invoiceId,
81
108
  () => getInvoiceData(),
82
- () => {
109
+ async () => {
83
110
  cancelSafetyNets();
84
111
  cancelLongPendingTimer();
85
112
  displayStatus.value = "failed";
113
+ if (options?.onFailed) try { await options.onFailed(); } catch { /* silent */ }
86
114
  },
87
- () => {
115
+ async () => {
88
116
  cancelSafetyNets();
89
117
  cancelLongPendingTimer();
90
118
  displayStatus.value = "canceled";
119
+ if (options?.onCanceled) try { await options.onCanceled(); } catch { /* silent */ }
91
120
  },
92
121
  );
93
122