@mohasinac/appkit 4.41.0 → 4.41.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 (166) hide show
  1. package/dist/features/shell/SectionForm.js +44 -5
  2. package/package.json +2 -2
  3. package/dist/_internal/server/features/classified/actions.d.ts +0 -12
  4. package/dist/_internal/server/features/classified/actions.js +0 -33
  5. package/dist/_internal/server/features/digital-code/actions.d.ts +0 -12
  6. package/dist/_internal/server/features/digital-code/actions.js +0 -33
  7. package/dist/_internal/server/features/homepage/data.d.ts +0 -18
  8. package/dist/_internal/server/features/homepage/data.js +0 -70
  9. package/dist/_internal/server/features/live/actions.d.ts +0 -6
  10. package/dist/_internal/server/features/live/actions.js +0 -27
  11. package/dist/_internal/server/features/messages/actions.d.ts +0 -6
  12. package/dist/_internal/server/features/messages/actions.js +0 -6
  13. package/dist/_internal/server/features/messages/data.d.ts +0 -10
  14. package/dist/_internal/server/features/messages/data.js +0 -24
  15. package/dist/_internal/server/features/messages/index.d.ts +0 -2
  16. package/dist/_internal/server/features/messages/index.js +0 -2
  17. package/dist/_internal/server/jobs/core/testerSandboxRefresh.d.ts +0 -12
  18. package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +0 -112
  19. package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.d.ts +0 -2
  20. package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.js +0 -2
  21. package/dist/_internal/shared/features/homepage/section-gate.d.ts +0 -14
  22. package/dist/_internal/shared/features/homepage/section-gate.js +0 -51
  23. package/dist/_internal/shared/features/lottery/config-form.d.ts +0 -58
  24. package/dist/_internal/shared/features/lottery/config-form.js +0 -71
  25. package/dist/client/api/useFeatureFlags.d.ts +0 -22
  26. package/dist/client/api/useFeatureFlags.js +0 -27
  27. package/dist/features/account/address-validation.d.ts +0 -20
  28. package/dist/features/account/address-validation.js +0 -35
  29. package/dist/features/account/components/ChatList.d.ts +0 -14
  30. package/dist/features/account/components/ChatList.js +0 -9
  31. package/dist/features/account/components/ChatWindow.d.ts +0 -17
  32. package/dist/features/account/components/ChatWindow.js +0 -6
  33. package/dist/features/account/components/MessagesView.d.ts +0 -16
  34. package/dist/features/account/components/MessagesView.js +0 -5
  35. package/dist/features/account/components/UserOffersView.d.ts +0 -10
  36. package/dist/features/account/components/UserOffersView.js +0 -5
  37. package/dist/features/account/constants/addresses.d.ts +0 -13
  38. package/dist/features/account/constants/addresses.js +0 -43
  39. package/dist/features/account/hooks/useAddressForm.d.ts +0 -24
  40. package/dist/features/account/hooks/useAddressForm.js +0 -58
  41. package/dist/features/admin/actions/chat-actions.d.ts +0 -29
  42. package/dist/features/admin/actions/chat-actions.js +0 -101
  43. package/dist/features/admin/components/AdminFeatureFlagsView.d.ts +0 -9
  44. package/dist/features/admin/components/AdminFeatureFlagsView.js +0 -109
  45. package/dist/features/admin/components/AdminSiteView.d.ts +0 -10
  46. package/dist/features/admin/components/AdminSiteView.js +0 -52
  47. package/dist/features/admin/hooks/useChat.d.ts +0 -24
  48. package/dist/features/admin/hooks/useChat.js +0 -100
  49. package/dist/features/admin/permission-map.d.ts +0 -3
  50. package/dist/features/admin/permission-map.js +0 -26
  51. package/dist/features/admin/repository/chat.repository.d.ts +0 -50
  52. package/dist/features/admin/repository/chat.repository.js +0 -223
  53. package/dist/features/auth/actions/password-change-otp-actions.d.ts +0 -18
  54. package/dist/features/auth/actions/password-change-otp-actions.js +0 -88
  55. package/dist/features/auth/actions/realtime-token-actions.d.ts +0 -11
  56. package/dist/features/auth/actions/realtime-token-actions.js +0 -38
  57. package/dist/features/auth/password-change-otp.d.ts +0 -34
  58. package/dist/features/auth/password-change-otp.js +0 -56
  59. package/dist/features/classified/components/ClassifiedContactSellerPanel.d.ts +0 -12
  60. package/dist/features/classified/components/ClassifiedContactSellerPanel.js +0 -49
  61. package/dist/features/events/api/route.d.ts +0 -12
  62. package/dist/features/events/api/route.js +0 -90
  63. package/dist/features/faq/api/route.d.ts +0 -11
  64. package/dist/features/faq/api/route.js +0 -52
  65. package/dist/features/faq/hooks/useFAQs.d.ts +0 -22
  66. package/dist/features/faq/hooks/useFAQs.js +0 -41
  67. package/dist/features/media/upload/video-poster.test.d.ts +0 -1
  68. package/dist/features/media/upload/video-poster.test.js +0 -42
  69. package/dist/features/messages/actions/index.d.ts +0 -2
  70. package/dist/features/messages/actions/index.js +0 -2
  71. package/dist/features/messages/actions/messages-actions.d.ts +0 -15
  72. package/dist/features/messages/actions/messages-actions.js +0 -33
  73. package/dist/features/messages/actions/ping-rtdb.d.ts +0 -2
  74. package/dist/features/messages/actions/ping-rtdb.js +0 -26
  75. package/dist/features/messages/hooks/useConversation.d.ts +0 -22
  76. package/dist/features/messages/hooks/useConversation.js +0 -155
  77. package/dist/features/messages/hooks/useConversations.d.ts +0 -15
  78. package/dist/features/messages/hooks/useConversations.js +0 -104
  79. package/dist/features/messages/index.d.ts +0 -6
  80. package/dist/features/messages/index.js +0 -4
  81. package/dist/features/messages/realtime.d.ts +0 -32
  82. package/dist/features/messages/realtime.js +0 -39
  83. package/dist/features/messages/repository/conversations.repository.d.ts +0 -71
  84. package/dist/features/messages/repository/conversations.repository.js +0 -284
  85. package/dist/features/messages/schemas/firestore.d.ts +0 -34
  86. package/dist/features/messages/schemas/firestore.js +0 -16
  87. package/dist/features/messages/schemas/index.d.ts +0 -177
  88. package/dist/features/messages/schemas/index.js +0 -35
  89. package/dist/features/messages/server.d.ts +0 -6
  90. package/dist/features/messages/server.js +0 -6
  91. package/dist/features/products/components/MakeOfferForm.d.ts +0 -20
  92. package/dist/features/products/components/MakeOfferForm.js +0 -9
  93. package/dist/features/reviews/api/route.d.ts +0 -21
  94. package/dist/features/reviews/api/route.js +0 -174
  95. package/dist/features/seller/api/offers/route.d.ts +0 -17
  96. package/dist/features/seller/api/offers/route.js +0 -63
  97. package/dist/features/seller/components/SellerAuctionsView.d.ts +0 -9
  98. package/dist/features/seller/components/SellerAuctionsView.js +0 -113
  99. package/dist/features/seller/components/SellerOffersPanel.d.ts +0 -21
  100. package/dist/features/seller/components/SellerOffersPanel.js +0 -160
  101. package/dist/features/seller/components/SellerPreOrdersView.d.ts +0 -6
  102. package/dist/features/seller/components/SellerPreOrdersView.js +0 -148
  103. package/dist/features/seller/components/SellerPrizeDrawsView.d.ts +0 -6
  104. package/dist/features/seller/components/SellerPrizeDrawsView.js +0 -152
  105. package/dist/features/seller/permission-map.d.ts +0 -5
  106. package/dist/features/seller/permission-map.js +0 -17
  107. package/dist/features/shell/StepForm.d.ts +0 -80
  108. package/dist/features/shell/StepForm.js +0 -136
  109. package/dist/features/stores/api/route.d.ts +0 -12
  110. package/dist/features/stores/api/route.js +0 -110
  111. package/dist/features/tester/seed-data/authored/account-auth__testing-program--admin.d.ts +0 -2
  112. package/dist/features/tester/seed-data/authored/account-auth__testing-program--admin.js +0 -33
  113. package/dist/features/tester/seed-data/authored/account-auth__testing-program.d.ts +0 -2
  114. package/dist/features/tester/seed-data/authored/account-auth__testing-program.js +0 -138
  115. package/dist/features/tester/seed-data/bids-tester-seed-data.d.ts +0 -2
  116. package/dist/features/tester/seed-data/bids-tester-seed-data.js +0 -37
  117. package/dist/features/tester/seed-data/blog-tester-seed-data.d.ts +0 -2
  118. package/dist/features/tester/seed-data/blog-tester-seed-data.js +0 -40
  119. package/dist/features/tester/seed-data/carts-tester-seed-data.d.ts +0 -3
  120. package/dist/features/tester/seed-data/carts-tester-seed-data.js +0 -70
  121. package/dist/features/tester/seed-data/categories-tester-seed-data.d.ts +0 -2
  122. package/dist/features/tester/seed-data/categories-tester-seed-data.js +0 -218
  123. package/dist/features/tester/seed-data/coupons-tester-seed-data.d.ts +0 -2
  124. package/dist/features/tester/seed-data/coupons-tester-seed-data.js +0 -76
  125. package/dist/features/tester/seed-data/events-tester-seed-data.d.ts +0 -2
  126. package/dist/features/tester/seed-data/events-tester-seed-data.js +0 -193
  127. package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +0 -2
  128. package/dist/features/tester/seed-data/offers-tester-seed-data.js +0 -191
  129. package/dist/features/tester/seed-data/orders-tester-seed-data.d.ts +0 -2
  130. package/dist/features/tester/seed-data/orders-tester-seed-data.js +0 -329
  131. package/dist/features/tester/seed-data/products-tester-seed-data.d.ts +0 -2
  132. package/dist/features/tester/seed-data/products-tester-seed-data.js +0 -815
  133. package/dist/features/tester/seed-data/stores-tester-seed-data.d.ts +0 -2
  134. package/dist/features/tester/seed-data/stores-tester-seed-data.js +0 -79
  135. package/dist/features/wishlist/components/WishlistToggleButton.d.ts +0 -12
  136. package/dist/features/wishlist/components/WishlistToggleButton.js +0 -36
  137. package/dist/monitoring/analytics.d.ts +0 -80
  138. package/dist/monitoring/analytics.js +0 -176
  139. package/dist/next/middleware/pii-redactor.d.ts +0 -32
  140. package/dist/next/middleware/pii-redactor.js +0 -84
  141. package/dist/providers/db-firebase/realtime.d.ts +0 -42
  142. package/dist/providers/db-firebase/realtime.js +0 -138
  143. package/dist/security/rbac/Can.d.ts +0 -31
  144. package/dist/security/rbac/Can.js +0 -22
  145. package/dist/security/rbac/default-roles.d.ts +0 -2
  146. package/dist/security/rbac/default-roles.js +0 -83
  147. package/dist/security/rbac/hook.d.ts +0 -20
  148. package/dist/security/rbac/hook.js +0 -30
  149. package/dist/security/rbac/index.d.ts +0 -8
  150. package/dist/security/rbac/index.js +0 -6
  151. package/dist/security/rbac/middleware.d.ts +0 -51
  152. package/dist/security/rbac/middleware.js +0 -51
  153. package/dist/security/rbac/resolver.d.ts +0 -5
  154. package/dist/security/rbac/resolver.js +0 -38
  155. package/dist/security/rbac/server.d.ts +0 -24
  156. package/dist/security/rbac/server.js +0 -42
  157. package/dist/security/rbac/types.d.ts +0 -22
  158. package/dist/security/rbac/types.js +0 -2
  159. package/dist/seed/conversations-seed-data.d.ts +0 -2
  160. package/dist/seed/conversations-seed-data.js +0 -292
  161. package/dist/utils/filter.helper.d.ts +0 -18
  162. package/dist/utils/filter.helper.js +0 -21
  163. package/dist/utils/pagination.helper.d.ts +0 -23
  164. package/dist/utils/pagination.helper.js +0 -24
  165. package/dist/utils/search-tokens.d.ts +0 -2
  166. package/dist/utils/search-tokens.js +0 -21
