@mohasinac/appkit 2.7.8 → 2.7.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/_internal/shared/config/index.d.ts +1 -1
  2. package/dist/_internal/shared/config/schema.d.ts +95 -1
  3. package/dist/client-entry.d.ts +1 -1
  4. package/dist/client.d.ts +3 -0
  5. package/dist/client.js +5 -0
  6. package/dist/configs/next.js +28 -76
  7. package/dist/features/account/components/UserOffersPanel.js +17 -5
  8. package/dist/features/auctions/components/AuctionDetailPageView.js +17 -1
  9. package/dist/features/auctions/components/PlaceBidFormClient.js +10 -5
  10. package/dist/features/categories/components/BundleBuyNowCta.js +13 -6
  11. package/dist/features/events/components/EventPollWidget.js +7 -1
  12. package/dist/features/events/components/SpinWheelView.js +12 -5
  13. package/dist/features/pre-orders/components/PreOrderActionsClient.js +10 -3
  14. package/dist/features/products/components/MakeOfferButton.js +16 -8
  15. package/dist/features/products/components/PrizeRevealModal.js +18 -5
  16. package/dist/features/products/components/ProductDetailPageView.js +23 -1
  17. package/dist/features/seller/components/SellerOffersPanel.js +17 -5
  18. package/dist/features/wishlist/components/WishlistToggleButton.js +26 -12
  19. package/dist/http/api-handler.js +7 -1
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.js +2 -0
  22. package/dist/server.d.ts +1 -0
  23. package/dist/server.js +2 -0
  24. package/dist/tailwind-utilities.css +1 -1
  25. package/dist/ui/components/BaseListingCard.js +8 -6
  26. package/dist/ui/components/Button.style.css +8 -5
  27. package/dist/ui/components/ListingLayout.style.css +19 -9
  28. package/dist/ui/components/LoginRequiredModal.d.ts +9 -0
  29. package/dist/ui/components/LoginRequiredModal.js +11 -0
  30. package/dist/ui/index.d.ts +2 -0
  31. package/dist/ui/index.js +1 -0
  32. package/dist/utils/auth-error.d.ts +8 -0
  33. package/dist/utils/auth-error.js +20 -0
  34. package/dist/utils/index.d.ts +1 -0
  35. package/dist/utils/index.js +1 -0
  36. package/package.json +1 -1
@@ -1,11 +1,13 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useState, useTransition } from "react";
4
- import { Button, Div, Input, Span, Text } from "../../../ui";
4
+ import { Button, Div, Input, LoginRequiredModal, Span, Text } from "../../../ui";
5
+ import { isAuthError } from "../../../utils/auth-error";
5
6
  import { formatCurrency } from "../../../utils/number.formatter";
