@mohasinac/appkit 3.1.7 → 3.2.1

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 (54) hide show
  1. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +39 -9
  2. package/dist/_internal/client/features/tour/TourProvider.d.ts +12 -0
  3. package/dist/_internal/client/features/tour/TourProvider.js +19 -0
  4. package/dist/_internal/server/features/checkout/actions.js +8 -2
  5. package/dist/_internal/server/features/media/contextGuards.js +2 -2
  6. package/dist/_internal/server/features/orders/actions.d.ts +6 -0
  7. package/dist/_internal/server/features/orders/actions.js +43 -1
  8. package/dist/_internal/server/features/products/data.js +5 -1
  9. package/dist/_internal/server/jobs/core/assignSpinPrize.js +4 -0
  10. package/dist/_internal/server/jobs/core/auctionSettlement.js +4 -0
  11. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +4 -0
  12. package/dist/_internal/server/jobs/core/bundleStockSync.js +4 -0
  13. package/dist/_internal/server/jobs/core/payoutBatch.js +4 -0
  14. package/dist/_internal/server/jobs/core/prizeRevealClose.js +4 -0
  15. package/dist/_internal/server/jobs/core/prizeRevealExpiry.js +4 -0
  16. package/dist/_internal/server/jobs/core/prizeRevealOpen.js +4 -0
  17. package/dist/_internal/server/jobs/core/prizeRevealReminder.js +4 -0
  18. package/dist/_internal/server/jobs/core/promotions.js +4 -0
  19. package/dist/_internal/server/jobs/core/triggerEventRaffle.js +4 -0
  20. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +4 -0
  21. package/dist/_internal/shared/actions/action-registry.js +13 -0
  22. package/dist/_internal/shared/features/checkout/config.d.ts +1 -1
  23. package/dist/_internal/shared/features/checkout/config.js +1 -1
  24. package/dist/features/admin/components/AdminOrderEditorView.d.ts +5 -1
  25. package/dist/features/admin/components/AdminOrderEditorView.js +30 -4
  26. package/dist/features/admin/components/AdminOrdersView.js +5 -1
  27. package/dist/features/layout/AppLayoutShell.d.ts +3 -1
  28. package/dist/features/layout/AppLayoutShell.js +2 -2
  29. package/dist/features/layout/TitleBarLayout.d.ts +3 -1
  30. package/dist/features/layout/TitleBarLayout.js +3 -2
  31. package/dist/features/orders/schemas/firestore.d.ts +9 -0
  32. package/dist/features/orders/schemas/firestore.js +1 -0
  33. package/dist/features/products/components/ProductForm.js +5 -3
  34. package/dist/features/products/components/ProductGrid.js +2 -2
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.js +1 -1
  37. package/dist/next/routing/route-map.d.ts +2 -0
  38. package/dist/next/routing/route-map.js +1 -0
  39. package/dist/seed/categories-seed-data.d.ts +1 -0
  40. package/dist/seed/categories-seed-data.js +6 -0
  41. package/dist/seed/index.d.ts +1 -1
  42. package/dist/seed/index.js +1 -1
  43. package/dist/seed/orders-seed-data.js +63 -9
  44. package/dist/seed/products-standard-seed-data.js +15 -15
  45. package/dist/seed/site-settings-seed-data.js +15 -5
  46. package/dist/server.d.ts +1 -0
  47. package/dist/server.js +2 -0
  48. package/dist/styles.css +9 -1
  49. package/dist/tailwind-utilities.css +1 -1
  50. package/dist/ui/components/PaginatedSelect.js +10 -1
  51. package/dist/ui/components/PaginatedSelect.style.css +8 -0
  52. package/dist/utils/id-generators.d.ts +6 -0
  53. package/dist/utils/id-generators.js +9 -0
  54. package/package.json +1 -1
@@ -34,36 +34,65 @@ import { Div } from "../../../../ui";
34
34
  * Hoisted drawer-state hook — the matchMedia-aware open/close logic that was
35
35
  * triplicated across admin/store/user layouts. Used internally by
36
36
  * DashboardLayoutClient; not exported because it's not generically useful.
37
+ *
38
+ * storageKey: variant-scoped localStorage key (`appkit:sidebar-open:{variant}`)
39
+ * so admin/store/user each persist their own collapse state independently.
37
40
  */