@@ -158,6 +158,20 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
158
158
  // validator on every render and re-fire the effect below in a loop.
159
159
  const onValidationChangeRef = useRef(onValidationChange);
160
160
  onValidationChangeRef.current = onValidationChange;
161
+ /**
162
+ * Are two field-error maps the same set of messages?
163
+ *
164
+ * Flat string maps, so a key sweep is exact — no deep compare needed. This is
165
+ * what lets the validation effect depend on `values` (a new object every
166
+ * render) without looping: an unchanged result returns the previous object
167
+ * and React bails out of the update.
168
+ */
169
+ const sameErrors = (a, b) => {
170
+ const ak = Object.keys(a);
171
+ if (ak.length !== Object.keys(b).length)
172
+ return false;
173
+ return ak.every((k) => a[k] === b[k]);
174
+ };
161
175
  /** Section ids holding at least one error, in render order. */
162
176
  const erroringSectionIds = useCallback((errs) => {
163
177
  const keys = Object.keys(errs);
@@ -176,8 +190,8 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
176
190
  // applyZodIssues' incremental setFieldError-per-issue cannot do.
177
191
  const parsed = activeSchema.safeParse(nextValues);
178
192
  if (parsed.success) {
179
- setFieldErrors({});
180
- setForceOpenIds([]);
193
+ setFieldErrors((prev) => (Object.keys(prev).length === 0 ? prev : {}));
194
+ setForceOpenIds((prev) => (prev.length === 0 ? prev : []));
181
195
  onValidationChangeRef.current?.({}, fieldToSectionIndex);
182
196
  return;
183
197
  }
@@ -187,7 +201,12 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
187
201
  continue;
188
202
  next[issue.path.map(String).join(".")] = issue.message;
189
203
  }
190
- setFieldErrors(next);
204
+ /*
205
+ * Return the PREVIOUS object when nothing actually changed. `values` is a
206
+ * fresh object on every render, so the effect below re-validates on every
207
+ * render; handing back an equal-but-new errors map would setState in a loop.
208
+ */
209
+ setFieldErrors((prev) => (sameErrors(prev, next) ? prev : next));
191
210
  // Release a force-opened section as soon as ITS OWN errors clear, rather
192
211
  // than waiting for the whole form to become valid — otherwise fixing
193
212
  // section 1 leaves it pinned open while the user works through section 5.
@@ -198,10 +217,30 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
198
217
  });
