feat: device-security nudge — verify this device / key backup (#110, #123)
CI / Build & Quality Checks (pull_request) Successful in 1m48s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 12s
CI / Playwright smoke (e2e) (pull_request) Successful in 11m38s
CI / Build & Quality Checks (pull_request) Successful in 1m48s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 12s
CI / Playwright smoke (e2e) (pull_request) Successful in 11m38s
One "security" strip, in the same top slot and style as the status banner (#124), for this device, in priority order: - "Verify this device" — cross-signing exists but this device isn't verified (can't unlock backed-up history, untrusted to others). First, because verifying with the recovery key also connects the backup. - "Connect this device to your key backup" — a backup exists, this device isn't using it. - "Set up key backup" — no backup at all (includes accounts without cross-signing; the setup flow does both). The button opens Settings → Devices (existing flows); "Not now" snoozes. Rules: nothing in a device's first 24 h; "Not now" snoozes that nudge 7 days; 3 dismissals stop it; never on a healthy device; waits until sync has settled (never under "Connecting…"); outage/maintenance/connection strips win. Per-device record in localStorage, wiped on logout. Mounted inside the Matrix client context and an error boundary (an early version outside the context crashed the app on load — caught before pushing). Also: the status strip wraps its text on phones instead of truncating it when there's no Details button (benefits #124's strips too). Design approved on #123 (real-client screenshots there). Tests: 4 unit (the #123 state table, loading, timing, stored record); e2e: nothing during the grace period, "Set up key backup" → Settings → Devices, "Not now" holds across a reload. Unit 1319, Playwright 29 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8e30c73e2f
commit
6a7627fa26
@@ -22,7 +22,7 @@ const readDismissed = (): string[] => {
|
||||
}
|
||||
};
|
||||
|
||||
const bannerIcon = (b: StatusBanner): IconSrc => {
|
||||
const bannerIcon = (b: StripContent): IconSrc => {
|
||||
if (b.kind === 'maintenance') return Icons.Setting;
|
||||
if (b.tone === 'Primary') return Icons.Info;
|
||||
return Icons.Warning;
|
||||
@@ -32,12 +32,28 @@ const bannerIcon = (b: StatusBanner): IconSrc => {
|
||||
* [Gitea #124] One status strip, in the same slot and style as the sync and
|
||||
* clock banners. Presentational: the caller picks the banner.
|
||||
*/
|
||||
export type StripContent = Pick<
|
||||
StatusBanner,
|
||||
'key' | 'tone' | 'text' | 'detail' | 'dismissable'
|
||||
> & {
|
||||
kind?: StatusBanner['kind'];
|
||||
};
|
||||
|
||||
export function StatusStrip({
|
||||
banner,
|
||||
onDismiss,
|
||||
icon,
|
||||
action,
|
||||
dismissLabel,
|
||||
}: {
|
||||
banner: StatusBanner;
|
||||
banner: StripContent;
|
||||
onDismiss?: () => void;
|
||||
/** Overrides the icon picked from the banner's tone/kind. */
|
||||
icon?: IconSrc;
|
||||
/** A primary action button (e.g. "Verify"). */
|
||||
action?: { label: string; onClick: () => void };
|
||||
/** A text button instead of the ✕ for dismissing (e.g. "Not now"). */
|
||||
dismissLabel?: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const mobile = useScreenSizeContext() === ScreenSize.Mobile;
|
||||
@@ -59,8 +75,8 @@ export function StatusStrip({
|
||||
gap="200"
|
||||
style={{ minHeight: config.size.X300 }}
|
||||
>
|
||||
<Icon size="100" src={bannerIcon(banner)} />
|
||||
<Text size="L400" truncate={mobile && !open} style={{ minWidth: 0 }}>
|
||||
<Icon size="100" src={icon ?? bannerIcon(banner)} />
|
||||
<Text size="L400" truncate={mobile && !!banner.detail && !open} style={{ minWidth: 0 }}>
|
||||
{banner.text}
|
||||
</Text>
|
||||
{banner.detail && (
|
||||
@@ -78,7 +94,31 @@ export function StatusStrip({
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
{banner.dismissable && onDismiss && (
|
||||
{action && (
|
||||
<Button
|
||||
size="300"
|
||||
variant={banner.tone}
|
||||
fill="Solid"
|
||||
radii="300"
|
||||
onClick={action.onClick}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<Text size="B300">{action.label}</Text>
|
||||
</Button>
|
||||
)}
|
||||
{banner.dismissable && onDismiss && dismissLabel && (
|
||||
<Button
|
||||
size="300"
|
||||
variant={banner.tone}
|
||||
fill="None"
|
||||
radii="300"
|
||||
onClick={onDismiss}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<Text size="B300">{dismissLabel}</Text>
|
||||
</Button>
|
||||
)}
|
||||
{banner.dismissable && onDismiss && !dismissLabel && (
|
||||
<IconButton
|
||||
size="300"
|
||||
variant={banner.tone}
|
||||
|
||||
Reference in New Issue
Block a user