CycleWingman 1.1: widgets everywhere
cyclewingmanreleaseengineering
CycleWingman turns a partner's cycle into a daily relationship-weather forecast — Green, Yellow, Orange or Red. Version 1.1 is about meeting that forecast where you already look: your home screen, your watch face, your notifications. Here's what shipped, and some of the engineering that went into it.
What's new
- Home-screen widgets on iPhone (small and medium), configurable per instance: the active partner, a pinned partner, or everyone at once — with an optional name-free mode.
- Apple Watch complications for every accessory slot, also configurable per slot. The day's color lives on your watch face now.
- An Android home-screen widget with the same configuration story, resizable down to 1×1 with size-adaptive faces.
- Daily heads-ups (opt-in, off by default): a morning forecast and a warning the evening before red days — scheduled fully on-device.
- App lock (opt-in): Face ID / passcode on iOS, fingerprint / screen lock on Android.
- A smarter forecast: when the predicted cycle date passes, the app asks instead of guessing — confirming rolls the history forward and the estimate self-corrects.
- A redesigned partner manager (drag-and-drop ordering, a dedicated edit page), platform back-gesture support, and a long list of fixes.
Under the hood
A forecast cache instead of a third engine
CycleWingman's forecast engine exists twice by design: the TypeScript original in the web layer and a Swift mirror in WingmanCore for the watch — kept honest by golden-fixture parity tests. The Android widget threatened to demand a third copy in Kotlin, which would mean a third parity surface to maintain forever.
We refused. Instead, the web layer precomputes a 30-day forecast cache for every partner on each state change and ships it to the Android shell; the widget just looks up today's row. If the app hasn't been opened for longer than the horizon, the widget honestly asks to be opened rather than showing a stale forecast. iOS widgets and watch complications, meanwhile, recompute in-process via WingmanCore — no cache needed where the engine already lives.
Back gestures in a WebView
The app's UI is a web app inside native shells, and web apps don't get platform back gestures for free: Android's back button minimized the whole app from any inner screen. The fix is a small History-API layer stack: every open UI layer (edit page, paywall, sheet) holds one history entry; popstate — which both Android's back gesture and iOS's edge swipe produce — closes exactly the top-most layer.
The subtle parts: history entries carry an index so an iOS forward-swipe into already-popped entries is detected and undone instead of closing a live layer; programmatic closes mark their entry stale rather than calling history.back(), which would race against layers opened in the same commit; and a layer may refuse to close while a purchase is in flight without losing its entry.
WebView performance war stories
backdrop-filter: bluris not hardware-accelerated in the Android WebView. A blur under a sticky header re-blurs every scrolled frame; we swapped it for near-opaque fills on Android and kept the blur on iOS.- Even without blur, the WebView can't smoothly animate layout properties. The collapsing header now snaps between states on Android (one relayout at a hysteresis-guarded threshold) while iOS keeps the animated shrink.
- Android's
RemoteViewsonly inflates an allowlist of view classes — a plain<View>spacer silently breaks the whole widget with "Problem loading widget".FrameLayoutspaces identically and is allowlisted.
Privacy decisions that shaped features
CycleWingman keeps everything on-device — no accounts, no servers, no analytics. That stance made several 1.1 calls for us: notification copy never contains the partner's name (lock screens are public), watch complications are name-free by design, and the widget's name display is a user choice. Biometric app-lock fails open when a device has no secure lock configured at all — it's the user's only copy of their data, and locking them out of it would be worse than not locking at all.
Subscriptions that lapse gracefully
If a Wingman+ subscription lapses with several partners saved, nothing is deleted. The first partner stays fully accessible; the rest are locked but visible — and deletable, because it's the user's data — with everything restored the moment the subscription returns. The same gate flows to the watch and widgets, so locked forecasts never leak to synced surfaces.