Refine account modal entry flow and local web binding
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -83,6 +83,7 @@
|
||||
8. 子面板返回按钮固定摆在面板右上角
|
||||
9. 设置首页与各级子面板都必须定义单一滚动容器,列表内容必须可稳定滚动,禁止外层与内层同时争夺滚动
|
||||
10. 二级或三级面板打开后,下层内容必须进入不可交互状态,并把焦点主动转移到当前面板内;禁止对仍保留焦点的祖先节点使用 `aria-hidden`
|
||||
11. 右上角头像、账号入口等身份入口直达“账号信息”时,只允许展示“账号信息”面板本身,不再同步弹出或保留“设置与账号安全”首页;只有“设置”入口才打开设置首页
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ npm run deploy:rust:remote
|
||||
5. 执行 `cargo build -p spacetime-module --release --target wasm32-unknown-unknown --manifest-path server-rs/Cargo.toml`,并把 `spacetime_module.wasm` 复制到目标目录。
|
||||
6. 把仓库根目录的 `.env` 与 `.env.local` 分别复制到目标目录根部和目标目录的 `web/` 下;复制后统一移除 UTF-8 BOM 与 CRLF,并把 `GENARRATIVE_SPACETIME_DATABASE` 覆盖为本次 `--database` 参数,避免 Jenkins 工作区里残留的旧 `.env.local` 覆盖发布包目标库。
|
||||
7. 在目标目录写入 `web-server.mjs`,用于托管 `web/` 并把 `/api/*`、`/generated-*`、`/healthz` 反代到本包内的 `api-server`。
|
||||
8. 在目标目录写入 `start.sh` 与 `stop.sh`;`start.sh` 会先按 `KEY=value` 子集加载发布目录根部的 `.env`、`.env.local`,兼容 UTF-8 BOM 与 CRLF,再回退到构建时通过 `--database`、`--api-port`、`--web-port`、`--spacetime-host`、`--spacetime-port` 写入的默认值,并默认导出 `NO_COLOR=1` 与 `CARGO_TERM_COLOR=never`,避免 ANSI 控制码写入日志文件;同时按 Ubuntu 发布环境使用发布目录内 `.spacetimedb/` 作为 root-dir,不再额外设置 `--data-dir`,启动前先执行 `sync_ubuntu_spacetime_install`,优先从 `/usr/.local/share/spacetime/bin/<version>/spacetimedb-cli` 或 `$HOME/.local/share/spacetime/bin/<version>/spacetimedb-cli` 同步到 `.spacetimedb/bin/current/spacetimedb-cli`,当前线上 `spacetime` 入口为 `/usr/local/bin/spacetime`;启动参数为 `spacetime --root-dir ./.spacetimedb start --edition standalone --listen-addr <host>:<port>`,探活必须确认 `server ping` 输出包含 `Server is online:`;普通启动先无清库发布,若 publish 输出可判定为 schema 冲突,则自动导出旧库、清库发布新 wasm、导入回灌;如果以 `--clear-database` 启动,则内部 `spacetime publish` 会追加 `-c=on-conflict`,代表人工确认清库,不触发自动回灌。
|
||||
8. 在目标目录写入 `start.sh` 与 `stop.sh`;`start.sh` 会先按 `KEY=value` 子集加载发布目录根部的 `.env`、`.env.local`,兼容 UTF-8 BOM 与 CRLF,再回退到构建时通过 `--database`、`--api-port`、`--web-host`、`--web-port`、`--spacetime-host`、`--spacetime-port` 写入的默认值,其中 Web 默认只监听 `127.0.0.1`;并默认导出 `NO_COLOR=1` 与 `CARGO_TERM_COLOR=never`,避免 ANSI 控制码写入日志文件;同时按 Ubuntu 发布环境使用发布目录内 `.spacetimedb/` 作为 root-dir,不再额外设置 `--data-dir`,启动前先执行 `sync_ubuntu_spacetime_install`,优先从 `/usr/.local/share/spacetime/bin/<version>/spacetimedb-cli` 或 `$HOME/.local/share/spacetime/bin/<version>/spacetimedb-cli` 同步到 `.spacetimedb/bin/current/spacetimedb-cli`,当前线上 `spacetime` 入口为 `/usr/local/bin/spacetime`;启动参数为 `spacetime --root-dir ./.spacetimedb start --edition standalone --listen-addr <host>:<port>`,探活必须确认 `server ping` 输出包含 `Server is online:`;普通启动先无清库发布,若 publish 输出可判定为 schema 冲突,则自动导出旧库、清库发布新 wasm、导入回灌;如果以 `--clear-database` 启动,则内部 `spacetime publish` 会追加 `-c=on-conflict`,代表人工确认清库,不触发自动回灌。
|
||||
9. 默认执行 `scp -r -i ~\.ssh\dsk.pem build/<timestamp> ubuntu@82.157.175.59:/home/ubuntu/genarrative/` 上传发布包。
|
||||
|
||||
SpacetimeDB database 名称必须匹配 `^[a-z0-9]+(-[a-z0-9]+)*$`:只能使用小写字母、数字,并用单个短横线分隔;大写字母、点号、下划线、首尾短横线和连续短横线都会触发 `spacetime publish` 的 `invalid characters in database name`。发布包构建脚本和 `start.sh` 都会提前拦截这类非法名称。
|
||||
@@ -174,6 +174,7 @@ build/<timestamp>/
|
||||
```bash
|
||||
npm run build:rust:ubuntu -- --name 20260422-153000
|
||||
npm run build:rust:ubuntu -- --database genarrative-dev --web-port 3000 --api-port 8082 --spacetime-port 3101
|
||||
npm run build:rust:ubuntu -- --database genarrative-dev --web-host 127.0.0.1 --web-port 3000
|
||||
npm run build:rust:ubuntu -- --skip-upload
|
||||
```
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ BUILD_NAME="$(date +%Y%m%d-%H%M%S)"
|
||||
DATABASE="xushi-p4wfr"
|
||||
API_HOST="127.0.0.1"
|
||||
API_PORT="8082"
|
||||
WEB_HOST="0.0.0.0"
|
||||
WEB_HOST="127.0.0.1"
|
||||
WEB_PORT="25001"
|
||||
SPACETIME_HOST="127.0.0.1"
|
||||
SPACETIME_PORT="3101"
|
||||
@@ -421,7 +421,7 @@ import {fileURLToPath} from 'node:url';
|
||||
|
||||
const releaseDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const webRoot = path.join(releaseDir, 'web');
|
||||
const webHost = process.env.GENARRATIVE_WEB_HOST || '0.0.0.0';
|
||||
const webHost = process.env.GENARRATIVE_WEB_HOST || '127.0.0.1';
|
||||
const webPort = Number(process.env.GENARRATIVE_WEB_PORT || '3000');
|
||||
const apiTarget = new URL(process.env.GENARRATIVE_API_TARGET || 'http://127.0.0.1:8082');
|
||||
const indexPath = path.join(webRoot, 'index.html');
|
||||
@@ -1215,7 +1215,7 @@ cat >"${TARGET_DIR}/README.md" <<'EOF'
|
||||
|
||||
- 启动时会先加载发布目录根部的 \`.env\` 与 \`.env.local\`,再回退到脚本内默认值。
|
||||
- 环境文件复制进发布包时会移除 UTF-8 BOM 与 CRLF;启动时也会按 \`KEY=value\` 子集解析,跳过不合法行。
|
||||
- 脚本内默认值来自构建时的 `--database`、`--api-port`、`--web-port`、`--spacetime-host`、`--spacetime-port` 参数。
|
||||
- 脚本内默认值来自构建时的 `--database`、`--api-port`、`--web-host`、`--web-port`、`--spacetime-host`、`--spacetime-port` 参数;Web 默认只监听 `127.0.0.1`。
|
||||
- 默认导出 \`NO_COLOR=1\` 与 \`CARGO_TERM_COLOR=never\`,避免 ANSI 颜色控制码写入日志文件;如确有需要可在启动前显式覆盖。
|
||||
- \`GENARRATIVE_WEB_HOST\` / \`GENARRATIVE_WEB_PORT\`
|
||||
- \`GENARRATIVE_API_HOST\` / \`GENARRATIVE_API_PORT\` / \`GENARRATIVE_API_LOG\`
|
||||
|
||||
@@ -26,6 +26,7 @@ const baseUser: AuthUser = {
|
||||
|
||||
function renderAccountModal(overrides?: {
|
||||
user?: AuthUser;
|
||||
entryMode?: 'settings' | 'account';
|
||||
riskBlocks?: AuthRiskBlockSummary[];
|
||||
sessions?: AuthSessionSummary[];
|
||||
auditLogs?: AuthAuditLogEntry[];
|
||||
@@ -41,6 +42,7 @@ function renderAccountModal(overrides?: {
|
||||
<AccountModal
|
||||
user={overrides?.user ?? baseUser}
|
||||
isOpen
|
||||
entryMode={overrides?.entryMode ?? 'settings'}
|
||||
initialSection={overrides?.initialSection ?? null}
|
||||
platformTheme="light"
|
||||
riskBlocks={overrides?.riskBlocks ?? []}
|
||||
@@ -91,6 +93,21 @@ test('settings header uses a generic title instead of the phone number', () => {
|
||||
expect(screen.queryByRole('button', { name: '退出全部设备' })).toBeNull();
|
||||
});
|
||||
|
||||
test('direct account entry does not render the settings shell as another dialog', () => {
|
||||
renderAccountModal({ entryMode: 'account' });
|
||||
|
||||
const accountDialog = screen.getByRole('dialog', { name: '账号信息' });
|
||||
expect(accountDialog).toBeTruthy();
|
||||
expect(screen.queryByRole('dialog', { name: '设置与账号安全' })).toBeNull();
|
||||
expect(screen.queryByText('设置与账号安全')).toBeNull();
|
||||
expect(
|
||||
within(accountDialog).getByRole('button', { name: '关闭' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(accountDialog).queryByRole('button', { name: '返回' }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test('account actions open in independent panels instead of inline expansion', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -131,9 +148,9 @@ test('nested settings panels keep back navigation without an extra close action'
|
||||
expect(
|
||||
within(accountDialog).getByRole('button', { name: '返回' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
accountHeader?.lastElementChild?.textContent?.includes('返回'),
|
||||
).toBe(true);
|
||||
expect(accountHeader?.lastElementChild?.textContent?.includes('返回')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
within(accountDialog).queryByRole('button', { name: '关闭' }),
|
||||
).toBeNull();
|
||||
|
||||
@@ -20,6 +20,7 @@ import { CaptchaChallengeField } from './CaptchaChallengeField';
|
||||
type AccountModalProps = {
|
||||
user: AuthUser;
|
||||
isOpen: boolean;
|
||||
entryMode?: 'settings' | 'account';
|
||||
initialSection?: PlatformSettingsSection | null;
|
||||
platformTheme: PlatformTheme;
|
||||
riskBlocks: AuthRiskBlockSummary[];
|
||||
@@ -159,6 +160,7 @@ function OverlayPanel({
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
standalone = false,
|
||||
onBack,
|
||||
onClose,
|
||||
children,
|
||||
@@ -167,64 +169,73 @@ function OverlayPanel({
|
||||
title: string;
|
||||
description?: string;
|
||||
action?: ReactNode;
|
||||
standalone?: boolean;
|
||||
onBack?: () => void;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const panel = (
|
||||
<div
|
||||
className="platform-auth-card flex max-h-full w-full min-h-0 flex-col overflow-hidden rounded-[28px] p-5 sm:max-w-3xl sm:p-6"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={title}
|
||||
style={{ maxHeight: ACCOUNT_MODAL_MAX_HEIGHT }}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs uppercase tracking-[0.28em] text-[var(--platform-cool-text)]">
|
||||
{eyebrow}
|
||||
</div>
|
||||
<div className="mt-2 text-2xl font-semibold text-[var(--platform-text-strong)]">
|
||||
{title}
|
||||
</div>
|
||||
{description ? (
|
||||
<div className="mt-2 text-sm text-[var(--platform-text-base)]">
|
||||
{description}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{action}
|
||||
{onBack ? (
|
||||
<button
|
||||
type="button"
|
||||
autoFocus
|
||||
className="platform-button platform-button--ghost min-h-0 rounded-full px-3 py-1.5 text-xs"
|
||||
onClick={onBack}
|
||||
>
|
||||
返回
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="platform-button platform-button--ghost min-h-0 rounded-full px-3 py-1.5 text-xs"
|
||||
onClick={onClose}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 min-h-0 flex-1 overflow-y-auto overscroll-y-contain pr-1">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (standalone) {
|
||||
return panel;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute inset-0 z-10 flex items-end bg-black/20 backdrop-blur-[2px] sm:items-center sm:justify-center sm:p-4"
|
||||
onClick={onBack ?? onClose}
|
||||
>
|
||||
<div
|
||||
className="platform-auth-card flex max-h-full w-full min-h-0 flex-col overflow-hidden rounded-[28px] p-5 sm:max-w-3xl sm:p-6"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={title}
|
||||
style={{ maxHeight: ACCOUNT_MODAL_MAX_HEIGHT }}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs uppercase tracking-[0.28em] text-[var(--platform-cool-text)]">
|
||||
{eyebrow}
|
||||
</div>
|
||||
<div className="mt-2 text-2xl font-semibold text-[var(--platform-text-strong)]">
|
||||
{title}
|
||||
</div>
|
||||
{description ? (
|
||||
<div className="mt-2 text-sm text-[var(--platform-text-base)]">
|
||||
{description}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{action}
|
||||
{onBack ? (
|
||||
<button
|
||||
type="button"
|
||||
autoFocus
|
||||
className="platform-button platform-button--ghost min-h-0 rounded-full px-3 py-1.5 text-xs"
|
||||
onClick={onBack}
|
||||
>
|
||||
返回
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="platform-button platform-button--ghost min-h-0 rounded-full px-3 py-1.5 text-xs"
|
||||
onClick={onClose}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 min-h-0 flex-1 overflow-y-auto overscroll-y-contain pr-1">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
{panel}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -266,6 +277,7 @@ function ThemeOptionCard({
|
||||
export function AccountModal({
|
||||
user,
|
||||
isOpen,
|
||||
entryMode = 'settings',
|
||||
initialSection = null,
|
||||
platformTheme,
|
||||
riskBlocks,
|
||||
@@ -314,6 +326,7 @@ export function AccountModal({
|
||||
const sectionTriggerRef = useRef<HTMLButtonElement | null>(null);
|
||||
const changePhoneTriggerRef = useRef<HTMLButtonElement | null>(null);
|
||||
const passwordTriggerRef = useRef<HTMLButtonElement | null>(null);
|
||||
const isDirectAccountMode = entryMode === 'account';
|
||||
|
||||
const focusAfterNextPaint = useCallback((element: HTMLElement | null) => {
|
||||
if (!element) {
|
||||
@@ -347,7 +360,11 @@ export function AccountModal({
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveSection(normalizeSettingsSection(initialSection));
|
||||
setActiveSection(
|
||||
isDirectAccountMode
|
||||
? 'account'
|
||||
: normalizeSettingsSection(initialSection),
|
||||
);
|
||||
setIsChangePhonePanelOpen(false);
|
||||
setIsPasswordPanelOpen(false);
|
||||
setAccountNotice('');
|
||||
@@ -356,7 +373,13 @@ export function AccountModal({
|
||||
passwordTriggerRef.current = null;
|
||||
resetChangePhoneDraft();
|
||||
resetPasswordDraft();
|
||||
}, [initialSection, isOpen, resetChangePhoneDraft, resetPasswordDraft]);
|
||||
}, [
|
||||
initialSection,
|
||||
isDirectAccountMode,
|
||||
isOpen,
|
||||
resetChangePhoneDraft,
|
||||
resetPasswordDraft,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const settingsHome = settingsHomeRef.current;
|
||||
@@ -446,47 +469,55 @@ export function AccountModal({
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="platform-auth-card relative flex h-[min(100%,calc(100vh-2rem))] w-full max-w-5xl min-h-0 flex-col overflow-hidden rounded-[28px] p-5 sm:p-6"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="设置与账号安全"
|
||||
className={
|
||||
isDirectAccountMode
|
||||
? 'relative flex max-h-full w-full max-w-3xl min-h-0 flex-col overflow-hidden'
|
||||
: 'platform-auth-card relative flex h-[min(100%,calc(100vh-2rem))] w-full max-w-5xl min-h-0 flex-col overflow-hidden rounded-[28px] p-5 sm:p-6'
|
||||
}
|
||||
role={isDirectAccountMode ? undefined : 'dialog'}
|
||||
aria-modal={isDirectAccountMode ? undefined : true}
|
||||
aria-label={isDirectAccountMode ? undefined : '设置与账号安全'}
|
||||
style={{ maxHeight: ACCOUNT_MODAL_MAX_HEIGHT }}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-2xl font-semibold text-[var(--platform-text-strong)]">
|
||||
设置与账号安全
|
||||
{!isDirectAccountMode ? (
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-2xl font-semibold text-[var(--platform-text-strong)]">
|
||||
设置与账号安全
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="platform-button platform-button--ghost min-h-0 rounded-full px-3 py-1.5 text-xs"
|
||||
onClick={onClose}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="platform-button platform-button--ghost min-h-0 rounded-full px-3 py-1.5 text-xs"
|
||||
onClick={onClose}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-5 min-h-0 flex-1 overflow-y-auto overscroll-y-contain pr-1">
|
||||
<div ref={settingsHomeRef} className="flex min-h-0 flex-col gap-4">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{SETTINGS_SECTIONS.map((section) => (
|
||||
<SettingsEntryCard
|
||||
key={section.id}
|
||||
label={section.label}
|
||||
detail={section.detail}
|
||||
summary={sectionSummaries[section.id]}
|
||||
onClick={(trigger) => {
|
||||
sectionTriggerRef.current = trigger;
|
||||
setAccountNotice('');
|
||||
setActiveSection(section.id);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{!isDirectAccountMode ? (
|
||||
<div className="mt-5 min-h-0 flex-1 overflow-y-auto overscroll-y-contain pr-1">
|
||||
<div ref={settingsHomeRef} className="flex min-h-0 flex-col gap-4">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{SETTINGS_SECTIONS.map((section) => (
|
||||
<SettingsEntryCard
|
||||
key={section.id}
|
||||
label={section.label}
|
||||
detail={section.detail}
|
||||
summary={sectionSummaries[section.id]}
|
||||
onClick={(trigger) => {
|
||||
sectionTriggerRef.current = trigger;
|
||||
setAccountNotice('');
|
||||
setActiveSection(section.id);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{activeSection === 'appearance' ? (
|
||||
<OverlayPanel
|
||||
@@ -538,7 +569,8 @@ export function AccountModal({
|
||||
eyebrow="身份信息"
|
||||
title="账号信息"
|
||||
description="统一查看身份、安全状态、登录设备与最近操作。"
|
||||
onBack={closeSectionPanel}
|
||||
standalone={isDirectAccountMode}
|
||||
onBack={isDirectAccountMode ? undefined : closeSectionPanel}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="flex min-h-0 flex-col gap-4">
|
||||
@@ -671,7 +703,10 @@ export function AccountModal({
|
||||
<span>{block.title}</span>
|
||||
<span className="text-xs">
|
||||
剩余约{' '}
|
||||
{Math.max(1, Math.ceil(block.remainingSeconds / 60))}{' '}
|
||||
{Math.max(
|
||||
1,
|
||||
Math.ceil(block.remainingSeconds / 60),
|
||||
)}{' '}
|
||||
分钟
|
||||
</span>
|
||||
</div>
|
||||
@@ -965,7 +1000,9 @@ export function AccountModal({
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="首次设置可留空"
|
||||
onChange={(event) => setCurrentPassword(event.target.value)}
|
||||
onChange={(event) =>
|
||||
setCurrentPassword(event.target.value)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
|
||||
|
||||
@@ -84,6 +84,9 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
const [wechatLoading, setWechatLoading] = useState(false);
|
||||
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||
const [showSettingsModal, setShowSettingsModal] = useState(false);
|
||||
const [settingsEntryMode, setSettingsEntryMode] = useState<
|
||||
'settings' | 'account'
|
||||
>('settings');
|
||||
const [initialSettingsSection, setInitialSettingsSection] =
|
||||
useState<PlatformSettingsSection | null>(null);
|
||||
const [sessions, setSessions] = useState<AuthSessionSummary[]>([]);
|
||||
@@ -126,6 +129,7 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
setStatus('unauthenticated');
|
||||
setShowLoginModal(false);
|
||||
setShowSettingsModal(false);
|
||||
setSettingsEntryMode('settings');
|
||||
setInitialSettingsSection(null);
|
||||
setSessions([]);
|
||||
setAuditLogs([]);
|
||||
@@ -169,6 +173,12 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
setError('');
|
||||
}, []);
|
||||
|
||||
const closeSettingsModal = useCallback(() => {
|
||||
setShowSettingsModal(false);
|
||||
setSettingsEntryMode('settings');
|
||||
setInitialSettingsSection(null);
|
||||
}, []);
|
||||
|
||||
const openLoginModal = useCallback(
|
||||
(postLoginAction?: (() => void) | null) => {
|
||||
if (readyUser) {
|
||||
@@ -192,6 +202,7 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
const openSettingsModal = useCallback(
|
||||
(section?: PlatformSettingsSection) => {
|
||||
if (readyUser) {
|
||||
setSettingsEntryMode('settings');
|
||||
setInitialSettingsSection(section ?? null);
|
||||
setShowSettingsModal(true);
|
||||
return;
|
||||
@@ -203,8 +214,15 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
);
|
||||
|
||||
const openAccountModal = useCallback(() => {
|
||||
openSettingsModal('account');
|
||||
}, [openSettingsModal]);
|
||||
if (readyUser) {
|
||||
setSettingsEntryMode('account');
|
||||
setInitialSettingsSection('account');
|
||||
setShowSettingsModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
openLoginModal();
|
||||
}, [openLoginModal, readyUser]);
|
||||
|
||||
useEffect(() => {
|
||||
let isActive = true;
|
||||
@@ -224,7 +242,7 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
|
||||
const resolveGuestFallback = async () => {
|
||||
try {
|
||||
const options = await loadLoginOptions();
|
||||
await loadLoginOptions();
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
@@ -555,6 +573,7 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
<AccountModal
|
||||
user={readyUser}
|
||||
isOpen={showSettingsModal}
|
||||
entryMode={settingsEntryMode}
|
||||
initialSection={initialSettingsSection}
|
||||
platformTheme={settings.platformTheme}
|
||||
riskBlocks={riskBlocks}
|
||||
@@ -566,7 +585,7 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
isHydratingSettings={settings.isHydratingSettings}
|
||||
isPersistingSettings={settings.isPersistingSettings}
|
||||
settingsError={settings.settingsError}
|
||||
onClose={() => setShowSettingsModal(false)}
|
||||
onClose={closeSettingsModal}
|
||||
onPlatformThemeChange={settings.setPlatformTheme}
|
||||
onLogout={logoutCurrentSession}
|
||||
onRefreshRiskBlocks={async () => {
|
||||
|
||||
Reference in New Issue
Block a user