@mohasinac/appkit 4.40.1 → 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 (223) hide show
  1. package/dist/features/categories/repository/categories.repository.js +2 -2
  2. package/dist/features/shell/SectionForm.js +44 -5
  3. package/dist/features/tester/seed-data/_money-flows.js +65 -49
  4. package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +6 -4
  5. package/dist/features/tester/seed-data/authored/admin__coupons.js +14 -13
  6. package/dist/features/tester/seed-data/authored/buying__bidding.js +154 -140
  7. package/dist/features/tester/seed-data/authored/buying__browsing-search.js +4 -4
  8. package/dist/features/tester/seed-data/authored/buying__buying-checkout.js +59 -56
  9. package/dist/features/tester/seed-data/authored/buying__buying-coupons.js +18 -18
  10. package/dist/features/tester/seed-data/authored/buying__cart.js +105 -79
  11. package/dist/features/tester/seed-data/authored/buying__image-tile-layout.js +6 -8
  12. package/dist/features/tester/seed-data/authored/buying__my-orders.js +13 -13
  13. package/dist/features/tester/seed-data/authored/buying__offers.js +6 -6
  14. package/dist/features/tester/seed-data/authored/buying__order-detail-actions.js +8 -7
  15. package/dist/features/tester/seed-data/authored/buying__product-detail.js +62 -56
  16. package/dist/features/tester/seed-data/authored/buying__return-request.js +6 -6
  17. package/dist/features/tester/seed-data/authored/buying__reviews-pagination.js +4 -4
  18. package/dist/features/tester/seed-data/authored/buying__reviews.js +1 -1
  19. package/dist/features/tester/seed-data/authored/buying__user-dashboard-extras.js +8 -8
  20. package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +1 -1
  21. package/dist/features/tester/seed-data/authored/content-discovery__coupons.js +29 -16
  22. package/dist/features/tester/seed-data/authored/content-discovery__event-detail-subroutes.js +9 -9
  23. package/dist/features/tester/seed-data/authored/content-discovery__events.js +69 -36
  24. package/dist/features/tester/seed-data/authored/content-discovery__notifications.js +1 -1
  25. package/dist/features/tester/seed-data/authored/cta-layout__checkout-bottom-bar.js +2 -2
  26. package/dist/features/tester/seed-data/authored/design-ux__back-to-top-button.js +2 -2
  27. package/dist/features/tester/seed-data/authored/design-ux__general-design.js +1 -1
  28. package/dist/features/tester/seed-data/authored/design-ux__status-badge-legibility.js +1 -1
  29. package/dist/features/tester/seed-data/authored/design-ux__sticky-cta-bar.js +3 -3
  30. package/dist/features/tester/seed-data/authored/index.js +0 -2
  31. package/dist/features/tester/seed-data/authored/page-wiring__data-loss.js +2 -2
  32. package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +6 -4
  33. package/dist/features/tester/seed-data/authored/public-pages__auth-error-pages.js +2 -2
  34. package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.js +1 -1
  35. package/dist/features/tester/seed-data/authored/public-pages__stores-sellers-directories.js +8 -10
  36. package/dist/features/tester/seed-data/authored/selling__become-seller.js +2 -2
  37. package/dist/features/tester/seed-data/authored/selling__digital-content-delivery.js +2 -2
  38. package/dist/features/tester/seed-data/authored/selling__listing-a-product.js +2 -2
  39. package/dist/features/tester/seed-data/authored/selling__listing-edit-roundtrip.js +2 -2
  40. package/dist/features/tester/seed-data/authored/selling__listing-lifecycle.js +2 -2
  41. package/dist/features/tester/seed-data/authored/selling__seller-listing-types.js +2 -2
  42. package/dist/features/tester/seed-data/authored/seo__og-images.js +3 -4
  43. package/dist/features/tester/seed-data/authored/seo__page-metadata.js +6 -8
  44. package/dist/features/tester/seed-data/authored/seo__sitemap-and-robots.js +5 -5
  45. package/dist/features/tester/seed-data/index.d.ts +0 -10
  46. package/dist/features/tester/seed-data/index.js +0 -10
  47. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +75 -89
  48. package/dist/features/tester/seed-data/tester-responses-seed-data.js +5 -5
  49. package/dist/index.d.ts +0 -8
  50. package/dist/index.js +0 -21
  51. package/dist/seed/addresses-seed-data.js +0 -34
  52. package/dist/seed/index.d.ts +1 -1
  53. package/dist/seed/index.js +4 -3
  54. package/dist/seed/manifest.js +11 -12
  55. package/dist/seed/notifications-seed-data.js +4 -4
  56. package/dist/seed/stores-seed-data.js +7 -52
  57. package/dist/seed/users-seed-data.js +3 -83
  58. package/package.json +2 -2
  59. package/scripts/seed-cli.mjs +15 -20
  60. package/dist/_internal/server/features/classified/actions.d.ts +0 -12
  61. package/dist/_internal/server/features/classified/actions.js +0 -33
  62. package/dist/_internal/server/features/digital-code/actions.d.ts +0 -12
  63. package/dist/_internal/server/features/digital-code/actions.js +0 -33
  64. package/dist/_internal/server/features/homepage/data.d.ts +0 -18
  65. package/dist/_internal/server/features/homepage/data.js +0 -70
  66. package/dist/_internal/server/features/live/actions.d.ts +0 -6
  67. package/dist/_internal/server/features/live/actions.js +0 -27
  68. package/dist/_internal/server/features/messages/actions.d.ts +0 -6
  69. package/dist/_internal/server/features/messages/actions.js +0 -6
  70. package/dist/_internal/server/features/messages/data.d.ts +0 -10
  71. package/dist/_internal/server/features/messages/data.js +0 -24
  72. package/dist/_internal/server/features/messages/index.d.ts +0 -2
  73. package/dist/_internal/server/features/messages/index.js +0 -2
  74. package/dist/_internal/server/jobs/core/testerSandboxRefresh.d.ts +0 -12
  75. package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +0 -112
  76. package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.d.ts +0 -2
  77. package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.js +0 -2
  78. package/dist/_internal/shared/features/homepage/section-gate.d.ts +0 -14
  79. package/dist/_internal/shared/features/homepage/section-gate.js +0 -51
  80. package/dist/_internal/shared/features/lottery/config-form.d.ts +0 -58
  81. package/dist/_internal/shared/features/lottery/config-form.js +0 -71
  82. package/dist/client/api/useFeatureFlags.d.ts +0 -22
  83. package/dist/client/api/useFeatureFlags.js +0 -27
  84. package/dist/features/account/address-validation.d.ts +0 -20
  85. package/dist/features/account/address-validation.js +0 -35
  86. package/dist/features/account/components/ChatList.d.ts +0 -14
  87. package/dist/features/account/components/ChatList.js +0 -9
  88. package/dist/features/account/components/ChatWindow.d.ts +0 -17
  89. package/dist/features/account/components/ChatWindow.js +0 -6
  90. package/dist/features/account/components/MessagesView.d.ts +0 -16
  91. package/dist/features/account/components/MessagesView.js +0 -5
  92. package/dist/features/account/components/UserOffersView.d.ts +0 -10
  93. package/dist/features/account/components/UserOffersView.js +0 -5
  94. package/dist/features/account/constants/addresses.d.ts +0 -13
  95. package/dist/features/account/constants/addresses.js +0 -43
  96. package/dist/features/account/hooks/useAddressForm.d.ts +0 -24
  97. package/dist/features/account/hooks/useAddressForm.js +0 -58
  98. package/dist/features/admin/actions/chat-actions.d.ts +0 -29
  99. package/dist/features/admin/actions/chat-actions.js +0 -101
  100. package/dist/features/admin/components/AdminFeatureFlagsView.d.ts +0 -9
  101. package/dist/features/admin/components/AdminFeatureFlagsView.js +0 -109
  102. package/dist/features/admin/components/AdminSiteView.d.ts +0 -10
  103. package/dist/features/admin/components/AdminSiteView.js +0 -52
  104. package/dist/features/admin/hooks/useChat.d.ts +0 -24
  105. package/dist/features/admin/hooks/useChat.js +0 -100
  106. package/dist/features/admin/permission-map.d.ts +0 -3
  107. package/dist/features/admin/permission-map.js +0 -26
  108. package/dist/features/admin/repository/chat.repository.d.ts +0 -50
  109. package/dist/features/admin/repository/chat.repository.js +0 -223
  110. package/dist/features/auth/actions/password-change-otp-actions.d.ts +0 -18
  111. package/dist/features/auth/actions/password-change-otp-actions.js +0 -88
  112. package/dist/features/auth/actions/realtime-token-actions.d.ts +0 -11
  113. package/dist/features/auth/actions/realtime-token-actions.js +0 -38
  114. package/dist/features/auth/password-change-otp.d.ts +0 -34
  115. package/dist/features/auth/password-change-otp.js +0 -56
  116. package/dist/features/classified/components/ClassifiedContactSellerPanel.d.ts +0 -12
  117. package/dist/features/classified/components/ClassifiedContactSellerPanel.js +0 -49
  118. package/dist/features/events/api/route.d.ts +0 -12
  119. package/dist/features/events/api/route.js +0 -90
  120. package/dist/features/faq/api/route.d.ts +0 -11
  121. package/dist/features/faq/api/route.js +0 -52
  122. package/dist/features/faq/hooks/useFAQs.d.ts +0 -22
  123. package/dist/features/faq/hooks/useFAQs.js +0 -41
  124. package/dist/features/media/upload/video-poster.test.d.ts +0 -1
  125. package/dist/features/media/upload/video-poster.test.js +0 -42
  126. package/dist/features/messages/actions/index.d.ts +0 -2
  127. package/dist/features/messages/actions/index.js +0 -2
  128. package/dist/features/messages/actions/messages-actions.d.ts +0 -15
  129. package/dist/features/messages/actions/messages-actions.js +0 -33
  130. package/dist/features/messages/actions/ping-rtdb.d.ts +0 -2
  131. package/dist/features/messages/actions/ping-rtdb.js +0 -26
  132. package/dist/features/messages/hooks/useConversation.d.ts +0 -22
  133. package/dist/features/messages/hooks/useConversation.js +0 -155
  134. package/dist/features/messages/hooks/useConversations.d.ts +0 -15
  135. package/dist/features/messages/hooks/useConversations.js +0 -104
  136. package/dist/features/messages/index.d.ts +0 -6
  137. package/dist/features/messages/index.js +0 -4
  138. package/dist/features/messages/realtime.d.ts +0 -32
  139. package/dist/features/messages/realtime.js +0 -39
  140. package/dist/features/messages/repository/conversations.repository.d.ts +0 -71
  141. package/dist/features/messages/repository/conversations.repository.js +0 -284
  142. package/dist/features/messages/schemas/firestore.d.ts +0 -34
  143. package/dist/features/messages/schemas/firestore.js +0 -16
  144. package/dist/features/messages/schemas/index.d.ts +0 -177
  145. package/dist/features/messages/schemas/index.js +0 -35
  146. package/dist/features/messages/server.d.ts +0 -6
  147. package/dist/features/messages/server.js +0 -6
  148. package/dist/features/products/components/MakeOfferForm.d.ts +0 -20
  149. package/dist/features/products/components/MakeOfferForm.js +0 -9
  150. package/dist/features/reviews/api/route.d.ts +0 -21
  151. package/dist/features/reviews/api/route.js +0 -174
  152. package/dist/features/seller/api/offers/route.d.ts +0 -17
  153. package/dist/features/seller/api/offers/route.js +0 -63
  154. package/dist/features/seller/components/SellerAuctionsView.d.ts +0 -9
  155. package/dist/features/seller/components/SellerAuctionsView.js +0 -113
  156. package/dist/features/seller/components/SellerOffersPanel.d.ts +0 -21
  157. package/dist/features/seller/components/SellerOffersPanel.js +0 -160
  158. package/dist/features/seller/components/SellerPreOrdersView.d.ts +0 -6
  159. package/dist/features/seller/components/SellerPreOrdersView.js +0 -148
  160. package/dist/features/seller/components/SellerPrizeDrawsView.d.ts +0 -6
  161. package/dist/features/seller/components/SellerPrizeDrawsView.js +0 -152
  162. package/dist/features/seller/permission-map.d.ts +0 -5
  163. package/dist/features/seller/permission-map.js +0 -17
  164. package/dist/features/shell/StepForm.d.ts +0 -80
  165. package/dist/features/shell/StepForm.js +0 -136
  166. package/dist/features/stores/api/route.d.ts +0 -12
  167. package/dist/features/stores/api/route.js +0 -110
  168. package/dist/features/tester/seed-data/authored/account-auth__testing-program--admin.d.ts +0 -2
  169. package/dist/features/tester/seed-data/authored/account-auth__testing-program--admin.js +0 -33
  170. package/dist/features/tester/seed-data/authored/account-auth__testing-program.d.ts +0 -2
  171. package/dist/features/tester/seed-data/authored/account-auth__testing-program.js +0 -138
  172. package/dist/features/tester/seed-data/bids-tester-seed-data.d.ts +0 -2
  173. package/dist/features/tester/seed-data/bids-tester-seed-data.js +0 -37
  174. package/dist/features/tester/seed-data/blog-tester-seed-data.d.ts +0 -2
  175. package/dist/features/tester/seed-data/blog-tester-seed-data.js +0 -40
  176. package/dist/features/tester/seed-data/carts-tester-seed-data.d.ts +0 -3
  177. package/dist/features/tester/seed-data/carts-tester-seed-data.js +0 -70
  178. package/dist/features/tester/seed-data/categories-tester-seed-data.d.ts +0 -2
  179. package/dist/features/tester/seed-data/categories-tester-seed-data.js +0 -218
  180. package/dist/features/tester/seed-data/coupons-tester-seed-data.d.ts +0 -2
  181. package/dist/features/tester/seed-data/coupons-tester-seed-data.js +0 -76
  182. package/dist/features/tester/seed-data/events-tester-seed-data.d.ts +0 -2
  183. package/dist/features/tester/seed-data/events-tester-seed-data.js +0 -193
  184. package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +0 -2
  185. package/dist/features/tester/seed-data/offers-tester-seed-data.js +0 -191
  186. package/dist/features/tester/seed-data/orders-tester-seed-data.d.ts +0 -2
  187. package/dist/features/tester/seed-data/orders-tester-seed-data.js +0 -329
  188. package/dist/features/tester/seed-data/products-tester-seed-data.d.ts +0 -2
  189. package/dist/features/tester/seed-data/products-tester-seed-data.js +0 -815
  190. package/dist/features/tester/seed-data/stores-tester-seed-data.d.ts +0 -2
  191. package/dist/features/tester/seed-data/stores-tester-seed-data.js +0 -79
  192. package/dist/features/wishlist/components/WishlistToggleButton.d.ts +0 -12
  193. package/dist/features/wishlist/components/WishlistToggleButton.js +0 -36
  194. package/dist/monitoring/analytics.d.ts +0 -80
  195. package/dist/monitoring/analytics.js +0 -176
  196. package/dist/next/middleware/pii-redactor.d.ts +0 -32
  197. package/dist/next/middleware/pii-redactor.js +0 -84
  198. package/dist/providers/db-firebase/realtime.d.ts +0 -42
  199. package/dist/providers/db-firebase/realtime.js +0 -138
  200. package/dist/security/rbac/Can.d.ts +0 -31
  201. package/dist/security/rbac/Can.js +0 -22
  202. package/dist/security/rbac/default-roles.d.ts +0 -2
  203. package/dist/security/rbac/default-roles.js +0 -83
  204. package/dist/security/rbac/hook.d.ts +0 -20
  205. package/dist/security/rbac/hook.js +0 -30
  206. package/dist/security/rbac/index.d.ts +0 -8
  207. package/dist/security/rbac/index.js +0 -6
  208. package/dist/security/rbac/middleware.d.ts +0 -51
  209. package/dist/security/rbac/middleware.js +0 -51
  210. package/dist/security/rbac/resolver.d.ts +0 -5
  211. package/dist/security/rbac/resolver.js +0 -38
  212. package/dist/security/rbac/server.d.ts +0 -24
  213. package/dist/security/rbac/server.js +0 -42
  214. package/dist/security/rbac/types.d.ts +0 -22
  215. package/dist/security/rbac/types.js +0 -2
  216. package/dist/seed/conversations-seed-data.d.ts +0 -2
  217. package/dist/seed/conversations-seed-data.js +0 -292
  218. package/dist/utils/filter.helper.d.ts +0 -18
  219. package/dist/utils/filter.helper.js +0 -21
  220. package/dist/utils/pagination.helper.d.ts +0 -23
  221. package/dist/utils/pagination.helper.js +0 -24
  222. package/dist/utils/search-tokens.d.ts +0 -2
  223. package/dist/utils/search-tokens.js +0 -21
@@ -1,4 +0,0 @@
1
- export * from "./hooks/useConversations";
2
- export * from "./hooks/useConversation";
3
- export { CONVERSATIONS_COLLECTION, CONVERSATIONS_INDEXED_FIELDS, } from "./schemas/firestore";
4
- export { conversationPingPath, userConversationsPingPath, buildConversationPingPaths, } from "./realtime";
@@ -1,32 +0,0 @@
1
- /**
2
- * Conversations realtime channel — shared between client hooks and server
3
- * routes. Lives in a non-`"use client"` module so server route handlers can
4
- * import the path builders without dragging the React hooks into a server
5
- * bundle.
6
- *
7
- * Strategy (D5 + VC7):
8
- * - Firestore = canonical store; messages embedded in `conversations/{id}`.
9
- * - RTDB = "ping" channel only — server writes Date.now() to two paths on
10
- * every mutation, clients subscribe and refetch.
11
- *
12
- * chats/{id}/lastUpdate — single-conversation ping
13
- * chats/user/{userId}/lastUpdate — per-user fan-out ping
14
- *
15
- * Both ends import these constants instead of inlining the path strings.
16
- */
17
- /** RTDB path bumped on every write to a single conversation. */
18
- export declare function conversationPingPath(conversationId: string): string;
19
- /**
20
- * RTDB path bumped on every write across all of a user's conversations.
21
- * Subscribing to this is how the chat list / nav bell refresh without
22
- * subscribing to every individual conversation.
23
- */
24
- export declare function userConversationsPingPath(userId: string): string;
25
- /** All ping paths to bump on a single conversation write. */
26
- export interface ConversationPingTargets {
27
- conversationId: string;
28
- buyerId: string;
29
- /** Resolved store-owner UID for the seller side; null when not known. */
30
- sellerOwnerId: string | null;
31
- }
32
- export declare function buildConversationPingPaths(targets: ConversationPingTargets): string[];
@@ -1,39 +0,0 @@
1
- /**
2
- * Conversations realtime channel — shared between client hooks and server
3
- * routes. Lives in a non-`"use client"` module so server route handlers can
4
- * import the path builders without dragging the React hooks into a server
5
- * bundle.
6
- *
7
- * Strategy (D5 + VC7):
8
- * - Firestore = canonical store; messages embedded in `conversations/{id}`.
9
- * - RTDB = "ping" channel only — server writes Date.now() to two paths on
10
- * every mutation, clients subscribe and refetch.
11
- *
12
- * chats/{id}/lastUpdate — single-conversation ping
13
- * chats/user/{userId}/lastUpdate — per-user fan-out ping
14
- *
15
- * Both ends import these constants instead of inlining the path strings.
16
- */
17
- const PING_KEY = "lastUpdate";
18
- /** RTDB path bumped on every write to a single conversation. */
19
- export function conversationPingPath(conversationId) {
20
- return `chats/${conversationId}/${PING_KEY}`;
21
- }
22
- /**
23
- * RTDB path bumped on every write across all of a user's conversations.
24
- * Subscribing to this is how the chat list / nav bell refresh without
25
- * subscribing to every individual conversation.
26
- */
27
- export function userConversationsPingPath(userId) {
28
- return `chats/user/${userId}/${PING_KEY}`;
29
- }
30
- export function buildConversationPingPaths(targets) {
31
- const paths = [
32
- conversationPingPath(targets.conversationId),
33
- userConversationsPingPath(targets.buyerId),
34
- ];
35
- if (targets.sellerOwnerId) {
36
- paths.push(userConversationsPingPath(targets.sellerOwnerId));
37
- }
38
- return paths;
39
- }
@@ -1,71 +0,0 @@
1
- /**
2
- * ConversationsRepository — buyer↔seller messaging.
3
- *
4
- * One document per buyer×storeId×productId tuple. Messages are embedded in
5
- * the document as an array (good up to ~200 entries; migrate to subcollection
6
- * later if threads explode).
7
- *
8
- * All mutations are transactional so concurrent writes don't drop messages
9
- * or unread counters.
10
- */
11
- import { type ConversationDocument, type ConversationMessage } from "../schemas/firestore";
12
- export declare class ConversationFullError extends Error {
13
- readonly code: "CONVERSATION_FULL";
14
- }
15
- export declare class ConversationsRepository {
16
- private collection;
17
- /**
18
- * Find an existing buyer×store×product conversation or create a new one.
19
- * Idempotent — safe to call on every "Contact Seller" click.
20
- */
21
- findOrCreateByContext(params: {
22
- buyerId: string;
23
- buyerDisplayName: string;
24
- storeId: string;
25
- storeName: string;
26
- sellerDisplayName: string;
27
- productId?: string;
28
- productTitle?: string;
29
- }): Promise<ConversationDocument>;
30
- findById(id: string): Promise<ConversationDocument | null>;
31
- listByBuyer(buyerId: string): Promise<ConversationDocument[]>;
32
- listByStore(storeId: string): Promise<ConversationDocument[]>;
33
- /**
34
- * Conversation ids this user may subscribe to, for the RTDB `conversationIds`
35
- * custom-token claim.
36
- *
37
- * 🛑 This claim previously had NO ISSUER ANYWHERE. The `chats/$conversationId`
38
- * rule requires `auth.token.conversationIds[$conversationId] == true`, but
39
- * `/api/realtime/token` only ever issued `chatIds`, built by
40
- * `chatRepository.getChatIdsForUser()` over the **`chatRooms`** collection —
41
- * a different collection with different document ids. So the buyer↔seller
42
- * live-message channel was denied for every user, and both subscribing hooks
43
- * swallowed the permission error (one into `setIsConnected(false)`, one into
44
- * a literal `// ignore`). Messages only ever appeared for the sender, via an
45
- * unrelated `refetch()` after send.
46
- *
47
- * `.select()` with no fields fetches ids only — the document bodies are not
48
- * needed and this runs on every token issue.
49
- *
50
- * @param limit hard cap. Firebase custom claims are limited to 1000 BYTES
51
- * total; each entry costs roughly `"<20-char id>":true,` ≈ 28 bytes, so a
52
- * user in hundreds of conversations would make `createCustomToken` throw
53
- * and take the whole route down (see the route's own comment).
54
- */
55
- getConversationIdsForUser(userId: string, storeIds?: readonly string[], limit?: number): Promise<string[]>;
56
- /**
57
- * Append a message to a conversation in a transaction. Bumps the counterparty
58
- * unread counter; updates lastMessage / lastMessageAt / updatedAt.
59
- */
60
- appendMessage(conversationId: string, msg: Omit<ConversationMessage, "id" | "sentAt" | "isRead"> & {
61
- id?: string;
62
- sentAt?: Date;
63
- }): Promise<ConversationDocument>;
64
- /**
65
- * Zero out the unread counter for one party. Also flips `isRead` on the
66
- * embedded messages sent by the other party so the seller side can see what
67
- * the buyer has already read.
68
- */
69
- markRead(conversationId: string, role: "buyer" | "seller"): Promise<void>;
70
- }
71
- export declare const conversationsRepository: ConversationsRepository;
@@ -1,284 +0,0 @@
1
- /**
2
- * ConversationsRepository — buyer↔seller messaging.
3
- *
4
- * One document per buyer×storeId×productId tuple. Messages are embedded in
5
- * the document as an array (good up to ~200 entries; migrate to subcollection
6
- * later if threads explode).
7
- *
8
- * All mutations are transactional so concurrent writes don't drop messages
9
- * or unread counters.
10
- */
11
- import { getAdminDb } from "../../../providers/db-firebase";
12
- import { encryptPiiFields, decryptPiiFields } from "../../../security/pii-encrypt";
13
- import { CONVERSATION_PII_FIELDS } from "../../../security/pii-schemas";
14
- import { serverLogger } from "../../../monitoring";
15
- import { CONVERSATION_FIELDS } from "../../../constants/field-names";
16
- import { CONVERSATIONS_COLLECTION, } from "../schemas/firestore";
17
- function toDate(raw) {
18
- if (raw instanceof Date)
19
- return raw;
20
- if (raw && typeof raw.toDate === "function") {
21
- return raw.toDate();
22
- }
23
- if (typeof raw === "string" || typeof raw === "number")
24
- return new Date(raw);
25
- return new Date();
26
- }
27
- function normaliseMessage(raw) {
28
- const r = (raw ?? {});
29
- return {
30
- id: String(r.id ?? ""),
31
- senderId: String(r.senderId ?? ""),
32
- senderRole: r.senderRole ?? "buyer",
33
- body: String(r.body ?? ""),
34
- isRead: r.isRead === true,
35
- sentAt: toDate(r.sentAt),
36
- attachments: Array.isArray(r.attachments)
37
- ? r.attachments
38
- : undefined,
39
- };
40
- }
41
- function normaliseDoc(id, raw) {
42
- // Decrypt here rather than at the six call sites: this is the one function
43
- // every read path already goes through, so a new reader cannot forget it.
44
- // `markRead` deliberately does NOT decrypt — it reads raw messages, flips
45
- // isRead and writes them straight back, so ciphertext round-trips untouched.
46
- const r = decryptPiiFields(raw, [...CONVERSATION_PII_FIELDS]);
47
- return {
48
- id,
49
- buyerId: String(r.buyerId ?? ""),
50
- buyerDisplayName: String(r.buyerDisplayName ?? ""),
51
- storeId: String(r.storeId ?? ""),
52
- storeName: String(r.storeName ?? ""),
53
- sellerDisplayName: String(r.sellerDisplayName ?? ""),
54
- productId: typeof r.productId === "string" ? r.productId : undefined,
55
- productTitle: typeof r.productTitle === "string" ? r.productTitle : undefined,
56
- messages: Array.isArray(r.messages) ? r.messages.map(normaliseMessage) : [],
57
- lastMessage: String(r.lastMessage ?? ""),
58
- lastMessageAt: toDate(r.lastMessageAt),
59
- unreadBuyer: typeof r.unreadBuyer === "number" ? r.unreadBuyer : 0,
60
- unreadSeller: typeof r.unreadSeller === "number" ? r.unreadSeller : 0,
61
- status: r.status ?? "active",
62
- createdAt: toDate(r.createdAt),
63
- updatedAt: toDate(r.updatedAt),
64
- };
65
- }
66
- export class ConversationFullError extends Error {
67
- constructor() {
68
- super(...arguments);
69
- this.code = "CONVERSATION_FULL";
70
- }
71
- }
72
- export class ConversationsRepository {
73
- collection() {
74
- return getAdminDb().collection(CONVERSATIONS_COLLECTION);
75
- }
76
- /**
77
- * Find an existing buyer×store×product conversation or create a new one.
78
- * Idempotent — safe to call on every "Contact Seller" click.
79
- */
80
- async findOrCreateByContext(params) {
81
- const db = getAdminDb();
82
- const coll = this.collection();
83
- const { buyerId, storeId, productId } = params;
84
- // Stable composite key: buyerId + storeId + (productId | "general")
85
- const productKey = productId ?? "general";
86
- const stableId = `conv-${buyerId}-${storeId}-${productKey}`;
87
- const ref = coll.doc(stableId);
88
- return db.runTransaction(async (tx) => {
89
- const snap = await tx.get(ref);
90
- if (snap.exists) {
91
- return normaliseDoc(snap.id, snap.data() ?? {});
92
- }
93
- const now = new Date();
94
- const newDoc = {
95
- buyerId: params.buyerId,
96
- buyerDisplayName: params.buyerDisplayName,
97
- storeId: params.storeId,
98
- storeName: params.storeName,
99
- sellerDisplayName: params.sellerDisplayName,
100
- productId: params.productId,
101
- productTitle: params.productTitle,
102
- messages: [],
103
- lastMessage: "",
104
- lastMessageAt: now,
105
- unreadBuyer: 0,
106
- unreadSeller: 0,
107
- status: "active",
108
- createdAt: now,
109
- updatedAt: now,
110
- };
111
- const encryptedDoc = encryptPiiFields(newDoc, [...CONVERSATION_PII_FIELDS]);
112
- tx.set(ref, encryptedDoc);
113
- return normaliseDoc(stableId, encryptedDoc);
114
- });
115
- }
116
- async findById(id) {
117
- const snap = await this.collection().doc(id).get();
118
- if (!snap.exists)
119
- return null;
120
- return normaliseDoc(snap.id, snap.data() ?? {});
121
- }
122
- async listByBuyer(buyerId) {
123
- const snap = await this.collection()
124
- .where(CONVERSATION_FIELDS.BUYER_ID, "==", buyerId)
125
- .orderBy(CONVERSATION_FIELDS.LAST_MESSAGE_AT, "desc")
126
- .get();
127
- return snap.docs.map((d) => normaliseDoc(d.id, d.data()));
128
- }
129
- async listByStore(storeId) {
130
- const snap = await this.collection()
131
- .where(CONVERSATION_FIELDS.STORE_ID, "==", storeId)
132
- .orderBy(CONVERSATION_FIELDS.LAST_MESSAGE_AT, "desc")
133
- .get();
134
- return snap.docs.map((d) => normaliseDoc(d.id, d.data()));
135
- }
136
- /**
137
- * Conversation ids this user may subscribe to, for the RTDB `conversationIds`
138
- * custom-token claim.
139
- *
140
- * 🛑 This claim previously had NO ISSUER ANYWHERE. The `chats/$conversationId`
141
- * rule requires `auth.token.conversationIds[$conversationId] == true`, but
142
- * `/api/realtime/token` only ever issued `chatIds`, built by
143
- * `chatRepository.getChatIdsForUser()` over the **`chatRooms`** collection —
144
- * a different collection with different document ids. So the buyer↔seller
145
- * live-message channel was denied for every user, and both subscribing hooks
146
- * swallowed the permission error (one into `setIsConnected(false)`, one into
147
- * a literal `// ignore`). Messages only ever appeared for the sender, via an
148
- * unrelated `refetch()` after send.
149
- *
150
- * `.select()` with no fields fetches ids only — the document bodies are not
151
- * needed and this runs on every token issue.
152
- *
153
- * @param limit hard cap. Firebase custom claims are limited to 1000 BYTES
154
- * total; each entry costs roughly `"<20-char id>":true,` ≈ 28 bytes, so a
155
- * user in hundreds of conversations would make `createCustomToken` throw
156
- * and take the whole route down (see the route's own comment).
157
- */
158
- async getConversationIdsForUser(userId, storeIds = [], limit = 25) {
159
- const queries = [
160
- this.collection()
161
- .where(CONVERSATION_FIELDS.BUYER_ID, "==", userId)
162
- .orderBy(CONVERSATION_FIELDS.LAST_MESSAGE_AT, "desc")
163
- .limit(limit)
164
- .select()
165
- .get(),
166
- // Firestore `in` caps at 30 values; a seller with more stores than that
167
- // is not a shape this app produces.
168
- ...(storeIds.length > 0
169
- ? [
170
- this.collection()
171
- .where(CONVERSATION_FIELDS.STORE_ID, "in", storeIds.slice(0, 30))
172
- .orderBy(CONVERSATION_FIELDS.LAST_MESSAGE_AT, "desc")
173
- .limit(limit)
174
- .select()
175
- .get(),
176
- ]
177
- : []),
178
- ];
179
- const snaps = await Promise.all(queries);
180
- const ids = new Set();
181
- for (const snap of snaps) {
182
- for (const doc of snap.docs)
183
- ids.add(doc.id);
184
- }
185
- // Most-recent-first is already the sort order; truncating here keeps the
186
- // claim inside the byte budget when a user is both buyer and seller.
187
- return [...ids].slice(0, limit);
188
- }
189
- /**
190
- * Append a message to a conversation in a transaction. Bumps the counterparty
191
- * unread counter; updates lastMessage / lastMessageAt / updatedAt.
192
- */
193
- async appendMessage(conversationId, msg) {
194
- const db = getAdminDb();
195
- const ref = this.collection().doc(conversationId);
196
- try {
197
- return await db.runTransaction(async (tx) => {
198
- const snap = await tx.get(ref);
199
- if (!snap.exists) {
200
- throw new Error("Conversation not found");
201
- }
202
- const data = snap.data() ?? {};
203
- const existing = Array.isArray(data.messages)
204
- ? data.messages.map(normaliseMessage)
205
- : [];
206
- const now = msg.sentAt ?? new Date();
207
- const newMessage = {
208
- id: msg.id ?? `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
209
- senderId: msg.senderId,
210
- senderRole: msg.senderRole,
211
- body: msg.body,
212
- isRead: false,
213
- sentAt: now,
214
- attachments: msg.attachments,
215
- };
216
- const nextMessages = [...existing, newMessage];
217
- const unreadBuyer = msg.senderRole === "seller"
218
- ? (typeof data.unreadBuyer === "number" ? data.unreadBuyer : 0) + 1
219
- : (typeof data.unreadBuyer === "number" ? data.unreadBuyer : 0);
220
- const unreadSeller = msg.senderRole === "buyer"
221
- ? (typeof data.unreadSeller === "number" ? data.unreadSeller : 0) + 1
222
- : (typeof data.unreadSeller === "number" ? data.unreadSeller : 0);
223
- const patch = {
224
- messages: nextMessages,
225
- lastMessage: msg.body,
226
- lastMessageAt: now,
227
- unreadBuyer,
228
- unreadSeller,
229
- updatedAt: now,
230
- };
231
- // Encrypt before the write; normaliseDoc decrypts on the way back out,
232
- // so the caller still receives plaintext.
233
- const encryptedPatch = encryptPiiFields(patch, [...CONVERSATION_PII_FIELDS]);
234
- tx.update(ref, encryptedPatch);
235
- return normaliseDoc(conversationId, { ...data, ...encryptedPatch });
236
- });
237
- }
238
- catch (error) {
239
- serverLogger.error("ConversationsRepository.appendMessage error", {
240
- conversationId,
241
- error,
242
- });
243
- throw error;
244
- }
245
- }
246
- /**
247
- * Zero out the unread counter for one party. Also flips `isRead` on the
248
- * embedded messages sent by the other party so the seller side can see what
249
- * the buyer has already read.
250
- */
251
- async markRead(conversationId, role) {
252
- const db = getAdminDb();
253
- const ref = this.collection().doc(conversationId);
254
- try {
255
- await db.runTransaction(async (tx) => {
256
- const snap = await tx.get(ref);
257
- if (!snap.exists)
258
- return;
259
- const data = snap.data() ?? {};
260
- const messages = Array.isArray(data.messages)
261
- ? data.messages.map(normaliseMessage)
262
- : [];
263
- // role="buyer" means the buyer just opened the chat → mark all
264
- // seller-sent messages read.
265
- const otherRole = role === "buyer" ? "seller" : "buyer";
266
- const nextMessages = messages.map((m) => m.senderRole === otherRole && !m.isRead ? { ...m, isRead: true } : m);
267
- tx.update(ref, {
268
- messages: nextMessages,
269
- [role === "buyer" ? "unreadBuyer" : "unreadSeller"]: 0,
270
- updatedAt: new Date(),
271
- });
272
- });
273
- }
274
- catch (error) {
275
- serverLogger.error("ConversationsRepository.markRead error", {
276
- conversationId,
277
- role,
278
- error,
279
- });
280
- throw error;
281
- }
282
- }
283
- }
284
- export const conversationsRepository = new ConversationsRepository();
@@ -1,34 +0,0 @@
1
- /**
2
- * Messages / Conversations Firestore Document Types & Constants
3
- *
4
- * Architecture: one `conversations` document per buyer↔seller pair on a product.
5
- * Messages are embedded as an array for seed purposes; production can migrate to
6
- * a subcollection when the thread exceeds ~200 messages.
7
- */
8
- import type { BaseDocument } from "../../../_internal/shared/types/base-document";
9
- export type ConversationMessage = {
10
- id: string;
11
- senderId: string;
12
- senderRole: "buyer" | "seller";
13
- body: string;
14
- isRead: boolean;
15
- sentAt: Date;
16
- attachments?: string[];
17
- };
18
- export interface ConversationDocument extends BaseDocument {
19
- buyerId: string;
20
- buyerDisplayName: string;
21
- storeId: string;
22
- storeName: string;
23
- sellerDisplayName: string;
24
- productId?: string;
25
- productTitle?: string;
26
- messages: ConversationMessage[];
27
- lastMessage: string;
28
- lastMessageAt: Date;
29
- unreadBuyer: number;
30
- unreadSeller: number;
31
- status: "active" | "archived" | "blocked";
32
- }
33
- export declare const CONVERSATIONS_COLLECTION: "conversations";
34
- export declare const CONVERSATIONS_INDEXED_FIELDS: readonly ["buyerId", "storeId", "productId", "status", "lastMessageAt", "createdAt"];
@@ -1,16 +0,0 @@
1
- /**
2
- * Messages / Conversations Firestore Document Types & Constants
3
- *
4
- * Architecture: one `conversations` document per buyer↔seller pair on a product.
5
- * Messages are embedded as an array for seed purposes; production can migrate to
6
- * a subcollection when the thread exceeds ~200 messages.
7
- */
8
- export const CONVERSATIONS_COLLECTION = "conversations";
9
- export const CONVERSATIONS_INDEXED_FIELDS = [
10
- "buyerId",
11
- "storeId",
12
- "productId",
13
- "status",
14
- "lastMessageAt",
15
- "createdAt",
16
- ];
@@ -1,177 +0,0 @@
1
- import { z } from "zod";
2
- export * from "./firestore";
3
- export declare const conversationMessageSenderRoleSchema: z.ZodEnum<["buyer", "seller"]>;
4
- export declare const conversationStatusSchema: z.ZodEnum<["active", "archived", "blocked"]>;
5
- export declare const conversationMessageSchema: z.ZodObject<{
6
- id: z.ZodString;
7
- senderId: z.ZodString;
8
- senderRole: z.ZodEnum<["buyer", "seller"]>;
9
- body: z.ZodString;
10
- isRead: z.ZodBoolean;
11
- sentAt: z.ZodUnion<[z.ZodDate, z.ZodString, z.ZodObject<{
12
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
13
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
14
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
15
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
16
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
17
- }, z.ZodTypeAny, "passthrough">>]>;
18
- attachments: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
19
- }, "strip", z.ZodTypeAny, {
20
- id: string;
21
- body: string;
22
- isRead: boolean;
23
- senderId: string;
24
- senderRole: "buyer" | "seller";
25
- sentAt: string | Date | z.objectOutputType<{
26
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
27
- }, z.ZodTypeAny, "passthrough">;
28
- attachments?: string[] | undefined;
29
- }, {
30
- id: string;
31
- body: string;
32
- isRead: boolean;
33
- senderId: string;
34
- senderRole: "buyer" | "seller";
35
- sentAt: string | Date | z.objectInputType<{
36
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
37
- }, z.ZodTypeAny, "passthrough">;
38
- attachments?: string[] | undefined;
39
- }>;
40
- export declare const conversationFirestoreSchema: z.ZodObject<{
41
- createdAt: z.ZodUnion<[z.ZodDate, z.ZodString, z.ZodObject<{
42
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
43
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
44
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
45
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
46
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
47
- }, z.ZodTypeAny, "passthrough">>]>;
48
- updatedAt: z.ZodUnion<[z.ZodDate, z.ZodString, z.ZodObject<{
49
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
50
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
51
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
52
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
53
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
54
- }, z.ZodTypeAny, "passthrough">>]>;
55
- id: z.ZodString;
56
- buyerId: z.ZodString;
57
- buyerDisplayName: z.ZodString;
58
- storeId: z.ZodString;
59
- storeName: z.ZodString;
60
- sellerDisplayName: z.ZodString;
61
- productId: z.ZodOptional<z.ZodString>;
62
- productTitle: z.ZodOptional<z.ZodString>;
63
- messages: z.ZodArray<z.ZodObject<{
64
- id: z.ZodString;
65
- senderId: z.ZodString;
66
- senderRole: z.ZodEnum<["buyer", "seller"]>;
67
- body: z.ZodString;
68
- isRead: z.ZodBoolean;
69
- sentAt: z.ZodUnion<[z.ZodDate, z.ZodString, z.ZodObject<{
70
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
71
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
72
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
73
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
74
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
75
- }, z.ZodTypeAny, "passthrough">>]>;
76
- attachments: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
77
- }, "strip", z.ZodTypeAny, {
78
- id: string;
79
- body: string;
80
- isRead: boolean;
81
- senderId: string;
82
- senderRole: "buyer" | "seller";
83
- sentAt: string | Date | z.objectOutputType<{
84
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
85
- }, z.ZodTypeAny, "passthrough">;
86
- attachments?: string[] | undefined;
87
- }, {
88
- id: string;
89
- body: string;
90
- isRead: boolean;
91
- senderId: string;
92
- senderRole: "buyer" | "seller";
93
- sentAt: string | Date | z.objectInputType<{
94
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
95
- }, z.ZodTypeAny, "passthrough">;
96
- attachments?: string[] | undefined;
97
- }>, "many">;
98
- lastMessage: z.ZodString;
99
- lastMessageAt: z.ZodUnion<[z.ZodDate, z.ZodString, z.ZodObject<{
100
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
101
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
102
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
103
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
104
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
105
- }, z.ZodTypeAny, "passthrough">>]>;
106
- unreadBuyer: z.ZodNumber;
107
- unreadSeller: z.ZodNumber;
108
- status: z.ZodEnum<["active", "archived", "blocked"]>;
109
- }, "strip", z.ZodTypeAny, {
110
- status: "archived" | "active" | "blocked";
111
- id: string;
112
- createdAt: string | Date | z.objectOutputType<{
113
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
114
- }, z.ZodTypeAny, "passthrough">;
115
- updatedAt: string | Date | z.objectOutputType<{
116
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
117
- }, z.ZodTypeAny, "passthrough">;
118
- storeId: string;
119
- storeName: string;
120
- buyerId: string;
121
- lastMessageAt: string | Date | z.objectOutputType<{
122
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
123
- }, z.ZodTypeAny, "passthrough">;
124
- lastMessage: string;
125
- messages: {
126
- id: string;
127
- body: string;
128
- isRead: boolean;
129
- senderId: string;
130
- senderRole: "buyer" | "seller";
131
- sentAt: string | Date | z.objectOutputType<{
132
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
133
- }, z.ZodTypeAny, "passthrough">;
134
- attachments?: string[] | undefined;
135
- }[];
136
- buyerDisplayName: string;
137
- sellerDisplayName: string;
138
- unreadBuyer: number;
139
- unreadSeller: number;
140
- productId?: string | undefined;
141
- productTitle?: string | undefined;
142
- }, {
143
- status: "archived" | "active" | "blocked";
144
- id: string;
145
- createdAt: string | Date | z.objectInputType<{
146
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
147
- }, z.ZodTypeAny, "passthrough">;
148
- updatedAt: string | Date | z.objectInputType<{
149
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
150
- }, z.ZodTypeAny, "passthrough">;
151
- storeId: string;
152
- storeName: string;
153
- buyerId: string;
154
- lastMessageAt: string | Date | z.objectInputType<{
155
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
156
- }, z.ZodTypeAny, "passthrough">;
157
- lastMessage: string;
158
- messages: {
159
- id: string;
160
- body: string;
161
- isRead: boolean;
162
- senderId: string;
163
- senderRole: "buyer" | "seller";
164
- sentAt: string | Date | z.objectInputType<{
165
- toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
166
- }, z.ZodTypeAny, "passthrough">;
167
- attachments?: string[] | undefined;
168
- }[];
169
- buyerDisplayName: string;
170
- sellerDisplayName: string;
171
- unreadBuyer: number;
172
- unreadSeller: number;
173
- productId?: string | undefined;
174
- productTitle?: string | undefined;
175
- }>;
176
- export type ConversationFromSchema = z.infer<typeof conversationFirestoreSchema>;
177
- export type ConversationMessageFromSchema = z.infer<typeof conversationMessageSchema>;