38
- function useResponsiveDrawer() {
41
+ function useResponsiveDrawer(storageKey) {
39
42
  const [desktopOpen, setDesktopOpen] = useState(false);
40
43
  const [mobileOpen, setMobileOpen] = useState(false);
41
44
  const { registerNav, unregisterNav } = useDashboardNav();
42
45
  const isDesktop = useCallback(() => typeof window !== "undefined" && window.matchMedia(DASHBOARD_DESKTOP_MEDIA_QUERY).matches, []);
46
+ // Restore persisted desktop-open state after hydration (avoids SSR mismatch).
47
+ useEffect(() => {
48
+ try {
49
+ if (localStorage.getItem(storageKey) === "true")
50
+ setDesktopOpen(true);
51
+ }
52
+ catch { /* localStorage unavailable (private-browse / SSR fallback) */ }
53
+ }, [storageKey]);
43
54
  const open = useCallback(() => {
44
55
  startTransition(() => {
45
- if (isDesktop())
56
+ if (isDesktop()) {
57
+ try {
58
+ localStorage.setItem(storageKey, "true");
59
+ }
60
+ catch { /* noop */ }
46
61
  setDesktopOpen(true);
62
+ }
47
63
  else
48
64
  setMobileOpen(true);
49
65
  });
50
- }, [isDesktop]);
66
+ }, [isDesktop, storageKey]);
51
67
  const close = useCallback(() => {
52
68
  startTransition(() => {
53
- if (isDesktop())
69
+ if (isDesktop()) {
70
+ try {
71
+ localStorage.setItem(storageKey, "false");
72
+ }
73
+ catch { /* noop */ }
54
74
  setDesktopOpen(false);
75
+ }
55
76
  else
56
77
  setMobileOpen(false);
57
78
  });
58
- }, [isDesktop]);
79
+ }, [isDesktop, storageKey]);
59
80
  const toggle = useCallback(() => {
60
81
  startTransition(() => {
61
- if (isDesktop())
62
- setDesktopOpen((prev) => !prev);
82
+ if (isDesktop()) {
83
+ setDesktopOpen((prev) => {
84
+ const next = !prev;
85
+ try {
86
+ localStorage.setItem(storageKey, String(next));
87
+ }
88
+ catch { /* noop */ }
89
+ return next;
90
+ });
91
+ }
63
92
  else
64
93
  setMobileOpen((prev) => !prev);
65
94
  });
66
- }, [isDesktop]);
95
+ }, [isDesktop, storageKey]);
67
96
  useEffect(() => {
68
97
  registerNav({ open, close, toggle });
69
98
  return () => unregisterNav();
@@ -87,7 +116,8 @@ const DEFAULT_CONTENT_MAX_WIDTH = "max-w-screen-2xl";
87
116
  export function DashboardLayoutClient({ variant, groups, permissions, activeHref: explicitActiveHref, responsive: _responsive, className, contentPadding, contentSurface, contentMaxWidth, children, }) {
88
117
  const pathname = usePathname();
89
118
  const activeHref = explicitActiveHref ?? pathname ?? "";
90
- const { desktopOpen, mobileOpen, close, toggle } = useResponsiveDrawer();
119
+ const storageKey = `appkit:sidebar-open:${variant}`;
120
+ const { desktopOpen, mobileOpen, close, toggle } = useResponsiveDrawer(storageKey);
91
121
  const { data: settings } = useSiteSettings();
92
122
  const navConfig = settings?.navConfig;
93
123
  const filteredGroups = filterGroups(groups, navConfig, permissions);
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+ export interface TourContextValue {
3
+ startTour: () => void;
4
+ }
5
+ export declare function useTour(): TourContextValue;
6
+ /**
7
+ * TourProvider — skeleton shell for Patch 1.
8
+ * driver.js steps are wired in Patch 2+; this provider is an identity wrapper.
9
+ */
10
+ export declare function TourProvider({ children }: {
11
+ children: React.ReactNode;
12
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import React from "react";
4
+ const TourContext = React.createContext({
5
+ startTour: () => { },
6
+ });
7
+ export function useTour() {
8
+ return React.useContext(TourContext);
9
+ }
10
+ /**
11
+ * TourProvider — skeleton shell for Patch 1.
12
+ * driver.js steps are wired in Patch 2+; this provider is an identity wrapper.
13
+ */
14
+ export function TourProvider({ children }) {
15
+ const startTour = React.useCallback(() => {
16
+ // driver.js will be imported and configured here in Patch 2+.
17
+ }, []);
18
+ return (_jsx(TourContext.Provider, { value: { startTour }, children: children }));
19
+ }
@@ -232,12 +232,18 @@ export async function createCheckoutOrderAction(input) {
232
232
  const { userId: uid, userName, userEmail, addressId, paymentMethod, notes, excludedProductIds = [], adminBypass = false, adminBypassBy, } = input;
233
233
  const siteSettings = await siteSettingsRepository.getSingleton();
234
234
  const commissions = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
235
- // W1-43 — enforce COD toggle. Admin bypass + non-COD payments still flow through.
235
+ // W1-43 — enforce COD toggle.
236
236
  if (!adminBypass &&
237
237
  paymentMethod === "cod" &&
238
238
  siteSettings?.payment?.codEnabled === false) {
239
239
  throw new ValidationError("Cash on Delivery is not currently accepted. Please choose another payment method.");
240
240
  }
241
+ // P-1 — enforce UPI/cash manual payment toggle.
242
+ if (!adminBypass &&
243
+ (paymentMethod === "upi_manual" || paymentMethod === "cash") &&
244
+ siteSettings?.payment?.upiManualEnabled === false) {
245
+ throw new ValidationError("Manual UPI / cash payment is not currently accepted. Please choose another payment method.");
246
+ }
241
247
  const cart = await unitOfWork.carts.getOrCreate(uid);
242
248
  if (!cart.items || cart.items.length === 0) {
243
249
  throw new ValidationError(ERROR_MESSAGES.CHECKOUT.CART_EMPTY);
@@ -470,7 +476,7 @@ export async function createCheckoutOrderAction(input) {
470
476
  });
471
477
  const totalQuantity = group.reduce((sum, { item }) => sum + item.quantity, 0);
472
478
  const { shippingFee, storeOwnerId } = await resolveShippingCost(firstItem.storeId, groupTotal, commissions);
473
- const isCodLike = paymentMethod === "cod" || paymentMethod === "upi_manual";
479
+ const isCodLike = paymentMethod === "cod" || paymentMethod === "upi_manual" || paymentMethod === "cash";
474
480
  const depositAmount = isCodLike
475
481
  ? Math.round(groupTotal * (commissions.codDepositPercent / 100) * 100) / 100
476
482
  : undefined;
@@ -45,7 +45,7 @@ export const CONTEXT_LIMITS = {
45
45
  RICH_TEXT_IMAGE_MAX: 20,
46
46
  };
47
47
  // Contexts that accept both image and PDF (proof documents).
48
- const IMAGE_OR_PDF_CONTEXTS = ["shipping-proof", "refund-proof"];
48
+ const IMAGE_OR_PDF_CONTEXTS = ["shipping-proof", "refund-proof", "payment-proof"];
49
49
  const PDF_ONLY_CONTEXTS = ["invoice", "payout-doc"];
50
50
  const IMAGE_ONLY_CONTEXT_TYPES = new Set([
51
51
  "store-logo",
@@ -215,7 +215,7 @@ export function applyMediaContextGuards({ detectedMime, context: ctx, }) {
215
215
  return {
216
216
  ok: false,
217
217
  status: 400,
218
- error: "PDF uploads are only allowed for invoice, payout-doc, shipping-proof, or refund-proof contexts",
218
+ error: "PDF uploads are only allowed for invoice, payout-doc, shipping-proof, refund-proof, or payment-proof contexts",
219
219
  details: { context: ctx.type, detected: detectedMime },
220
220
  };
221
221
  }
@@ -3,3 +3,9 @@ export declare function createOrderAction(input: unknown): Promise<ActionResult<
3
3
  export declare function cancelOrderAction(input: unknown): Promise<ActionResult<unknown>>;
4
4
  export declare function requestReturnAction(input: unknown): Promise<ActionResult<unknown>>;
5
5
  export declare function updateOrderStatusAction(input: unknown): Promise<ActionResult<unknown>>;
6
+ export declare function attachPaymentProofAction(orderId: string, proof: {
7
+ proofUrl: string;
8
+ transactionId?: string;
9
+ mimeType?: string;
10
+ }): Promise<ActionResult<void>>;
11
+ export declare function adminVerifyPaymentAction(orderId: string): Promise<ActionResult<void>>;
@@ -6,7 +6,7 @@ import { createOrderSchema, updateOrderStatusSchema, cancelOrderSchema, } from "
6
6
  import { assertOrderCancellable, assertReturnWindowOpen } from "./service";
7
7
  import { ValidationError } from "../../../shared/errors/index";
8
8
  import { OrderNotFoundError, OrderOwnershipError } from "../../../shared/features/orders/errors";
9
- import { isAdminUser } from "../../../../features/auth/role-predicates";
9
+ import { isAdminUser, isModeratorUser } from "../../../../features/auth/role-predicates";
10
10
  export async function createOrderAction(input) {
11
11
  return wrapAction(async () => {
12
12
  const user = await requireRoleUser(["buyer", "seller", "admin"]);
@@ -58,3 +58,45 @@ export async function updateOrderStatusAction(input) {
58
58
  });
59
59
  });
60
60
  }
61
+ // ── P-1 Manual payment proof ─────────────────────────────────────────────────
62
+ export async function attachPaymentProofAction(orderId, proof) {
63
+ return wrapAction(async () => {
64
+ const user = await requireRoleUser(["buyer", "seller", "admin"]);
65
+ const order = await orderRepository.findById(orderId).catch(() => null);
66
+ if (!order)
67
+ throw new OrderNotFoundError(orderId);
68
+ if (!isAdminUser(user) && order.userId !== user.uid)
69
+ throw new OrderOwnershipError(orderId);
70
+ const pm = order.paymentMethod ?? "";
71
+ if (pm !== "cash" && pm !== "upi_manual") {
72
+ throw new ValidationError("Payment proof can only be attached to cash or UPI orders");
73
+ }
74
+ if (order.paymentProofUrl) {
75
+ throw new ValidationError("PROOF_ALREADY_ATTACHED");
76
+ }
77
+ await orderRepository.update(orderId, {
78
+ paymentProofUrl: proof.proofUrl,
79
+ paymentTransactionId: proof.transactionId,
80
+ paymentProofMimeType: proof.mimeType,
81
+ paymentProofUploadedAt: new Date(),
82
+ });
83
+ });
84
+ }
85
+ export async function adminVerifyPaymentAction(orderId) {
86
+ return wrapAction(async () => {
87
+ const user = await requireRoleUser(["admin", "moderator"]);
88
+ if (!isAdminUser(user) && !isModeratorUser(user)) {
89
+ throw new ValidationError("Only admin or moderator can verify payments");
90
+ }
91
+ const order = await orderRepository.findById(orderId).catch(() => null);
92
+ if (!order)
93
+ throw new OrderNotFoundError(orderId);
94
+ if (order.paymentStatus === "paid")
95
+ return; // idempotent
96
+ await orderRepository.update(orderId, {
97
+ paymentStatus: "paid",
98
+ paymentId: order.paymentTransactionId ?? order.paymentId ?? `manual-${orderId}`,
99
+ status: "processing",
100
+ });
101
+ });
102
+ }
@@ -11,7 +11,11 @@ import { PRODUCT_FIELDS } from "../../../../constants/field-names";
11
11
  // ---------------------------------------------------------------------------
12
12
  /** Fetch a single product by slug or id, deduped per request. */
13
13
  export const getProductForDetail = cache(async (slugOrId) => {
14
- return (await productRepository.findByIdOrSlug(slugOrId).catch(() => undefined)) ?? null;
14
+ const product = (await productRepository.findByIdOrSlug(slugOrId).catch(() => undefined)) ?? null;
15
+ // Fire-and-forget viewCount increment — must not affect response time
16
+ if (product)
17
+ void productRepository.incrementViewCount(product.id);
18
+ return product;
15
19
  });
16
20
  /** Fetch the first page of approved reviews for a product, deduped per request. */
17
21
  export const getReviewsForProduct = cache(async (productId) => {
@@ -8,6 +8,10 @@ const EVENTS_COLLECTION = "events";
8
8
  const EVENT_ENTRIES_COLLECTION = "eventEntries";
9
9
  const COUPONS_COLLECTION = "coupons";
10
10
  export async function runAssignSpinPrize(input, ctx) {
11
+ if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true") {
12
+ ctx.logger.info("FEATURE_PRIZE_DRAWS disabled — skipping spin prize assignment");
13
+ return { eventId: input.eventId, userId: input.userId, reason: "feature_disabled" };
14
+ }
11
15
  const { eventId, userId } = input;
12
16
  const eventSnap = await ctx.db.collection(EVENTS_COLLECTION).doc(eventId).get();
13
17
  if (!eventSnap.exists) {
@@ -57,6 +57,10 @@ async function settleAuction(ctx, product) {
57
57
  });
58
58
  }
59
59
  export async function runAuctionSettlement(ctx) {
60
+ if (ctx.env("FEATURE_AUCTIONS") !== "true") {
61
+ ctx.logger.info("FEATURE_AUCTIONS disabled — skipping auction settlement");
62
+ return;
63
+ }
60
64
  ctx.logger.info("Starting auction settlement sweep");
61
65
  const expired = await productRepository.getExpiredAuctions(ctx.now);
62
66
  if (expired.length === 0) {
@@ -13,6 +13,10 @@ function getBusinessDayCutoff(daysAgo) {
13
13
  return cutoff;
14
14
  }
15
15
  export async function runAutoPayoutEligibility(ctx) {
16
+ if (ctx.env("FEATURE_PAYOUTS") !== "true") {
17
+ ctx.logger.info("FEATURE_PAYOUTS disabled — skipping auto-payout eligibility");
18
+ return;
19
+ }
16
20
  ctx.logger.info("Starting daily auto-payout eligibility sweep", {
17
21
  windowDays: AUTO_PAYOUT_WINDOW_DAYS,
18
22
  });
@@ -36,6 +36,10 @@ async function computeBundleStockStatus(productIds, ctx) {
36
36
  return "in_stock";
37
37
  }
38
38
  export async function runBundleStockSync(ctx) {
39
+ if (ctx.env("FEATURE_BUNDLES") !== "true") {
40
+ ctx.logger.info("FEATURE_BUNDLES disabled — skipping bundle stock sync");
41
+ return;
42
+ }
39
43
  ctx.logger.info("Bundle stock sync starting (SB-UNI-V categories)");
40
44
  const snap = await ctx.db
41
45
  .collection(CATEGORIES_COLLECTION)
@@ -87,6 +87,10 @@ async function dispatch(ctx, entry) {
87
87
  }
88
88
  }
89
89
  export async function runPayoutBatch(ctx) {
90
+ if (ctx.env("FEATURE_PAYOUTS") !== "true") {
91
+ ctx.logger.info("FEATURE_PAYOUTS disabled — skipping payout batch");
92
+ return;
93
+ }
90
94
  ctx.logger.info("Starting payout batch sweep");
91
95
  const pending = await payoutRepository.getPending();
92
96
  if (pending.length === 0) {
@@ -2,6 +2,10 @@ import { PRODUCT_FIELDS, COMMON_FIELDS } from "../../../../constants/field-names
2
2
  const PRODUCT_COLLECTION = "products";
3
3
  const PRIZE_DRAW_LISTING_TYPE = "prize-draw";
4
4
  export async function runPrizeRevealClose(ctx) {
5
+ if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true") {
6
+ ctx.logger.info("FEATURE_PRIZE_DRAWS disabled — skipping prize reveal close");
7
+ return;
8
+ }
5
9
  ctx.logger.info("Prize reveal close sweep starting");
6
10
  const snap = await ctx.db
7
11
  .collection(PRODUCT_COLLECTION)
@@ -3,6 +3,10 @@ import { sendNotification } from "../../../../features/admin/actions/notificatio
3
3
  import { ORDER_FIELDS, PRODUCT_FIELDS, COMMON_FIELDS } from "../../../../constants/field-names";
4
4
  const ORDER_COLLECTION = "orders";
5
5
  export async function runPrizeRevealExpiry(ctx) {
6
+ if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true") {
7
+ ctx.logger.info("FEATURE_PRIZE_DRAWS disabled — skipping prize reveal expiry");
8
+ return;
9
+ }
6
10
  ctx.logger.info("Prize reveal expiry sweep starting");
7
11
  const snap = await ctx.db
8
12
  .collection(ORDER_COLLECTION)
@@ -5,6 +5,10 @@ const PRODUCT_COLLECTION = "products";
5
5
  const ORDER_COLLECTION = "orders";
6
6
  const PRIZE_DRAW_LISTING_TYPE = "prize-draw";
7
7
  export async function runPrizeRevealOpen(ctx) {
8
+ if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true") {
9
+ ctx.logger.info("FEATURE_PRIZE_DRAWS disabled — skipping prize reveal open");
10
+ return;
11
+ }
8
12
  ctx.logger.info("Prize reveal open sweep starting");
9
13
  const snap = await ctx.db
10
14
  .collection(PRODUCT_COLLECTION)
@@ -4,6 +4,10 @@ import { ORDER_FIELDS, PRODUCT_FIELDS } from "../../../../constants/field-names"
4
4
  const ORDER_COLLECTION = "orders";
5
5
  const ONE_DAY_MS = 86400000;
6
6
  export async function runPrizeRevealReminder(ctx) {
7
+ if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true") {
8
+ ctx.logger.info("FEATURE_PRIZE_DRAWS disabled — skipping prize reveal reminder");
9
+ return;
10
+ }
7
11
  ctx.logger.info("Prize reveal reminder sweep starting");
8
12
  const cutoff = new Date(ctx.now.getTime() + ONE_DAY_MS);
9
13
  const snap = await ctx.db
@@ -6,6 +6,10 @@ import { ProductStatusValues } from "../../../../features/products/schemas/fires
6
6
  * (coupon.startDate IS NULL OR <= now) filter Firestore can't express.
7
7
  */
8
8
  export async function runPromotions(_input, ctx) {
9
+ if (ctx.env("FEATURE_EVENTS") !== "true") {
10
+ ctx.logger.info("FEATURE_EVENTS disabled — skipping promotions");
11
+ return { promotedProducts: [], featuredProducts: [], activeCoupons: [] };
12
+ }
9
13
  ctx.logger.info("Promotions data requested");
10
14
  const now = new Date();
11
15
  const nowIso = now.toISOString();
@@ -7,6 +7,10 @@ import { EVENT_ENTRY_FIELDS } from "../../../../constants/field-names";
7
7
  const EVENTS_COLLECTION = "events";
8
8
  const EVENT_ENTRIES_COLLECTION = "eventEntries";
9
9
  export async function runTriggerEventRaffle(input, ctx) {
10
+ if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true") {
11
+ ctx.logger.info("FEATURE_PRIZE_DRAWS disabled — skipping event raffle");
12
+ return { eventId: input.eventId, raffleEntryCount: 0 };
13
+ }
10
14
  const eventRef = ctx.db.collection(EVENTS_COLLECTION).doc(input.eventId);
11
15
  const eventSnap = await eventRef.get();
12
16
  if (!eventSnap.exists) {
@@ -7,6 +7,10 @@ import { BATCH_LIMIT } from "../handlers/messages";
7
7
  import { getDefaultCurrency } from "../../../../core";
8
8
  const PLATFORM_COMMISSION_RATE = 0.05;
9
9
  export async function runWeeklyPayoutEligibility(ctx) {
10
+ if (ctx.env("FEATURE_PAYOUTS") !== "true") {
11
+ ctx.logger.info("FEATURE_PAYOUTS disabled — skipping weekly payout eligibility");
12
+ return;
13
+ }
10
14
  ctx.logger.info("Starting weekly payout eligibility sweep");
11
15
  const eligible = await orderRepository.getEligibleShiprocket();
12
16
  if (eligible.length === 0) {
@@ -835,6 +835,19 @@ export const ACTIONS = {
835
835
  kind: "secondary",
836
836
  permissions: ["admin"],
837
837
  },
838
+ "verify-payment": {
839
+ id: "admin.verify-payment",
840
+ label: "Verify Payment",
841
+ ariaLabel: "Mark payment as received and verified",
842
+ description: "Confirms manual cash/UPI payment was received. Moves order to Processing.",
843
+ kind: "primary",
844
+ permissions: ["admin", "moderator"],
845
+ confirmation: {
846
+ title: "Verify payment?",
847
+ body: "This marks the payment as received. The order will move to Processing. This action cannot be undone.",
848
+ confirmLabel: "Yes, verify",
849
+ },
850
+ },
838
851
  "verify-vendor": {
839
852
  id: "admin.verify-vendor",
840
853
  label: "Verify vendor",
@@ -4,5 +4,5 @@ export declare const CHECKOUT_DEFAULT_COMMISSIONS: {
4
4
  readonly platformShippingPercent: 10;
5
5
  readonly platformShippingFixedMin: 50;
6
6
  };
7
- export declare const CHECKOUT_PAYMENT_METHODS: readonly ["cod", "online", "upi_manual", "admin_bypass"];
7
+ export declare const CHECKOUT_PAYMENT_METHODS: readonly ["cash", "cod", "online", "upi_manual", "admin_bypass"];
8
8
  export type CheckoutPaymentMethod = (typeof CHECKOUT_PAYMENT_METHODS)[number];
@@ -4,4 +4,4 @@ export const CHECKOUT_DEFAULT_COMMISSIONS = {
4
4
  platformShippingPercent: 10,
5
5
  platformShippingFixedMin: 50,
6
6
  };
7
- export const CHECKOUT_PAYMENT_METHODS = ["cod", "online", "upi_manual", "admin_bypass"];
7
+ export const CHECKOUT_PAYMENT_METHODS = ["cash", "cod", "online", "upi_manual", "admin_bypass"];
@@ -4,5 +4,9 @@ export interface AdminOrderEditorViewProps {
4
4
  orderId?: string;
5
5
  orderLabel?: string;
6
6
  currentStatus?: string;
7
+ paymentProofUrl?: string;
8
+ paymentTransactionId?: string;
9
+ paymentMethod?: string;
10
+ paymentStatus?: string;
7
11
  }
8
- export declare function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, }: AdminOrderEditorViewProps): import("react/jsx-runtime").JSX.Element;
12
+ export declare function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, paymentProofUrl, paymentTransactionId, paymentMethod, paymentStatus, }: AdminOrderEditorViewProps): import("react/jsx-runtime").JSX.Element;
@@ -1,11 +1,14 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useApiMutation } from "@mohasinac/appkit/client";
4
- import React from "react";
4
+ import React, { useState } from "react";
5
+ import { normalizeError } from "../../../errors/normalize";
5
6
  import { useQueryClient } from "@tanstack/react-query";
6
- import { Button, Form, FormActions, Input, Label, Select, SideDrawer, Stack, useToast } from "../../../ui";
7
+ import { Button, Div, Form, FormActions, Input, Label, Select, SideDrawer, Stack, Text, useToast } from "../../../ui";
8
+ import { MediaImage } from "../../media";
7
9
  import { apiClient } from "../../../http";
8
10
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
11
+ import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
9
12
  const STATUS_OPTIONS = [
10
13
  { label: "Pending", value: "pending" },
11
14
  { label: "Processing", value: "processing" },
@@ -25,7 +28,7 @@ const CARRIER_OPTIONS = [
25
28
  { label: "Other", value: "Other" },
26
29
  ];
27
30
  // --- Component ---------------------------------------------------------------
28
- export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, }) {
31
+ export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, paymentProofUrl, paymentTransactionId, paymentMethod, paymentStatus, }) {
29
32
  const queryClient = useQueryClient();
30
33
  const { showToast } = useToast();
31
34
  const [status, setStatus] = React.useState(currentStatus ?? "pending");
@@ -33,6 +36,9 @@ export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, curre
33
36
  const [carrier, setCarrier] = React.useState("");
34
37
  const [notes, setNotes] = React.useState("");
35
38
  const [refundAmount, setRefundAmount] = React.useState("");
39
+ const [isVerifyingPayment, setIsVerifyingPayment] = useState(false);
40
+ const isCashOrUpi = paymentMethod === "cash" || paymentMethod === "upi_manual";
41
+ const needsVerification = isCashOrUpi && paymentStatus === "pending";
36
42
  React.useEffect(() => {
37
43
  if (open) {
38
44
  setStatus(currentStatus ?? "pending");
@@ -68,8 +74,28 @@ export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, curre
68
74
  showToast(err?.message ?? "Failed to update order.", "error");
69
75
  },
70
76
  });
77
+ const handleVerifyPayment = async () => {
78
+ if (!orderId)
79
+ return;
80
+ setIsVerifyingPayment(true);
81
+ try {
82
+ await apiClient.patch(`/api/admin/orders/${orderId}/payment-verify`, {});
83
+ showToast("Payment verified. Order moved to Processing.", "success");
84
+ queryClient.invalidateQueries({ queryKey: ["admin", "orders"] });
85
+ onClose();
86
+ }
87
+ catch (err) {
88
+ void normalizeError(err);
89
+ showToast(err?.message ?? "Failed to verify payment.", "error");
90
+ }
91
+ finally {
92
+ setIsVerifyingPayment(false);
93
+ }
94
+ };
71
95
  return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: orderLabel ? `Order: ${orderLabel}` : "Update Order", children: _jsxs(Form, { onSubmit: (e) => {
72
96
  e.preventDefault();
73
97
  saveMutation.mutate();
74
- }, spacing: "md", padding: "md", children: [_jsx(Select, { label: "Order status", options: STATUS_OPTIONS, value: status, onValueChange: setStatus }), _jsx(Input, { label: "Tracking number (optional)", value: trackingNumber, onChange: (e) => setTrackingNumber(e.target.value), placeholder: "e.g. DEL1234567890IN" }), _jsx(Select, { label: "Carrier (optional)", options: CARRIER_OPTIONS, value: carrier, onValueChange: setCarrier }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Internal note (optional)" }), _jsx("textarea", { value: notes, onChange: (e) => setNotes(e.target.value), rows: 3, placeholder: "Reason for status change, escalation notes\u2026", className: "w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary-500" })] }), (status === "refunded" || status === "return_requested") && (_jsx(Input, { label: "Refund amount \u20B9 (optional)", type: "number", min: "0", step: "0.01", value: refundAmount, onChange: (e) => setRefundAmount(e.target.value), placeholder: "e.g. 499.00" })), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "secondary", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", isLoading: saveMutation.isPending, disabled: !orderId || saveMutation.isPending, children: "Save changes" })] })] }) }));
98
+ }, spacing: "md", padding: "md", children: [_jsx(Select, { label: "Order status", options: STATUS_OPTIONS, value: status, onValueChange: setStatus }), _jsx(Input, { label: "Tracking number (optional)", value: trackingNumber, onChange: (e) => setTrackingNumber(e.target.value), placeholder: "e.g. DEL1234567890IN" }), _jsx(Select, { label: "Carrier (optional)", options: CARRIER_OPTIONS, value: carrier, onValueChange: setCarrier }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Internal note (optional)" }), _jsx("textarea", { value: notes, onChange: (e) => setNotes(e.target.value), rows: 3, placeholder: "Reason for status change, escalation notes\u2026", className: "w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary-500" })] }), (status === "refunded" || status === "return_requested") && (_jsx(Input, { label: "Refund amount \u20B9 (optional)", type: "number", min: "0", step: "0.01", value: refundAmount, onChange: (e) => setRefundAmount(e.target.value), placeholder: "e.g. 499.00" })), isCashOrUpi && (_jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Payment Proof" }), paymentProofUrl ? (_jsxs(Stack, { gap: "xs", children: [_jsx(MediaImage, { src: paymentProofUrl, alt: "Payment screenshot", size: "card", className: "rounded-lg border border-zinc-200 dark:border-zinc-700" }), paymentTransactionId && (_jsxs(Text, { size: "xs", color: "muted", children: ["UTR: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: paymentTransactionId })] })), needsVerification && (_jsx(Button, { type: "button", action: ACTIONS.ADMIN["verify-payment"], onClick: handleVerifyPayment, isLoading: isVerifyingPayment, disabled: isVerifyingPayment, variant: "primary", className: "mt-1 w-full" })), !needsVerification && paymentStatus === "paid" && (
99
+ // audit-variant-ok: themed success border color not in BORDER_MAP
100
+ _jsx(Div, { rounded: "lg", padding: "inlineSm", className: "border border-success/20", surface: "success-surface", children: _jsx(Text, { size: "xs", className: "text-success", weight: "medium", children: "Payment verified" }) }))] })) : (_jsx(Text, { size: "xs", color: "faint", children: "No proof uploaded yet." }))] })), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "secondary", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", isLoading: saveMutation.isPending, disabled: !orderId || saveMutation.isPending, children: "Save changes" })] })] }) }));
75
101
  }
@@ -51,6 +51,10 @@ export function AdminOrdersView({ children, ...props }) {
51
51
  ].join(" · "),
52
52
  status: toStringValue(item.status, "Unknown"),
53
53
  updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
54
+ paymentProofUrl: toStringValue(item.paymentProofUrl, "") || undefined,
55
+ paymentTransactionId: toStringValue(item.paymentTransactionId, "") || undefined,
56
+ paymentMethod: toStringValue(item.paymentMethod, "") || undefined,
57
+ paymentStatus: toStringValue(item.paymentStatus, "") || undefined,
54
58
  })),
