Files
margin-calendar/src-tauri/Cargo.toml
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

70 lines
2.6 KiB
TOML

[package]
name = "margin-calendar"
version = "0.1.0"
description = "A calendar for Google Calendar"
authors = ["Margin"]
edition = "2021"
[lib]
name = "margin_calendar_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
# Mobile has no loopback listener to catch Google's redirect, so the OAuth answer comes back as a
# custom URI scheme the OS routes to this app. Registered on desktop too, so both flows are one
# code path with one difference in it rather than two.
tauri-plugin-deep-link = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
base64 = "0.22"
sha2 = "0.10"
rand = "0.8"
url = "2"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
rusqlite = { version = "0.37", features = ["bundled"] }
rrule = "0.14"
chrono = { version = "0.4", features = ["serde"] }
chrono-tz = "0.10"
tokio = { version = "1", features = ["sync", "time"] }
# Refresh tokens are sealed with XChaCha20-Poly1305 and kept in the app data directory. There is no
# `keyring` here on purpose: it has no Android backend at all, and on macOS it ties the item to the
# code signature, so every rebuild re-prompts for authorization. src/google/secrets.rs states what
# the file is and is not worth on each platform.
chacha20poly1305 = "0.10"
# There is no auto-updater and no process to restart on a phone: the store is the update channel.
# Gated here as well as behind cfg(desktop) in lib.rs so a mobile build does not compile them at all.
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-process = "2"
tauri-plugin-updater = "2"
# Two things wry leaves to us: one UIKit property (stop_uikit_shrinking_the_viewport in lib.rs) and
# the SFSafariViewController that shows the consent page (google/browser.rs). These versions are the
# ones wry already resolves for its own iOS backend, so matching them keeps a single copy of objc2 in
# the build rather than a second incompatible one.
[target.'cfg(target_os = "ios")'.dependencies]
objc2 = "0.6"
objc2-ui-kit = { version = "0.3", default-features = false, features = [
"std",
"UIResponder",
"UIView",
"UIScrollView",
] }
objc2-foundation = { version = "0.3", default-features = false, features = [
"std",
"NSString",
"NSURL",
] }
# Only for the null completion handler the two present/dismiss calls take. Passing a raw null
# pointer there would encode as an object rather than a block, which objc2 checks and rejects.
block2 = "0.6"
[dev-dependencies]
tempfile = "3"