medusa-ui-common 2.3.0 → 3.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 +8 -3
- package/src/common/components/breadcrumb/index.tsx +2 -43
- package/src/common/components/checkbox/index.tsx +15 -7
- package/src/common/components/filter-checkbox-group/index.tsx +2 -134
- package/src/common/components/login-popup/index.tsx +2 -78
- package/src/common/components/side-panel/index.tsx +2 -65
- package/src/constants/payments.tsx +2 -31
- package/src/page-compose/types.ts +31 -0
- package/src/storefront-providers.tsx +2 -10
- package/src/util/checkout-dom.ts +2 -65
- package/src/util/returns.ts +3 -72
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "medusa-ui-common",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Shared storefront UI primitives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
"medusa-contact-logic-plugin": "^2.0.0",
|
|
24
24
|
"medusa-reviews-logic": "^2.0.0",
|
|
25
25
|
"medusa-storefront-analytics": "^1.0.0",
|
|
26
|
-
"medusa-storefront-data": "^2.
|
|
26
|
+
"medusa-storefront-data": "^2.5.3",
|
|
27
27
|
"medusa-storefront-hooks": "^1.0.0",
|
|
28
28
|
"medusa-wishlist-logic": "^2.0.0",
|
|
29
|
-
"medusa-storefront-theme-base": "^
|
|
29
|
+
"medusa-storefront-theme-base": "^3.0.0",
|
|
30
30
|
"next": ">=14.0.0",
|
|
31
31
|
"react": "^18.0.0 || ^19.0.0",
|
|
32
32
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
@@ -87,6 +87,11 @@
|
|
|
87
87
|
"types": "./src/constants/*",
|
|
88
88
|
"import": "./src/constants/*",
|
|
89
89
|
"default": "./src/constants/*"
|
|
90
|
+
},
|
|
91
|
+
"./page-compose/types": {
|
|
92
|
+
"types": "./src/page-compose/types.ts",
|
|
93
|
+
"import": "./src/page-compose/types.ts",
|
|
94
|
+
"default": "./src/page-compose/types.ts"
|
|
90
95
|
}
|
|
91
96
|
},
|
|
92
97
|
"typesVersions": {
|
|
@@ -1,43 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type BreadcrumbProps = {
|
|
5
|
-
product: HttpTypes.StoreProduct
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export default function Breadcrumb({ product }: BreadcrumbProps) {
|
|
9
|
-
const primaryCategory = product.categories?.[0]
|
|
10
|
-
|
|
11
|
-
return (
|
|
12
|
-
<nav className="text-xs sm:text-sm text-[var(--sf-color-text)] mb-3 sm:mb-4 overflow-x-auto font-medium">
|
|
13
|
-
<div className="flex items-center whitespace-nowrap">
|
|
14
|
-
<LocalizedClientLink href="/" className="hover:text-[var(--sf-color-text)] transition-colors">
|
|
15
|
-
Home
|
|
16
|
-
</LocalizedClientLink>
|
|
17
|
-
<span className="mx-2">/</span>
|
|
18
|
-
<LocalizedClientLink href="/store" className="hover:text-[var(--sf-color-text)] transition-colors">
|
|
19
|
-
Shop
|
|
20
|
-
</LocalizedClientLink>
|
|
21
|
-
{primaryCategory && (
|
|
22
|
-
<>
|
|
23
|
-
<span className="mx-2">/</span>
|
|
24
|
-
<LocalizedClientLink
|
|
25
|
-
href={`/store?category=${primaryCategory.id}`}
|
|
26
|
-
className="hover:text-[var(--sf-color-text)] transition-colors"
|
|
27
|
-
>
|
|
28
|
-
{primaryCategory.name}
|
|
29
|
-
</LocalizedClientLink>
|
|
30
|
-
</>
|
|
31
|
-
)}
|
|
32
|
-
<span className="mx-2">/</span>
|
|
33
|
-
<span className="text-[var(--sf-color-text)] truncate max-w-[150px] sm:max-w-[300px] md:max-w-[400px] inline-block align-bottom" title={product.title ?? ""}>
|
|
34
|
-
{product.title}
|
|
35
|
-
</span>
|
|
36
|
-
</div>
|
|
37
|
-
</nav>
|
|
38
|
-
)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
1
|
+
/** @deprecated Import from `medusa-ui-product/products/components/breadcrumb` */
|
|
2
|
+
export { default } from "medusa-ui-product/products/components/breadcrumb"
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Label } from "@medusajs/ui"
|
|
2
1
|
import React from "react"
|
|
3
2
|
import Color from "color"
|
|
4
3
|
|
|
@@ -43,18 +42,21 @@ const CheckboxWithLabel: React.FC<CheckboxProps> = ({
|
|
|
43
42
|
}
|
|
44
43
|
}, [hex, color])
|
|
45
44
|
|
|
45
|
+
const labelId = React.useId()
|
|
46
|
+
|
|
46
47
|
return (
|
|
47
48
|
<div className="flex items-center space-x-2 px-1 py-0.5">
|
|
48
49
|
<button
|
|
49
50
|
type="button"
|
|
50
51
|
role="checkbox"
|
|
51
52
|
aria-checked={checked}
|
|
53
|
+
aria-labelledby={labelId}
|
|
52
54
|
onClick={onChange}
|
|
53
55
|
name={name}
|
|
54
56
|
data-testid={dataTestId}
|
|
55
57
|
data-ga-event={dataGaEvent}
|
|
56
58
|
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"
|
|
59
|
+
className="w-5 h-5 shrink-0 rounded-md border-2 flex items-center justify-center transition-all focus:outline-none focus:ring-2 focus:ring-offset-2"
|
|
58
60
|
style={checked ? {
|
|
59
61
|
backgroundColor: 'var(--sf-btn-primary)',
|
|
60
62
|
borderColor: 'var(--sf-btn-primary)'
|
|
@@ -77,11 +79,17 @@ const CheckboxWithLabel: React.FC<CheckboxProps> = ({
|
|
|
77
79
|
</svg>
|
|
78
80
|
)}
|
|
79
81
|
</button>
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
size="large"
|
|
82
|
+
<span
|
|
83
|
+
id={labelId}
|
|
84
|
+
role="presentation"
|
|
84
85
|
onClick={onChange}
|
|
86
|
+
onKeyDown={(e) => {
|
|
87
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
88
|
+
e.preventDefault()
|
|
89
|
+
onChange?.()
|
|
90
|
+
}
|
|
91
|
+
}}
|
|
92
|
+
className="txt-medium cursor-pointer flex items-center gap-2 text-[var(--sf-color-text)]"
|
|
85
93
|
>
|
|
86
94
|
{colorHex && (
|
|
87
95
|
<div
|
|
@@ -90,7 +98,7 @@ const CheckboxWithLabel: React.FC<CheckboxProps> = ({
|
|
|
90
98
|
/>
|
|
91
99
|
)}
|
|
92
100
|
{label}
|
|
93
|
-
</
|
|
101
|
+
</span>
|
|
94
102
|
</div>
|
|
95
103
|
)
|
|
96
104
|
}
|
|
@@ -1,134 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import React from "react"
|
|
4
|
-
|
|
5
|
-
type FilterCheckboxGroupProps = {
|
|
6
|
-
title: string
|
|
7
|
-
items: { value: string; label: string }[]
|
|
8
|
-
values: string[]
|
|
9
|
-
handleChange: (value: string) => void
|
|
10
|
-
isColor?: boolean
|
|
11
|
-
'data-testid'?: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const FilterCheckboxGroup = ({
|
|
15
|
-
title,
|
|
16
|
-
items,
|
|
17
|
-
values,
|
|
18
|
-
handleChange,
|
|
19
|
-
isColor,
|
|
20
|
-
'data-testid': dataTestId,
|
|
21
|
-
}: FilterCheckboxGroupProps) => {
|
|
22
|
-
const [isOpen, setIsOpen] = React.useState(true)
|
|
23
|
-
const [showAll, setShowAll] = React.useState(false)
|
|
24
|
-
|
|
25
|
-
// Optimistic local state — reflects clicks instantly without waiting for server
|
|
26
|
-
const [optimisticValues, setOptimisticValues] = React.useState<string[]>(values)
|
|
27
|
-
|
|
28
|
-
// Sync back when server props update (after navigation completes)
|
|
29
|
-
React.useEffect(() => {
|
|
30
|
-
setOptimisticValues(values)
|
|
31
|
-
}, [values])
|
|
32
|
-
|
|
33
|
-
const initialItemsCount = 6
|
|
34
|
-
const hasMore = items?.length > initialItemsCount
|
|
35
|
-
|
|
36
|
-
// Auto-expand if a selected item is hidden or if any item is selected
|
|
37
|
-
React.useEffect(() => {
|
|
38
|
-
if (values && values.length > 0) {
|
|
39
|
-
setIsOpen(true)
|
|
40
|
-
if (hasMore && items) {
|
|
41
|
-
const hiddenItems = items.slice(initialItemsCount)
|
|
42
|
-
const isAnyHiddenItemSelected = hiddenItems.some(item => values.includes(item.value))
|
|
43
|
-
if (isAnyHiddenItemSelected) {
|
|
44
|
-
setShowAll(true)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}, [values, items, hasMore])
|
|
49
|
-
|
|
50
|
-
const handleOptimisticChange = (value: string) => {
|
|
51
|
-
// Immediately toggle locally — no flicker
|
|
52
|
-
setOptimisticValues((prev) =>
|
|
53
|
-
prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value]
|
|
54
|
-
)
|
|
55
|
-
// Trigger actual URL navigation in background
|
|
56
|
-
handleChange(value)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const displayedItems = showAll ? items : items?.slice(0, initialItemsCount)
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
<div className="flex flex-col border-b border-gray-100 pb-4 last:border-0 last:pb-0">
|
|
63
|
-
<button
|
|
64
|
-
onClick={() => setIsOpen(!isOpen)}
|
|
65
|
-
className="flex items-center justify-between w-full group transition-all py-1"
|
|
66
|
-
>
|
|
67
|
-
<Heading
|
|
68
|
-
level="h3"
|
|
69
|
-
className="text-sm font-bold text-[var(--sf-color-text)] uppercase tracking-wider group-hover:text-[var(--sf-btn-primary)] transition-colors"
|
|
70
|
-
>
|
|
71
|
-
{title}
|
|
72
|
-
</Heading>
|
|
73
|
-
<span
|
|
74
|
-
className={`transform transition-transform duration-300 text-[var(--sf-color-text-muted)] group-hover:text-[var(--sf-btn-primary)] ${isOpen ? "" : "-rotate-180"}`}
|
|
75
|
-
>
|
|
76
|
-
<svg
|
|
77
|
-
width="18"
|
|
78
|
-
height="18"
|
|
79
|
-
viewBox="0 0 24 24"
|
|
80
|
-
fill="none"
|
|
81
|
-
stroke="currentColor"
|
|
82
|
-
strokeWidth="2.5"
|
|
83
|
-
strokeLinecap="round"
|
|
84
|
-
strokeLinejoin="round"
|
|
85
|
-
>
|
|
86
|
-
<polyline points="18 15 12 9 6 15"></polyline>
|
|
87
|
-
</svg>
|
|
88
|
-
</span>
|
|
89
|
-
</button>
|
|
90
|
-
|
|
91
|
-
<div
|
|
92
|
-
className={`flex flex-col gap-y-1 transition-all duration-300 ease-in-out ${isOpen ? "mt-3 opacity-100 px-0.5" : "max-h-0 opacity-0 invisible overflow-hidden"}`}
|
|
93
|
-
style={isOpen ? { maxHeight: showAll ? '1000px' : '300px' } : {}}
|
|
94
|
-
>
|
|
95
|
-
{displayedItems?.map((i) => (
|
|
96
|
-
<CheckboxWithLabel
|
|
97
|
-
key={i.value}
|
|
98
|
-
label={i.label}
|
|
99
|
-
color={isColor ? i.value : undefined}
|
|
100
|
-
checked={optimisticValues.includes(i.value)}
|
|
101
|
-
onChange={() => handleOptimisticChange(i.value)}
|
|
102
|
-
data-testid={dataTestId}
|
|
103
|
-
data-ga-event="filter_checkbox_click"
|
|
104
|
-
data-ga-label={`${title} - ${i.label}`}
|
|
105
|
-
/>
|
|
106
|
-
))}
|
|
107
|
-
|
|
108
|
-
{hasMore && isOpen && (
|
|
109
|
-
<button
|
|
110
|
-
onClick={() => setShowAll(!showAll)}
|
|
111
|
-
className="text-xs font-bold text-[var(--sf-btn-primary)] hover:underline mt-2 flex items-center gap-1 uppercase tracking-tight"
|
|
112
|
-
>
|
|
113
|
-
{showAll ? "View Less" : `View More (${items.length - initialItemsCount})`}
|
|
114
|
-
<svg
|
|
115
|
-
width="12"
|
|
116
|
-
height="12"
|
|
117
|
-
viewBox="0 0 24 24"
|
|
118
|
-
fill="none"
|
|
119
|
-
stroke="currentColor"
|
|
120
|
-
strokeWidth="3"
|
|
121
|
-
strokeLinecap="round"
|
|
122
|
-
strokeLinejoin="round"
|
|
123
|
-
className={`transform transition-transform ${showAll ? "rotate-180" : ""}`}
|
|
124
|
-
>
|
|
125
|
-
<polyline points="6 9 12 15 18 9"></polyline>
|
|
126
|
-
</svg>
|
|
127
|
-
</button>
|
|
128
|
-
)}
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export default FilterCheckboxGroup
|
|
1
|
+
/** @deprecated Import from `medusa-ui-product/store/components/filter-checkbox-group` */
|
|
2
|
+
export { default } from "medusa-ui-product/store/components/filter-checkbox-group"
|
|
@@ -1,78 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { useRouter, usePathname } from "next/navigation"
|
|
4
|
-
|
|
5
|
-
interface LoginPopupProps {
|
|
6
|
-
isOpen: boolean
|
|
7
|
-
onClose: () => void
|
|
8
|
-
countryCode?: string
|
|
9
|
-
message?: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export default function LoginPopup({ isOpen, onClose, countryCode = "us", message = "Please login to add products to your wishlist." }: LoginPopupProps) {
|
|
13
|
-
const router = useRouter()
|
|
14
|
-
const pathname = usePathname()
|
|
15
|
-
|
|
16
|
-
const handleLoginClick = () => {
|
|
17
|
-
// Store current URL in localStorage for redirect after login
|
|
18
|
-
if (typeof window !== "undefined") {
|
|
19
|
-
const currentPath = pathname || window.location.pathname
|
|
20
|
-
// Add a special marker so the login component knows to keep the user on this page
|
|
21
|
-
const redirectWithContext = currentPath.includes('?')
|
|
22
|
-
? `${currentPath}&login_context=keep`
|
|
23
|
-
: `${currentPath}?login_context=keep`
|
|
24
|
-
|
|
25
|
-
localStorage.setItem("loginRedirectUrl", redirectWithContext)
|
|
26
|
-
}
|
|
27
|
-
router.push(`/${countryCode}/account`)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (!isOpen) {
|
|
31
|
-
return null
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-white/10 backdrop-blur-md p-4 animate-in fade-in duration-200">
|
|
36
|
-
<div
|
|
37
|
-
className="bg-[var(--sf-color-background)] rounded-2xl shadow-2xl p-8 max-w-sm w-full mx-auto border border-gray-100 transform transition-all scale-100 animate-in zoom-in-95 duration-200"
|
|
38
|
-
onClick={(e) => e.stopPropagation()}
|
|
39
|
-
>
|
|
40
|
-
<div className="text-center">
|
|
41
|
-
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-purple-100 mb-4">
|
|
42
|
-
<svg className="h-6 w-6 text-[var(--sf-btn-primary)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
43
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
44
|
-
</svg>
|
|
45
|
-
</div>
|
|
46
|
-
<h2 className="text-xl font-bold text-[var(--sf-color-text)] mb-2">
|
|
47
|
-
Login Required
|
|
48
|
-
</h2>
|
|
49
|
-
<p className="text-[var(--sf-color-text-muted)] mb-8 text-sm leading-relaxed">
|
|
50
|
-
{message}
|
|
51
|
-
</p>
|
|
52
|
-
|
|
53
|
-
<div className="grid grid-cols-2 gap-3">
|
|
54
|
-
<button
|
|
55
|
-
onClick={onClose}
|
|
56
|
-
className="px-4 py-2.5 border border-gray-200 rounded-xl font-semibold text-[var(--sf-color-text-muted)] hover:bg-[color-mix(in_srgb,var(--sf-color-text-muted)_8%,transparent)] hover:border-gray-300 active:scale-95 transition-all duration-200"
|
|
57
|
-
>
|
|
58
|
-
Cancel
|
|
59
|
-
</button>
|
|
60
|
-
<button
|
|
61
|
-
onClick={handleLoginClick}
|
|
62
|
-
className="px-4 py-2.5 rounded-xl font-semibold text-white transition-all duration-200 hover:shadow-lg hover:shadow-purple-100 active:scale-95"
|
|
63
|
-
style={{ backgroundColor: 'var(--sf-btn-primary)' }}
|
|
64
|
-
onMouseEnter={(e) => {
|
|
65
|
-
e.currentTarget.style.backgroundColor = 'var(--sf-btn-primary-hover)'
|
|
66
|
-
}}
|
|
67
|
-
onMouseLeave={(e) => {
|
|
68
|
-
e.currentTarget.style.backgroundColor = 'var(--sf-btn-primary)'
|
|
69
|
-
}}
|
|
70
|
-
>
|
|
71
|
-
Login
|
|
72
|
-
</button>
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
</div>
|
|
77
|
-
)
|
|
78
|
-
}
|
|
1
|
+
/** @deprecated Import from `medusa-ui-product/products/components/login-popup` */
|
|
2
|
+
export { default } from "medusa-ui-product/products/components/login-popup"
|
|
@@ -1,65 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type SidePanelProps = {
|
|
5
|
-
isOpen: boolean
|
|
6
|
-
onClose: () => void
|
|
7
|
-
title: string
|
|
8
|
-
children: React.ReactNode
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const SidePanel: React.FC<SidePanelProps> = ({ isOpen, onClose, title, children }) => {
|
|
12
|
-
// Prevent body scroll when panel is open
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
if (isOpen) {
|
|
15
|
-
document.body.style.overflow = 'hidden'
|
|
16
|
-
} else {
|
|
17
|
-
document.body.style.overflow = 'unset'
|
|
18
|
-
}
|
|
19
|
-
return () => {
|
|
20
|
-
document.body.style.overflow = 'unset'
|
|
21
|
-
}
|
|
22
|
-
}, [isOpen])
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<>
|
|
26
|
-
{/* Backdrop */}
|
|
27
|
-
<div
|
|
28
|
-
className={clx(
|
|
29
|
-
"fixed inset-0 bg-black/40 backdrop-blur-sm transition-opacity duration-300 z-[999]",
|
|
30
|
-
isOpen ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"
|
|
31
|
-
)}
|
|
32
|
-
onClick={onClose}
|
|
33
|
-
/>
|
|
34
|
-
|
|
35
|
-
{/* Panel */}
|
|
36
|
-
<div
|
|
37
|
-
className={clx(
|
|
38
|
-
"fixed top-0 right-0 h-full w-[90%] min-[400px]:w-[350px] sm:w-[400px] md:w-[450px] lg:w-[480px] bg-[var(--sf-color-background)] shadow-2xl transition-transform duration-300 ease-in-out z-[1000] flex flex-col",
|
|
39
|
-
isOpen ? "translate-x-0" : "translate-x-full"
|
|
40
|
-
)}
|
|
41
|
-
>
|
|
42
|
-
{/* Header */}
|
|
43
|
-
<div className="flex items-center justify-between p-6 border-b border-gray-100">
|
|
44
|
-
<h2 className="text-xl font-bold text-[var(--sf-color-text)]">{title}</h2>
|
|
45
|
-
<button
|
|
46
|
-
onClick={onClose}
|
|
47
|
-
className="p-2 hover:bg-[color-mix(in_srgb,var(--sf-color-text-muted)_10%,transparent)] rounded-full transition-colors"
|
|
48
|
-
>
|
|
49
|
-
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
50
|
-
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
51
|
-
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
52
|
-
</svg>
|
|
53
|
-
</button>
|
|
54
|
-
</div>
|
|
55
|
-
|
|
56
|
-
{/* Content */}
|
|
57
|
-
<div className="flex-1 overflow-y-auto p-6">
|
|
58
|
-
{children}
|
|
59
|
-
</div>
|
|
60
|
-
</div>
|
|
61
|
-
</>
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export default SidePanel
|
|
1
|
+
/** @deprecated Import from `medusa-ui-product/products/components/side-panel` */
|
|
2
|
+
export { default } from "medusa-ui-product/products/components/side-panel"
|
|
@@ -1,31 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import Ideal from "medusa-ui-common/common/icons/ideal";
|
|
4
|
-
import Bancontact from "medusa-ui-common/common/icons/bancontact";
|
|
5
|
-
import PayPal from "medusa-ui-common/common/icons/paypal";
|
|
6
|
-
|
|
7
|
-
export const paymentInfoMap: Record<string, { title: string; icon: React.JSX.Element }> = {
|
|
8
|
-
pp_stripe_stripe: { title: "Credit card", icon: <CreditCard /> },
|
|
9
|
-
"pp_medusa-payments_default": { title: "Credit card", icon: <CreditCard /> },
|
|
10
|
-
"pp_stripe-ideal_stripe": { title: "iDeal", icon: <Ideal /> },
|
|
11
|
-
"pp_stripe-bancontact_stripe": { title: "Bancontact", icon: <Bancontact /> },
|
|
12
|
-
pp_paypal_paypal: { title: "PayPal", icon: <PayPal /> },
|
|
13
|
-
pp_system_default: { title: "Cash on Delivery (Cash/UPI)", icon: <CreditCard /> },
|
|
14
|
-
razorpay: { title: "Razorpay", icon: <CreditCard /> },
|
|
15
|
-
pp_razorpay_razorpay: { title: "Razorpay", icon: <CreditCard /> },
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export const isStripeLike = (providerId?: string) =>
|
|
19
|
-
providerId?.startsWith("pp_stripe_") || providerId?.startsWith("pp_medusa-");
|
|
20
|
-
|
|
21
|
-
export const isPaypal = (providerId?: string) => providerId?.startsWith("pp_paypal");
|
|
22
|
-
|
|
23
|
-
export const isManual = (providerId?: string) => providerId?.startsWith("pp_system_default");
|
|
24
|
-
|
|
25
|
-
export const isRazorpay = (providerId?: string) =>
|
|
26
|
-
providerId?.startsWith("razorpay") || providerId?.startsWith("pp_razorpay");
|
|
27
|
-
|
|
28
|
-
export const noDivisionCurrencies = [
|
|
29
|
-
"krw", "jpy", "vnd", "clp", "pyg", "xaf", "xof", "bif", "djf", "gnf", "kmf", "mga", "rwf",
|
|
30
|
-
"xpf", "htg", "vuv", "xag", "xdr", "xau",
|
|
31
|
-
];
|
|
1
|
+
/** @deprecated Import from `medusa-ui-checkout/checkout/constants/payments` */
|
|
2
|
+
export * from "medusa-ui-checkout/checkout/constants/payments"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
/** Props passed to every page section wrapper. */
|
|
4
|
+
export type SectionThemeProps<TTheme extends Record<string, string>> = {
|
|
5
|
+
theme?: Partial<TTheme>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Registry entry for composing a page from sections in the main app.
|
|
10
|
+
* @example
|
|
11
|
+
* const HOME_SECTIONS = {
|
|
12
|
+
* hero: { Component: HeroSection, load: loadHeroSectionData },
|
|
13
|
+
* }
|
|
14
|
+
*/
|
|
15
|
+
export type PageSectionEntry<TProps, TData = TProps> = {
|
|
16
|
+
Component: (props: TProps) => ReactNode
|
|
17
|
+
load?: (ctx: PageLoadContext) => Promise<TData | null>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type PageLoadContext = {
|
|
21
|
+
countryCode: string
|
|
22
|
+
pageInput?: Record<string, unknown>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type PageSectionRegistry = Record<string, PageSectionEntry<unknown>>
|
|
26
|
+
|
|
27
|
+
/** Ordered list of section ids for a page variant. */
|
|
28
|
+
export type PageVariantDefinition<TId extends string = string> = {
|
|
29
|
+
id: string
|
|
30
|
+
sectionIds: readonly TId[]
|
|
31
|
+
}
|
|
@@ -74,23 +74,16 @@ export type StorefrontUiProvidersProps = {
|
|
|
74
74
|
themeClasses?: StorefrontThemeClasses;
|
|
75
75
|
/** Form field classNames — from defineSiteTheme().siteFormTheme */
|
|
76
76
|
formTheme?: FormThemeClassNames;
|
|
77
|
-
/**
|
|
78
|
-
* @deprecated Prefer siteTheme — button CSS vars are derived from siteTheme.colors.
|
|
79
|
-
*/
|
|
80
|
-
buttonCssVars?: Record<string, string>;
|
|
81
77
|
};
|
|
82
78
|
|
|
83
79
|
function ButtonThemeFromSite({
|
|
84
80
|
children,
|
|
85
81
|
buttonTheme,
|
|
86
|
-
legacyButtonCssVars,
|
|
87
82
|
}: {
|
|
88
83
|
children: ReactNode;
|
|
89
84
|
buttonTheme: ButtonTheme;
|
|
90
|
-
legacyButtonCssVars?: Record<string, string>;
|
|
91
85
|
}) {
|
|
92
|
-
const { buttonCssVars:
|
|
93
|
-
const cssVars = legacyButtonCssVars ?? fromSite;
|
|
86
|
+
const { buttonCssVars: cssVars } = useSiteTheme();
|
|
94
87
|
return (
|
|
95
88
|
<ButtonThemeProvider theme={buttonTheme} cssVars={cssVars}>
|
|
96
89
|
{children}
|
|
@@ -105,12 +98,11 @@ export function StorefrontUiProviders({
|
|
|
105
98
|
buttonTheme = defaultButtonTheme,
|
|
106
99
|
themeClasses,
|
|
107
100
|
formTheme,
|
|
108
|
-
buttonCssVars,
|
|
109
101
|
}: StorefrontUiProvidersProps) {
|
|
110
102
|
return (
|
|
111
103
|
<SiteThemeProvider theme={siteTheme}>
|
|
112
104
|
<StorefrontThemeClassesProvider themeClasses={themeClasses} form={formTheme}>
|
|
113
|
-
<ButtonThemeFromSite buttonTheme={buttonTheme}
|
|
105
|
+
<ButtonThemeFromSite buttonTheme={buttonTheme}>
|
|
114
106
|
<WishlistProvider>{children}</WishlistProvider>
|
|
115
107
|
</ButtonThemeFromSite>
|
|
116
108
|
</StorefrontThemeClassesProvider>
|
package/src/util/checkout-dom.ts
CHANGED
|
@@ -1,65 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export function normalizeCheckoutEmail(email?: string | null): string {
|
|
4
|
-
return (email ?? "").trim().toLowerCase()
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function isValidCheckoutEmail(email?: string | null): boolean {
|
|
8
|
-
const normalized = normalizeCheckoutEmail(email)
|
|
9
|
-
return normalized.length > 0 && EMAIL_PATTERN.test(normalized)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function readCheckoutAddressFromDom(fallback?: {
|
|
13
|
-
email?: string | null
|
|
14
|
-
shipping_address?: Record<string, unknown> | null
|
|
15
|
-
}) {
|
|
16
|
-
const fullNameInput = document.getElementById("shipping_full_name_field") as HTMLInputElement | null
|
|
17
|
-
const address1Input = document.getElementById("shipping_address_1_field") as HTMLInputElement | null
|
|
18
|
-
const companyInput = document.getElementById("shipping_company_field") as HTMLInputElement | null
|
|
19
|
-
const cityInput = document.getElementById("shipping_city_field") as HTMLInputElement | null
|
|
20
|
-
const provinceInput = document.getElementById("shipping_province_field") as HTMLInputElement | null
|
|
21
|
-
const postalInput = document.getElementById("shipping_postal_code_field") as HTMLInputElement | null
|
|
22
|
-
const phoneInput = document.getElementById("shipping_phone_field_internal") as HTMLInputElement | null
|
|
23
|
-
const emailInput = document.getElementById("shipping_email_field") as HTMLInputElement | null
|
|
24
|
-
|
|
25
|
-
const fullName = fullNameInput?.value?.trim() || ""
|
|
26
|
-
const [firstName, ...lastNameParts] = fullName.split(/\s+/).filter(Boolean)
|
|
27
|
-
const lastName = lastNameParts.join(" ") || "."
|
|
28
|
-
|
|
29
|
-
const shipping = fallback?.shipping_address ?? {}
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
email: normalizeCheckoutEmail(emailInput?.value || (fallback?.email as string) || ""),
|
|
33
|
-
shipping_address: {
|
|
34
|
-
first_name: firstName || (shipping.first_name as string) || "Visitor",
|
|
35
|
-
last_name: lastName || (shipping.last_name as string) || "Customer",
|
|
36
|
-
address_1: address1Input?.value?.trim() || (shipping.address_1 as string) || "",
|
|
37
|
-
company: companyInput?.value?.trim() || (shipping.company as string) || "",
|
|
38
|
-
city: cityInput?.value?.trim() || (shipping.city as string) || "",
|
|
39
|
-
province: provinceInput?.value?.trim() || (shipping.province as string) || "",
|
|
40
|
-
postal_code: postalInput?.value?.trim() || (shipping.postal_code as string) || "",
|
|
41
|
-
phone: phoneInput?.value?.trim() || (shipping.phone as string) || "",
|
|
42
|
-
country_code: (shipping.country_code as string) || "in",
|
|
43
|
-
},
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function syncCheckoutAddressFromDom(fallback?: {
|
|
48
|
-
email?: string | null
|
|
49
|
-
shipping_address?: Record<string, unknown> | null
|
|
50
|
-
}) {
|
|
51
|
-
const payload = readCheckoutAddressFromDom(fallback)
|
|
52
|
-
|
|
53
|
-
if (!isValidCheckoutEmail(payload.email)) {
|
|
54
|
-
throw new Error("Please enter a valid email address before continuing.")
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const { updateAddressSilently } = await import("medusa-storefront-data/cart")
|
|
58
|
-
const result = await updateAddressSilently(payload)
|
|
59
|
-
|
|
60
|
-
if (result && "success" in result && result.success === false) {
|
|
61
|
-
throw new Error("Could not save your delivery details. Please check your email and try again.")
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return payload
|
|
65
|
-
}
|
|
1
|
+
/** @deprecated Import from `medusa-ui-checkout/checkout/util/checkout-dom` */
|
|
2
|
+
export * from "medusa-ui-checkout/checkout/util/checkout-dom"
|
package/src/util/returns.ts
CHANGED
|
@@ -1,72 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export type ItemWithDeliveryStatus = HttpTypes.StoreOrderLineItem & {
|
|
5
|
-
returnable_quantity: number
|
|
6
|
-
delivered_quantity: number
|
|
7
|
-
return_requested_quantity: number
|
|
8
|
-
return_received_quantity: number
|
|
9
|
-
written_off_quantity: number
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const calculateReturnableQuantity = (
|
|
13
|
-
item: HttpTypes.StoreOrderLineItem
|
|
14
|
-
): number => {
|
|
15
|
-
// In a real implementation, these would come from the item.detail or similar
|
|
16
|
-
// For now, we'll assume we can calculate it from available data or default to quantity
|
|
17
|
-
// Adjust this based on your actual data structure for item details
|
|
18
|
-
|
|
19
|
-
// Note: The actual data structure depends on how Medusa returns order item details
|
|
20
|
-
// Typically it's in item.detail associated with fulfillments/returns
|
|
21
|
-
|
|
22
|
-
// For the purpose of this storefront implementation (assuming standard Medusa):
|
|
23
|
-
// We strictly follow the logic: delivered - requested - received - written_off
|
|
24
|
-
|
|
25
|
-
// Checking if properties exist on item (they might be on a 'detail' object or top level depending on version)
|
|
26
|
-
const anyItem = item as any
|
|
27
|
-
|
|
28
|
-
// Defaulting to item.quantity if details missing (safe fallback for initial dev)
|
|
29
|
-
// BUT per requirements, we must implement the logic.
|
|
30
|
-
|
|
31
|
-
// If the backend provides these fields on the item directly:
|
|
32
|
-
const delivered = anyItem.delivered_quantity ?? anyItem.quantity ?? 0 // Fallback to quantity if assumed delivered
|
|
33
|
-
const requested = anyItem.return_requested_quantity ?? 0
|
|
34
|
-
const received = anyItem.return_received_quantity ?? 0
|
|
35
|
-
const writtenOff = anyItem.written_off_quantity ?? 0
|
|
36
|
-
|
|
37
|
-
// If fulfillment_status is not fulfilled/shipped/partially_shipped, returnable might be 0
|
|
38
|
-
// But strict formula:
|
|
39
|
-
const returnable = Math.max(0, delivered - requested - received - writtenOff)
|
|
40
|
-
|
|
41
|
-
return returnable
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export const isItemReturnable = (item: HttpTypes.StoreOrderLineItem): boolean => {
|
|
45
|
-
return calculateReturnableQuantity(item) > 0
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export const hasReturnableItems = (order: HttpTypes.StoreOrder): boolean => {
|
|
49
|
-
if (!order || !order.items) return false
|
|
50
|
-
|
|
51
|
-
// Simple check: if order is canceled, no returns
|
|
52
|
-
if (order.status === "canceled") return false
|
|
53
|
-
|
|
54
|
-
return order.items.some((item) => isItemReturnable(item))
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export const enhanceItemsWithReturnStatus = (
|
|
58
|
-
items: HttpTypes.StoreOrderLineItem[]
|
|
59
|
-
): ItemWithDeliveryStatus[] => {
|
|
60
|
-
return items.map((item) => {
|
|
61
|
-
const anyItem = item as any
|
|
62
|
-
return {
|
|
63
|
-
...item,
|
|
64
|
-
// Ensure these properties exist
|
|
65
|
-
delivered_quantity: anyItem.delivered_quantity ?? item.quantity, // Optimistic default
|
|
66
|
-
return_requested_quantity: anyItem.return_requested_quantity ?? 0,
|
|
67
|
-
return_received_quantity: anyItem.return_received_quantity ?? 0,
|
|
68
|
-
written_off_quantity: anyItem.written_off_quantity ?? 0,
|
|
69
|
-
returnable_quantity: calculateReturnableQuantity(item),
|
|
70
|
-
}
|
|
71
|
-
})
|
|
72
|
-
}
|
|
1
|
+
/** @deprecated Account flows: `medusa-ui-account/account/util/returns`. Order checks: `medusa-ui-order/order/util/returns`. */
|
|
2
|
+
export * from "medusa-ui-account/account/util/returns"
|
|
3
|
+
export { hasReturnableItems } from "medusa-ui-order/order/util/returns"
|