6
7
  export function MakeOfferButton({ productId, listedPrice, currency, minOfferPercent = 70, onMakeOffer, className = "", }) {
7
8
  const [state, setState] = useState("idle");
8
9
  const [errorMsg, setErrorMsg] = useState("");
10
+ const [showLoginModal, setShowLoginModal] = useState(false);
9
11
  const [isPending, startTransition] = useTransition();
10
12
  const minOffer = Math.round(listedPrice * (minOfferPercent / 100));
11
13
  const defaultOffer = Math.round(listedPrice * 0.9);
@@ -41,13 +43,19 @@ export function MakeOfferButton({ productId, listedPrice, currency, minOfferPerc
41
43
  setState("success");
42
44
  }
43
45
  catch (err) {
44
- const msg = err instanceof Error ? err.message : String(err);
45
- if (msg.includes("active offer") || msg.includes("ACTIVE_OFFER")) {
46
- setState("pending");
46
+ if (isAuthError(err)) {
47
+ setState("idle");
48
+ setShowLoginModal(true);
47
49
  }
48
50
  else {
49
- setErrorMsg(msg || "Could not send offer. Please try again.");
50
- setState("error");
51
+ const msg = err instanceof Error ? err.message : String(err);
52
+ if (msg.includes("active offer") || msg.includes("ACTIVE_OFFER")) {
53
+ setState("pending");
54
+ }
55
+ else {
56
+ setErrorMsg(msg || "Could not send offer. Please try again.");
57
+ setState("error");
58
+ }
51
59
  }
52
60
  }
53
61
  });
@@ -64,5 +72,5 @@ export function MakeOfferButton({ productId, listedPrice, currency, minOfferPerc
64
72
  if (state === "error") {
65
73
  return (_jsxs(Div, { className: `rounded-xl border border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20 p-4 space-y-2 ${className}`, children: [_jsx(Text, { className: "text-xs text-red-600 dark:text-red-400 text-center", children: errorMsg }), _jsxs(Div, { className: "flex gap-2", children: [_jsx(Button, { variant: "ghost", size: "sm", className: "flex-1", onClick: handleCancel, children: "Cancel" }), _jsx(Button, { variant: "secondary", size: "sm", className: "flex-1", onClick: () => setState("confirm"), children: "Try Again" })] })] }));
66
74
  }
67
- return (_jsx(Button, { variant: "ghost", size: "md", className: `w-full border border-zinc-300 dark:border-zinc-600 ${className}`, onClick: handleOpenConfirm, children: "Make Offer" }));
75
+ return (_jsxs(_Fragment, { children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to make an offer. Please log in or create an account to continue." }), _jsx(Button, { variant: "ghost", size: "md", className: `w-full border border-zinc-300 dark:border-zinc-600 ${className}`, onClick: handleOpenConfirm, children: "Make Offer" })] }));
68
76
  }
@@ -15,7 +15,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
15
  * nor LetItRip admins can influence the outcome.
16
16
  */
17
17
  import { useCallback, useEffect, useRef, useState } from "react";
18
- import { Button, Div, Heading, Modal, Stack, Text } from "../../../ui";
18
+ import { Button, Div, Heading, LoginRequiredModal, Modal, Stack, Text } from "../../../ui";
19
+ import { isAuthError } from "../../../utils/auth-error";
19
20
  import { PrizeDrawCollage } from "./PrizeDrawCollage";
20
21
  const REVEAL_DURATION_MS = 3200; // total animation length
21
22
  const CYCLE_INTERVAL_FAST = 80;
@@ -26,6 +27,7 @@ export function PrizeRevealModal({ open, onClose, items, orderId, productId, onR
26
27
  const [winner, setWinner] = useState(initialPrizeWon);
27
28
  const [errorMessage, setErrorMessage] = useState("");
28
29
  const [effectiveRngUrl, setEffectiveRngUrl] = useState(rngSourceUrl);
30
+ const [showLoginModal, setShowLoginModal] = useState(false);
29
31
  const cycleTimerRef = useRef(null);
30
32
  const endTimerRef = useRef(null);
31
33
  // Clean up timers on close/unmount so we don't leak between sessions.
@@ -83,12 +85,23 @@ export function PrizeRevealModal({ open, onClose, items, orderId, productId, onR
83
85
  body: JSON.stringify({ orderId }),
84
86
  });
85
87
  const json = (await res.json());
86
- if (!res.ok)
88
+ if (!res.ok) {
89
+ if (res.status === 401 || res.status === 403) {
90
+ setPhase("idle");
91
+ setShowLoginModal(true);
92
+ return;
93
+ }
87
94
  throw new Error("Reveal request failed");
95
+ }
88
96
  response = json.data ?? {};
89
97
  }
90
98
  }
91
99
  catch (err) {
100
+ if (isAuthError(err)) {
101
+ setPhase("idle");
102
+ setShowLoginModal(true);
103
+ return;
104
+ }
92
105
  setPhase("error");
93
106
  setErrorMessage(err instanceof Error ? err.message : "Reveal request failed");
94
107
  return;
@@ -117,8 +130,8 @@ export function PrizeRevealModal({ open, onClose, items, orderId, productId, onR
117
130
  }, REVEAL_DURATION_MS);
118
131
  }, [onReveal, orderId, phase, productId, startAnimation]);
119
132
  const winnerImg = winner?.images?.[0];
120
- return (_jsx(Modal, { isOpen: open, onClose: onClose, title: "Prize Reveal", size: "lg", children: _jsxs(Stack, { gap: "md", children: [phase === "refunded" ? (_jsxs(Div, { className: "rounded border border-yellow-400/40 bg-yellow-50 px-4 py-3 text-yellow-900 dark:bg-yellow-900/30 dark:text-yellow-100", children: [_jsx(Heading, { level: 3, className: "mb-1", children: "Pool exhausted \u2014 you've been refunded" }), _jsx(Text, { className: "text-sm", children: "Every prize in this draw was already claimed by the time your entry rolled. Your order has been marked refunded automatically." })] })) : null, phase === "error" ? (_jsxs(Div, { className: "rounded border border-red-400/40 bg-red-50 px-4 py-3 text-red-900 dark:bg-red-900/30 dark:text-red-100", children: [_jsx(Text, { className: "text-sm font-semibold", children: "Something went wrong" }), _jsx(Text, { className: "text-sm", children: errorMessage })] })) : null, _jsx(PrizeDrawCollage, { items: items, hideWonState: true, highlightItemNumber: highlight }), phase === "idle" ? (_jsx(Button, { type: "button", variant: "primary", size: "lg", onClick: handleRevealClick, children: "\u2728 Reveal my prize" })) : null, phase === "revealing" ? (_jsxs(Div, { className: "rounded bg-[var(--appkit-color-surface-muted)] p-4 text-center", children: [_jsx(Text, { className: "text-lg font-semibold", children: "Rolling\u2026" }), _jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: "The winner was locked by the server before this animation started. Hang tight \u2014 we're just making it look pretty." })] })) : null, phase === "won" && winner ? (_jsxs(Div, { className: "rounded-lg border-2 border-[var(--appkit-color-primary)] bg-[var(--appkit-color-surface)] p-4 text-center", children: [_jsx(Text, { className: "text-xs uppercase tracking-wider text-[var(--appkit-color-text-muted)]", children: "You won" }), _jsxs(Heading, { level: 2, className: "my-2", children: ["#", winner.itemNumber, " \u2014 ", winner.title] }), winnerImg ? (
121
- /* eslint-disable-next-line @next/next/no-img-element */
122
- _jsx("img", { src: winnerImg, alt: winner.title, className: "mx-auto max-h-64 rounded" })) : null, winner.estimatedValue != null ? (_jsxs(Text, { className: "mt-2 text-sm text-[var(--appkit-color-text-muted)]", children: ["Estimated value: \u20B9", (winner.estimatedValue / 100).toLocaleString("en-IN")] })) : null] })) : null, _jsxs(Div, { className: "rounded border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-muted)] px-3 py-2 text-xs text-[var(--appkit-color-text-muted)]", children: [_jsx("strong", { className: "text-[var(--appkit-color-text)]", children: "Fairness guarantee:" }), " ", "Winners are picked by ", _jsx("code", { children: "crypto.randomInt" }), " running on LetItRip's server before the animation starts. The animation is theatrical \u2014 neither the store nor LetItRip staff can influence the outcome.", " ", effectiveRngUrl ? (_jsx("a", { href: effectiveRngUrl, target: "_blank", rel: "noreferrer noopener", className: "underline", children: "View RNG source code \u2192" })) : null] })] }) }));
133
+ return (_jsxs(Modal, { isOpen: open, onClose: onClose, title: "Prize Reveal", size: "lg", children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to reveal your prize. Please log in or create an account to continue." }), _jsxs(Stack, { gap: "md", children: [phase === "refunded" ? (_jsxs(Div, { className: "rounded border border-yellow-400/40 bg-yellow-50 px-4 py-3 text-yellow-900 dark:bg-yellow-900/30 dark:text-yellow-100", children: [_jsx(Heading, { level: 3, className: "mb-1", children: "Pool exhausted \u2014 you've been refunded" }), _jsx(Text, { className: "text-sm", children: "Every prize in this draw was already claimed by the time your entry rolled. Your order has been marked refunded automatically." })] })) : null, phase === "error" ? (_jsxs(Div, { className: "rounded border border-red-400/40 bg-red-50 px-4 py-3 text-red-900 dark:bg-red-900/30 dark:text-red-100", children: [_jsx(Text, { className: "text-sm font-semibold", children: "Something went wrong" }), _jsx(Text, { className: "text-sm", children: errorMessage })] })) : null, _jsx(PrizeDrawCollage, { items: items, hideWonState: true, highlightItemNumber: highlight }), phase === "idle" ? (_jsx(Button, { type: "button", variant: "primary", size: "lg", onClick: handleRevealClick, children: "\u2728 Reveal my prize" })) : null, phase === "revealing" ? (_jsxs(Div, { className: "rounded bg-[var(--appkit-color-surface-muted)] p-4 text-center", children: [_jsx(Text, { className: "text-lg font-semibold", children: "Rolling\u2026" }), _jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: "The winner was locked by the server before this animation started. Hang tight \u2014 we're just making it look pretty." })] })) : null, phase === "won" && winner ? (_jsxs(Div, { className: "rounded-lg border-2 border-[var(--appkit-color-primary)] bg-[var(--appkit-color-surface)] p-4 text-center", children: [_jsx(Text, { className: "text-xs uppercase tracking-wider text-[var(--appkit-color-text-muted)]", children: "You won" }), _jsxs(Heading, { level: 2, className: "my-2", children: ["#", winner.itemNumber, " \u2014 ", winner.title] }), winnerImg ? (
134
+ /* eslint-disable-next-line @next/next/no-img-element */
135
+ _jsx("img", { src: winnerImg, alt: winner.title, className: "mx-auto max-h-64 rounded" })) : null, winner.estimatedValue != null ? (_jsxs(Text, { className: "mt-2 text-sm text-[var(--appkit-color-text-muted)]", children: ["Estimated value: \u20B9", (winner.estimatedValue / 100).toLocaleString("en-IN")] })) : null] })) : null, _jsxs(Div, { className: "rounded border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-muted)] px-3 py-2 text-xs text-[var(--appkit-color-text-muted)]", children: [_jsx("strong", { className: "text-[var(--appkit-color-text)]", children: "Fairness guarantee:" }), " ", "Winners are picked by ", _jsx("code", { children: "crypto.randomInt" }), " running on LetItRip's server before the animation starts. The animation is theatrical \u2014 neither the store nor LetItRip staff can influence the outcome.", " ", effectiveRngUrl ? (_jsx("a", { href: effectiveRngUrl, target: "_blank", rel: "noreferrer noopener", className: "underline", children: "View RNG source code \u2192" })) : null] })] })] }));
123
136
  }
