@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.
- package/components/checkout/MgCartItemList.vue +94 -43
- package/components/checkout/MgCartSummary.vue +100 -137
- package/components/checkout/MgCheckoutSidebar.vue +0 -2
- package/components/checkout/MgCouponInput.vue +211 -0
- package/components/checkout/MgGuestEmailForm.vue +0 -2
- package/components/checkout/MgPaymentMethodSelector.vue +17 -16
- package/components/checkout/MgPixQRCode.vue +0 -2
- package/components/ui/MgConfirmationPage.vue +415 -0
- package/composables/useConfirmation.ts +34 -5
- package/composables/useMgCheckout.ts +398 -166
- package/composables/usePaymentMethods.ts +3 -2
- package/package.json +2 -2
|
@@ -1,85 +1,111 @@
|
|
|
1
|
-
import { ref,
|
|
2
|
-
import
|
|
1
|
+
import { ref, computed, watch, nextTick, provide, inject } from "vue";
|
|
2
|
+
import { storeToRefs } from "pinia";
|
|
3
3
|
import { usePaymentMethods } from "./usePaymentMethods";
|
|
4
|
-
import type {
|
|
4
|
+
import type { AxiosInstance } from "axios";
|
|
5
|
+
|
|
6
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
5
7
|
|
|
6
8
|
export interface CartItem {
|
|
7
9
|
id: number;
|
|
8
10
|
item_id: number;
|
|
9
11
|
item_type: string;
|
|
10
12
|
quantity: number;
|
|
11
|
-
price: number;
|
|
12
|
-
price_with_discount
|
|
13
|
-
discount
|
|
14
|
-
currency_id
|
|
15
|
-
item
|
|
13
|
+
price: number | string;
|
|
14
|
+
price_with_discount?: number | string;
|
|
15
|
+
discount?: number;
|
|
16
|
+
currency_id?: number;
|
|
17
|
+
item?: {
|
|
16
18
|
id: number;
|
|
17
|
-
name
|
|
19
|
+
name?: string;
|
|
20
|
+
localized_name?: string;
|
|
18
21
|
description?: string;
|
|
19
22
|
slug?: string;
|
|
20
23
|
image_url?: string;
|
|
24
|
+
products?: any[];
|
|
21
25
|
};
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export interface CartData {
|
|
25
|
-
items
|
|
26
|
-
original_total
|
|
27
|
-
total_with_discount
|
|
29
|
+
items?: { data?: CartItem[] } | CartItem[];
|
|
30
|
+
original_total?: number;
|
|
31
|
+
total_with_discount?: number;
|
|
28
32
|
total_discount?: number;
|
|
29
33
|
formatted_original_total?: string;
|
|
30
34
|
formatted_total_with_discount?: string;
|
|
35
|
+
formatted_total?: string;
|
|
31
36
|
formatted_total_discount?: string;
|
|
32
|
-
discount_coupon?:
|
|
37
|
+
discount_coupon?: { code: string; discount?: number };
|
|
33
38
|
discount_coupon_code?: string;
|
|
39
|
+
currency?: { data?: { code?: string; symbol?: string } };
|
|
40
|
+
user_email?: string;
|
|
41
|
+
[key: string]: any;
|
|
34
42
|
}
|
|
35
43
|
|
|
36
|
-
export interface
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
export interface PlanSelection {
|
|
45
|
+
id: number;
|
|
46
|
+
name?: string;
|
|
47
|
+
slug?: string;
|
|
48
|
+
[key: string]: any;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface PlanPricingSelection {
|
|
52
|
+
id: number;
|
|
53
|
+
value?: number;
|
|
54
|
+
payment_period_id?: number;
|
|
55
|
+
[key: string]: any;
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
const CHECKOUT_KEY = "mg-checkout-instance";
|
|
48
59
|
|
|
49
|
-
export function
|
|
50
|
-
|
|
60
|
+
export function provideMgCheckout(instance: ReturnType<typeof useMgCheckout>) {
|
|
61
|
+
provide(CHECKOUT_KEY, instance);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function injectMgCheckout(): ReturnType<typeof useMgCheckout> | undefined {
|
|
65
|
+
return inject<ReturnType<typeof useMgCheckout>>(CHECKOUT_KEY);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Composable ───────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
export function useMgCheckout(httpService: AxiosInstance, options?: {
|
|
71
|
+
buyerPageName?: string;
|
|
72
|
+
successPath?: string; // e.g. "/confirmation"
|
|
73
|
+
cancelPath?: string; // e.g. "/pricing"
|
|
74
|
+
}) {
|
|
75
|
+
const nuxtApp = useNuxtApp();
|
|
76
|
+
const { locale } = useI18n();
|
|
77
|
+
const router = useRouter();
|
|
78
|
+
const authStore = useAuthStore();
|
|
79
|
+
const { signedIn } = storeToRefs(authStore);
|
|
51
80
|
const paymentMethods = usePaymentMethods(httpService);
|
|
52
81
|
|
|
53
|
-
|
|
54
|
-
const
|
|
82
|
+
const buyerPageName = options?.buyerPageName ?? "";
|
|
83
|
+
const successPath = options?.successPath ?? "/confirmation";
|
|
84
|
+
const cancelPath = options?.cancelPath ?? "/pricing";
|
|
85
|
+
|
|
86
|
+
// ── Cart ──────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
const cart = ref<CartData>({});
|
|
55
89
|
const cartLoading = ref(false);
|
|
56
90
|
const cartError = ref<string | null>(null);
|
|
57
91
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
92
|
+
const getCartItems = computed((): CartItem[] => {
|
|
93
|
+
const items = cart.value?.items;
|
|
94
|
+
if (!items) return [];
|
|
95
|
+
if (Array.isArray(items)) return items;
|
|
96
|
+
return items.data ?? [];
|
|
97
|
+
});
|
|
62
98
|
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
const emailError = ref("");
|
|
99
|
+
const hasItems = computed(() => getCartItems.value.length > 0 || !!selectedPlan.value);
|
|
100
|
+
const hasDiscount = computed(() => !!cart.value?.total_discount && cart.value.total_discount !== 0);
|
|
66
101
|
|
|
67
|
-
// Terms
|
|
68
|
-
const termsAccepted = ref(false);
|
|
69
|
-
|
|
70
|
-
// Coupon
|
|
71
|
-
const couponCode = ref("");
|
|
72
|
-
const couponLoading = ref(false);
|
|
73
|
-
const couponError = ref<string | null>(null);
|
|
74
|
-
const couponInfo = ref<any>(null);
|
|
75
|
-
|
|
76
|
-
// Cart operations
|
|
77
102
|
async function fetchCart() {
|
|
78
103
|
cartLoading.value = true;
|
|
79
104
|
cartError.value = null;
|
|
80
105
|
try {
|
|
81
|
-
const res = await httpService.get("/shopping-cart");
|
|
82
|
-
cart.value = res.data?.data ??
|
|
106
|
+
const res = await httpService.get("api/v1/shopping-cart");
|
|
107
|
+
cart.value = res.data?.data ?? {};
|
|
108
|
+
_syncCartState(cart.value);
|
|
83
109
|
} catch (e: any) {
|
|
84
110
|
cartError.value = e?.message ?? "Failed to load cart";
|
|
85
111
|
} finally {
|
|
@@ -87,58 +113,85 @@ export function useMgCheckout(httpService: AxiosInstance) {
|
|
|
87
113
|
}
|
|
88
114
|
}
|
|
89
115
|
|
|
116
|
+
function _syncCartState(data: CartData) {
|
|
117
|
+
const items = Array.isArray(data.items) ? data.items : (data.items?.data ?? []);
|
|
118
|
+
orderCart.value = [...items].sort((a: any, b: any) => {
|
|
119
|
+
if (a.item_type < b.item_type) return -1;
|
|
120
|
+
if (a.item_type > b.item_type) return 1;
|
|
121
|
+
return (a.item_id ?? 0) - (b.item_id ?? 0);
|
|
122
|
+
});
|
|
123
|
+
orderCartDiscount.value = data.formatted_original_total ?? "";
|
|
124
|
+
orderCartTotal.value = data.formatted_total_with_discount ?? data.formatted_total ?? "";
|
|
125
|
+
hasDiscountFlag.value = (data.total_discount ?? 0) !== 0;
|
|
126
|
+
if (data.discount_coupon) couponInfo.value = data.discount_coupon;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Raw ordered cart for display
|
|
130
|
+
const orderCart = ref<CartItem[]>([]);
|
|
131
|
+
const orderCartTotal = ref<string | number>(0);
|
|
132
|
+
const orderCartDiscount = ref<string | number>(0);
|
|
133
|
+
const hasDiscountFlag = ref(false);
|
|
134
|
+
|
|
90
135
|
async function addItem(itemId: number, itemType: string, quantity = 1) {
|
|
136
|
+
if (cartLoading.value) return;
|
|
137
|
+
// Clear plan if adding product (different flows)
|
|
138
|
+
if (selectedPlan.value) { selectedPlan.value = null; selectedPlanPricing.value = null; }
|
|
91
139
|
cartLoading.value = true;
|
|
92
140
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
item_type: itemType,
|
|
96
|
-
quantity,
|
|
97
|
-
});
|
|
98
|
-
cart.value = res.data?.data ?? cart.value;
|
|
141
|
+
await httpService.post("api/v1/shopping-cart/add", { item_id: itemId, item_type: itemType, quantity });
|
|
142
|
+
await fetchCart();
|
|
99
143
|
} finally {
|
|
100
144
|
cartLoading.value = false;
|
|
101
145
|
}
|
|
102
146
|
}
|
|
103
147
|
|
|
104
148
|
async function removeItem(itemId: number, itemType: string, quantity = 1) {
|
|
149
|
+
if (cartLoading.value) return;
|
|
105
150
|
cartLoading.value = true;
|
|
106
151
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
item_type: itemType,
|
|
110
|
-
quantity,
|
|
111
|
-
});
|
|
112
|
-
cart.value = res.data?.data ?? cart.value;
|
|
152
|
+
await httpService.post("api/v1/shopping-cart/remove", { item_id: itemId, item_type: itemType, quantity });
|
|
153
|
+
await fetchCart();
|
|
113
154
|
} finally {
|
|
114
155
|
cartLoading.value = false;
|
|
115
156
|
}
|
|
116
157
|
}
|
|
117
158
|
|
|
118
|
-
async function
|
|
159
|
+
async function clearCartItems() {
|
|
160
|
+
if (cartLoading.value) return;
|
|
119
161
|
cartLoading.value = true;
|
|
120
162
|
try {
|
|
121
|
-
await httpService.post("/shopping-cart/clear");
|
|
122
|
-
cart.value =
|
|
163
|
+
await httpService.post("api/v1/shopping-cart/clear");
|
|
164
|
+
cart.value = {};
|
|
165
|
+
orderCart.value = [];
|
|
166
|
+
orderCartTotal.value = 0;
|
|
167
|
+
orderCartDiscount.value = 0;
|
|
168
|
+
hasDiscountFlag.value = false;
|
|
123
169
|
} finally {
|
|
124
170
|
cartLoading.value = false;
|
|
125
171
|
}
|
|
126
172
|
}
|
|
127
173
|
|
|
128
|
-
// Coupon
|
|
129
|
-
|
|
174
|
+
// ── Coupon ────────────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
const couponCode = ref("");
|
|
177
|
+
const couponLoading = ref(false);
|
|
178
|
+
const couponError = ref<string | null>(null);
|
|
179
|
+
const couponInfo = ref<any>(null);
|
|
180
|
+
|
|
181
|
+
async function applyCoupon(code?: string) {
|
|
182
|
+
const codeToApply = (code ?? couponCode.value).toUpperCase().trim();
|
|
183
|
+
if (!codeToApply) return;
|
|
130
184
|
couponLoading.value = true;
|
|
131
185
|
couponError.value = null;
|
|
132
186
|
try {
|
|
133
|
-
const res = await httpService.post("/shopping-cart/apply-coupon", {
|
|
134
|
-
code: code.toUpperCase(),
|
|
135
|
-
});
|
|
187
|
+
const res = await httpService.post("api/v1/shopping-cart/apply-coupon", { code: codeToApply });
|
|
136
188
|
const data = res.data?.data ?? res.data;
|
|
137
189
|
if (res.data?.warnings?.length) {
|
|
138
190
|
couponError.value = res.data.warnings[0]?.message ?? "Invalid coupon";
|
|
139
191
|
} else {
|
|
140
192
|
cart.value = data;
|
|
141
|
-
|
|
193
|
+
_syncCartState(data);
|
|
194
|
+
couponInfo.value = data?.discount_coupon ?? { code: codeToApply };
|
|
142
195
|
}
|
|
143
196
|
} catch (e: any) {
|
|
144
197
|
couponError.value = e?.response?.data?.message ?? "Invalid coupon code";
|
|
@@ -150,8 +203,10 @@ export function useMgCheckout(httpService: AxiosInstance) {
|
|
|
150
203
|
async function removeCoupon() {
|
|
151
204
|
couponLoading.value = true;
|
|
152
205
|
try {
|
|
153
|
-
const res = await httpService.post("/shopping-cart/remove-coupon");
|
|
154
|
-
|
|
206
|
+
const res = await httpService.post("api/v1/shopping-cart/remove-coupon");
|
|
207
|
+
const data = res.data?.data ?? res.data;
|
|
208
|
+
cart.value = data;
|
|
209
|
+
_syncCartState(data);
|
|
155
210
|
couponInfo.value = null;
|
|
156
211
|
couponCode.value = "";
|
|
157
212
|
} finally {
|
|
@@ -159,110 +214,279 @@ export function useMgCheckout(httpService: AxiosInstance) {
|
|
|
159
214
|
}
|
|
160
215
|
}
|
|
161
216
|
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
217
|
+
// ── Plan / Subscription ────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
const selectedPlan = ref<PlanSelection | null>(null);
|
|
220
|
+
const selectedPlanPricing = ref<PlanPricingSelection | null>(null);
|
|
221
|
+
const hasPlanInCart = computed(() => !!selectedPlan.value);
|
|
222
|
+
|
|
223
|
+
const isSubscriber = computed(() => {
|
|
224
|
+
const sub = (authStore as any).user?.subscription ?? (authStore as any).subscription;
|
|
225
|
+
return sub?.is_subscriber === true || sub?.active === true;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
async function selectPlan(plan: PlanSelection, pricing: PlanPricingSelection) {
|
|
229
|
+
selectedPlan.value = plan;
|
|
230
|
+
selectedPlanPricing.value = pricing;
|
|
231
|
+
// Clear product cart when selecting a plan
|
|
232
|
+
if (orderCart.value.length > 0) {
|
|
233
|
+
try {
|
|
234
|
+
await httpService.post("api/v1/shopping-cart/clear");
|
|
235
|
+
orderCart.value = [];
|
|
236
|
+
orderCartTotal.value = 0;
|
|
237
|
+
orderCartDiscount.value = 0;
|
|
238
|
+
hasDiscountFlag.value = false;
|
|
239
|
+
} catch { /* silent */ }
|
|
167
240
|
}
|
|
241
|
+
}
|
|
168
242
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
243
|
+
function removePlan() {
|
|
244
|
+
selectedPlan.value = null;
|
|
245
|
+
selectedPlanPricing.value = null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── Payment / Gateway ──────────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
const selectedPaymentGateway = ref<string>("");
|
|
251
|
+
const checkoutError = ref<string | null>(null);
|
|
252
|
+
const loading = ref(false);
|
|
174
253
|
|
|
175
|
-
|
|
254
|
+
function selectPaymentType(value: string) {
|
|
255
|
+
selectedPaymentGateway.value = value;
|
|
176
256
|
checkoutError.value = null;
|
|
257
|
+
}
|
|
177
258
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
259
|
+
// ── Terms & Confirm Modal ──────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
const termUseAccepted = ref(false);
|
|
262
|
+
const sendError = ref(false);
|
|
263
|
+
const openConfirmModal = ref(false);
|
|
264
|
+
|
|
265
|
+
function changeTerm(value: boolean) {
|
|
266
|
+
termUseAccepted.value = value;
|
|
267
|
+
sendError.value = false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── Guest Checkout ─────────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
const offlineEmail = ref("");
|
|
273
|
+
const emailError = ref("");
|
|
274
|
+
const isInvalid = ref(false);
|
|
275
|
+
|
|
276
|
+
function isValidEmail(email: string): boolean {
|
|
277
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
278
|
+
}
|
|
188
279
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
// paid and returns payer_action: null (NO gateway). Send the user to the
|
|
201
|
-
// confirmation/success URL with the invoice id appended, so the
|
|
202
|
-
// confirmation page can read it — never redirect to a null payer_action.
|
|
203
|
-
if (checkoutResponse.value.payment_type === "free") {
|
|
204
|
-
const invId =
|
|
205
|
-
(data.order?.invoice_id ?? data.order?.reference ?? "") as string;
|
|
206
|
-
if (successUrl) {
|
|
207
|
-
const sep = successUrl.includes("?") ? "&" : "?";
|
|
208
|
-
window.location.href = invId
|
|
209
|
-
? `${successUrl}${sep}invoice_id=${encodeURIComponent(invId)}`
|
|
210
|
-
: successUrl;
|
|
211
|
-
}
|
|
212
|
-
} else if (
|
|
213
|
-
checkoutResponse.value.payment_type === "redirect" &&
|
|
214
|
-
checkoutResponse.value.payer_action
|
|
215
|
-
) {
|
|
216
|
-
// Stripe / PayPal — redirect to the gateway.
|
|
217
|
-
window.location.href = checkoutResponse.value.payer_action;
|
|
280
|
+
// ── handlePurchase: gate check before opening modal ───────────────────────
|
|
281
|
+
|
|
282
|
+
function handlePurchase() {
|
|
283
|
+
if (!signedIn.value) {
|
|
284
|
+
if (!offlineEmail.value || !isValidEmail(offlineEmail.value)) {
|
|
285
|
+
emailError.value = _t("wallet.offline_order.error", "Please enter a valid email.");
|
|
286
|
+
isInvalid.value = true;
|
|
287
|
+
nextTick(() => {
|
|
288
|
+
document.getElementById("offlineEmail")?.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
289
|
+
});
|
|
290
|
+
return;
|
|
218
291
|
}
|
|
292
|
+
}
|
|
293
|
+
if (!selectedPaymentGateway.value) {
|
|
294
|
+
checkoutError.value = _t("checkout.select_payment_method", "Please select a payment method.");
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (!termUseAccepted.value) {
|
|
298
|
+
sendError.value = true;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
openConfirmModal.value = true;
|
|
302
|
+
}
|
|
219
303
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
304
|
+
function _t(key: string, fallback: string): string {
|
|
305
|
+
try { return (nuxtApp.$i18n as any).t(key) || fallback; } catch { return fallback; }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ── startValidation: the actual checkout submission ────────────────────────
|
|
309
|
+
|
|
310
|
+
async function startValidation() {
|
|
311
|
+
if (loading.value) return;
|
|
312
|
+
try {
|
|
313
|
+
loading.value = true;
|
|
314
|
+
if (buyerPageName) localStorage.setItem("buyerPage", buyerPageName);
|
|
315
|
+
|
|
316
|
+
const gatewayStr = selectedPaymentGateway.value || "stripe";
|
|
317
|
+
const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
|
|
318
|
+
const localeStr = locale.value || "en";
|
|
319
|
+
|
|
320
|
+
if (hasPlanInCart.value && selectedPlanPricing.value) {
|
|
321
|
+
// ── Subscription / plan flow ──────────────────────────────────────
|
|
322
|
+
await _doSubscriptionCheckout(gatewayStr, baseUrl, localeStr, false);
|
|
323
|
+
|
|
324
|
+
} else {
|
|
325
|
+
// ── Product / cart flow ────────────────────────────────────────────
|
|
326
|
+
await _doCartCheckout(gatewayStr, baseUrl, localeStr);
|
|
327
|
+
}
|
|
328
|
+
} catch (error: any) {
|
|
329
|
+
openConfirmModal.value = false;
|
|
330
|
+
await _handleCheckoutError(error, gatewayStr());
|
|
226
331
|
} finally {
|
|
227
|
-
|
|
332
|
+
loading.value = false;
|
|
228
333
|
}
|
|
229
334
|
}
|
|
230
335
|
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
336
|
+
// helpers captured for error handler closure
|
|
337
|
+
let _lastGateway = "";
|
|
338
|
+
function gatewayStr() { return _lastGateway || selectedPaymentGateway.value || "stripe"; }
|
|
339
|
+
|
|
340
|
+
async function _doSubscriptionCheckout(gateway: string, baseUrl: string, loc: string, isUpgrade: boolean) {
|
|
341
|
+
_lastGateway = gateway;
|
|
342
|
+
const endpoint = isUpgrade
|
|
343
|
+
? "api/v1/plans/subscriptions/upgrade"
|
|
344
|
+
: "api/v1/plans/subscriptions";
|
|
345
|
+
|
|
346
|
+
const payload: Record<string, any> = {
|
|
347
|
+
mg_network_plan_pricing_id: selectedPlanPricing.value!.id,
|
|
348
|
+
success_url: `${baseUrl}/${loc}${successPath}`,
|
|
349
|
+
cancel_url: `${baseUrl}/${loc}${cancelPath}`,
|
|
350
|
+
payment_gateway: gateway,
|
|
351
|
+
};
|
|
352
|
+
if (couponInfo.value?.code) payload.discount_coupon_code = couponInfo.value.code;
|
|
353
|
+
if (!signedIn.value && offlineEmail.value) payload.email = offlineEmail.value;
|
|
354
|
+
|
|
355
|
+
const res = await httpService.post(endpoint, payload);
|
|
356
|
+
const meta = res.data?.meta ?? res.data ?? {};
|
|
357
|
+
const payerAction = meta.payer_action ?? meta.checkout_url ?? null;
|
|
358
|
+
const invoiceId = meta.invoice_id ?? meta.id;
|
|
359
|
+
|
|
360
|
+
if (invoiceId) {
|
|
361
|
+
localStorage.setItem("invoiceUuid", String(invoiceId));
|
|
362
|
+
}
|
|
363
|
+
localStorage.setItem("paymentGateway", gateway);
|
|
364
|
+
localStorage.removeItem("checkoutType");
|
|
365
|
+
localStorage.removeItem("subscriptionId");
|
|
366
|
+
|
|
367
|
+
openConfirmModal.value = false;
|
|
368
|
+
removePlan();
|
|
369
|
+
|
|
370
|
+
if (payerAction) {
|
|
371
|
+
window.location.href = payerAction;
|
|
372
|
+
} else {
|
|
373
|
+
// Free / immediate — go straight to confirmation
|
|
374
|
+
router.push(`/${loc}${successPath}${invoiceId ? `?invoice_id=${invoiceId}` : ""}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
235
377
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
cart.value.original_total !== cart.value.total_with_discount
|
|
240
|
-
);
|
|
378
|
+
async function _doCartCheckout(gateway: string, baseUrl: string, loc: string) {
|
|
379
|
+
_lastGateway = gateway;
|
|
380
|
+
localStorage.setItem("paymentGateway", gateway);
|
|
241
381
|
|
|
242
|
-
|
|
243
|
-
|
|
382
|
+
const payload: Record<string, any> = {
|
|
383
|
+
payment_method: gateway,
|
|
384
|
+
success_url: `${baseUrl}/${loc}${successPath}`,
|
|
385
|
+
cancel_url: `${baseUrl}/${loc}${cancelPath}`,
|
|
386
|
+
};
|
|
387
|
+
if (!signedIn.value && offlineEmail.value) payload.email = offlineEmail.value;
|
|
244
388
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
389
|
+
const res = await httpService.post("api/v1/shopping-cart/checkout", payload);
|
|
390
|
+
const data = res.data ?? {};
|
|
391
|
+
const payerAction = data.payer_action ?? data.checkout_url ?? null;
|
|
392
|
+
const invoiceId = data.order?.invoice_id ?? data.invoice_id;
|
|
393
|
+
|
|
394
|
+
if (invoiceId) localStorage.setItem("invoiceUuid", String(invoiceId));
|
|
395
|
+
|
|
396
|
+
openConfirmModal.value = false;
|
|
397
|
+
|
|
398
|
+
if (!payerAction) {
|
|
399
|
+
// Free order — confirmation directly
|
|
400
|
+
router.push(`/${loc}${successPath}${invoiceId ? `?invoice_id=${invoiceId}` : ""}`);
|
|
401
|
+
} else {
|
|
402
|
+
window.location.href = payerAction;
|
|
403
|
+
}
|
|
248
404
|
}
|
|
249
405
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
406
|
+
async function _handleCheckoutError(error: any, currentGateway: string) {
|
|
407
|
+
openConfirmModal.value = false;
|
|
408
|
+
const status = error?.response?.status ?? 0;
|
|
409
|
+
const msg: string = error?.response?.data?.message ?? "";
|
|
410
|
+
const code: string = error?.response?.data?.code ?? "";
|
|
411
|
+
|
|
412
|
+
// 409 — already has subscription → auto-retry as upgrade
|
|
413
|
+
const isAlreadySubscribed =
|
|
414
|
+
status === 409 &&
|
|
415
|
+
(code === "ALREADY_SUBSCRIBED" ||
|
|
416
|
+
msg.toLowerCase().includes("already have an active") ||
|
|
417
|
+
msg.toLowerCase().includes("already subscribed"));
|
|
418
|
+
|
|
419
|
+
if (isAlreadySubscribed && !isSubscriber.value && hasPlanInCart.value && selectedPlanPricing.value) {
|
|
420
|
+
try {
|
|
421
|
+
const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
|
|
422
|
+
await _doSubscriptionCheckout(currentGateway, baseUrl, locale.value || "en", true);
|
|
423
|
+
return;
|
|
424
|
+
} catch (upgradeErr: any) {
|
|
425
|
+
checkoutError.value =
|
|
426
|
+
upgradeErr?.response?.data?.message ??
|
|
427
|
+
_t("pricing.error_upgrade_failed", "Unable to upgrade plan. Please contact support.");
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
253
431
|
|
|
432
|
+
const isGatewayError =
|
|
433
|
+
msg.includes("GatewayPlan") ||
|
|
434
|
+
msg.includes("No payment credential") ||
|
|
435
|
+
status === 404;
|
|
436
|
+
|
|
437
|
+
// Auto-fallback to other gateway
|
|
438
|
+
if (isGatewayError && hasPlanInCart.value) {
|
|
439
|
+
const fallback = currentGateway === "paypal" ? "stripe" : "paypal";
|
|
440
|
+
const fallbackLabel = fallback === "paypal" ? "PayPal" : "Stripe";
|
|
441
|
+
checkoutError.value = _t("pricing.error_gateway_fallback", `Trying ${fallbackLabel}...`).replace("{gateway}", fallbackLabel);
|
|
442
|
+
selectedPaymentGateway.value = fallback;
|
|
443
|
+
setTimeout(() => {
|
|
444
|
+
checkoutError.value = null;
|
|
445
|
+
handlePurchase();
|
|
446
|
+
}, 2000);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (isGatewayError) {
|
|
451
|
+
checkoutError.value = _t("pricing.error_gateway_not_available", "Payment method unavailable. Please try another.");
|
|
452
|
+
} else if (msg.includes(".")) {
|
|
453
|
+
const translated = _t(`errors.${msg}`, msg);
|
|
454
|
+
checkoutError.value = translated;
|
|
455
|
+
} else if (msg) {
|
|
456
|
+
checkoutError.value = msg;
|
|
457
|
+
} else {
|
|
458
|
+
checkoutError.value = _t("checkout.generic_error", "Checkout failed. Please try again.");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ── Watchers ───────────────────────────────────────────────────────────────
|
|
463
|
+
|
|
464
|
+
watch(offlineEmail, (val) => {
|
|
465
|
+
if (val && isValidEmail(val)) { emailError.value = ""; isInvalid.value = false; }
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
watch(sendError, (v) => {
|
|
469
|
+
if (v) openConfirmModal.value = false;
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// ── Return ─────────────────────────────────────────────────────────────────
|
|
473
|
+
|
|
474
|
+
return {
|
|
254
475
|
// Cart
|
|
255
476
|
cart,
|
|
256
477
|
cartLoading,
|
|
257
478
|
cartError,
|
|
479
|
+
orderCart,
|
|
480
|
+
orderCartTotal,
|
|
481
|
+
orderCartDiscount,
|
|
482
|
+
hasDiscountFlag,
|
|
483
|
+
hasItems,
|
|
484
|
+
hasDiscount,
|
|
258
485
|
fetchCart,
|
|
259
486
|
addItem,
|
|
260
487
|
removeItem,
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
hasDiscount,
|
|
264
|
-
cartTotal,
|
|
265
|
-
cartOriginalTotal,
|
|
488
|
+
clearCartItems,
|
|
489
|
+
getCartItems,
|
|
266
490
|
|
|
267
491
|
// Coupon
|
|
268
492
|
couponCode,
|
|
@@ -272,29 +496,37 @@ export function useMgCheckout(httpService: AxiosInstance) {
|
|
|
272
496
|
applyCoupon,
|
|
273
497
|
removeCoupon,
|
|
274
498
|
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
499
|
+
// Plan
|
|
500
|
+
selectedPlan,
|
|
501
|
+
selectedPlanPricing,
|
|
502
|
+
hasPlanInCart,
|
|
503
|
+
isSubscriber,
|
|
504
|
+
selectPlan,
|
|
505
|
+
removePlan,
|
|
278
506
|
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
// Checkout
|
|
283
|
-
checkout,
|
|
284
|
-
checkoutLoading,
|
|
285
|
-
checkoutResponse,
|
|
507
|
+
// Payment
|
|
508
|
+
paymentMethods,
|
|
509
|
+
selectedPaymentGateway,
|
|
286
510
|
checkoutError,
|
|
287
|
-
|
|
511
|
+
loading,
|
|
512
|
+
selectPaymentType,
|
|
288
513
|
|
|
289
|
-
|
|
290
|
-
|
|
514
|
+
// Terms & modal
|
|
515
|
+
termUseAccepted,
|
|
516
|
+
sendError,
|
|
517
|
+
openConfirmModal,
|
|
518
|
+
changeTerm,
|
|
291
519
|
|
|
292
|
-
|
|
520
|
+
// Guest
|
|
521
|
+
signedIn,
|
|
522
|
+
offlineEmail,
|
|
523
|
+
emailError,
|
|
524
|
+
isInvalid,
|
|
293
525
|
|
|
294
|
-
|
|
295
|
-
|
|
526
|
+
// Actions
|
|
527
|
+
handlePurchase,
|
|
528
|
+
startValidation,
|
|
529
|
+
};
|
|
296
530
|
}
|
|
297
531
|
|
|
298
|
-
export
|
|
299
|
-
return inject<MgCheckoutInstance>(CHECKOUT_KEY);
|
|
300
|
-
}
|
|
532
|
+
export type MgCheckoutInstance = ReturnType<typeof useMgCheckout>;
|