Repository navigation
feat(ios-client): expose Live Activity push state getters and stop duplicate interaction delivery - #353
Conversation
Physical device test: taps while JavaScript is suspendedThe simulator runs couldn't cover a suspended app, because the simulator keeps JavaScript running in the background. So I repeated the test on a physical iPhone 17 Pro (iOS 27.0) with a Release build of the example app at The probe is one
The log survives the app being killed. A 1 s heartbeat records any gap in which JavaScript didn't run, which shows when iOS suspended it.
Across all scenarios: 7 taps made, 7 received, no duplicates, and every tap arrived in the order it was made. Sample from the 198 s suspension: What this means
Not covered
|
What is this?
iOS apps can now read their Live Activity push state whenever they need it, instead of only catching it as a one-off event.
getLiveActivityPushTokens()returns the push token of every running Live Activity (static and Dynamic),getLiveActivityPushToStartToken()returns the push-to-start token, andareLiveActivitiesEnabled()says whether the user allows Live Activities. A newactivityEnablementChangedevent fires when the user flips that setting.It also fixes how button and toggle taps (
interactionevents) reach JavaScript. A tap the app had already handled was replayed on the next launch or Metro reload, and a tap that launched the app could be replayed before any listener was attached and lost.How does it work?
Tokens and enablement are treated as state. The getters read ActivityKit at call time, so they return current values, and the existing token events plus the new enablement event report changes. The recommended pattern is to subscribe first and then call the getter, with the backend upserting tokens, since the same token can arrive from both. The token getters return nothing when
enablePushNotificationsis off; the enablement API works regardless.Interactions are treated as events that JavaScript pulls. Every tap is recorded in a small queue in the App Group (in the app's memory when there is no App Group), and native code only signals that taps are available. JavaScript takes them with a synchronous call that removes and returns them in one step — when a listener attaches, on the signal, and when the app becomes active — and hands them to every listener attached at that moment. A tap is therefore never delivered twice, and taps made while the app was killed or suspended arrive once the app runs with a listener. Taps carry a unique
idand wait at most 15 minutes (the newest 100 are kept). AVoltraViewonly takes taps from its own content, and taps whose listeners went away mid-dispatch are put back. The UserDefaults queue from earlier releases is discarded on upgrade, since it contains taps that were already handled.The design, the alternatives that were rejected (replaying the last token on subscribe, queueing tokens, and a first version that delivered taps hot without persisting them), and the remaining edge cases — including the at-most-once window if the app dies mid-dispatch — are recorded in ADR 0011. Android is unaffected: it sends no native events and Voltra owns no push tokens there.
Why is this useful?
A backend that needs a Live Activity's token no longer depends on JavaScript subscribing before ActivityKit hands the token over. An app can register tokens after sign-in or after a reload, and can hide Live Activity features when the user has turned them off. Apps that handle taps no longer see a tap twice, and taps made while the app was in the background or not running are no longer lost.