124
137
  export default PrizeRevealModal;
@@ -182,8 +182,30 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
182
182
  : Promise.resolve([]),
183
183
  ]);
184
184
  const reviews = reviewDocs.map(toReview);
185
+ const _now = new Date();
185
186
  const relatedItems = relatedDocs
186
- .filter((r) => r.id !== product.id)
187
+ .filter((r) => {
188
+ if (r.id === product.id)
189
+ return false;
190
+ const s = r.status;
191
+ if (s && ["sold", "out_of_stock", "archived", "discontinued", "draft"].includes(s))
192
+ return false;
193
+ if (r.isSold === true)
194
+ return false;
195
+ if (r.availableQuantity === 0)
196
+ return false;
197
+ if (r.listingType === "auction" && r.auctionEndDate) {
198
+ const end = r.auctionEndDate;
199
+ const endDate = typeof end.toDate === "function"
200
+ ? end.toDate()
201
+ : end instanceof Date ? end : new Date(String(end));
202
+ if (endDate <= _now)
203
+ return false;
204
+ }
205
+ if (r.listingType === "prize-draw" && r.prizeRevealStatus === "closed")
206
+ return false;
207
+ return true;
208
+ })
187
209
  .slice(0, 8)
188
210
  .map(toProductItem);
189
211
  const formattedPrice = price !== null ? formatCurrency(price, currency) : null;
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useCallback, useEffect, useState, useTransition } from "react";
4
- import { Alert, Badge, Button, Div, Heading, Input, Spinner, Text } from "../../../ui";
4
+ import { Alert, Badge, Button, Div, Heading, Input, LoginRequiredModal, Spinner, Text } from "../../../ui";
5
+ import { isAuthError } from "../../../utils/auth-error";
5
6
  function formatRupees(amount) {
6
7
  if (amount === undefined || amount === null)
7
8
  return "—";
@@ -46,7 +47,7 @@ function statusVariant(status) {
46
47
  default: return "default";
47
48
  }
48
49
  }
