@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.
- package/dist/features/categories/repository/categories.repository.js +2 -2
- package/dist/features/shell/SectionForm.js +44 -5
- package/dist/features/tester/seed-data/_money-flows.js +65 -49
- package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +6 -4
- package/dist/features/tester/seed-data/authored/admin__coupons.js +14 -13
- package/dist/features/tester/seed-data/authored/buying__bidding.js +154 -140
- package/dist/features/tester/seed-data/authored/buying__browsing-search.js +4 -4
- package/dist/features/tester/seed-data/authored/buying__buying-checkout.js +59 -56
- package/dist/features/tester/seed-data/authored/buying__buying-coupons.js +18 -18
- package/dist/features/tester/seed-data/authored/buying__cart.js +105 -79
- package/dist/features/tester/seed-data/authored/buying__image-tile-layout.js +6 -8
- package/dist/features/tester/seed-data/authored/buying__my-orders.js +13 -13
- package/dist/features/tester/seed-data/authored/buying__offers.js +6 -6
- package/dist/features/tester/seed-data/authored/buying__order-detail-actions.js +8 -7
- package/dist/features/tester/seed-data/authored/buying__product-detail.js +62 -56
- package/dist/features/tester/seed-data/authored/buying__return-request.js +6 -6
- package/dist/features/tester/seed-data/authored/buying__reviews-pagination.js +4 -4
- package/dist/features/tester/seed-data/authored/buying__reviews.js +1 -1
- package/dist/features/tester/seed-data/authored/buying__user-dashboard-extras.js +8 -8
- package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +1 -1
- package/dist/features/tester/seed-data/authored/content-discovery__coupons.js +29 -16
- package/dist/features/tester/seed-data/authored/content-discovery__event-detail-subroutes.js +9 -9
- package/dist/features/tester/seed-data/authored/content-discovery__events.js +69 -36
- package/dist/features/tester/seed-data/authored/content-discovery__notifications.js +1 -1
- package/dist/features/tester/seed-data/authored/cta-layout__checkout-bottom-bar.js +2 -2
- package/dist/features/tester/seed-data/authored/design-ux__back-to-top-button.js +2 -2
- package/dist/features/tester/seed-data/authored/design-ux__general-design.js +1 -1
- package/dist/features/tester/seed-data/authored/design-ux__status-badge-legibility.js +1 -1
- package/dist/features/tester/seed-data/authored/design-ux__sticky-cta-bar.js +3 -3
- package/dist/features/tester/seed-data/authored/index.js +0 -2
- package/dist/features/tester/seed-data/authored/page-wiring__data-loss.js +2 -2
- package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +6 -4
- package/dist/features/tester/seed-data/authored/public-pages__auth-error-pages.js +2 -2
- package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.js +1 -1
- package/dist/features/tester/seed-data/authored/public-pages__stores-sellers-directories.js +8 -10
- package/dist/features/tester/seed-data/authored/selling__become-seller.js +2 -2
- package/dist/features/tester/seed-data/authored/selling__digital-content-delivery.js +2 -2
- package/dist/features/tester/seed-data/authored/selling__listing-a-product.js +2 -2
- package/dist/features/tester/seed-data/authored/selling__listing-edit-roundtrip.js +2 -2
- package/dist/features/tester/seed-data/authored/selling__listing-lifecycle.js +2 -2
- package/dist/features/tester/seed-data/authored/selling__seller-listing-types.js +2 -2
- package/dist/features/tester/seed-data/authored/seo__og-images.js +3 -4
- package/dist/features/tester/seed-data/authored/seo__page-metadata.js +6 -8
- package/dist/features/tester/seed-data/authored/seo__sitemap-and-robots.js +5 -5
- package/dist/features/tester/seed-data/index.d.ts +0 -10
- package/dist/features/tester/seed-data/index.js +0 -10
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +75 -89
- package/dist/features/tester/seed-data/tester-responses-seed-data.js +5 -5
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -21
- package/dist/seed/addresses-seed-data.js +0 -34
- package/dist/seed/index.d.ts +1 -1
- package/dist/seed/index.js +4 -3
- package/dist/seed/manifest.js +11 -12
- package/dist/seed/notifications-seed-data.js +4 -4
- package/dist/seed/stores-seed-data.js +7 -52
- package/dist/seed/users-seed-data.js +3 -83
- package/package.json +2 -2
- package/scripts/seed-cli.mjs +15 -20
- package/dist/_internal/server/features/classified/actions.d.ts +0 -12
- package/dist/_internal/server/features/classified/actions.js +0 -33
- package/dist/_internal/server/features/digital-code/actions.d.ts +0 -12
- package/dist/_internal/server/features/digital-code/actions.js +0 -33
- package/dist/_internal/server/features/homepage/data.d.ts +0 -18
- package/dist/_internal/server/features/homepage/data.js +0 -70
- package/dist/_internal/server/features/live/actions.d.ts +0 -6
- package/dist/_internal/server/features/live/actions.js +0 -27
- package/dist/_internal/server/features/messages/actions.d.ts +0 -6
- package/dist/_internal/server/features/messages/actions.js +0 -6
- package/dist/_internal/server/features/messages/data.d.ts +0 -10
- package/dist/_internal/server/features/messages/data.js +0 -24
- package/dist/_internal/server/features/messages/index.d.ts +0 -2
- package/dist/_internal/server/features/messages/index.js +0 -2
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.d.ts +0 -12
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +0 -112
- package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.js +0 -2
- package/dist/_internal/shared/features/homepage/section-gate.d.ts +0 -14
- package/dist/_internal/shared/features/homepage/section-gate.js +0 -51
- package/dist/_internal/shared/features/lottery/config-form.d.ts +0 -58
- package/dist/_internal/shared/features/lottery/config-form.js +0 -71
- package/dist/client/api/useFeatureFlags.d.ts +0 -22
- package/dist/client/api/useFeatureFlags.js +0 -27
- package/dist/features/account/address-validation.d.ts +0 -20
- package/dist/features/account/address-validation.js +0 -35
- package/dist/features/account/components/ChatList.d.ts +0 -14
- package/dist/features/account/components/ChatList.js +0 -9
- package/dist/features/account/components/ChatWindow.d.ts +0 -17
- package/dist/features/account/components/ChatWindow.js +0 -6
- package/dist/features/account/components/MessagesView.d.ts +0 -16
- package/dist/features/account/components/MessagesView.js +0 -5
- package/dist/features/account/components/UserOffersView.d.ts +0 -10
- package/dist/features/account/components/UserOffersView.js +0 -5
- package/dist/features/account/constants/addresses.d.ts +0 -13
- package/dist/features/account/constants/addresses.js +0 -43
- package/dist/features/account/hooks/useAddressForm.d.ts +0 -24
- package/dist/features/account/hooks/useAddressForm.js +0 -58
- package/dist/features/admin/actions/chat-actions.d.ts +0 -29
- package/dist/features/admin/actions/chat-actions.js +0 -101
- package/dist/features/admin/components/AdminFeatureFlagsView.d.ts +0 -9
- package/dist/features/admin/components/AdminFeatureFlagsView.js +0 -109
- package/dist/features/admin/components/AdminSiteView.d.ts +0 -10
- package/dist/features/admin/components/AdminSiteView.js +0 -52
- package/dist/features/admin/hooks/useChat.d.ts +0 -24
- package/dist/features/admin/hooks/useChat.js +0 -100
- package/dist/features/admin/permission-map.d.ts +0 -3
- package/dist/features/admin/permission-map.js +0 -26
- package/dist/features/admin/repository/chat.repository.d.ts +0 -50
- package/dist/features/admin/repository/chat.repository.js +0 -223
- package/dist/features/auth/actions/password-change-otp-actions.d.ts +0 -18
- package/dist/features/auth/actions/password-change-otp-actions.js +0 -88
- package/dist/features/auth/actions/realtime-token-actions.d.ts +0 -11
- package/dist/features/auth/actions/realtime-token-actions.js +0 -38
- package/dist/features/auth/password-change-otp.d.ts +0 -34
- package/dist/features/auth/password-change-otp.js +0 -56
- package/dist/features/classified/components/ClassifiedContactSellerPanel.d.ts +0 -12
- package/dist/features/classified/components/ClassifiedContactSellerPanel.js +0 -49
- package/dist/features/events/api/route.d.ts +0 -12
- package/dist/features/events/api/route.js +0 -90
- package/dist/features/faq/api/route.d.ts +0 -11
- package/dist/features/faq/api/route.js +0 -52
- package/dist/features/faq/hooks/useFAQs.d.ts +0 -22
- package/dist/features/faq/hooks/useFAQs.js +0 -41
- package/dist/features/media/upload/video-poster.test.d.ts +0 -1
- package/dist/features/media/upload/video-poster.test.js +0 -42
- package/dist/features/messages/actions/index.d.ts +0 -2
- package/dist/features/messages/actions/index.js +0 -2
- package/dist/features/messages/actions/messages-actions.d.ts +0 -15
- package/dist/features/messages/actions/messages-actions.js +0 -33
- package/dist/features/messages/actions/ping-rtdb.d.ts +0 -2
- package/dist/features/messages/actions/ping-rtdb.js +0 -26
- package/dist/features/messages/hooks/useConversation.d.ts +0 -22
- package/dist/features/messages/hooks/useConversation.js +0 -155
- package/dist/features/messages/hooks/useConversations.d.ts +0 -15
- package/dist/features/messages/hooks/useConversations.js +0 -104
- package/dist/features/messages/index.d.ts +0 -6
- package/dist/features/messages/index.js +0 -4
- package/dist/features/messages/realtime.d.ts +0 -32
- package/dist/features/messages/realtime.js +0 -39
- package/dist/features/messages/repository/conversations.repository.d.ts +0 -71
- package/dist/features/messages/repository/conversations.repository.js +0 -284
- package/dist/features/messages/schemas/firestore.d.ts +0 -34
- package/dist/features/messages/schemas/firestore.js +0 -16
- package/dist/features/messages/schemas/index.d.ts +0 -177
- package/dist/features/messages/schemas/index.js +0 -35
- package/dist/features/messages/server.d.ts +0 -6
- package/dist/features/messages/server.js +0 -6
- package/dist/features/products/components/MakeOfferForm.d.ts +0 -20
- package/dist/features/products/components/MakeOfferForm.js +0 -9
- package/dist/features/reviews/api/route.d.ts +0 -21
- package/dist/features/reviews/api/route.js +0 -174
- package/dist/features/seller/api/offers/route.d.ts +0 -17
- package/dist/features/seller/api/offers/route.js +0 -63
- package/dist/features/seller/components/SellerAuctionsView.d.ts +0 -9
- package/dist/features/seller/components/SellerAuctionsView.js +0 -113
- package/dist/features/seller/components/SellerOffersPanel.d.ts +0 -21
- package/dist/features/seller/components/SellerOffersPanel.js +0 -160
- package/dist/features/seller/components/SellerPreOrdersView.d.ts +0 -6
- package/dist/features/seller/components/SellerPreOrdersView.js +0 -148
- package/dist/features/seller/components/SellerPrizeDrawsView.d.ts +0 -6
- package/dist/features/seller/components/SellerPrizeDrawsView.js +0 -152
- package/dist/features/seller/permission-map.d.ts +0 -5
- package/dist/features/seller/permission-map.js +0 -17
- package/dist/features/shell/StepForm.d.ts +0 -80
- package/dist/features/shell/StepForm.js +0 -136
- package/dist/features/stores/api/route.d.ts +0 -12
- package/dist/features/stores/api/route.js +0 -110
- package/dist/features/tester/seed-data/authored/account-auth__testing-program--admin.d.ts +0 -2
- package/dist/features/tester/seed-data/authored/account-auth__testing-program--admin.js +0 -33
- package/dist/features/tester/seed-data/authored/account-auth__testing-program.d.ts +0 -2
- package/dist/features/tester/seed-data/authored/account-auth__testing-program.js +0 -138
- package/dist/features/tester/seed-data/bids-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/bids-tester-seed-data.js +0 -37
- package/dist/features/tester/seed-data/blog-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/blog-tester-seed-data.js +0 -40
- package/dist/features/tester/seed-data/carts-tester-seed-data.d.ts +0 -3
- package/dist/features/tester/seed-data/carts-tester-seed-data.js +0 -70
- package/dist/features/tester/seed-data/categories-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/categories-tester-seed-data.js +0 -218
- package/dist/features/tester/seed-data/coupons-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/coupons-tester-seed-data.js +0 -76
- package/dist/features/tester/seed-data/events-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/events-tester-seed-data.js +0 -193
- package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/offers-tester-seed-data.js +0 -191
- package/dist/features/tester/seed-data/orders-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/orders-tester-seed-data.js +0 -329
- package/dist/features/tester/seed-data/products-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/products-tester-seed-data.js +0 -815
- package/dist/features/tester/seed-data/stores-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/stores-tester-seed-data.js +0 -79
- package/dist/features/wishlist/components/WishlistToggleButton.d.ts +0 -12
- package/dist/features/wishlist/components/WishlistToggleButton.js +0 -36
- package/dist/monitoring/analytics.d.ts +0 -80
- package/dist/monitoring/analytics.js +0 -176
- package/dist/next/middleware/pii-redactor.d.ts +0 -32
- package/dist/next/middleware/pii-redactor.js +0 -84
- package/dist/providers/db-firebase/realtime.d.ts +0 -42
- package/dist/providers/db-firebase/realtime.js +0 -138
- package/dist/security/rbac/Can.d.ts +0 -31
- package/dist/security/rbac/Can.js +0 -22
- package/dist/security/rbac/default-roles.d.ts +0 -2
- package/dist/security/rbac/default-roles.js +0 -83
- package/dist/security/rbac/hook.d.ts +0 -20
- package/dist/security/rbac/hook.js +0 -30
- package/dist/security/rbac/index.d.ts +0 -8
- package/dist/security/rbac/index.js +0 -6
- package/dist/security/rbac/middleware.d.ts +0 -51
- package/dist/security/rbac/middleware.js +0 -51
- package/dist/security/rbac/resolver.d.ts +0 -5
- package/dist/security/rbac/resolver.js +0 -38
- package/dist/security/rbac/server.d.ts +0 -24
- package/dist/security/rbac/server.js +0 -42
- package/dist/security/rbac/types.d.ts +0 -22
- package/dist/security/rbac/types.js +0 -2
- package/dist/seed/conversations-seed-data.d.ts +0 -2
- package/dist/seed/conversations-seed-data.js +0 -292
- package/dist/utils/filter.helper.d.ts +0 -18
- package/dist/utils/filter.helper.js +0 -21
- package/dist/utils/pagination.helper.d.ts +0 -23
- package/dist/utils/pagination.helper.js +0 -24
- package/dist/utils/search-tokens.d.ts +0 -2
- 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,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,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";
|