Files
cozsweet-frontend-nextjs/src/app/external-entry/external-entry-persist.tsx
T

166 lines
4.2 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import {
persistExternalEntryPayload,
resolveExternalEntryDestination,
resolveExternalEntryPromotionType,
resolveExternalEntryTarget,
shouldBindExternalFacebookIdentity,
} from "@/lib/navigation/external_entry";
import {
clearPendingChatPromotion,
savePendingChatPromotion,
} from "@/lib/navigation/chat_unlock_session";
import { ROUTES } from "@/router/routes";
import {
DEFAULT_CHARACTER_ID,
getCharacterBySlug,
} from "@/data/constants/character";
import { Logger } from "@/utils/logger";
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
const log = new Logger("ExternalEntryPersist");
interface ExternalEntryPersistProps {
deviceId: string | null;
asid: string | null;
psid: string | null;
avatarUrl: string | null;
target: string | null;
character: string | null;
mode: string | null;
promotionType: string | null;
}
export default function ExternalEntryPersist({
deviceId,
asid,
psid,
avatarUrl,
target,
character,
mode,
promotionType,
}: ExternalEntryPersistProps) {
const router = useRouter();
const authState = useAuthState();
const authDispatch = useAuthDispatch();
const [hasPersisted, setHasPersisted] = useState(false);
const hasNavigatedRef = useRef(false);
const hasReinitializedForPsidRef = useRef(false);
const targetRoute = resolveExternalEntryTarget({ target });
const destination = resolveExternalEntryDestination({ target, character });
const characterId =
getCharacterBySlug(character)?.id ?? DEFAULT_CHARACTER_ID;
const resolvedPromotionType = resolveExternalEntryPromotionType({
mode,
promotionType,
});
useEffect(() => {
let cancelled = false;
void (async () => {
const results = await Promise.allSettled([
persistExternalEntryPayload({
deviceId,
asid,
psid,
avatarUrl,
}),
targetRoute === ROUTES.chat && resolvedPromotionType
? savePendingChatPromotion(resolvedPromotionType, characterId)
: clearPendingChatPromotion(),
]);
for (const result of results) {
if (result.status === "rejected") {
log.warn(
"[ExternalEntryPersist] failed to persist payload",
result.reason,
);
}
}
if (!cancelled) setHasPersisted(true);
})();
return () => {
cancelled = true;
};
}, [
avatarUrl,
asid,
character,
characterId,
destination,
deviceId,
mode,
promotionType,
psid,
resolvedPromotionType,
targetRoute,
]);
useEffect(() => {
if (hasNavigatedRef.current || !hasPersisted) return;
// AuthStatusChecker can initialize before this entry has persisted PSID.
// Re-run the check so PSID direct login is not skipped by that race.
if (hasValue(psid) && !hasReinitializedForPsidRef.current) {
if (!authState.hasInitialized || authState.isLoading) return;
hasReinitializedForPsidRef.current = true;
authDispatch({ type: "AuthInit" });
return;
}
if (!authState.hasInitialized || authState.isLoading) return;
hasNavigatedRef.current = true;
if (
shouldBindExternalFacebookIdentity({
hasInitialized: authState.hasInitialized,
isLoading: authState.isLoading,
loginStatus: authState.loginStatus,
asid,
psid,
})
) {
authDispatch({
type: "AuthExternalFacebookIdentityBindRequested",
asid: asid ?? undefined,
psid: psid ?? undefined,
});
}
router.replace(destination);
}, [
asid,
authDispatch,
authState.hasInitialized,
authState.isLoading,
authState.loginStatus,
destination,
hasPersisted,
psid,
router,
]);
return (
<div
className="flex flex-1 items-center justify-center"
style={{ minHeight: "100dvh" }}
>
<div
aria-label="Redirecting"
className="size-10 animate-spin rounded-full border-4 border-t-transparent"
style={{ borderColor: "var(--color-accent)" }}
/>
</div>
);
}
function hasValue(value: string | null): value is string {
return typeof value === "string" && value.trim().length > 0;
}