199
218
  onValidationChangeRef.current?.(next, fieldToSectionIndex);
200
219
  }, [schema, inheritedSchema, fieldToSectionIndex, erroringSectionIds]);
220
+ /*
221
+ * 🛑 `values` MUST be a dependency, and leaving it out cost the entire admin
222
+ * coupon editor.
223
+ *
224
+ * This effect used to run once on mount with an eslint-disable, so it
225
+ * validated the INITIAL draft. On an edit form that draft is the empty
226
+ * defaults: the record arrives asynchronously (useQuery → useEffect →
227
+ * patch()), and `patch` is not `handleFieldChange`, so the only other caller
228
+ * of runValidation never fired. The mount-time errors therefore outlived the
229
+ * data that fixed them.
230
+ *
231
+ * Measured on production 2026-09-15: opening ARENA25 showed "3 issues" with
232
+ * "A discount value is required." under a field containing 25 and "Give the
233
+ * campaign a name." under a populated name — the three fields that are empty
234
+ * BEFORE the load. Save then refused with "Coupon: This field is required"
235
+ * and issued no PATCH at all, so no coupon could be edited through the UI.
236
+ *
237
+ * Re-validating per render is safe because runValidation returns the previous
238
+ * errors object when nothing changed (see `sameErrors`); without that this
239
+ * would setState in a loop, which is presumably why the dep was dropped.
240
+ */
201
241
  useEffect(() => {
202
242
  runValidation(values);
203
- // eslint-disable-next-line react-hooks/exhaustive-deps
204
- }, [schema, inheritedSchema, fieldToSectionIndex]);
243
+ }, [runValidation, values]);
205
244
  const handleFieldChange = useCallback((partial) => {
206
245
  onChange(partial);
207
246
  runValidation({ ...values, ...partial });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.41.0",
3
+ "version": "4.41.1",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -151,7 +151,7 @@
151
151
  },
152
152
  "scripts": {
153
153
  "postinstall": "node scripts/dedupe-peer-deps.mjs",
154
- "build": "node scripts/dedupe-peer-deps.mjs && node -e \"const fs=require('fs');try{fs.rmSync('tsconfig.build.tsbuildinfo')}catch(e){}\" && tsc -p tsconfig.build.json && node scripts/copy-assets.mjs && tailwindcss -i src/tailwind-input.css -o dist/tailwind-utilities.css --minify && node scripts/bundle-css.mjs && node scripts/verify-css-build.mjs",
154
+ "build": "node scripts/dedupe-peer-deps.mjs && node -e \"const fs=require('fs');for(const t of ['tsconfig.build.tsbuildinfo','dist']){try{fs.rmSync(t,{recursive:true})}catch(e){}}\" && tsc -p tsconfig.build.json && node scripts/copy-assets.mjs && tailwindcss -i src/tailwind-input.css -o dist/tailwind-utilities.css --minify && node scripts/bundle-css.mjs && node scripts/verify-css-build.mjs",
155
155
  "prepublishOnly": "npm run build",
156
156
  "watch": "concurrently --kill-others --restart-tries 0 --names \"ts,css\" \"tsc -p tsconfig.build.json --watch\" \"npm run watch:css\"",
157
157
  "watch:css": "tailwindcss -i src/tailwind-input.css -o dist/tailwind-utilities.css --watch",
@@ -1,12 +0,0 @@
1
- import { type ActionResult } from "@mohasinac/appkit/server";
2
- import type { ConversationDocument } from "../../../../features/messages/schemas/firestore";
3
- export interface StartConversationInput {
4
- productId: string;
5
- }
6
- /**
7
- * Open (or resume) a buyer↔seller conversation for a classified listing.
8
- * Called when the buyer clicks "Contact Seller" on the classified PDP.
9
- * Idempotent — returns the existing conversation if one already exists for
10
- * this buyer × store × product tuple.
11
- */
12
- export declare function startClassifiedConversationAction(input: StartConversationInput): Promise<ActionResult<ConversationDocument>>;
@@ -1,33 +0,0 @@
1
- "use server";
2
- import { wrapAction } from "@mohasinac/appkit/server";
3
- import { conversationsRepository } from "../../../../features/messages/repository/conversations.repository";
4
- import { requireRoleUser } from "../../../../providers/auth-firebase/helpers";
5
- import { storeRepository, productRepository } from "../../../../repositories";
6
- /**
7
- * Open (or resume) a buyer↔seller conversation for a classified listing.
8
- * Called when the buyer clicks "Contact Seller" on the classified PDP.
9
- * Idempotent — returns the existing conversation if one already exists for
10
- * this buyer × store × product tuple.
11
- */
12
- export async function startClassifiedConversationAction(input) {
13
- return wrapAction(async () => {
14
- const user = await requireRoleUser(["user", "buyer", "seller", "admin"]);
15
- const product = await productRepository.findByIdOrSlug(input.productId);
16
- if (!product || product.listingType !== "classified") {
17
- throw new Error("Product not found or not a classified listing");
18
- }
19
- const store = await storeRepository.findById(product.storeId);
20
- if (!store) {
21
- throw new Error("Store not found");
22
- }
23
- return conversationsRepository.findOrCreateByContext({
24
- buyerId: user.uid,
25
- buyerDisplayName: user.name ?? user.email ?? user.uid,
26
- storeId: product.storeId,
27
- storeName: store.storeName,
28
- sellerDisplayName: store.storeName,
29
- productId: product.id,
30
- productTitle: product.title,
31
- });
32
- });
33
- }
@@ -1,12 +0,0 @@
1
- import { type ActionResult } from "@mohasinac/appkit/server";
2
- import type { ConversationDocument } from "../../../../features/messages/schemas/firestore";
3
- export interface StartDigitalCodeConversationInput {
4
- productId: string;
5
- }
6
- /**
7
- * Open a buyer↔seller conversation for a digital-code listing (questions
8
- * about platform/region/edition before purchase). Post-purchase code reveal
9
- * is handled by the existing `/api/orders/[id]/code` endpoint, not a server
10
- * action — the code reveal panel lives in the order detail view.
11
- */
12
- export declare function startDigitalCodeConversationAction(input: StartDigitalCodeConversationInput): Promise<ActionResult<ConversationDocument>>;
@@ -1,33 +0,0 @@
1
- "use server";
2
- import { wrapAction } from "@mohasinac/appkit/server";
3
- import { conversationsRepository } from "../../../../features/messages/repository/conversations.repository";
4
- import { requireRoleUser } from "../../../../providers/auth-firebase/helpers";
5
- import { storeRepository, productRepository } from "../../../../repositories";
6
- /**
7
- * Open a buyer↔seller conversation for a digital-code listing (questions
8
- * about platform/region/edition before purchase). Post-purchase code reveal
9
- * is handled by the existing `/api/orders/[id]/code` endpoint, not a server
10
- * action — the code reveal panel lives in the order detail view.
11
- */
12
- export async function startDigitalCodeConversationAction(input) {
13
- return wrapAction(async () => {
14
- const user = await requireRoleUser(["user", "buyer", "seller", "admin"]);
15
- const product = await productRepository.findByIdOrSlug(input.productId);
16
- if (!product || product.listingType !== "digital-code") {
17
- throw new Error("Product not found or not a digital-code listing");
18
- }
19
- const store = await storeRepository.findById(product.storeId);
20
- if (!store) {
21
- throw new Error("Store not found");
22
- }
23
- return conversationsRepository.findOrCreateByContext({
24
- buyerId: user.uid,
25
- buyerDisplayName: user.name ?? user.email ?? user.uid,
26
- storeId: product.storeId,
27
- storeName: store.storeName,
28
- sellerDisplayName: store.storeName,
29
- productId: product.id,
30
- productTitle: product.title,
31
- });
32
- });
33
- }
@@ -1,18 +0,0 @@
1
- export declare const getHomepageSections: () => Promise<import("../../../..").HomepageSectionDocument[]>;
2
- export declare const getHeroCarouselSlides: () => Promise<never[] | import("../../../..").CarouselSlideDocument[]>;
3
- /**
4
- * One parallel fetch returning every piece of data the homepage sections need.
5
- * Page components call this once; React.cache() deduplicates per request.
6
- */
7
- export declare const getHomepageInitial: () => Promise<{
8
- sections: import("../../../..").HomepageSectionDocument[];
9
- carouselSlides: never[] | import("../../../..").CarouselSlideDocument[];
10
- featuredProducts: never[] | import("../../../..").ProductDocument[];
11
- activeAuctions: never[] | import("../../../..").ProductDocument[];
12
- activePreOrders: never[] | import("../../../..").ProductDocument[];
13
- activeBrands: never[] | import("../../../..").CategoryDocument[];
14
- featuredReviews: never[] | import("../../../..").ReviewDocument[];
15
- recentBlogPosts: never[] | import("../../../..").BlogPostDocument[];
16
- activeEvents: never[] | import("../../../..").EventDocument[];
17
- featuredCategories: never[] | import("../../../..").CategoryDocument[];
18
- }>;
@@ -1,70 +0,0 @@
1
- import { cache } from "react";
2
- import { homepageSectionsRepository, carouselRepository, productRepository, reviewRepository, blogRepository, eventRepository, categoriesRepository, siteSettingsRepository, } from "../../../../repositories";
3
- import { HOMEPAGE_FEATURED_REVIEWS_LIMIT, HOMEPAGE_RECENT_BLOG_POSTS_LIMIT, } from "../../../shared/features/homepage/config";
4
- /** Section types that require a feature flag to be enabled to show on the homepage. */
5
- const SECTION_FEATURE_GATE = {
6
- auctions: "auctions",
7
- "pre-orders": "preOrders",
8
- events: "events",
9
- "event-raffles": "events",
10
- "prize-draws": "events",
11
- "blog-articles": "blog",
12
- };
13
- export const getHomepageSections = cache(async () => {
14
- const [sections, settings] = await Promise.all([
15
- homepageSectionsRepository.getEnabledSections().catch(() => []),
16
- siteSettingsRepository.getSingleton().catch(() => null),
17
- ]);
18
- const flags = settings?.featureFlags;
19
- if (!flags)
20
- return sections;
21
- return sections.filter((s) => {
22
- const flagKey = SECTION_FEATURE_GATE[s.type];
23
- if (!flagKey)
24
- return true;
25
- return flags[flagKey] !== false;
26
- });
27
- });
28
- export const getHeroCarouselSlides = cache(async () => {
29
- return carouselRepository.getActiveSlides().catch(() => []);
30
- });
31
- /**
32
- * One parallel fetch returning every piece of data the homepage sections need.
33
- * Page components call this once; React.cache() deduplicates per request.
34
- */
35
- export const getHomepageInitial = cache(async () => {
36
- const [rawSections, settings, carouselSlides, featuredProducts, activeAuctions, activePreOrders, activeBrands, featuredReviews, recentBlogPosts, activeEvents, featuredCategories,] = await Promise.all([
37
- homepageSectionsRepository.getEnabledSections().catch(() => []),
38
- siteSettingsRepository.getSingleton().catch(() => null),
39
- carouselRepository.getActiveSlides().catch(() => []),
40
- productRepository.findFeatured().catch(() => []),
41
- productRepository.findActiveAuctions().catch(() => []),
42
- productRepository.findActivePreOrders().catch(() => []),
43
- categoriesRepository.findActiveBrands().catch(() => []),
44
- reviewRepository.findFeatured(HOMEPAGE_FEATURED_REVIEWS_LIMIT).catch(() => []),
45
- blogRepository.listPublished({}, { page: 1, pageSize: HOMEPAGE_RECENT_BLOG_POSTS_LIMIT, sorts: "-publishedAt" }).then((r) => r.items ?? []).catch(() => []),
46
- eventRepository.listActive().catch(() => []),
47
- categoriesRepository.getFeaturedCategories().catch(() => []),
48
- ]);
49
- const flags = settings?.featureFlags;
50
- const sections = flags
51
- ? rawSections.filter((s) => {
52
- const flagKey = SECTION_FEATURE_GATE[s.type];
53
- if (!flagKey)
54
- return true;
55
- return flags[flagKey] !== false;
56
- })
57
- : rawSections;
58
- return {
59
- sections,
60
- carouselSlides,
61
- featuredProducts,
62
- activeAuctions,
63
- activePreOrders,
64
- activeBrands,
65
- featuredReviews,
66
- recentBlogPosts,
67
- activeEvents,
68
- featuredCategories,
69
- };
70
- });
@@ -1,6 +0,0 @@
1
- import { type ActionResult } from "@mohasinac/appkit/server";
2
- import type { ConversationDocument } from "../../../../features/messages/schemas/firestore";
3
- export interface StartLiveConversationInput {
4
- productId: string;
5
- }
6
- export declare function startLiveConversationAction(input: StartLiveConversationInput): Promise<ActionResult<ConversationDocument>>;
@@ -1,27 +0,0 @@
1
- "use server";
2
- import { wrapAction } from "@mohasinac/appkit/server";
3
- import { conversationsRepository } from "../../../../features/messages/repository/conversations.repository";
4
- import { requireRoleUser } from "../../../../providers/auth-firebase/helpers";
5
- import { storeRepository, productRepository } from "../../../../repositories";
6
- export async function startLiveConversationAction(input) {
7
- return wrapAction(async () => {
8
- const user = await requireRoleUser(["user", "buyer", "seller", "admin"]);
9
- const product = await productRepository.findByIdOrSlug(input.productId);
10
- if (!product || product.listingType !== "live") {
11
- throw new Error("Product not found or not a live-item listing");
12
- }
13
- const store = await storeRepository.findById(product.storeId);
14
- if (!store) {
15
- throw new Error("Store not found");
16
- }
17
- return conversationsRepository.findOrCreateByContext({
18
- buyerId: user.uid,
19
- buyerDisplayName: user.name ?? user.email ?? user.uid,
20
- storeId: product.storeId,
21
- storeName: store.storeName,
22
- sellerDisplayName: store.storeName,
23
- productId: product.id,
24
- productTitle: product.title,
25
- });
26
- });
27
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * Messages server actions — thin re-exports. Keeps RTDB ping-channel behaviour
3
- * (firestore canonical + RTDB notification) in the source action so consumers
4
- * cannot bypass it.
5
- */
6
- export { sendMessage as sendMessageAction, markConversationRead as markConversationReadAction, MESSAGE_MAX_LENGTH, } from "../../../../features/messages/actions/messages-actions";
@@ -1,6 +0,0 @@
1
- /**
2
- * Messages server actions — thin re-exports. Keeps RTDB ping-channel behaviour
3
- * (firestore canonical + RTDB notification) in the source action so consumers
4
- * cannot bypass it.
5
- */
6
- export { sendMessage as sendMessageAction, markConversationRead as markConversationReadAction, MESSAGE_MAX_LENGTH, } from "../../../../features/messages/actions/messages-actions";
@@ -1,10 +0,0 @@
1
- /**
2
- * Messages data layer — server-only fetchers with React.cache dedup.
3
- * RTDB ping channel is preserved (the actions module owns that side effect).
4
- */
5
- export interface MessagesDataOptions {
6
- _reserved?: never;
7
- }
8
- export declare const listConversationsForBuyer: (buyerId: string, _opts?: MessagesDataOptions) => Promise<import("../../../..").ConversationDocument[]>;
9
- export declare const listConversationsForStore: (storeId: string, _opts?: MessagesDataOptions) => Promise<import("../../../..").ConversationDocument[]>;
10
- export declare const getConversation: (convId: string, _opts?: MessagesDataOptions) => Promise<import("../../../..").ConversationDocument | null>;
@@ -1,24 +0,0 @@
1
- /**
2
- * Messages data layer — server-only fetchers with React.cache dedup.
3
- * RTDB ping channel is preserved (the actions module owns that side effect).
4
- */
5
- import { cache } from "react";
6
- import { listConversationsForBuyer as _listConversationsForBuyer, listConversationsForStore as _listConversationsForStore, getConversation as _getConversation, } from "../../../../features/messages/actions/messages-actions";
7
- export const listConversationsForBuyer = cache(async (buyerId, _opts) => {
8
- void _opts;
9
- if (!buyerId)
10
- return [];
11
- return _listConversationsForBuyer(buyerId);
12
- });
13
- export const listConversationsForStore = cache(async (storeId, _opts) => {
14
- void _opts;
15
- if (!storeId)
16
- return [];
17
- return _listConversationsForStore(storeId);
18
- });
19
- export const getConversation = cache(async (convId, _opts) => {
20
- void _opts;
21
- if (!convId)
22
- return null;
23
- return _getConversation(convId);
24
- });
@@ -1,2 +0,0 @@
1
- export * from "./data";
2
- export * from "./actions";
@@ -1,2 +0,0 @@
1
- export * from "./data";
2
- export * from "./actions";
@@ -1,12 +0,0 @@
1
- import type { JobContext } from "../runtime/types";
2
- /**
3
- * Reverts every live tester-sandbox fixture back to its canonical seeded
4
- * shape (undoing edits made through admin/seller forms) and prunes any
5
- * isTestData doc that isn't one of the known fixtures (something a tester
6
- * created during QA, e.g. a cloned product). Runs every 4 hours — distinct
7
- * from `runTesterSandboxCleanup`, which only deletes docs whose 7-day
8
- * `testDataExpiresAt` TTL has actually lapsed and never touches still-live
9
- * docs' fields. Scoped to the tester sandbox only (`isTestData: true`) —
10
- * the permanent Beyblade catalog real visitors browse is never touched.
11
- */
12
- export declare function runTesterSandboxRefresh(ctx: JobContext): Promise<void>;
@@ -1,112 +0,0 @@
1
- import { batchDelete, getTestDataRefs } from "../handlers/_helpers";
2
- import { BID_FIELDS } from "../../../../constants/field-names";
3
- import { encryptPiiFields } from "../../../../security/pii-encrypt";
4
- import { BID_PII_FIELDS, OFFER_PII_FIELDS, } from "../../../../security/pii-schemas";
5
- import { SANDBOX_COLLECTIONS } from "./testerSandboxCleanup";
6
- // Direct imports from each defining module (not the seed-data barrel) —
7
- // this is internal appkit code, not part of the package's public API surface.
8
- import { categoriesTesterSeedData } from "../../../../features/tester/seed-data/categories-tester-seed-data";
9
- import { storesTesterSeedData } from "../../../../features/tester/seed-data/stores-tester-seed-data";
10
- import { productsTesterSeedData } from "../../../../features/tester/seed-data/products-tester-seed-data";
11
- import { blogTesterSeedData } from "../../../../features/tester/seed-data/blog-tester-seed-data";
12
- import { eventsTesterSeedData } from "../../../../features/tester/seed-data/events-tester-seed-data";
13
- import { offersTesterSeedData } from "../../../../features/tester/seed-data/offers-tester-seed-data";
14
- import { bidsTesterSeedData } from "../../../../features/tester/seed-data/bids-tester-seed-data";
15
- /**
16
- * PII per sandbox collection — the same map `seed-cli.mjs` applies.
17
- *
18
- * This job wrote every document with a bare `batch.set(...)`, while its own
19
- * comment claimed to mirror seed-cli's loadGeneric. loadGeneric ENCRYPTS;
20
- * this did not. So the 4-hourly refresh re-seeded sandbox PII in plaintext and
21
- * quietly undid the backfill for those fixtures — caught by reading the raw
22
- * documents after a deploy, where `bids` had 37 encrypted rows and one
23
- * plaintext one that had not existed at backfill time.
24
- *
25
- * A scheduled job writing straight to Firestore bypasses `applyWriteHooks`
26
- * exactly like the hand-rolled repository overrides did. Same defect, one
27
- * level up.
28
- */
29
- const SANDBOX_PII = {
30
- offers: OFFER_PII_FIELDS,
31
- bids: BID_PII_FIELDS,
32
- products: ["sellerName", "sellerEmail"],
33
- };
34
- function encryptForSandbox(collection, data) {
35
- const fields = SANDBOX_PII[collection];
36
- return fields ? encryptPiiFields(data, [...fields]) : data;
37
- }
38
- const SEED_BY_COLLECTION = {
39
- categories: categoriesTesterSeedData,
40
- stores: storesTesterSeedData,
41
- products: productsTesterSeedData,
42
- blogPosts: blogTesterSeedData,
43
- events: eventsTesterSeedData,
44
- offers: offersTesterSeedData,
45
- };
46
- const BID_CHUNK_SIZE = 30; // Firestore `in` query cap
47
- /**
48
- * Reverts every live tester-sandbox fixture back to its canonical seeded
49
- * shape (undoing edits made through admin/seller forms) and prunes any
50
- * isTestData doc that isn't one of the known fixtures (something a tester
51
- * created during QA, e.g. a cloned product). Runs every 4 hours — distinct
52
- * from `runTesterSandboxCleanup`, which only deletes docs whose 7-day
53
- * `testDataExpiresAt` TTL has actually lapsed and never touches still-live
54
- * docs' fields. Scoped to the tester sandbox only (`isTestData: true`) —
55
- * the permanent Beyblade catalog real visitors browse is never touched.
56
- */
57
- export async function runTesterSandboxRefresh(ctx) {
58
- let reverted = 0;
59
- let deletedExtras = 0;
60
- for (const collection of SANDBOX_COLLECTIONS) {
61
- const seedDocs = SEED_BY_COLLECTION[collection] ?? [];
62
- const validIds = new Set(seedDocs.map((d) => d.id).filter((id) => Boolean(id)));
63
- if (seedDocs.length > 0) {
64
- const batch = ctx.db.batch();
65
- for (const doc of seedDocs) {
66
- const { id, ...data } = doc;
67
- if (!id)
68
- continue;
69
- // merge:true mirrors seed-cli.mjs's loadGeneric — the seed payload
70
- // carries every canonical field, so this fully overwrites anything
71
- // a tester edited through the UI.
72
- batch.set(ctx.db.collection(collection).doc(id), encryptForSandbox(collection, data), { merge: true });
73
- reverted++;
74
- }
75
- await batch.commit();
76
- }
77
- const testDataRefs = await getTestDataRefs(ctx.db, collection, null);
78
- const extraRefs = testDataRefs.filter((ref) => !validIds.has(ref.id));
79
- deletedExtras += await batchDelete(ctx, extraRefs);
80
- }
81
- // Bids aren't isTestData-flagged themselves — scoped by productId
82
- // referencing a sandbox product instead, mirroring runTesterSandboxCleanup's
83
- // cascade convention.
84
- const sandboxProductIds = (SEED_BY_COLLECTION.products ?? [])
85
- .map((d) => d.id)
86
- .filter((id) => Boolean(id));
87
- const validBidIds = new Set(bidsTesterSeedData.map((d) => d.id).filter((id) => Boolean(id)));
88
- if (bidsTesterSeedData.length > 0) {
89
- const bidBatch = ctx.db.batch();
90
- for (const bid of bidsTesterSeedData) {
91
- const { id, ...data } = bid;
92
- if (!id)
93
- continue;
94
- bidBatch.set(ctx.db.collection("bids").doc(id), encryptForSandbox("bids", data), { merge: true });
95
- reverted++;
96
- }
97
- await bidBatch.commit();
98
- }
99
- const extraBidRefs = [];
100
- for (let i = 0; i < sandboxProductIds.length; i += BID_CHUNK_SIZE) {
101
- const chunk = sandboxProductIds.slice(i, i + BID_CHUNK_SIZE);
102
- if (chunk.length === 0)
103
- continue;
104
- const snap = await ctx.db.collection("bids").where(BID_FIELDS.PRODUCT_ID, "in", chunk).get();
105
- for (const doc of snap.docs) {
106
- if (!validBidIds.has(doc.id))
107
- extraBidRefs.push(doc.ref);
108
- }
109
- }
110
- deletedExtras += await batchDelete(ctx, extraBidRefs);
111
- ctx.logger.info("Tester sandbox refresh complete", { reverted, deletedExtras });
112
- }
@@ -1,2 +0,0 @@
1
- import type { ScheduleHandler } from "../runtime/types";
2
- export declare const testerSandboxRefreshHandler: ScheduleHandler;
@@ -1,2 +0,0 @@
1
- import { runTesterSandboxRefresh } from "../core/testerSandboxRefresh";
2
- export const testerSandboxRefreshHandler = (ctx) => runTesterSandboxRefresh(ctx);
@@ -1,14 +0,0 @@
1
- import type { SectionType } from "../../../../features/homepage/schemas/firestore";
2
- import type { FeatureFlagKey } from "../../../../features/admin/schemas/firestore";
3
- /** Section types that require a feature flag to be enabled to show on the homepage. */
4
- export declare const SECTION_FEATURE_GATE: Partial<Record<SectionType, FeatureFlagKey>>;
5
- /**
6
- * Drop sections whose feature flag is off.
7
- *
8
- * A section type with no gate entry always passes, and a MISSING flag value
9
- * counts as enabled (`!== false`) — the flags map is partial, and a section
10
- * must not vanish just because an admin has never opened Site Settings.
11
- */
12
- export declare function filterSectionsByFeatureFlags<T extends {
13
- type: string;
14
- }>(sections: readonly T[], flags: Partial<Record<FeatureFlagKey, unknown>> | null | undefined): T[];
@@ -1,51 +0,0 @@
1
- /*
2
- * WHY: The only code in the repo that gated homepage sections on
3
- * `siteSettings.featureFlags` lived inside `getHomepageInitial`, a
4
- * function with ZERO call sites — the live homepage
5
- * (`MarketplaceHomepageView`) reads `homepageSectionsRepository`
6
- * directly. So turning off the `auctions` flag hid auctions everywhere
7
- * except the homepage, which kept advertising them.
8
- *
9
- * `getHomepageInitial` itself was deleted rather than wired up: its other
10
- * unique calls, `findActiveAuctions()` / `findActivePreOrders()`, are
11
- * unbounded `.get()` collection scans with no `.limit()`, which Rule #6
12
- * forbids. The gate is the only part worth keeping, so it moved here.
13
- *
14
- * EXPORTS: SECTION_FEATURE_GATE, filterSectionsByFeatureFlags
15
- *
16
- * @tag domain:homepage
17
- * @tag layer:shared
18
- * @tag pattern:none
19
- * @tag access:isomorphic
20
- * @tag consumers:MarketplaceHomepageView
21
- * @tag sideEffects:none
22
- */
23
- /** Section types that require a feature flag to be enabled to show on the homepage. */
24
- export const SECTION_FEATURE_GATE = {
25
- auctions: "auctions",
26
- "pre-orders": "preOrders",
27
- events: "events",
28
- "event-raffles": "events",
29
- "prize-draws": "events",
30
- "blog-articles": "blog",
31
- };
32
- /**
33
- * Drop sections whose feature flag is off.
34
- *
35
- * A section type with no gate entry always passes, and a MISSING flag value
36
- * counts as enabled (`!== false`) — the flags map is partial, and a section
37
- * must not vanish just because an admin has never opened Site Settings.
38
- */
39
- export function filterSectionsByFeatureFlags(sections,
40
- // Not `Partial<Record<FeatureFlagKey, boolean>>`: two of the flag keys
41
- // (`listingTypes`, `categoryTypes`) hold nested per-type objects rather than
42
- // booleans. Neither is a gate key, so the `=== false` test below simply
43
- // never fires for them.
44
- flags) {
45
- if (!flags)
46
- return [...sections];
47
- return sections.filter((section) => {
48
- const flagKey = SECTION_FEATURE_GATE[section.type];
49
- return !flagKey || flags[flagKey] !== false;
50
- });
51
- }
@@ -1,58 +0,0 @@
1
- import { z } from "zod";
2
- export declare const lotteryConfigFormSchema: z.ZodEffects<z.ZodObject<{
3
- pricingMode: z.ZodEnum<["uniform", "variable"]>;
4
- uniformPrice: z.ZodOptional<z.ZodNumber>;
5
- slots: z.ZodArray<z.ZodObject<{
6
- slotNumber: z.ZodNumber;
7
- name: z.ZodOptional<z.ZodString>;
8
- price: z.ZodOptional<z.ZodNumber>;
9
- image: z.ZodOptional<z.ZodString>;
10
- }, "strip", z.ZodTypeAny, {
11
- slotNumber: number;
12
- image?: string | undefined;
13
- name?: string | undefined;
14
- price?: number | undefined;
15
- }, {
16
- slotNumber: number;
17
- image?: string | undefined;
18
- name?: string | undefined;
19
- price?: number | undefined;
20
- }>, "many">;
21
- }, "strip", z.ZodTypeAny, {
22
- slots: {
23
- slotNumber: number;
24
- image?: string | undefined;
25
- name?: string | undefined;
26
- price?: number | undefined;
27
- }[];
28
- pricingMode: "uniform" | "variable";
29
- uniformPrice?: number | undefined;
30
- }, {
31
- slots: {
32
- slotNumber: number;
33
- image?: string | undefined;
34
- name?: string | undefined;
35
- price?: number | undefined;
36
- }[];
37
- pricingMode: "uniform" | "variable";
38
- uniformPrice?: number | undefined;
39
- }>, {
40
- slots: {
41
- slotNumber: number;
42
- image?: string | undefined;
43
- name?: string | undefined;
44
- price?: number | undefined;
45
- }[];
46
- pricingMode: "uniform" | "variable";
47
- uniformPrice?: number | undefined;
48
- }, {
49
- slots: {
50
- slotNumber: number;
51
- image?: string | undefined;
52
- name?: string | undefined;
53
- price?: number | undefined;
54
- }[];
55
- pricingMode: "uniform" | "variable";
56
- uniformPrice?: number | undefined;
57
- }>;
58
- export type LotteryConfigFormValues = z.infer<typeof lotteryConfigFormSchema>;