Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 56 additions & 3 deletions packages/app-shell/src/console/home/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* @module
*/

import { useMemo, type ComponentType } from 'react';
import { useMemo, useState, useEffect, type ComponentType } from 'react';
import { useNavigate } from 'react-router-dom';
import { useMetadata } from '../../providers/MetadataProvider';
import { useRecentItems } from '../../hooks/useRecentItems';
Expand All @@ -27,7 +27,7 @@ import { AppCard } from './AppCard';
import { RecentApps } from './RecentApps';
import { StarredApps } from './StarredApps';
import { Empty, EmptyTitle, EmptyDescription, Button } from '@object-ui/components';
import { Plus, Settings, Sparkles, Star, Clock, ArrowDown, Store, LayoutGrid } from 'lucide-react';
import { Plus, Settings, Sparkles, Star, Clock, ArrowDown, Store, LayoutGrid, ShieldAlert, X } from 'lucide-react';

function pickGreetingKey(hour: number): string {
if (hour < 5) return 'home.greetingNight';
Expand Down Expand Up @@ -102,6 +102,54 @@ function StatPill({
);
}

/**
* Dismissible nudge to set a local recovery password — shown when the user
* signed in via SSO and has no local credential yet. We no longer force this
* before the first session (it walled off the magic moment); this gentle,
* one-time reminder preserves instance self-sufficiency without the friction.
*/
function RecoveryPasswordReminder({ t }: { t: (key: string, opts?: any) => string }) {
const navigate = useNavigate();
const { hasLocalPassword } = useAuth();
const [show, setShow] = useState(false);
useEffect(() => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('os:recovery-pw-dismissed') === '1') return;
let cancelled = false;
Promise.resolve(hasLocalPassword?.())
.then((has) => { if (!cancelled && has === false) setShow(true); })
.catch(() => { /* unknown → don't nag */ });
return () => { cancelled = true; };
}, [hasLocalPassword]);
const dismiss = () => {
try { localStorage.setItem('os:recovery-pw-dismissed', '1'); } catch { /* ignore */ }
setShow(false);
};
if (!show) return null;
return (
<div className="px-4 sm:px-6 lg:px-8 pt-4">
<div className="max-w-7xl mx-auto">
<div className="flex items-center gap-3 rounded-xl border border-amber-300/60 dark:border-amber-700/50 bg-amber-50 dark:bg-amber-950/30 px-4 py-3">
<ShieldAlert className="h-5 w-5 shrink-0 text-amber-600 dark:text-amber-400" />
<p className="flex-1 min-w-0 text-sm text-amber-900 dark:text-amber-200">
{t('home.recoveryReminder.message', { defaultValue: 'Set a recovery password so you can still sign in if single sign-on is ever unavailable.' })}
</p>
<Button size="sm" variant="outline" onClick={() => navigate('/set-password')} data-testid="recovery-pw-set">
{t('home.recoveryReminder.cta', { defaultValue: 'Set password' })}
</Button>
<button
type="button"
onClick={dismiss}
aria-label={t('home.recoveryReminder.dismiss', { defaultValue: 'Dismiss' })}
className="shrink-0 rounded-md p-1 text-amber-700/70 hover:text-amber-900 dark:text-amber-300/70 dark:hover:text-amber-100"
>
<X className="h-4 w-4" />
</button>
</div>
</div>
</div>
);
}

export function HomePage() {
const navigate = useNavigate();
const { t } = useObjectTranslation();
Expand Down Expand Up @@ -134,7 +182,9 @@ export function HomePage() {

if (activeApps.length === 0) {
return (
<div className="flex flex-1 items-center justify-center p-6">
<div className="flex flex-col flex-1">
<RecoveryPasswordReminder t={t} />
<div className="flex flex-1 items-center justify-center p-6">
<Empty>
<EmptyTitle>{t('home.welcome', { defaultValue: 'Welcome to ObjectUI' })}</EmptyTitle>
<EmptyDescription>
Expand Down Expand Up @@ -164,6 +214,7 @@ export function HomePage() {
</Button>
</div>
</Empty>
</div>
</div>
);
}
Expand All @@ -177,6 +228,8 @@ export function HomePage() {
the gradient display name in the hero.
*/}

<RecoveryPasswordReminder t={t} />

{/* Hero */}
<section className="px-4 sm:px-6 lg:px-8 pt-10 pb-6">
<div className="max-w-7xl mx-auto">
Expand Down
5 changes: 5 additions & 0 deletions packages/i18n/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1455,6 +1455,11 @@ const en = {
welcome: 'Build your business system with AI',
welcomeDescription: 'Describe your business in one sentence — AI generates the objects, screens, APIs and agent tools. Or start from scratch.',
buildWithAI: 'Build with AI',
recoveryReminder: {
message: 'Set a recovery password so you can still sign in if single sign-on is ever unavailable.',
cta: 'Set password',
dismiss: 'Dismiss',
},
createFirstApp: 'Create app manually',
systemSettings: 'System Settings',
browseMarketplace: 'Browse App Marketplace',
Expand Down
5 changes: 5 additions & 0 deletions packages/i18n/src/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1468,6 +1468,11 @@ const zh = {
welcome: '用 AI 搭建你的业务系统',
welcomeDescription: '用一句话描述你的业务,AI 帮你生成对象、界面、API 和 agent 工具。也可以手动从零开始。',
buildWithAI: '用 AI 搭建',
recoveryReminder: {
message: '建议设置一个备用密码,这样即使单点登录不可用,你仍能直接登录此环境。',
cta: '设置密码',
dismiss: '关闭',
},
createFirstApp: '手动创建应用',
systemSettings: '系统设置',
browseMarketplace: '浏览应用市场',
Expand Down
Loading