refactor(router): centralize app route transitions

This commit is contained in:
2026-07-03 14:15:52 +08:00
parent 91dde42f92
commit 873a0a8bce
10 changed files with 188 additions and 133 deletions
+148 -75
View File
@@ -1,8 +1,14 @@
"use client";
import { useCallback, useMemo } from "react";
import { useRouter } from "next/navigation";
import { NavigationStorage } from "@/data/storage/navigation";
import type { SubscriptionReturnTo } from "@/lib/navigation/subscription_exit";
import {
consumeSubscriptionExitUrl,
peekSubscriptionExplicitExitUrl,
} from "@/lib/navigation/subscription_exit";
import { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel";
import { useAuthState } from "@/stores/auth/auth-context";
import { useUserState } from "@/stores/user/user-context";
@@ -25,8 +31,13 @@ import type {
export interface AppNavigator {
push: (href: string, options?: { scroll?: boolean }) => void;
replace: (href: string, options?: { scroll?: boolean }) => void;
back: () => void;
openAuth: (redirectTo?: string) => void;
openChat: (options?: { replace?: boolean; scroll?: boolean }) => void;
openChatImage: (messageId: string) => void;
openSubscription: (input: OpenSubscriptionInput) => void;
exitSubscription: (returnTo: SubscriptionReturnTo) => void;
exitSubscriptionAfterSuccess: (returnTo: SubscriptionReturnTo) => void;
startMessageUnlock: (input: StartMessageUnlockInput) => void;
openSubscriptionForPendingUnlock: (
input: OpenSubscriptionForPendingUnlockInput,
@@ -41,100 +52,162 @@ export function useAppNavigator(): AppNavigator {
const userState = useUserState();
const isAuthenticatedUser = resolveIsAuthenticatedUser(authState.loginStatus);
function push(href: string, options?: { scroll?: boolean }): void {
const push = useCallback((href: string, options?: { scroll?: boolean }): void => {
router.push(href, options);
}
}, [router]);
function replace(href: string, options?: { scroll?: boolean }): void {
const replace = useCallback((href: string, options?: { scroll?: boolean }): void => {
router.replace(href, options);
}
}, [router]);
function openAuth(redirectTo: string = ROUTES.chat): void {
const back = useCallback((): void => {
router.back();
}, [router]);
const openAuth = useCallback((redirectTo: string = ROUTES.chat): void => {
router.push(ROUTE_BUILDERS.authWithRedirect(redirectTo));
}
}, [router]);
function getDefaultPayChannel() {
const openChat = useCallback((options: { replace?: boolean; scroll?: boolean } = {}): void => {
const navOptions = { scroll: options.scroll ?? false };
if (options.replace) {
router.replace(ROUTES.chat, navOptions);
return;
}
router.push(ROUTES.chat, navOptions);
}, [router]);
const openChatImage = useCallback((messageId: string): void => {
router.push(ROUTE_BUILDERS.chatImage(messageId), { scroll: false });
}, [router]);
const getDefaultPayChannel = useCallback(() => {
return getDefaultPayChannelForCountryCode(userState.currentUser?.countryCode);
}
}, [userState.currentUser?.countryCode]);
function openSubscription({
type,
payChannel = getDefaultPayChannel(),
returnTo = null,
replace: shouldReplace = false,
}: OpenSubscriptionInput): void {
const target = ROUTE_BUILDERS.subscription(type, {
payChannel,
returnTo: returnTo ?? undefined,
});
const nextUrl = resolveAuthenticatedNavigation({
loginStatus: authState.loginStatus,
targetUrl: target,
});
if (shouldReplace) {
router.replace(nextUrl);
return;
}
router.push(nextUrl);
}
function startMessageUnlock({
messageId,
kind,
returnUrl,
stage = "auth",
onAuthenticated,
}: StartMessageUnlockInput): void {
if (isAuthenticatedUser) {
onAuthenticated();
return;
}
void (async () => {
await NavigationStorage.savePendingChatUnlock({
messageId,
kind,
returnUrl,
stage,
});
router.push(ROUTE_BUILDERS.authWithRedirect(returnUrl));
})();
}
function openSubscriptionForPendingUnlock({
messageId,
kind,
returnUrl,
type,
payChannel = getDefaultPayChannel(),
}: OpenSubscriptionForPendingUnlockInput): void {
void (async () => {
await NavigationStorage.savePendingChatUnlock({
messageId,
kind,
returnUrl,
stage: "payment",
});
openSubscription({
type,
const openSubscription = useCallback(
({
type,
payChannel = getDefaultPayChannel(),
returnTo = null,
replace: shouldReplace = false,
}: OpenSubscriptionInput): void => {
const target = ROUTE_BUILDERS.subscription(type, {
payChannel,
returnTo: "chat",
returnTo: returnTo ?? undefined,
});
})();
}
return {
const nextUrl = resolveAuthenticatedNavigation({
loginStatus: authState.loginStatus,
targetUrl: target,
});
if (shouldReplace) {
router.replace(nextUrl);
return;
}
router.push(nextUrl);
},
[authState.loginStatus, getDefaultPayChannel, router],
);
const startMessageUnlock = useCallback(
({
messageId,
kind,
returnUrl,
stage = "auth",
onAuthenticated,
}: StartMessageUnlockInput): void => {
if (isAuthenticatedUser) {
onAuthenticated();
return;
}
void (async () => {
await NavigationStorage.savePendingChatUnlock({
messageId,
kind,
returnUrl,
stage,
});
router.push(ROUTE_BUILDERS.authWithRedirect(returnUrl));
})();
},
[isAuthenticatedUser, router],
);
const openSubscriptionForPendingUnlock = useCallback(
({
messageId,
kind,
returnUrl,
type,
payChannel = getDefaultPayChannel(),
}: OpenSubscriptionForPendingUnlockInput): void => {
void (async () => {
await NavigationStorage.savePendingChatUnlock({
messageId,
kind,
returnUrl,
stage: "payment",
});
openSubscription({
type,
payChannel,
returnTo: "chat",
});
})();
},
[getDefaultPayChannel, openSubscription],
);
const exitSubscription = useCallback((returnTo: SubscriptionReturnTo): void => {
void (async () => {
router.replace(await consumeSubscriptionExitUrl(returnTo));
})();
}, [router]);
const exitSubscriptionAfterSuccess = useCallback((returnTo: SubscriptionReturnTo): void => {
void (async () => {
const pendingExitUrl = await peekSubscriptionExplicitExitUrl();
if (pendingExitUrl) {
router.replace(pendingExitUrl);
return;
}
router.replace(await consumeSubscriptionExitUrl(returnTo));
})();
}, [router]);
return useMemo(() => ({
push,
replace,
back,
openAuth,
openChat,
openChatImage,
openSubscription,
exitSubscription,
exitSubscriptionAfterSuccess,
startMessageUnlock,
openSubscriptionForPendingUnlock,
getDefaultPayChannel,
isAuthenticatedUser,
};
}), [
back,
exitSubscription,
exitSubscriptionAfterSuccess,
getDefaultPayChannel,
isAuthenticatedUser,
openAuth,
openChat,
openChatImage,
openSubscription,
openSubscriptionForPendingUnlock,
push,
replace,
startMessageUnlock,
]);
}
export type { Route };