@mohasinac/appkit 4.21.0 → 4.23.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.
Files changed (102) hide show
  1. package/dist/_internal/server/features/checkout/locked-lines.js +12 -2
  2. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  3. package/dist/_internal/server/features/stores/adapters.js +5 -0
  4. package/dist/_internal/server/jobs/core/auctionSettlement.js +10 -4
  5. package/dist/_internal/server/jobs/core/offerExpiry.js +10 -2
  6. package/dist/_internal/server/jobs/core/payoutBatch.js +15 -3
  7. package/dist/_internal/shared/features/products/schema.d.ts +8 -8
  8. package/dist/_internal/shared/features/promotions/schema.d.ts +4 -4
  9. package/dist/_internal/shared/history/index.d.ts +1 -1
  10. package/dist/_internal/shared/history/types.d.ts +16 -0
  11. package/dist/client.d.ts +7 -0
  12. package/dist/client.js +4 -0
  13. package/dist/constants/field-names.d.ts +12 -0
  14. package/dist/constants/field-names.js +12 -0
  15. package/dist/features/account/components/AddressForm.js +45 -29
  16. package/dist/features/admin/actions/admin-actions.js +13 -3
  17. package/dist/features/admin/components/AdminBidsView.js +7 -1
  18. package/dist/features/admin/components/AdminBundleEditorView.js +3 -0
  19. package/dist/features/admin/components/AdminScammerEditorView.d.ts +5 -1
  20. package/dist/features/admin/components/AdminScammerEditorView.js +3 -2
  21. package/dist/features/admin/components/AdminSupportTicketDetailView.d.ts +5 -1
  22. package/dist/features/admin/components/AdminSupportTicketDetailView.js +3 -2
  23. package/dist/features/auctions/components/PlaceBidFormClient.js +3 -1
  24. package/dist/features/auctions/repository/bid.repository.d.ts +21 -8
  25. package/dist/features/auctions/repository/bid.repository.js +57 -33
  26. package/dist/features/auctions/schemas/firestore.d.ts +27 -0
  27. package/dist/features/auctions/schemas/firestore.js +21 -0
  28. package/dist/features/auth/schemas/index.d.ts +2 -2
  29. package/dist/features/catalogue/actions/approve-catalogue-listing.js +7 -6
  30. package/dist/features/catalogue/actions/list-from-catalogue.js +2 -2
  31. package/dist/features/catalogue/actions/submit-for-approval.js +1 -4
  32. package/dist/features/catalogue/components/AdminCatalogueApprovalsView.js +5 -1
  33. package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -1
  34. package/dist/features/catalogue/repository/catalogue.repository.d.ts +23 -0
  35. package/dist/features/catalogue/repository/catalogue.repository.js +28 -1
  36. package/dist/features/catalogue/schemas/firestore.d.ts +19 -0
  37. package/dist/features/catalogue/schemas/firestore.js +12 -0
  38. package/dist/features/categories/schemas/index.d.ts +2 -2
  39. package/dist/features/layout/BottomActions.js +18 -1
  40. package/dist/features/layout/BottomActionsContext.d.ts +19 -0
  41. package/dist/features/layout/BottomActionsContext.js +37 -0
  42. package/dist/features/layout/hooks/useBottomActions.d.ts +10 -0
  43. package/dist/features/layout/hooks/useBottomActions.js +48 -11
  44. package/dist/features/layout/hooks/useFormBottomActions.d.ts +57 -0
  45. package/dist/features/layout/hooks/useFormBottomActions.js +89 -0
  46. package/dist/features/layout/index.d.ts +2 -0
  47. package/dist/features/layout/index.js +1 -0
  48. package/dist/features/messages/schemas/index.d.ts +6 -6
  49. package/dist/features/orders/schemas/index.d.ts +12 -12
  50. package/dist/features/payments/repository/payout.repository.d.ts +18 -5
  51. package/dist/features/payments/repository/payout.repository.js +45 -19
  52. package/dist/features/payments/schemas/firestore.d.ts +33 -1
  53. package/dist/features/payments/schemas/firestore.js +39 -0
  54. package/dist/features/payments/schemas/index.d.ts +12 -12
  55. package/dist/features/products/components/MakeOfferButton.js +5 -3
  56. package/dist/features/products/schemas/index.d.ts +2 -2
  57. package/dist/features/promotions/schemas/index.d.ts +12 -12
  58. package/dist/features/scams/repository/scammer.repository.d.ts +18 -1
  59. package/dist/features/scams/repository/scammer.repository.js +24 -2
  60. package/dist/features/scams/schemas/firestore.d.ts +24 -0
  61. package/dist/features/scams/schemas/firestore.js +30 -0
  62. package/dist/features/shell/QuickFormDrawer.js +15 -1
  63. package/dist/features/shell/SectionForm.d.ts +16 -2
  64. package/dist/features/shell/SectionForm.js +28 -2
  65. package/dist/features/shipments/components/AdminShipmentEditorView.js +5 -2
  66. package/dist/features/shipments/repository/shipments.repository.d.ts +18 -0
  67. package/dist/features/shipments/repository/shipments.repository.js +23 -1
  68. package/dist/features/shipments/schemas/firestore.d.ts +14 -0
  69. package/dist/features/shipments/schemas/firestore.js +19 -0
  70. package/dist/features/status-history/components/RecordStatusTimeline.d.ts +12 -0
  71. package/dist/features/status-history/components/RecordStatusTimeline.js +90 -0
  72. package/dist/features/status-history/index.d.ts +2 -0
  73. package/dist/features/status-history/index.js +1 -0
  74. package/dist/features/stores/repository/store.repository.d.ts +30 -2
  75. package/dist/features/stores/repository/store.repository.js +43 -4
  76. package/dist/features/stores/schemas/firestore.d.ts +29 -0
  77. package/dist/features/stores/schemas/firestore.js +31 -0
  78. package/dist/features/support/repository/support.repository.d.ts +47 -3
  79. package/dist/features/support/repository/support.repository.js +72 -12
  80. package/dist/features/support/schemas/firestore.d.ts +38 -1
  81. package/dist/features/support/schemas/firestore.js +34 -0
  82. package/dist/features/support/schemas/index.d.ts +2 -2
  83. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +90 -0
  84. package/dist/index.d.ts +7 -1
  85. package/dist/index.js +16 -1
  86. package/dist/schemas/registry.d.ts +32 -32
  87. package/dist/seed/bids-seed-data.js +8 -0
  88. package/dist/ui/components/Form.d.ts +22 -1
  89. package/dist/ui/components/Form.js +30 -2
  90. package/dist/ui/components/Modal.js +4 -3
  91. package/dist/ui/components/SideDrawer.js +5 -4
  92. package/dist/ui/components/overlay-context.d.ts +21 -0
  93. package/dist/ui/components/overlay-context.js +25 -0
  94. package/dist/ui/forms/FormErrorList.d.ts +29 -0
  95. package/dist/ui/forms/FormErrorList.js +36 -0
  96. package/dist/ui/forms/FormErrorSummary.d.ts +8 -0
  97. package/dist/ui/forms/FormErrorSummary.js +15 -8
  98. package/dist/ui/forms/FormShell.d.ts +25 -1
  99. package/dist/ui/forms/FormShell.js +39 -4
  100. package/dist/ui/forms/index.d.ts +2 -0
  101. package/dist/ui/forms/index.js +1 -0
  102. package/package.json +1 -1
