@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,43 @@
|
|
|
1
|
+
export function validateVariantId(variantId?: string): void {
|
|
2
|
+
if (!variantId) {
|
|
3
|
+
throw new Error("Missing variant ID when adding to cart")
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function validateLineId(lineId?: string): void {
|
|
8
|
+
if (!lineId) {
|
|
9
|
+
throw new Error("Missing lineItem ID when updating line item")
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function validateCartId(cartId?: string | null): void {
|
|
14
|
+
if (!cartId) {
|
|
15
|
+
throw new Error("Missing cart ID when updating line item")
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function validateQuantity(quantity: number): void {
|
|
20
|
+
if (quantity < 1) {
|
|
21
|
+
throw new Error("Quantity must be at least 1")
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function validateAddToCartInput(input: {
|
|
26
|
+
variantId: string
|
|
27
|
+
quantity: number
|
|
28
|
+
}): void {
|
|
29
|
+
validateVariantId(input.variantId)
|
|
30
|
+
validateQuantity(input.quantity)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function validateLineItemUpdateInput(input: {
|
|
34
|
+
lineId: string
|
|
35
|
+
quantity: number
|
|
36
|
+
}): void {
|
|
37
|
+
validateLineId(input.lineId)
|
|
38
|
+
validateQuantity(input.quantity)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function validateLineItemDeleteInput(lineId?: string): void {
|
|
42
|
+
validateLineId(lineId)
|
|
43
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { HttpTypes } from "@medusajs/types"
|
|
2
|
+
import { getProductPrice, getPricesForVariant } from "@core/util/get-product-price"
|
|
3
|
+
|
|
4
|
+
export type VariantPrice = NonNullable<ReturnType<typeof getPricesForVariant>>
|
|
5
|
+
|
|
6
|
+
export function getDisplayPrice(
|
|
7
|
+
product: HttpTypes.StoreProduct,
|
|
8
|
+
selectedVariant?: HttpTypes.StoreProductVariant
|
|
9
|
+
): VariantPrice | null {
|
|
10
|
+
if (selectedVariant) {
|
|
11
|
+
const variantPrice = getPricesForVariant(selectedVariant)
|
|
12
|
+
if (variantPrice) {
|
|
13
|
+
return variantPrice
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { cheapestPrice } = getProductPrice({ product })
|
|
18
|
+
return cheapestPrice
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getDiscountPercentage(
|
|
22
|
+
displayPrice: VariantPrice | null | undefined
|
|
23
|
+
): number | null {
|
|
24
|
+
const originalPrice = displayPrice?.original_price_number
|
|
25
|
+
const currentPrice = displayPrice?.calculated_price_number
|
|
26
|
+
|
|
27
|
+
if (originalPrice && currentPrice && originalPrice > currentPrice) {
|
|
28
|
+
return Math.round(((originalPrice - currentPrice) / originalPrice) * 100)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatINRPrice(amount: number): string {
|
|
35
|
+
return `₹${amount.toLocaleString("en-IN")}`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function formatDisplayPrice(
|
|
39
|
+
displayPrice: VariantPrice,
|
|
40
|
+
currentPrice?: number | null
|
|
41
|
+
): string {
|
|
42
|
+
if (currentPrice !== undefined && currentPrice !== null) {
|
|
43
|
+
return formatINRPrice(currentPrice)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return displayPrice.calculated_price
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { getProductPrice, getPricesForVariant }
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { HttpTypes } from "@medusajs/types"
|
|
2
|
+
import { isEqual } from "lodash"
|
|
3
|
+
|
|
4
|
+
export type ProductOptions = Record<string, string | undefined>
|
|
5
|
+
|
|
6
|
+
export function optionsAsKeymap(
|
|
7
|
+
variantOptions: HttpTypes.StoreProductVariant["options"]
|
|
8
|
+
): ProductOptions | undefined {
|
|
9
|
+
return variantOptions?.reduce((acc: Record<string, string>, varopt) => {
|
|
10
|
+
if (varopt.option_id) {
|
|
11
|
+
acc[varopt.option_id] = varopt.value
|
|
12
|
+
}
|
|
13
|
+
return acc
|
|
14
|
+
}, {})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getAutoSelectPartialOptions(
|
|
18
|
+
product: HttpTypes.StoreProduct
|
|
19
|
+
): Record<string, string> {
|
|
20
|
+
const partialOptions: Record<string, string> = {}
|
|
21
|
+
|
|
22
|
+
product.options?.forEach((option) => {
|
|
23
|
+
const uniqueValues = new Set(option.values?.map((v) => v.value))
|
|
24
|
+
|
|
25
|
+
if (uniqueValues.size === 1) {
|
|
26
|
+
const uniqueValue = Array.from(uniqueValues)[0]
|
|
27
|
+
if (uniqueValue) {
|
|
28
|
+
partialOptions[option.id] = uniqueValue
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return partialOptions
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getInitialOptions(
|
|
37
|
+
product: HttpTypes.StoreProduct,
|
|
38
|
+
variantIdFromUrl?: string | null
|
|
39
|
+
): ProductOptions {
|
|
40
|
+
if (variantIdFromUrl && product.variants) {
|
|
41
|
+
const variantFromUrl = product.variants.find((v) => v.id === variantIdFromUrl)
|
|
42
|
+
if (variantFromUrl) {
|
|
43
|
+
return optionsAsKeymap(variantFromUrl.options) ?? {}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (product.variants?.length === 1) {
|
|
48
|
+
return optionsAsKeymap(product.variants[0].options) ?? {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const partialOptions = getAutoSelectPartialOptions(product)
|
|
52
|
+
if (Object.keys(partialOptions).length > 0) {
|
|
53
|
+
return partialOptions
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function findVariantByOptions(
|
|
60
|
+
product: HttpTypes.StoreProduct,
|
|
61
|
+
options: ProductOptions
|
|
62
|
+
): HttpTypes.StoreProductVariant | undefined {
|
|
63
|
+
if (!product.variants || product.variants.length === 0) {
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return product.variants.find((v) => {
|
|
68
|
+
const variantOptions = optionsAsKeymap(v.options)
|
|
69
|
+
return isEqual(variantOptions, options)
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function isValidVariantSelection(
|
|
74
|
+
product: HttpTypes.StoreProduct,
|
|
75
|
+
options: ProductOptions
|
|
76
|
+
): boolean {
|
|
77
|
+
return (
|
|
78
|
+
product.variants?.some((v) => {
|
|
79
|
+
const variantOptions = optionsAsKeymap(v.options)
|
|
80
|
+
return isEqual(variantOptions, options)
|
|
81
|
+
}) ?? false
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function findColorOption(
|
|
86
|
+
product: HttpTypes.StoreProduct
|
|
87
|
+
): HttpTypes.StoreProductOption | undefined {
|
|
88
|
+
return product.options?.find(
|
|
89
|
+
(opt) =>
|
|
90
|
+
opt.title?.toLowerCase().includes("color") ||
|
|
91
|
+
opt.title?.toLowerCase().includes("colour")
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function findSizeOption(
|
|
96
|
+
product: HttpTypes.StoreProduct
|
|
97
|
+
): HttpTypes.StoreProductOption | undefined {
|
|
98
|
+
return product.options?.find((opt) => opt.title?.toLowerCase().includes("size"))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getUniqueColorVariants(
|
|
102
|
+
product: HttpTypes.StoreProduct,
|
|
103
|
+
colorOption: HttpTypes.StoreProductOption
|
|
104
|
+
): HttpTypes.StoreProductVariant[] {
|
|
105
|
+
if (!product.variants) {
|
|
106
|
+
return []
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const uniqueColors = new Map<string, HttpTypes.StoreProductVariant>()
|
|
110
|
+
product.variants.forEach((variant) => {
|
|
111
|
+
const colorValue = variant.options?.find(
|
|
112
|
+
(opt) => opt.option_id === colorOption.id
|
|
113
|
+
)?.value
|
|
114
|
+
if (colorValue && !uniqueColors.has(colorValue)) {
|
|
115
|
+
uniqueColors.set(colorValue, variant)
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
return Array.from(uniqueColors.values())
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getOtherOptions(
|
|
123
|
+
product: HttpTypes.StoreProduct
|
|
124
|
+
): HttpTypes.StoreProductOption[] {
|
|
125
|
+
return (product.options || []).filter((opt) => {
|
|
126
|
+
const isColor =
|
|
127
|
+
opt.title?.toLowerCase().includes("color") ||
|
|
128
|
+
opt.title?.toLowerCase().includes("colour")
|
|
129
|
+
const isSize = opt.title?.toLowerCase().includes("size")
|
|
130
|
+
return !isColor && !isSize
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getMissingOptionIds(
|
|
135
|
+
product: HttpTypes.StoreProduct,
|
|
136
|
+
options: ProductOptions
|
|
137
|
+
): Record<string, boolean> {
|
|
138
|
+
const missingOptions: Record<string, boolean> = {}
|
|
139
|
+
product.options?.forEach((option) => {
|
|
140
|
+
if (!options[option.id]) {
|
|
141
|
+
missingOptions[option.id] = true
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
return missingOptions
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function isVariantInStock(
|
|
148
|
+
variant: HttpTypes.StoreProductVariant | undefined,
|
|
149
|
+
product: HttpTypes.StoreProduct,
|
|
150
|
+
quantity: number,
|
|
151
|
+
quantityInCart: number
|
|
152
|
+
): boolean {
|
|
153
|
+
if (!variant) {
|
|
154
|
+
return Boolean(product.variants && product.variants.length > 0)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!variant.manage_inventory) {
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (variant.allow_backorder) {
|
|
162
|
+
return true
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const totalInventory = variant.inventory_quantity || 0
|
|
166
|
+
const availableQuantity = totalInventory - quantityInCart
|
|
167
|
+
|
|
168
|
+
return availableQuantity >= quantity
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function getInventoryLimit(
|
|
172
|
+
variant: HttpTypes.StoreProductVariant | undefined,
|
|
173
|
+
quantityInCart: number
|
|
174
|
+
): number | null {
|
|
175
|
+
if (!variant || !variant.manage_inventory || variant.allow_backorder) {
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const totalInventory = variant.inventory_quantity || 0
|
|
180
|
+
return totalInventory - quantityInCart
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function getVariantCartItem(
|
|
184
|
+
cart: HttpTypes.StoreCart | null | undefined,
|
|
185
|
+
variantId: string | undefined
|
|
186
|
+
): HttpTypes.StoreCartLineItem | null {
|
|
187
|
+
if (!cart || !variantId) {
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const item = cart.items?.find((i) => i.variant_id === variantId)
|
|
192
|
+
return item || null
|
|
193
|
+
}
|
package/src/firebase.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { initializeApp, getApps, getApp } from "firebase/app"
|
|
2
|
+
import { getMessaging, getToken, onMessage } from "firebase/messaging"
|
|
3
|
+
|
|
4
|
+
const firebaseConfig = {
|
|
5
|
+
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
|
|
6
|
+
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
|
|
7
|
+
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
|
|
8
|
+
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
|
|
9
|
+
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
|
|
10
|
+
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const app = getApps().length > 0 ? getApp() : initializeApp(firebaseConfig)
|
|
14
|
+
|
|
15
|
+
export const requestNotificationPermission = async () => {
|
|
16
|
+
try {
|
|
17
|
+
if (typeof window === "undefined" || !("Notification" in window)) {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const permission = await Notification.requestPermission()
|
|
22
|
+
if (permission === "granted") {
|
|
23
|
+
// Explicitly register service worker to avoid path issues
|
|
24
|
+
const registration = await navigator.serviceWorker.register(
|
|
25
|
+
"/firebase-messaging-sw.js"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const messaging = getMessaging(app)
|
|
29
|
+
const token = await getToken(messaging, {
|
|
30
|
+
vapidKey: process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY,
|
|
31
|
+
serviceWorkerRegistration: registration,
|
|
32
|
+
})
|
|
33
|
+
return token
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw error // Throwing so UI can catch it
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const onMessageListener = () =>
|
|
41
|
+
new Promise((resolve) => {
|
|
42
|
+
const messaging = getMessaging(app)
|
|
43
|
+
onMessage(messaging, (payload) => {
|
|
44
|
+
resolve(payload)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
export default app
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { useAddToCart } from "./use-add-to-cart"
|
|
2
|
+
export { useProductReviews } from "./use-product-reviews"
|
|
3
|
+
export { useServerAction } from "./use-server-action"
|
|
4
|
+
export { useCart } from "./use-cart"
|
|
5
|
+
export { useProductVariant } from "./use-product-variant"
|
|
6
|
+
export { useProductActions } from "./use-product-actions"
|
|
7
|
+
export { useWishlist } from "./use-wishlist"
|
|
8
|
+
export { useCheckout } from "./use-checkout"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from "react"
|
|
4
|
+
import { useParams } from "next/navigation"
|
|
5
|
+
import { HttpTypes } from "@medusajs/types"
|
|
6
|
+
import { addToCart } from "@core/data/cart"
|
|
7
|
+
import { trackAddToCart } from "@core/analytics/ga4-ecommerce"
|
|
8
|
+
|
|
9
|
+
type UseAddToCartOptions = {
|
|
10
|
+
region?: HttpTypes.StoreRegion
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useAddToCart({ region }: UseAddToCartOptions = {}) {
|
|
14
|
+
const { countryCode } = useParams() as { countryCode: string }
|
|
15
|
+
const [isAdding, setIsAdding] = useState<string | null>(null)
|
|
16
|
+
|
|
17
|
+
const handleAddToCart = useCallback(
|
|
18
|
+
async (product: HttpTypes.StoreProduct) => {
|
|
19
|
+
if (!product.variants || product.variants.length === 0) return
|
|
20
|
+
|
|
21
|
+
const firstVariant = product.variants[0]
|
|
22
|
+
if (!firstVariant.id) return
|
|
23
|
+
|
|
24
|
+
setIsAdding(product.id || null)
|
|
25
|
+
try {
|
|
26
|
+
await addToCart({
|
|
27
|
+
variantId: firstVariant.id,
|
|
28
|
+
quantity: 1,
|
|
29
|
+
countryCode,
|
|
30
|
+
})
|
|
31
|
+
const currency =
|
|
32
|
+
(
|
|
33
|
+
firstVariant as { calculated_price?: { currency_code?: string } }
|
|
34
|
+
).calculated_price?.currency_code?.toUpperCase() ??
|
|
35
|
+
region?.currency_code ??
|
|
36
|
+
"USD"
|
|
37
|
+
const unitPrice =
|
|
38
|
+
(firstVariant as { calculated_price?: { calculated_amount?: number } })
|
|
39
|
+
.calculated_price?.calculated_amount ?? 0
|
|
40
|
+
trackAddToCart({
|
|
41
|
+
currency,
|
|
42
|
+
value: unitPrice / 100,
|
|
43
|
+
items: [
|
|
44
|
+
{
|
|
45
|
+
item_id: firstVariant.id,
|
|
46
|
+
item_name: product.title ?? "",
|
|
47
|
+
price: unitPrice / 100,
|
|
48
|
+
quantity: 1,
|
|
49
|
+
item_variant: firstVariant.title ?? undefined,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
})
|
|
53
|
+
} catch {
|
|
54
|
+
// Silence error — caller may handle UI feedback
|
|
55
|
+
} finally {
|
|
56
|
+
setIsAdding(null)
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
[countryCode, region]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return { isAdding, handleAddToCart }
|
|
63
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from "react"
|
|
4
|
+
import { useParams } from "next/navigation"
|
|
5
|
+
import {
|
|
6
|
+
addToCart,
|
|
7
|
+
updateLineItem,
|
|
8
|
+
deleteLineItem,
|
|
9
|
+
updateLineItemVariant,
|
|
10
|
+
} from "@core/data/cart"
|
|
11
|
+
|
|
12
|
+
type CartMutationResult = {
|
|
13
|
+
success: boolean
|
|
14
|
+
error?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useCart() {
|
|
18
|
+
const { countryCode } = useParams() as { countryCode: string }
|
|
19
|
+
const [loadingLineId, setLoadingLineId] = useState<string | null>(null)
|
|
20
|
+
const [isAdding, setIsAdding] = useState(false)
|
|
21
|
+
const [error, setError] = useState<string | null>(null)
|
|
22
|
+
|
|
23
|
+
const addItem = useCallback(
|
|
24
|
+
async (variantId: string, quantity = 1): Promise<CartMutationResult> => {
|
|
25
|
+
setError(null)
|
|
26
|
+
setIsAdding(true)
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await addToCart({ variantId, quantity, countryCode })
|
|
30
|
+
return { success: true }
|
|
31
|
+
} catch (err) {
|
|
32
|
+
const message =
|
|
33
|
+
err instanceof Error ? err.message : "Failed to add item to cart"
|
|
34
|
+
setError(message)
|
|
35
|
+
return { success: false, error: message }
|
|
36
|
+
} finally {
|
|
37
|
+
setIsAdding(false)
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
[countryCode]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const updateItem = useCallback(
|
|
44
|
+
async (lineId: string, quantity: number): Promise<CartMutationResult> => {
|
|
45
|
+
setError(null)
|
|
46
|
+
setLoadingLineId(lineId)
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await updateLineItem({ lineId, quantity })
|
|
50
|
+
return { success: true }
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const message =
|
|
53
|
+
err instanceof Error ? err.message : "Failed to update cart item"
|
|
54
|
+
setError(message)
|
|
55
|
+
return { success: false, error: message }
|
|
56
|
+
} finally {
|
|
57
|
+
setLoadingLineId(null)
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
[]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const removeItem = useCallback(
|
|
64
|
+
async (lineId: string): Promise<CartMutationResult> => {
|
|
65
|
+
setError(null)
|
|
66
|
+
setLoadingLineId(lineId)
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await deleteLineItem(lineId)
|
|
70
|
+
return { success: true }
|
|
71
|
+
} catch (err) {
|
|
72
|
+
const message =
|
|
73
|
+
err instanceof Error ? err.message : "Failed to remove cart item"
|
|
74
|
+
setError(message)
|
|
75
|
+
return { success: false, error: message }
|
|
76
|
+
} finally {
|
|
77
|
+
setLoadingLineId(null)
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
[]
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const updateItemVariant = useCallback(
|
|
84
|
+
async (
|
|
85
|
+
lineId: string,
|
|
86
|
+
variantId: string,
|
|
87
|
+
quantity: number
|
|
88
|
+
): Promise<CartMutationResult> => {
|
|
89
|
+
setError(null)
|
|
90
|
+
setLoadingLineId(lineId)
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await updateLineItemVariant({
|
|
94
|
+
lineId,
|
|
95
|
+
variantId,
|
|
96
|
+
quantity,
|
|
97
|
+
countryCode,
|
|
98
|
+
})
|
|
99
|
+
return { success: true }
|
|
100
|
+
} catch (err) {
|
|
101
|
+
const message =
|
|
102
|
+
err instanceof Error ? err.message : "Failed to update cart item variant"
|
|
103
|
+
setError(message)
|
|
104
|
+
return { success: false, error: message }
|
|
105
|
+
} finally {
|
|
106
|
+
setLoadingLineId(null)
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
[countryCode]
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
const clearError = useCallback(() => {
|
|
113
|
+
setError(null)
|
|
114
|
+
}, [])
|
|
115
|
+
|
|
116
|
+
const isLineLoading = useCallback(
|
|
117
|
+
(lineId: string) => loadingLineId === lineId,
|
|
118
|
+
[loadingLineId]
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
addItem,
|
|
123
|
+
updateItem,
|
|
124
|
+
removeItem,
|
|
125
|
+
updateItemVariant,
|
|
126
|
+
isAdding,
|
|
127
|
+
loadingLineId,
|
|
128
|
+
isLineLoading,
|
|
129
|
+
error,
|
|
130
|
+
clearError,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from "react"
|
|
4
|
+
import { useParams, useRouter } from "next/navigation"
|
|
5
|
+
import {
|
|
6
|
+
setAddresses,
|
|
7
|
+
placeOrder,
|
|
8
|
+
setShippingMethod,
|
|
9
|
+
initiatePaymentSession,
|
|
10
|
+
} from "@core/data/cart"
|
|
11
|
+
import { HttpTypes } from "@medusajs/types"
|
|
12
|
+
import { useServerAction } from "./use-server-action"
|
|
13
|
+
|
|
14
|
+
type UseCheckoutOptions = {
|
|
15
|
+
cart: HttpTypes.StoreCart
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useCheckout({ cart }: UseCheckoutOptions) {
|
|
19
|
+
const router = useRouter()
|
|
20
|
+
const { countryCode } = useParams() as { countryCode: string }
|
|
21
|
+
const [isPlacingOrder, setIsPlacingOrder] = useState(false)
|
|
22
|
+
|
|
23
|
+
const addressAction = useServerAction((formData: FormData) =>
|
|
24
|
+
setAddresses(null, formData)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const handleSetShipping = useCallback(
|
|
28
|
+
async (shippingMethodId: string) => {
|
|
29
|
+
await setShippingMethod({ cartId: cart.id, shippingMethodId })
|
|
30
|
+
router.refresh()
|
|
31
|
+
},
|
|
32
|
+
[cart.id, router]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const handleInitiatePayment = useCallback(
|
|
36
|
+
async (data: HttpTypes.StoreInitializePaymentSession) => {
|
|
37
|
+
return initiatePaymentSession(cart, data)
|
|
38
|
+
},
|
|
39
|
+
[cart]
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const handlePlaceOrder = useCallback(
|
|
43
|
+
async (cartId?: string) => {
|
|
44
|
+
setIsPlacingOrder(true)
|
|
45
|
+
try {
|
|
46
|
+
await placeOrder(cartId ?? cart.id)
|
|
47
|
+
} finally {
|
|
48
|
+
setIsPlacingOrder(false)
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
[cart.id]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
countryCode,
|
|
56
|
+
isPlacingOrder,
|
|
57
|
+
addressAction,
|
|
58
|
+
handleSetShipping,
|
|
59
|
+
handleInitiatePayment,
|
|
60
|
+
handlePlaceOrder,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { RefObject, useEffect, useState } from "react"
|
|
2
|
+
|
|
3
|
+
export const useIntersection = (
|
|
4
|
+
element: RefObject<HTMLDivElement | null>,
|
|
5
|
+
rootMargin: string
|
|
6
|
+
) => {
|
|
7
|
+
const [isVisible, setState] = useState(false)
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!element.current) {
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const el = element.current
|
|
15
|
+
|
|
16
|
+
const observer = new IntersectionObserver(
|
|
17
|
+
([entry]) => {
|
|
18
|
+
setState(entry.isIntersecting)
|
|
19
|
+
},
|
|
20
|
+
{ rootMargin }
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
observer.observe(el)
|
|
24
|
+
|
|
25
|
+
return () => observer.unobserve(el)
|
|
26
|
+
}, [element, rootMargin])
|
|
27
|
+
|
|
28
|
+
return isVisible
|
|
29
|
+
}
|