@pradip1995/commerce-core 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.
- package/README.md +15 -0
- package/package.json +70 -0
- package/src/analytics/ga4-ecommerce.ts +96 -0
- package/src/config.ts +36 -0
- package/src/constants.tsx +84 -0
- package/src/context/modal-context.tsx +40 -0
- package/src/context/wishlist-context.tsx +96 -0
- package/src/data/cart/abandoned.ts +111 -0
- package/src/data/cart/buyNow.ts +184 -0
- package/src/data/cart/checkout.ts +487 -0
- package/src/data/cart/index.ts +7 -0
- package/src/data/cart/mutations.ts +189 -0
- package/src/data/cart/promotions.ts +121 -0
- package/src/data/cart/region.ts +66 -0
- package/src/data/cart/retrieve.ts +162 -0
- package/src/data/categories.ts +90 -0
- package/src/data/collections.ts +109 -0
- package/src/data/contact.ts +143 -0
- package/src/data/cookies.ts +170 -0
- package/src/data/customer-registration.ts +365 -0
- package/src/data/customer.ts +638 -0
- package/src/data/dynamic-config.ts +420 -0
- package/src/data/fulfillment.ts +95 -0
- package/src/data/guest.ts +357 -0
- package/src/data/locale-actions.ts +74 -0
- package/src/data/locales.ts +28 -0
- package/src/data/newsletter.ts +41 -0
- package/src/data/notifications.ts +22 -0
- package/src/data/onboarding.ts +9 -0
- package/src/data/orders.ts +500 -0
- package/src/data/payment-details.ts +68 -0
- package/src/data/payment.ts +32 -0
- package/src/data/products.ts +424 -0
- package/src/data/regions.ts +64 -0
- package/src/data/returns.ts +305 -0
- package/src/data/reviews.ts +279 -0
- package/src/data/swaps.ts +154 -0
- package/src/data/variants.ts +38 -0
- package/src/data/wishlist.ts +292 -0
- package/src/domain/cart/abandoned-carts.ts +49 -0
- package/src/domain/cart/buy-now.ts +15 -0
- package/src/domain/cart/checkout.ts +25 -0
- package/src/domain/cart/index.ts +8 -0
- package/src/domain/cart/metadata.ts +21 -0
- package/src/domain/cart/payment.ts +21 -0
- package/src/domain/cart/phone.ts +17 -0
- package/src/domain/cart/reorder.ts +19 -0
- package/src/domain/cart/validation.ts +43 -0
- package/src/domain/product/pricing.ts +49 -0
- package/src/domain/product/variant-selection.ts +193 -0
- package/src/firebase.ts +48 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/use-add-to-cart.ts +63 -0
- package/src/hooks/use-cart.ts +132 -0
- package/src/hooks/use-checkout.ts +62 -0
- package/src/hooks/use-in-view.tsx +29 -0
- package/src/hooks/use-product-actions.ts +190 -0
- package/src/hooks/use-product-reviews.ts +18 -0
- package/src/hooks/use-product-variant.ts +142 -0
- package/src/hooks/use-server-action.ts +30 -0
- package/src/hooks/use-toggle-state.tsx +46 -0
- package/src/hooks/use-wishlist.ts +3 -0
- package/src/theme/inline-vars.ts +12 -0
- package/src/types/account.ts +21 -0
- package/src/types/cart.ts +13 -0
- package/src/types/home.ts +52 -0
- package/src/types/layout.ts +29 -0
- package/src/types/product-card.ts +17 -0
- package/src/util/compare-addresses.ts +28 -0
- package/src/util/env.ts +3 -0
- package/src/util/get-locale-header.ts +8 -0
- package/src/util/get-percentage-diff.ts +6 -0
- package/src/util/get-product-price.ts +78 -0
- package/src/util/google-oauth.ts +28 -0
- package/src/util/isEmpty.ts +11 -0
- package/src/util/medusa-error.ts +18 -0
- package/src/util/money.ts +26 -0
- package/src/util/order-status.tsx +179 -0
- package/src/util/product.ts +431 -0
- package/src/util/repeat.ts +5 -0
- package/src/util/returns.ts +71 -0
- package/src/util/sort-products.ts +48 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use server"
|
|
2
|
+
|
|
3
|
+
import { sdk } from "@core/config"
|
|
4
|
+
import { HttpTypes } from "@medusajs/types"
|
|
5
|
+
import { cookies } from "next/headers"
|
|
6
|
+
import { getAuthHeaders } from "./cookies"
|
|
7
|
+
import { revalidateTag } from "next/cache"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create an exchange (swap) request.
|
|
11
|
+
* Supports both authenticated customers and guest users.
|
|
12
|
+
*/
|
|
13
|
+
export const createSwapRequest = async (
|
|
14
|
+
prevState: any,
|
|
15
|
+
formData: FormData
|
|
16
|
+
): Promise<{
|
|
17
|
+
success: boolean
|
|
18
|
+
error: string | null
|
|
19
|
+
swap: any | null
|
|
20
|
+
}> => {
|
|
21
|
+
const orderId = formData.get("order_id") as string
|
|
22
|
+
const returnItemsJson = formData.get("return_items") as string
|
|
23
|
+
const newItemsJson = formData.get("new_items") as string
|
|
24
|
+
const reason = formData.get("reason") as string
|
|
25
|
+
const note = formData.get("note") as string
|
|
26
|
+
|
|
27
|
+
if (!orderId) return { success: false, error: "Order ID is required", swap: null }
|
|
28
|
+
if (!returnItemsJson)
|
|
29
|
+
return { success: false, error: "Items to return are required", swap: null }
|
|
30
|
+
if (!newItemsJson)
|
|
31
|
+
return { success: false, error: "New items are required", swap: null }
|
|
32
|
+
|
|
33
|
+
let returnItems: any[] = []
|
|
34
|
+
let newItems: any[] = []
|
|
35
|
+
try {
|
|
36
|
+
returnItems = JSON.parse(returnItemsJson)
|
|
37
|
+
newItems = JSON.parse(newItemsJson)
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return { success: false, error: "Invalid items data format", swap: null }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (returnItems.length === 0) {
|
|
43
|
+
return {
|
|
44
|
+
success: false,
|
|
45
|
+
error: "At least one item must be selected for return",
|
|
46
|
+
swap: null,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const headers = {
|
|
51
|
+
...(await getAuthHeaders()),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const payload = {
|
|
56
|
+
order_id: orderId,
|
|
57
|
+
return_items: returnItems,
|
|
58
|
+
new_items: newItems,
|
|
59
|
+
reason: reason || "Size exchange",
|
|
60
|
+
note: note || "Exchange requested from storefront",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const cookieStore = await cookies()
|
|
64
|
+
const guestToken =
|
|
65
|
+
cookieStore.get("_medusa_guest_jwt")?.value ||
|
|
66
|
+
cookieStore.get("_medusa_guest_token")?.value
|
|
67
|
+
const token = cookieStore.get("_medusa_jwt")?.value
|
|
68
|
+
|
|
69
|
+
let swapData: any
|
|
70
|
+
|
|
71
|
+
// Check if we should use the guest endpoint
|
|
72
|
+
if (guestToken && !token) {
|
|
73
|
+
const response = await sdk.client.fetch<any>(
|
|
74
|
+
`/store/guest-orders/${orderId}/swaps`,
|
|
75
|
+
{
|
|
76
|
+
method: "POST",
|
|
77
|
+
body: {
|
|
78
|
+
return_items: payload.return_items,
|
|
79
|
+
new_items: payload.new_items,
|
|
80
|
+
reason: payload.reason,
|
|
81
|
+
note: payload.note,
|
|
82
|
+
},
|
|
83
|
+
headers: {
|
|
84
|
+
...headers,
|
|
85
|
+
Authorization: `Bearer ${guestToken}`,
|
|
86
|
+
},
|
|
87
|
+
cache: "no-store",
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
swapData = response.swap || response
|
|
91
|
+
} else {
|
|
92
|
+
const response = await sdk.client.fetch<any>(`/store/swaps`, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
body: payload,
|
|
95
|
+
headers,
|
|
96
|
+
cache: "no-store",
|
|
97
|
+
})
|
|
98
|
+
swapData = response.swap || response
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
revalidateTag("orders")
|
|
102
|
+
|
|
103
|
+
return { success: true, error: null, swap: swapData }
|
|
104
|
+
} catch (error: any) {
|
|
105
|
+
// Extract error message from response if possible
|
|
106
|
+
let errorMsg = "Failed to create exchange request"
|
|
107
|
+
if (error.response?.data?.message) {
|
|
108
|
+
errorMsg = error.response.data.message
|
|
109
|
+
} else if (error.message) {
|
|
110
|
+
errorMsg = error.message
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { success: false, error: errorMsg, swap: null }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* List swaps for an order or customer
|
|
119
|
+
*/
|
|
120
|
+
export const listSwaps = async (orderId?: string) => {
|
|
121
|
+
const headers = {
|
|
122
|
+
...(await getAuthHeaders()),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Authenticated flow
|
|
126
|
+
if ((headers as any).authorization) {
|
|
127
|
+
return sdk.client.fetch<any>(`/store/swaps`, {
|
|
128
|
+
method: "GET",
|
|
129
|
+
query: orderId ? { order_id: orderId } : {},
|
|
130
|
+
headers,
|
|
131
|
+
cache: "no-store",
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Guest flow
|
|
136
|
+
if (orderId) {
|
|
137
|
+
const cookieStore = await cookies()
|
|
138
|
+
const guestToken =
|
|
139
|
+
cookieStore.get("_medusa_guest_jwt")?.value ||
|
|
140
|
+
cookieStore.get("_medusa_guest_token")?.value
|
|
141
|
+
if (guestToken) {
|
|
142
|
+
return sdk.client.fetch<any>(`/store/guest-orders/${orderId}/swaps`, {
|
|
143
|
+
method: "GET",
|
|
144
|
+
headers: {
|
|
145
|
+
...headers,
|
|
146
|
+
Authorization: `Bearer ${guestToken}`,
|
|
147
|
+
},
|
|
148
|
+
cache: "no-store",
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { swaps: [], count: 0 }
|
|
154
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use server"
|
|
2
|
+
|
|
3
|
+
import { sdk } from "@core/config"
|
|
4
|
+
import { HttpTypes } from "@medusajs/types"
|
|
5
|
+
|
|
6
|
+
import { getAuthHeaders, getCacheOptions } from "./cookies"
|
|
7
|
+
|
|
8
|
+
export const retrieveVariant = async (
|
|
9
|
+
variant_id: string
|
|
10
|
+
): Promise<HttpTypes.StoreProductVariant | null> => {
|
|
11
|
+
const authHeaders = await getAuthHeaders()
|
|
12
|
+
|
|
13
|
+
if (!authHeaders) return null
|
|
14
|
+
|
|
15
|
+
const headers = {
|
|
16
|
+
...authHeaders,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const next = {
|
|
20
|
+
...(await getCacheOptions("variants")),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return await sdk.client
|
|
24
|
+
.fetch<{ variant: HttpTypes.StoreProductVariant }>(
|
|
25
|
+
`/store/product-variants/${variant_id}`,
|
|
26
|
+
{
|
|
27
|
+
method: "GET",
|
|
28
|
+
query: {
|
|
29
|
+
fields: "*images",
|
|
30
|
+
},
|
|
31
|
+
headers,
|
|
32
|
+
next,
|
|
33
|
+
cache: "force-cache",
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
.then(({ variant }) => variant)
|
|
37
|
+
.catch(() => null)
|
|
38
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"use server"
|
|
2
|
+
|
|
3
|
+
import { getAuthHeaders } from "./cookies"
|
|
4
|
+
import { HttpTypes } from "@medusajs/types"
|
|
5
|
+
|
|
6
|
+
const getBaseUrl = () => {
|
|
7
|
+
return process.env.MEDUSA_BACKEND_URL || "http://localhost:9000"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const getPublishableKey = () => {
|
|
11
|
+
return process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Add a product to wishlist
|
|
16
|
+
*/
|
|
17
|
+
export async function addToWishlist(productId: string) {
|
|
18
|
+
try {
|
|
19
|
+
const authHeaders = await getAuthHeaders()
|
|
20
|
+
const publishableKey = getPublishableKey()
|
|
21
|
+
const baseUrl = getBaseUrl()
|
|
22
|
+
|
|
23
|
+
if (!publishableKey) {
|
|
24
|
+
return {
|
|
25
|
+
success: false,
|
|
26
|
+
error: "Configuration error: Publishable API key is missing.",
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!authHeaders || !("authorization" in authHeaders)) {
|
|
31
|
+
return {
|
|
32
|
+
success: false,
|
|
33
|
+
error: "Please login to add items to wishlist.",
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const headers: Record<string, string> = {
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
"x-publishable-api-key": publishableKey,
|
|
40
|
+
...authHeaders,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const response = await fetch(`${baseUrl}/store/wishlist`, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers,
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
product_id: productId,
|
|
48
|
+
}),
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
if (response.status === 401 || response.status === 403) {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
error: "Login required",
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const error = await response.json().catch(() => ({ message: "Unknown error" }))
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
error: error.message || `HTTP ${response.status}: Failed to add to wishlist`,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const data = await response.json()
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
data,
|
|
69
|
+
}
|
|
70
|
+
} catch (error: any) {
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
error: error.message || "An error occurred while adding to wishlist",
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Remove a product from wishlist
|
|
80
|
+
*/
|
|
81
|
+
export async function removeFromWishlist(productId: string) {
|
|
82
|
+
try {
|
|
83
|
+
const authHeaders = await getAuthHeaders()
|
|
84
|
+
const publishableKey = getPublishableKey()
|
|
85
|
+
const baseUrl = getBaseUrl()
|
|
86
|
+
|
|
87
|
+
if (!publishableKey) {
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
error: "Configuration error: Publishable API key is missing.",
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!authHeaders || !("authorization" in authHeaders)) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
error: "Please login to remove items from wishlist.",
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const headers: Record<string, string> = {
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
"x-publishable-api-key": publishableKey,
|
|
104
|
+
...authHeaders,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const response = await fetch(`${baseUrl}/store/wishlist/${productId}`, {
|
|
108
|
+
method: "DELETE",
|
|
109
|
+
headers,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
if (response.status === 401 || response.status === 403) {
|
|
114
|
+
return {
|
|
115
|
+
success: false,
|
|
116
|
+
error: "Login required",
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const error = await response.json().catch(() => ({ message: "Unknown error" }))
|
|
120
|
+
return {
|
|
121
|
+
success: false,
|
|
122
|
+
error:
|
|
123
|
+
error.message || `HTTP ${response.status}: Failed to remove from wishlist`,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
success: true,
|
|
129
|
+
}
|
|
130
|
+
} catch (error: any) {
|
|
131
|
+
return {
|
|
132
|
+
success: false,
|
|
133
|
+
error: error.message || "An error occurred while removing from wishlist",
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get wishlist items with full product details
|
|
140
|
+
*/
|
|
141
|
+
export async function getWishlist(
|
|
142
|
+
includeDetails: boolean = true,
|
|
143
|
+
countryCode?: string
|
|
144
|
+
) {
|
|
145
|
+
try {
|
|
146
|
+
const authHeaders = await getAuthHeaders()
|
|
147
|
+
const publishableKey = getPublishableKey()
|
|
148
|
+
const baseUrl = getBaseUrl()
|
|
149
|
+
|
|
150
|
+
if (!publishableKey) {
|
|
151
|
+
return {
|
|
152
|
+
success: false,
|
|
153
|
+
error: "Configuration error: Publishable API key is missing.",
|
|
154
|
+
data: [],
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!authHeaders || !("authorization" in authHeaders)) {
|
|
159
|
+
// Return empty array if not logged in
|
|
160
|
+
return {
|
|
161
|
+
success: true,
|
|
162
|
+
data: [],
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const headers: Record<string, string> = {
|
|
167
|
+
"Content-Type": "application/json",
|
|
168
|
+
"x-publishable-api-key": publishableKey,
|
|
169
|
+
...authHeaders,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const url = new URL(`${baseUrl}/store/wishlist`)
|
|
173
|
+
if (includeDetails) {
|
|
174
|
+
url.searchParams.set("include_details", "true")
|
|
175
|
+
// Using the same aggressive expansion style that worked for the cart
|
|
176
|
+
url.searchParams.set(
|
|
177
|
+
"fields",
|
|
178
|
+
"id,product_id,*product,product.thumbnail,product.images"
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const response = await fetch(url.toString(), {
|
|
183
|
+
method: "GET",
|
|
184
|
+
headers,
|
|
185
|
+
cache: "no-store",
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
if (!response.ok) {
|
|
189
|
+
// If 401 or 403, user is not authenticated, return empty array
|
|
190
|
+
if (response.status === 401 || response.status === 403) {
|
|
191
|
+
return {
|
|
192
|
+
success: true,
|
|
193
|
+
data: [],
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const error = await response.json().catch(() => ({ message: "Unknown error" }))
|
|
198
|
+
return {
|
|
199
|
+
success: false,
|
|
200
|
+
error: error.message || `HTTP ${response.status}: Failed to fetch wishlist`,
|
|
201
|
+
data: [],
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const data = await response.json()
|
|
206
|
+
let wishlistItems = data.wishlist || (Array.isArray(data) ? data : [])
|
|
207
|
+
|
|
208
|
+
// If we have items and details are requested, fetch full product details separately
|
|
209
|
+
if (includeDetails && wishlistItems.length > 0) {
|
|
210
|
+
const productIds = wishlistItems
|
|
211
|
+
.map((item: any) => item.product_id || item.id)
|
|
212
|
+
.filter(Boolean)
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const { sdk } = await import("@core/config")
|
|
216
|
+
|
|
217
|
+
let regionId: string | undefined = undefined
|
|
218
|
+
if (countryCode) {
|
|
219
|
+
const { getRegion } = await import("./regions")
|
|
220
|
+
const region = await getRegion(countryCode)
|
|
221
|
+
regionId = region?.id
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const productsResponse = await sdk.store.product.list(
|
|
225
|
+
// Fetch comprehensive product details including variants, options and inventory for availability checks
|
|
226
|
+
{
|
|
227
|
+
id: productIds,
|
|
228
|
+
fields:
|
|
229
|
+
"*thumbnail,*images,*variants,*variants.options,*options,*options.values,*variants.calculated_price,*variants.inventory_quantity,*variants.manage_inventory,*variants.allow_backorder",
|
|
230
|
+
...(regionId ? { region_id: regionId } : {}),
|
|
231
|
+
},
|
|
232
|
+
{ next: { tags: ["products"] } }
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
const fullProducts =
|
|
236
|
+
productsResponse.products as unknown as HttpTypes.StoreProduct[]
|
|
237
|
+
|
|
238
|
+
// Merge full product data back into wishlist items
|
|
239
|
+
wishlistItems = wishlistItems.map((item: any) => {
|
|
240
|
+
const itemId = item.product_id || item.id
|
|
241
|
+
const fullProduct = fullProducts.find((p: any) => p.id === itemId)
|
|
242
|
+
|
|
243
|
+
if (fullProduct) {
|
|
244
|
+
// Prefer the full product data from the SDK as it has the relations we need
|
|
245
|
+
return {
|
|
246
|
+
...item,
|
|
247
|
+
product: fullProduct,
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return item
|
|
251
|
+
})
|
|
252
|
+
} catch (e) {
|
|
253
|
+
// Handle error silently
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
success: true,
|
|
259
|
+
data: wishlistItems,
|
|
260
|
+
}
|
|
261
|
+
} catch (error: any) {
|
|
262
|
+
return {
|
|
263
|
+
success: false,
|
|
264
|
+
error: error.message || "An error occurred while fetching wishlist",
|
|
265
|
+
data: [],
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get wishlist product IDs only (for checking if product is in wishlist)
|
|
272
|
+
*/
|
|
273
|
+
export async function getWishlistProductIds(): Promise<string[]> {
|
|
274
|
+
try {
|
|
275
|
+
const result = await getWishlist(false)
|
|
276
|
+
if (result.success && Array.isArray(result.data)) {
|
|
277
|
+
const ids = result.data
|
|
278
|
+
.map((item: any) => {
|
|
279
|
+
if (typeof item === "string") return item
|
|
280
|
+
if (item.product_id) return item.product_id
|
|
281
|
+
if (item.id) return item.id
|
|
282
|
+
if (item.product?.id) return item.product.id
|
|
283
|
+
return null
|
|
284
|
+
})
|
|
285
|
+
.filter((id: string | null): id is string => id !== null)
|
|
286
|
+
return ids
|
|
287
|
+
}
|
|
288
|
+
return []
|
|
289
|
+
} catch (error) {
|
|
290
|
+
return []
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { HttpTypes } from "@medusajs/types"
|
|
2
|
+
import { isBuyNowCart, isReorderCart } from "./metadata"
|
|
3
|
+
|
|
4
|
+
export type AbandonedCartsResult = {
|
|
5
|
+
buyNowCarts: HttpTypes.StoreCart[]
|
|
6
|
+
reorderCarts: HttpTypes.StoreCart[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function filterOutCurrentCart(
|
|
10
|
+
carts: HttpTypes.StoreCart[],
|
|
11
|
+
currentCartId?: string | null
|
|
12
|
+
): HttpTypes.StoreCart[] {
|
|
13
|
+
if (!currentCartId) {
|
|
14
|
+
return carts
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return carts.filter((cart) => cart.id !== currentCartId)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function partitionAbandonedCarts(
|
|
21
|
+
carts: HttpTypes.StoreCart[]
|
|
22
|
+
): AbandonedCartsResult {
|
|
23
|
+
return {
|
|
24
|
+
buyNowCarts: carts.filter((cart) => isBuyNowCart(cart.metadata)),
|
|
25
|
+
reorderCarts: carts.filter((cart) => isReorderCart(cart.metadata)),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function hasAbandonedCarts(abandoned: AbandonedCartsResult): boolean {
|
|
30
|
+
return abandoned.buyNowCarts.length > 0 || abandoned.reorderCarts.length > 0
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getDefaultAbandonedTab(
|
|
34
|
+
abandoned: AbandonedCartsResult
|
|
35
|
+
): "buy_now" | "reorder" {
|
|
36
|
+
if (abandoned.buyNowCarts.length > 0) {
|
|
37
|
+
return "buy_now"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return "reorder"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getCartTotalItemCount(cart: HttpTypes.StoreCart): number {
|
|
44
|
+
return cart.items?.reduce((total, item) => total + item.quantity, 0) ?? 0
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getAbandonedTotalItemCount(carts: HttpTypes.StoreCart[]): number {
|
|
48
|
+
return carts.reduce((total, cart) => total + getCartTotalItemCount(cart), 0)
|
|
49
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function getBuyNowCheckoutUrl(
|
|
2
|
+
countryCode: string,
|
|
3
|
+
cartId: string,
|
|
4
|
+
skipToPayment: boolean
|
|
5
|
+
): string {
|
|
6
|
+
if (skipToPayment) {
|
|
7
|
+
return `/${countryCode}/checkout?step=payment&cart_id=${cartId}`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return `/${countryCode}/checkout?cart_id=${cartId}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function shouldSkipToPayment(shippingOptionsCount: number): boolean {
|
|
14
|
+
return shippingOptionsCount > 0
|
|
15
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function isCheckoutPath(pathname: string): boolean {
|
|
2
|
+
return pathname.includes("/checkout")
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function shouldCheckEphemeralCartMetadata(options: {
|
|
6
|
+
explicitCartId?: string
|
|
7
|
+
pathname: string
|
|
8
|
+
}): boolean {
|
|
9
|
+
return !options.explicitCartId && !isCheckoutPath(options.pathname)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type PostOrderCookieAction = "remove_buy_now" | "remove_main_cart"
|
|
13
|
+
|
|
14
|
+
export function getPostOrderCookieAction(
|
|
15
|
+
hasBuyNowCookie: boolean
|
|
16
|
+
): PostOrderCookieAction {
|
|
17
|
+
return hasBuyNowCookie ? "remove_buy_now" : "remove_main_cart"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getResumeAbandonedCheckoutUrl(
|
|
21
|
+
countryCode: string,
|
|
22
|
+
cartId: string
|
|
23
|
+
): string {
|
|
24
|
+
return `/${countryCode}/checkout?cart_id=${cartId}`
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type CartMetadata = Record<string, unknown>
|
|
2
|
+
|
|
3
|
+
export function isBuyNowCart(metadata?: CartMetadata | null): boolean {
|
|
4
|
+
return metadata?.is_buy_now === true
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isReorderCart(metadata?: CartMetadata | null): boolean {
|
|
8
|
+
return metadata?.is_reorder === true
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isEphemeralCart(metadata?: CartMetadata | null): boolean {
|
|
12
|
+
return isBuyNowCart(metadata) || isReorderCart(metadata)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createBuyNowMetadata(): CartMetadata {
|
|
16
|
+
return { is_buy_now: true }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createReorderMetadata(): CartMetadata {
|
|
20
|
+
return { is_reorder: true }
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function requiresRazorpayContact(providerId: string): boolean {
|
|
2
|
+
return providerId.includes("razorpay")
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function validateRazorpayPreflight(options: {
|
|
6
|
+
providerId: string
|
|
7
|
+
phone?: string
|
|
8
|
+
email?: string
|
|
9
|
+
}): void {
|
|
10
|
+
if (requiresRazorpayContact(options.providerId) && !options.phone) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
"CRITICAL Error: Phone number is missing from your address. Razorpay requires a contact number for verification. Please enter your phone number."
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (requiresRazorpayContact(options.providerId) && !options.email) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
"CRITICAL Error: Email is missing from your cart. Razorpay requires an email address. Please enter your email."
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function normalizePhoneForRazorpay(phone?: string): string | undefined {
|
|
2
|
+
if (!phone) {
|
|
3
|
+
return undefined
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const cleaned = String(phone).replace(/\D/g, "")
|
|
7
|
+
|
|
8
|
+
if (cleaned.length === 10) {
|
|
9
|
+
return `+91${cleaned}`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (cleaned.length > 10 && !String(phone).startsWith("+")) {
|
|
13
|
+
return `+${cleaned}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return phone
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createReorderMetadata } from "./metadata"
|
|
2
|
+
|
|
3
|
+
export function extractNewCartIdFromReorderResponse(
|
|
4
|
+
data: Record<string, unknown>
|
|
5
|
+
): string | undefined {
|
|
6
|
+
const cart = data.cart as { id?: string } | undefined
|
|
7
|
+
const reorder = data.reorder as { cart_id?: string } | undefined
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
cart?.id ||
|
|
11
|
+
(data.id as string | undefined) ||
|
|
12
|
+
(data.cart_id as string | undefined) ||
|
|
13
|
+
reorder?.cart_id
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildReorderCartMetadata() {
|
|
18
|
+
return createReorderMetadata()
|
|
19
|
+
}
|