Symptom

On Android only, the first-time “Accept the Terms of Service” dialog re-prompted on every cold launch. Desktop and iOS showed it once. The second FTUE step (PIN entry) persisted correctly on all platforms, including Android.

Root cause

TOS acceptance was recorded by flipping ClientMetaData.ftue to false on the local Client node and persisting that node via FileOperations. ClientMetaData is part of the swarm: it is serialized, broadcast to peers, and reconstructed on connect, and it rides on the same node store that gets rewritten throughout a session. That made a local, one-time flag fragile, and on Android it did not survive a relaunch (getSelfWithInfo() read back ftue == true, the default). The PIN, by contrast, is stored in a dedicated, local-only ClientPinStore — which is exactly why it persisted reliably while the TOS flag did not. Static analysis confirmed the Android read/write paths matched iOS, so rather than chase the exact Android-specific persistence failure, the fix removes the fragility class entirely.

Fix

Added an OnboardingStore abstraction (interface in shared/commonMain, one impl per platform — Android SharedPreferences, JVM marker file, iOS NSUserDefaults, browser localStorage) bound in each platformModule, mirroring the proven ClientPinStore. The TOS gate in ClientScreen.AvatarWithSpeechBubble now reads onboardingStore.tosAccepted() and the Continue button calls markTosAccepted(); the ClientMetaData.ftue write is kept only for peer-facing display. Clients that accepted before this store existed (persisted ftue == false) are migrated into the store on first launch so they are not re-prompted.

Prevention

Local, one-time, single-device flags (TOS accepted, “don’t show again”, per-device opt-ins) must not ride on swarm-shared, serialized node metadata — persist them in a dedicated local store like ClientPinStore/OnboardingStore. JvmOnboardingStoreTest locks the durability contract with a fresh-instance-over-same-file test that simulates a relaunch (the exact regression), using an injected temp path so it never touches a real production path.