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.
This commit is contained in:
pj committed 2026-08-12 18:32:45 +05:30
1 parent 661100dfdc
commit d4c3a304b5
31 files changed
+1319 -114

No files matched your search

+9 -2
View File
@@ -430,12 +430,19 @@ export function GridView({ defaultCalendarId }: GridViewProps) {
if (e.button !== 0 || gesture.current) return;
e.stopPropagation();
select(keyOf(item.instance));
if (item.instance.readOnly || useGrid.getState().draft) return;
const dayStart = startOfDay(item.startMs);
const index = days.findIndex((d) => d === dayStart);
if (index === -1) return;
const { startMin, endMin } = dayMinutes(item, dayStart);
begin(e, mode, item, index, startMin, endMin);
// A block nothing can be done to is still something a swipe has to travel through. The press
// is stopped here rather than on the canvas, so without this the page turn was dead over
// every read-only event, which on a day with a couple of meetings marked busy is most of the
// column. It gets a gesture with no long press behind it: the only thing it can become is
// the swipe, and there is no item on it to commit a move to.
const inert = item.instance.readOnly || useGrid.getState().draft !== null;
if (inert && !isCoarse(e)) return;
begin(e, mode, inert ? null : item, index, startMin, endMin);
if (inert && gesture.current) clearPress(gesture.current);
},
// `begin` closes over the current layout and days, which is what a fresh gesture wants.
[days, layout, select],