@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,184 @@
|
|
|
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, revalidatePath } from "next/cache"
|
|
7
|
+
import { redirect } from "next/navigation"
|
|
8
|
+
import { headers as getRequestHeaders } from "next/headers"
|
|
9
|
+
import {
|
|
10
|
+
getAuthHeaders,
|
|
11
|
+
getCacheOptions,
|
|
12
|
+
getCacheTag,
|
|
13
|
+
getCartId,
|
|
14
|
+
removeCartId,
|
|
15
|
+
setCartId,
|
|
16
|
+
getHoldCartId,
|
|
17
|
+
setHoldCartId,
|
|
18
|
+
removeHoldCartId,
|
|
19
|
+
setBuyNowCartId,
|
|
20
|
+
removeBuyNowCartId,
|
|
21
|
+
} from "../cookies"
|
|
22
|
+
import { getRegion } from "../regions"
|
|
23
|
+
import { getLocale } from "@core/data/locale-actions"
|
|
24
|
+
import { retrieveCustomer, transferCart } from "@core/data/customer"
|
|
25
|
+
import { cache } from "react"
|
|
26
|
+
import {
|
|
27
|
+
createBuyNowMetadata,
|
|
28
|
+
filterOutCurrentCart,
|
|
29
|
+
getBuyNowCheckoutUrl,
|
|
30
|
+
getPostOrderCookieAction,
|
|
31
|
+
getResumeAbandonedCheckoutUrl,
|
|
32
|
+
isEphemeralCart,
|
|
33
|
+
normalizePhoneForRazorpay,
|
|
34
|
+
partitionAbandonedCarts,
|
|
35
|
+
shouldCheckEphemeralCartMetadata,
|
|
36
|
+
shouldSkipToPayment,
|
|
37
|
+
validateAddToCartInput,
|
|
38
|
+
validateCartId,
|
|
39
|
+
validateLineItemDeleteInput,
|
|
40
|
+
validateLineItemUpdateInput,
|
|
41
|
+
validateRazorpayPreflight,
|
|
42
|
+
} from "@core/domain/cart"
|
|
43
|
+
|
|
44
|
+
export async function buyNow({
|
|
45
|
+
variantId,
|
|
46
|
+
quantity,
|
|
47
|
+
countryCode,
|
|
48
|
+
}: {
|
|
49
|
+
variantId: string
|
|
50
|
+
quantity: number
|
|
51
|
+
countryCode: string
|
|
52
|
+
}) {
|
|
53
|
+
validateAddToCartInput({ variantId, quantity })
|
|
54
|
+
|
|
55
|
+
const region = await getRegion(countryCode)
|
|
56
|
+
|
|
57
|
+
if (!region) {
|
|
58
|
+
throw new Error(`Region not found for country code: ${countryCode}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const headers = {
|
|
62
|
+
...(await getAuthHeaders()),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const locale = await getLocale()
|
|
66
|
+
|
|
67
|
+
// 1. Create a NEW cart regardless of existing one
|
|
68
|
+
const cartResp = await sdk.store.cart.create(
|
|
69
|
+
{
|
|
70
|
+
region_id: region.id,
|
|
71
|
+
locale: locale || undefined,
|
|
72
|
+
metadata: createBuyNowMetadata(),
|
|
73
|
+
},
|
|
74
|
+
{},
|
|
75
|
+
headers
|
|
76
|
+
)
|
|
77
|
+
const cart = cartResp.cart
|
|
78
|
+
|
|
79
|
+
// Save the new cart id in "buy_now_cart_id" cookie
|
|
80
|
+
await removeBuyNowCartId()
|
|
81
|
+
await setBuyNowCartId(cart.id)
|
|
82
|
+
|
|
83
|
+
// Ensure this new cart is not merged with existing ones
|
|
84
|
+
try {
|
|
85
|
+
await sdk.client.fetch(`/store/carts/merge/skip`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
body: { cart_id: cart.id },
|
|
88
|
+
headers,
|
|
89
|
+
})
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.error("Failed to skip cart merge for buy now cart:", e)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 2. Add the item to this new cart
|
|
95
|
+
await sdk.store.cart.createLineItem(
|
|
96
|
+
cart.id,
|
|
97
|
+
{
|
|
98
|
+
variant_id: variantId,
|
|
99
|
+
quantity,
|
|
100
|
+
},
|
|
101
|
+
{},
|
|
102
|
+
headers
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
// 3. (REMOVED: No longer overwriting the main cart ID, because getCartId handles buy_now_cart_id priority)
|
|
106
|
+
|
|
107
|
+
// 4. OPTIMIZATION: If user is logged in and has addresses, auto-fill and skip to payment
|
|
108
|
+
const customer = await retrieveCustomer().catch(() => null)
|
|
109
|
+
const defaultAddress =
|
|
110
|
+
customer?.addresses?.find(
|
|
111
|
+
(a) =>
|
|
112
|
+
a.is_default_shipping ||
|
|
113
|
+
a.metadata?.is_default === "true" ||
|
|
114
|
+
a.metadata?.is_default === true
|
|
115
|
+
) || customer?.addresses?.[0]
|
|
116
|
+
|
|
117
|
+
let skipToPayment = false
|
|
118
|
+
|
|
119
|
+
if (defaultAddress) {
|
|
120
|
+
try {
|
|
121
|
+
// Set addresses
|
|
122
|
+
await sdk.store.cart.update(
|
|
123
|
+
cart.id,
|
|
124
|
+
{
|
|
125
|
+
shipping_address: {
|
|
126
|
+
first_name: defaultAddress.first_name,
|
|
127
|
+
last_name: defaultAddress.last_name,
|
|
128
|
+
address_1: defaultAddress.address_1,
|
|
129
|
+
address_2: defaultAddress.address_2,
|
|
130
|
+
city: defaultAddress.city,
|
|
131
|
+
country_code: defaultAddress.country_code,
|
|
132
|
+
postal_code: defaultAddress.postal_code,
|
|
133
|
+
province: defaultAddress.province,
|
|
134
|
+
phone: defaultAddress.phone,
|
|
135
|
+
company: defaultAddress.company,
|
|
136
|
+
},
|
|
137
|
+
billing_address: {
|
|
138
|
+
first_name: defaultAddress.first_name,
|
|
139
|
+
last_name: defaultAddress.last_name,
|
|
140
|
+
address_1: defaultAddress.address_1,
|
|
141
|
+
address_2: defaultAddress.address_2,
|
|
142
|
+
city: defaultAddress.city,
|
|
143
|
+
country_code: defaultAddress.country_code,
|
|
144
|
+
postal_code: defaultAddress.postal_code,
|
|
145
|
+
province: defaultAddress.province,
|
|
146
|
+
phone: defaultAddress.phone,
|
|
147
|
+
company: defaultAddress.company,
|
|
148
|
+
},
|
|
149
|
+
email: customer?.email,
|
|
150
|
+
},
|
|
151
|
+
{},
|
|
152
|
+
headers
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
// Fetch and set first shipping method
|
|
156
|
+
const { shipping_options } = await sdk.client.fetch<{
|
|
157
|
+
shipping_options: HttpTypes.StoreCartShippingOption[]
|
|
158
|
+
}>("/store/shipping-options", {
|
|
159
|
+
query: { cart_id: cart.id },
|
|
160
|
+
headers,
|
|
161
|
+
cache: "no-store",
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
if (shouldSkipToPayment(shipping_options?.length ?? 0)) {
|
|
165
|
+
await sdk.store.cart.addShippingMethod(
|
|
166
|
+
cart.id,
|
|
167
|
+
{ option_id: shipping_options[0].id },
|
|
168
|
+
{},
|
|
169
|
+
headers
|
|
170
|
+
)
|
|
171
|
+
skipToPayment = true
|
|
172
|
+
}
|
|
173
|
+
} catch (e) {
|
|
174
|
+
// Fallback to normal checkout if anything fails
|
|
175
|
+
console.error("Failed to auto-fill address and shipping:", e)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const cartCacheTag = await getCacheTag("carts")
|
|
180
|
+
revalidateTag(cartCacheTag)
|
|
181
|
+
revalidatePath(`/${countryCode}/checkout`, "page")
|
|
182
|
+
|
|
183
|
+
redirect(getBuyNowCheckoutUrl(countryCode, cart.id, skipToPayment))
|
|
184
|
+
}
|
|
@@ -0,0 +1,487 @@
|
|
|
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, revalidatePath } from "next/cache"
|
|
7
|
+
import { redirect } from "next/navigation"
|
|
8
|
+
import { headers as getRequestHeaders } from "next/headers"
|
|
9
|
+
import {
|
|
10
|
+
getAuthHeaders,
|
|
11
|
+
getCacheOptions,
|
|
12
|
+
getCacheTag,
|
|
13
|
+
getCartId,
|
|
14
|
+
removeCartId,
|
|
15
|
+
setCartId,
|
|
16
|
+
getHoldCartId,
|
|
17
|
+
setHoldCartId,
|
|
18
|
+
removeHoldCartId,
|
|
19
|
+
setBuyNowCartId,
|
|
20
|
+
removeBuyNowCartId,
|
|
21
|
+
} from "../cookies"
|
|
22
|
+
import { getRegion } from "../regions"
|
|
23
|
+
import { getLocale } from "@core/data/locale-actions"
|
|
24
|
+
import { retrieveCustomer, transferCart } from "@core/data/customer"
|
|
25
|
+
import { cache } from "react"
|
|
26
|
+
import {
|
|
27
|
+
createBuyNowMetadata,
|
|
28
|
+
filterOutCurrentCart,
|
|
29
|
+
getBuyNowCheckoutUrl,
|
|
30
|
+
getPostOrderCookieAction,
|
|
31
|
+
getResumeAbandonedCheckoutUrl,
|
|
32
|
+
isEphemeralCart,
|
|
33
|
+
normalizePhoneForRazorpay,
|
|
34
|
+
partitionAbandonedCarts,
|
|
35
|
+
shouldCheckEphemeralCartMetadata,
|
|
36
|
+
shouldSkipToPayment,
|
|
37
|
+
validateAddToCartInput,
|
|
38
|
+
validateCartId,
|
|
39
|
+
validateLineItemDeleteInput,
|
|
40
|
+
validateLineItemUpdateInput,
|
|
41
|
+
validateRazorpayPreflight,
|
|
42
|
+
} from "@core/domain/cart"
|
|
43
|
+
import { retrieveCart } from "./retrieve"
|
|
44
|
+
import { updateCart } from "./mutations"
|
|
45
|
+
|
|
46
|
+
export async function setShippingMethod({
|
|
47
|
+
cartId,
|
|
48
|
+
shippingMethodId,
|
|
49
|
+
}: {
|
|
50
|
+
cartId: string
|
|
51
|
+
shippingMethodId: string
|
|
52
|
+
}) {
|
|
53
|
+
const headers = {
|
|
54
|
+
...(await getAuthHeaders()),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return sdk.store.cart
|
|
58
|
+
.addShippingMethod(cartId, { option_id: shippingMethodId }, {}, headers)
|
|
59
|
+
.then(async () => {
|
|
60
|
+
const cartCacheTag = await getCacheTag("carts")
|
|
61
|
+
revalidateTag(cartCacheTag)
|
|
62
|
+
})
|
|
63
|
+
.catch(medusaError)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Sets a shipping method silently without triggering a full page revalidation.
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
export async function setShippingMethodSilently({
|
|
71
|
+
cartId,
|
|
72
|
+
shippingMethodId,
|
|
73
|
+
}: {
|
|
74
|
+
cartId: string
|
|
75
|
+
shippingMethodId: string
|
|
76
|
+
}) {
|
|
77
|
+
const headers = {
|
|
78
|
+
...(await getAuthHeaders()),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await sdk.store.cart.addShippingMethod(
|
|
83
|
+
cartId,
|
|
84
|
+
{ option_id: shippingMethodId },
|
|
85
|
+
{},
|
|
86
|
+
headers
|
|
87
|
+
)
|
|
88
|
+
return { success: true }
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return { success: false }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function initiatePaymentSession(
|
|
95
|
+
cart: HttpTypes.StoreCart,
|
|
96
|
+
data: HttpTypes.StoreInitializePaymentSession
|
|
97
|
+
) {
|
|
98
|
+
const headers = {
|
|
99
|
+
...(await getAuthHeaders()),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 1. RE-FETCH cart with NO CACHE to ensure we have the latest data from the DB
|
|
103
|
+
const latestCart = await retrieveCart(cart.id)
|
|
104
|
+
if (!latestCart) throw new Error("Cart not found")
|
|
105
|
+
|
|
106
|
+
const rawPhone =
|
|
107
|
+
latestCart.shipping_address?.phone || latestCart.billing_address?.phone
|
|
108
|
+
const phone = normalizePhoneForRazorpay(rawPhone)
|
|
109
|
+
|
|
110
|
+
// 2. Critical Fix for Razorpay: Ensure normalized phone is in BOTH shipping and billing addresses
|
|
111
|
+
if (phone) {
|
|
112
|
+
const addressUpdates: any = {}
|
|
113
|
+
|
|
114
|
+
// Clean address helper to ensure we pass a clean object to the update API
|
|
115
|
+
const cleanAddress = (addr: any) => {
|
|
116
|
+
if (!addr) return null
|
|
117
|
+
return {
|
|
118
|
+
first_name: addr.first_name,
|
|
119
|
+
last_name: addr.last_name,
|
|
120
|
+
address_1: addr.address_1,
|
|
121
|
+
address_2: addr.address_2 || "",
|
|
122
|
+
city: addr.city,
|
|
123
|
+
country_code: addr.country_code,
|
|
124
|
+
postal_code: addr.postal_code,
|
|
125
|
+
province: addr.province,
|
|
126
|
+
company: addr.company,
|
|
127
|
+
phone: phone,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Update if phone is missing in DB
|
|
132
|
+
if (latestCart.shipping_address && !latestCart.shipping_address.phone) {
|
|
133
|
+
addressUpdates.shipping_address = cleanAddress(latestCart.shipping_address)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (latestCart.billing_address && !latestCart.billing_address.phone) {
|
|
137
|
+
addressUpdates.billing_address =
|
|
138
|
+
cleanAddress(latestCart.billing_address) ||
|
|
139
|
+
cleanAddress(latestCart.shipping_address)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (Object.keys(addressUpdates).length > 0) {
|
|
143
|
+
await sdk.store.cart.update(latestCart.id, addressUpdates, {}, headers)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 3. Final re-fetch (direct SDK call to bypass any lib-level cache)
|
|
148
|
+
const cartResponse = await sdk.store.cart.retrieve(
|
|
149
|
+
cart.id,
|
|
150
|
+
{ fields: "*shipping_address,*billing_address" },
|
|
151
|
+
headers
|
|
152
|
+
)
|
|
153
|
+
const finalCart = cartResponse.cart
|
|
154
|
+
|
|
155
|
+
const displayPhone =
|
|
156
|
+
finalCart.shipping_address?.phone || finalCart.billing_address?.phone || phone
|
|
157
|
+
|
|
158
|
+
validateRazorpayPreflight({
|
|
159
|
+
providerId: data.provider_id,
|
|
160
|
+
phone: displayPhone,
|
|
161
|
+
email: finalCart.email,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// 5. ENHANCED DATA FOR RAZORPAY:
|
|
165
|
+
const enhancedData = {
|
|
166
|
+
...data,
|
|
167
|
+
data: {
|
|
168
|
+
...(data.data || {}),
|
|
169
|
+
billing_address: finalCart.billing_address || finalCart.shipping_address,
|
|
170
|
+
customer: {
|
|
171
|
+
email: finalCart.email,
|
|
172
|
+
phone: displayPhone,
|
|
173
|
+
first_name:
|
|
174
|
+
finalCart.billing_address?.first_name ||
|
|
175
|
+
finalCart.shipping_address?.first_name,
|
|
176
|
+
last_name:
|
|
177
|
+
finalCart.billing_address?.last_name || finalCart.shipping_address?.last_name,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return sdk.store.payment
|
|
183
|
+
.initiatePaymentSession(finalCart, enhancedData, {}, headers)
|
|
184
|
+
.then(async (resp) => {
|
|
185
|
+
const cartCacheTag = await getCacheTag("carts")
|
|
186
|
+
revalidateTag(cartCacheTag)
|
|
187
|
+
return resp
|
|
188
|
+
})
|
|
189
|
+
.catch((e) => {
|
|
190
|
+
const errorMsg = e.message || e.response?.data?.message || "Unknown error"
|
|
191
|
+
throw new Error(`Razorpay Initialization Failed: ${errorMsg}`)
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function setAddresses(currentState: unknown, formData: FormData) {
|
|
196
|
+
try {
|
|
197
|
+
if (!formData) {
|
|
198
|
+
throw new Error("No form data found when setting addresses")
|
|
199
|
+
}
|
|
200
|
+
const cartId = await getCartId()
|
|
201
|
+
if (!cartId) {
|
|
202
|
+
throw new Error("No existing cart found when setting addresses")
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const shippingCountryCode =
|
|
206
|
+
(formData.get("shipping_address.country_code") as string)?.toLowerCase() || "in"
|
|
207
|
+
|
|
208
|
+
const rawPhone = formData.get("shipping_address.phone") as string
|
|
209
|
+
const phone = normalizePhoneForRazorpay(rawPhone)
|
|
210
|
+
|
|
211
|
+
const shippingAddress = {
|
|
212
|
+
first_name: formData.get("shipping_address.first_name") as string,
|
|
213
|
+
last_name: formData.get("shipping_address.last_name") as string,
|
|
214
|
+
address_1: formData.get("shipping_address.address_1") as string,
|
|
215
|
+
address_2: (formData.get("shipping_address.address_2") as string) || "",
|
|
216
|
+
company: formData.get("shipping_address.company") as string,
|
|
217
|
+
postal_code: formData.get("shipping_address.postal_code") as string,
|
|
218
|
+
city: formData.get("shipping_address.city") as string,
|
|
219
|
+
country_code: shippingCountryCode,
|
|
220
|
+
province: formData.get("shipping_address.province") as string,
|
|
221
|
+
phone: phone as string,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const data = {
|
|
225
|
+
shipping_address: shippingAddress,
|
|
226
|
+
email: formData.get("email"),
|
|
227
|
+
} as any
|
|
228
|
+
|
|
229
|
+
// Save address to customer profile if logged in
|
|
230
|
+
const authHeaders = await getAuthHeaders()
|
|
231
|
+
if (authHeaders && "authorization" in authHeaders) {
|
|
232
|
+
try {
|
|
233
|
+
// Fetch current customer to check if address already exists
|
|
234
|
+
const { customer } = await sdk.client.fetch<{
|
|
235
|
+
customer: HttpTypes.StoreCustomer
|
|
236
|
+
}>(`/store/customers/me`, {
|
|
237
|
+
method: "GET",
|
|
238
|
+
query: { fields: "*addresses" },
|
|
239
|
+
headers: authHeaders as Record<string, string>,
|
|
240
|
+
cache: "no-store",
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const addressExists = customer?.addresses?.some(
|
|
244
|
+
(a: any) =>
|
|
245
|
+
a.address_1 === shippingAddress.address_1 &&
|
|
246
|
+
a.postal_code === shippingAddress.postal_code &&
|
|
247
|
+
a.city === shippingAddress.city &&
|
|
248
|
+
a.first_name === shippingAddress.first_name &&
|
|
249
|
+
a.last_name === shippingAddress.last_name
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if (!addressExists) {
|
|
253
|
+
await sdk.store.customer.createAddress(
|
|
254
|
+
shippingAddress,
|
|
255
|
+
{},
|
|
256
|
+
authHeaders as Record<string, string>
|
|
257
|
+
)
|
|
258
|
+
const customerCacheTag = await getCacheTag("customers")
|
|
259
|
+
revalidateTag(customerCacheTag)
|
|
260
|
+
}
|
|
261
|
+
} catch (e) {
|
|
262
|
+
// Silently fail when saving address to customer profile
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const sameAsBilling = formData.get("same_as_billing")
|
|
267
|
+
if (sameAsBilling === "on") {
|
|
268
|
+
data.billing_address = { ...data.shipping_address }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (sameAsBilling !== "on") {
|
|
272
|
+
const bPhone =
|
|
273
|
+
normalizePhoneForRazorpay(formData.get("billing_address.phone") as string) ||
|
|
274
|
+
phone
|
|
275
|
+
data.billing_address = {
|
|
276
|
+
first_name: formData.get("billing_address.first_name"),
|
|
277
|
+
last_name: formData.get("billing_address.last_name"),
|
|
278
|
+
address_1: formData.get("billing_address.address_1"),
|
|
279
|
+
address_2: formData.get("billing_address.address_2") || "",
|
|
280
|
+
company: formData.get("billing_address.company"),
|
|
281
|
+
postal_code: formData.get("billing_address.postal_code"),
|
|
282
|
+
city: formData.get("billing_address.city"),
|
|
283
|
+
country_code: shippingCountryCode,
|
|
284
|
+
province: formData.get("billing_address.province"),
|
|
285
|
+
phone: bPhone,
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
await updateCart(data)
|
|
289
|
+
} catch (e: any) {
|
|
290
|
+
return e.message
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
revalidateTag("carts")
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Places an order for a cart. If no cart ID is provided, it will use the cart ID from the cookies.
|
|
298
|
+
* @param cartId - optional - The ID of the cart to place an order for.
|
|
299
|
+
* @returns The cart object if the order was successful, or null if not.
|
|
300
|
+
*/
|
|
301
|
+
|
|
302
|
+
export async function placeOrder(cartId?: string) {
|
|
303
|
+
const id = cartId || (await getCartId())
|
|
304
|
+
|
|
305
|
+
if (!id) {
|
|
306
|
+
throw new Error("No existing cart found when placing an order")
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const headers = {
|
|
310
|
+
...(await getAuthHeaders()),
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const cartRes = await sdk.store.cart
|
|
314
|
+
.complete(id, {}, headers)
|
|
315
|
+
.then(async (cartRes) => {
|
|
316
|
+
const cartCacheTag = await getCacheTag("carts")
|
|
317
|
+
revalidateTag(cartCacheTag)
|
|
318
|
+
return cartRes
|
|
319
|
+
})
|
|
320
|
+
.catch(medusaError)
|
|
321
|
+
|
|
322
|
+
if (cartRes?.type === "order") {
|
|
323
|
+
const countryCode = cartRes.order.shipping_address?.country_code?.toLowerCase()
|
|
324
|
+
|
|
325
|
+
const orderCacheTag = await getCacheTag("orders")
|
|
326
|
+
revalidateTag(orderCacheTag)
|
|
327
|
+
|
|
328
|
+
// NEW LOGIC: Check if we just completed a Buy Now cart
|
|
329
|
+
const { cookies: nextCookies } = await import("next/headers")
|
|
330
|
+
const cookiesStore = await nextCookies()
|
|
331
|
+
const buyNowId = cookiesStore.get("buy_now_cart_id")?.value
|
|
332
|
+
|
|
333
|
+
const cookieAction = getPostOrderCookieAction(!!buyNowId)
|
|
334
|
+
|
|
335
|
+
if (cookieAction === "remove_buy_now") {
|
|
336
|
+
await removeBuyNowCartId()
|
|
337
|
+
} else {
|
|
338
|
+
await removeCartId()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
redirect(`/${countryCode}/order/${cartRes?.order.id}/confirmed`)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return cartRes.cart
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Updates the countrycode param and revalidates the regions cache
|
|
349
|
+
* @param regionId
|
|
350
|
+
* @param countryCode
|
|
351
|
+
*/
|
|
352
|
+
|
|
353
|
+
export async function addCustomerAddressToCart(
|
|
354
|
+
currentState: unknown,
|
|
355
|
+
formData: FormData
|
|
356
|
+
) {
|
|
357
|
+
try {
|
|
358
|
+
const headers = {
|
|
359
|
+
...(await getAuthHeaders()),
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const shippingCountryCode =
|
|
363
|
+
(formData.get("shipping_address.country_code") as string)?.toLowerCase() || "in"
|
|
364
|
+
|
|
365
|
+
// Parse address data from formData (which uses shipping_address. prefix)
|
|
366
|
+
const addressData = {
|
|
367
|
+
first_name: formData.get("shipping_address.first_name") as string,
|
|
368
|
+
last_name: formData.get("shipping_address.last_name") as string,
|
|
369
|
+
company: formData.get("shipping_address.company") as string,
|
|
370
|
+
address_1: formData.get("shipping_address.address_1") as string,
|
|
371
|
+
address_2: formData.get("shipping_address.address_2") as string,
|
|
372
|
+
city: formData.get("shipping_address.city") as string,
|
|
373
|
+
postal_code: formData.get("shipping_address.postal_code") as string,
|
|
374
|
+
province: formData.get("shipping_address.province") as string,
|
|
375
|
+
country_code: shippingCountryCode,
|
|
376
|
+
phone: formData.get("shipping_address.phone") as string,
|
|
377
|
+
metadata: {
|
|
378
|
+
is_default: formData.get("is_default") === "true",
|
|
379
|
+
address_type: formData.get("address_type"),
|
|
380
|
+
},
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const addressId = formData.get("address_id") as string
|
|
384
|
+
|
|
385
|
+
// 1. Add/Update Customer Address Book
|
|
386
|
+
// We try/catch this separately so if it fails (e.g. user not logged in), we still try to proceed with checkout
|
|
387
|
+
try {
|
|
388
|
+
if (addressId) {
|
|
389
|
+
await sdk.store.customer.updateAddress(addressId, addressData, {}, headers)
|
|
390
|
+
} else {
|
|
391
|
+
await sdk.store.customer.createAddress(addressData, {}, headers)
|
|
392
|
+
}
|
|
393
|
+
const customerCacheTag = await getCacheTag("customers")
|
|
394
|
+
revalidateTag(customerCacheTag)
|
|
395
|
+
} catch (e) {
|
|
396
|
+
// verify if it is an auth error, maybe we shouldn't fail silently?
|
|
397
|
+
// But for checkout flow, the priority is to proceed.
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// 2. Set as Cart Shipping Address
|
|
401
|
+
const cartId = await getCartId()
|
|
402
|
+
if (!cartId) {
|
|
403
|
+
throw new Error("No existing cart found when setting addresses")
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const cartData = {
|
|
407
|
+
shipping_address: {
|
|
408
|
+
first_name: addressData.first_name,
|
|
409
|
+
last_name: addressData.last_name,
|
|
410
|
+
company: addressData.company,
|
|
411
|
+
address_1: addressData.address_1,
|
|
412
|
+
address_2: addressData.address_2,
|
|
413
|
+
city: addressData.city,
|
|
414
|
+
postal_code: addressData.postal_code,
|
|
415
|
+
province: addressData.province,
|
|
416
|
+
country_code: addressData.country_code,
|
|
417
|
+
phone: addressData.phone,
|
|
418
|
+
},
|
|
419
|
+
email: formData.get("email"),
|
|
420
|
+
} as any
|
|
421
|
+
|
|
422
|
+
const sameAsBilling = formData.get("same_as_billing")
|
|
423
|
+
if (sameAsBilling === "on") {
|
|
424
|
+
cartData.billing_address = cartData.shipping_address
|
|
425
|
+
} else {
|
|
426
|
+
// If not same as billing, we should ideally handle billing address too,
|
|
427
|
+
// but AddAddressModal usually forces same_as_billing="on" (Line 52 in AddAddressModal).
|
|
428
|
+
// If we ever change that, we'll need logic here. For now, AddAddressModal is strictly for "Add New Shippping Address".
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
await updateCart(cartData)
|
|
432
|
+
} catch (e: any) {
|
|
433
|
+
return e.message
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
revalidateTag("carts")
|
|
437
|
+
// No redirect if we are already in checkout to prevent disrupting the user
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Updates address in the background without redirecting.
|
|
442
|
+
* Used for real-time shipping calculation when pincode is entered.
|
|
443
|
+
*/
|
|
444
|
+
|
|
445
|
+
export async function updateAddressSilently(data: any) {
|
|
446
|
+
const cartId = await getCartId()
|
|
447
|
+
if (!cartId) return
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
const headers = {
|
|
451
|
+
...(await getAuthHeaders()),
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
await sdk.store.cart.update(cartId, data, {}, headers)
|
|
455
|
+
|
|
456
|
+
// Revalidate tags to ensure CartTotals gets new shipping info
|
|
457
|
+
const cartCacheTag = await getCacheTag("carts")
|
|
458
|
+
revalidateTag(cartCacheTag)
|
|
459
|
+
|
|
460
|
+
const fulfillmentCacheTag = await getCacheTag("fulfillment")
|
|
461
|
+
revalidateTag(fulfillmentCacheTag)
|
|
462
|
+
|
|
463
|
+
return { success: true }
|
|
464
|
+
} catch (e) {
|
|
465
|
+
return { success: false }
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Updates cart metadata silently without triggering a full page revalidation.
|
|
470
|
+
*/
|
|
471
|
+
|
|
472
|
+
export async function updateCartMetadataSilently(metadata: any) {
|
|
473
|
+
const cartId = await getCartId()
|
|
474
|
+
if (!cartId) return
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
const headers = {
|
|
478
|
+
...(await getAuthHeaders()),
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
await sdk.store.cart.update(cartId, { metadata }, {}, headers)
|
|
482
|
+
// We intentionally DO NOT call revalidateTag here to prevent page refresh loops
|
|
483
|
+
return { success: true }
|
|
484
|
+
} catch (e) {
|
|
485
|
+
return { success: false }
|
|
486
|
+
}
|
|
487
|
+
}
|