Skip to content

feat(mobile): hard-block under-18 users on the store age signal - #4665

Draft
brow wants to merge 3 commits into
mainfrom
mobile-age-signal-gate
Draft

feat(mobile): hard-block under-18 users on the store age signal#4665
brow wants to merge 3 commits into
mainfrom
mobile-age-signal-gate

Conversation

@brow

@brow brow commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

What changed?

Buzz mobile now asks the app store once per launch whether the signed-in person is under 18, and hard-blocks the app behind a full-screen 18+ notice when the store says yes.

Both platforms expose one buzz/age_signal method channel with a single requestAgeSignal method. The native handlers return data, never a verdict: exactly {status: "signal" | "noSignal", ageUpper: int?}. iOS uses AgeRangeService.requestAgeRange(ageGates: 18, in:) from DeclaredAgeRange (iOS 26.0+); Android uses Play age-signals, calling checkAgeSignals() only when access status is SHARED. All decision logic lives in one Dart function, so the whole verdict is unit-testable.

The gate sits in MaterialApp.builder, above the navigator, so a route pushed while the request is still in flight is also covered.

The signal is never persisted. It is read, used to compute one boolean, and discarded. Nothing is written to disk, and neither the age value nor the range is logged or sent to analytics.

Why?

App stores now expose a declared-age range, and Buzz's Terms require users to be 18 or older. This wires the store signal to that rule with the smallest change that is defensible.

The behavior is deliberately fail-open: absent, errored, declined, undeterminable, or old-OS all allow the app through. Only an affirmative under-18 signal blocks. A store signal is not an identity check, so treating "we could not tell" as "block" would lock out adults over a signal the platform never promised to deliver.

Two things reviewers usually ask about:

  • New dependency com.google.android.play:age-signals:0.0.4. This is the only way to read the Play signal. Its AAR declares minSdkVersion 23, below the app's 24, so it forces no minSdk bump, and it self-declares its own consent activity, so no AndroidManifest change is needed.
  • No iOS project changes. DeclaredAgeRange autolinks from the import via LC_LINKER_OPTION, so project.pbxproj and Podfile are untouched. The framework binds weakly at the iOS 16.0 floor (verified LC_LOAD_WEAK_DYLIB on both the simulator and device slices), and the call site is behind #available(iOS 26.0, *).

How is it tested?

Build and run, plus simulator runs on iOS 18.1 (framework absent) and iOS 26.5 (framework present); the app launches and reaches the normal pairing UI on both, confirming fail-open.

Added tests:

Deployment note

iOS requires a one-time RESCIND_CONSENT / Declared Age Range setup in App Store Connect before the API returns a signal in production. That is an ops task, not a code change, and is not covered by this PR.

🤖 This PR was authored with an agent.

npub12wpjffj7q5qjsky5jvk4ldwlxmse5xll3d8gytk4wqd0c5y7jvwspg37n6 and others added 3 commits August 3, 2026 21:11
Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant