@mohasinac/appkit 4.41.0 → 4.41.2

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 (169) hide show
  1. package/dist/features/seller/schemas/coupon-form.d.ts +37 -37
  2. package/dist/features/seller/schemas/coupon-form.js +36 -8
  3. package/dist/features/shell/SectionForm.js +44 -5
  4. package/dist/schemas/registry.d.ts +10 -10
  5. package/package.json +2 -2
  6. package/dist/_internal/server/features/classified/actions.d.ts +0 -12
  7. package/dist/_internal/server/features/classified/actions.js +0 -33
  8. package/dist/_internal/server/features/digital-code/actions.d.ts +0 -12
  9. package/dist/_internal/server/features/digital-code/actions.js +0 -33
  10. package/dist/_internal/server/features/homepage/data.d.ts +0 -18
  11. package/dist/_internal/server/features/homepage/data.js +0 -70
  12. package/dist/_internal/server/features/live/actions.d.ts +0 -6
  13. package/dist/_internal/server/features/live/actions.js +0 -27
  14. package/dist/_internal/server/features/messages/actions.d.ts +0 -6
  15. package/dist/_internal/server/features/messages/actions.js +0 -6
  16. package/dist/_internal/server/features/messages/data.d.ts +0 -10
  17. package/dist/_internal/server/features/messages/data.js +0 -24
  18. package/dist/_internal/server/features/messages/index.d.ts +0 -2
  19. package/dist/_internal/server/features/messages/index.js +0 -2
  20. package/dist/_internal/server/jobs/core/testerSandboxRefresh.d.ts +0 -12
  21. package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +0 -112
  22. package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.d.ts +0 -2
  23. package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.js +0 -2
  24. package/dist/_internal/shared/features/homepage/section-gate.d.ts +0 -14
  25. package/dist/_internal/shared/features/homepage/section-gate.js +0 -51
  26. package/dist/_internal/shared/features/lottery/config-form.d.ts +0 -58
  27. package/dist/_internal/shared/features/lottery/config-form.js +0 -71
  28. package/dist/client/api/useFeatureFlags.d.ts +0 -22
  29. package/dist/client/api/useFeatureFlags.js +0 -27
  30. package/dist/features/account/address-validation.d.ts +0 -20
  31. package/dist/features/account/address-validation.js +0 -35
  32. package/dist/features/account/components/ChatList.d.ts +0 -14
  33. package/dist/features/account/components/ChatList.js +0 -9
  34. package/dist/features/account/components/ChatWindow.d.ts +0 -17
  35. package/dist/features/account/components/ChatWindow.js +0 -6
  36. package/dist/features/account/components/MessagesView.d.ts +0 -16
  37. package/dist/features/account/components/MessagesView.js +0 -5
  38. package/dist/features/account/components/UserOffersView.d.ts +0 -10
  39. package/dist/features/account/components/UserOffersView.js +0 -5
  40. package/dist/features/account/constants/addresses.d.ts +0 -13
  41. package/dist/features/account/constants/addresses.js +0 -43
  42. package/dist/features/account/hooks/useAddressForm.d.ts +0 -24
  43. package/dist/features/account/hooks/useAddressForm.js +0 -58
  44. package/dist/features/admin/actions/chat-actions.d.ts +0 -29
  45. package/dist/features/admin/actions/chat-actions.js +0 -101
  46. package/dist/features/admin/components/AdminFeatureFlagsView.d.ts +0 -9
  47. package/dist/features/admin/components/AdminFeatureFlagsView.js +0 -109
  48. package/dist/features/admin/components/AdminSiteView.d.ts +0 -10
  49. package/dist/features/admin/components/AdminSiteView.js +0 -52
  50. package/dist/features/admin/hooks/useChat.d.ts +0 -24
  51. package/dist/features/admin/hooks/useChat.js +0 -100
  52. package/dist/features/admin/permission-map.d.ts +0 -3
  53. package/dist/features/admin/permission-map.js +0 -26
  54. package/dist/features/admin/repository/chat.repository.d.ts +0 -50
  55. package/dist/features/admin/repository/chat.repository.js +0 -223
  56. package/dist/features/auth/actions/password-change-otp-actions.d.ts +0 -18
  57. package/dist/features/auth/actions/password-change-otp-actions.js +0 -88
  58. package/dist/features/auth/actions/realtime-token-actions.d.ts +0 -11
  59. package/dist/features/auth/actions/realtime-token-actions.js +0 -38
  60. package/dist/features/auth/password-change-otp.d.ts +0 -34
  61. package/dist/features/auth/password-change-otp.js +0 -56
  62. package/dist/features/classified/components/ClassifiedContactSellerPanel.d.ts +0 -12
  63. package/dist/features/classified/components/ClassifiedContactSellerPanel.js +0 -49
  64. package/dist/features/events/api/route.d.ts +0 -12
  65. package/dist/features/events/api/route.js +0 -90
  66. package/dist/features/faq/api/route.d.ts +0 -11
  67. package/dist/features/faq/api/route.js +0 -52
  68. package/dist/features/faq/hooks/useFAQs.d.ts +0 -22
  69. package/dist/features/faq/hooks/useFAQs.js +0 -41
  70. package/dist/features/media/upload/video-poster.test.d.ts +0 -1
  71. package/dist/features/media/upload/video-poster.test.js +0 -42
  72. package/dist/features/messages/actions/index.d.ts +0 -2
  73. package/dist/features/messages/actions/index.js +0 -2
  74. package/dist/features/messages/actions/messages-actions.d.ts +0 -15
  75. package/dist/features/messages/actions/messages-actions.js +0 -33
  76. package/dist/features/messages/actions/ping-rtdb.d.ts +0 -2
  77. package/dist/features/messages/actions/ping-rtdb.js +0 -26
  78. package/dist/features/messages/hooks/useConversation.d.ts +0 -22
  79. package/dist/features/messages/hooks/useConversation.js +0 -155
  80. package/dist/features/messages/hooks/useConversations.d.ts +0 -15
  81. package/dist/features/messages/hooks/useConversations.js +0 -104
  82. package/dist/features/messages/index.d.ts +0 -6
  83. package/dist/features/messages/index.js +0 -4
  84. package/dist/features/messages/realtime.d.ts +0 -32
  85. package/dist/features/messages/realtime.js +0 -39
  86. package/dist/features/messages/repository/conversations.repository.d.ts +0 -71
  87. package/dist/features/messages/repository/conversations.repository.js +0 -284
  88. package/dist/features/messages/schemas/firestore.d.ts +0 -34
  89. package/dist/features/messages/schemas/firestore.js +0 -16
  90. package/dist/features/messages/schemas/index.d.ts +0 -177
  91. package/dist/features/messages/schemas/index.js +0 -35
  92. package/dist/features/messages/server.d.ts +0 -6
  93. package/dist/features/messages/server.js +0 -6
  94. package/dist/features/products/components/MakeOfferForm.d.ts +0 -20
  95. package/dist/features/products/components/MakeOfferForm.js +0 -9
  96. package/dist/features/reviews/api/route.d.ts +0 -21
  97. package/dist/features/reviews/api/route.js +0 -174
  98. package/dist/features/seller/api/offers/route.d.ts +0 -17
  99. package/dist/features/seller/api/offers/route.js +0 -63
  100. package/dist/features/seller/components/SellerAuctionsView.d.ts +0 -9
  101. package/dist/features/seller/components/SellerAuctionsView.js +0 -113
  102. package/dist/features/seller/components/SellerOffersPanel.d.ts +0 -21
  103. package/dist/features/seller/components/SellerOffersPanel.js +0 -160
  104. package/dist/features/seller/components/SellerPreOrdersView.d.ts +0 -6
  105. package/dist/features/seller/components/SellerPreOrdersView.js +0 -148
  106. package/dist/features/seller/components/SellerPrizeDrawsView.d.ts +0 -6
  107. package/dist/features/seller/components/SellerPrizeDrawsView.js +0 -152
  108. package/dist/features/seller/permission-map.d.ts +0 -5
  109. package/dist/features/seller/permission-map.js +0 -17
  110. package/dist/features/shell/StepForm.d.ts +0 -80
  111. package/dist/features/shell/StepForm.js +0 -136
  112. package/dist/features/stores/api/route.d.ts +0 -12
  113. package/dist/features/stores/api/route.js +0 -110
  114. package/dist/features/tester/seed-data/authored/account-auth__testing-program--admin.d.ts +0 -2
  115. package/dist/features/tester/seed-data/authored/account-auth__testing-program--admin.js +0 -33
  116. package/dist/features/tester/seed-data/authored/account-auth__testing-program.d.ts +0 -2
  117. package/dist/features/tester/seed-data/authored/account-auth__testing-program.js +0 -138
  118. package/dist/features/tester/seed-data/bids-tester-seed-data.d.ts +0 -2
  119. package/dist/features/tester/seed-data/bids-tester-seed-data.js +0 -37
  120. package/dist/features/tester/seed-data/blog-tester-seed-data.d.ts +0 -2
  121. package/dist/features/tester/seed-data/blog-tester-seed-data.js +0 -40
  122. package/dist/features/tester/seed-data/carts-tester-seed-data.d.ts +0 -3
  123. package/dist/features/tester/seed-data/carts-tester-seed-data.js +0 -70
  124. package/dist/features/tester/seed-data/categories-tester-seed-data.d.ts +0 -2
  125. package/dist/features/tester/seed-data/categories-tester-seed-data.js +0 -218
  126. package/dist/features/tester/seed-data/coupons-tester-seed-data.d.ts +0 -2
  127. package/dist/features/tester/seed-data/coupons-tester-seed-data.js +0 -76
  128. package/dist/features/tester/seed-data/events-tester-seed-data.d.ts +0 -2
  129. package/dist/features/tester/seed-data/events-tester-seed-data.js +0 -193
  130. package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +0 -2
  131. package/dist/features/tester/seed-data/offers-tester-seed-data.js +0 -191
  132. package/dist/features/tester/seed-data/orders-tester-seed-data.d.ts +0 -2
  133. package/dist/features/tester/seed-data/orders-tester-seed-data.js +0 -329
  134. package/dist/features/tester/seed-data/products-tester-seed-data.d.ts +0 -2
  135. package/dist/features/tester/seed-data/products-tester-seed-data.js +0 -815
  136. package/dist/features/tester/seed-data/stores-tester-seed-data.d.ts +0 -2
  137. package/dist/features/tester/seed-data/stores-tester-seed-data.js +0 -79
  138. package/dist/features/wishlist/components/WishlistToggleButton.d.ts +0 -12
  139. package/dist/features/wishlist/components/WishlistToggleButton.js +0 -36
  140. package/dist/monitoring/analytics.d.ts +0 -80
  141. package/dist/monitoring/analytics.js +0 -176
  142. package/dist/next/middleware/pii-redactor.d.ts +0 -32
  143. package/dist/next/middleware/pii-redactor.js +0 -84
  144. package/dist/providers/db-firebase/realtime.d.ts +0 -42
  145. package/dist/providers/db-firebase/realtime.js +0 -138
  146. package/dist/security/rbac/Can.d.ts +0 -31
  147. package/dist/security/rbac/Can.js +0 -22
  148. package/dist/security/rbac/default-roles.d.ts +0 -2
  149. package/dist/security/rbac/default-roles.js +0 -83
  150. package/dist/security/rbac/hook.d.ts +0 -20
  151. package/dist/security/rbac/hook.js +0 -30
  152. package/dist/security/rbac/index.d.ts +0 -8
  153. package/dist/security/rbac/index.js +0 -6
  154. package/dist/security/rbac/middleware.d.ts +0 -51
  155. package/dist/security/rbac/middleware.js +0 -51
  156. package/dist/security/rbac/resolver.d.ts +0 -5
  157. package/dist/security/rbac/resolver.js +0 -38
  158. package/dist/security/rbac/server.d.ts +0 -24
  159. package/dist/security/rbac/server.js +0 -42
  160. package/dist/security/rbac/types.d.ts +0 -22
  161. package/dist/security/rbac/types.js +0 -2
  162. package/dist/seed/conversations-seed-data.d.ts +0 -2
  163. package/dist/seed/conversations-seed-data.js +0 -292
  164. package/dist/utils/filter.helper.d.ts +0 -18
  165. package/dist/utils/filter.helper.js +0 -21
  166. package/dist/utils/pagination.helper.d.ts +0 -23
  167. package/dist/utils/pagination.helper.js +0 -24
  168. package/dist/utils/search-tokens.d.ts +0 -2
  169. package/dist/utils/search-tokens.js +0 -21
