@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.
Files changed (82) hide show
  1. package/README.md +15 -0
  2. package/package.json +70 -0
  3. package/src/analytics/ga4-ecommerce.ts +96 -0
  4. package/src/config.ts +36 -0
  5. package/src/constants.tsx +84 -0
  6. package/src/context/modal-context.tsx +40 -0
  7. package/src/context/wishlist-context.tsx +96 -0
  8. package/src/data/cart/abandoned.ts +111 -0
  9. package/src/data/cart/buyNow.ts +184 -0
  10. package/src/data/cart/checkout.ts +487 -0
  11. package/src/data/cart/index.ts +7 -0
  12. package/src/data/cart/mutations.ts +189 -0
  13. package/src/data/cart/promotions.ts +121 -0
  14. package/src/data/cart/region.ts +66 -0
  15. package/src/data/cart/retrieve.ts +162 -0
  16. package/src/data/categories.ts +90 -0
  17. package/src/data/collections.ts +109 -0
  18. package/src/data/contact.ts +143 -0
  19. package/src/data/cookies.ts +170 -0
  20. package/src/data/customer-registration.ts +365 -0
  21. package/src/data/customer.ts +638 -0
  22. package/src/data/dynamic-config.ts +420 -0
  23. package/src/data/fulfillment.ts +95 -0
  24. package/src/data/guest.ts +357 -0
  25. package/src/data/locale-actions.ts +74 -0
  26. package/src/data/locales.ts +28 -0
  27. package/src/data/newsletter.ts +41 -0
  28. package/src/data/notifications.ts +22 -0
  29. package/src/data/onboarding.ts +9 -0
  30. package/src/data/orders.ts +500 -0
  31. package/src/data/payment-details.ts +68 -0
  32. package/src/data/payment.ts +32 -0
  33. package/src/data/products.ts +424 -0
  34. package/src/data/regions.ts +64 -0
  35. package/src/data/returns.ts +305 -0
  36. package/src/data/reviews.ts +279 -0
  37. package/src/data/swaps.ts +154 -0
  38. package/src/data/variants.ts +38 -0
  39. package/src/data/wishlist.ts +292 -0
  40. package/src/domain/cart/abandoned-carts.ts +49 -0
  41. package/src/domain/cart/buy-now.ts +15 -0
  42. package/src/domain/cart/checkout.ts +25 -0
  43. package/src/domain/cart/index.ts +8 -0
  44. package/src/domain/cart/metadata.ts +21 -0
  45. package/src/domain/cart/payment.ts +21 -0
  46. package/src/domain/cart/phone.ts +17 -0
  47. package/src/domain/cart/reorder.ts +19 -0
  48. package/src/domain/cart/validation.ts +43 -0
  49. package/src/domain/product/pricing.ts +49 -0
  50. package/src/domain/product/variant-selection.ts +193 -0
  51. package/src/firebase.ts +48 -0
  52. package/src/hooks/index.ts +8 -0
  53. package/src/hooks/use-add-to-cart.ts +63 -0
  54. package/src/hooks/use-cart.ts +132 -0
  55. package/src/hooks/use-checkout.ts +62 -0
  56. package/src/hooks/use-in-view.tsx +29 -0
  57. package/src/hooks/use-product-actions.ts +190 -0
  58. package/src/hooks/use-product-reviews.ts +18 -0
  59. package/src/hooks/use-product-variant.ts +142 -0
  60. package/src/hooks/use-server-action.ts +30 -0
  61. package/src/hooks/use-toggle-state.tsx +46 -0
  62. package/src/hooks/use-wishlist.ts +3 -0
  63. package/src/theme/inline-vars.ts +12 -0
  64. package/src/types/account.ts +21 -0
  65. package/src/types/cart.ts +13 -0
  66. package/src/types/home.ts +52 -0
  67. package/src/types/layout.ts +29 -0
  68. package/src/types/product-card.ts +17 -0
  69. package/src/util/compare-addresses.ts +28 -0
  70. package/src/util/env.ts +3 -0
  71. package/src/util/get-locale-header.ts +8 -0
  72. package/src/util/get-percentage-diff.ts +6 -0
  73. package/src/util/get-product-price.ts +78 -0
  74. package/src/util/google-oauth.ts +28 -0
  75. package/src/util/isEmpty.ts +11 -0
  76. package/src/util/medusa-error.ts +18 -0
  77. package/src/util/money.ts +26 -0
  78. package/src/util/order-status.tsx +179 -0
  79. package/src/util/product.ts +431 -0
  80. package/src/util/repeat.ts +5 -0
  81. package/src/util/returns.ts +71 -0
  82. package/src/util/sort-products.ts +48 -0