@@ -114,7 +114,7 @@ export default function BottomActions() {
114
114
  setPickerOpen(false);
115
115
  setInfoOpen(false);
116
116
  });
117
- const { actions, bulk, infoLabel, secondaryLabel, infoPanel, desktop } = state;
117
+ const { actions, bulk, infoLabel, secondaryLabel, infoPanel, infoOpenSignal, desktop } = state;
118
118
  // Desktop reveal-on-scroll. Kept as a plain passive listener to match BackToTop —
119
119
  // there is no shared scroll-position hook in the codebase and one consumer doesn't
120
120
  // justify inventing one. Only armed when a page actually opts into "after-scroll".
@@ -144,6 +144,23 @@ export default function BottomActions() {
144
144
  if (!hasInfoPanel)
145
145
  setInfoOpen(false);
146
146
  }, [hasInfoPanel]);
147
+ /*
148
+ * Force-open on a bumped signal — a failed form submit, today. Depends on
149
+ * the counter alone, so a re-render with the same value never re-opens what
150
+ * the user has since collapsed. `hasInfoPanel` is read but deliberately NOT
151
+ * a dependency: publishing the panel and bumping the signal are two separate
152
+ * setState calls, so re-running on `hasInfoPanel` would open the panel again
153
+ * one render after the user closed it.
154
+ */
155
+ const lastSignalRef = useRef(undefined);
156
+ useEffect(() => {
157
+ if (infoOpenSignal == null)
158
+ return;
159
+ if (lastSignalRef.current === infoOpenSignal)
160
+ return;
161
+ lastSignalRef.current = infoOpenSignal;
162
+ setInfoOpen(true);
163
+ }, [infoOpenSignal]);
147
164
  // Keep selectedActionId in sync with available bulk actions
