medusa-ui-common 2.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/package.json +93 -0
- package/src/common/components/breadcrumb/index.tsx +43 -0
- package/src/common/components/cart-totals/index.tsx +562 -0
- package/src/common/components/checkbox/index.tsx +98 -0
- package/src/common/components/delete-button/index.tsx +158 -0
- package/src/common/components/discount-code/index.tsx +220 -0
- package/src/common/components/divider/index.tsx +9 -0
- package/src/common/components/error-message/index.tsx +13 -0
- package/src/common/components/filter-checkbox-group/index.tsx +134 -0
- package/src/common/components/filter-radio-group/index.tsx +62 -0
- package/src/common/components/input/index.tsx +79 -0
- package/src/common/components/interactive-link/index.tsx +33 -0
- package/src/common/components/line-item-options/index.tsx +26 -0
- package/src/common/components/line-item-price/index.tsx +64 -0
- package/src/common/components/line-item-unit-price/index.tsx +64 -0
- package/src/common/components/localized-client-link/index.tsx +32 -0
- package/src/common/components/login-popup/index.tsx +78 -0
- package/src/common/components/modal/index.tsx +123 -0
- package/src/common/components/native-select/index.tsx +75 -0
- package/src/common/components/obfuscated-email/index.tsx +30 -0
- package/src/common/components/processing-overlay/index.tsx +83 -0
- package/src/common/components/radio/index.tsx +27 -0
- package/src/common/components/side-panel/index.tsx +65 -0
- package/src/common/components/submit-button/index.tsx +32 -0
- package/src/common/icons/arrow-left.tsx +36 -0
- package/src/common/icons/back.tsx +37 -0
- package/src/common/icons/bancontact.tsx +26 -0
- package/src/common/icons/chevron-down.tsx +30 -0
- package/src/common/icons/delivered.tsx +29 -0
- package/src/common/icons/envelope.tsx +27 -0
- package/src/common/icons/eye-off.tsx +37 -0
- package/src/common/icons/eye.tsx +37 -0
- package/src/common/icons/fast-delivery.tsx +65 -0
- package/src/common/icons/ideal.tsx +26 -0
- package/src/common/icons/lock.tsx +31 -0
- package/src/common/icons/map-pin.tsx +37 -0
- package/src/common/icons/medusa.tsx +27 -0
- package/src/common/icons/menu.tsx +45 -0
- package/src/common/icons/nextjs.tsx +27 -0
- package/src/common/icons/package.tsx +44 -0
- package/src/common/icons/paypal.tsx +30 -0
- package/src/common/icons/phone.tsx +30 -0
- package/src/common/icons/placeholder-image.tsx +44 -0
- package/src/common/icons/refresh.tsx +51 -0
- package/src/common/icons/spinner.tsx +37 -0
- package/src/common/icons/trash.tsx +51 -0
- package/src/common/icons/user.tsx +37 -0
- package/src/common/icons/x.tsx +37 -0
- package/src/constants/payments.tsx +31 -0
- package/src/context/modal-context.tsx +37 -0
- package/src/context/wishlist-context.tsx +83 -0
- package/src/index.ts +16 -0
- package/src/skeletons/components/skeleton-button/index.tsx +5 -0
- package/src/skeletons/components/skeleton-card-details/index.tsx +10 -0
- package/src/skeletons/components/skeleton-cart-item/index.tsx +35 -0
- package/src/skeletons/components/skeleton-cart-totals/index.tsx +30 -0
- package/src/skeletons/components/skeleton-code-form/index.tsx +13 -0
- package/src/skeletons/components/skeleton-line-item/index.tsx +35 -0
- package/src/skeletons/components/skeleton-order-confirmed-header/index.tsx +14 -0
- package/src/skeletons/components/skeleton-order-information/index.tsx +36 -0
- package/src/skeletons/components/skeleton-order-items/index.tsx +43 -0
- package/src/skeletons/components/skeleton-order-summary/index.tsx +15 -0
- package/src/skeletons/components/skeleton-product-preview/index.tsx +15 -0
- package/src/skeletons/templates/skeleton-cart-page/index.tsx +65 -0
- package/src/skeletons/templates/skeleton-order-confirmed/index.tsx +21 -0
- package/src/skeletons/templates/skeleton-product-grid/index.tsx +23 -0
- package/src/skeletons/templates/skeleton-related-products/index.tsx +25 -0
- package/src/types/global.ts +24 -0
- package/src/types/icon.ts +6 -0
- package/src/util/checkout-dom.ts +65 -0
- package/src/util/compare-addresses.ts +28 -0
- package/src/util/env.ts +3 -0
- package/src/util/get-percentage-diff.ts +6 -0
- package/src/util/get-product-price.ts +79 -0
- package/src/util/isEmpty.ts +11 -0
- package/src/util/money.ts +26 -0
- package/src/util/product.ts +86 -0
- package/src/util/repeat.ts +5 -0
- package/src/util/returns.ts +72 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { convertToLocale } from "medusa-ui-common/util/money"
|
|
4
|
+
import { syncCheckoutAddressFromDom } from "medusa-ui-common/util/checkout-dom"
|
|
5
|
+
import React from "react"
|
|
6
|
+
import { HttpTypes } from "@medusajs/types"
|
|
7
|
+
import { clx } from "@medusajs/ui"
|
|
8
|
+
import LocalizedClientLink from "medusa-ui-common/common/components/localized-client-link"
|
|
9
|
+
import { useRouter, usePathname, useSearchParams } from "next/navigation"
|
|
10
|
+
import ProcessingOverlay from "medusa-ui-common/common/components/processing-overlay"
|
|
11
|
+
import { useSiteCompany } from "medusa-storefront-core"
|
|
12
|
+
|
|
13
|
+
type CartTotalsProps = {
|
|
14
|
+
totals: {
|
|
15
|
+
total?: number | null
|
|
16
|
+
subtotal?: number | null
|
|
17
|
+
tax_total?: number | null
|
|
18
|
+
currency_code: string
|
|
19
|
+
item_subtotal?: number | null
|
|
20
|
+
shipping_subtotal?: number | null
|
|
21
|
+
discount_subtotal?: number | null
|
|
22
|
+
}
|
|
23
|
+
items?: HttpTypes.StoreCartLineItem[]
|
|
24
|
+
checkoutStep?: string
|
|
25
|
+
customer?: HttpTypes.StoreCustomer | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const CartTotals: React.FC<CartTotalsProps> = ({ totals, items = [], checkoutStep, customer }) => {
|
|
29
|
+
const company = useSiteCompany()
|
|
30
|
+
const router = useRouter()
|
|
31
|
+
const pathname = usePathname()
|
|
32
|
+
const searchParams = useSearchParams()
|
|
33
|
+
const activeStep = searchParams.get("step")
|
|
34
|
+
|
|
35
|
+
// Check if we're on the payment page, order page, or cart page
|
|
36
|
+
const isOnPaymentPage = pathname.includes("/checkout/payment")
|
|
37
|
+
const isOnOrderPage = pathname.includes("/order/")
|
|
38
|
+
const isOnCartPage = pathname.includes("/cart")
|
|
39
|
+
|
|
40
|
+
const {
|
|
41
|
+
currency_code,
|
|
42
|
+
total,
|
|
43
|
+
tax_total,
|
|
44
|
+
item_subtotal,
|
|
45
|
+
shipping_subtotal,
|
|
46
|
+
discount_subtotal,
|
|
47
|
+
} = totals
|
|
48
|
+
|
|
49
|
+
// Calculate Total MRP (sum of original_total from all items)
|
|
50
|
+
const totalMRP = items.reduce((sum, item) => {
|
|
51
|
+
return sum + (item.original_total || item.total || 0)
|
|
52
|
+
}, 0)
|
|
53
|
+
|
|
54
|
+
// Calculate total discount (difference between MRP and current subtotal)
|
|
55
|
+
const calculatedDiscount = totalMRP > (item_subtotal || 0)
|
|
56
|
+
? totalMRP - (item_subtotal || 0)
|
|
57
|
+
: (discount_subtotal || 0)
|
|
58
|
+
|
|
59
|
+
// Use GST label for taxes (as shown in the image)
|
|
60
|
+
const gstAmount = tax_total || 0
|
|
61
|
+
const shippingAmount = shipping_subtotal || 0
|
|
62
|
+
const finalTotal = total || 0
|
|
63
|
+
const itemCount = items.length
|
|
64
|
+
|
|
65
|
+
const [isRedirecting, setIsRedirecting] = React.useState(false)
|
|
66
|
+
const [showOverlay, setShowOverlay] = React.useState(false)
|
|
67
|
+
const [processingVariant, setProcessingVariant] = React.useState<"payment" | "order">("payment")
|
|
68
|
+
const [isAutoSettingShipping, setIsAutoSettingShipping] = React.useState(false)
|
|
69
|
+
|
|
70
|
+
// Auto-set shipping method if on payment step and none is selected
|
|
71
|
+
const hasAutoSet = React.useRef<string | null>(null)
|
|
72
|
+
|
|
73
|
+
// Listen for real-time shipping updates
|
|
74
|
+
React.useEffect(() => {
|
|
75
|
+
const handlePostalUpdate = () => {
|
|
76
|
+
// Show loading state in totals
|
|
77
|
+
setIsAutoSettingShipping(true)
|
|
78
|
+
// Reset auto-set ref to allow re-selection of shipping for new pincode
|
|
79
|
+
hasAutoSet.current = ""
|
|
80
|
+
// Trigger a single router refresh as a safeguard
|
|
81
|
+
router.refresh()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
window.addEventListener('postal-code-updated', handlePostalUpdate)
|
|
85
|
+
window.addEventListener('address-updated-silent', () => {
|
|
86
|
+
setIsAutoSettingShipping(true)
|
|
87
|
+
// Reset auto-set ref
|
|
88
|
+
hasAutoSet.current = ""
|
|
89
|
+
})
|
|
90
|
+
return () => {
|
|
91
|
+
window.removeEventListener('postal-code-updated', handlePostalUpdate)
|
|
92
|
+
window.removeEventListener('address-updated-silent', () => {})
|
|
93
|
+
}
|
|
94
|
+
}, [router])
|
|
95
|
+
|
|
96
|
+
// Safety reset for Calculating state
|
|
97
|
+
React.useEffect(() => {
|
|
98
|
+
const cart = totals as any
|
|
99
|
+
if (cart?.shipping_methods && cart.shipping_methods.length > 0) {
|
|
100
|
+
if (isAutoSettingShipping) {
|
|
101
|
+
setIsAutoSettingShipping(false)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}, [totals, isAutoSettingShipping])
|
|
105
|
+
|
|
106
|
+
React.useEffect(() => {
|
|
107
|
+
autoSetShipping()
|
|
108
|
+
}, [activeStep, totals, router])
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
const isSetting = React.useRef(false)
|
|
112
|
+
|
|
113
|
+
const autoSetShipping = async (retryCount = 0) => {
|
|
114
|
+
const cart = totals as any
|
|
115
|
+
if (!cart?.id || isSetting.current) return
|
|
116
|
+
|
|
117
|
+
// If shipping methods already exist, we are done
|
|
118
|
+
if (cart?.shipping_methods && cart.shipping_methods.length > 0) {
|
|
119
|
+
setIsAutoSettingShipping(false)
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const { listCartOptions, setShippingMethod } = await import("medusa-storefront-data/cart")
|
|
124
|
+
const currentKey = cart.id + cart.shipping_address?.postal_code
|
|
125
|
+
|
|
126
|
+
const isCheckoutPage = pathname.includes("/checkout")
|
|
127
|
+
const shouldAutoSet = (activeStep === "payment" || isCheckoutPage) && cart?.shipping_address?.postal_code?.length === 6
|
|
128
|
+
|
|
129
|
+
if (shouldAutoSet && hasAutoSet.current !== currentKey) {
|
|
130
|
+
|
|
131
|
+
isSetting.current = true
|
|
132
|
+
setIsAutoSettingShipping(true)
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const { shipping_options } = await listCartOptions()
|
|
136
|
+
|
|
137
|
+
if (shipping_options && shipping_options.length > 0) {
|
|
138
|
+
await setShippingMethod({ cartId: cart.id, shippingMethodId: shipping_options[0].id })
|
|
139
|
+
hasAutoSet.current = currentKey // Mark as done for this pincode
|
|
140
|
+
router.refresh()
|
|
141
|
+
// Final unlock delay
|
|
142
|
+
setTimeout(() => {
|
|
143
|
+
setIsAutoSettingShipping(false)
|
|
144
|
+
isSetting.current = false
|
|
145
|
+
}, 1500)
|
|
146
|
+
} else if (retryCount < 6) {
|
|
147
|
+
// Keep trying if no options yet
|
|
148
|
+
setTimeout(() => {
|
|
149
|
+
isSetting.current = false
|
|
150
|
+
autoSetShipping(retryCount + 1)
|
|
151
|
+
}, 1500)
|
|
152
|
+
} else {
|
|
153
|
+
setIsAutoSettingShipping(false)
|
|
154
|
+
isSetting.current = false
|
|
155
|
+
}
|
|
156
|
+
} catch (e) {
|
|
157
|
+
setIsAutoSettingShipping(false)
|
|
158
|
+
isSetting.current = false
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
setIsAutoSettingShipping(false)
|
|
162
|
+
isSetting.current = false
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
React.useEffect(() => {
|
|
167
|
+
autoSetShipping()
|
|
168
|
+
}, [activeStep, totals, router])
|
|
169
|
+
|
|
170
|
+
const syncLastEtdToCart = async () => {
|
|
171
|
+
try {
|
|
172
|
+
const cart = totals as any
|
|
173
|
+
const cookiesArr = document.cookie.split('; ')
|
|
174
|
+
const etdCookie = cookiesArr.find(row => row.startsWith('_medusa_last_etd='))
|
|
175
|
+
const lastEtdValue = etdCookie ? etdCookie.split('=')[1] : null
|
|
176
|
+
|
|
177
|
+
if (lastEtdValue && cart?.id && cart.metadata?.estimated_delivery !== lastEtdValue) {
|
|
178
|
+
const { updateCartMetadataSilently } = await import("medusa-storefront-data/cart")
|
|
179
|
+
await updateCartMetadataSilently({
|
|
180
|
+
...cart.metadata,
|
|
181
|
+
estimated_delivery: lastEtdValue
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
} catch (e) {
|
|
185
|
+
// Ignore sync failure as it's non-critical
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const trackCheckoutAnalytics = async (cart: HttpTypes.StoreCart) => {
|
|
190
|
+
try {
|
|
191
|
+
const { cartToEcommercePayload, trackInitiateCheckoutFromCart } = await import(
|
|
192
|
+
"medusa-storefront-analytics"
|
|
193
|
+
)
|
|
194
|
+
const checkoutPayload = cartToEcommercePayload(cart)
|
|
195
|
+
if (checkoutPayload) {
|
|
196
|
+
trackInitiateCheckoutFromCart(checkoutPayload, cart?.id)
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
// Analytics must never block checkout navigation.
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const trackPaymentMethodClick = async (paymentType: "cod" | "razorpay") => {
|
|
204
|
+
try {
|
|
205
|
+
const { cartToEcommercePayload, trackAddPaymentInfo } = await import(
|
|
206
|
+
"medusa-storefront-analytics"
|
|
207
|
+
)
|
|
208
|
+
const payload = cartToEcommercePayload(totals as HttpTypes.StoreCart)
|
|
209
|
+
if (payload) {
|
|
210
|
+
trackAddPaymentInfo(payload, { payment_type: paymentType })
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
// Analytics must never block payment.
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const handleProceedToCheckout = async () => {
|
|
218
|
+
if (isRedirecting) return
|
|
219
|
+
|
|
220
|
+
const countryCode = pathname.split("/")[1] || "in"
|
|
221
|
+
const cart = totals as HttpTypes.StoreCart
|
|
222
|
+
const savedAddresses = customer?.addresses ?? []
|
|
223
|
+
const checkoutStep = savedAddresses.length > 0 ? "payment" : "address"
|
|
224
|
+
|
|
225
|
+
setIsRedirecting(true)
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
await trackCheckoutAnalytics(cart)
|
|
229
|
+
|
|
230
|
+
if (savedAddresses.length > 0) {
|
|
231
|
+
const defaultAddress =
|
|
232
|
+
savedAddresses.find(
|
|
233
|
+
(a) =>
|
|
234
|
+
a.is_default_shipping ||
|
|
235
|
+
a.metadata?.is_default === "true" ||
|
|
236
|
+
a.metadata?.is_default === true
|
|
237
|
+
) || savedAddresses[0]
|
|
238
|
+
|
|
239
|
+
if (defaultAddress) {
|
|
240
|
+
const formData = new FormData()
|
|
241
|
+
formData.append("shipping_address.first_name", defaultAddress.first_name || "")
|
|
242
|
+
formData.append("shipping_address.last_name", defaultAddress.last_name || "")
|
|
243
|
+
formData.append("shipping_address.address_1", defaultAddress.address_1 || "")
|
|
244
|
+
formData.append("shipping_address.address_2", defaultAddress.address_2 || "")
|
|
245
|
+
formData.append("shipping_address.company", defaultAddress.company || "")
|
|
246
|
+
formData.append("shipping_address.postal_code", defaultAddress.postal_code || "")
|
|
247
|
+
formData.append("shipping_address.city", defaultAddress.city || "")
|
|
248
|
+
formData.append("shipping_address.country_code", defaultAddress.country_code || "")
|
|
249
|
+
formData.append("shipping_address.province", defaultAddress.province || "")
|
|
250
|
+
formData.append("shipping_address.phone", defaultAddress.phone || "")
|
|
251
|
+
formData.append("email", customer?.email || "")
|
|
252
|
+
formData.append("same_as_billing", "on")
|
|
253
|
+
|
|
254
|
+
const { setAddresses } = await import("medusa-storefront-data/cart")
|
|
255
|
+
await setAddresses(null, formData)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (isOnCartPage) {
|
|
260
|
+
router.push(`/${countryCode}/checkout?step=${checkoutStep}`)
|
|
261
|
+
}
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.error("Proceed to checkout failed:", error)
|
|
264
|
+
if (isOnCartPage) {
|
|
265
|
+
router.push(`/${countryCode}/checkout?step=address`)
|
|
266
|
+
}
|
|
267
|
+
} finally {
|
|
268
|
+
setIsRedirecting(false)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<div className="bg-[#FFFAFE] rounded-lg p-3 min-[550px]:p-4 sm:p-5 md:p-6 min-[1023px]:p-4 min-[1150px]:p-5 min-[1360px]:p-6 border shadow-sm w-full" style={{ borderColor: '#C0C0C0' }}>
|
|
274
|
+
<ProcessingOverlay isOpen={showOverlay} variant={processingVariant} />
|
|
275
|
+
<h3 className="text-sm min-[550px]:text-base sm:text-lg min-[1023px]:text-base min-[1150px]:text-lg min-[1360px]:text-lg font-bold text-gray-900 mb-2.5 min-[550px]:mb-3 sm:mb-4 min-[1023px]:mb-3 min-[1150px]:mb-4 min-[1360px]:mb-4">
|
|
276
|
+
PRICE SUMMARY ({itemCount} {itemCount === 1 ? "ITEM" : "ITEMS"})
|
|
277
|
+
</h3>
|
|
278
|
+
|
|
279
|
+
<div className="flex flex-col gap-y-2 min-[550px]:gap-y-2.5 sm:gap-y-3 min-[1023px]:gap-y-2 min-[1150px]:gap-y-3 min-[1360px]:gap-y-3 text-xs min-[550px]:text-sm sm:text-base min-[1023px]:text-sm min-[1150px]:text-base min-[1360px]:text-base text-gray-900">
|
|
280
|
+
<div className="flex items-center justify-between">
|
|
281
|
+
<span>Total MRP</span>
|
|
282
|
+
<span data-testid="cart-mrp" data-value={totalMRP}>
|
|
283
|
+
{convertToLocale({ amount: totalMRP, currency_code })}
|
|
284
|
+
</span>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
{calculatedDiscount > 0 && (
|
|
288
|
+
<div className="flex items-center justify-between">
|
|
289
|
+
<span>Discount</span>
|
|
290
|
+
<span
|
|
291
|
+
className="text-gray-900"
|
|
292
|
+
data-testid="cart-discount"
|
|
293
|
+
data-value={calculatedDiscount}
|
|
294
|
+
>
|
|
295
|
+
{convertToLocale({
|
|
296
|
+
amount: -calculatedDiscount,
|
|
297
|
+
currency_code,
|
|
298
|
+
})}
|
|
299
|
+
</span>
|
|
300
|
+
</div>
|
|
301
|
+
)}
|
|
302
|
+
|
|
303
|
+
<div className="flex items-center justify-between">
|
|
304
|
+
<span>GST</span>
|
|
305
|
+
<span data-testid="cart-taxes" data-value={gstAmount}>
|
|
306
|
+
{convertToLocale({ amount: gstAmount, currency_code })}
|
|
307
|
+
</span>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<div className="flex items-center justify-between">
|
|
311
|
+
<span>Shipping</span>
|
|
312
|
+
<span data-testid="cart-shipping" data-value={shippingAmount} className="flex items-center gap-x-2">
|
|
313
|
+
{isAutoSettingShipping && (
|
|
314
|
+
<span className="w-4 h-4 border-2 border-[#8B5AB1] border-t-transparent rounded-full animate-spin"></span>
|
|
315
|
+
)}
|
|
316
|
+
{convertToLocale({ amount: shippingAmount, currency_code })}
|
|
317
|
+
</span>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<div className="h-px w-full border-b border-gray-200 my-3 min-[550px]:my-4" />
|
|
322
|
+
|
|
323
|
+
<div className="flex items-center justify-between text-xs min-[550px]:text-sm sm:text-base min-[1023px]:text-sm min-[1150px]:text-base min-[1360px]:text-base font-bold text-gray-900 mb-2.5 min-[550px]:mb-3 sm:mb-4 min-[1023px]:mb-3 min-[1150px]:mb-4 min-[1360px]:mb-4">
|
|
324
|
+
<span>Total</span>
|
|
325
|
+
<span
|
|
326
|
+
className="text-sm min-[550px]:text-base sm:text-lg min-[1023px]:text-base min-[1150px]:text-lg min-[1360px]:text-lg"
|
|
327
|
+
data-testid="cart-total"
|
|
328
|
+
data-value={finalTotal}
|
|
329
|
+
>
|
|
330
|
+
{convertToLocale({ amount: finalTotal, currency_code })}
|
|
331
|
+
</span>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
{calculatedDiscount > 0 && (
|
|
335
|
+
<>
|
|
336
|
+
<div className="h-px w-full border-b border-dashed border-gray-300 my-2.5 min-[550px]:my-3 sm:my-4 min-[1023px]:my-3 min-[1150px]:my-4 min-[1360px]:my-4" />
|
|
337
|
+
<div className="text-center">
|
|
338
|
+
<p className="text-green-600 font-medium text-xs min-[550px]:text-sm sm:text-base min-[1023px]:text-sm min-[1150px]:text-base min-[1360px]:text-base">
|
|
339
|
+
🎉 Yayy!!! You've saved {convertToLocale({ amount: calculatedDiscount, currency_code })}
|
|
340
|
+
</p>
|
|
341
|
+
</div>
|
|
342
|
+
</>
|
|
343
|
+
)}
|
|
344
|
+
|
|
345
|
+
{/* No manual continue button needed as we are in one-step mode */}
|
|
346
|
+
{isOnCartPage && (
|
|
347
|
+
<button
|
|
348
|
+
onClick={handleProceedToCheckout}
|
|
349
|
+
disabled={isRedirecting}
|
|
350
|
+
data-ga-event="proceed_to_checkout_click"
|
|
351
|
+
data-meta-event="InitiateCheckout"
|
|
352
|
+
data-meta-action="proceed_to_checkout"
|
|
353
|
+
data-ga-label={convertToLocale({ amount: finalTotal, currency_code })}
|
|
354
|
+
className="w-full h-10 min-[550px]:h-11 sm:h-12 min-[1023px]:h-11 min-[1150px]:h-12 min-[1360px]:h-12 text-xs min-[550px]:text-sm sm:text-base min-[1023px]:text-sm min-[1150px]:text-base min-[1360px]:text-base text-white font-medium transition-colors duration-200 hover:opacity-90 mt-3 min-[550px]:mt-4 sm:mt-6 min-[1023px]:mt-5 min-[1150px]:mt-6 min-[1360px]:mt-6 disabled:opacity-50"
|
|
355
|
+
style={{
|
|
356
|
+
backgroundColor: '#8B5AB1',
|
|
357
|
+
borderRadius: '30px'
|
|
358
|
+
}}
|
|
359
|
+
data-testid="checkout-button"
|
|
360
|
+
>
|
|
361
|
+
{isRedirecting ? "Loading..." : "Proceed to Checkout"}
|
|
362
|
+
</button>
|
|
363
|
+
)}
|
|
364
|
+
{!isOnCartPage && (
|
|
365
|
+
<div className="w-full">
|
|
366
|
+
{activeStep === "delivery" || activeStep === "payment" || !((totals as any)?.shipping_address?.address_1) || pathname.includes("/checkout") ? (
|
|
367
|
+
<div className="flex flex-col gap-3 mt-3 min-[550px]:mt-4 sm:mt-6 min-[1023px]:mt-5 min-[1150px]:mt-6 min-[1360px]:mt-6">
|
|
368
|
+
{!isRedirecting ? (
|
|
369
|
+
(() => {
|
|
370
|
+
const isCheckoutReady =
|
|
371
|
+
((totals as any).shipping_methods?.length > 0) &&
|
|
372
|
+
!!((totals as any).shipping_address?.address_1) &&
|
|
373
|
+
((totals as any).shipping_address?.postal_code?.length >= 6);
|
|
374
|
+
const isPaymentDisabled = isAutoSettingShipping || !isCheckoutReady;
|
|
375
|
+
|
|
376
|
+
return (
|
|
377
|
+
<>
|
|
378
|
+
<button
|
|
379
|
+
onClick={async () => {
|
|
380
|
+
trackPaymentMethodClick("cod")
|
|
381
|
+
setProcessingVariant("order")
|
|
382
|
+
setShowOverlay(true)
|
|
383
|
+
setIsRedirecting(true)
|
|
384
|
+
try {
|
|
385
|
+
await syncCheckoutAddressFromDom({
|
|
386
|
+
email: (totals as any)?.email,
|
|
387
|
+
shipping_address: (totals as any)?.shipping_address,
|
|
388
|
+
})
|
|
389
|
+
await syncLastEtdToCart()
|
|
390
|
+
const { initiatePaymentSession, placeOrder } = await import("medusa-storefront-data/cart")
|
|
391
|
+
await initiatePaymentSession(totals as any, { provider_id: "pp_system_default" })
|
|
392
|
+
await placeOrder()
|
|
393
|
+
} catch (err: any) {
|
|
394
|
+
if (err.message === "NEXT_REDIRECT") {
|
|
395
|
+
throw err // Next.js needs this error to perform the redirect
|
|
396
|
+
}
|
|
397
|
+
setIsRedirecting(false)
|
|
398
|
+
setShowOverlay(false)
|
|
399
|
+
alert("Oops! Something went wrong with Cash on Delivery. " + err.message)
|
|
400
|
+
}
|
|
401
|
+
}}
|
|
402
|
+
disabled={isPaymentDisabled}
|
|
403
|
+
data-ga-event="cod_payment_click"
|
|
404
|
+
data-meta-event="AddPaymentInfo"
|
|
405
|
+
data-meta-action="select_payment_method"
|
|
406
|
+
data-meta-payment-type="cod"
|
|
407
|
+
className={clx(
|
|
408
|
+
"w-full py-2.5 min-[550px]:py-3 text-xs min-[550px]:text-sm sm:text-base text-gray-900 bg-white border-2 font-bold rounded-lg transition-colors border-[#8B5AB1]",
|
|
409
|
+
{
|
|
410
|
+
"hover:bg-purple-50": !isPaymentDisabled,
|
|
411
|
+
"opacity-50 cursor-not-allowed": isPaymentDisabled
|
|
412
|
+
}
|
|
413
|
+
)}
|
|
414
|
+
style={{ borderRadius: '30px' }}
|
|
415
|
+
>
|
|
416
|
+
Cash on Delivery
|
|
417
|
+
</button>
|
|
418
|
+
<button
|
|
419
|
+
onClick={async () => {
|
|
420
|
+
trackPaymentMethodClick("razorpay")
|
|
421
|
+
setProcessingVariant("payment")
|
|
422
|
+
setShowOverlay(true)
|
|
423
|
+
setIsRedirecting(true)
|
|
424
|
+
try {
|
|
425
|
+
const synced = await syncCheckoutAddressFromDom({
|
|
426
|
+
email: (totals as any)?.email,
|
|
427
|
+
shipping_address: (totals as any)?.shipping_address,
|
|
428
|
+
})
|
|
429
|
+
await syncLastEtdToCart()
|
|
430
|
+
const { initiatePaymentSession, placeOrder } = await import("medusa-storefront-data/cart")
|
|
431
|
+
const updatedCart: any = await initiatePaymentSession(totals as any, { provider_id: "pp_razorpay_razorpay" }).catch(() => initiatePaymentSession(totals as any, { provider_id: "razorpay" }))
|
|
432
|
+
|
|
433
|
+
const razorpaySession = updatedCart?.payment_collection?.payment_sessions?.find(
|
|
434
|
+
(s: any) => s.provider_id.includes("razorpay")
|
|
435
|
+
)
|
|
436
|
+
if (!razorpaySession?.data?.id) throw new Error("Could not initialize Razorpay")
|
|
437
|
+
|
|
438
|
+
const paymentEmail = synced.email || (updatedCart as any)?.email || (totals as any)?.email
|
|
439
|
+
const paymentPhone =
|
|
440
|
+
synced.shipping_address.phone ||
|
|
441
|
+
(updatedCart as any)?.shipping_address?.phone ||
|
|
442
|
+
(totals as any)?.shipping_address?.phone
|
|
443
|
+
|
|
444
|
+
// Load loadRazorpay dynamically so it resolves window.Razorpay
|
|
445
|
+
const useRazorpayMod = await import("react-razorpay")
|
|
446
|
+
const useRazorpay = useRazorpayMod.useRazorpay || useRazorpayMod.default?.useRazorpay || (globalThis as any).useRazorpay
|
|
447
|
+
|
|
448
|
+
// We must load via script if hook isn't directly invocable in this async context
|
|
449
|
+
// The safest pure JS way:
|
|
450
|
+
const script = document.createElement("script")
|
|
451
|
+
script.src = "https://checkout.razorpay.com/v1/checkout.js"
|
|
452
|
+
script.onload = () => {
|
|
453
|
+
const options = {
|
|
454
|
+
key: process.env.NEXT_PUBLIC_RAZORPAY_KEY ?? "",
|
|
455
|
+
amount: razorpaySession.amount,
|
|
456
|
+
order_id: razorpaySession.data.id,
|
|
457
|
+
currency: currency_code.toUpperCase(),
|
|
458
|
+
name: company.name,
|
|
459
|
+
description: `Secure checkout for your order`,
|
|
460
|
+
handler: async function (response: any) {
|
|
461
|
+
await placeOrder().catch((err: any) => {
|
|
462
|
+
if (err.message === "NEXT_REDIRECT") throw err;
|
|
463
|
+
alert("Error placing order: " + err.message);
|
|
464
|
+
})
|
|
465
|
+
},
|
|
466
|
+
prefill: {
|
|
467
|
+
name: `${(totals as any).billing_address?.first_name || synced.shipping_address.first_name || ""} ${(totals as any).billing_address?.last_name || synced.shipping_address.last_name || ""}`.trim(),
|
|
468
|
+
email: paymentEmail,
|
|
469
|
+
contact: paymentPhone || undefined,
|
|
470
|
+
},
|
|
471
|
+
theme: {
|
|
472
|
+
color: "#8B5AB1"
|
|
473
|
+
},
|
|
474
|
+
modal: {
|
|
475
|
+
ondismiss: () => {
|
|
476
|
+
setIsRedirecting(false)
|
|
477
|
+
setShowOverlay(false)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const rzp = new (window as any).Razorpay(options)
|
|
482
|
+
rzp.on("payment.failed", function (response: any) {
|
|
483
|
+
setIsRedirecting(false)
|
|
484
|
+
setShowOverlay(false)
|
|
485
|
+
})
|
|
486
|
+
rzp.open()
|
|
487
|
+
}
|
|
488
|
+
document.body.appendChild(script)
|
|
489
|
+
|
|
490
|
+
} catch (err: any) {
|
|
491
|
+
setIsRedirecting(false)
|
|
492
|
+
setShowOverlay(false)
|
|
493
|
+
alert("Oops! Something went wrong with Razorpay. " + err.message)
|
|
494
|
+
}
|
|
495
|
+
}}
|
|
496
|
+
disabled={isPaymentDisabled}
|
|
497
|
+
data-ga-event="razorpay_payment_click"
|
|
498
|
+
data-meta-event="AddPaymentInfo"
|
|
499
|
+
data-meta-action="select_payment_method"
|
|
500
|
+
data-meta-payment-type="razorpay"
|
|
501
|
+
className={clx(
|
|
502
|
+
"w-full py-2.5 min-[550px]:py-3 text-xs min-[550px]:text-sm sm:text-base text-white font-bold rounded-lg transition-colors bg-[#8B5AB1]",
|
|
503
|
+
{
|
|
504
|
+
"hover:opacity-90": !isPaymentDisabled,
|
|
505
|
+
"opacity-50 cursor-not-allowed": isPaymentDisabled
|
|
506
|
+
}
|
|
507
|
+
)}
|
|
508
|
+
style={{ borderRadius: '30px' }}
|
|
509
|
+
>
|
|
510
|
+
Pay with Razorpay
|
|
511
|
+
</button>
|
|
512
|
+
</>
|
|
513
|
+
); })()
|
|
514
|
+
) : (
|
|
515
|
+
<button
|
|
516
|
+
disabled
|
|
517
|
+
className="w-full py-2.5 min-[550px]:py-3 text-xs min-[550px]:text-sm sm:text-base text-white font-bold rounded-lg bg-gray-400 opacity-50 cursor-not-allowed"
|
|
518
|
+
style={{ borderRadius: '30px' }}
|
|
519
|
+
>
|
|
520
|
+
Processing...
|
|
521
|
+
</button>
|
|
522
|
+
)}
|
|
523
|
+
</div>
|
|
524
|
+
) : activeStep === "address" ? (
|
|
525
|
+
<button
|
|
526
|
+
onClick={() => {
|
|
527
|
+
window.dispatchEvent(new CustomEvent('trigger-checkout-step'))
|
|
528
|
+
}}
|
|
529
|
+
className="w-full py-2.5 min-[550px]:py-3 sm:py-4 min-[1023px]:py-3 min-[1150px]:py-4 min-[1360px]:py-4 text-xs min-[550px]:text-sm sm:text-base min-[1023px]:text-sm min-[1150px]:text-base min-[1360px]:text-base text-white font-medium mt-3 min-[550px]:mt-4 sm:mt-6 min-[1023px]:mt-5 min-[1150px]:mt-6 min-[1360px]:mt-6 rounded-lg transition-colors duration-200 hover:opacity-90"
|
|
530
|
+
style={{
|
|
531
|
+
backgroundColor: '#8B5AB1',
|
|
532
|
+
borderRadius: '30px'
|
|
533
|
+
}}
|
|
534
|
+
data-testid="checkout-button-address"
|
|
535
|
+
>
|
|
536
|
+
Continue to delivery
|
|
537
|
+
</button>
|
|
538
|
+
) : (
|
|
539
|
+
<button
|
|
540
|
+
onClick={handleProceedToCheckout}
|
|
541
|
+
disabled={isRedirecting}
|
|
542
|
+
data-ga-event="proceed_to_checkout_click"
|
|
543
|
+
data-meta-event="InitiateCheckout"
|
|
544
|
+
data-meta-action="proceed_to_checkout"
|
|
545
|
+
data-ga-label={convertToLocale({ amount: finalTotal, currency_code })}
|
|
546
|
+
className="w-full h-10 min-[550px]:h-11 sm:h-12 min-[1023px]:h-11 min-[1150px]:h-12 min-[1360px]:h-12 text-xs min-[550px]:text-sm sm:text-base min-[1023px]:text-sm min-[1150px]:text-base min-[1360px]:text-base text-white font-medium transition-colors duration-200 hover:opacity-90 mt-3 min-[550px]:mt-4 sm:mt-6 min-[1023px]:mt-5 min-[1150px]:mt-6 min-[1360px]:mt-6 disabled:opacity-50"
|
|
547
|
+
style={{
|
|
548
|
+
backgroundColor: '#8B5AB1',
|
|
549
|
+
borderRadius: '30px'
|
|
550
|
+
}}
|
|
551
|
+
data-testid="checkout-button"
|
|
552
|
+
>
|
|
553
|
+
{isRedirecting ? "Loading..." : "Proceed to Checkout"}
|
|
554
|
+
</button>
|
|
555
|
+
)}
|
|
556
|
+
</div>
|
|
557
|
+
)}
|
|
558
|
+
</div>
|
|
559
|
+
)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export default CartTotals
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Label } from "@medusajs/ui"
|
|
2
|
+
import React from "react"
|
|
3
|
+
import Color from "color"
|
|
4
|
+
|
|
5
|
+
type CheckboxProps = {
|
|
6
|
+
checked?: boolean
|
|
7
|
+
onChange?: () => void
|
|
8
|
+
label: string
|
|
9
|
+
name?: string
|
|
10
|
+
color?: string
|
|
11
|
+
hex?: string
|
|
12
|
+
'data-testid'?: string
|
|
13
|
+
'data-ga-event'?: string
|
|
14
|
+
'data-ga-label'?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const CheckboxWithLabel: React.FC<CheckboxProps> = ({
|
|
18
|
+
checked = true,
|
|
19
|
+
onChange,
|
|
20
|
+
label,
|
|
21
|
+
name,
|
|
22
|
+
color,
|
|
23
|
+
hex,
|
|
24
|
+
'data-testid': dataTestId,
|
|
25
|
+
'data-ga-event': dataGaEvent,
|
|
26
|
+
'data-ga-label': dataGaLabel
|
|
27
|
+
}) => {
|
|
28
|
+
const colorHex = React.useMemo(() => {
|
|
29
|
+
if (hex) return hex
|
|
30
|
+
if (!color) return null
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Try parsing the original color name (handles things like "darkgreen")
|
|
34
|
+
return Color(color.toLowerCase().replace(/\s+/g, '')).hex()
|
|
35
|
+
} catch (e) {
|
|
36
|
+
try {
|
|
37
|
+
// Try parsing without modification (handles things like "rgb(0,0,0)")
|
|
38
|
+
return Color(color).hex()
|
|
39
|
+
} catch (e2) {
|
|
40
|
+
// Final fallback to the color string itself (might be a valid CSS name or hex already)
|
|
41
|
+
return color
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}, [hex, color])
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="flex items-center space-x-2 px-1 py-0.5">
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
role="checkbox"
|
|
51
|
+
aria-checked={checked}
|
|
52
|
+
onClick={onChange}
|
|
53
|
+
name={name}
|
|
54
|
+
data-testid={dataTestId}
|
|
55
|
+
data-ga-event={dataGaEvent}
|
|
56
|
+
data-ga-label={dataGaLabel}
|
|
57
|
+
className="w-5 h-5 rounded-md border-2 flex items-center justify-center transition-all focus:outline-none focus:ring-2 focus:ring-offset-2"
|
|
58
|
+
style={checked ? {
|
|
59
|
+
backgroundColor: '#8B5AB1',
|
|
60
|
+
borderColor: '#8B5AB1'
|
|
61
|
+
} : {
|
|
62
|
+
backgroundColor: 'white',
|
|
63
|
+
borderColor: '#C0C0C0'
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
{checked && (
|
|
67
|
+
<svg
|
|
68
|
+
className="w-3 h-3 text-white"
|
|
69
|
+
fill="none"
|
|
70
|
+
strokeLinecap="round"
|
|
71
|
+
strokeLinejoin="round"
|
|
72
|
+
strokeWidth="2"
|
|
73
|
+
viewBox="0 0 24 24"
|
|
74
|
+
stroke="currentColor"
|
|
75
|
+
>
|
|
76
|
+
<path d="M5 13l4 4L19 7"></path>
|
|
77
|
+
</svg>
|
|
78
|
+
)}
|
|
79
|
+
</button>
|
|
80
|
+
<Label
|
|
81
|
+
htmlFor={name || "checkbox"}
|
|
82
|
+
className="!transform-none !txt-medium cursor-pointer flex items-center gap-2"
|
|
83
|
+
size="large"
|
|
84
|
+
onClick={onChange}
|
|
85
|
+
>
|
|
86
|
+
{colorHex && (
|
|
87
|
+
<div
|
|
88
|
+
className="w-4 h-4 rounded-full border border-gray-100 shadow-sm"
|
|
89
|
+
style={{ backgroundColor: colorHex }}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
{label}
|
|
93
|
+
</Label>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default CheckboxWithLabel
|