55
59
  getTotal: (response, mappedRows) => typeof response.meta?.total === "number" ? response.meta.total : mappedRows.length,
56
60
  buildFilters: (f) => (f.status && f.status !== "All" ? sieveFilter("status", SIEVE_OP.EQ, f.status) : undefined),
@@ -117,5 +121,5 @@ export function AdminOrdersView({ children, ...props }) {
117
121
  ] })),
118
122
  renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsx(FilterChipGroup, { label: "Status", tabs: ADMIN_ORDER_STATUS_TABS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) })),
119
123
  };
120
- return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(AdminOrderEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), orderId: selectedRow?.id, orderLabel: selectedRow?.primary, currentStatus: selectedRow?.status })] }));
124
+ return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(AdminOrderEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), orderId: selectedRow?.id, orderLabel: selectedRow?.primary, currentStatus: selectedRow?.status, paymentProofUrl: selectedRow?.paymentProofUrl, paymentTransactionId: selectedRow?.paymentTransactionId, paymentMethod: selectedRow?.paymentMethod, paymentStatus: selectedRow?.paymentStatus })] }));
121
125
  }
@@ -88,6 +88,8 @@ export interface AppLayoutShellProps {
88
88
  logout?: string;
89
89
  };
90
90
  eventBannerSlot?: React.ReactNode;
