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

+48 -2
View File
@@ -103,15 +103,26 @@ select {
flex: none;
position: relative;
z-index: 45;
height: var(--titlebar-h);
/* Out of the way of a status bar or a notch, the same way the phone bars do it. A desktop reads
both insets as zero; an iPad gets this header rather than those bars and does not. */
height: calc(var(--titlebar-h) + var(--safe-top));
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
padding: 0 14px 0 var(--traffic-pad);
padding: var(--safe-top) 14px 0;
background: var(--shell);
border-bottom: 1px solid var(--line);
}
/* The macOS traffic lights float over this row, so it opens a lane for them and costs no extra
height. macOS is the only platform with any: they come from `titleBarStyle: "Overlay"`, which is
a macOS-only window setting, and on Linux, Windows, an iPad or a browser the lane was 84px of
nothing, pushing the view switcher off centre and starving the date range of the room it needed
to say what day it is. */
:root[data-traffic] .titlebar {
padding-left: var(--traffic-pad);
}
.titlebar .lead {
display: flex;
align-items: center;
@@ -273,9 +284,44 @@ select {
min-height: 0;
display: flex;
flex-direction: column;
/* The first-run note lays itself over whatever is here, so this is the box it covers. */
position: relative;
background: var(--paper);
}
/* Above every part of the grid, which stacks up to 15, and under the overlays, which start at 20:
the panel this opens has to come up in front of it. */
.first-run {
position: absolute;
inset: 0;
z-index: 19;
display: grid;
place-items: center;
padding: 24px;
background: var(--paper);
}
.first-run-text {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
max-width: 320px;
text-align: center;
}
.first-run-title {
margin: 0;
font-family: var(--font-heading);
font-weight: 500;
font-size: 18px;
}
.first-run-note {
margin: 0;
color: var(--ink-soft);
}
/* Overlays, margin's idiom: summoned by a key, dismissed with Escape, never resident. */
.overlay {
position: fixed;
+10
View File
@@ -45,6 +45,16 @@
max-height: min(480px, calc(100vh - var(--titlebar-h) - 16px));
}
/* There is no title bar here. What the card has to fit inside is the window less both bars and the
insets they pad themselves with, which is the same box the placement measures. A cap any taller
than that does not merely scroll, it gets placed over the top bar: the card is laid out first and
pinned to its block second, so a card too tall for the gap has nowhere legal to go. */
:root[data-phone] .details-card {
max-height: calc(
100dvh - var(--phonebar-h) - var(--safe-top) - var(--tabbar-h) - var(--safe-bottom) - 16px
);
}
/* The one scroll region. The footer is outside it, so however long the description runs, Edit,
Delete and Close are where they were. */
.details-body {