App Gates
Biometric lock, updates, onboarding, legal consent, and auth routing.
App gates are the launch-time and cross-cutting flows that decide what the user sees before the main shell: biometric unlock, error fallback, updates, onboarding, legal consent, and session-driven auth routing.
Some template notes list an older order. The current code order is
BiometricGate -> ErrorBoundary -> UpdateGate -> OnboardingGate -> ConsentGate -> MainNavGraph.
What it provides
| Gate | Status | Implementation |
|---|---|---|
| Biometric app lock | Integrated | BiometricGate wraps the entire app content |
| Error fallback | Integrated | ErrorBoundary observes AppErrorBus |
| Update gate | Play-only | UpdateGate shows force or soft update dialogs |
| Onboarding | Integrated | Four-page HorizontalPager, persisted in DataStore |
| Legal consent | Integrated | Versioned terms/privacy/EULA acceptance in DataStore |
| Auth routing | Integrated | SessionController.currentUser pops the auth graph after sign-in |
| Store review | Play-only | RatingController prompts after smart triggers |
Key files
Launch composition
MainActivity sets the app theme and locale host, then wraps the navigation
graph like this:
LocalizedAppHost
-> BiometricGate
-> ErrorBoundary
-> AppNavGraph
-> UpdateGate
-> OnboardingGate
-> ConsentGate
-> MainNavGraph
-> MainShell or AuthGraphMainNavGraph is session-driven. It starts at the main shell, can open the auth
graph when a screen requires sign-in, and pops the auth graph when
SessionController.currentUser becomes non-null.
Biometric gate
BiometricGate protects the whole app only when the user enabled biometric
lock from Account Security. It uses BiometricManager with BIOMETRIC_STRONG,
stores the enabled flag in DataStore, and keeps lastAuthenticatedAtMillis in
memory.
Behavior:
- If biometric lock is disabled, the gate immediately renders content.
- If no strong biometric is enrolled, Account Security reports
NotEnrolledorUnavailable. - If enabled and the relock delay has passed, the gate shows a lock screen and
opens
BiometricPrompt. - The default relock delay is
30_000milliseconds.
Update gate
UpdateGate is Play-only for the real update flow. It reads these
BuildConfig values from local.properties:
MIN_APP_VERSION=1.0.0
LATEST_APP_VERSION=1.1.0
APP_UPDATE_URL=https://play.google.com/store/apps/details?id=com.soarstarter.kotlin
APP_UPDATE_MESSAGE=Update to keep using the latest experience.UpdateController compares those versions against BuildConfig.VERSION_NAME.
If the installed version is below MIN_APP_VERSION, it shows a non-dismissable
force-update dialog and requests a Play In-App Updates immediate update. If the
installed version is below LATEST_APP_VERSION, it shows a dismissible soft
update and requests a flexible update.
When Play In-App Updates cannot start, the app opens APP_UPDATE_URL, or the
default Play Store URL for the package. Local debug builds can test version
comparison and the fallback link; Play-delivered builds are needed to verify
the real in-app update flow.
Onboarding gate
OnboardingGate waits for the DataStore-backed onboardingCompleted flag to
hydrate. If the flag is false, it shows OnboardingScreen, a four-page
HorizontalPager with skip, next, and get-started actions.
When onboarding completes, the gate:
- Tracks
AnalyticsEvent.OnboardingCompleted(slideCount). - Persists
onboarding_completed = truein DataStore. - Records a store-review trigger with reason
onboarding_completed.
The Showcase tab also contains an onboarding replay screen, but that replay does not change the real DataStore flag.
Consent gate
ConsentGate waits for the accepted legal version to hydrate. The current
version is a code constant:
const val LEGAL_VERSION = "2026-04-30"It is not a local.properties key. Bump this constant when bundled
terms/privacy/EULA text changes and you want users to accept again.
Legal documents are bundled string resources with optional online URLs from:
LEGAL_TERMS_URL=https://example.com/terms
LEGAL_PRIVACY_URL=https://example.com/privacy
LEGAL_EULA_URL=https://example.com/eulaThe gate stores the accepted version in DataStore under
accepted_legal_version.
The current app starts PostHog in SoarApp.onCreate() before ConsentGate.
The legal gate captures acceptance of bundled terms; it does not currently
block analytics initialization. If your policy requires consent-before-
tracking, move analyticsController.start() behind hasAcceptedCurrent or
keep POSTHOG_DISABLED=true until consent is collected.
Store review
RatingController wraps Google Play In-App Review. It records trigger actions
from onboarding completion and referral sharing, and the Showcase rating demo
can request the flow manually.
The smart prompt rules are:
| Rule | Value |
|---|---|
| Minimum recorded actions | 3 |
| Same-version repeat prompt | Blocked |
| Cooldown after a shown prompt | 90 days |
The real review UI is Play-only and controlled by Google Play. A debug build can call the controller, but Play may choose not to show a dialog.
Customizing gates
- Reorder gates by changing the wrappers in
MainActivityandAppNavGraph. - Remove onboarding by deleting
OnboardingGateand its DataStore key reads. - Remove legal consent by deleting
ConsentGate, or keep the legal screens and open them from Settings only. - Change biometric timing by passing a different
relockDelayMillistoBiometricGate. - Replace static update values with hosted config by feeding
UpdateController.overrideRemoteConfig().
Verify
- Fresh install shows onboarding, then legal consent, then the main shell.
- Bumping
LEGAL_VERSIONre-prompts the consent gate. - Setting
MIN_APP_VERSIONabove the currentversionNameshows a force update dialog. - Setting
LATEST_APP_VERSIONabove the currentversionNameshows a soft update dialog that can be dismissed once for that version. - Enabling biometric lock from Account Security locks the app after the relock delay.
- Store review triggers only after the action count, version, and cooldown rules allow it.
