mirror of
https://github.com/priyanshujain/margin-calendar.git
synced 2026-10-04 12:07:03 +00:00
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:
1 parent
661100dfdc
commit
d4c3a304b5
31 files changed
+1319
-114
No files matched your search
@@ -10,6 +10,12 @@ type Phase = "idle" | "connecting" | "working" | "error";
|
||||
interface AccountsState {
|
||||
accounts: Account[];
|
||||
calendars: Calendar[];
|
||||
/**
|
||||
* True once a list has actually come back. An empty `accounts` before that is a page that has not
|
||||
* asked yet, and telling the two apart is the difference between "connect one" and a first frame
|
||||
* of it on every launch.
|
||||
*/
|
||||
loaded: boolean;
|
||||
phase: Phase;
|
||||
error: string | null;
|
||||
authUrl: string | null;
|
||||
@@ -17,7 +23,7 @@ interface AccountsState {
|
||||
refresh: () => Promise<void>;
|
||||
connect: () => Promise<boolean>;
|
||||
cancelConnect: () => void;
|
||||
handleAuthEvent: (ok: boolean, error: string | null) => Promise<void>;
|
||||
handleAuthEvent: (ok: boolean, error: string | null, cancelled?: boolean) => Promise<void>;
|
||||
openAuthUrl: () => void;
|
||||
copyAuthUrl: () => Promise<void>;
|
||||
disconnect: (accountId: string) => Promise<void>;
|
||||
@@ -27,6 +33,7 @@ interface AccountsState {
|
||||
export const useAccounts = create<AccountsState>((set, get) => ({
|
||||
accounts: [],
|
||||
calendars: [],
|
||||
loaded: false,
|
||||
phase: "idle",
|
||||
error: null,
|
||||
authUrl: null,
|
||||
@@ -35,7 +42,7 @@ export const useAccounts = create<AccountsState>((set, get) => ({
|
||||
if (!live()) return;
|
||||
try {
|
||||
const [accounts, calendars] = await Promise.all([accountsList(), calendarsList()]);
|
||||
set({ accounts, calendars });
|
||||
set({ accounts, calendars, loaded: true });
|
||||
} catch (e) {
|
||||
set({ error: String(e) });
|
||||
}
|
||||
@@ -50,7 +57,7 @@ export const useAccounts = create<AccountsState>((set, get) => ({
|
||||
.then((url) => set({ authUrl: url }))
|
||||
.catch((e) => {
|
||||
set({ phase: "error", error: String(e), resolveConnect: null });
|
||||
notify(`Could not connect: ${e}`);
|
||||
notify(`Could not connect your Google account: ${e}`);
|
||||
resolve(false);
|
||||
});
|
||||
}),
|
||||
@@ -59,17 +66,22 @@ export const useAccounts = create<AccountsState>((set, get) => ({
|
||||
set({ phase: "idle", authUrl: null, resolveConnect: null });
|
||||
resolve?.(false);
|
||||
},
|
||||
handleAuthEvent: async (ok, error) => {
|
||||
handleAuthEvent: async (ok, error, cancelled = false) => {
|
||||
if (get().phase !== "connecting") return;
|
||||
const resolve = get().resolveConnect;
|
||||
if (ok) {
|
||||
await get().refresh();
|
||||
set({ phase: "idle", authUrl: null, error: null, resolveConnect: null });
|
||||
notify("Connected to Google Calendar");
|
||||
} else if (cancelled) {
|
||||
// Closing the consent browser is an answer, not a fault. Back to idle with nothing said:
|
||||
// the user already knows what they did, and a red panel telling them about it reads as
|
||||
// though shutting the sheet broke something.
|
||||
set({ phase: "idle", authUrl: null, error: null, resolveConnect: null });
|
||||
} else {
|
||||
const message = error ?? "authorization failed";
|
||||
set({ phase: "error", authUrl: null, error: message, resolveConnect: null });
|
||||
notify(`Could not connect: ${message}`);
|
||||
notify(`Could not connect your Google account: ${message}`);
|
||||
}
|
||||
resolve?.(ok);
|
||||
},
|
||||
@@ -93,10 +105,10 @@ export const useAccounts = create<AccountsState>((set, get) => ({
|
||||
await accountDisconnect(accountId);
|
||||
await get().refresh();
|
||||
set({ phase: "idle" });
|
||||
notify("Disconnected");
|
||||
notify("Google account disconnected");
|
||||
} catch (e) {
|
||||
set({ phase: "error", error: String(e) });
|
||||
notify(`Could not disconnect: ${e}`);
|
||||
notify(`Could not disconnect that Google account: ${e}`);
|
||||
}
|
||||
},
|
||||
setSelected: async (calendarId, selected) => {
|
||||
|
||||
Reference in new issue
Block a user