@@ -1,90 +0,0 @@
1
- /**
2
- * feat-events — Next.js App Router API handler (GET /api/events)
3
- *
4
- * Consuming projects can create a 2-line stub:
5
- *
6
- * ```ts
7
- * // app/api/events/route.ts
8
- * export { GET } from "@mohasinac/feat-events";
9
- * ```
10
- */
11
- import { NextResponse } from "next/server.js";
12
- import { getProviders } from "../../../contracts";
13
- import { normalizeError } from "../../../errors/normalize";
14
- function param(url, key) {
15
- return url.searchParams.get(key);
16
- }
17
- function numParam(url, key, fallback) {
18
- const v = url.searchParams.get(key);
19
- const n = v !== null ? Number(v) : NaN;
20
- return Number.isFinite(n) ? n : fallback;
21
- }
22
- const SAFE_EVENT_FILTER_FIELDS = new Set([
23
- "status",
24
- "title",
25
- "type",
26
- "startsAt",
27
- "endsAt",
28
- "createdAt",
29
- ]);
30
- function validateSieveFilters(raw, allowedFields) {
31
- return raw
32
- .split(",")
33
- .map((c) => c.trim())
34
- .filter((c) => {
35
- const m = c.match(/^([^<>=!@]+)\s*(?:==|!=|<=|>=|<|>|@=\*?)/);
36
- return m ? allowedFields.has(m[1].trim()) : false;
37
- })
38
- .join(",");
39
- }
40
- // ——— GET /api/events ————————————————————————————————
41
- export async function GET(request) {
42
- try {
43
- const url = new URL(request.url);
44
- const page = numParam(url, "page", 1);
45
- const pageSize = numParam(url, "pageSize", 24);
46
- const sort = param(url, "sorts") ?? param(url, "sort") ?? "-startsAt";
47
- const q = param(url, "q");
48
- const raw = param(url, "filters");
49
- const safe = raw ? validateSieveFilters(raw, SAFE_EVENT_FILTER_FIELDS) : "";
50
- // Only default to active events when no explicit status filter is provided.
51
- const hasStatusFilter = safe.includes("status==");
52
- const parts = hasStatusFilter ? [] : ["status==active"];
53
- if (q)
54
- parts.push(`title@=*${q}`);
55
- if (safe)
56
- parts.push(safe);
57
- const filters = parts.join(",");
58
- const { db } = getProviders();
59
- if (!db) {
60
- return NextResponse.json({ success: false, error: "Database provider not registered" }, { status: 503 });
61
- }
62
- const repo = db.getRepository("events");
63
- const result = await repo.findAll({
64
- filters,
65
- sort,
66
- page,
67
- perPage: pageSize,
68
- });
69
- const totalPages = Math.max(1, Math.ceil(result.total / pageSize));
70
- // Strip internal fields (createdBy) before returning
71
- const items = result.data.map((event) => {
72
- const { createdBy: _createdBy, ...publicEvent } = event;
73
- return publicEvent;
74
- });
75
- const body = {
76
- items: items,
77
- total: result.total,
78
- page: result.page,
79
- pageSize,
80
- totalPages,
81
- hasMore: result.page < totalPages,
82
- };
83
- return NextResponse.json({ success: true, data: body });
84
- }
85
- catch (error) {
86
- void normalizeError(error);
87
- console.error("[feat-events] GET /api/events failed", error);
88
- return NextResponse.json({ success: false, error: "Failed to fetch events" }, { status: 500 });
89
- }
90
- }
@@ -1,11 +0,0 @@
1
- /**
2
- * feat-faq — Next.js App Router API handler (GET /api/faq)
3
- *
4
- * 2-line stub:
5
- * ```ts
6
- * // app/api/faq/route.ts
7
- * export { GET } from "@mohasinac/feat-faq";
8
- * ```
9
- */
10
- import { NextResponse } from "next/server.js";
11
- export declare function GET(request: Request): Promise<NextResponse>;
@@ -1,52 +0,0 @@
1
- /**
2
- * feat-faq — Next.js App Router API handler (GET /api/faq)
3
- *
4
- * 2-line stub:
5
- * ```ts
6
- * // app/api/faq/route.ts
7
- * export { GET } from "@mohasinac/feat-faq";
8
- * ```
9
- */
10
- import { NextResponse } from "next/server.js";
11
- import { getProviders } from "../../../contracts";
12
- import { normalizeError } from "../../../errors/normalize";
13
- function param(url, key) {
14
- return url.searchParams.get(key);
15
- }
16
- export async function GET(request) {
17
- try {
18
- const url = new URL(request.url);
19
- const parts = ["isActive==true"];
20
- const category = param(url, "category");
21
- if (category)
22
- parts.push(`category==${category}`);
23
- const homepage = param(url, "homepage");
24
- if (homepage === "true")
25
- parts.push("showOnHomepage==true");
26
- const q = param(url, "q");
27
- if (q)
28
- parts.push(`question@=*${q}`);
29
- const filters = parts.join(",");
30
- const { db } = getProviders();
31
- if (!db) {
32
- return NextResponse.json({ success: false, error: "Database provider not registered" }, { status: 503 });
33
- }
34
- const repo = db.getRepository("faqs");
35
- const result = await repo.findAll({
36
- filters,
37
- sort: "order",
38
- order: "asc",
39
- perPage: 100,
40
- });
41
- const body = {
42
- items: result.data,
43
- total: result.total,
44
- };
45
- return NextResponse.json({ success: true, data: body });
46
- }
47
- catch (error) {
48
- void normalizeError(error);
49
- console.error("[feat-faq] GET /api/faq failed", error);
50
- return NextResponse.json({ success: false, error: "Failed to fetch FAQs" }, { status: 500 });
51
- }
52
- }
@@ -1,22 +0,0 @@
1
- import type { FAQ, FAQListResponse, FAQListParams } from "../types";
2
- interface UseFAQsOptions {
3
- initialData?: FAQListResponse;
4
- enabled?: boolean;
5
- endpoint?: string;
6
- }
7
- export declare function useFAQs(params?: FAQListParams, opts?: UseFAQsOptions): {
8
- faqs: FAQ[];
9
- total: number;
10
- isLoading: boolean;
11
- error: Error | null;
12
- };
13
- export declare function useFAQ(id: string, opts?: {
14
- initialData?: FAQ;
15
- enabled?: boolean;
16
- endpoint?: string;
17
- }): {
18
- faq: FAQ | undefined;
19
- isLoading: boolean;
20
- error: Error | null;
21
- };
22
- export {};
@@ -1,41 +0,0 @@
1
- import { useQuery } from "@tanstack/react-query";
2
- import { apiClient } from "../../../http";
3
- import { FAQ_ENDPOINTS } from "../../../constants/api-endpoints";
4
- export function useFAQs(params = {}, opts) {
5
- const sp = new URLSearchParams();
6
- if (params.category)
7
- sp.set("category", params.category);
8
- if (params.homepage !== undefined)
9
- sp.set("homepage", String(params.homepage));
10
- if (params.q)
11
- sp.set("q", params.q);
12
- if (params.page)
13
- sp.set("page", String(params.page));
14
- if (params.perPage)
15
- sp.set("perPage", String(params.perPage));
16
- const qs = sp.toString();
17
- const endpoint = opts?.endpoint ?? `${FAQ_ENDPOINTS.LIST}${qs ? `?${qs}` : ""}`;
18
- const query = useQuery({
19
- queryKey: ["faqs", qs],
20
- queryFn: () => apiClient.get(endpoint),
21
- initialData: opts?.initialData,
22
- enabled: opts?.enabled,
23
- staleTime: 10 * 60 * 1000,
24
- });
25
- return {
26
- faqs: query.data?.items ?? [],
27
- total: query.data?.total ?? 0,
28
- isLoading: query.isLoading,
29
- error: query.error,
30
- };
31
- }
32
- export function useFAQ(id, opts) {
33
- const endpoint = opts?.endpoint ?? FAQ_ENDPOINTS.BY_ID(id);
34
- const query = useQuery({
35
- queryKey: ["faqs", id],
36
- queryFn: () => apiClient.get(endpoint),
37
- initialData: opts?.initialData,
38
- enabled: opts?.enabled !== false && !!id,
39
- });
40
- return { faq: query.data, isLoading: query.isLoading, error: query.error };
41
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,42 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { buildPosterFilename, extractVideoPosterFrame, posterBlobAsFile, } from "./video-poster";
3
- describe("buildPosterFilename", () => {
4
- it("strips the extension and appends .poster.jpg", () => {
5
- expect(buildPosterFilename("vid-123.mp4")).toBe("vid-123.poster.jpg");
6
- expect(buildPosterFilename("hero.webm")).toBe("hero.poster.jpg");
7
- expect(buildPosterFilename("Sunset.MOV")).toBe("Sunset.poster.jpg");
8
- });
9
- it("handles names with multiple dots — only the last segment is the extension", () => {
10
- expect(buildPosterFilename("clip.v2.mp4")).toBe("clip.v2.poster.jpg");
11
- expect(buildPosterFilename("2026.01.01-recap.webm")).toBe("2026.01.01-recap.poster.jpg");
12
- });
13
- it("works when the source has no extension", () => {
14
- expect(buildPosterFilename("untitled")).toBe("untitled.poster.jpg");
15
- });
16
- });
17
- describe("posterBlobAsFile", () => {
18
- it("wraps the blob as an image/jpeg File with the derived filename", () => {
19
- const blob = new Blob([new Uint8Array([0xff, 0xd8, 0xff])], {
20
- type: "image/jpeg",
21
- });
22
- const file = posterBlobAsFile({ blob, width: 320, height: 180, duration: 12.5 }, "promo-reel.mp4");
23
- expect(file).toBeInstanceOf(File);
24
- expect(file.name).toBe("promo-reel.poster.jpg");
25
- expect(file.type).toBe("image/jpeg");
26
- expect(file.size).toBe(3);
27
- });
28
- });
29
- describe("extractVideoPosterFrame — early-return paths", () => {
30
- it("returns null for a non-video File without touching the DOM", async () => {
31
- const file = new File([new Uint8Array([1, 2, 3])], "photo.jpg", {
32
- type: "image/jpeg",
33
- });
34
- const result = await extractVideoPosterFrame(file);
35
- expect(result).toBeNull();
36
- });
37
- it("returns null when the File has an empty MIME type", async () => {
38
- const file = new File([new Uint8Array([1])], "mystery.bin", { type: "" });
39
- const result = await extractVideoPosterFrame(file);
40
- expect(result).toBeNull();
41
- });
42
- });
@@ -1,2 +0,0 @@
1
- export * from "./messages-actions";
2
- export * from "./ping-rtdb";
@@ -1,2 +0,0 @@
1
- export * from "./messages-actions";
2
- export * from "./ping-rtdb";
@@ -1,15 +0,0 @@
1
- import { type ConversationsRepository } from "../repository/conversations.repository";
2
- import type { ConversationDocument, ConversationMessage } from "../schemas/firestore";
3
- export declare const MESSAGE_MAX_LENGTH = 2000;
4
- export declare function listConversationsForBuyer(buyerId: string): Promise<ConversationDocument[]>;
5
- export declare function listConversationsForStore(storeId: string): Promise<ConversationDocument[]>;
6
- export declare function getConversation(conversationId: string): Promise<ConversationDocument | null>;
7
- export interface SendMessageInput {
8
- conversationId: string;
9
- senderId: string;
10
- senderRole: "buyer" | "seller";
11
- body: string;
12
- }
13
- export declare function sendMessage(input: SendMessageInput): Promise<ConversationDocument>;
14
- export declare function markConversationRead(conversationId: string, role: "buyer" | "seller"): Promise<void>;
15
- export type { ConversationsRepository, ConversationDocument, ConversationMessage };
@@ -1,33 +0,0 @@
1
- /**
2
- * Messages domain actions — pure business functions; auth/rate-limit is the
3
- * caller's responsibility.
4
- */
5
- import { ValidationError } from "../../../errors";
6
- import { ERROR_MESSAGES } from "../../../errors/messages";
7
- import { conversationsRepository, } from "../repository/conversations.repository";
8
- export const MESSAGE_MAX_LENGTH = 2000;
9
- export async function listConversationsForBuyer(buyerId) {
10
- return conversationsRepository.listByBuyer(buyerId);
11
- }
12
- export async function listConversationsForStore(storeId) {
13
- return conversationsRepository.listByStore(storeId);
14
- }
15
- export async function getConversation(conversationId) {
16
- return conversationsRepository.findById(conversationId);
17
- }
18
- export async function sendMessage(input) {
19
- const body = input.body?.trim();
20
- if (!body)
21
- throw new ValidationError(ERROR_MESSAGES.CONVERSATIONS.BODY_REQUIRED);
22
- if (body.length > MESSAGE_MAX_LENGTH) {
23
- throw new ValidationError(`${ERROR_MESSAGES.CONVERSATIONS.BODY_TOO_LONG} (max ${MESSAGE_MAX_LENGTH} characters)`);
24
- }
25
- return conversationsRepository.appendMessage(input.conversationId, {
26
- senderId: input.senderId,
27
- senderRole: input.senderRole,
28
- body,
29
- });
30
- }
31
- export async function markConversationRead(conversationId, role) {
32
- await conversationsRepository.markRead(conversationId, role);
33
- }
@@ -1,2 +0,0 @@
1
- import { type ConversationPingTargets } from "../realtime";
2
- export declare function pingConversationRtdb(targets: ConversationPingTargets): Promise<void>;
@@ -1,26 +0,0 @@
1
- /**
2
- * pingConversationRtdb — server-side fan-out for conversation updates.
3
- *
4
- * Writes `Date.now()` to each ping path so RTDB-subscribed clients refetch.
5
- * Best-effort: failures are logged and swallowed (clients fall back to focus /
6
- * explicit refetch).
7
- */
8
- import { getAdminRealtimeDb } from "../../../providers/db-firebase";
9
- import { serverLogger } from "../../../monitoring";
10
- import { buildConversationPingPaths, } from "../realtime";
11
- import { normalizeError } from "../../../errors/normalize";
12
- export async function pingConversationRtdb(targets) {
13
- const paths = buildConversationPingPaths(targets);
14
- try {
15
- const db = getAdminRealtimeDb();
16
- const now = Date.now();
17
- await Promise.all(paths.filter(Boolean).map((p) => db.ref(p).set(now)));
18
- }
19
- catch (err) {
20
- void normalizeError(err);
21
- serverLogger.warn("conversations: RTDB ping failed", {
22
- paths,
23
- error: err instanceof Error ? err.message : String(err),
24
- });
25
- }
26
- }
@@ -1,22 +0,0 @@
1
- import { conversationPingPath, userConversationsPingPath } from "../realtime";
2
- import type { ConversationDocument } from "../schemas/firestore";
3
- /**
4
- * @deprecated Re-exported for backwards compat — import `conversationPingPath`
5
- * from `@mohasinac/appkit` instead.
6
- */
7
- export declare const CONVERSATIONS_PING_PATH: typeof conversationPingPath;
8
- /**
9
- * @deprecated Re-exported for backwards compat — import
10
- * `userConversationsPingPath` from `@mohasinac/appkit` instead.
11
- */
12
- export declare const CONVERSATIONS_PING_USER_PATH: typeof userConversationsPingPath;
13
- export interface UseConversationReturn {
14
- conversation: ConversationDocument | null;
15
- isLoading: boolean;
16
- error: Error | null;
17
- isConnected: boolean;
18
- sendMessage: (body: string) => Promise<void>;
19
- markRead: () => Promise<void>;
20
- refetch: () => Promise<void>;
21
- }
22
- export declare function useConversation(conversationId: string | null): UseConversationReturn;
@@ -1,155 +0,0 @@
1
- "use client";
2
- /**
3
- * useConversation — load + subscribe to a single conversation.
4
- *
5
- * Strategy: Firestore is the canonical store; messages live embedded in the
6
- * conversation document. The realtime layer is a single RTDB "ping" path
7
- * (`chats/{conversationId}/lastUpdate`) that the send/mark-read API routes
8
- * bump on every write. This hook subscribes to that ping and re-fetches the
9
- * doc through the API on each tick. Zero RTDB schema duplication and the
10
- * canonical Firestore message ordering wins.
11
- */
12
- import { useCallback, useEffect, useState } from "react";
13
- import { acquireRealtimeSession } from "../../../contracts/realtime-session";
14
- // One scope for both message hooks, deliberately: /api/realtime/token issues a
15
- // SINGLE token covering all of this user's conversationIds, so a per-id scope
16
- // would make the list and the detail hook re-sign against each other on the
17
- // same page for no gain.
18
- //
19
- // 🛑 Known limitation, not fixed here: because the scope is constant, a
20
- // conversation created AFTER sign-in is not in the cached token's claims and
21
- // its subscription is denied until the token's expiry skew forces a re-sign.
22
- // The denial is now surfaced (see the error callback below) rather than
23
- // swallowed. The durable fix is to re-acquire on a permission denial.
24
- const MESSAGES_SCOPE = "messages";
25
- import { conversationPingPath, userConversationsPingPath, } from "../realtime";
26
- import { normalizeError } from "../../../errors/normalize";
27
- import { apiClient, ApiClientError } from "../../../http/ApiClient";
28
- import { CONVERSATION_ENDPOINTS, ADMIN_ENDPOINTS, } from "../../../constants/api-endpoints";
29
- /**
30
- * @deprecated Re-exported for backwards compat — import `conversationPingPath`
31
- * from `@mohasinac/appkit` instead.
32
- */
33
- export const CONVERSATIONS_PING_PATH = conversationPingPath;
34
- /**
35
- * @deprecated Re-exported for backwards compat — import
36
- * `userConversationsPingPath` from `@mohasinac/appkit` instead.
37
- */
38
- export const CONVERSATIONS_PING_USER_PATH = userConversationsPingPath;
39
- const DETAIL_ENDPOINT = CONVERSATION_ENDPOINTS.BY_ID;
40
- const SEND_ENDPOINT = CONVERSATION_ENDPOINTS.MESSAGES;
41
- const READ_ENDPOINT = CONVERSATION_ENDPOINTS.READ;
42
- async function fetchDetail(id) {
43
- try {
44
- return await apiClient.get(DETAIL_ENDPOINT(id));
45
- }
46
- catch (err) {
47
- if (err instanceof ApiClientError && err.status === 404)
48
- return null;
49
- throw err;
50
- }
51
- }
52
- export function useConversation(conversationId) {
53
- const [conversation, setConversation] = useState(null);
54
- const [isLoading, setIsLoading] = useState(false);
55
- const [error, setError] = useState(null);
56
- const [isConnected, setIsConnected] = useState(false);
57
- const refetch = useCallback(async () => {
58
- if (!conversationId) {
59
- setConversation(null);
60
- return;
61
- }
62
- setIsLoading(true);
63
- setError(null);
64
- try {
65
- setConversation(await fetchDetail(conversationId));
66
- }
67
- catch (e) {
68
- void normalizeError(e);
69
- setError(e instanceof Error ? e : new Error(String(e)));
70
- }
71
- finally {
72
- setIsLoading(false);
73
- }
74
- }, [conversationId]);
75
- useEffect(() => {
76
- void refetch();
77
- }, [refetch]);
78
- useEffect(() => {
79
- if (!conversationId) {
80
- setIsConnected(false);
81
- return;
82
- }
83
- let unsubscribe;
84
- let release;
85
- let cancelled = false;
86
- // 🛑 Sign in BEFORE subscribing. Without a session `auth == null`, and the
87
- // `chats/$conversationId` rule requires
88
- // `auth.token.conversationIds[$conversationId]` — a claim that, until this
89
- // change, NOTHING issued (the token route only ever sent `chatIds`, from
90
- // the unrelated `chatRooms` collection). Two stacked faults on one channel:
91
- // no sign-in, and a claim with no issuer. The recipient of a message never
92
- // saw it until they manually refreshed; the sender only saw their own
93
- // because `send()` calls `refetch()` directly.
94
- void (async () => {
95
- try {
96
- const lease = await acquireRealtimeSession(MESSAGES_SCOPE, () => apiClient.post(ADMIN_ENDPOINTS.REALTIME_TOKEN, {}));
97
- release = lease.release;
98
- if (cancelled) {
99
- release();
100
- return;
101
- }
102
- // Subscribe through the LEASE's provider — it is bound to the app this
103
- // scope authenticated on. The global provider may be on a different app
104
- // carrying another channel's claims.
105
- unsubscribe = lease.provider.subscribe(conversationPingPath(conversationId), () => {
106
- setIsConnected(true);
107
- void refetch();
108
- }, (err) => {
109
- void normalizeError(err);
110
- setIsConnected(false);
111
- });
112
- }
113
- catch (_err) {
114
- void normalizeError(_err);
115
- // Provider unregistered or token fetch failed — falls back to the
116
- // one-shot fetch above.
117
- setIsConnected(false);
118
- }
119
- })();
120
- return () => {
121
- cancelled = true;
122
- setIsConnected(false);
123
- unsubscribe?.();
124
- release?.();
125
- };
126
- }, [conversationId, refetch]);
127
- const sendMessage = useCallback(async (body) => {
128
- if (!conversationId)
129
- return;
130
- const trimmed = body.trim();
131
- if (!trimmed)
132
- return;
133
- await apiClient.post(SEND_ENDPOINT(conversationId), { body: trimmed });
134
- // RTDB ping will trigger refetch — but kick off one immediately in case
135
- // the ping path isn't subscribed (e.g. provider not registered).
136
- await refetch();
137
- }, [conversationId, refetch]);
138
- const markRead = useCallback(() => {
139
- if (!conversationId)
140
- return Promise.resolve();
141
- return apiClient
142
- .post(READ_ENDPOINT(conversationId))
143
- .then(() => void refetch())
144
- .catch((err) => void normalizeError(err));
145
- }, [conversationId, refetch]);
146
- return {
147
- conversation,
148
- isLoading,
149
- error,
150
- isConnected,
151
- sendMessage,
152
- markRead,
153
- refetch,
154
- };
155
- }
@@ -1,15 +0,0 @@
1
- import type { ConversationDocument } from "../schemas/firestore";
2
- export interface UseConversationsOptions {
3
- /** Override the list endpoint — e.g. the seller-scoped /api/store/conversations. Defaults to the buyer endpoint. */
4
- endpoint?: string;
5
- /** Which per-conversation unread counter to sum into totalUnread. Defaults to "unreadBuyer". */
6
- unreadField?: "unreadBuyer" | "unreadSeller";
7
- }
8
- export interface UseConversationsReturn {
9
- conversations: ConversationDocument[];
10
- isLoading: boolean;
11
- error: Error | null;
12
- totalUnread: number;
13
- refetch: () => Promise<void>;
14
- }
15
- export declare function useConversations(userId: string | null | undefined, opts?: UseConversationsOptions): UseConversationsReturn;
@@ -1,104 +0,0 @@
1
- "use client";
2
- /**
3
- * useConversations — list the current buyer's conversations. Subscribes to a
4
- * single RTDB ping path so the list refreshes whenever any of the user's
5
- * conversations gets a new message.
6
- */
7
- import { useCallback, useEffect, useState } from "react";
8
- import { acquireRealtimeSession } from "../../../contracts/realtime-session";
9
- // One scope for both message hooks, deliberately: /api/realtime/token issues a
10
- // SINGLE token covering all of this user's conversationIds, so a per-id scope
11
- // would make the list and the detail hook re-sign against each other on the
12
- // same page for no gain.
13
- //
14
- // 🛑 Known limitation, not fixed here: because the scope is constant, a
15
- // conversation created AFTER sign-in is not in the cached token's claims and
16
- // its subscription is denied until the token's expiry skew forces a re-sign.
17
- // The denial is now surfaced (see the error callback below) rather than
18
- // swallowed. The durable fix is to re-acquire on a permission denial.
19
- const MESSAGES_SCOPE = "messages";
20
- import { userConversationsPingPath } from "../realtime";
21
- import { normalizeError } from "../../../errors/normalize";
22
- import { apiClient } from "../../../http/ApiClient";
23
- import { CONVERSATION_ENDPOINTS, ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
24
- const LIST_ENDPOINT = CONVERSATION_ENDPOINTS.LIST;
25
- async function fetchList(endpoint) {
26
- const data = await apiClient.get(endpoint);
27
- return data.items ?? [];
28
- }
29
- export function useConversations(userId, opts = {}) {
30
- const { endpoint = LIST_ENDPOINT, unreadField = "unreadBuyer" } = opts;
31
- const [conversations, setConversations] = useState([]);
32
- const [isLoading, setIsLoading] = useState(false);
33
- const [error, setError] = useState(null);
34
- const refetch = useCallback(async () => {
35
- if (!userId) {
36
- setConversations([]);
37
- return;
38
- }
39
- setIsLoading(true);
40
- setError(null);
41
- try {
42
- setConversations(await fetchList(endpoint));
43
- }
44
- catch (e) {
45
- void normalizeError(e);
46
- setError(e instanceof Error ? e : new Error(String(e)));
47
- }
48
- finally {
49
- setIsLoading(false);
50
- }
51
- }, [userId, endpoint]);
52
- useEffect(() => {
53
- void refetch();
54
- }, [refetch]);
55
- // Subscribe to the per-user ping path — any send/mark-read in any of the
56
- // user's conversations bumps this and triggers a fresh list fetch.
57
- useEffect(() => {
58
- if (!userId)
59
- return;
60
- let unsubscribe;
61
- let release;
62
- let cancelled = false;
63
- // 🛑 Sign in BEFORE subscribing. This hook used to call subscribe() with no
64
- // authenticated session at all, so `auth == null` and the
65
- // `chats/user/$userId` rule (`auth.uid == $userId`) could never pass. Every
66
- // update was permission-denied and the error handler below was a literal
67
- // `// ignore`, so the whole live-list channel was inert while looking wired.
68
- void (async () => {
69
- try {
70
- const lease = await acquireRealtimeSession(MESSAGES_SCOPE, () => apiClient.post(ADMIN_ENDPOINTS.REALTIME_TOKEN, {}));
71
- release = lease.release;
72
- if (cancelled) {
73
- release();
74
- return;
75
- }
76
- // Subscribe through the LEASE's provider — see useConversation.
77
- unsubscribe = lease.provider.subscribe(userConversationsPingPath(userId), () => {
78
- void refetch();
79
- }, (err) => {
80
- // A denied subscription is a real defect — silence is what hid this
81
- // channel being dead for months — so it is logged, not swallowed.
82
- // (This comment previously claimed "Surfaced, not ignored" while the
83
- // body only called normalizeError, which classifies without
84
- // reporting. That was the same documentation-that-lies pattern this
85
- // workstream exists to remove, so it now actually logs.)
86
- void normalizeError(err);
87
- console.warn("[messages] conversations live-update subscription denied; list falls back to fetch-on-load");
88
- });
89
- // Provider unregistered or token fetch failed — non-fatal; the list
90
- // still works via the one-shot fetch above, just without live updates.
91
- }
92
- catch (_err) {
93
- void normalizeError(_err);
94
- }
95
- })();
96
- return () => {
97
- cancelled = true;
98
- unsubscribe?.();
99
- release?.();
100
- };
101
- }, [userId, refetch]);
102
- const totalUnread = conversations.reduce((sum, c) => sum + (c[unreadField] ?? 0), 0);
103
- return { conversations, isLoading, error, totalUnread, refetch };
104
- }
@@ -1,6 +0,0 @@
1
- export * from "./hooks/useConversations";
2
- export * from "./hooks/useConversation";
3
- export type { ConversationDocument, ConversationMessage, } from "./schemas/firestore";
4
- export { CONVERSATIONS_COLLECTION, CONVERSATIONS_INDEXED_FIELDS, } from "./schemas/firestore";
5
- export { conversationPingPath, userConversationsPingPath, buildConversationPingPaths, } from "./realtime";
6
- export type { ConversationPingTargets } from "./realtime";