From b355568189a9729a0238a969678b8d71573555ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E7=89=A9?= <253518756@qq.com> Date: Fri, 24 Apr 2026 16:15:00 +0800 Subject: [PATCH] 1 --- ...TION_NEW_WORK_COMPACT_LAYOUT_2026-04-24.md | 26 +++ ...TEGORY_AND_CREATE_TAB_DESIGN_2026-04-24.md | 64 +++++ docs/design/README.md | 2 + ...HAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md | 5 +- ...RD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md | 36 ++- ...SULT_EDITING_MIGRATION_AUDIT_2026-04-24.md | 2 + .../crates/api-server/src/password_entry.rs | 6 +- server-rs/crates/module-auth/src/lib.rs | 68 ++++-- src/components/auth/AuthGate.tsx | 6 +- src/components/auth/LoginScreen.tsx | 154 +++++------- .../CustomWorldCreationStartCard.tsx | 28 ++- .../RpgCreationResultViewImpl.tsx | 17 -- ...gEntryFlowShell.agent.interaction.test.tsx | 4 +- src/components/rpg-entry/RpgEntryHomeView.tsx | 219 +++++++++++++----- src/index.css | 46 +++- src/services/authService.ts | 29 ++- 16 files changed, 495 insertions(+), 217 deletions(-) create mode 100644 docs/design/MOBILE_CREATION_NEW_WORK_COMPACT_LAYOUT_2026-04-24.md create mode 100644 docs/design/PLATFORM_CATEGORY_AND_CREATE_TAB_DESIGN_2026-04-24.md diff --git a/docs/design/MOBILE_CREATION_NEW_WORK_COMPACT_LAYOUT_2026-04-24.md b/docs/design/MOBILE_CREATION_NEW_WORK_COMPACT_LAYOUT_2026-04-24.md new file mode 100644 index 00000000..735e248d --- /dev/null +++ b/docs/design/MOBILE_CREATION_NEW_WORK_COMPACT_LAYOUT_2026-04-24.md @@ -0,0 +1,26 @@ +# 移动端创作页新建作品紧凑布局设计 + +## 目标 + +移动端创作页顶部的新建作品模块只承担快速进入创作模板的作用,不承担规则解释和长说明承载。模块在首屏中最多占用约 1/3 高度,把更多空间留给作品列表和筛选操作。 + +## 落地范围 + +- 入口组件:`src/components/custom-world-home/CustomWorldCreationStartCard.tsx` +- 外层页面:`src/components/custom-world-home/CustomWorldCreationHub.tsx` +- 模板元数据继续复用 `PLATFORM_CREATION_TYPES`,不新增前端业务逻辑。 + +## 移动端布局规则 + +1. 顶部标题行压缩成单行:左侧标题,右侧仅保留简短状态,不再显示说明段落。 +2. 模板入口在手机端使用横向滚动胶囊卡片,每个卡片保持单行动作感,不堆叠成长列表。 +3. 卡片高度控制在约 4rem 内,标题与状态信息并排组织,避免大留白。 +4. 模块本体使用 `max-height: 33svh` 作为硬约束,内容超出时优先在模板入口行内横向滚动,不撑高页面。 +5. 桌面端保持网格入口,但同步收紧内边距和卡片留白,避免移动端与桌面端表现割裂。 + +## 文案约束 + +- UI 不新增规则说明类文案。 +- 原有“直接选择游戏创作模板,立刻进入对应的共创工作台。”说明在移动端隐藏,桌面端保留为辅助说明。 +- 锁定、可创建、正在开启等状态继续来自既有模板元数据或忙碌状态。 + diff --git a/docs/design/PLATFORM_CATEGORY_AND_CREATE_TAB_DESIGN_2026-04-24.md b/docs/design/PLATFORM_CATEGORY_AND_CREATE_TAB_DESIGN_2026-04-24.md new file mode 100644 index 00000000..0479c045 --- /dev/null +++ b/docs/design/PLATFORM_CATEGORY_AND_CREATE_TAB_DESIGN_2026-04-24.md @@ -0,0 +1,64 @@ +# 平台入口分类与创作 Tab 强化设计 + +## 1. 目标 + +在不新建平台入口系统的前提下,直接扩展现有 `RpgEntryHomeView` 主 Tab: + +- 新增“分类” Tab,用作品标签聚合所有公开发布作品。 +- 强化“创作” Tab 的导航视觉权重,让它在底部导航中居中并更醒目。 +- 登录态底部导航顺序为:首页、分类、创作、存档、我的。 +- 未登录态底部导航只保留:首页、创作、分类,其中创作保持居中。 + +## 2. 数据边界 + +本次只做前端展示重排,不新增后端接口: + +- 分类数据来源使用现有 `latestEntries` 与 `featuredEntries` 的公开作品列表。 +- 标签来源沿用 `buildPlatformWorldTags(entry)`,公开作品会映射为题材、角色数、地标数。 +- 同一公开作品若同时出现在精选与最新中,按 `ownerUserId + profileId` 去重。 +- 点击分类作品继续走现有 `onOpenGalleryDetail`,不改变详情页和登录拦截逻辑。 + +## 3. 交互规则 + +### 3.1 登录态 + +底部导航展示 5 个入口: + +1. 首页 +2. 分类 +3. 创作 +4. 存档 +5. 我的 + +创作入口位于第三位,视觉上使用更大的图标壳、轻微上浮、渐变高亮和阴影,保证它是主行动入口。 + +### 3.2 未登录态 + +底部导航展示 3 个入口: + +1. 首页 +2. 创作 +3. 分类 + +不展示“存档”和“我的”,避免未登录用户在底部导航看到必须登录后才有价值的入口。创作入口位于第二位,保持几何居中。 + +### 3.3 桌面端 + +桌面侧栏同步增加“分类”,但保持纵向导航,不强行做居中布局。创作入口仍使用强调样式。 + +## 4. 分类页布局 + +分类页为独立 Tab 面板,不在首页下方展开: + +- 顶部展示标签胶囊,默认选中作品数量最多的标签。 +- 标签切换后,下方网格展示该标签下所有公开作品。 +- 无公开作品时展示现有空状态组件。 +- 分类页不写玩法规则说明类长文案,只保留必要标题、短状态文案和作品卡片。 + +## 5. 验收点 + +- 登录态移动端底部导航顺序准确,创作在 5 个 Tab 中居中。 +- 未登录态移动端底部导航只显示 3 个 Tab,创作在中间。 +- 分类 Tab 能按标签切换并展示公开作品。 +- 创作 Tab 在移动端和桌面端都比普通 Tab 更醒目。 +- 不修改 server-node,不新增后端逻辑。 diff --git a/docs/design/README.md b/docs/design/README.md index 38ebb042..aaac0fbf 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -9,6 +9,8 @@ - [CUSTOM_WORLD_CREATOR_PURE_AGENT_COMPARISON_AND_CONVERSION_DESIGN_2026-04-12.md](./CUSTOM_WORLD_CREATOR_PURE_AGENT_COMPARISON_AND_CONVERSION_DESIGN_2026-04-12.md):纯 Agent 式创作工具与结构化工作台方案的优缺点对比,以及转型设计。 - [CUSTOM_WORLD_TEMPLATE_DECOUPLING_AND_CROSS_GENRE_GENERALIZATION_DESIGN_2026-04-08.md](./CUSTOM_WORLD_TEMPLATE_DECOUPLING_AND_CROSS_GENRE_GENERALIZATION_DESIGN_2026-04-08.md):把自定义世界从武侠/仙侠模板依赖迁到跨题材通用设定层的优化设计。 - [CUSTOM_WORLD_SELF_OWNED_SETTING_LAYER_OPTIMIZATION_2026-04-08.md](./CUSTOM_WORLD_SELF_OWNED_SETTING_LAYER_OPTIMIZATION_2026-04-08.md):把模板依赖逐步迁成自定义世界自有设定层,并保证不破坏当前生成流程的优化方案。 +- [MOBILE_CREATION_NEW_WORK_COMPACT_LAYOUT_2026-04-24.md](./MOBILE_CREATION_NEW_WORK_COMPACT_LAYOUT_2026-04-24.md):移动端创作页新建作品模块最多占用首屏约 1/3 高度的紧凑布局设计。 +- [PLATFORM_CATEGORY_AND_CREATE_TAB_DESIGN_2026-04-24.md](./PLATFORM_CATEGORY_AND_CREATE_TAB_DESIGN_2026-04-24.md):平台入口新增分类 Tab、登录态导航裁剪与创作 Tab 视觉强化设计。 - [AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md](./AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md):运行时物品生成系统重设计。 - [LEVEL_PROGRESS_AND_CHAPTER_NPC_AUTO_SCALING_DESIGN_2026-04-20.md](./LEVEL_PROGRESS_AND_CHAPTER_NPC_AUTO_SCALING_DESIGN_2026-04-20.md):等级成长、章节经验节奏与 NPC 自动定级设计。 - [RPG_NARRATIVE_PLANNING_FULL_PIPELINE_WORKFLOW_2026-04-12.md](./RPG_NARRATIVE_PLANNING_FULL_PIPELINE_WORKFLOW_2026-04-12.md):专业剧情策划构建 RPG 游戏全剧情的工作流程与交付模板。 diff --git a/docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md b/docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md index 131bac39..698e5902 100644 --- a/docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md +++ b/docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md @@ -1217,8 +1217,9 @@ Phase 4 本轮已完成以下主链接线: - published works 明确输出 `canEnterWorld=true` 4. 前端 Agent 结果页已开始消费服务端 Phase4 状态: - 结果页在 Agent 草稿未发布时把主 CTA 改成“发布并进入世界” - - 结果页会展示服务端 preview source、publish blockers、warning 数量 - - 有 blocker 时会禁用“发布并进入世界”按钮,不再让前端继续假装可以直接进入世界 + - 结果页会消费服务端 gate 语义,但不再把 preview source 做成底部常驻提示 + - publish blockers 改为点击“发布并进入世界”时,通过独立面板提示 + - warning 数量仍可作为非阻断摘要展示 5. `useRpgCreationEnterWorld.ts` 与 `RpgEntryFlowShellImpl.tsx` 已把 Agent 结果页进入世界主链改成: - 先 `sync_result_profile` - 再执行后端 `publish_world` diff --git a/docs/technical/PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md b/docs/technical/PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md index c4da3d47..bcca40d9 100644 --- a/docs/technical/PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md +++ b/docs/technical/PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md @@ -4,12 +4,13 @@ ## 1. 目标边界 -本次迭代开放账号密码登录、登录后修改密码、手机号验证码重置密码,但不开放密码注册。 +本次迭代开放手机号密码登录、登录后修改密码、手机号验证码重置密码,但不开放独立注册页面。 1. 新用户只能通过手机号验证码完成注册与首次登录。 2. 已有用户可以在登录后设置或修改密码。 3. 忘记密码时,只能通过已绑定手机号验证码重置密码。 -4. 密码登录只校验已存在且已设置密码的账号,不自动创建新账号。 +4. 密码登录只校验已存在且已设置密码的手机号账号,不自动创建新账号。 +5. 登录面板本地缓存最近一次成功登录的手机号,只用于回填手机号输入框,不缓存密码或验证码。 ## 2. 接口设计 @@ -17,10 +18,11 @@ 沿用现有 `POST /api/auth/entry`: -1. 请求字段:`username`、`password`。 -2. 用户不存在时返回 `401`,不创建账号。 -3. 用户存在但未设置密码时返回 `401`。 -4. 校验成功后签发 access token,并写入 refresh cookie。 +1. 请求字段沿用 `username`、`password`,但前端固定把手机号填入 `username`。 +2. 后端优先按标准手机号归一化后查找账号,兼容历史用户名只作为开发游客兜底能力。 +3. 手机号不存在时返回 `401`,不创建账号。 +4. 手机号存在但未设置密码时返回 `401`。 +5. 校验成功后签发 access token,并写入 refresh cookie。 ### 2.2 修改密码 @@ -46,14 +48,24 @@ 复用 `POST /api/auth/phone/send-code`,`scene` 增加 `reset_password`。 +### 2.5 验证码注册/登录 + +复用现有 `POST /api/auth/phone/login`: + +1. 请求字段:`phone`、`code`。 +2. 验证码校验成功后,若手机号已绑定账号,则直接完成登录。 +3. 验证码校验成功后,若手机号没有账号信息,则后端自动创建手机号账号,再完成登录。 +4. 自动创建账号默认不设置用户可用密码,用户后续可在账号设置或忘记密码流程设置密码。 + ## 3. 前端交互 -登录弹窗拆成两个页签: +登录弹窗不再拆独立注册页签: -1. `登录`:提供密码登录、手机号验证码登录、忘记密码入口。 -2. `注册`:只提供手机号验证码注册/登录,不提供账号密码注册。 -3. `忘记密码`:从登录页进入独立重置面板,不在当前表单下方展开。 -4. 账号设置面板提供密码修改入口;未设置密码的账号显示为设置密码。 +1. 面板直接展示手机号和密码输入,用于已设置密码账号登录。 +2. 登录按钮文本固定为 `注册/登录`,避免用户在登录和首次进入之间做页面切换。 +3. 忘记密码入口显示在登录按钮右下侧,点击后仍进入独立重置面板,不在当前表单下方展开。 +4. 同一面板保留手机号验证码注册/登录能力,用于新用户自动注册和已注册用户免密码登录。 +5. 账号设置面板提供密码修改入口;未设置密码的账号显示为设置密码。 ## 4. 数据约束 @@ -63,4 +75,4 @@ 2. 微信待绑定账号默认没有用户可用密码。 3. 只有用户显式修改或重置密码后,才允许密码登录。 -后续迁移到 SpacetimeDB 表时,保持同一语义:密码哈希字段允许为空,密码登录 reducer 不承担注册能力。 +后续迁移到 SpacetimeDB 表时,保持同一语义:密码哈希字段允许为空,密码登录 reducer 不承担注册能力,验证码登录 reducer 承担“无账号则自动注册”的唯一注册入口。 diff --git a/docs/technical/RPG_CREATION_RESULT_EDITING_MIGRATION_AUDIT_2026-04-24.md b/docs/technical/RPG_CREATION_RESULT_EDITING_MIGRATION_AUDIT_2026-04-24.md index e2a03002..3c2848e9 100644 --- a/docs/technical/RPG_CREATION_RESULT_EDITING_MIGRATION_AUDIT_2026-04-24.md +++ b/docs/technical/RPG_CREATION_RESULT_EDITING_MIGRATION_AUDIT_2026-04-24.md @@ -14,6 +14,8 @@ 已改为: +补充修复:`RpgCreationResultViewImpl` 已补齐 `previewSourceLabel` props 解构,避免 Agent 结果页在渲染数据源提示时触发 `ReferenceError`。 + ```text Agent 结果页点击新增场景角色 / 新增场景 -> RpgCreationResultView.onGenerateEntity diff --git a/server-rs/crates/api-server/src/password_entry.rs b/server-rs/crates/api-server/src/password_entry.rs index 6702147d..3f307b12 100644 --- a/server-rs/crates/api-server/src/password_entry.rs +++ b/server-rs/crates/api-server/src/password_entry.rs @@ -65,7 +65,7 @@ pub async fn password_entry( fn map_password_entry_error(error: PasswordEntryError) -> AppError { match error { PasswordEntryError::InvalidUsername => AppError::from_status(StatusCode::BAD_REQUEST) - .with_message("用户名只允许 3 到 24 位字母、数字、下划线") + .with_message("手机号格式不正确") .with_details(json!({ "field": "username", })), @@ -80,10 +80,10 @@ fn map_password_entry_error(error: PasswordEntryError) -> AppError { "field": "username", })), PasswordEntryError::InvalidCredentials => { - AppError::from_status(StatusCode::UNAUTHORIZED).with_message("用户名或密码错误") + AppError::from_status(StatusCode::UNAUTHORIZED).with_message("手机号或密码错误") } PasswordEntryError::UserNotFound => { - AppError::from_status(StatusCode::UNAUTHORIZED).with_message("用户名或密码错误") + AppError::from_status(StatusCode::UNAUTHORIZED).with_message("手机号或密码错误") } PasswordEntryError::Store(_) | PasswordEntryError::PasswordHash(_) => { AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error.to_string()) diff --git a/server-rs/crates/module-auth/src/lib.rs b/server-rs/crates/module-auth/src/lib.rs index 093ecd45..d2f7df41 100644 --- a/server-rs/crates/module-auth/src/lib.rs +++ b/server-rs/crates/module-auth/src/lib.rs @@ -475,24 +475,25 @@ impl PasswordEntryService { &self, input: PasswordEntryInput, ) -> Result { - let username = normalize_username(&input.username)?; validate_password(&input.password)?; - if let Some(existing_user) = self.store.find_by_username(&username)? { - if !existing_user.password_login_enabled { + // 登录面板现在固定使用手机号作为密码登录标识;先走手机号索引, + // 再保留历史用户名路径给开发游客和旧测试数据使用。 + if let Ok(normalized_phone) = normalize_mainland_china_phone_number(&input.username) { + let Some(existing_user) = self + .store + .find_by_phone_number_for_password(&normalized_phone.e164)? + else { return Err(PasswordEntryError::InvalidCredentials); - } - let is_valid = verify_password(&existing_user.password_hash, &input.password) - .await - .map_err(|error| PasswordEntryError::PasswordHash(error.to_string()))?; - if !is_valid { - return Err(PasswordEntryError::InvalidCredentials); - } + }; - return Ok(PasswordEntryResult { - user: existing_user.user, - created: false, - }); + return verify_stored_password_user(existing_user, &input.password).await; + } + + let username = normalize_username(&input.username)?; + + if let Some(existing_user) = self.store.find_by_username(&username)? { + return verify_stored_password_user(existing_user, &input.password).await; } Err(PasswordEntryError::InvalidCredentials) @@ -1292,6 +1293,24 @@ impl InMemoryAuthStore { .cloned()) } + fn find_by_phone_number_for_password( + &self, + phone_number: &str, + ) -> Result, PasswordEntryError> { + let state = self + .inner + .lock() + .map_err(|_| PasswordEntryError::Store("用户仓储锁已中毒".to_string()))?; + let Some(user_id) = state.phone_to_user_id.get(phone_number) else { + return Ok(None); + }; + Ok(state + .users_by_username + .values() + .find(|stored_user| stored_user.user.id == *user_id) + .cloned()) + } + fn create_phone_user( &self, phone_number: PhoneNumberSnapshot, @@ -2220,6 +2239,27 @@ fn validate_password(password: &str) -> Result<(), PasswordEntryError> { Ok(()) } +async fn verify_stored_password_user( + existing_user: StoredPasswordUser, + password: &str, +) -> Result { + if !existing_user.password_login_enabled { + return Err(PasswordEntryError::InvalidCredentials); + } + + let is_valid = verify_password(&existing_user.password_hash, password) + .await + .map_err(|error| PasswordEntryError::PasswordHash(error.to_string()))?; + if !is_valid { + return Err(PasswordEntryError::InvalidCredentials); + } + + Ok(PasswordEntryResult { + user: existing_user.user, + created: false, + }) +} + fn verify_sms_code_format(verify_code: &str) -> Result<(), PhoneAuthError> { let verify_code = verify_code.trim(); if verify_code.len() != SMS_CODE_LENGTH diff --git a/src/components/auth/AuthGate.tsx b/src/components/auth/AuthGate.tsx index 5e9559c1..195e4c03 100644 --- a/src/components/auth/AuthGate.tsx +++ b/src/components/auth/AuthGate.tsx @@ -15,11 +15,11 @@ import { import { type AuthAuditLogEntry, type AuthCaptchaChallenge, + authEntry, type AuthLoginMethod, type AuthRiskBlockSummary, type AuthSessionSummary, type AuthUser, - authEntry, bindWechatPhone, changePassword, changePhoneNumber, @@ -38,6 +38,7 @@ import { resetPassword, revokeAuthSession, sendPhoneLoginCode, + setStoredLastLoginPhone, startWechatLogin, } from '../../services/authService'; import { AccountModal } from './AccountModal'; @@ -694,6 +695,7 @@ export function AuthGate({ children }: AuthGateProps) { setError(''); try { const nextUser = await loginWithPhoneCode(phone, code); + setStoredLastLoginPhone(phone); setLoginCaptchaChallenge(null); activateReadyUser(nextUser); } catch (loginError) { @@ -711,6 +713,7 @@ export function AuthGate({ children }: AuthGateProps) { setError(''); try { const nextUser = await authEntry(username, password); + setStoredLastLoginPhone(username); activateReadyUser(nextUser); } catch (loginError) { setError( @@ -727,6 +730,7 @@ export function AuthGate({ children }: AuthGateProps) { setError(''); try { const nextUser = await resetPassword(phone, code, newPassword); + setStoredLastLoginPhone(phone); activateReadyUser(nextUser); } catch (resetError) { setError( diff --git a/src/components/auth/LoginScreen.tsx b/src/components/auth/LoginScreen.tsx index dea916f1..ea02a7e3 100644 --- a/src/components/auth/LoginScreen.tsx +++ b/src/components/auth/LoginScreen.tsx @@ -6,6 +6,7 @@ import type { AuthCaptchaChallenge, AuthLoginMethod, } from '../../services/authService'; +import { getStoredLastLoginPhone } from '../../services/authService'; import { CaptchaChallengeField } from './CaptchaChallengeField'; type SmsScene = 'login' | 'reset_password'; @@ -57,11 +58,9 @@ export function LoginScreen({ onResetPassword, onStartWechatLogin, }: LoginScreenProps) { - const [activeTab, setActiveTab] = useState<'login' | 'register'>('login'); const [isResetPanelOpen, setIsResetPanelOpen] = useState(false); - const [username, setUsername] = useState(''); + const [phone, setPhone] = useState(() => getStoredLastLoginPhone()); const [password, setPassword] = useState(''); - const [phone, setPhone] = useState(''); const [code, setCode] = useState(''); const [resetPhone, setResetPhone] = useState(''); const [resetCode, setResetCode] = useState(''); @@ -154,75 +153,55 @@ export function LoginScreen({ /> ) : (
-
- setActiveTab('login')} - /> - setActiveTab('register')} - /> -
- - {activeTab === 'login' ? ( + {passwordLoginEnabled ? (
{ event.preventDefault(); - if (!passwordLoginEnabled) { - return; - } - void onPasswordSubmit(username, password); + void onPasswordSubmit(phone, password); }} > - {passwordLoginEnabled ? ( - <> - - - - ) : null} + + {error ? : null} - {passwordLoginEnabled ? ( +
- ) : null} - - + +
{wechatLoginEnabled ? ( ) : null} - ) : ( + ) : null} + + {phoneLoginEnabled ? ( onPhoneSubmit(phone, code)} /> - )} + ) : null} {!passwordLoginEnabled && !phoneLoginEnabled && !wechatLoginEnabled ? (
@@ -276,30 +258,6 @@ export function LoginScreen({ ); } -function TabButton({ - active, - label, - onClick, -}: { - active: boolean; - label: string; - onClick: () => void; -}) { - return ( - - ); -} - function PhoneCodeForm({ phone, code, @@ -312,6 +270,7 @@ function PhoneCodeForm({ hint, submitLabel, enabled, + showPhoneField, onPhoneChange, onCodeChange, onCaptchaAnswerChange, @@ -329,6 +288,7 @@ function PhoneCodeForm({ hint: string; submitLabel: string; enabled: boolean; + showPhoneField: boolean; onPhoneChange: (value: string) => void; onCodeChange: (value: string) => void; onCaptchaAnswerChange: (value: string) => void; @@ -347,17 +307,19 @@ function PhoneCodeForm({ void onSubmit(); }} > - + {showPhoneField ? ( + + ) : null}