@mohasinac/appkit 4.41.0 → 4.41.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/features/shell/SectionForm.js +44 -5
- package/package.json +2 -2
- 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,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";
|
|
@@ -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;
|