feat(mobile): hard-block under-18 users on the store age signal - #4665
Draft
brow wants to merge 3 commits into
Draft
feat(mobile): hard-block under-18 users on the store age signal#4665brow wants to merge 3 commits into
brow wants to merge 3 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_signalmethod channel with a singlerequestAgeSignalmethod. The native handlers return data, never a verdict: exactly{status: "signal" | "noSignal", ageUpper: int?}. iOS usesAgeRangeService.requestAgeRange(ageGates: 18, in:)fromDeclaredAgeRange(iOS 26.0+); Android uses Playage-signals, callingcheckAgeSignals()only when access status isSHARED. 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:
com.google.android.play:age-signals:0.0.4. This is the only way to read the Play signal. Its AAR declaresminSdkVersion 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.DeclaredAgeRangeautolinks from theimportviaLC_LINKER_OPTION, soproject.pbxprojandPodfileare untouched. The framework binds weakly at the iOS 16.0 floor (verifiedLC_LOAD_WEAK_DYLIBon 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:
age_signal_provider_testcovers the 17/18 boundary, open-ended ranges, no-signal, platform errors, malformed payloads, and request-onceage_gate_app_testproves an authenticated session is still blocked, and that a route pushed before the signal arrives is torn downage_restriction_page_testasserts the block screen offers no bypass affordanceAgeSignalPayloadTestpins the Android payload to exactly{status, ageUpper}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.