fix: remove recommend login gate
This commit is contained in:
@@ -98,6 +98,8 @@ npm run check:server-rs-ddd
|
|||||||
|
|
||||||
该拆分只改变 `api-server` 文件组织,不改变 `/api/runtime/puzzle/*` route、DTO、error envelope、SpacetimeDB schema、公开 gallery cache 语义或计费语义;后续继续细分时也必须先保持行为不变,再单独讨论领域规则下沉。
|
该拆分只改变 `api-server` 文件组织,不改变 `/api/runtime/puzzle/*` route、DTO、error envelope、SpacetimeDB schema、公开 gallery cache 语义或计费语义;后续继续细分时也必须先保持行为不变,再单独讨论领域规则下沉。
|
||||||
|
|
||||||
|
`/api/runtime/puzzle/runs*` 当前接受 `RuntimePrincipal`,可同时识别登录用户 Bearer 和 runtime guest token。推荐页嵌入运行态的正式开局、交换、拖拽、下一关、暂停、道具与排行榜请求,应由前端在登录态下继续携带账号 access token;匿名游客仅在确认为未登录时走 runtime guest token。不要再把拼图 runtime 当成只认普通 Bearer 的纯账号接口。
|
||||||
|
|
||||||
抓大鹅 Match3D `api-server` 内部拆分:
|
抓大鹅 Match3D `api-server` 内部拆分:
|
||||||
|
|
||||||
- `server-rs/crates/api-server/src/modules/match3d.rs` 继续负责路由装配和 body limit;对外 handler 名称保持不变。
|
- `server-rs/crates/api-server/src/modules/match3d.rs` 继续负责路由装配和 body limit;对外 handler 名称保持不变。
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ Codex 项目级 hook 已放在 `.codex/config.toml` 与 `.codex/hooks/`:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo check -p api-server --manifest-path server-rs/Cargo.toml
|
cargo check -p api-server --manifest-path server-rs/Cargo.toml
|
||||||
npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "logged out recommend page can enter runtime without login gate|logged out desktop recommend page renders runtime directly without login gate"
|
npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "logged out recommend tab enters runtime without login modal|logged out desktop recommend page renders runtime directly|logged out desktop recommend rail enters runtime without login modal"
|
||||||
```
|
```
|
||||||
|
|
||||||
涉及 SpacetimeDB schema 时必须补:
|
涉及 SpacetimeDB schema 时必须补:
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ RPG / 拼图等运行态存档选择入口统一在个人中心 `次级入口 >
|
|||||||
- 结果页单关测试只能把完整草稿持久化,并通过 `levelId` 指定运行态起始关卡;不得把单关快照作为整份草稿调用 `updatePuzzleWork`,否则 source session 和作品 profile 的 `levels` 会被覆盖成单关,退出重进后其它关卡会丢失。
|
- 结果页单关测试只能把完整草稿持久化,并通过 `levelId` 指定运行态起始关卡;不得把单关快照作为整份草稿调用 `updatePuzzleWork`,否则 source session 和作品 profile 的 `levels` 会被覆盖成单关,退出重进后其它关卡会丢失。
|
||||||
- 结果页生成关卡图时若关卡名为空,前端必须传 `shouldAutoNameLevel=true`,后端复用首关命名契约先按画面描述生成关卡名,再在图片生成后用视觉命名结果精修,并把生成名和 UI 背景提示词随本次关卡快照写回。
|
- 结果页生成关卡图时若关卡名为空,前端必须传 `shouldAutoNameLevel=true`,后端复用首关命名契约先按画面描述生成关卡名,再在图片生成后用视觉命名结果精修,并把生成名和 UI 背景提示词随本次关卡快照写回。
|
||||||
- 拼图运行态背景优先读取当前关卡 `levelBackgroundImageSrc/levelBackgroundImageObjectKey`,旧数据才兼容 `uiBackgroundImageSrc/uiBackgroundImageObjectKey`;本地试玩、直达指定关卡和正式 `next-level` 推进时,目标关卡缺关卡背景时必须继承同作品首个可用关卡背景,仍缺失时才沿用当前运行态快照背景或默认 UI。运行态按钮视觉优先读取当前关卡 `uiSpritesheetImageSrc/uiSpritesheetImageObjectKey`,先按透明 alpha 自动边界检测识别 spritesheet 中的独立按钮展示矩形,再按原图位置从左到右、从上到下映射到返回、设置、下一关、提示、原图、冻结;同一组件还要按较高 alpha 阈值派生紧致点击热区,透明留白和柔边低 alpha 区域尽量不响应点击。检测失败时回退旧固定六格裁切,缺失时才用现有图标按钮兜底。有 spritesheet 时,返回和设置按钮的点击容器只提供透明点击区,不再叠加默认白色圆形底;底部提示、原图、冻结三枚素材按检测矩形的原始宽高比显示,不能强行拉伸成正圆或铺满整列。底部道具区不再使用连片胶囊背景,提示、原图、冻结三个按钮均匀分布;运行态只展示按钮素材本身,不额外叠加“提示 / 原图 / 冻结”文字。
|
- 拼图运行态背景优先读取当前关卡 `levelBackgroundImageSrc/levelBackgroundImageObjectKey`,旧数据才兼容 `uiBackgroundImageSrc/uiBackgroundImageObjectKey`;本地试玩、直达指定关卡和正式 `next-level` 推进时,目标关卡缺关卡背景时必须继承同作品首个可用关卡背景,仍缺失时才沿用当前运行态快照背景或默认 UI。运行态按钮视觉优先读取当前关卡 `uiSpritesheetImageSrc/uiSpritesheetImageObjectKey`,先按透明 alpha 自动边界检测识别 spritesheet 中的独立按钮展示矩形,再按原图位置从左到右、从上到下映射到返回、设置、下一关、提示、原图、冻结;同一组件还要按较高 alpha 阈值派生紧致点击热区,透明留白和柔边低 alpha 区域尽量不响应点击。检测失败时回退旧固定六格裁切,缺失时才用现有图标按钮兜底。有 spritesheet 时,返回和设置按钮的点击容器只提供透明点击区,不再叠加默认白色圆形底;底部提示、原图、冻结三枚素材按检测矩形的原始宽高比显示,不能强行拉伸成正圆或铺满整列。底部道具区不再使用连片胶囊背景,提示、原图、冻结三个按钮均匀分布;运行态只展示按钮素材本身,不额外叠加“提示 / 原图 / 冻结”文字。
|
||||||
|
- 推荐页本身不是登录门禁入口,未登录用户点击底部或侧边栏的推荐 Tab 应直接进入嵌入运行态,不主动打开登录弹窗。推荐页嵌入运行态必须按真实身份分流:已登录用户或本地已有 access token 时,启动拼图和后续排行榜 / 下一关等正式请求继续走账号 Bearer;只有确认为匿名访客时才申请并透传 runtime guest token。`/api/runtime/puzzle/runs*` 后端统一接受 `RuntimePrincipal`,可识别账号用户和匿名 runtime guest;推荐卡片的后台读写请求仍使用 local auth impact,避免单卡 401 清空整站登录态。创作、个人作品、删除、发布、Remix 等账号或所有权动作仍保持普通用户鉴权。
|
||||||
- 拼图运行态棋盘不叠加分块蒙版、描边、阴影、选中底色或合并块 SVG 轮廓;拼图片本体需要裁切为圆角形状,单块使用独立圆角裁切,合并块使用 SVG 原生 `clipPath` 裁切整体外轮廓,外凸角和内凹角分别计算半径,内凹角半径要比外凸角更明显以避免手机 WebView 中看起来仍是直角。原图道具只在用户主动确认后打开独立原图查看层,不在当前拼图棋盘上叠加原图。
|
- 拼图运行态棋盘不叠加分块蒙版、描边、阴影、选中底色或合并块 SVG 轮廓;拼图片本体需要裁切为圆角形状,单块使用独立圆角裁切,合并块使用 SVG 原生 `clipPath` 裁切整体外轮廓,外凸角和内凹角分别计算半径,内凹角半径要比外凸角更明显以避免手机 WebView 中看起来仍是直角。原图道具只在用户主动确认后打开独立原图查看层,不在当前拼图棋盘上叠加原图。
|
||||||
- 拼图运行态拖拽必须完全跟随手指或鼠标位置,`pointermove` 期间即时写入可见拼块的 transform,不依赖等待后端回包、React 重渲染或下一帧动画队列;进入拖动后不展示拼块选中态或“已选择”提示,松手后再提交目标格同步规则真相。
|
- 拼图运行态拖拽必须完全跟随手指或鼠标位置,`pointermove` 期间即时写入可见拼块的 transform,不依赖等待后端回包、React 重渲染或下一帧动画队列;进入拖动后不展示拼块选中态或“已选择”提示,松手后再提交目标格同步规则真相。
|
||||||
- 拼图运行态的提示、设置等点击弹层跟随当前运行态主色主题,使用普通圆角主题面板,不复用像素九宫格素材框。
|
- 拼图运行态的提示、设置等点击弹层跟随当前运行态主色主题,使用普通圆角主题面板,不复用像素九宫格素材框。
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use axum::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::require_bearer_auth,
|
auth::{require_bearer_auth, require_runtime_principal_auth},
|
||||||
puzzle::{
|
puzzle::{
|
||||||
advance_puzzle_next_level, claim_puzzle_work_point_incentive, create_puzzle_agent_session,
|
advance_puzzle_next_level, claim_puzzle_work_point_incentive, create_puzzle_agent_session,
|
||||||
delete_puzzle_work, drag_puzzle_piece_or_group, execute_puzzle_agent_action,
|
delete_puzzle_work, drag_puzzle_piece_or_group, execute_puzzle_agent_action,
|
||||||
@@ -130,56 +130,56 @@ pub fn router(state: AppState) -> Router<AppState> {
|
|||||||
"/api/runtime/puzzle/runs",
|
"/api/runtime/puzzle/runs",
|
||||||
post(start_puzzle_run).route_layer(middleware::from_fn_with_state(
|
post(start_puzzle_run).route_layer(middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
require_bearer_auth,
|
require_runtime_principal_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/runtime/puzzle/runs/{run_id}",
|
"/api/runtime/puzzle/runs/{run_id}",
|
||||||
get(get_puzzle_run).route_layer(middleware::from_fn_with_state(
|
get(get_puzzle_run).route_layer(middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
require_bearer_auth,
|
require_runtime_principal_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/runtime/puzzle/runs/{run_id}/swap",
|
"/api/runtime/puzzle/runs/{run_id}/swap",
|
||||||
post(swap_puzzle_pieces).route_layer(middleware::from_fn_with_state(
|
post(swap_puzzle_pieces).route_layer(middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
require_bearer_auth,
|
require_runtime_principal_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/runtime/puzzle/runs/{run_id}/drag",
|
"/api/runtime/puzzle/runs/{run_id}/drag",
|
||||||
post(drag_puzzle_piece_or_group).route_layer(middleware::from_fn_with_state(
|
post(drag_puzzle_piece_or_group).route_layer(middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
require_bearer_auth,
|
require_runtime_principal_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/runtime/puzzle/runs/{run_id}/next-level",
|
"/api/runtime/puzzle/runs/{run_id}/next-level",
|
||||||
post(advance_puzzle_next_level).route_layer(middleware::from_fn_with_state(
|
post(advance_puzzle_next_level).route_layer(middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
require_bearer_auth,
|
require_runtime_principal_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/runtime/puzzle/runs/{run_id}/pause",
|
"/api/runtime/puzzle/runs/{run_id}/pause",
|
||||||
post(update_puzzle_run_pause).route_layer(middleware::from_fn_with_state(
|
post(update_puzzle_run_pause).route_layer(middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
require_bearer_auth,
|
require_runtime_principal_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/runtime/puzzle/runs/{run_id}/props",
|
"/api/runtime/puzzle/runs/{run_id}/props",
|
||||||
post(use_puzzle_runtime_prop).route_layer(middleware::from_fn_with_state(
|
post(use_puzzle_runtime_prop).route_layer(middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
require_bearer_auth,
|
require_runtime_principal_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/runtime/puzzle/runs/{run_id}/leaderboard",
|
"/api/runtime/puzzle/runs/{run_id}/leaderboard",
|
||||||
post(submit_puzzle_leaderboard).route_layer(middleware::from_fn_with_state(
|
post(submit_puzzle_leaderboard).route_layer(middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
require_bearer_auth,
|
require_runtime_principal_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.with_state(PuzzleApiState::from_ref(&state))
|
.with_state(PuzzleApiState::from_ref(&state))
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ use crate::{
|
|||||||
execute_billable_asset_operation, execute_billable_asset_operation_with_cost,
|
execute_billable_asset_operation, execute_billable_asset_operation_with_cost,
|
||||||
should_skip_asset_operation_billing_for_connectivity,
|
should_skip_asset_operation_billing_for_connectivity,
|
||||||
},
|
},
|
||||||
auth::AuthenticatedAccessToken,
|
auth::{AuthenticatedAccessToken, RuntimePrincipal},
|
||||||
generated_asset_sheets::apply_generated_asset_sheet_green_screen_alpha,
|
generated_asset_sheets::apply_generated_asset_sheet_green_screen_alpha,
|
||||||
http_error::AppError,
|
http_error::AppError,
|
||||||
llm_model_routing::{CREATION_TEMPLATE_LLM_MODEL, PUZZLE_LEVEL_NAME_VISION_LLM_MODEL},
|
llm_model_routing::{CREATION_TEMPLATE_LLM_MODEL, PUZZLE_LEVEL_NAME_VISION_LLM_MODEL},
|
||||||
|
|||||||
@@ -1666,7 +1666,7 @@ pub async fn remix_puzzle_gallery_work(
|
|||||||
pub async fn start_puzzle_run(
|
pub async fn start_puzzle_run(
|
||||||
State(state): State<PuzzleApiState>,
|
State(state): State<PuzzleApiState>,
|
||||||
Extension(request_context): Extension<RequestContext>,
|
Extension(request_context): Extension<RequestContext>,
|
||||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
Extension(principal): Extension<RuntimePrincipal>,
|
||||||
payload: Result<Json<StartPuzzleRunRequest>, JsonRejection>,
|
payload: Result<Json<StartPuzzleRunRequest>, JsonRejection>,
|
||||||
) -> Result<Json<Value>, Response> {
|
) -> Result<Json<Value>, Response> {
|
||||||
let Json(payload) = payload.map_err(|error| {
|
let Json(payload) = payload.map_err(|error| {
|
||||||
@@ -1690,7 +1690,7 @@ pub async fn start_puzzle_run(
|
|||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.start_puzzle_run(PuzzleRunStartRecordInput {
|
.start_puzzle_run(PuzzleRunStartRecordInput {
|
||||||
run_id: build_prefixed_uuid_id("puzzle-run-"),
|
run_id: build_prefixed_uuid_id("puzzle-run-"),
|
||||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
owner_user_id: principal.subject().to_string(),
|
||||||
profile_id: payload.profile_id.clone(),
|
profile_id: payload.profile_id.clone(),
|
||||||
level_id: payload.level_id.clone(),
|
level_id: payload.level_id.clone(),
|
||||||
started_at_micros: current_utc_micros(),
|
started_at_micros: current_utc_micros(),
|
||||||
@@ -1707,16 +1707,18 @@ pub async fn start_puzzle_run(
|
|||||||
record_puzzle_work_play_start_after_success(
|
record_puzzle_work_play_start_after_success(
|
||||||
&state,
|
&state,
|
||||||
&request_context,
|
&request_context,
|
||||||
WorkPlayTrackingDraft::new(
|
WorkPlayTrackingDraft::runtime_principal(
|
||||||
"puzzle",
|
"puzzle",
|
||||||
payload.profile_id.clone(),
|
payload.profile_id.clone(),
|
||||||
&authenticated,
|
&principal,
|
||||||
"/api/runtime/puzzle/...",
|
"/api/runtime/puzzle/...",
|
||||||
)
|
)
|
||||||
.profile_id(payload.profile_id.clone())
|
.profile_id(payload.profile_id.clone())
|
||||||
|
.owner_user_id(principal.subject().to_string())
|
||||||
.extra(json!({
|
.extra(json!({
|
||||||
"levelId": payload.level_id,
|
"levelId": payload.level_id,
|
||||||
"runId": run.run_id,
|
"runId": run.run_id,
|
||||||
|
"principalKind": principal.kind().as_str(),
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -1733,13 +1735,13 @@ pub async fn get_puzzle_run(
|
|||||||
State(state): State<PuzzleApiState>,
|
State(state): State<PuzzleApiState>,
|
||||||
AxumPath(run_id): AxumPath<String>,
|
AxumPath(run_id): AxumPath<String>,
|
||||||
Extension(request_context): Extension<RequestContext>,
|
Extension(request_context): Extension<RequestContext>,
|
||||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
Extension(principal): Extension<RuntimePrincipal>,
|
||||||
) -> Result<Json<Value>, Response> {
|
) -> Result<Json<Value>, Response> {
|
||||||
ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &run_id, "runId")?;
|
ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &run_id, "runId")?;
|
||||||
|
|
||||||
let run = state
|
let run = state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.get_puzzle_run(run_id, authenticated.claims().user_id().to_string())
|
.get_puzzle_run(run_id, principal.subject().to_string())
|
||||||
.await
|
.await
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
puzzle_error_response(
|
puzzle_error_response(
|
||||||
@@ -1761,7 +1763,7 @@ pub async fn swap_puzzle_pieces(
|
|||||||
State(state): State<PuzzleApiState>,
|
State(state): State<PuzzleApiState>,
|
||||||
AxumPath(run_id): AxumPath<String>,
|
AxumPath(run_id): AxumPath<String>,
|
||||||
Extension(request_context): Extension<RequestContext>,
|
Extension(request_context): Extension<RequestContext>,
|
||||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
Extension(principal): Extension<RuntimePrincipal>,
|
||||||
payload: Result<Json<SwapPuzzlePiecesRequest>, JsonRejection>,
|
payload: Result<Json<SwapPuzzlePiecesRequest>, JsonRejection>,
|
||||||
) -> Result<Json<Value>, Response> {
|
) -> Result<Json<Value>, Response> {
|
||||||
let Json(payload) = payload.map_err(|error| {
|
let Json(payload) = payload.map_err(|error| {
|
||||||
@@ -1792,7 +1794,7 @@ pub async fn swap_puzzle_pieces(
|
|||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.swap_puzzle_pieces(PuzzleRunSwapRecordInput {
|
.swap_puzzle_pieces(PuzzleRunSwapRecordInput {
|
||||||
run_id,
|
run_id,
|
||||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
owner_user_id: principal.subject().to_string(),
|
||||||
first_piece_id: payload.first_piece_id,
|
first_piece_id: payload.first_piece_id,
|
||||||
second_piece_id: payload.second_piece_id,
|
second_piece_id: payload.second_piece_id,
|
||||||
swapped_at_micros: current_utc_micros(),
|
swapped_at_micros: current_utc_micros(),
|
||||||
@@ -1818,7 +1820,7 @@ pub async fn drag_puzzle_piece_or_group(
|
|||||||
State(state): State<PuzzleApiState>,
|
State(state): State<PuzzleApiState>,
|
||||||
AxumPath(run_id): AxumPath<String>,
|
AxumPath(run_id): AxumPath<String>,
|
||||||
Extension(request_context): Extension<RequestContext>,
|
Extension(request_context): Extension<RequestContext>,
|
||||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
Extension(principal): Extension<RuntimePrincipal>,
|
||||||
payload: Result<Json<DragPuzzlePieceRequest>, JsonRejection>,
|
payload: Result<Json<DragPuzzlePieceRequest>, JsonRejection>,
|
||||||
) -> Result<Json<Value>, Response> {
|
) -> Result<Json<Value>, Response> {
|
||||||
let Json(payload) = payload.map_err(|error| {
|
let Json(payload) = payload.map_err(|error| {
|
||||||
@@ -1843,7 +1845,7 @@ pub async fn drag_puzzle_piece_or_group(
|
|||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.drag_puzzle_piece_or_group(PuzzleRunDragRecordInput {
|
.drag_puzzle_piece_or_group(PuzzleRunDragRecordInput {
|
||||||
run_id,
|
run_id,
|
||||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
owner_user_id: principal.subject().to_string(),
|
||||||
piece_id: payload.piece_id,
|
piece_id: payload.piece_id,
|
||||||
target_row: payload.target_row,
|
target_row: payload.target_row,
|
||||||
target_col: payload.target_col,
|
target_col: payload.target_col,
|
||||||
@@ -1870,7 +1872,7 @@ pub async fn advance_puzzle_next_level(
|
|||||||
State(state): State<PuzzleApiState>,
|
State(state): State<PuzzleApiState>,
|
||||||
AxumPath(run_id): AxumPath<String>,
|
AxumPath(run_id): AxumPath<String>,
|
||||||
Extension(request_context): Extension<RequestContext>,
|
Extension(request_context): Extension<RequestContext>,
|
||||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
Extension(principal): Extension<RuntimePrincipal>,
|
||||||
payload: Result<Json<AdvancePuzzleNextLevelRequest>, JsonRejection>,
|
payload: Result<Json<AdvancePuzzleNextLevelRequest>, JsonRejection>,
|
||||||
) -> Result<Json<Value>, Response> {
|
) -> Result<Json<Value>, Response> {
|
||||||
ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &run_id, "runId")?;
|
ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &run_id, "runId")?;
|
||||||
@@ -1897,7 +1899,7 @@ pub async fn advance_puzzle_next_level(
|
|||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.advance_puzzle_next_level(spacetime_client::PuzzleRunNextLevelRecordInput {
|
.advance_puzzle_next_level(spacetime_client::PuzzleRunNextLevelRecordInput {
|
||||||
run_id,
|
run_id,
|
||||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
owner_user_id: principal.subject().to_string(),
|
||||||
target_profile_id: payload.target_profile_id,
|
target_profile_id: payload.target_profile_id,
|
||||||
advanced_at_micros: current_utc_micros(),
|
advanced_at_micros: current_utc_micros(),
|
||||||
})
|
})
|
||||||
@@ -1922,7 +1924,7 @@ pub async fn update_puzzle_run_pause(
|
|||||||
State(state): State<PuzzleApiState>,
|
State(state): State<PuzzleApiState>,
|
||||||
AxumPath(run_id): AxumPath<String>,
|
AxumPath(run_id): AxumPath<String>,
|
||||||
Extension(request_context): Extension<RequestContext>,
|
Extension(request_context): Extension<RequestContext>,
|
||||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
Extension(principal): Extension<RuntimePrincipal>,
|
||||||
payload: Result<Json<UpdatePuzzleRuntimePauseRequest>, JsonRejection>,
|
payload: Result<Json<UpdatePuzzleRuntimePauseRequest>, JsonRejection>,
|
||||||
) -> Result<Json<Value>, Response> {
|
) -> Result<Json<Value>, Response> {
|
||||||
let Json(payload) = payload.map_err(|error| {
|
let Json(payload) = payload.map_err(|error| {
|
||||||
@@ -1941,7 +1943,7 @@ pub async fn update_puzzle_run_pause(
|
|||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.update_puzzle_run_pause(PuzzleRunPauseRecordInput {
|
.update_puzzle_run_pause(PuzzleRunPauseRecordInput {
|
||||||
run_id,
|
run_id,
|
||||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
owner_user_id: principal.subject().to_string(),
|
||||||
paused: payload.paused,
|
paused: payload.paused,
|
||||||
updated_at_micros: current_utc_micros(),
|
updated_at_micros: current_utc_micros(),
|
||||||
})
|
})
|
||||||
@@ -1966,7 +1968,7 @@ pub async fn use_puzzle_runtime_prop(
|
|||||||
State(state): State<PuzzleApiState>,
|
State(state): State<PuzzleApiState>,
|
||||||
AxumPath(run_id): AxumPath<String>,
|
AxumPath(run_id): AxumPath<String>,
|
||||||
Extension(request_context): Extension<RequestContext>,
|
Extension(request_context): Extension<RequestContext>,
|
||||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
Extension(principal): Extension<RuntimePrincipal>,
|
||||||
payload: Result<Json<UsePuzzleRuntimePropRequest>, JsonRejection>,
|
payload: Result<Json<UsePuzzleRuntimePropRequest>, JsonRejection>,
|
||||||
) -> Result<Json<Value>, Response> {
|
) -> Result<Json<Value>, Response> {
|
||||||
let Json(payload) = payload.map_err(|error| {
|
let Json(payload) = payload.map_err(|error| {
|
||||||
@@ -1987,7 +1989,7 @@ pub async fn use_puzzle_runtime_prop(
|
|||||||
"propKind",
|
"propKind",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let owner_user_id = authenticated.claims().user_id().to_string();
|
let owner_user_id = principal.subject().to_string();
|
||||||
let prop_kind = payload.prop_kind.trim().to_string();
|
let prop_kind = payload.prop_kind.trim().to_string();
|
||||||
let billing_asset_kind = match prop_kind.as_str() {
|
let billing_asset_kind = match prop_kind.as_str() {
|
||||||
"hint" => "puzzle_prop_hint",
|
"hint" => "puzzle_prop_hint",
|
||||||
@@ -2064,7 +2066,7 @@ pub async fn submit_puzzle_leaderboard(
|
|||||||
State(state): State<PuzzleApiState>,
|
State(state): State<PuzzleApiState>,
|
||||||
AxumPath(run_id): AxumPath<String>,
|
AxumPath(run_id): AxumPath<String>,
|
||||||
Extension(request_context): Extension<RequestContext>,
|
Extension(request_context): Extension<RequestContext>,
|
||||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
Extension(principal): Extension<RuntimePrincipal>,
|
||||||
payload: Result<Json<SubmitPuzzleLeaderboardRequest>, JsonRejection>,
|
payload: Result<Json<SubmitPuzzleLeaderboardRequest>, JsonRejection>,
|
||||||
) -> Result<Json<Value>, Response> {
|
) -> Result<Json<Value>, Response> {
|
||||||
let Json(payload) = payload.map_err(|error| {
|
let Json(payload) = payload.map_err(|error| {
|
||||||
@@ -2084,7 +2086,7 @@ pub async fn submit_puzzle_leaderboard(
|
|||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.submit_puzzle_leaderboard_entry(PuzzleLeaderboardSubmitRecordInput {
|
.submit_puzzle_leaderboard_entry(PuzzleLeaderboardSubmitRecordInput {
|
||||||
run_id,
|
run_id,
|
||||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
owner_user_id: principal.subject().to_string(),
|
||||||
profile_id: payload.profile_id,
|
profile_id: payload.profile_id,
|
||||||
grid_size: payload.grid_size,
|
grid_size: payload.grid_size,
|
||||||
elapsed_ms: payload.elapsed_ms.max(1_000),
|
elapsed_ms: payload.elapsed_ms.max(1_000),
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ import { resolveWorkNotFoundRecoveryAction } from '../../routing/runtimeNotFound
|
|||||||
import {
|
import {
|
||||||
ApiClientError,
|
ApiClientError,
|
||||||
BACKGROUND_AUTH_REQUEST_OPTIONS,
|
BACKGROUND_AUTH_REQUEST_OPTIONS,
|
||||||
|
getStoredAccessToken,
|
||||||
} from '../../services/apiClient';
|
} from '../../services/apiClient';
|
||||||
import {
|
import {
|
||||||
ensureRuntimeGuestToken,
|
ensureRuntimeGuestToken,
|
||||||
@@ -559,6 +560,25 @@ async function buildRecommendRuntimeGuestOptions() {
|
|||||||
runtimeGuestToken: token,
|
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 PUZZLE_DRAFT_GENERATION_POINT_COST = 2;
|
||||||
const MATCH3D_DRAFT_GENERATION_POINT_COST = 10;
|
const MATCH3D_DRAFT_GENERATION_POINT_COST = 10;
|
||||||
const BARK_BATTLE_DRAFT_GENERATION_POINT_COST = 3;
|
const BARK_BATTLE_DRAFT_GENERATION_POINT_COST = 3;
|
||||||
@@ -7386,9 +7406,10 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
profileId: targetProfileId,
|
profileId: targetProfileId,
|
||||||
mode: 'play' as const,
|
mode: 'play' as const,
|
||||||
};
|
};
|
||||||
const runtimeGuestOptions = options.embedded
|
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||||
? await buildRecommendRuntimeGuestOptions()
|
authUi,
|
||||||
: {};
|
options.embedded,
|
||||||
|
);
|
||||||
const { run } = options.embedded
|
const { run } = options.embedded
|
||||||
? await startVisualNovelRun(
|
? await startVisualNovelRun(
|
||||||
targetProfileId,
|
targetProfileId,
|
||||||
@@ -7419,6 +7440,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
authUi,
|
||||||
resolvePuzzleErrorMessage,
|
resolvePuzzleErrorMessage,
|
||||||
setIsVisualNovelBusy,
|
setIsVisualNovelBusy,
|
||||||
setSelectionStage,
|
setSelectionStage,
|
||||||
@@ -7442,7 +7464,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
try {
|
try {
|
||||||
const runtimeGuestOptions =
|
const runtimeGuestOptions =
|
||||||
activeRecommendRuntimeKind === 'visual-novel'
|
activeRecommendRuntimeKind === 'visual-novel'
|
||||||
? await buildRecommendRuntimeGuestOptions()
|
? await buildRecommendRuntimeAuthOptions(authUi, true)
|
||||||
: {};
|
: {};
|
||||||
const nextRun = await streamVisualNovelRuntimeAction(
|
const nextRun = await streamVisualNovelRuntimeAction(
|
||||||
visualNovelRun.runId,
|
visualNovelRun.runId,
|
||||||
@@ -7460,6 +7482,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
activeRecommendRuntimeKind,
|
activeRecommendRuntimeKind,
|
||||||
|
authUi,
|
||||||
isVisualNovelBusy,
|
isVisualNovelBusy,
|
||||||
resolvePuzzleErrorMessage,
|
resolvePuzzleErrorMessage,
|
||||||
setIsVisualNovelBusy,
|
setIsVisualNovelBusy,
|
||||||
@@ -7868,9 +7891,10 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setJumpHopError(null);
|
setJumpHopError(null);
|
||||||
setJumpHopRuntimeReturnStage(options.returnStage ?? 'work-detail');
|
setJumpHopRuntimeReturnStage(options.returnStage ?? 'work-detail');
|
||||||
try {
|
try {
|
||||||
const runtimeGuestOptions = options.embedded
|
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||||
? await buildRecommendRuntimeGuestOptions()
|
authUi,
|
||||||
: {};
|
options.embedded,
|
||||||
|
);
|
||||||
const [detail, runResponse] = await Promise.all([
|
const [detail, runResponse] = await Promise.all([
|
||||||
jumpHopClient.getWorkDetail(normalizedProfileId).catch(() => null),
|
jumpHopClient.getWorkDetail(normalizedProfileId).catch(() => null),
|
||||||
jumpHopClient.startRun(normalizedProfileId, runtimeGuestOptions),
|
jumpHopClient.startRun(normalizedProfileId, runtimeGuestOptions),
|
||||||
@@ -7898,7 +7922,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setIsJumpHopBusy(false);
|
setIsJumpHopBusy(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setSelectionStage],
|
[authUi, setSelectionStage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const restartJumpHopRuntimeRun = useCallback(async () => {
|
const restartJumpHopRuntimeRun = useCallback(async () => {
|
||||||
@@ -8205,9 +8229,10 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setWoodenFishError(null);
|
setWoodenFishError(null);
|
||||||
setWoodenFishRuntimeReturnStage(options.returnStage ?? 'work-detail');
|
setWoodenFishRuntimeReturnStage(options.returnStage ?? 'work-detail');
|
||||||
try {
|
try {
|
||||||
const runtimeGuestOptions = options.embedded
|
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||||
? await buildRecommendRuntimeGuestOptions()
|
authUi,
|
||||||
: {};
|
options.embedded,
|
||||||
|
);
|
||||||
const [detail, runResponse] = await Promise.all([
|
const [detail, runResponse] = await Promise.all([
|
||||||
woodenFishClient.getWorkDetail(normalizedProfileId).catch(() => null),
|
woodenFishClient.getWorkDetail(normalizedProfileId).catch(() => null),
|
||||||
options.embedded
|
options.embedded
|
||||||
@@ -8237,7 +8262,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setIsWoodenFishBusy(false);
|
setIsWoodenFishBusy(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setSelectionStage],
|
[authUi, setSelectionStage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const checkpointWoodenFishRuntimeRun = useCallback(
|
const checkpointWoodenFishRuntimeRun = useCallback(
|
||||||
@@ -8640,12 +8665,14 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
profileId: item.profileId,
|
profileId: item.profileId,
|
||||||
levelId: levelId ?? null,
|
levelId: levelId ?? null,
|
||||||
};
|
};
|
||||||
const runtimeGuestOptions = options.embedded
|
const canUseRuntimeGuestAuth =
|
||||||
|
options.embedded || options.authMode === 'isolated';
|
||||||
|
const useRuntimeGuestAuth =
|
||||||
|
canUseRuntimeGuestAuth && shouldUseRecommendRuntimeGuestAuth(authUi);
|
||||||
|
const runtimeGuestOptions = useRuntimeGuestAuth
|
||||||
? await buildRecommendRuntimeGuestOptions()
|
? await buildRecommendRuntimeGuestOptions()
|
||||||
: {};
|
: {};
|
||||||
const authMode = options.embedded
|
const authMode = useRuntimeGuestAuth ? 'isolated' : 'default';
|
||||||
? 'isolated'
|
|
||||||
: (options.authMode ?? 'default');
|
|
||||||
const { run } =
|
const { run } =
|
||||||
authMode === 'isolated'
|
authMode === 'isolated'
|
||||||
? await startPuzzleRun(startRunPayload, runtimeGuestOptions)
|
? await startPuzzleRun(startRunPayload, runtimeGuestOptions)
|
||||||
@@ -8692,6 +8719,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
isPuzzleBusy,
|
isPuzzleBusy,
|
||||||
|
authUi,
|
||||||
resolvePuzzleErrorMessage,
|
resolvePuzzleErrorMessage,
|
||||||
setIsPuzzleBusy,
|
setIsPuzzleBusy,
|
||||||
setPuzzleError,
|
setPuzzleError,
|
||||||
@@ -8744,9 +8772,10 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
runtimeProfile.generatedBackgroundAsset,
|
runtimeProfile.generatedBackgroundAsset,
|
||||||
{ expireSeconds: 300 },
|
{ expireSeconds: 300 },
|
||||||
);
|
);
|
||||||
const runtimeGuestOptions = options.embedded
|
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||||
? await buildRecommendRuntimeGuestOptions()
|
authUi,
|
||||||
: {};
|
options.embedded,
|
||||||
|
);
|
||||||
const runtimeOptions = {
|
const runtimeOptions = {
|
||||||
...runtimeGuestOptions,
|
...runtimeGuestOptions,
|
||||||
...(typeof options.itemTypeCountOverride === 'number'
|
...(typeof options.itemTypeCountOverride === 'number'
|
||||||
@@ -8793,6 +8822,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
isMatch3DBusy,
|
isMatch3DBusy,
|
||||||
|
authUi,
|
||||||
match3dFlow,
|
match3dFlow,
|
||||||
match3dRuntimeAdapter,
|
match3dRuntimeAdapter,
|
||||||
resolveMatch3DErrorMessage,
|
resolveMatch3DErrorMessage,
|
||||||
@@ -8816,9 +8846,10 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setSquareHoleError(null);
|
setSquareHoleError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const runtimeGuestOptions = options.embedded
|
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||||
? await buildRecommendRuntimeGuestOptions()
|
authUi,
|
||||||
: {};
|
options.embedded,
|
||||||
|
);
|
||||||
const { run } = options.embedded
|
const { run } = options.embedded
|
||||||
? await startSquareHoleRun(profile.profileId, runtimeGuestOptions)
|
? await startSquareHoleRun(profile.profileId, runtimeGuestOptions)
|
||||||
: await startSquareHoleRun(profile.profileId);
|
: await startSquareHoleRun(profile.profileId);
|
||||||
@@ -8852,6 +8883,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
isSquareHoleBusy,
|
isSquareHoleBusy,
|
||||||
|
authUi,
|
||||||
resolveSquareHoleErrorMessage,
|
resolveSquareHoleErrorMessage,
|
||||||
setSelectionStage,
|
setSelectionStage,
|
||||||
setSquareHoleError,
|
setSquareHoleError,
|
||||||
@@ -8974,7 +9006,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
try {
|
try {
|
||||||
const runtimeGuestOptions =
|
const runtimeGuestOptions =
|
||||||
activeRecommendRuntimeKind === 'big-fish'
|
activeRecommendRuntimeKind === 'big-fish'
|
||||||
? await buildRecommendRuntimeGuestOptions()
|
? await buildRecommendRuntimeAuthOptions(authUi, true)
|
||||||
: {};
|
: {};
|
||||||
const { run } = await submitBigFishRuntimeInput(
|
const { run } = await submitBigFishRuntimeInput(
|
||||||
bigFishRun.runId,
|
bigFishRun.runId,
|
||||||
@@ -8992,6 +9024,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
activeRecommendRuntimeKind,
|
activeRecommendRuntimeKind,
|
||||||
|
authUi,
|
||||||
bigFishRun,
|
bigFishRun,
|
||||||
resolveBigFishErrorMessage,
|
resolveBigFishErrorMessage,
|
||||||
setBigFishError,
|
setBigFishError,
|
||||||
@@ -9008,10 +9041,9 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setBigFishRuntimeStartedAt(null);
|
setBigFishRuntimeStartedAt(null);
|
||||||
const reportPromise =
|
const reportPromise =
|
||||||
activeRecommendRuntimeKind === 'big-fish'
|
activeRecommendRuntimeKind === 'big-fish'
|
||||||
? recordBigFishPlay(
|
? buildRecommendRuntimeAuthOptions(authUi, true).then(
|
||||||
sessionId,
|
(runtimeAuthOptions) =>
|
||||||
{ elapsedMs },
|
recordBigFishPlay(sessionId, { elapsedMs }, runtimeAuthOptions),
|
||||||
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
|
||||||
)
|
)
|
||||||
: recordBigFishPlay(sessionId, { elapsedMs });
|
: recordBigFishPlay(sessionId, { elapsedMs });
|
||||||
void reportPromise.catch((error) => {
|
void reportPromise.catch((error) => {
|
||||||
@@ -9021,6 +9053,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
activeRecommendRuntimeKind,
|
activeRecommendRuntimeKind,
|
||||||
|
authUi,
|
||||||
bigFishRun?.sessionId,
|
bigFishRun?.sessionId,
|
||||||
bigFishRuntimeStartedAt,
|
bigFishRuntimeStartedAt,
|
||||||
resolveBigFishErrorMessage,
|
resolveBigFishErrorMessage,
|
||||||
@@ -11315,9 +11348,10 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setBigFishRuntimeReturnStage(returnStage);
|
setBigFishRuntimeReturnStage(returnStage);
|
||||||
setBigFishRun(null);
|
setBigFishRun(null);
|
||||||
try {
|
try {
|
||||||
const runtimeGuestOptions = options.embedded
|
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||||
? await buildRecommendRuntimeGuestOptions()
|
authUi,
|
||||||
: {};
|
options.embedded,
|
||||||
|
);
|
||||||
const { run } = options.embedded
|
const { run } = options.embedded
|
||||||
? await startBigFishRuntimeRun(sessionId, runtimeGuestOptions)
|
? await startBigFishRuntimeRun(sessionId, runtimeGuestOptions)
|
||||||
: await startBigFishRuntimeRun(sessionId);
|
: await startBigFishRuntimeRun(sessionId);
|
||||||
@@ -11345,7 +11379,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[bigFishFlow, resolveBigFishErrorMessage, setBigFishError, setSelectionStage],
|
[authUi, bigFishFlow, resolveBigFishErrorMessage, setBigFishError, setSelectionStage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const startBarkBattleRunFromWork = useCallback(
|
const startBarkBattleRunFromWork = useCallback(
|
||||||
@@ -11365,9 +11399,10 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setBarkBattlePublishedConfig(mapBarkBattleWorkToPublishedConfig(item));
|
setBarkBattlePublishedConfig(mapBarkBattleWorkToPublishedConfig(item));
|
||||||
setBarkBattleRuntimeReturnStage(returnStage);
|
setBarkBattleRuntimeReturnStage(returnStage);
|
||||||
try {
|
try {
|
||||||
const runtimeGuestOptions = options.embedded
|
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||||
? await buildRecommendRuntimeGuestOptions()
|
authUi,
|
||||||
: {};
|
options.embedded,
|
||||||
|
);
|
||||||
const runResponse = options.embedded
|
const runResponse = options.embedded
|
||||||
? await startBarkBattleRun(item.workId, {}, runtimeGuestOptions)
|
? await startBarkBattleRun(item.workId, {}, runtimeGuestOptions)
|
||||||
: await startBarkBattleRun(item.workId);
|
: await startBarkBattleRun(item.workId);
|
||||||
@@ -11390,7 +11425,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[resolveBarkBattleErrorMessage, setSelectionStage],
|
[authUi, resolveBarkBattleErrorMessage, setSelectionStage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const startSelectedPublicWork = useCallback(() => {
|
const startSelectedPublicWork = useCallback(() => {
|
||||||
|
|||||||
@@ -6121,11 +6121,52 @@ test('home recommendation starts embedded puzzle without global auth reset on lo
|
|||||||
profileId: 'puzzle-profile-public-1',
|
profileId: 'puzzle-profile-public-1',
|
||||||
levelId: null,
|
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 () => {
|
test('home recommendation Match3D runtime keeps profile generated models when card summary is stale', async () => {
|
||||||
const match3dCard: Match3DWorkSummary = {
|
const match3dCard: Match3DWorkSummary = {
|
||||||
workId: 'match3d-work-card-1',
|
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',
|
profileId: 'puzzle-profile-public-1',
|
||||||
levelId: null,
|
levelId: null,
|
||||||
},
|
},
|
||||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
|
||||||
);
|
);
|
||||||
vi.mocked(listProfileSaveArchives).mockClear();
|
vi.mocked(listProfileSaveArchives).mockClear();
|
||||||
vi.mocked(listProfileSaveArchives).mockRejectedValueOnce(
|
vi.mocked(listProfileSaveArchives).mockRejectedValueOnce(
|
||||||
@@ -7159,7 +7199,6 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
|
|||||||
elapsedMs: 18_000,
|
elapsedMs: 18_000,
|
||||||
nickname: '测试玩家',
|
nickname: '测试玩家',
|
||||||
},
|
},
|
||||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -7180,7 +7219,6 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
|
|||||||
expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
|
expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
|
||||||
clearedFirstLevel.runId,
|
clearedFirstLevel.runId,
|
||||||
{},
|
{},
|
||||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
@@ -7343,7 +7381,6 @@ test('formal puzzle similar work keeps current run level progression', async ()
|
|||||||
expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
|
expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
|
||||||
clearedThirdLevel.runId,
|
clearedThirdLevel.runId,
|
||||||
{ targetProfileId: 'puzzle-profile-similar-2' },
|
{ targetProfileId: 'puzzle-profile-similar-2' },
|
||||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(startPuzzleRun).not.toHaveBeenCalled();
|
expect(startPuzzleRun).not.toHaveBeenCalled();
|
||||||
@@ -7527,7 +7564,6 @@ test('recommend puzzle remix return restarts recommendation instead of stale loa
|
|||||||
profileId: 'puzzle-profile-public-1',
|
profileId: 'puzzle-profile-public-1',
|
||||||
levelId: null,
|
levelId: null,
|
||||||
},
|
},
|
||||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(screen.queryByText('正在进入拼图关卡')).toBeNull();
|
expect(screen.queryByText('正在进入拼图关卡')).toBeNull();
|
||||||
|
|||||||
@@ -2697,7 +2697,7 @@ test('logged out mobile shell defaults to discover tab', () => {
|
|||||||
).toBeNull();
|
).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 user = userEvent.setup();
|
||||||
const { container, openLoginModal } = renderStatefulLoggedOutHomeView({
|
const { container, openLoginModal } = renderStatefulLoggedOutHomeView({
|
||||||
latestEntries: [puzzlePublicEntry],
|
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: '推荐' }),
|
within(bottomNav as HTMLElement).getByRole('button', { name: '推荐' }),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(openLoginModal).toHaveBeenCalledTimes(1);
|
expect(openLoginModal).not.toHaveBeenCalled();
|
||||||
expect(
|
expect(container.querySelector('.platform-recommend-cover-only')).toBeNull();
|
||||||
container.querySelector('.platform-recommend-cover-only'),
|
|
||||||
).toBeTruthy();
|
|
||||||
expect(container.querySelector('.platform-mobile-topbar')).toBeNull();
|
expect(container.querySelector('.platform-mobile-topbar')).toBeNull();
|
||||||
expect(
|
expect(
|
||||||
container.querySelector('.platform-mobile-entry-shell--recommend'),
|
container.querySelector('.platform-mobile-entry-shell--recommend'),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
expect(screen.queryByTestId('recommend-runtime')).toBeNull();
|
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
|
||||||
expect(screen.queryByLabelText('奇幻拼图 作品信息')).toBeNull();
|
expect(screen.getByLabelText('奇幻拼图 作品信息')).toBeTruthy();
|
||||||
expect(screen.getAllByText('奇幻拼图').length).toBeGreaterThan(0);
|
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 user = userEvent.setup();
|
||||||
const onOpenGalleryDetail = vi.fn();
|
const onOpenGalleryDetail = vi.fn();
|
||||||
const { openLoginModal } = renderStatefulLoggedOutHomeView({
|
const { openLoginModal } = renderStatefulLoggedOutHomeView({
|
||||||
@@ -2741,12 +2739,9 @@ test('logged out recommend cover opens login modal again', async () => {
|
|||||||
await user.click(
|
await user.click(
|
||||||
within(bottomNav as HTMLElement).getByRole('button', { name: '推荐' }),
|
within(bottomNav as HTMLElement).getByRole('button', { name: '推荐' }),
|
||||||
);
|
);
|
||||||
await user.click(
|
|
||||||
screen.getByRole('button', { name: /登录后游玩 奇幻拼图/u }),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(openLoginModal).toHaveBeenCalledTimes(2);
|
expect(openLoginModal).not.toHaveBeenCalled();
|
||||||
expect(openLoginModal).toHaveBeenLastCalledWith();
|
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
|
||||||
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
|
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2780,6 +2775,26 @@ test('logged out recommend page can enter runtime without login gate', () => {
|
|||||||
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
|
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 () => {
|
test('logged in recommend page uses gated recommend detail callback', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const onOpenGalleryDetail = vi.fn();
|
const onOpenGalleryDetail = vi.fn();
|
||||||
|
|||||||
@@ -5372,7 +5372,7 @@ export function RpgEntryHomeView({
|
|||||||
{recommendRuntimeError}
|
{recommendRuntimeError}
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
) : isStartingRecommendEntry || !recommendRuntimeContent ? (
|
) : isStartingRecommendEntry ? (
|
||||||
<section className="platform-recommend-runtime-panel">
|
<section className="platform-recommend-runtime-panel">
|
||||||
<div className="platform-recommend-runtime-state">加载中...</div>
|
<div className="platform-recommend-runtime-state">加载中...</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -6761,12 +6761,6 @@ export function RpgEntryHomeView({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAuthenticated && tab === 'home') {
|
|
||||||
onTabChange(tab);
|
|
||||||
authUi?.openLoginModal();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onTabChange(tab);
|
onTabChange(tab);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -6924,12 +6918,6 @@ export function RpgEntryHomeView({
|
|||||||
emphasized={tab === 'create'}
|
emphasized={tab === 'create'}
|
||||||
showDot={tab === 'saves' && hasUnreadDraftUpdate}
|
showDot={tab === 'saves' && hasUnreadDraftUpdate}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!isAuthenticated && tab === 'home') {
|
|
||||||
onTabChange(tab);
|
|
||||||
authUi?.openLoginModal();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onTabChange(tab);
|
onTabChange(tab);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ export function useRpgEntryBootstrap(
|
|||||||
!hasInitialAgentSession &&
|
!hasInitialAgentSession &&
|
||||||
!hasExplicitPlatformTabSelectionRef.current
|
!hasExplicitPlatformTabSelectionRef.current
|
||||||
) {
|
) {
|
||||||
// 中文注释:新用户先进入发现页;推荐页只在用户主动点击后作为登录门禁入口。
|
// 中文注释:新用户先进入发现页;推荐页可直接进入,真正受保护的动作再单独做登录门禁。
|
||||||
setPlatformTabState(isAuthenticated ? 'home' : 'category');
|
setPlatformTabState(isAuthenticated ? 'home' : 'category');
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user