49
- function OfferCard({ offer, onRespond, onUpdate }) {
50
+ function OfferCard({ offer, onRespond, onUpdate, onNeedsLogin }) {
50
51
  const [uiState, setUiState] = useState("idle");
51
52
  const [counterInput, setCounterInput] = useState("");
52
53
  const [sellerNote, setSellerNote] = useState("");
@@ -68,7 +69,12 @@ function OfferCard({ offer, onRespond, onUpdate }) {
68
69
  setUiState("idle");
69
70
  }
70
71
  catch (err) {
71
- setError(err instanceof Error ? err.message : "Something went wrong.");
72
+ if (isAuthError(err)) {
73
+ onNeedsLogin();
74
+ }
75
+ else {
76
+ setError(err instanceof Error ? err.message : "Something went wrong.");
77
+ }
72
78
  }
73
79
  });
74
80
  }
@@ -100,6 +106,7 @@ export function SellerOffersPanel({ fetchEndpoint = "/api/store/offers", onRespo
100
106
  const [offers, setOffers] = useState([]);
101
107
  const [loading, setLoading] = useState(true);
102
108
  const [fetchError, setFetchError] = useState("");
109
+ const [showLoginModal, setShowLoginModal] = useState(false);
103
110
  const [statusFilter, setStatusFilter] = useState("all");
104
111
  const loadOffers = useCallback(async () => {
105
112
  setLoading(true);
@@ -109,8 +116,13 @@ export function SellerOffersPanel({ fetchEndpoint = "/api/store/offers", onRespo
109
116
  ? fetchEndpoint
110
117
  : `${fetchEndpoint}?status=${statusFilter}`;
111
118
  const res = await fetch(url);
112
- if (!res.ok)
119
+ if (!res.ok) {
120
+ if (res.status === 401 || res.status === 403) {
121
+ setShowLoginModal(true);
122
+ return;
123
+ }
113
124
  throw new Error(`Error ${res.status}`);
125
+ }
114
126
  const json = (await res.json());
115
127
  setOffers(json.items ?? json.offers ?? []);
116
128
  }
@@ -134,5 +146,5 @@ export function SellerOffersPanel({ fetchEndpoint = "/api/store/offers", onRespo
134
146
  { value: "expired", label: "Expired" },
135
147
  ];
136
148
  const pending = offers.filter((o) => o.status === "pending").length;
137
- return (_jsxs(Div, { className: `space-y-4 ${className}`, children: [_jsxs(Div, { className: "flex items-center justify-between flex-wrap gap-2", children: [_jsxs(Div, { children: [_jsx(Heading, { level: 2, className: "text-lg font-semibold text-zinc-900 dark:text-zinc-100", children: "Offers Received" }), pending > 0 && (_jsxs(Text, { className: "text-xs text-amber-600 dark:text-amber-400 mt-0.5", children: [pending, " pending offer", pending > 1 ? "s" : "", " awaiting your response"] }))] }), _jsx(Button, { size: "sm", variant: "ghost", onClick: loadOffers, disabled: loading, className: "border border-zinc-300 dark:border-zinc-600 text-xs", children: loading ? "Refreshing…" : "Refresh" })] }), _jsx(Div, { className: "flex gap-1 flex-wrap", children: STATUS_FILTERS.map((f) => (_jsx(Button, { size: "sm", variant: statusFilter === f.value ? "primary" : "ghost", onClick: () => setStatusFilter(f.value), className: "text-xs px-2 py-1", children: f.label }, f.value))) }), fetchError && _jsx(Alert, { variant: "error", children: _jsx(Text, { className: "text-sm", children: fetchError }) }), loading && (_jsx(Div, { className: "flex justify-center py-12", children: _jsx(Spinner, { size: "lg" }) })), !loading && offers.length === 0 && (_jsx(Div, { className: "text-center py-12", children: _jsx(Text, { className: "text-zinc-400 dark:text-zinc-500 text-sm", children: "No offers found" }) })), !loading && offers.length > 0 && (_jsx(Div, { className: "space-y-3", children: offers.map((offer) => (_jsx(OfferCard, { offer: offer, onRespond: onRespond, onUpdate: handleUpdate }, offer.id))) }))] }));
149
+ return (_jsxs(Div, { className: `space-y-4 ${className}`, children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to manage offers. Please log in or create an account to continue." }), _jsxs(Div, { className: "flex items-center justify-between flex-wrap gap-2", children: [_jsxs(Div, { children: [_jsx(Heading, { level: 2, className: "text-lg font-semibold text-zinc-900 dark:text-zinc-100", children: "Offers Received" }), pending > 0 && (_jsxs(Text, { className: "text-xs text-amber-600 dark:text-amber-400 mt-0.5", children: [pending, " pending offer", pending > 1 ? "s" : "", " awaiting your response"] }))] }), _jsx(Button, { size: "sm", variant: "ghost", onClick: loadOffers, disabled: loading, className: "border border-zinc-300 dark:border-zinc-600 text-xs", children: loading ? "Refreshing…" : "Refresh" })] }), _jsx(Div, { className: "flex gap-1 flex-wrap", children: STATUS_FILTERS.map((f) => (_jsx(Button, { size: "sm", variant: statusFilter === f.value ? "primary" : "ghost", onClick: () => setStatusFilter(f.value), className: "text-xs px-2 py-1", children: f.label }, f.value))) }), fetchError && _jsx(Alert, { variant: "error", children: _jsx(Text, { className: "text-sm", children: fetchError }) }), loading && (_jsx(Div, { className: "flex justify-center py-12", children: _jsx(Spinner, { size: "lg" }) })), !loading && offers.length === 0 && (_jsx(Div, { className: "text-center py-12", children: _jsx(Text, { className: "text-zinc-400 dark:text-zinc-500 text-sm", children: "No offers found" }) })), !loading && offers.length > 0 && (_jsx(Div, { className: "space-y-3", children: offers.map((offer) => (_jsx(OfferCard, { offer: offer, onRespond: onRespond, onUpdate: handleUpdate, onNeedsLogin: () => setShowLoginModal(true) }, offer.id))) }))] }));
138
150
  }
@@ -1,5 +1,8 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Button } from "../../../ui";
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { Button, LoginRequiredModal } from "../../../ui";
5
+ import { isAuthError } from "../../../utils/auth-error";
3
6
  const sizeClasses = {
4
7
  sm: "w-7 h-7",
5
8
  md: "w-9 h-9",
@@ -7,14 +10,25 @@ const sizeClasses = {
7
10
  };
8
11
  export function WishlistToggleButton({ inWishlist, isLoading = false, onToggle, addLabel, removeLabel, className = "", size = "md", }) {
9
12
  const label = inWishlist ? removeLabel : addLabel;
10
- return (_jsx(Button, { type: "button", onClick: onToggle, disabled: isLoading, "aria-label": label, title: label, className: `
11
- flex items-center justify-center rounded-full
12
- transition-all duration-150
13
- ${inWishlist
14
- ? "bg-rose-50 text-rose-500 hover:bg-rose-100"
15
- : "bg-white/80 text-zinc-400 hover:text-rose-400"}
16
- ${sizeClasses[size]}
17
- ${isLoading ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}
18
- ${className}
19
- `, children: _jsx("svg", { className: "w-4/6 h-4/6", viewBox: "0 0 24 24", fill: inWishlist ? "currentColor" : "none", stroke: "currentColor", strokeWidth: 2, children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" }) }) }));
13
+ const [showLoginModal, setShowLoginModal] = useState(false);
14
+ async function handleClick(e) {
15
+ try {
16
+ await onToggle(e);
17
+ }
18
+ catch (err) {
19
+ if (isAuthError(err)) {
20
+ setShowLoginModal(true);
21
+ }
22
+ }
23
+ }
24
+ return (_jsxs(_Fragment, { children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to save items to your wishlist. Please log in or create an account." }), _jsx(Button, { type: "button", onClick: handleClick, disabled: isLoading, "aria-label": label, title: label, className: `
25
+ flex items-center justify-center rounded-full
26
+ transition-all duration-150
27
+ ${inWishlist
28
+ ? "bg-rose-50 text-rose-500 hover:bg-rose-100"
29
+ : "bg-white/80 text-zinc-400 hover:text-rose-400"}
30
+ ${sizeClasses[size]}
31
+ ${isLoading ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}
32
+ ${className}
33
+ `, children: _jsx("svg", { className: "w-4/6 h-4/6", viewBox: "0 0 24 24", fill: inWishlist ? "currentColor" : "none", stroke: "currentColor", strokeWidth: 2, children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" }) }) })] }));
20
34
  }
@@ -34,7 +34,13 @@ async function getUserFromRequest(request) {
34
34
  if (!sessionCookie)
35
35
  return null;
36
36
  const providers = getProviders();
37
- const decoded = await providers.session.verifySession(sessionCookie);
37
+ let decoded;
38
+ try {
39
+ decoded = await providers.session.verifySession(sessionCookie);
40
+ }
41
+ catch {
42
+ throw new AuthenticationError(ERROR_MESSAGES.USER.NOT_AUTHENTICATED);
43
+ }
38
44
  const db = providers.db;
39
45
  if (!db) {
40
46
  throw new Error("Database provider is not registered");
package/dist/index.d.ts CHANGED
@@ -2931,6 +2931,9 @@ export { SlottedListingView, DetailViewShell, StackedViewShell } from "./ui/inde
2931
2931
  export { buildColumns, createColumnBuilder } from "./ui/index";
2932
2932
  export { renderBoolean, renderCurrency, renderCurrencyCompact, renderCount, renderNullable } from "./ui/index";
2933
2933
  export { Ol } from "./ui/index";
2934
+ export { LoginRequiredModal } from "./ui/components/LoginRequiredModal";
2935
+ export type { LoginRequiredModalProps } from "./ui/components/LoginRequiredModal";
2936
+ export { isAuthError } from "./utils/auth-error";
2934
2937
  export { API_ENDPOINTS, API_ROUTES, LOGS_ENDPOINTS, AUTH_ENDPOINTS, ACCOUNT_ENDPOINTS, NOTIFICATIONS_ENDPOINTS, ADMIN_ENDPOINTS, CHAT_ENDPOINTS, AUCTION_ENDPOINTS, BID_ENDPOINTS, CART_ENDPOINTS, CATEGORY_ENDPOINTS, CHECKOUT_ENDPOINTS, PAYMENT_ENDPOINTS, COPILOT_ENDPOINTS, CORPORATE_ENDPOINTS, EVENT_ENDPOINTS, FAQ_ENDPOINTS, HOMEPAGE_ENDPOINTS, LOYALTY_ENDPOINTS, MEDIA_ENDPOINTS, ORDER_ENDPOINTS, PREORDER_ENDPOINTS, PRODUCT_ENDPOINTS, REVIEW_ENDPOINTS, SEARCH_ENDPOINTS, SELLER_ENDPOINTS, BLOG_ENDPOINTS, WISHLIST_ENDPOINTS, DEMO_ENDPOINTS, COLLECTION_CACHE_PATHS, resolveEndpoint, resolveEndpointFn, ROUTES, PUBLIC_ROUTES, PROTECTED_ROUTES, AUTH_ROUTES, WISHLIST_MAX, HISTORY_MAX, CART_MAX_ITEMS, WISHLIST_DOC_ID, HISTORY_DOC_ID, WISHLIST_COLLECTION, HISTORY_COLLECTION, } from "./constants/index";
2935
2938
  export { WishlistFullError } from "./features/wishlist/server";
2936
2939
  export { useHistory, useHistoryMergeOnLogin, getGuestHistory, trackGuestHistory, removeGuestHistoryItem, clearGuestHistory, getGuestHistoryCount, type GuestHistoryItem, type GuestHistoryType, type UserHistoryItem, type HistoryProductType, type HistoryItemSnapshot, type TrackArgs as TrackHistoryArgs, } from "./features/history";
package/dist/index.js CHANGED
@@ -5362,6 +5362,8 @@ export { SlottedListingView, DetailViewShell, StackedViewShell } from "./ui/inde
5362
5362
  export { buildColumns, createColumnBuilder } from "./ui/index";
5363
5363
  export { renderBoolean, renderCurrency, renderCurrencyCompact, renderCount, renderNullable } from "./ui/index";
5364
5364
  export { Ol } from "./ui/index";
5365
+ export { LoginRequiredModal } from "./ui/components/LoginRequiredModal";
5366
+ export { isAuthError } from "./utils/auth-error";
5365
5367
  // -- Missing constants ----------------------------------------------------------
5366
5368
  export { API_ENDPOINTS, API_ROUTES, LOGS_ENDPOINTS, AUTH_ENDPOINTS, ACCOUNT_ENDPOINTS, NOTIFICATIONS_ENDPOINTS, ADMIN_ENDPOINTS, CHAT_ENDPOINTS, AUCTION_ENDPOINTS, BID_ENDPOINTS, CART_ENDPOINTS, CATEGORY_ENDPOINTS, CHECKOUT_ENDPOINTS, PAYMENT_ENDPOINTS, COPILOT_ENDPOINTS, CORPORATE_ENDPOINTS, EVENT_ENDPOINTS, FAQ_ENDPOINTS, HOMEPAGE_ENDPOINTS, LOYALTY_ENDPOINTS, MEDIA_ENDPOINTS, ORDER_ENDPOINTS, PREORDER_ENDPOINTS, PRODUCT_ENDPOINTS, REVIEW_ENDPOINTS, SEARCH_ENDPOINTS, SELLER_ENDPOINTS, BLOG_ENDPOINTS, WISHLIST_ENDPOINTS, DEMO_ENDPOINTS, COLLECTION_CACHE_PATHS, resolveEndpoint, resolveEndpointFn, ROUTES, PUBLIC_ROUTES, PROTECTED_ROUTES, AUTH_ROUTES, WISHLIST_MAX, HISTORY_MAX, CART_MAX_ITEMS, WISHLIST_DOC_ID, HISTORY_DOC_ID, WISHLIST_COLLECTION, HISTORY_COLLECTION, } from "./constants/index";
5367
5369
  // Wishlist server errors (server-only via tree-shake — class only, no firebase-admin at module scope)
package/dist/server.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { isAuthError } from "./utils/auth-error";
1
2
  export { ADDRESS_FIXTURES } from "./seed/index";
2
3
  export { BID_FIXTURES } from "./seed/index";
3
4
  export { CART_FIXTURES } from "./seed/index";
package/dist/server.js CHANGED
@@ -1,4 +1,6 @@
1
1
  // Server-only public exports
2
+ // isAuthError - Pure util — detects auth errors from caught exceptions (usable in server actions).
3
+ export { isAuthError } from "./utils/auth-error";
2
4
  // [SERVER-ONLY]-Server-only — uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
3
5
  // ADDRESS_FIXTURES - Constant used across modules.
4
6
  export { ADDRESS_FIXTURES } from "./seed/index";