perf(client): defer chat persistence dependencies
This commit is contained in:
@@ -30,17 +30,17 @@ Compressed sizes below are Turbopack module estimates. `Eager` is the route's sy
|
|||||||
|
|
||||||
| Route | Direct-import baseline | Current eager baseline | Change | Current async baseline |
|
| Route | Direct-import baseline | Current eager baseline | Change | Current async baseline |
|
||||||
| --- | ---: | ---: | ---: | ---: |
|
| --- | ---: | ---: | ---: | ---: |
|
||||||
| `/splash` | 646.2 KiB | 649.5 KiB | +3.3 KiB | 14.3 KiB |
|
| `/splash` | 646.2 KiB | 599.7 KiB | -46.5 KiB | 49.2 KiB |
|
||||||
| `/chat` | 669.8 KiB | 672.8 KiB | +3.0 KiB | 14.7 KiB |
|
| `/chat` | 669.8 KiB | 637.8 KiB | -32.0 KiB | 49.6 KiB |
|
||||||
| `/subscription` | 658.4 KiB | 662.0 KiB | +3.6 KiB | 20.6 KiB |
|
| `/subscription` | 658.4 KiB | 616.9 KiB | -41.5 KiB | 20.6 KiB |
|
||||||
| `/tip` | 651.6 KiB | 656.0 KiB | +4.4 KiB | 20.6 KiB |
|
| `/tip` | 651.6 KiB | 610.9 KiB | -40.7 KiB | 20.6 KiB |
|
||||||
|
|
||||||
The earlier direct-import pass replaced root `@/utils` and `@/data/storage`
|
The earlier direct-import pass replaced root `@/utils` and `@/data/storage`
|
||||||
barrel imports with symbol-level module paths. The current column captures the
|
barrel imports with symbol-level module paths. The current Client-boundary pass
|
||||||
full application after the subsequent feature work and is the baseline enforced
|
separates the Payment → User and Payment → Chat synchronizers, uses direct
|
||||||
by the budgets below. Dexie and UA Parser remain in the analyzed routes through
|
provider imports, and loads the Dexie-backed chat repository through a dynamic
|
||||||
active feature dependencies and require separate feature-boundary work to
|
import. UA Parser remains an intentional eager dependency because the app still
|
||||||
remove from the eager graph.
|
requires its general browser, OS, and device detection behavior.
|
||||||
|
|
||||||
Module loading baseline:
|
Module loading baseline:
|
||||||
|
|
||||||
@@ -51,6 +51,8 @@ Module loading baseline:
|
|||||||
| Stripe payment dialog | eager | async |
|
| Stripe payment dialog | eager | async |
|
||||||
| Chat reply animation | eager | async |
|
| Chat reply animation | eager | async |
|
||||||
| `lottie-react` | dependency present, not bundled | removed |
|
| `lottie-react` | dependency present, not bundled | removed |
|
||||||
|
| `ua-parser-js` | eager | eager (retained intentionally) |
|
||||||
|
| Dexie | eager on all baseline routes | async on `/splash` and `/chat`; absent on `/subscription` and `/tip` |
|
||||||
|
|
||||||
The total potential graph can grow slightly because an async chunk has its own loader metadata. The primary budget is the eager graph and the audited module phase, not the sum of eager and unopened async code.
|
The total potential graph can grow slightly because an async chunk has its own loader metadata. The primary budget is the eager graph and the audited module phase, not the sum of eager and unopened async code.
|
||||||
|
|
||||||
@@ -58,10 +60,10 @@ The initial budgets are intentionally above the baseline and are enforced by CI:
|
|||||||
|
|
||||||
| Route | Eager client JS budget |
|
| Route | Eager client JS budget |
|
||||||
| --- | ---: |
|
| --- | ---: |
|
||||||
| `/splash` | 650 KiB |
|
| `/splash` | 605 KiB |
|
||||||
| `/chat` | 675 KiB |
|
| `/chat` | 645 KiB |
|
||||||
| `/subscription` | 665 KiB |
|
| `/subscription` | 625 KiB |
|
||||||
| `/tip` | 660 KiB |
|
| `/tip` | 620 KiB |
|
||||||
|
|
||||||
## Lab Web Vitals baseline
|
## Lab Web Vitals baseline
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,21 @@ const routes = parseList(
|
|||||||
const enforceBudgets = process.env.PERF_BUNDLE_ENFORCE === "1";
|
const enforceBudgets = process.env.PERF_BUNDLE_ENFORCE === "1";
|
||||||
|
|
||||||
const eagerClientJsBudgets = new Map([
|
const eagerClientJsBudgets = new Map([
|
||||||
["/splash", 650 * 1024],
|
["/splash", 605 * 1024],
|
||||||
["/chat", 675 * 1024],
|
["/chat", 645 * 1024],
|
||||||
["/subscription", 665 * 1024],
|
["/subscription", 625 * 1024],
|
||||||
["/tip", 660 * 1024],
|
["/tip", 620 * 1024],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const auditTargets = [
|
const auditTargets = [
|
||||||
|
{
|
||||||
|
name: "UA parser",
|
||||||
|
matches: (path) => path.includes("/node_modules/ua-parser-js/"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Dexie",
|
||||||
|
matches: (path) => path.includes("/node_modules/dexie/"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Stripe React SDK",
|
name: "Stripe React SDK",
|
||||||
matches: (path) => path.includes("/node_modules/@stripe/react-stripe-js/"),
|
matches: (path) => path.includes("/node_modules/@stripe/react-stripe-js/"),
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import { shallowEqual } from "@xstate/react";
|
|||||||
|
|
||||||
import type { LoginStatus } from "@/data/dto/auth";
|
import type { LoginStatus } from "@/data/dto/auth";
|
||||||
import { useAppNavigator } from "@/router/use-app-navigator";
|
import { useAppNavigator } from "@/router/use-app-navigator";
|
||||||
import { usePaymentDispatch, usePaymentSelector } from "@/stores/payment";
|
import {
|
||||||
|
usePaymentDispatch,
|
||||||
|
usePaymentSelector,
|
||||||
|
} from "@/stores/payment/payment-context";
|
||||||
import { useUserSelector } from "@/stores/user/user-context";
|
import { useUserSelector } from "@/stores/user/user-context";
|
||||||
import { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel";
|
import { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import type { IChatRepository } from "./interfaces/ichat_repository";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the browser-only chat repository behind an explicit async boundary.
|
||||||
|
*
|
||||||
|
* The repository owns the Dexie-backed history and media stores. Keeping this
|
||||||
|
* loader separate prevents those stores from entering every route's eager
|
||||||
|
* client graph through shared providers.
|
||||||
|
*/
|
||||||
|
export async function loadChatRepository(): Promise<IChatRepository> {
|
||||||
|
const { getChatRepository } = await import("./chat_repository");
|
||||||
|
return getChatRepository();
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { ChatMediaKind } from "@/data/dto/chat";
|
import type { ChatMediaKind } from "@/data/dto/chat";
|
||||||
import { getChatRepository } from "@/data/repositories/chat_repository";
|
import { loadChatRepository } from "@/data/repositories/chat_repository_loader";
|
||||||
import { Result, type Result as ResultT } from "@/utils/result";
|
import { Result, type Result as ResultT } from "@/utils/result";
|
||||||
|
|
||||||
import { localChatMediaRowToBlob } from "./chat_media_blob";
|
import { localChatMediaRowToBlob } from "./chat_media_blob";
|
||||||
@@ -19,7 +19,8 @@ export interface CacheRemoteChatMediaBlobInput
|
|||||||
export async function resolveCachedChatMediaBlob(
|
export async function resolveCachedChatMediaBlob(
|
||||||
input: ResolveCachedChatMediaBlobInput,
|
input: ResolveCachedChatMediaBlobInput,
|
||||||
): Promise<ResultT<Blob | null>> {
|
): Promise<ResultT<Blob | null>> {
|
||||||
const result = await getChatRepository().getCachedMedia(input);
|
const repository = await loadChatRepository();
|
||||||
|
const result = await repository.getCachedMedia(input);
|
||||||
if (Result.isErr(result)) return result;
|
if (Result.isErr(result)) return result;
|
||||||
if (!result.data) return Result.ok(null);
|
if (!result.data) return Result.ok(null);
|
||||||
return Result.ok(localChatMediaRowToBlob(result.data));
|
return Result.ok(localChatMediaRowToBlob(result.data));
|
||||||
@@ -28,7 +29,8 @@ export async function resolveCachedChatMediaBlob(
|
|||||||
export async function cacheRemoteChatMediaBlob(
|
export async function cacheRemoteChatMediaBlob(
|
||||||
input: CacheRemoteChatMediaBlobInput,
|
input: CacheRemoteChatMediaBlobInput,
|
||||||
): Promise<ResultT<Blob>> {
|
): Promise<ResultT<Blob>> {
|
||||||
const result = await getChatRepository().cacheRemoteMedia(input);
|
const repository = await loadChatRepository();
|
||||||
|
const result = await repository.cacheRemoteMedia(input);
|
||||||
if (Result.isErr(result)) return result;
|
if (Result.isErr(result)) return result;
|
||||||
const blob = localChatMediaRowToBlob(result.data);
|
const blob = localChatMediaRowToBlob(result.data);
|
||||||
if (!blob) return Result.err(new Error("Cached media has no readable bytes."));
|
if (!blob) return Result.err(new Error("Cached media has no readable bytes."));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getChatRepository } from "@/data/repositories/chat_repository";
|
import { loadChatRepository } from "@/data/repositories/chat_repository_loader";
|
||||||
import {
|
import {
|
||||||
resolveChatCacheOwnerKey,
|
resolveChatCacheOwnerKey,
|
||||||
type ChatCacheIdentityResolver,
|
type ChatCacheIdentityResolver,
|
||||||
@@ -44,7 +44,8 @@ export async function loadSplashLatestMessagePreview({
|
|||||||
export async function fetchSplashLatestMessagePreview(): Promise<
|
export async function fetchSplashLatestMessagePreview(): Promise<
|
||||||
ResultT<string | null>
|
ResultT<string | null>
|
||||||
> {
|
> {
|
||||||
const result = await getChatRepository().getHistory(1, 0);
|
const repository = await loadChatRepository();
|
||||||
|
const result = await repository.getHistory(1, 0);
|
||||||
if (Result.isErr(result)) return Result.err(result.error);
|
if (Result.isErr(result)) return Result.err(result.error);
|
||||||
|
|
||||||
return Result.ok(getLatestSplashMessagePreview(result.data.messages));
|
return Result.ok(getLatestSplashMessagePreview(result.data.messages));
|
||||||
|
|||||||
@@ -3,12 +3,10 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { ChatProvider } from "@/stores/chat/chat-context";
|
import { ChatProvider } from "@/stores/chat/chat-context";
|
||||||
import { PaymentProvider } from "@/stores/payment";
|
import { PaymentProvider } from "@/stores/payment/payment-context";
|
||||||
import {
|
import { ChatAuthSync } from "@/stores/sync/chat-auth-sync";
|
||||||
ChatAuthSync,
|
import { ChatPaymentSuccessSync } from "@/stores/sync/chat-payment-success-sync";
|
||||||
ChatPaymentSuccessSync,
|
import { PaymentSuccessSync } from "@/stores/sync/payment-success-sync";
|
||||||
PaymentSuccessSync,
|
|
||||||
} from "@/stores/sync";
|
|
||||||
|
|
||||||
export function ChatRouteProviders({ children }: { children: ReactNode }) {
|
export function ChatRouteProviders({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { PaymentProvider } from "@/stores/payment";
|
import { PaymentProvider } from "@/stores/payment/payment-context";
|
||||||
import { PaymentSuccessSync } from "@/stores/sync";
|
import { PaymentSuccessSync } from "@/stores/sync/payment-success-sync";
|
||||||
|
|
||||||
export function PaymentRouteProvider({ children }: { children: ReactNode }) {
|
export function PaymentRouteProvider({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { AppNavigationGuard } from "@/router/app-navigation-guard";
|
|||||||
import { AuthProvider } from "@/stores/auth/auth-context";
|
import { AuthProvider } from "@/stores/auth/auth-context";
|
||||||
import { AuthStatusChecker } from "@/stores/auth/auth-status-checker";
|
import { AuthStatusChecker } from "@/stores/auth/auth-status-checker";
|
||||||
import { OAuthSessionSync } from "@/stores/auth/oauth-session-sync";
|
import { OAuthSessionSync } from "@/stores/auth/oauth-session-sync";
|
||||||
import { UserAuthSync } from "@/stores/sync";
|
import { UserAuthSync } from "@/stores/sync/user-auth-sync";
|
||||||
import { UserProvider } from "@/stores/user/user-context";
|
import { UserProvider } from "@/stores/user/user-context";
|
||||||
|
|
||||||
export interface RootProvidersProps {
|
export interface RootProvidersProps {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { UiMessage } from "@/data/dto/chat";
|
import type { UiMessage } from "@/data/dto/chat";
|
||||||
import { resolveChatCacheOwnerKey } from "@/data/repositories/chat_cache_identity";
|
import { resolveChatCacheOwnerKey } from "@/data/repositories/chat_cache_identity";
|
||||||
import { getChatRepository } from "@/data/repositories/chat_repository";
|
import { loadChatRepository } from "@/data/repositories/chat_repository_loader";
|
||||||
import { Logger } from "@/utils/logger";
|
import { Logger } from "@/utils/logger";
|
||||||
import { Result } from "@/utils/result";
|
import { Result } from "@/utils/result";
|
||||||
import { todayString } from "@/utils/date";
|
import { todayString } from "@/utils/date";
|
||||||
@@ -47,7 +47,7 @@ export async function resolveHistoryCacheIdentity(): Promise<string | null> {
|
|||||||
export async function readLocalHistorySnapshot(
|
export async function readLocalHistorySnapshot(
|
||||||
cacheIdentity: string | null,
|
cacheIdentity: string | null,
|
||||||
): Promise<LocalHistorySnapshotOutput> {
|
): Promise<LocalHistorySnapshotOutput> {
|
||||||
const chatRepo = getChatRepository();
|
const chatRepo = await loadChatRepository();
|
||||||
const greetingMessage = createGreetingMessage();
|
const greetingMessage = createGreetingMessage();
|
||||||
|
|
||||||
const localResult = cacheIdentity
|
const localResult = cacheIdentity
|
||||||
@@ -77,7 +77,7 @@ export async function syncNetworkHistory(
|
|||||||
localCount: number,
|
localCount: number,
|
||||||
cacheIdentity: string | null,
|
cacheIdentity: string | null,
|
||||||
): Promise<NetworkHistorySyncOutput | null> {
|
): Promise<NetworkHistorySyncOutput | null> {
|
||||||
const chatRepo = getChatRepository();
|
const chatRepo = await loadChatRepository();
|
||||||
const greetingMessage = createGreetingMessage();
|
const greetingMessage = createGreetingMessage();
|
||||||
|
|
||||||
const networkResult = await chatRepo.getHistory(CHAT_HISTORY_LIMIT, 0);
|
const networkResult = await chatRepo.getHistory(CHAT_HISTORY_LIMIT, 0);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { fromCallback, fromPromise } from "xstate";
|
|||||||
import { ExceptionHandler } from "@/core/errors";
|
import { ExceptionHandler } from "@/core/errors";
|
||||||
import { MessageQueue } from "@/core/net/message-queue";
|
import { MessageQueue } from "@/core/net/message-queue";
|
||||||
import type { ChatSendResponse } from "@/data/dto/chat";
|
import type { ChatSendResponse } from "@/data/dto/chat";
|
||||||
import { getChatRepository } from "@/data/repositories/chat_repository";
|
import { loadChatRepository } from "@/data/repositories/chat_repository_loader";
|
||||||
import { resolveChatCacheOwnerKey } from "@/data/repositories/chat_cache_identity";
|
import { resolveChatCacheOwnerKey } from "@/data/repositories/chat_cache_identity";
|
||||||
import { Logger } from "@/utils/logger";
|
import { Logger } from "@/utils/logger";
|
||||||
import { Result } from "@/utils/result";
|
import { Result } from "@/utils/result";
|
||||||
@@ -66,7 +66,7 @@ async function sendMessageViaHttp(content: string): Promise<{
|
|||||||
response: ChatSendResponse;
|
response: ChatSendResponse;
|
||||||
reply: UiMessage | null;
|
reply: UiMessage | null;
|
||||||
}> {
|
}> {
|
||||||
const chatRepo = getChatRepository();
|
const chatRepo = await loadChatRepository();
|
||||||
const cacheIdentityResult = await resolveChatCacheOwnerKey();
|
const cacheIdentityResult = await resolveChatCacheOwnerKey();
|
||||||
const result = await chatRepo.sendMessage(content);
|
const result = await chatRepo.sendMessage(content);
|
||||||
if (Result.isErr(result)) {
|
if (Result.isErr(result)) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { fromPromise } from "xstate";
|
import { fromPromise } from "xstate";
|
||||||
|
|
||||||
import { getChatRepository } from "@/data/repositories/chat_repository";
|
import { loadChatRepository } from "@/data/repositories/chat_repository_loader";
|
||||||
import { resolveChatCacheOwnerKey } from "@/data/repositories/chat_cache_identity";
|
import { resolveChatCacheOwnerKey } from "@/data/repositories/chat_cache_identity";
|
||||||
import { Logger } from "@/utils/logger";
|
import { Logger } from "@/utils/logger";
|
||||||
import { Result } from "@/utils/result";
|
import { Result } from "@/utils/result";
|
||||||
@@ -23,7 +23,7 @@ export interface UnlockHistoryOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const unlockHistoryActor = fromPromise<UnlockHistoryOutput>(async () => {
|
export const unlockHistoryActor = fromPromise<UnlockHistoryOutput>(async () => {
|
||||||
const chatRepo = getChatRepository();
|
const chatRepo = await loadChatRepository();
|
||||||
const unlockResult = await chatRepo.unlockHistory();
|
const unlockResult = await chatRepo.unlockHistory();
|
||||||
if (Result.isErr(unlockResult)) {
|
if (Result.isErr(unlockResult)) {
|
||||||
log.error("[chat-machine] unlockHistoryActor failed", {
|
log.error("[chat-machine] unlockHistoryActor failed", {
|
||||||
@@ -45,7 +45,7 @@ export const unlockMessageActor = fromPromise<
|
|||||||
UnlockMessageOutput,
|
UnlockMessageOutput,
|
||||||
UnlockMessageRequest
|
UnlockMessageRequest
|
||||||
>(async ({ input }) => {
|
>(async ({ input }) => {
|
||||||
const chatRepo = getChatRepository();
|
const chatRepo = await loadChatRepository();
|
||||||
const cacheIdentityResult = await resolveChatCacheOwnerKey();
|
const cacheIdentityResult = await resolveChatCacheOwnerKey();
|
||||||
const unlockResult = await chatRepo.unlockPrivateMessage({
|
const unlockResult = await chatRepo.unlockPrivateMessage({
|
||||||
...(input.messageId ? { messageId: input.messageId } : {}),
|
...(input.messageId ? { messageId: input.messageId } : {}),
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/** Payment → Chat bridge, mounted only while the chat route is active. */
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { shallowEqual } from "@xstate/react";
|
||||||
|
|
||||||
|
import { hasPendingChatUnlock } from "@/lib/navigation/chat_unlock_session";
|
||||||
|
import { useChatDispatch } from "@/stores/chat/chat-context";
|
||||||
|
import { usePaymentSelector } from "@/stores/payment/payment-context";
|
||||||
|
|
||||||
|
export function ChatPaymentSuccessSync() {
|
||||||
|
const paymentState = usePaymentSelector(
|
||||||
|
(state) => ({
|
||||||
|
currentOrderId: state.context.currentOrderId,
|
||||||
|
isPaid: state.matches("paid"),
|
||||||
|
orderStatus: state.context.orderStatus,
|
||||||
|
}),
|
||||||
|
shallowEqual,
|
||||||
|
);
|
||||||
|
const chatDispatch = useChatDispatch();
|
||||||
|
const lastPaidKeyRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!paymentState.isPaid || !paymentState.currentOrderId) return;
|
||||||
|
|
||||||
|
const paidKey = `${paymentState.currentOrderId}:${paymentState.orderStatus}`;
|
||||||
|
if (lastPaidKeyRef.current === paidKey) return;
|
||||||
|
lastPaidKeyRef.current = paidKey;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
void (async () => {
|
||||||
|
if (await hasPendingChatUnlock()) return;
|
||||||
|
if (!cancelled) chatDispatch({ type: "ChatPaymentSucceeded" });
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
chatDispatch,
|
||||||
|
paymentState.currentOrderId,
|
||||||
|
paymentState.isPaid,
|
||||||
|
paymentState.orderStatus,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./chat-auth-sync";
|
export * from "./chat-auth-sync";
|
||||||
|
export * from "./chat-payment-success-sync";
|
||||||
export * from "./payment-success-sync";
|
export * from "./payment-success-sync";
|
||||||
export * from "./user-auth-sync";
|
export * from "./user-auth-sync";
|
||||||
|
|||||||
@@ -8,9 +8,7 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { shallowEqual } from "@xstate/react";
|
import { shallowEqual } from "@xstate/react";
|
||||||
|
|
||||||
import { useChatDispatch } from "@/stores/chat/chat-context";
|
|
||||||
import { useUserDispatch } from "@/stores/user/user-context";
|
import { useUserDispatch } from "@/stores/user/user-context";
|
||||||
import { hasPendingChatUnlock } from "@/lib/navigation/chat_unlock_session";
|
|
||||||
import {
|
import {
|
||||||
usePaymentDispatch,
|
usePaymentDispatch,
|
||||||
usePaymentSelector,
|
usePaymentSelector,
|
||||||
@@ -54,42 +52,3 @@ export function PaymentSuccessSync() {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Payment → Chat bridge, mounted only while the chat route is active. */
|
|
||||||
export function ChatPaymentSuccessSync() {
|
|
||||||
const paymentState = usePaymentSelector(
|
|
||||||
(state) => ({
|
|
||||||
currentOrderId: state.context.currentOrderId,
|
|
||||||
isPaid: state.matches("paid"),
|
|
||||||
orderStatus: state.context.orderStatus,
|
|
||||||
}),
|
|
||||||
shallowEqual,
|
|
||||||
);
|
|
||||||
const chatDispatch = useChatDispatch();
|
|
||||||
const lastPaidKeyRef = useRef<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!paymentState.isPaid || !paymentState.currentOrderId) return;
|
|
||||||
|
|
||||||
const paidKey = `${paymentState.currentOrderId}:${paymentState.orderStatus}`;
|
|
||||||
if (lastPaidKeyRef.current === paidKey) return;
|
|
||||||
lastPaidKeyRef.current = paidKey;
|
|
||||||
|
|
||||||
let cancelled = false;
|
|
||||||
void (async () => {
|
|
||||||
if (await hasPendingChatUnlock()) return;
|
|
||||||
if (!cancelled) chatDispatch({ type: "ChatPaymentSucceeded" });
|
|
||||||
})();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
chatDispatch,
|
|
||||||
paymentState.currentOrderId,
|
|
||||||
paymentState.isPaid,
|
|
||||||
paymentState.orderStatus,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user