148
165
  useEffect(() => {
149
166
  if (!isBulkMode) {
@@ -109,6 +109,19 @@ export interface BottomActionsState {
109
109
  * bulk mode, which owns the same space for its action picker.
110
110
  */
111
111
  infoPanel?: React.ReactNode;
112
+ /**
113
+ * A COUNTER, not a boolean — bump it to force `infoPanel` open once.
114
+ *
115
+ * `BottomActions` owns `infoOpen` in local state so the user can collapse
116
+ * the panel at will. A boolean `forceOpen` would fight that: it stays true,
117
+ * so every subsequent render re-opens what they just closed. A counter
118
+ * expresses the real intent — "this is a NEW event, show it" — and fires
119
+ * exactly once per increment.
120
+ *
121
+ * Used by a form's failed submit: the error list should appear without a
122
+ * tap, and stay dismissible afterwards.
123
+ */
124
+ infoOpenSignal?: number;
112
125
  }
113
126
  interface BottomActionsContextValue {
114
127
  state: BottomActionsState;
@@ -131,10 +144,16 @@ interface BottomActionsContextValue {
131
144
  setSecondaryLabel: (label: string | undefined) => void;
132
145
  /** Set or clear the expandable panel revealed above the bar. */
133
146
  setInfoPanel: (panel: React.ReactNode | undefined) => void;
147
+ /** Set the force-open counter. See BottomActionsState.infoOpenSignal. */
148
+ setInfoOpenSignal: (signal: number | undefined) => void;
134
149
  /** Set the desktop visibility policy. */
135
150
  setDesktopMode: (mode: BottomActionsDesktopMode | undefined) => void;
136
151
  /** Clear all state (called on feature unmount). */
137
152
  clearAll: () => void;
153
+ claim: (id: string) => void;
154
+ release: (id: string) => void;
155
+ /** The id currently allowed to publish. `useBottomActions` gates on this. */
156
+ topClaimId: string | null;
138
157
  }
139
158
  export declare function BottomActionsProvider({ children, }: {
140
159
  children: React.ReactNode;
@@ -47,6 +47,32 @@ export function BottomActionsProvider({ children, }) {
47
47
  const actionCallbacksRef = useRef(new Map());
48
48
  const bulkCallbacksRef = useRef(new Map());
49
49
  const bulkClearRef = useRef(undefined);
50
+ // Stack of claimant ids, oldest first. The last entry owns the bar.
51
+ const claimsRef = useRef([]);
52
+ const [topClaimId, setTopClaimId] = useState(null);
53
+ const claim = useCallback((id) => {
54
+ if (claimsRef.current.includes(id))
55
+ return;
56
+ claimsRef.current = [...claimsRef.current, id];
57
+ setTopClaimId(id);
58
+ }, []);
59
+ const release = useCallback((id) => {
60
+ const next = claimsRef.current.filter((c) => c !== id);
61
+ if (next.length === claimsRef.current.length)
62
+ return;
63
+ claimsRef.current = next;
64
+ const newTop = next.length > 0 ? next[next.length - 1] : null;
65
+ setTopClaimId(newTop);
66
+ // Only blank the bar when nobody is left. Otherwise the newly-top
67
+ // consumer republishes on the very next effect pass, so clearing here
68
+ // would be a visible flicker of an empty bar.
69
+ if (newTop === null) {
70
+ actionCallbacksRef.current = new Map();
71
+ bulkCallbacksRef.current = new Map();
72
+ bulkClearRef.current = undefined;
73
+ setState(EMPTY);
74
+ }
75
+ }, []);
50
76
  const setActions = useCallback((actions) => {
51
77
  // Update callback map (always latest)
52
78
  actionCallbacksRef.current = new Map(actions.map((a) => [a.id, a.onClick]));
@@ -90,6 +116,9 @@ export function BottomActionsProvider({ children, }) {
90
116
  const setInfoPanel = useCallback((infoPanel) => {
91
117
  setState((prev) => ({ ...prev, infoPanel }));
92
118
  }, []);
119
+ const setInfoOpenSignal = useCallback((infoOpenSignal) => {
120
+ setState((prev) => (prev.infoOpenSignal === infoOpenSignal ? prev : { ...prev, infoOpenSignal }));
121
+ }, []);
93
122
  const setDesktopMode = useCallback((desktop) => {
94
123
  setState((prev) => (prev.desktop === desktop ? prev : { ...prev, desktop }));
95
124
  }, []);
@@ -109,8 +138,12 @@ export function BottomActionsProvider({ children, }) {
109
138
  setInfoLabel,
110
139
  setSecondaryLabel,
111
140
  setInfoPanel,
141
+ setInfoOpenSignal,
112
142
  setDesktopMode,
113
143
  clearAll,
144
+ claim,
145
+ release,
146
+ topClaimId,
114
147
  }), [
115
148
  state,
116
149
  setActions,
@@ -118,8 +151,12 @@ export function BottomActionsProvider({ children, }) {
118
151
  setInfoLabel,
119
152
  setSecondaryLabel,
120
153
  setInfoPanel,
154
+ setInfoOpenSignal,
121
155
  setDesktopMode,
122
156
  clearAll,
157
+ claim,
158
+ release,
159
+ topClaimId,
123
160
  ]);
124
161
  return (_jsx(BottomActionsContext.Provider, { value: value, children: children }));
125
162
  }
@@ -11,10 +11,20 @@ export interface UseBottomActionsOptions {
11
11
  * See BottomActionsState.infoPanel.
12
12
  */
13
13
  infoPanel?: React.ReactNode;
14
+ /** Bump to force `infoPanel` open once. See BottomActionsState.infoOpenSignal. */
15
+ infoOpenSignal?: number;
14
16
  /**
15
17
  * Whether the bar may also show on desktop. Defaults to "hidden" (mobile/tablet only),
16
18
  * so pages that don't opt in behave exactly as before.
17
19
  */
18
20
  desktop?: BottomActionsDesktopMode;
21
+ /**
22
+ * Set false to opt out entirely — no claim is taken, so this consumer is
23
+ * invisible to the stack and cannot displace whoever currently owns the
24
+ * bar. Load-bearing: `useFormBottomActions` is called unconditionally from
25
+ * `<Form>`, which mounts on ~75 surfaces, and a claim from each of those
26
+ * would push a listing's bulk bar off the stack for nothing.
27
+ */
28
+ enabled?: boolean;
19
29
  }
20
30
  export declare function useBottomActions(options?: UseBottomActionsOptions): void;
@@ -1,8 +1,28 @@
1
1
  "use client";
2
- import { useEffect, useRef } from "react";
2
+ import { useEffect, useId, useRef } from "react";
3
3
  import { useBottomActionsContext, } from "../BottomActionsContext";
4
4
  export function useBottomActions(options = {}) {
5
- const { setActions, setBulkConfig, setInfoLabel, setSecondaryLabel, setInfoPanel, setDesktopMode, clearAll, } = useBottomActionsContext();
5
+ const { setActions, setBulkConfig, setInfoLabel, setSecondaryLabel, setInfoPanel, setInfoOpenSignal, setDesktopMode, claim, release, topClaimId, } = useBottomActionsContext();
6
+ /*
7
+ * One bar, many potential claimants. `DataListingView` already holds it on
8
+ * ~70 admin screens, so a form opened in a drawer over a listing used to
9
+ * overwrite the listing's bar and blank it on close.
10
+ *
11
+ * Claim on mount, release on unmount. `isTop` gates every publish below, so
12
+ * a backgrounded consumer stops writing — and because `isTop` is a
13
+ * dependency of each effect, regaining the top re-publishes everything
14
+ * automatically. That is what restores the listing's bar when the form
15
+ * closes, with no cooperation needed from either side.
16
+ */
17
+ const enabled = options.enabled !== false;
18
+ const claimId = useId();
19
+ useEffect(() => {
20
+ if (!enabled)
21
+ return;
22
+ claim(claimId);
23
+ return () => release(claimId);
24
+ }, [claim, release, claimId, enabled]);
25
+ const isTop = enabled && topClaimId === claimId;
6
26
  const actionsRef = useRef(options.actions ?? []);
7
27
  actionsRef.current = options.actions ?? [];
8
28
  const bulkRef = useRef(options.bulk);
@@ -14,17 +34,25 @@ export function useBottomActions(options = {}) {
14
34
  ? `${options.bulk.selectedCount}|${options.bulk.actions.map((a) => `${a.id}|${a.label}|${a.variant}|${a.badge}|${a.disabled}|${a.loading}|${a.grow}`).join(",")}`
15
35
  : "";
16
36
  useEffect(() => {
37
+ if (!isTop)
38
+ return;
17
39
  setActions(actionsRef.current);
18
- }, [setActions, actionKey]);
40
+ }, [setActions, actionKey, isTop]);
19
41
  useEffect(() => {
42
+ if (!isTop)
43
+ return;
20
44
  setBulkConfig(bulkRef.current);
21
- }, [setBulkConfig, bulkCountKey]);
45
+ }, [setBulkConfig, bulkCountKey, isTop]);
22
46
  useEffect(() => {
47
+ if (!isTop)
48
+ return;
23
49
  setInfoLabel(options.infoLabel);
24
- }, [setInfoLabel, options.infoLabel]);
50
+ }, [setInfoLabel, options.infoLabel, isTop]);
25
51
  useEffect(() => {
52
+ if (!isTop)
53
+ return;
26
54
  setSecondaryLabel(options.secondaryLabel);
27
- }, [setSecondaryLabel, options.secondaryLabel]);
55
+ }, [setSecondaryLabel, options.secondaryLabel, isTop]);
28
56
  // A ReactNode is a fresh object on every render, so it can't be a dependency
29
57
  // without looping. Mirror it through a ref and re-publish whenever anything
30
58
  // the panel actually derives from changes — callers pass `infoLabel`, which
@@ -33,12 +61,21 @@ export function useBottomActions(options = {}) {
33
61
  infoPanelRef.current = options.infoPanel;
34
62
  const hasInfoPanel = options.infoPanel != null;
35
63
  useEffect(() => {
64
+ if (!isTop)
65
+ return;
36
66
  setInfoPanel(infoPanelRef.current);
37
- }, [setInfoPanel, hasInfoPanel, options.infoLabel, options.secondaryLabel]);
67
+ }, [setInfoPanel, hasInfoPanel, options.infoLabel, options.secondaryLabel, isTop]);
38
68
  useEffect(() => {
39
- setDesktopMode(options.desktop);
40
- }, [setDesktopMode, options.desktop]);
69
+ if (!isTop)
70
+ return;
71
+ setInfoOpenSignal(options.infoOpenSignal);
72
+ }, [setInfoOpenSignal, options.infoOpenSignal, isTop]);
41
73
  useEffect(() => {
42
- return () => clearAll();
43
- }, [clearAll]);
74
+ if (!isTop)
75
+ return;
76
+ setDesktopMode(options.desktop);
77
+ }, [setDesktopMode, options.desktop, isTop]);
78
+ // NOTE: no clearAll() on unmount any more — `release()` above owns that, and
79
+ // only blanks the bar when the stack empties. Clearing here would flash an
80
+ // empty bar between a drawer closing and the listing behind it republishing.
44
81
  }
