fix: remove recommend login gate

This commit is contained in:
2026-05-25 21:52:05 +08:00
parent bdb81466ce
commit e6e2d821a8
11 changed files with 176 additions and 97 deletions

View File

@@ -115,6 +115,7 @@ import { resolveWorkNotFoundRecoveryAction } from '../../routing/runtimeNotFound
import {
ApiClientError,
BACKGROUND_AUTH_REQUEST_OPTIONS,
getStoredAccessToken,
} from '../../services/apiClient';
import {
ensureRuntimeGuestToken,
@@ -559,6 +560,25 @@ async function buildRecommendRuntimeGuestOptions() {
runtimeGuestToken: token,
};
}
function shouldUseRecommendRuntimeGuestAuth(
authUi: { user?: { id?: string } | null } | null | undefined,
) {
return !authUi?.user?.id?.trim() && !getStoredAccessToken();
}
async function buildRecommendRuntimeAuthOptions(
authUi: { user?: { id?: string } | null } | null | undefined,
embedded?: boolean,
) {
if (!embedded) {
return {};
}
if (shouldUseRecommendRuntimeGuestAuth(authUi)) {
return buildRecommendRuntimeGuestOptions();
}
return RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
}
const PUZZLE_DRAFT_GENERATION_POINT_COST = 2;
const MATCH3D_DRAFT_GENERATION_POINT_COST = 10;
const BARK_BATTLE_DRAFT_GENERATION_POINT_COST = 3;
@@ -7386,9 +7406,10 @@ export function PlatformEntryFlowShellImpl({
profileId: targetProfileId,
mode: 'play' as const,
};
const runtimeGuestOptions = options.embedded
? await buildRecommendRuntimeGuestOptions()
: {};
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
authUi,
options.embedded,
);
const { run } = options.embedded
? await startVisualNovelRun(
targetProfileId,
@@ -7419,6 +7440,7 @@ export function PlatformEntryFlowShellImpl({
}
},
[
authUi,
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
setSelectionStage,
@@ -7442,7 +7464,7 @@ export function PlatformEntryFlowShellImpl({
try {
const runtimeGuestOptions =
activeRecommendRuntimeKind === 'visual-novel'
? await buildRecommendRuntimeGuestOptions()
? await buildRecommendRuntimeAuthOptions(authUi, true)
: {};
const nextRun = await streamVisualNovelRuntimeAction(
visualNovelRun.runId,
@@ -7460,6 +7482,7 @@ export function PlatformEntryFlowShellImpl({
},
[
activeRecommendRuntimeKind,
authUi,
isVisualNovelBusy,
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
@@ -7868,9 +7891,10 @@ export function PlatformEntryFlowShellImpl({
setJumpHopError(null);
setJumpHopRuntimeReturnStage(options.returnStage ?? 'work-detail');
try {
const runtimeGuestOptions = options.embedded
? await buildRecommendRuntimeGuestOptions()
: {};
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
authUi,
options.embedded,
);
const [detail, runResponse] = await Promise.all([
jumpHopClient.getWorkDetail(normalizedProfileId).catch(() => null),
jumpHopClient.startRun(normalizedProfileId, runtimeGuestOptions),
@@ -7898,7 +7922,7 @@ export function PlatformEntryFlowShellImpl({
setIsJumpHopBusy(false);
}
},
[setSelectionStage],
[authUi, setSelectionStage],
);
const restartJumpHopRuntimeRun = useCallback(async () => {
@@ -8205,9 +8229,10 @@ export function PlatformEntryFlowShellImpl({
setWoodenFishError(null);
setWoodenFishRuntimeReturnStage(options.returnStage ?? 'work-detail');
try {
const runtimeGuestOptions = options.embedded
? await buildRecommendRuntimeGuestOptions()
: {};
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
authUi,
options.embedded,
);
const [detail, runResponse] = await Promise.all([
woodenFishClient.getWorkDetail(normalizedProfileId).catch(() => null),
options.embedded
@@ -8237,7 +8262,7 @@ export function PlatformEntryFlowShellImpl({
setIsWoodenFishBusy(false);
}
},
[setSelectionStage],
[authUi, setSelectionStage],
);
const checkpointWoodenFishRuntimeRun = useCallback(
@@ -8640,12 +8665,14 @@ export function PlatformEntryFlowShellImpl({
profileId: item.profileId,
levelId: levelId ?? null,
};
const runtimeGuestOptions = options.embedded
const canUseRuntimeGuestAuth =
options.embedded || options.authMode === 'isolated';
const useRuntimeGuestAuth =
canUseRuntimeGuestAuth && shouldUseRecommendRuntimeGuestAuth(authUi);
const runtimeGuestOptions = useRuntimeGuestAuth
? await buildRecommendRuntimeGuestOptions()
: {};
const authMode = options.embedded
? 'isolated'
: (options.authMode ?? 'default');
const authMode = useRuntimeGuestAuth ? 'isolated' : 'default';
const { run } =
authMode === 'isolated'
? await startPuzzleRun(startRunPayload, runtimeGuestOptions)
@@ -8692,6 +8719,7 @@ export function PlatformEntryFlowShellImpl({
},
[
isPuzzleBusy,
authUi,
resolvePuzzleErrorMessage,
setIsPuzzleBusy,
setPuzzleError,
@@ -8744,9 +8772,10 @@ export function PlatformEntryFlowShellImpl({
runtimeProfile.generatedBackgroundAsset,
{ expireSeconds: 300 },
);
const runtimeGuestOptions = options.embedded
? await buildRecommendRuntimeGuestOptions()
: {};
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
authUi,
options.embedded,
);
const runtimeOptions = {
...runtimeGuestOptions,
...(typeof options.itemTypeCountOverride === 'number'
@@ -8793,6 +8822,7 @@ export function PlatformEntryFlowShellImpl({
},
[
isMatch3DBusy,
authUi,
match3dFlow,
match3dRuntimeAdapter,
resolveMatch3DErrorMessage,
@@ -8816,9 +8846,10 @@ export function PlatformEntryFlowShellImpl({
setSquareHoleError(null);
try {
const runtimeGuestOptions = options.embedded
? await buildRecommendRuntimeGuestOptions()
: {};
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
authUi,
options.embedded,
);
const { run } = options.embedded
? await startSquareHoleRun(profile.profileId, runtimeGuestOptions)
: await startSquareHoleRun(profile.profileId);
@@ -8852,6 +8883,7 @@ export function PlatformEntryFlowShellImpl({
},
[
isSquareHoleBusy,
authUi,
resolveSquareHoleErrorMessage,
setSelectionStage,
setSquareHoleError,
@@ -8974,7 +9006,7 @@ export function PlatformEntryFlowShellImpl({
try {
const runtimeGuestOptions =
activeRecommendRuntimeKind === 'big-fish'
? await buildRecommendRuntimeGuestOptions()
? await buildRecommendRuntimeAuthOptions(authUi, true)
: {};
const { run } = await submitBigFishRuntimeInput(
bigFishRun.runId,
@@ -8992,6 +9024,7 @@ export function PlatformEntryFlowShellImpl({
},
[
activeRecommendRuntimeKind,
authUi,
bigFishRun,
resolveBigFishErrorMessage,
setBigFishError,
@@ -9008,10 +9041,9 @@ export function PlatformEntryFlowShellImpl({
setBigFishRuntimeStartedAt(null);
const reportPromise =
activeRecommendRuntimeKind === 'big-fish'
? recordBigFishPlay(
sessionId,
{ elapsedMs },
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
? buildRecommendRuntimeAuthOptions(authUi, true).then(
(runtimeAuthOptions) =>
recordBigFishPlay(sessionId, { elapsedMs }, runtimeAuthOptions),
)
: recordBigFishPlay(sessionId, { elapsedMs });
void reportPromise.catch((error) => {
@@ -9021,6 +9053,7 @@ export function PlatformEntryFlowShellImpl({
});
}, [
activeRecommendRuntimeKind,
authUi,
bigFishRun?.sessionId,
bigFishRuntimeStartedAt,
resolveBigFishErrorMessage,
@@ -11315,9 +11348,10 @@ export function PlatformEntryFlowShellImpl({
setBigFishRuntimeReturnStage(returnStage);
setBigFishRun(null);
try {
const runtimeGuestOptions = options.embedded
? await buildRecommendRuntimeGuestOptions()
: {};
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
authUi,
options.embedded,
);
const { run } = options.embedded
? await startBigFishRuntimeRun(sessionId, runtimeGuestOptions)
: await startBigFishRuntimeRun(sessionId);
@@ -11345,7 +11379,7 @@ export function PlatformEntryFlowShellImpl({
return false;
}
},
[bigFishFlow, resolveBigFishErrorMessage, setBigFishError, setSelectionStage],
[authUi, bigFishFlow, resolveBigFishErrorMessage, setBigFishError, setSelectionStage],
);
const startBarkBattleRunFromWork = useCallback(
@@ -11365,9 +11399,10 @@ export function PlatformEntryFlowShellImpl({
setBarkBattlePublishedConfig(mapBarkBattleWorkToPublishedConfig(item));
setBarkBattleRuntimeReturnStage(returnStage);
try {
const runtimeGuestOptions = options.embedded
? await buildRecommendRuntimeGuestOptions()
: {};
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
authUi,
options.embedded,
);
const runResponse = options.embedded
? await startBarkBattleRun(item.workId, {}, runtimeGuestOptions)
: await startBarkBattleRun(item.workId);
@@ -11390,7 +11425,7 @@ export function PlatformEntryFlowShellImpl({
return false;
}
},
[resolveBarkBattleErrorMessage, setSelectionStage],
[authUi, resolveBarkBattleErrorMessage, setSelectionStage],
);
const startSelectedPublicWork = useCallback(() => {

View File

@@ -6121,11 +6121,52 @@ test('home recommendation starts embedded puzzle without global auth reset on lo
profileId: 'puzzle-profile-public-1',
levelId: null,
},
ISOLATED_RUNTIME_AUTH_OPTIONS,
);
});
});
test('home recommendation keeps logged-in puzzle start on default auth instead of guest token', async () => {
const publishedPuzzleWork = {
workId: 'puzzle-work-public-2',
profileId: 'puzzle-profile-public-2',
ownerUserId: 'user-2',
sourceSessionId: 'puzzle-session-public-2',
authorDisplayName: '拼图作者',
levelName: '星桥机关',
summary: '旋转碎片并接通星桥机关。',
themeTags: ['机关', '星桥'],
coverImageSrc: null,
coverAssetId: null,
publicationStatus: 'published',
updatedAt: '2026-04-25T09:00:00.000Z',
publishedAt: '2026-04-25T09:00:00.000Z',
playCount: 3,
likeCount: 0,
publishReady: true,
} satisfies PuzzleWorkSummary;
vi.mocked(listPuzzleGallery).mockResolvedValue({
items: [publishedPuzzleWork],
});
vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
item: publishedPuzzleWork,
});
render(<TestWrapper withAuth />);
await waitFor(() => {
expect(startPuzzleRun).toHaveBeenCalledWith(
{
profileId: 'puzzle-profile-public-2',
levelId: null,
},
);
});
expect(vi.mocked(startPuzzleRun).mock.calls[0]?.[1]).not.toEqual(
ISOLATED_RUNTIME_AUTH_OPTIONS,
);
});
test('home recommendation Match3D runtime keeps profile generated models when card summary is stale', async () => {
const match3dCard: Match3DWorkSummary = {
workId: 'match3d-work-card-1',
@@ -7135,7 +7176,6 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
profileId: 'puzzle-profile-public-1',
levelId: null,
},
ISOLATED_RUNTIME_AUTH_OPTIONS,
);
vi.mocked(listProfileSaveArchives).mockClear();
vi.mocked(listProfileSaveArchives).mockRejectedValueOnce(
@@ -7159,7 +7199,6 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
elapsedMs: 18_000,
nickname: '测试玩家',
},
ISOLATED_RUNTIME_AUTH_OPTIONS,
);
});
@@ -7180,7 +7219,6 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
clearedFirstLevel.runId,
{},
ISOLATED_RUNTIME_AUTH_OPTIONS,
);
});
expect(
@@ -7343,7 +7381,6 @@ test('formal puzzle similar work keeps current run level progression', async ()
expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
clearedThirdLevel.runId,
{ targetProfileId: 'puzzle-profile-similar-2' },
ISOLATED_RUNTIME_AUTH_OPTIONS,
);
});
expect(startPuzzleRun).not.toHaveBeenCalled();
@@ -7527,7 +7564,6 @@ test('recommend puzzle remix return restarts recommendation instead of stale loa
profileId: 'puzzle-profile-public-1',
levelId: null,
},
ISOLATED_RUNTIME_AUTH_OPTIONS,
);
});
expect(screen.queryByText('正在进入拼图关卡')).toBeNull();

View File

@@ -2697,7 +2697,7 @@ test('logged out mobile shell defaults to discover tab', () => {
).toBeNull();
});
test('logged out recommend tab opens login modal and shows cover only', async () => {
test('logged out recommend tab enters runtime without login modal', async () => {
const user = userEvent.setup();
const { container, openLoginModal } = renderStatefulLoggedOutHomeView({
latestEntries: [puzzlePublicEntry],
@@ -2712,20 +2712,18 @@ test('logged out recommend tab opens login modal and shows cover only', async ()
within(bottomNav as HTMLElement).getByRole('button', { name: '推荐' }),
);
expect(openLoginModal).toHaveBeenCalledTimes(1);
expect(
container.querySelector('.platform-recommend-cover-only'),
).toBeTruthy();
expect(openLoginModal).not.toHaveBeenCalled();
expect(container.querySelector('.platform-recommend-cover-only')).toBeNull();
expect(container.querySelector('.platform-mobile-topbar')).toBeNull();
expect(
container.querySelector('.platform-mobile-entry-shell--recommend'),
).toBeTruthy();
expect(screen.queryByTestId('recommend-runtime')).toBeNull();
expect(screen.queryByLabelText('奇幻拼图 作品信息')).toBeNull();
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
expect(screen.getByLabelText('奇幻拼图 作品信息')).toBeTruthy();
expect(screen.getAllByText('奇幻拼图').length).toBeGreaterThan(0);
});
test('logged out recommend cover opens login modal again', async () => {
test('logged out recommend page keeps runtime visible without login gate', async () => {
const user = userEvent.setup();
const onOpenGalleryDetail = vi.fn();
const { openLoginModal } = renderStatefulLoggedOutHomeView({
@@ -2741,12 +2739,9 @@ test('logged out recommend cover opens login modal again', async () => {
await user.click(
within(bottomNav as HTMLElement).getByRole('button', { name: '推荐' }),
);
await user.click(
screen.getByRole('button', { name: / /u }),
);
expect(openLoginModal).toHaveBeenCalledTimes(2);
expect(openLoginModal).toHaveBeenLastCalledWith();
expect(openLoginModal).not.toHaveBeenCalled();
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
});
@@ -2780,6 +2775,26 @@ test('logged out recommend page can enter runtime without login gate', () => {
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
});
test('logged out desktop recommend rail enters runtime without login modal', async () => {
mockDesktopLayout();
const user = userEvent.setup();
const openLoginModal = vi.fn();
renderLoggedOutHomeView(
openLoginModal,
{
latestEntries: [puzzlePublicEntry],
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
},
'category',
);
await user.click(screen.getByRole('button', { name: '推荐' }));
expect(openLoginModal).not.toHaveBeenCalled();
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
});
test('logged in recommend page uses gated recommend detail callback', async () => {
const user = userEvent.setup();
const onOpenGalleryDetail = vi.fn();

View File

@@ -5372,7 +5372,7 @@ export function RpgEntryHomeView({
{recommendRuntimeError}
</button>
</section>
) : isStartingRecommendEntry || !recommendRuntimeContent ? (
) : isStartingRecommendEntry ? (
<section className="platform-recommend-runtime-panel">
<div className="platform-recommend-runtime-state">...</div>
</section>
@@ -6761,12 +6761,6 @@ export function RpgEntryHomeView({
return;
}
if (!isAuthenticated && tab === 'home') {
onTabChange(tab);
authUi?.openLoginModal();
return;
}
onTabChange(tab);
}}
/>
@@ -6924,12 +6918,6 @@ export function RpgEntryHomeView({
emphasized={tab === 'create'}
showDot={tab === 'saves' && hasUnreadDraftUpdate}
onClick={() => {
if (!isAuthenticated && tab === 'home') {
onTabChange(tab);
authUi?.openLoginModal();
return;
}
onTabChange(tab);
}}
/>

View File

@@ -351,7 +351,7 @@ export function useRpgEntryBootstrap(
!hasInitialAgentSession &&
!hasExplicitPlatformTabSelectionRef.current
) {
// 中文注释:新用户先进入发现页;推荐页只在用户主动点击后作为登录门禁入口
// 中文注释:新用户先进入发现页;推荐页可直接进入,真正受保护的动作再单独做登录门禁。
setPlatformTabState(isAuthenticated ? 'home' : 'category');
}
} finally {