233 lines
6.3 KiB
TypeScript
233 lines
6.3 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";
|
|
import { consumeTopUpHandoff } from "@/lib/auth/top_up_handoff";
|
|
import { Result } from "@/utils/result";
|
|
import {
|
|
isFavoriteEntryRequest,
|
|
persistFavoriteEntryIntent,
|
|
} from "@/lib/navigation/favorite_entry";
|
|
|
|
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;
|
|
favorite: string | null;
|
|
handoffToken: string | null;
|
|
}
|
|
|
|
export default function ExternalEntryPersist({
|
|
deviceId,
|
|
asid,
|
|
psid,
|
|
avatarUrl,
|
|
target,
|
|
character,
|
|
mode,
|
|
promotionType,
|
|
favorite,
|
|
handoffToken,
|
|
}: ExternalEntryPersistProps) {
|
|
const router = useRouter();
|
|
const authState = useAuthState();
|
|
const authDispatch = useAuthDispatch();
|
|
const [hasPersisted, setHasPersisted] = useState(false);
|
|
const [handoffError, setHandoffError] = useState<string | null>(null);
|
|
const [handoffCompleted, setHandoffCompleted] = useState(false);
|
|
const hasNavigatedRef = useRef(false);
|
|
const handoffStartedRef = useRef(false);
|
|
const targetRoute = resolveExternalEntryTarget({ target });
|
|
const destination = resolveExternalEntryDestination({ target, character });
|
|
const characterId =
|
|
getCharacterBySlug(character)?.id ?? DEFAULT_CHARACTER_ID;
|
|
const resolvedPromotionType = resolveExternalEntryPromotionType({
|
|
mode,
|
|
promotionType,
|
|
});
|
|
const isTopUpHandoff = targetRoute === ROUTES.subscription;
|
|
const displayedHandoffError =
|
|
handoffError ??
|
|
(isTopUpHandoff && !hasValue(handoffToken)
|
|
? "This top-up link is invalid. Please request a new link in Messenger."
|
|
: null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
|
|
void (async () => {
|
|
const results = await Promise.allSettled([
|
|
persistExternalEntryPayload({
|
|
deviceId,
|
|
asid,
|
|
psid,
|
|
avatarUrl,
|
|
}),
|
|
targetRoute === ROUTES.chat && resolvedPromotionType
|
|
? savePendingChatPromotion(resolvedPromotionType, characterId)
|
|
: clearPendingChatPromotion(),
|
|
]);
|
|
if (isFavoriteEntryRequest(favorite)) {
|
|
persistFavoriteEntryIntent();
|
|
}
|
|
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,
|
|
favorite,
|
|
mode,
|
|
promotionType,
|
|
psid,
|
|
resolvedPromotionType,
|
|
targetRoute,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (hasNavigatedRef.current || !hasPersisted) return;
|
|
|
|
if (isTopUpHandoff) {
|
|
if (!authState.hasInitialized || authState.isLoading) return;
|
|
if (handoffCompleted) {
|
|
if (
|
|
authState.loginStatus === "notLoggedIn" ||
|
|
authState.loginStatus === "guest"
|
|
) {
|
|
return;
|
|
}
|
|
hasNavigatedRef.current = true;
|
|
router.replace(destination);
|
|
return;
|
|
}
|
|
if (handoffStartedRef.current) return;
|
|
if (!hasValue(handoffToken)) return;
|
|
handoffStartedRef.current = true;
|
|
void (async () => {
|
|
const result = await consumeTopUpHandoff(handoffToken);
|
|
if (Result.isErr(result)) {
|
|
log.warn("[ExternalEntryPersist] top-up handoff failed", result.error);
|
|
window.history.replaceState(
|
|
window.history.state,
|
|
"",
|
|
"/external-entry?target=topup",
|
|
);
|
|
setHandoffError(
|
|
"This top-up link is invalid or has expired. Please request a new link in Messenger.",
|
|
);
|
|
return;
|
|
}
|
|
setHandoffCompleted(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,
|
|
handoffCompleted,
|
|
handoffToken,
|
|
isTopUpHandoff,
|
|
psid,
|
|
router,
|
|
]);
|
|
|
|
return (
|
|
<div
|
|
className="flex flex-1 items-center justify-center"
|
|
style={{ minHeight: "100dvh" }}
|
|
>
|
|
{displayedHandoffError ? (
|
|
<div className="mx-6 max-w-sm text-center">
|
|
<p className="text-sm text-[var(--color-text-secondary)]">
|
|
{displayedHandoffError}
|
|
</p>
|
|
<button
|
|
className="mt-5 rounded-full bg-[var(--color-accent)] px-5 py-2.5 text-sm font-semibold text-white"
|
|
type="button"
|
|
onClick={() => router.replace(ROUTES.root)}
|
|
>
|
|
Back to Cozsweet
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<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;
|
|
}
|