@@ -0,0 +1,57 @@
1
+ import { type FormShellContextValue } from "../../../ui/forms/FormShell";
2
+ export interface UseFormBottomActionsOptions {
3
+ /** Runs when the pinned primary button is pressed. */
4
+ onSubmit: () => void | Promise<void>;
5
+ /** Omit to render no Cancel button — a settings page with nowhere to go back to. */
6
+ onCancel?: () => void;
7
+ submitLabel?: string;
8
+ cancelLabel?: string;
9
+ isLoading?: boolean;
10
+ disabled?: boolean;
11
+ /** Set false to leave the bar to someone else on this route. */
12
+ enabled?: boolean;
13
+ /**
14
+ * Read the form state from here instead of the ambient context. `<Form>`
15
+ * needs it: the hook runs in `Form`'s body, which is ABOVE the provider
16
+ * `Form` itself renders, so the ambient lookup would find the parent's
17
+ * context or none at all.
18
+ */
19
+ ctx?: FormShellContextValue;
20
+ }
21
+ /**
22
+ * Pin a form's Save/Cancel row to the bottom of a phone screen, and turn its
23
+ * validation errors into a pull-up sheet above it.
24
+ *
25
+ * ## Nothing here is new chrome
26
+ *
27
+ * This is `useBottomActions` with a form-shaped payload. The cart's bottom bar
28
+ * — "Total ₹777 · Details ⌃" over a pinned CTA — is 100% that primitive's
29
+ * output already, so the sheet mechanics, the collapse rule
30
+ * (`grid-rows-[1fr] ↔ [0fr]`, background on the innermost div),
31
+ * click-outside-to-close, the `ResizeObserver` that publishes
32
+ * `--bottom-chrome-height`, and all three bottom-edge offsets come for free.
33
+ * Re-deriving any of that would be a second, worse copy.
34
+ *
35
+ * ## Three details that will bite anyone editing this
36
+ *
37
+ * 1. **The label must encode the count.** `useBottomActions` re-publishes
38
+ * `infoPanel` only when `hasInfoPanel`, `infoLabel` or `secondaryLabel`
39
+ * change — a ReactNode is a fresh object every render and cannot be a
40
+ * dependency without looping. "Fix 6 issues" → "Fix 5 issues" is
41
+ * therefore what pushes the updated list through. A generic label like
42
+ * "Errors" would freeze the sheet's contents at whatever they were the
43
+ * first time it opened.
44
+ *
45
+ * 2. **The panel is rendered outside the form's provider.** `BottomActions`
46
+ * lives in the layout shell, so the detached node cannot reach
47
+ * `FormShellContext` by itself — hence `<FormErrorList ctx={ctx}>` with
48
+ * the context passed explicitly.
49
+ *
50
+ * 3. **Suppressed inside a Modal or SideDrawer.** An overlay owns its own
51
+ * footer, and a viewport-fixed bar would render *behind* the backdrop,
52
+ * below the dialog it belongs to. `useIsInsideOverlay()` is how the
53
+ * overlay tells us, because neither portal leaves a detectable trace in
54
+ * the DOM at the form's position.
55
+ */
56
+ export declare function useFormBottomActions({ onSubmit, onCancel, submitLabel, cancelLabel, isLoading, disabled, enabled, ctx: ctxProp, }: UseFormBottomActionsOptions): void;
57
+ export default useFormBottomActions;
@@ -0,0 +1,89 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useContext, useMemo } from "react";
4
+ import { FormShellContext } from "../../../ui/forms/FormShell";
5
+ import { FormErrorList } from "../../../ui/forms/FormErrorList";
6
+ import { useIsInsideOverlay } from "../../../ui/components/overlay-context";
7
+ import { useBottomActions } from "./useBottomActions";
8
+ /**
9
+ * Pin a form's Save/Cancel row to the bottom of a phone screen, and turn its
10
+ * validation errors into a pull-up sheet above it.
11
+ *
12
+ * ## Nothing here is new chrome
13
+ *
14
+ * This is `useBottomActions` with a form-shaped payload. The cart's bottom bar
15
+ * — "Total ₹777 · Details ⌃" over a pinned CTA — is 100% that primitive's
16
+ * output already, so the sheet mechanics, the collapse rule
17
+ * (`grid-rows-[1fr] ↔ [0fr]`, background on the innermost div),
18
+ * click-outside-to-close, the `ResizeObserver` that publishes
19
+ * `--bottom-chrome-height`, and all three bottom-edge offsets come for free.
20
+ * Re-deriving any of that would be a second, worse copy.
21
+ *
22
+ * ## Three details that will bite anyone editing this
23
+ *
24
+ * 1. **The label must encode the count.** `useBottomActions` re-publishes
25
+ * `infoPanel` only when `hasInfoPanel`, `infoLabel` or `secondaryLabel`
26
+ * change — a ReactNode is a fresh object every render and cannot be a
27
+ * dependency without looping. "Fix 6 issues" → "Fix 5 issues" is
28
+ * therefore what pushes the updated list through. A generic label like
29
+ * "Errors" would freeze the sheet's contents at whatever they were the
30
+ * first time it opened.
31
+ *
32
+ * 2. **The panel is rendered outside the form's provider.** `BottomActions`
33
+ * lives in the layout shell, so the detached node cannot reach
34
+ * `FormShellContext` by itself — hence `<FormErrorList ctx={ctx}>` with
35
+ * the context passed explicitly.
36
+ *
37
+ * 3. **Suppressed inside a Modal or SideDrawer.** An overlay owns its own
38
+ * footer, and a viewport-fixed bar would render *behind* the backdrop,
39
+ * below the dialog it belongs to. `useIsInsideOverlay()` is how the
40
+ * overlay tells us, because neither portal leaves a detectable trace in
41
+ * the DOM at the form's position.
42
+ */
43
+ export function useFormBottomActions({ onSubmit, onCancel, submitLabel = "Save", cancelLabel = "Cancel", isLoading = false, disabled = false, enabled = true, ctx: ctxProp, }) {
44
+ const ambient = useContext(FormShellContext);
45
+ const ctx = ctxProp ?? ambient;
46
+ const insideOverlay = useIsInsideOverlay();
47
+ const active = enabled && !insideOverlay && ctx != null;
48
+ const errorCount = ctx ? Object.keys(ctx.errors).length : 0;
49
+ const showErrors = !!ctx?.submitAttempted && errorCount > 0;
50
+ const actions = useMemo(() => {
51
+ if (!active)
52
+ return [];
53
+ const list = [];
54
+ if (onCancel) {
55
+ list.push({
56
+ id: "form-cancel",
57
+ label: cancelLabel,
58
+ variant: "outline",
59
+ grow: false,
60
+ onClick: onCancel,
61
+ disabled: isLoading,
62
+ });
63
+ }
64
+ list.push({
65
+ id: "form-submit",
66
+ label: submitLabel,
67
+ variant: "primary",
68
+ grow: true,
69
+ loading: isLoading,
70
+ disabled: disabled || isLoading,
71
+ onClick: () => void onSubmit(),
72
+ });
73
+ return list;
74
+ }, [active, onCancel, cancelLabel, submitLabel, isLoading, disabled, onSubmit]);
75
+ useBottomActions({
76
+ actions,
77
+ // Encodes the count on purpose — see (1) in the header.
78
+ infoLabel: showErrors
79
+ ? `Fix ${errorCount} ${errorCount === 1 ? "issue" : "issues"}`
80
+ : undefined,
81
+ infoPanel: showErrors && ctx ? _jsx(FormErrorList, { ctx: ctx }) : undefined,
82
+ // Bumped by every submit attempt, so a second failed Save re-opens a
83
+ // sheet the user collapsed — while an ordinary re-render does not.
84
+ infoOpenSignal: showErrors ? ctx?.submitAttemptCount : undefined,
85
+ // No claim at all when inactive — see UseBottomActionsOptions.enabled.
86
+ enabled: active,
87
+ });
88
+ }
89
+ export default useFormBottomActions;
@@ -30,6 +30,8 @@ export { BottomActionsProvider, useBottomActionsContext, } from "./BottomActions
30
30
  export type { BottomAction, BottomBulkConfig, BottomActionsState, BottomActionsDesktopMode, } from "./BottomActionsContext";