91
+ /** When provided, renders a tour-start icon button in the title bar. Null in Patch 1. */
92
+ onTourStart?: () => void;
91
93
  /**
92
94
  * Override className for the main content wrapper div.
93
95
  * When set, replaces the default `w-full px-4 py-6 …`
@@ -113,4 +115,4 @@ export interface AppLayoutShellProps {
113
115
  };
114
116
  };
115
117
  }
116
- export declare function AppLayoutShell({ children, navItems, sidebarItems, sidebarSections, sidebarPrimaryActions, sidebarTitle, hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle, suppressDashboardNav, hideSidebarToggle, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, eventBannerSlot, contentClassName, lightBackground, darkBackground, }: AppLayoutShellProps): import("react/jsx-runtime").JSX.Element;
118
+ export declare function AppLayoutShell({ children, navItems, sidebarItems, sidebarSections, sidebarPrimaryActions, sidebarTitle, hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle, suppressDashboardNav, hideSidebarToggle, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, eventBannerSlot, onTourStart, contentClassName, lightBackground, darkBackground, }: AppLayoutShellProps): import("react/jsx-runtime").JSX.Element;
@@ -104,7 +104,7 @@ function SidebarContent({ sidebarItems, sidebarSections, sidebarPrimaryActions,
104
104
  ].join(" "), children: action.label }, `${action.href}-${action.label}`))) })), isAuthenticated && (_jsx(CollapsibleNavGroup, { title: labels.sectionTitle, children: _jsxs(Ul, { spacing: "2xs", children: [_jsx(Li, { children: _jsx(TextLink, { href: profileHref, variant: "none", className: navItemClass, children: labels.profile }) }), userOrdersHref && (_jsx(Li, { children: _jsx(TextLink, { href: userOrdersHref, variant: "none", className: navItemClass, children: labels.orders }) })), userWishlistHref && (_jsx(Li, { children: _jsx(TextLink, { href: userWishlistHref, variant: "none", className: navItemClass, children: labels.wishlist }) })), userSettingsHref && (_jsx(Li, { children: _jsx(TextLink, { href: userSettingsHref, variant: "none", className: navItemClass, children: labels.settings }) }))] }) })), isAuthenticated && user?.stats && (_jsxs(Div, { layout: "grid", gap: "2", className: "grid-cols-2", children: [user.stats.totalOrders != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.totalOrders }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Orders" })] })), user.stats.reviewsCount != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.reviewsCount }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Reviews" })] })), user.stats.auctionsWon != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.auctionsWon }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Auctions Won" })] })), user.stats.itemsSold != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.itemsSold }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Items Sold" })] }))] })), isAuthenticated && isAdminOrSeller && (adminHref || resolvedStoreHref) && (_jsx(CollapsibleNavGroup, { title: labels.dashboardSectionTitle, children: _jsxs(Ul, { spacing: "2xs", children: [adminHref && role === "admin" && (_jsx(Li, { children: _jsx(TextLink, { href: adminHref, variant: "none", className: navItemClass, children: labels.adminDashboard }) })), resolvedStoreHref && isAdminOrSeller && (_jsx(Li, { children: _jsx(TextLink, { href: resolvedStoreHref, variant: "none", className: navItemClass, children: labels.storeDashboard }) }))] }) })), normalizedSections.map((section, sectionIndex) => (_jsx(CollapsibleSidebarSection, { section: section, navItemClass: navItemClass }, `sidebar-section-${sectionIndex}`))), (sidebarLocaleSlot || showThemeToggleInSidebar || (isAuthenticated && onLogout)) && (_jsxs(Stack, { border: "default", className: "border-t dark:border-slate-800", padding: "t-md", gap: "3", children: [sidebarLocaleSlot, showThemeToggleInSidebar && (_jsxs("button", { type: "button", onClick: toggleTheme, className: "flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm text-zinc-700 transition-colors hover:bg-primary-50 hover:text-primary-800 dark:text-zinc-300 dark:hover:bg-slate-800 dark:hover:text-secondary-300", children: [_jsx(Span, { "aria-hidden": "true", children: theme === "dark" ? "☀️" : "🌙" }), theme === "dark" ? "Light mode" : "Dark mode"] })), isAuthenticated && onLogout && (_jsx("button", { type: "button", onClick: () => { onLogout(); onAfterLogout(); }, className: CLS_LOGOUT_BTN, children: labels.logout }))] }))] }));
105
105
  }
106
106
  // ─── Main component ────────────────────────────────────────────────────────────
107
- export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarSections, sidebarPrimaryActions, sidebarTitle = "Navigation", hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle = false, suppressDashboardNav = false, hideSidebarToggle = false, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar = false, sidebarProfileLabels, eventBannerSlot, contentClassName, lightBackground = DEFAULT_LIGHT_BG, darkBackground = DEFAULT_DARK_BG, }) {
107
+ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarSections, sidebarPrimaryActions, sidebarTitle = "Navigation", hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle = false, suppressDashboardNav = false, hideSidebarToggle = false, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar = false, sidebarProfileLabels, eventBannerSlot, onTourStart, contentClassName, lightBackground = DEFAULT_LIGHT_BG, darkBackground = DEFAULT_DARK_BG, }) {
108
108
  // QueryClientProvider used to be created here so AppLayoutShell could stand
109
109
  // alone. The real cause of the 2026-06-10/11 "No QueryClient set" prod crash
110
110
  // turned out to be a peer-dep duplicate of @tanstack/query-core under
@@ -164,5 +164,5 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
164
164
  opacity: darkBackground.overlay?.opacity ?? 0,
165
165
  },
166
166
  };
167
- return (_jsx(_Fragment, { children: _jsxs(Stack, { className: "min-h-screen w-full overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(NavbarWithSettings, { navItems: navItems, hiddenNavItems: hiddenNavItems, permissions: authUser?.permissions }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot)] }), eventBannerSlot, _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { layout: "flex", className: "relative w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsx(SidebarUserHeader, { user: user, onClose: () => setSidebarOpen(false) })) : (_jsx(SidebarGuestHeader, { sidebarTitle: sidebarTitle, onClose: () => setSidebarOpen(false) })), onClose: () => setSidebarOpen(false), children: sidebarContent }), _jsx(Main, { id: "main-content", className: `w-full flex-1 flex flex-col ${hasBottomActions ? "mb-28" : "mb-16"} md:mb-0`, children: _jsx(Div, { padding: "y-lg", className: `flex-1 ${contentClassName ?? "w-full px-5 md:px-6 lg:px-8"}`, children: children }) })] }), _jsx(BackToTop, {}), _jsx(FooterLayout, { ...footer }), _jsx(BottomActions, {}), _jsx(BottomNavbar, { user: user, homeHref: homeHref, shopHref: shopHref, cartHref: cartHref, profileHref: profileHref, loginHref: loginHref, onSearchToggle: () => setSearchOpen((prev) => !prev), navItems: navItems, onMoreToggle: hasDashboardNav ? toggleDashboardNav : handleTogglePublicSidebar }), _jsx(UnsavedChangesModal, {})] }) }));
167
+ return (_jsx(_Fragment, { children: _jsxs(Stack, { className: "min-h-screen w-full overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onTourStart: onTourStart, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(NavbarWithSettings, { navItems: navItems, hiddenNavItems: hiddenNavItems, permissions: authUser?.permissions }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot)] }), eventBannerSlot, _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { layout: "flex", className: "relative w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsx(SidebarUserHeader, { user: user, onClose: () => setSidebarOpen(false) })) : (_jsx(SidebarGuestHeader, { sidebarTitle: sidebarTitle, onClose: () => setSidebarOpen(false) })), onClose: () => setSidebarOpen(false), children: sidebarContent }), _jsx(Main, { id: "main-content", className: `w-full flex-1 flex flex-col ${hasBottomActions ? "mb-28" : "mb-16"} md:mb-0`, children: _jsx(Div, { padding: "y-lg", className: `flex-1 ${contentClassName ?? "mx-auto w-full max-w-screen-xl px-5 md:px-6 lg:px-8"}`, children: children }) })] }), _jsx(BackToTop, {}), _jsx(FooterLayout, { ...footer }), _jsx(BottomActions, {}), _jsx(BottomNavbar, { user: user, homeHref: homeHref, shopHref: shopHref, cartHref: cartHref, profileHref: profileHref, loginHref: loginHref, onSearchToggle: () => setSearchOpen((prev) => !prev), navItems: navItems, onMoreToggle: hasDashboardNav ? toggleDashboardNav : handleTogglePublicSidebar }), _jsx(UnsavedChangesModal, {})] }) }));
168
168
  }
@@ -52,6 +52,8 @@ export interface TitleBarLayoutProps {
52
52
  onToggleDashboardNav?: () => void;
53
53
  /** Hide the public sidebar toggle button when nested layouts own navigation. */
54
54
  hideSidebarToggle?: boolean;
55
+ /** When provided, renders a tour-start icon button before the theme toggle. Null in Patch 1. */
56
+ onTourStart?: () => void;
55
57
  id?: string;
56
58
  className?: string;
57
59
  }
@@ -64,4 +66,4 @@ export interface TitleBarLayoutProps {
64
66
  *
65
67
  * Receives all domain data as props — zero domain imports.
66
68
  */
67
- export declare function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen: _searchOpen, brandName, brandShortName: _brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount, cartHref, cartCount, profileHref, unreadNotificationCount, notificationsHref, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark, onToggleTheme, hasDashboardNav, onToggleDashboardNav, hideSidebarToggle, id, className, }: TitleBarLayoutProps): import("react/jsx-runtime").JSX.Element;
69
+ export declare function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen: _searchOpen, brandName, brandShortName: _brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount, cartHref, cartCount, profileHref, unreadNotificationCount, notificationsHref, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark, onToggleTheme, onTourStart, hasDashboardNav, onToggleDashboardNav, hideSidebarToggle, id, className, }: TitleBarLayoutProps): import("react/jsx-runtime").JSX.Element;