@@ -0,0 +1,357 @@
1
+ "use server"
2
+
3
+ import { cookies, headers } from "next/headers"
4
+ import { getGuestAuthHeaders } from "./cookies"
5
+ import { sdk } from "@core/config"
6
+
7
+ const BACKEND_URL =
8
+ process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL || "http://localhost:9000"
9
+ const PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY
10
+
11
+ /**
12
+ * Compatibility wrapper for sendOTP
13
+ */
14
+ export async function sendOTP(email: string, type: string = "email_verification") {
15
+ const res = await requestGuestOTP(email)
16
+ // The previous project expected a token or success.
17
+ // We return a dummy token if successful because the UI checks for it.
18
+ if (res.success) {
19
+ return { success: true, token: "otp_sent_successfully" }
20
+ }
21
+ return res
22
+ }
23
+
24
+ /**
25
+ * Compatibility wrapper for verifyOTP
26
+ */
27
+ export async function verifyOTP(email: string, token: string, otp: string) {
28
+ // The previous implementation used 3 args, but guest.ts uses 2.
29
+ // We ignore the mid 'token' as per user's working code.
30
+ return await verifyGuestOTP(email, otp)
31
+ }
32
+
33
+ import { retrieveOrder } from "./orders"
34
+
35
+ /**
36
+ * Compatibility wrapper for listGuestOrders
37
+ */
38
+ export async function listGuestOrders(token: string) {
39
+ const res = await getGuestOrders(token)
40
+ if (!res.success) {
41
+ throw new Error(res.error || "Failed to fetch orders")
42
+ }
43
+
44
+ // Fetch full details for each order to ensure latest fulfillment_status is available
45
+ // because the custom list endpoint might not return all fields correctly.
46
+ const fullOrders = await Promise.all(
47
+ (res.orders || []).map(async (order: any) => {
48
+ try {
49
+ return await retrieveOrder(order.id)
50
+ } catch (e) {
51
+ return order
52
+ }
53
+ })
54
+ )
55
+
56
+ return { orders: fullOrders, count: fullOrders.length }
57
+ }
58
+
59
+ export async function requestGuestOTP(email: string) {
60
+ const headers: Record<string, string> = {
61
+ "Content-Type": "application/json",
62
+ }
63
+ if (PUBLISHABLE_KEY) headers["x-publishable-api-key"] = PUBLISHABLE_KEY
64
+
65
+ try {
66
+ const res = await fetch(`${BACKEND_URL}/store/otp/request`, {
67
+ method: "POST",
68
+ headers,
69
+ body: JSON.stringify({ email }),
70
+ cache: "no-store",
71
+ })
72
+
73
+ if (!res.ok) {
74
+ const errorData = await res.json().catch(() => ({}))
75
+ throw new Error(errorData.message || "Failed to send OTP")
76
+ }
77
+ return { success: true }
78
+ } catch (error: any) {
79
+ return { success: false, error: error.message }
80
+ }
81
+ }
82
+
83
+ export async function verifyGuestOTP(email: string, otp: string) {
84
+ const headers: Record<string, string> = {
85
+ "Content-Type": "application/json",
86
+ }
87
+ if (PUBLISHABLE_KEY) headers["x-publishable-api-key"] = PUBLISHABLE_KEY
88
+
89
+ try {
90
+ const res = await fetch(`${BACKEND_URL}/store/otp/verify`, {
91
+ method: "POST",
92
+ headers,
93
+ body: JSON.stringify({ email, otp }),
94
+ cache: "no-store",
95
+ })
96
+
97
+ if (!res.ok) {
98
+ const errorData = await res.json().catch(() => ({}))
99
+ throw new Error(errorData.message || "Failed to verify OTP")
100
+ }
101
+
102
+ const data = await res.json()
103
+
104
+ if (data.token) {
105
+ const cookieStore = await cookies()
106
+ cookieStore.set("_medusa_guest_jwt", data.token, {
107
+ httpOnly: true,
108
+ secure: process.env.NODE_ENV === "production",
109
+ maxAge: 60 * 60 * 24, // 1 day
110
+ path: "/",
111
+ })
112
+ } else {
113
+ throw new Error("No token returned")
114
+ }
115
+
116
+ return { success: true, token: data.token }
117
+ } catch (error: any) {
118
+ return { success: false, error: error.message }
119
+ }
120
+ }
121
+
122
+ export async function logoutGuest() {
123
+ const cookieStore = await cookies()
124
+ cookieStore.set("_medusa_guest_jwt", "", {
125
+ maxAge: -1,
126
+ path: "/",
127
+ })
128
+ return { success: true }
129
+ }
130
+
131
+ export async function getGuestOrders(token?: string) {
132
+ const cookieStore = await cookies()
133
+ const headersStore = await headers()
134
+ const authHeader = headersStore.get("authorization")
135
+ const headerToken = authHeader?.startsWith("Bearer ")
136
+ ? authHeader.split(" ")[1]
137
+ : undefined
138
+
139
+ const authToken = token || headerToken || cookieStore.get("_medusa_guest_jwt")?.value
140
+ if (!authToken) return { success: false, error: "Unauthorized" }
141
+
142
+ const requestHeaders: Record<string, string> = {
143
+ Authorization: `Bearer ${authToken}`,
144
+ }
145
+ if (PUBLISHABLE_KEY) requestHeaders["x-publishable-api-key"] = PUBLISHABLE_KEY
146
+
147
+ try {
148
+ const response = await sdk.client.fetch<any>(`/store/guest-orders`, {
149
+ method: "GET",
150
+ headers: requestHeaders,
151
+ query: {
152
+ fields:
153
+ "id,display_id,status,fulfillment_status,payment_status,created_at,currency_code,total,subtotal,tax_total,shipping_total,discount_total,metadata,email,*items,*items.variant,*items.variant.product",
154
+ },
155
+ cache: "no-store",
156
+ })
157
+
158
+ return { success: true, orders: response.orders || [] }
159
+ } catch (error: any) {
160
+ return { success: false, error: error.message }
161
+ }
162
+ }
163
+
164
+ export async function getGuestOrder(id: string, token?: string) {
165
+ const cookieStore = await cookies()
166
+ const headersStore = await headers()
167
+ const authHeader = headersStore.get("authorization")
168
+ const headerToken = authHeader?.startsWith("Bearer ")
169
+ ? authHeader.split(" ")[1]
170
+ : undefined
171
+
172
+ const authToken = token || headerToken || cookieStore.get("_medusa_guest_jwt")?.value
173
+ if (!authToken) return { success: false, error: "Unauthorized" }
174
+
175
+ const requestHeaders: Record<string, string> = {
176
+ Authorization: `Bearer ${authToken}`,
177
+ }
178
+ if (PUBLISHABLE_KEY) requestHeaders["x-publishable-api-key"] = PUBLISHABLE_KEY
179
+
180
+ try {
181
+ const response = await sdk.client.fetch<any>(`/store/orders/${id}`, {
182
+ method: "GET",
183
+ headers: requestHeaders,
184
+ query: {
185
+ fields:
186
+ "*payment_collections.payments,*items,*items.metadata,*items.variant,*items.variant.images,*items.variant.product,*items.variant.product.thumbnail,*items.variant.product.images,*items.product,*items.product.thumbnail,*items.product.images,*fulfillments,*fulfillments.items,*fulfillments.location_id,*returns,*returns.items,*cart,*shipping_address,*billing_address,*region,*shipping_methods",
187
+ },
188
+ cache: "no-store",
189
+ })
190
+
191
+ if (response.order) {
192
+ return { success: true, order: response.order }
193
+ } else {
194
+ return { success: false, error: "Order not found" }
195
+ }
196
+ } catch (error: any) {
197
+ return { success: false, error: error.message }
198
+ }
199
+ }
200
+
201
+ export async function cancelGuestOrder(orderId: string, token?: string) {
202
+ const authHeaders = await getGuestAuthHeaders()
203
+ const authToken = token || (authHeaders as any).authorization?.split(" ")[1]
204
+
205
+ if (!authToken) return { success: false, error: "Unauthorized" }
206
+
207
+ const requestHeaders: Record<string, string> = {
208
+ Authorization: `Bearer ${authToken}`,
209
+ }
210
+ if (PUBLISHABLE_KEY) requestHeaders["x-publishable-api-key"] = PUBLISHABLE_KEY
211
+
212
+ try {
213
+ const response = await fetch(
214
+ `${BACKEND_URL}/store/guest-orders/${orderId}/cancel`,
215
+ {
216
+ method: "POST",
217
+ headers: {
218
+ "Content-Type": "application/json",
219
+ ...requestHeaders,
220
+ },
221
+ cache: "no-store",
222
+ }
223
+ )
224
+
225
+ if (!response.ok) {
226
+ const errorData = await response.json().catch(() => ({}))
227
+ throw new Error(errorData.message || "Failed to cancel order")
228
+ }
229
+
230
+ return { success: true }
231
+ } catch (error: any) {
232
+ return { success: false, error: error.message }
233
+ }
234
+ }
235
+
236
+ export async function getGuestOrderInvoice(orderId: string, token?: string) {
237
+ const authHeaders = await getGuestAuthHeaders()
238
+ const authToken = token || (authHeaders as any).authorization?.split(" ")[1]
239
+
240
+ if (!authToken) return { success: false, error: "Unauthorized" }
241
+
242
+ const requestHeaders: Record<string, string> = {
243
+ Authorization: `Bearer ${authToken}`,
244
+ }
245
+ if (PUBLISHABLE_KEY) requestHeaders["x-publishable-api-key"] = PUBLISHABLE_KEY
246
+
247
+ try {
248
+ const response = await fetch(
249
+ `${BACKEND_URL}/store/guest/invoice/download/${orderId}`,
250
+ {
251
+ method: "GET",
252
+ headers: requestHeaders,
253
+ cache: "no-store",
254
+ }
255
+ )
256
+
257
+ if (!response.ok) {
258
+ const errorData = await response.json().catch(() => ({}))
259
+ throw new Error(errorData.message || "Failed to get invoice")
260
+ }
261
+
262
+ const contentType = response.headers.get("content-type")
263
+ if (contentType && contentType.includes("application/pdf")) {
264
+ const arrayBuffer = await response.arrayBuffer()
265
+ const base64 = Buffer.from(arrayBuffer).toString("base64")
266
+ return { success: true, type: "pdf", data: base64 }
267
+ } else {
268
+ const data = await response.json()
269
+ return { success: true, type: "json", data }
270
+ }
271
+ } catch (error: any) {
272
+ return { success: false, error: error.message }
273
+ }
274
+ }
275
+
276
+ export async function guestReorder(orderId: string, token?: string) {
277
+ const authHeaders = await getGuestAuthHeaders()
278
+ const authToken = token || (authHeaders as any).authorization?.split(" ")[1]
279
+
280
+ if (!authToken) return { success: false, error: "Unauthorized" }
281
+
282
+ const requestHeaders: Record<string, string> = {
283
+ Authorization: `Bearer ${authToken}`,
284
+ }
285
+ if (PUBLISHABLE_KEY) requestHeaders["x-publishable-api-key"] = PUBLISHABLE_KEY
286
+
287
+ try {
288
+ const response = await fetch(
289
+ `${BACKEND_URL}/store/guest-orders/${orderId}/reorder`,
290
+ {
291
+ method: "POST",
292
+ headers: {
293
+ "Content-Type": "application/json",
294
+ ...requestHeaders,
295
+ },
296
+ cache: "no-store",
297
+ }
298
+ )
299
+
300
+ if (!response.ok) {
301
+ const errorData = await response.json().catch(() => ({}))
302
+ throw new Error(errorData.message || "Failed to reorder")
303
+ }
304
+
305
+ const data = await response.json()
306
+ return { success: true, data }
307
+ } catch (error: any) {
308
+ return { success: false, error: error.message }
309
+ }
310
+ }
311
+
312
+ export async function createGuestReturn(
313
+ orderId: string,
314
+ items: { id: string; quantity: number }[],
315
+ reason_id?: string,
316
+ note?: string,
317
+ token?: string
318
+ ) {
319
+ const authHeaders = await getGuestAuthHeaders()
320
+ const authToken = token || (authHeaders as any).authorization?.split(" ")[1]
321
+
322
+ if (!authToken) return { success: false, error: "Unauthorized" }
323
+
324
+ const requestHeaders: Record<string, string> = {
325
+ Authorization: `Bearer ${authToken}`,
326
+ }
327
+ if (PUBLISHABLE_KEY) requestHeaders["x-publishable-api-key"] = PUBLISHABLE_KEY
328
+
329
+ try {
330
+ const response = await fetch(
331
+ `${BACKEND_URL}/store/guest-orders/${orderId}/returns`,
332
+ {
333
+ method: "POST",
334
+ headers: {
335
+ "Content-Type": "application/json",
336
+ ...requestHeaders,
337
+ },
338
+ body: JSON.stringify({
339
+ items,
340
+ reason_id,
341
+ note,
342
+ }),
343
+ cache: "no-store",
344
+ }
345
+ )
346
+
347
+ if (!response.ok) {
348
+ const errorData = await response.json().catch(() => ({}))
349
+ throw new Error(errorData.message || "Failed to create return request")
350
+ }
351
+
352
+ const data = await response.json()
353
+ return { success: true, data }
354
+ } catch (error: any) {
355
+ return { success: false, error: error.message }
356
+ }
357
+ }
@@ -0,0 +1,74 @@
1
+ "use server"
2
+
3
+ import { sdk } from "@core/config"
4
+ import { revalidateTag } from "next/cache"
5
+ import { cookies as nextCookies } from "next/headers"
6
+ import { getAuthHeaders, getCacheTag, getCartId } from "./cookies"
7
+
8
+ const LOCALE_COOKIE_NAME = "_medusa_locale"
9
+
10
+ /**
11
+ * Gets the current locale from cookies
12
+ */
13
+ export const getLocale = async (): Promise<string | null> => {
14
+ try {
15
+ const cookies = await nextCookies()
16
+ return cookies.get(LOCALE_COOKIE_NAME)?.value ?? null
17
+ } catch {
18
+ return null
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Sets the locale cookie
24
+ */
25
+ export const setLocaleCookie = async (locale: string) => {
26
+ const cookies = await nextCookies()
27
+ cookies.set(LOCALE_COOKIE_NAME, locale, {
28
+ maxAge: 60 * 60 * 24 * 365, // 1 year
29
+ httpOnly: false, // Allow client-side access
30
+ sameSite: "strict",
31
+ secure: process.env.NODE_ENV === "production",
32
+ })
33
+ }
34
+
35
+ /**
36
+ * Updates the locale preference via SDK and stores in cookie.
37
+ * Also updates the cart with the new locale if one exists.
38
+ */
39
+ export const updateLocale = async (localeCode: string): Promise<string> => {
40
+ await setLocaleCookie(localeCode)
41
+
42
+ // Update cart with the new locale if a cart exists
43
+ const cartId = await getCartId()
44
+ if (cartId) {
45
+ const headers = {
46
+ ...(await getAuthHeaders()),
47
+ }
48
+
49
+ await sdk.store.cart.update(cartId, { locale: localeCode }, {}, headers)
50
+
51
+ const cartCacheTag = await getCacheTag("carts")
52
+ if (cartCacheTag) {
53
+ revalidateTag(cartCacheTag)
54
+ }
55
+ }
56
+
57
+ // Revalidate relevant caches to refresh content
58
+ const productsCacheTag = await getCacheTag("products")
59
+ if (productsCacheTag) {
60
+ revalidateTag(productsCacheTag)
61
+ }
62
+
63
+ const categoriesCacheTag = await getCacheTag("categories")
64
+ if (categoriesCacheTag) {
65
+ revalidateTag(categoriesCacheTag)
66
+ }
67
+
68
+ const collectionsCacheTag = await getCacheTag("collections")
69
+ if (collectionsCacheTag) {
70
+ revalidateTag(collectionsCacheTag)
71
+ }
72
+
73
+ return localeCode
74
+ }
@@ -0,0 +1,28 @@
1
+ "use server"
2
+
3
+ import { sdk } from "@core/config"
4
+ import { getCacheOptions } from "./cookies"
5
+
6
+ export type Locale = {
7
+ code: string
8
+ name: string
9
+ }
10
+
11
+ /**
12
+ * Fetches available locales from the backend.
13
+ * Returns null if the endpoint returns 404 (locales not configured).
14
+ */
15
+ export const listLocales = async (): Promise<Locale[] | null> => {
16
+ const next = {
17
+ ...(await getCacheOptions("locales")),
18
+ }
19
+
20
+ return sdk.client
21
+ .fetch<{ locales: Locale[] }>(`/store/locales`, {
22
+ method: "GET",
23
+ next,
24
+ cache: "force-cache",
25
+ })
26
+ .then(({ locales }) => locales)
27
+ .catch(() => null)
28
+ }
@@ -0,0 +1,41 @@
1
+ "use server"
2
+
3
+ export async function subscribeToNewsletter(email: string) {
4
+ const backendUrl = process.env.MEDUSA_BACKEND_URL || "http://localhost:9000"
5
+ const publishableKey = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY
6
+
7
+ try {
8
+ if (!publishableKey) {
9
+ return { success: false, message: "Server configuration error (missing key)" }
10
+ }
11
+
12
+ const headers: Record<string, string> = {
13
+ "Content-Type": "application/json",
14
+ "x-publishable-api-key": publishableKey,
15
+ }
16
+
17
+ const response = await fetch(`${backendUrl}/store/contact-email-subscriptions`, {
18
+ method: "POST",
19
+ headers,
20
+ body: JSON.stringify({
21
+ email: email,
22
+ status: "subscribed",
23
+ source: "footer",
24
+ }),
25
+ cache: "no-store",
26
+ })
27
+
28
+ if (!response.ok) {
29
+ const errorData = await response
30
+ .json()
31
+ .catch(() => ({ message: "Failed to subscribe" }))
32
+ return { success: false, message: errorData.message || `Failed to subscribe` }
33
+ }
34
+
35
+ const data = await response.json()
36
+
37
+ return { success: true, message: "Thank you for subscribing!", data }
38
+ } catch (error: any) {
39
+ return { success: false, message: error.message || "Something went wrong" }
40
+ }
41
+ }
@@ -0,0 +1,22 @@
1
+ "use server"
2
+
3
+ import { sdk } from "@core/config"
4
+ import { getAuthHeaders } from "./cookies"
5
+
6
+ export async function registerNotificationToken(token: string, deviceInfo?: any) {
7
+ const headers = (await getAuthHeaders()) as Record<string, string>
8
+
9
+ try {
10
+ const result = await sdk.client.fetch<any>(`/store/notification-tokens`, {
11
+ method: "POST",
12
+ body: {
13
+ token,
14
+ device_info: deviceInfo,
15
+ },
16
+ headers,
17
+ })
18
+ return result
19
+ } catch (error: any) {
20
+ return null
21
+ }
22
+ }
@@ -0,0 +1,9 @@
1
+ "use server"
2
+ import { cookies as nextCookies } from "next/headers"
3
+ import { redirect } from "next/navigation"
4
+
5
+ export async function resetOnboardingState(orderId: string) {
6
+ const cookies = await nextCookies()
7
+ cookies.set("_medusa_onboarding", "false", { maxAge: -1 })
8
+ redirect(`http://localhost:7001/a/orders/${orderId}`)
9
+ }