31
31
  export { useBottomActions } from "./hooks/useBottomActions";
32
32
  export type { UseBottomActionsOptions } from "./hooks/useBottomActions";
33
+ export { useFormBottomActions } from "./hooks/useFormBottomActions";
34
+ export type { UseFormBottomActionsOptions } from "./hooks/useFormBottomActions";
33
35
  export { default as BottomActions } from "./BottomActions";
34
36
  export { BottomChrome } from "./BottomChrome";
35
37
  export type { BottomChromeProps } from "./BottomChrome";
@@ -16,6 +16,7 @@ export { LayoutClient } from "./LayoutClient";
16
16
  export { AppLayoutShell } from "./AppLayoutShell";
17
17
  export { BottomActionsProvider, useBottomActionsContext, } from "./BottomActionsContext";
18
18
  export { useBottomActions } from "./hooks/useBottomActions";
19
+ export { useFormBottomActions } from "./hooks/useFormBottomActions";
19
20
  export { default as BottomActions } from "./BottomActions";
20
21
  export { BottomChrome } from "./BottomChrome";
21
22
  export { BottomNavbar } from "./BottomNavbar";
@@ -21,7 +21,7 @@ export declare const conversationMessageSchema: z.ZodObject<{
21
21
  body: string;
22
22
  isRead: boolean;
23
23
  senderId: string;
24
- senderRole: "seller" | "buyer";
24
+ senderRole: "buyer" | "seller";
25
25
  sentAt: string | Date | z.objectOutputType<{
26
26
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
27
27
  }, z.ZodTypeAny, "passthrough">;
@@ -31,7 +31,7 @@ export declare const conversationMessageSchema: z.ZodObject<{
31
31
  body: string;
32
32
  isRead: boolean;
33
33
  senderId: string;
34
- senderRole: "seller" | "buyer";
34
+ senderRole: "buyer" | "seller";
35
35
  sentAt: string | Date | z.objectInputType<{
36
36
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
37
37
  }, z.ZodTypeAny, "passthrough">;
@@ -79,7 +79,7 @@ export declare const conversationFirestoreSchema: z.ZodObject<{
79
79
  body: string;
80
80
  isRead: boolean;
81
81
  senderId: string;
82
- senderRole: "seller" | "buyer";
82
+ senderRole: "buyer" | "seller";
83
83
  sentAt: string | Date | z.objectOutputType<{
84
84
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
85
85
  }, z.ZodTypeAny, "passthrough">;
@@ -89,7 +89,7 @@ export declare const conversationFirestoreSchema: z.ZodObject<{
89
89
  body: string;
90
90
  isRead: boolean;
91
91
  senderId: string;
92
- senderRole: "seller" | "buyer";
92
+ senderRole: "buyer" | "seller";
93
93
  sentAt: string | Date | z.objectInputType<{
94
94
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
95
95
  }, z.ZodTypeAny, "passthrough">;
@@ -127,7 +127,7 @@ export declare const conversationFirestoreSchema: z.ZodObject<{
127
127
  body: string;
128
128
  isRead: boolean;
129
129
  senderId: string;
130
- senderRole: "seller" | "buyer";
130
+ senderRole: "buyer" | "seller";
131
131
  sentAt: string | Date | z.objectOutputType<{
132
132
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
133
133
  }, z.ZodTypeAny, "passthrough">;
@@ -160,7 +160,7 @@ export declare const conversationFirestoreSchema: z.ZodObject<{
160
160
  body: string;
161
161
  isRead: boolean;
162
162
  senderId: string;
163
- senderRole: "seller" | "buyer";
163
+ senderRole: "buyer" | "seller";
164
164
  sentAt: string | Date | z.objectInputType<{
165
165
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
166
166
  }, z.ZodTypeAny, "passthrough">;
@@ -128,9 +128,9 @@ export declare const orderRefundEventSchema: z.ZodObject<{
128
128
  overrideReason: z.ZodOptional<z.ZodString>;
129
129
  }, "strip", z.ZodTypeAny, {
130
130
  type: "full" | "partial";
131
+ reason: string;
131
132
  amount: number;
132
133
  refundId: string;
133
- reason: string;
134
134
  refundedAt: string | Date | z.objectOutputType<{
135
135
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
136
136
  }, z.ZodTypeAny, "passthrough">;
@@ -143,9 +143,9 @@ export declare const orderRefundEventSchema: z.ZodObject<{
143
143
  overrideReason?: string | undefined;
144
144
  }, {
145
145
  type: "full" | "partial";
146
+ reason: string;
146
147
  amount: number;
147
148
  refundId: string;
148
- reason: string;
149
149
  refundedAt: string | Date | z.objectInputType<{
150
150
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
151
151
  }, z.ZodTypeAny, "passthrough">;
@@ -168,14 +168,14 @@ export declare const appliedOrderDiscountSchema: z.ZodObject<{
168
168
  code: string;
169
169
  type: "auto" | "coupon" | "deal";
170
170
  discountAmount: number;
171
- scope?: "admin" | "seller" | undefined;
171
+ scope?: "seller" | "admin" | undefined;
172
172
  storeId?: string | undefined;
173
173
  couponId?: string | undefined;
174
174
  }, {
175
175
  code: string;
176
176
  type: "auto" | "coupon" | "deal";
177
177
  discountAmount: number;
178
- scope?: "admin" | "seller" | undefined;
178
+ scope?: "seller" | "admin" | undefined;
179
179
  storeId?: string | undefined;
180
180
  couponId?: string | undefined;
181
181
  }>;
@@ -326,14 +326,14 @@ export declare const orderFirestoreSchema: z.ZodObject<{
326
326
  code: string;
327
327
  type: "auto" | "coupon" | "deal";
328
328
  discountAmount: number;
329
- scope?: "admin" | "seller" | undefined;
329
+ scope?: "seller" | "admin" | undefined;
330
330
  storeId?: string | undefined;
331
331
  couponId?: string | undefined;
332
332
  }, {
333
333
  code: string;
334
334
  type: "auto" | "coupon" | "deal";
335
335
  discountAmount: number;
336
- scope?: "admin" | "seller" | undefined;
336
+ scope?: "seller" | "admin" | undefined;
337
337
  storeId?: string | undefined;
338
338
  couponId?: string | undefined;
339
339
  }>, "many">>;
@@ -464,9 +464,9 @@ export declare const orderFirestoreSchema: z.ZodObject<{
464
464
  overrideReason: z.ZodOptional<z.ZodString>;
465
465
  }, "strip", z.ZodTypeAny, {
466
466
  type: "full" | "partial";
467
+ reason: string;
467
468
  amount: number;
468
469
  refundId: string;
469
- reason: string;
470
470
  refundedAt: string | Date | z.objectOutputType<{
471
471
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
472
472
  }, z.ZodTypeAny, "passthrough">;
@@ -479,9 +479,9 @@ export declare const orderFirestoreSchema: z.ZodObject<{
479
479
  overrideReason?: string | undefined;
480
480
  }, {
481
481
  type: "full" | "partial";
482
+ reason: string;
482
483
  amount: number;
483
484
  refundId: string;
484
- reason: string;
485
485
  refundedAt: string | Date | z.objectInputType<{
486
486
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
487
487
  }, z.ZodTypeAny, "passthrough">;
@@ -622,7 +622,7 @@ export declare const orderFirestoreSchema: z.ZodObject<{
622
622
  code: string;
623
623
  type: "auto" | "coupon" | "deal";
624
624
  discountAmount: number;
625
- scope?: "admin" | "seller" | undefined;
625
+ scope?: "seller" | "admin" | undefined;
626
626
  storeId?: string | undefined;
627
627
  couponId?: string | undefined;
628
628
  }[] | undefined;
@@ -654,9 +654,9 @@ export declare const orderFirestoreSchema: z.ZodObject<{
654
654
  paymentBatchId?: string | undefined;
655
655
  refunds?: {
656
656
  type: "full" | "partial";
657
+ reason: string;
657
658
  amount: number;
658
659
  refundId: string;
659
- reason: string;
660
660
  refundedAt: string | Date | z.objectOutputType<{
661
661
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
662
662
  }, z.ZodTypeAny, "passthrough">;
@@ -770,7 +770,7 @@ export declare const orderFirestoreSchema: z.ZodObject<{
770
770
  code: string;
771
771
  type: "auto" | "coupon" | "deal";
772
772
  discountAmount: number;
773
- scope?: "admin" | "seller" | undefined;
773
+ scope?: "seller" | "admin" | undefined;
774
774
  storeId?: string | undefined;
775
775
  couponId?: string | undefined;
776
776
  }[] | undefined;
@@ -802,9 +802,9 @@ export declare const orderFirestoreSchema: z.ZodObject<{
802
802
  paymentBatchId?: string | undefined;
803
803
  refunds?: {
804
804
  type: "full" | "partial";
805
+ reason: string;
805
806
  amount: number;
806
807
  refundId: string;
807
- reason: string;
808
808
  refundedAt: string | Date | z.objectInputType<{
809
809
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
810
810
  }, z.ZodTypeAny, "passthrough">;
@@ -7,6 +7,14 @@
7
7
  import { BaseRepository, parseSieveDateValue, type SieveModel, type FirebaseSieveResult } from "../../../providers/db-firebase";
8
8
  import type { DocumentReference, WriteResult } from "firebase-admin/firestore";
9
9
  import type { PayoutDocument, PayoutCreateInput, PayoutRefundDeduction, PayoutStatus, PayoutUpdateInput } from "../schemas";
10
+ import { type HistoryActor } from "../../../_internal/shared/history/index";
11
+ /** Who/why for a payout write that lands in `statusHistory`. */
12
+ export interface PayoutWriteContext {
13
+ actor?: HistoryActor;
14
+ trigger?: string;
15
+ reason?: string;
16
+ note?: string;
17
+ }
10
18
  export declare class PayoutRepository extends BaseRepository<PayoutDocument> {
11
19
  constructor();
12
20
  /** Decrypt PII fields on a payout document after Firestore read */
@@ -46,12 +54,17 @@ export declare class PayoutRepository extends BaseRepository<PayoutDocument> {
46
54
  data: PayoutDocument;
47
55
  }>>;
48
56
  /**
49
- * Update payout status (admin action)
57
+ * Update payout status (admin action).
58
+ *
59
+ * `prior` keeps the timeline entry read-free — the admin route already
60
+ * fetches the payout to authorise the change (Rule #6).
50
61
  */
51
- updateStatus(payoutId: string, status: PayoutStatus, extra?: PayoutUpdateInput): Promise<PayoutDocument>;
52
- markProcessing(ref: DocumentReference): Promise<WriteResult>;
53
- recordSuccess(ref: DocumentReference, razorpayPayoutId: string, razorpayStatus: string): Promise<WriteResult>;
54
- recordFailure(ref: DocumentReference, failureCount: number, reason: string, isFinal: boolean): Promise<WriteResult>;
62
+ updateStatus(payoutId: string, status: PayoutStatus, extra?: PayoutUpdateInput, ctx?: PayoutWriteContext, prior?: PayoutDocument | null): Promise<PayoutDocument>;
63
+ markProcessing(ref: DocumentReference, ctx?: PayoutWriteContext, prior?: PayoutDocument | null): Promise<WriteResult>;
64
+ recordSuccess(ref: DocumentReference, razorpayPayoutId: string, razorpayStatus: string, ctx?: PayoutWriteContext, prior?: PayoutDocument | null): Promise<WriteResult>;
65
+ recordFailure(ref: DocumentReference, failureCount: number, reason: string, isFinal: boolean, ctx?: PayoutWriteContext, prior?: PayoutDocument | null): Promise<WriteResult>;
66
+ /** Append a timeline entry when the patch touches a tracked field. */
67
+ private withPayoutHistory;
55
68
  /**
56
69
  * Get all order IDs that have already been paid out for a seller.
57
70
  * Used to avoid double-paying the same orders.