Files
margin-calendar/src/components/Settings.tsx
T
pj d4c3a304b5 Sign in on a phone with no console work, in the browser's own session
Mobile OAuth reused the desktop client all along; what stopped it was the
browser. Sending the user out to Safari or Chrome backgrounds the app, iOS
suspends it, and the redirect carrying the code arrives at a socket nobody
is accepting on. The consent page now opens in front of the app instead, in
SFSafariViewController or a Chrome Custom Tab, so the loopback listener
stays live and the existing `installed` client is enough. Verified against
Google's real consent screen on a simulator and an emulator.

A per-platform client is still supported and is now an upgrade rather than a
prerequisite. On iOS it buys ASWebAuthenticationSession, which shares
Safari's session so nobody is asked to sign in to Google twice. Android
needs nothing: Custom Tabs share Chrome's cookies, measured rather than
assumed. iOS session sharing could not be confirmed on the simulator and
wants a real device.

Never an app-owned WebView: Google blocks it, and rightly, since a webview
the app controls can read the password typed into it.

Cancelling is no longer reported as a failure. AuthEvent carries a
`cancelled` flag, set by comparing against the constant every back-out path
returns, and Google's `access_denied` on desktop counts too.

Five frontend bugs found by driving the real UI, not by reading it: the
details card slid under the tab bar leaving its buttons unhittable; the
ghost click after a touch pressed a button in the card that tap had just
opened, opening the editor by itself; the swipe that pages the day was dead
over every read-only block; 84px of macOS traffic-light lane was reserved on
platforms with no traffic lights; and the desktop header ignored the top
safe area on an iPad. A first launch now says what to do next rather than
showing an empty grid, and accounts are named as Google accounts throughout.
2026-08-12 18:32:45 +05:30

112 lines
3.5 KiB
TypeScript

// Global preferences, and only the ones that genuinely are global. Everything else about this app
// is a key away, which is the point of it, so there is nothing here to pad the panel with.
import { useState } from "react";
import { useAccounts } from "../store/useAccounts";
import { useCalendarView } from "../store/useCalendarView";
import { useOverlays } from "../store/useOverlays";
import { useTheme } from "../store/useTheme";
import { setWeekStartDay, weekStartDay } from "../time";
import { Sheet } from "./overlayShell";
export function Settings() {
const open = useOverlays((s) => s.open);
const close = useOverlays((s) => s.close);
const push = useOverlays((s) => s.push);
const theme = useTheme((s) => s.theme);
const toggleTheme = useTheme((s) => s.toggle);
const accounts = useAccounts((s) => s.accounts);
const [start, setStart] = useState<0 | 1>(() => (weekStartDay() === 0 ? 0 : 1));
const chooseTheme = (next: "light" | "dark") => {
if (next !== theme) toggleTheme();
};
const chooseStart = (day: 0 | 1) => {
if (day === start) return;
setWeekStartDay(day);
setStart(day);
// The week span is derived from this on every read, so nudge the view into recomputing it.
useCalendarView.getState().setAnchor(useCalendarView.getState().anchor);
};
return (
<Sheet
open={open === "settings"}
title="Settings"
onClose={close}
foot={
<button type="button" className="panel-button" data-autofocus="" onClick={close}>
Done
</button>
}
>
<div className="setting-row">
<span className="setting-name">Theme</span>
<div className="segment">
<button
type="button"
className="segment-option"
data-on={theme === "light" || undefined}
onClick={() => chooseTheme("light")}
>
Light
</button>
<button
type="button"
className="segment-option"
data-on={theme === "dark" || undefined}
onClick={() => chooseTheme("dark")}
>
Dark
</button>
</div>
</div>
<div className="setting-row">
<span className="setting-text">
<span className="setting-name">Week starts on</span>
<span className="setting-note">
Week view begins on this day rather than on today. The arrows and h and l still slide
it a day at a time when you want a look either side.
</span>
</span>
<div className="segment">
<button
type="button"
className="segment-option"
data-on={start === 0 || undefined}
onClick={() => chooseStart(0)}
>
Sunday
</button>
<button
type="button"
className="segment-option"
data-on={start === 1 || undefined}
onClick={() => chooseStart(1)}
>
Monday
</button>
</div>
</div>
<div className="setting-row">
<span className="setting-text">
<span className="setting-name">Google accounts</span>
<span className="setting-note">
{accounts.length === 0
? "No Google account connected yet."
: `${accounts.length} connected.`}
</span>
</span>
<button type="button" className="panel-button" onClick={() => push("accounts")}>
Manage
</button>
</div>
</Sheet>
);
}
export default Settings;