@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,638 @@
1
+ "use server"
2
+
3
+ import { sdk } from "@core/config"
4
+ import medusaError from "@core/util/medusa-error"
5
+ import { HttpTypes } from "@medusajs/types"
6
+ import { revalidateTag } from "next/cache"
7
+ import { redirect } from "next/navigation"
8
+ import { cookies } from "next/headers"
9
+ import {
10
+ getAuthHeaders,
11
+ getCacheOptions,
12
+ getCacheTag,
13
+ getCartId,
14
+ removeAuthToken,
15
+ removeCartId,
16
+ removeSyncLock,
17
+ setAuthToken,
18
+ setCartId,
19
+ } from "./cookies"
20
+
21
+ import { cache } from "react"
22
+ import { getGoogleOAuthCallbackUrl } from "@core/util/google-oauth"
23
+
24
+ export const retrieveCustomer = cache(
25
+ async (): Promise<HttpTypes.StoreCustomer | null> => {
26
+ const authHeaders = await getAuthHeaders()
27
+
28
+ if (!authHeaders) return null
29
+
30
+ const headers = {
31
+ ...authHeaders,
32
+ }
33
+
34
+ try {
35
+ const { customer } = await sdk.client.fetch<{
36
+ customer: HttpTypes.StoreCustomer
37
+ }>(`/store/customers/me`, {
38
+ method: "GET",
39
+ query: { fields: "*addresses" },
40
+ headers: headers as Record<string, string>,
41
+ cache: "no-store",
42
+ })
43
+ return customer
44
+ } catch (error: any) {
45
+ const errStr = error.toString().toLowerCase()
46
+ // If the error indicates a pending deletion, return a special object
47
+ if (
48
+ errStr.includes("active account deletion request") ||
49
+ (error.message &&
50
+ error.message.toLowerCase().includes("active account deletion request"))
51
+ ) {
52
+ const cookieStore = await cookies()
53
+ const cookieEmail = cookieStore.get("_medusa_customer_email")?.value
54
+ return { id: "pending_deletion", email: cookieEmail || "pending" } as any
55
+ }
56
+ return null
57
+ }
58
+ }
59
+ )
60
+
61
+ export const updateCustomer = async (body: HttpTypes.StoreUpdateCustomer) => {
62
+ const headers = {
63
+ ...(await getAuthHeaders()),
64
+ }
65
+
66
+ const updateRes = await sdk.store.customer
67
+ .update(body, {}, headers)
68
+ .then(({ customer }) => customer)
69
+ .catch(medusaError)
70
+
71
+ const cacheTag = await getCacheTag("customers")
72
+ revalidateTag(cacheTag)
73
+
74
+ return updateRes
75
+ }
76
+
77
+ export async function signup(_currentState: unknown, formData: FormData) {
78
+ const password = formData.get("password") as string
79
+ const customerForm = {
80
+ email: formData.get("email") as string,
81
+ first_name: formData.get("first_name") as string,
82
+ last_name: formData.get("last_name") as string,
83
+ phone: (formData.get("phone") as string)?.replace(/[^\d+]/g, ""),
84
+ }
85
+
86
+ try {
87
+ const token = await sdk.auth.register("customer", "emailpass", {
88
+ email: customerForm.email,
89
+ password: password,
90
+ })
91
+
92
+ await setAuthToken(token as string)
93
+
94
+ const headers = {
95
+ ...(await getAuthHeaders()),
96
+ }
97
+
98
+ const { customer: createdCustomer } = await sdk.store.customer.create(
99
+ customerForm,
100
+ {},
101
+ headers
102
+ )
103
+
104
+ const loginToken = await sdk.auth.login("customer", "emailpass", {
105
+ email: customerForm.email,
106
+ password,
107
+ })
108
+
109
+ await setAuthToken(loginToken as string)
110
+
111
+ const customerCacheTag = await getCacheTag("customers")
112
+ revalidateTag(customerCacheTag)
113
+
114
+ await transferCart(loginToken as string)
115
+
116
+ return createdCustomer
117
+ } catch (error: any) {
118
+ return error.toString()
119
+ }
120
+ }
121
+
122
+ export async function login(_currentState: unknown, formData: FormData) {
123
+ let email = formData.get("email_or_phone") as string
124
+ const password = formData.get("password") as string
125
+
126
+ // Sanitize phone number: remove spaces, dashes, etc if it looks like a phone number
127
+ // If it doesn't have '@', we treat it as a potential phone number and clean it
128
+ if (email && !email.includes("@")) {
129
+ email = email.replace(/[^\d+]/g, "")
130
+ }
131
+
132
+ let shouldRedirect = false
133
+ let countryCode = "in"
134
+
135
+ try {
136
+ const token = await sdk.auth.login("customer", "emailpass", {
137
+ email_or_phone: email,
138
+ password,
139
+ })
140
+ await setAuthToken(token as string)
141
+
142
+ // Store email in cookie for deletion flow (needed for cancellation modal)
143
+ const cookieStore = await cookies()
144
+ cookieStore.set("_medusa_customer_email", email, {
145
+ maxAge: 60 * 60 * 24 * 7,
146
+ path: "/",
147
+ })
148
+
149
+ // Force a check to see if we are blocked
150
+ try {
151
+ await sdk.store.customer.retrieve(
152
+ {},
153
+ {
154
+ authorization: `Bearer ${token}`,
155
+ }
156
+ )
157
+ } catch (err: any) {
158
+ const errStr = err.toString().toLowerCase()
159
+ if (errStr.includes("active account deletion request")) {
160
+ return "ACCOUNT_DELETION_PENDING"
161
+ }
162
+ // If it's another error, we might still want to know, but deletion is our priority
163
+ throw err
164
+ }
165
+
166
+ // Transfer cart after login success - Moved here to ensure execution
167
+ try {
168
+ await transferCart(token as string)
169
+ } catch (e) {
170
+ console.error("Transfer cart failed in login:", e)
171
+ }
172
+
173
+ const customerCacheTag = await getCacheTag("customers")
174
+ revalidateTag(customerCacheTag)
175
+
176
+ countryCode = (formData.get("country_code") as string) || "in"
177
+ shouldRedirect = true
178
+ } catch (error: any) {
179
+ const errorStr = error.toString().toLowerCase()
180
+
181
+ if (
182
+ errorStr.includes("active account deletion request") ||
183
+ (error.message &&
184
+ error.message.toLowerCase().includes("active account deletion request"))
185
+ ) {
186
+ // Still set email cookie even if blocked
187
+ const cookieStore = await cookies()
188
+ cookieStore.set("_medusa_customer_email", email, {
189
+ maxAge: 60 * 60 * 24 * 7,
190
+ path: "/",
191
+ })
192
+ return "ACCOUNT_DELETION_PENDING"
193
+ }
194
+
195
+ if (
196
+ errorStr.includes("invalid") ||
197
+ errorStr.includes("401") ||
198
+ errorStr.includes("not found")
199
+ ) {
200
+ return "Invalid credentials. Please try again."
201
+ }
202
+ return error.message || error.toString()
203
+ }
204
+
205
+ if (shouldRedirect) {
206
+ const redirectUrl = formData.get("redirect_url") as string
207
+
208
+ // Check if it's a "keep-me-here" context (Wishlist or Review Popups)
209
+ const hasKeepContext = redirectUrl?.includes("login_context=keep")
210
+
211
+ // Security + Context check
212
+ if (redirectUrl && redirectUrl.startsWith("/") && hasKeepContext) {
213
+ redirect(redirectUrl)
214
+ } else {
215
+ // DEFAULT: Always Home Page
216
+ redirect(`/${countryCode}`)
217
+ }
218
+ }
219
+ }
220
+
221
+ export async function signout(countryCode: string) {
222
+ await sdk.auth.logout()
223
+
224
+ await removeAuthToken()
225
+ await removeCartId()
226
+ await removeSyncLock()
227
+
228
+ const customerCacheTag = await getCacheTag("customers")
229
+ revalidateTag(customerCacheTag)
230
+
231
+ await removeCartId()
232
+
233
+ const cartCacheTag = await getCacheTag("carts")
234
+ revalidateTag(cartCacheTag)
235
+
236
+ redirect(`/${countryCode}`)
237
+ }
238
+
239
+ export async function transferCart(token?: string) {
240
+ const cartId = await getCartId()
241
+ const authHeaders = token
242
+ ? { authorization: `Bearer ${token}` }
243
+ : await getAuthHeaders()
244
+
245
+ console.log("--- TransferCart Debug ---")
246
+ console.log("Token provided:", !!token)
247
+ console.log("Guest Cart ID:", cartId)
248
+
249
+ const publishableKey = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY
250
+ const backendUrl =
251
+ process.env.MEDUSA_BACKEND_URL ||
252
+ process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL ||
253
+ "http://localhost:9000"
254
+
255
+ try {
256
+ console.log("Calling merge API (always)...")
257
+
258
+ const response = await fetch(`${backendUrl}/store/carts/merge`, {
259
+ method: "POST",
260
+ headers: {
261
+ "Content-Type": "application/json",
262
+ "x-publishable-api-key": publishableKey || "",
263
+ ...(authHeaders as any),
264
+ },
265
+ // Only include body if cartId exists
266
+ ...(cartId ? { body: JSON.stringify({ cart_id: cartId }) } : {}),
267
+ cache: "no-store",
268
+ })
269
+
270
+ if (response.ok) {
271
+ const data = await response.json()
272
+ console.log("--- Login Merge Response (JSON) ---")
273
+ console.log(JSON.stringify(data, null, 2))
274
+ console.log("-----------------------------------")
275
+
276
+ const activeCartId = data.cart?.id || data.id || data.cart_id
277
+
278
+ if (activeCartId) {
279
+ console.log("Storing Cart ID in cookie:", activeCartId)
280
+ await setCartId(activeCartId)
281
+ }
282
+ } else if (response.status === 404) {
283
+ // 404 is fine - it just means the customer has no active cart yet
284
+ console.log("No active cart found for this customer (expected).")
285
+ } else {
286
+ const errorText = await response.text()
287
+ console.log(`Merge API Info (${response.status}):`, errorText)
288
+ if (cartId) {
289
+ await sdk.store.cart.transferCart(cartId, {}, authHeaders).catch(() => {})
290
+ }
291
+ }
292
+ } catch (error: any) {
293
+ console.error("Merge API error:", error.message)
294
+ if (cartId) {
295
+ await sdk.store.cart.transferCart(cartId, {}, authHeaders).catch(() => {})
296
+ }
297
+ }
298
+
299
+ console.log("--------------------------")
300
+ const cartCacheTag = await getCacheTag("carts")
301
+ revalidateTag(cartCacheTag)
302
+ }
303
+
304
+ export const addCustomerAddress = async (
305
+ currentState: Record<string, unknown>,
306
+ formData: FormData
307
+ ): Promise<any> => {
308
+ const isDefaultBilling = (currentState.isDefaultBilling as boolean) || false
309
+ const isDefaultShipping = (currentState.isDefaultShipping as boolean) || false
310
+
311
+ const address: any = {
312
+ first_name: formData.get("first_name") as string,
313
+ last_name: formData.get("last_name") as string,
314
+ company: formData.get("company") as string,
315
+ address_1: formData.get("address_1") as string,
316
+ address_2: formData.get("address_2") as string,
317
+ city: formData.get("city") as string,
318
+ postal_code: formData.get("postal_code") as string,
319
+ province: formData.get("province") as string,
320
+ country_code: formData.get("country_code") as string,
321
+ phone: formData.get("phone") as string,
322
+ is_default_billing: isDefaultBilling,
323
+ is_default_shipping: isDefaultShipping,
324
+ metadata: {
325
+ address_type: (formData.get("address_type") as string) || "HOME",
326
+ },
327
+ }
328
+
329
+ const headers = {
330
+ ...(await getAuthHeaders()),
331
+ }
332
+
333
+ return sdk.store.customer
334
+ .createAddress(address, {}, headers)
335
+ .then(async ({ customer }) => {
336
+ const customerCacheTag = await getCacheTag("customers")
337
+ revalidateTag(customerCacheTag)
338
+ return { success: true, error: null }
339
+ })
340
+ .catch((err) => {
341
+ return { success: false, error: err.toString() }
342
+ })
343
+ }
344
+
345
+ export const deleteCustomerAddress = async (addressId: string): Promise<void> => {
346
+ const headers = {
347
+ ...(await getAuthHeaders()),
348
+ }
349
+
350
+ await sdk.store.customer
351
+ .deleteAddress(addressId, headers)
352
+ .then(async () => {
353
+ const customerCacheTag = await getCacheTag("customers")
354
+ revalidateTag(customerCacheTag)
355
+ return { success: true, error: null }
356
+ })
357
+ .catch((err) => {
358
+ return { success: false, error: err.toString() }
359
+ })
360
+ }
361
+
362
+ export const updateCustomerAddress = async (
363
+ currentState: Record<string, unknown>,
364
+ formData: FormData
365
+ ): Promise<any> => {
366
+ const addressId =
367
+ (currentState.addressId as string) || (formData.get("addressId") as string)
368
+
369
+ if (!addressId) {
370
+ return { success: false, error: "Address ID is required" }
371
+ }
372
+
373
+ const address = {
374
+ first_name: formData.get("first_name") as string,
375
+ last_name: formData.get("last_name") as string,
376
+ company: formData.get("company") as string,
377
+ address_1: formData.get("address_1") as string,
378
+ address_2: formData.get("address_2") as string,
379
+ city: formData.get("city") as string,
380
+ postal_code: formData.get("postal_code") as string,
381
+ province: formData.get("province") as string,
382
+ country_code: formData.get("country_code") as string,
383
+ metadata: {
384
+ address_type: (formData.get("address_type") as string) || "HOME",
385
+ },
386
+ } as any
387
+
388
+ const phone = formData.get("phone") as string
389
+
390
+ if (phone) {
391
+ address.phone = phone
392
+ }
393
+
394
+ const headers = {
395
+ ...(await getAuthHeaders()),
396
+ }
397
+
398
+ return sdk.store.customer
399
+ .updateAddress(addressId, address, {}, headers)
400
+ .then(async () => {
401
+ const customerCacheTag = await getCacheTag("customers")
402
+ revalidateTag(customerCacheTag)
403
+ return { success: true, error: null }
404
+ })
405
+ .catch((err) => {
406
+ return { success: false, error: err.toString() }
407
+ })
408
+ }
409
+
410
+ export const setDefaultAddress = async (addressId: string) => {
411
+ const headers = {
412
+ ...(await getAuthHeaders()),
413
+ }
414
+
415
+ return sdk.store.customer
416
+ .updateAddress(addressId, { is_default_shipping: true }, {}, headers)
417
+ .then(async () => {
418
+ const customerCacheTag = await getCacheTag("customers")
419
+ revalidateTag(customerCacheTag)
420
+ return { success: true, error: null }
421
+ })
422
+ .catch((err) => {
423
+ return { success: false, error: err.toString() }
424
+ })
425
+ }
426
+
427
+ export async function initiateGoogleAuth(countryCode?: string) {
428
+ try {
429
+ const backendUrl =
430
+ process.env.MEDUSA_BACKEND_URL || process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL
431
+
432
+ if (!backendUrl) {
433
+ return { error: "Backend URL not configured" }
434
+ }
435
+
436
+ const callbackUrl = getGoogleOAuthCallbackUrl(countryCode)
437
+
438
+ const response = await fetch(`${backendUrl}/auth/customer/google`, {
439
+ method: "POST",
440
+ headers: {
441
+ "Content-Type": "application/json",
442
+ },
443
+ body: JSON.stringify({ callback_url: callbackUrl }),
444
+ cache: "no-store",
445
+ })
446
+
447
+ if (!response.ok) {
448
+ return { error: "Google auth failed" }
449
+ }
450
+
451
+ const result = await response.json()
452
+ const redirectUrl =
453
+ typeof result === "string" ? result : (result as { location?: string })?.location
454
+
455
+ if (!redirectUrl) {
456
+ return { error: "No redirect URL received from Google OAuth" }
457
+ }
458
+
459
+ return { redirectUrl }
460
+ } catch (error: any) {
461
+ return { error: error.message || "Internal server error" }
462
+ }
463
+ }
464
+
465
+ export async function handleGoogleAuthCallback(params: Record<string, string>) {
466
+ try {
467
+ if (!params.code || !params.state) {
468
+ return { error: "Missing authentication parameters" }
469
+ }
470
+
471
+ const backendUrl =
472
+ process.env.MEDUSA_BACKEND_URL || process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL
473
+
474
+ if (!backendUrl) {
475
+ return { error: "Backend URL not configured" }
476
+ }
477
+
478
+ const query = new URLSearchParams({
479
+ code: params.code,
480
+ state: params.state,
481
+ })
482
+
483
+ const response = await fetch(
484
+ `${backendUrl}/auth/customer/google/callback?${query.toString()}`,
485
+ {
486
+ method: "GET",
487
+ headers: {
488
+ "Content-Type": "application/json",
489
+ },
490
+ cache: "no-store",
491
+ }
492
+ )
493
+
494
+ const data = await response.json().catch(() => ({}))
495
+
496
+ if (!response.ok) {
497
+ const message =
498
+ typeof data?.message === "string" ? data.message : "Authentication failed"
499
+ return { error: message }
500
+ }
501
+ const token = data.token || data
502
+
503
+ if (!token || typeof token !== "string") {
504
+ return { error: "No token received" }
505
+ }
506
+
507
+ return { token }
508
+ } catch (error: any) {
509
+ return { error: error.message || "Internal server error" }
510
+ }
511
+ }
512
+
513
+ export async function handleGoogleCallback(
514
+ token: string,
515
+ email?: string,
516
+ first_name?: string,
517
+ last_name?: string
518
+ ) {
519
+ try {
520
+ if (!token || typeof token !== "string") {
521
+ return { error: "Token is required" }
522
+ }
523
+
524
+ await setAuthToken(token)
525
+ await transferCart(token).catch(() => {})
526
+
527
+ if (email && typeof email === "string") {
528
+ const cookieStore = await cookies()
529
+ cookieStore.set("_medusa_customer_email", email, {
530
+ maxAge: 60 * 60 * 24 * 7,
531
+ path: "/",
532
+ })
533
+
534
+ const headers = await getAuthHeaders()
535
+ if (headers && "authorization" in headers) {
536
+ try {
537
+ await sdk.store.customer.create(
538
+ {
539
+ email,
540
+ ...(first_name && { first_name }),
541
+ ...(last_name && { last_name }),
542
+ },
543
+ {},
544
+ headers
545
+ )
546
+ } catch (err: any) {
547
+ if (
548
+ !err.message?.includes("already exists") &&
549
+ !err.message?.includes("duplicate")
550
+ ) {
551
+ }
552
+ }
553
+ }
554
+ }
555
+
556
+ const customerCacheTag = await getCacheTag("customers")
557
+ if (customerCacheTag) revalidateTag(customerCacheTag)
558
+
559
+ return { success: true }
560
+ } catch (error: any) {
561
+ return { error: error.message || "Internal server error" }
562
+ }
563
+ }
564
+
565
+ export async function uploadFiles(formData: FormData) {
566
+ try {
567
+ const authHeaders = await getAuthHeaders()
568
+ const publishableKey = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY
569
+ const backendUrl =
570
+ process.env.MEDUSA_BACKEND_URL || process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL
571
+
572
+ const response = await fetch(`${backendUrl}/store/upload`, {
573
+ method: "POST",
574
+ headers: {
575
+ ...(authHeaders as Record<string, string>),
576
+ "x-publishable-api-key": publishableKey || "",
577
+ },
578
+ body: formData,
579
+ })
580
+
581
+ if (!response.ok) {
582
+ const errorData = await response.json().catch(() => ({}))
583
+ throw new Error(errorData.message || "Failed to upload to server")
584
+ }
585
+
586
+ const data = await response.json()
587
+ // Medusa usually returns { files: [{ url: '...' }] }
588
+ const files = data.files || []
589
+ return { success: true, files }
590
+ } catch (error: any) {
591
+ return { success: false, error: error.message || "Failed to upload" }
592
+ }
593
+ }
594
+
595
+ export async function uploadProfileImage(formData: FormData) {
596
+ try {
597
+ const imageFile = formData.get("image") as File
598
+
599
+ if (!imageFile) {
600
+ return { error: "No image file provided" }
601
+ }
602
+
603
+ // Create new FormData with 'files' field as expected by the backend
604
+ const uploadFormData = new FormData()
605
+ uploadFormData.append("files", imageFile)
606
+
607
+ const uploadResult = await uploadFiles(uploadFormData)
608
+ if (!uploadResult.success) {
609
+ return { error: uploadResult.error }
610
+ }
611
+
612
+ const imageUrl = uploadResult.files?.[0]?.url
613
+
614
+ if (!imageUrl) {
615
+ throw new Error("No image URL returned from server")
616
+ }
617
+
618
+ // Fetch current customer to preserve existing metadata
619
+ const currentCustomer = await retrieveCustomer()
620
+ const existingMetadata = (currentCustomer?.metadata as Record<string, any>) || {}
621
+ const newMetadata = {
622
+ ...existingMetadata,
623
+ profile_image_url: imageUrl,
624
+ }
625
+
626
+ // Update customer metadata
627
+ await updateCustomer({ metadata: newMetadata })
628
+
629
+ const customerCacheTag = await getCacheTag("customers")
630
+ if (customerCacheTag) {
631
+ revalidateTag(customerCacheTag)
632
+ }
633
+
634
+ return { success: true, imageUrl }
635
+ } catch (error: any) {
636
+ return { error: error.message || "Failed to upload image" }
637
+ }
638
+ }