fix: prevent login screen flicker for authenticated users#774
fix: prevent login screen flicker for authenticated users#774Arunodoy18 wants to merge 1 commit intoAOSSIE-Org:masterfrom
Conversation
|
🎉 Welcome @Arunodoy18!
We appreciate your contribution! 🚀 |
📝 WalkthroughWalkthroughModified Android build signing configuration to conditionally fall back to debug signing when keystore properties are absent. Introduced AuthStatus enum and new navigation method to resolve authentication state during startup. Refactored splash screen to concurrently wait for auth resolution and minimum display duration before routing. Changes
Sequence Diagram(s)sequenceDiagram
participant App as App Launch
participant Splash as SplashScreen
participant Auth as AuthStateController
participant About as AboutAppScreenController
participant Nav as Navigation System
App->>Splash: Show Splash
Splash->>Splash: Start concurrent timers
Splash->>Auth: Observe authStatus (checking)
Note over Auth: Startup auth check begins
Splash->>Splash: Wait min 3 seconds + auth resolution
Auth->>Auth: setUserProfileData()
Auth->>Auth: Update authStatus (authenticated/unauthenticated)
Splash->>Auth: _waitForAuthResolution() completes
Splash->>About: Check for updates
About->>About: Handle update logic
About->>Splash: Update result
Splash->>Auth: navigateBasedOnAuthState()
Alt authStatus = authenticated
Auth->>Nav: Navigate to appropriate screen<br/>(tabview, onBoarding, or userBlockedScreen)
Else authStatus = unauthenticated
Auth->>Nav: Navigate to landing or welcomeScreen
End
Nav->>App: Route to destination
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@android/app/build.gradle.kts`:
- Around line 47-66: The release build currently falls back to debug signing
when keystorePropertiesFile is missing (in buildTypes -> release ->
signingConfig), which risks producing shippable artifacts signed with debug
keys; change this by making the fallback explicit or failing the build: check
keystorePropertiesFile (or a new boolean opt-in property like
allowDebugSigningForRelease) and if missing and opt-in is false throw a
GradleException to fail the release build, otherwise only use
signingConfigs.getByName("release") when the keystore exists or require the
opt-in property to be present before using the debug signing config; update the
logic around signingConfigs, keystorePropertiesFile, and the release
signingConfig assignment accordingly.
In `@lib/controllers/auth_state_controller.dart`:
- Around line 301-303: The crash happens because reportsCount (a late int set
only in setUserProfileData when isUserProfileComplete == true) can be
uninitialized when navigateBasedOnAuthState() runs; update the guard before
accessing reportsCount by short-circuiting on isUserProfileComplete (e.g. change
the condition to check isUserProfileComplete == true && reportsCount > 5) so
Get.offNamed(AppRoutes.userBlockedScreen) is only evaluated when the profile is
complete and reportsCount is initialized.
In `@lib/views/screens/splash_screen.dart`:
- Around line 89-108: The _waitForAuthResolution() routine can miss a state flip
that occurs between the initial check and the ever() subscription; update it so
that after attaching the worker (ever on authController.authStatus) you
immediately re-check authController.authStatus and complete the Completer if
it's no longer AuthStatus.checking, then dispose the worker; ensure you still
guard with !completer.isCompleted to avoid double-completion.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
android/app/build.gradle.ktslib/controllers/auth_state_controller.dartlib/views/screens/splash_screen.dart
| if (keystorePropertiesFile.exists()) { | ||
| signingConfigs { | ||
| create("release") { | ||
| keyAlias = keystoreProperties["keyAlias"] as String | ||
| keyPassword = keystoreProperties["keyPassword"] as String | ||
| storeFile = keystoreProperties["storeFile"]?.let { file(it) } | ||
| storePassword = keystoreProperties["storePassword"] as String | ||
| create("release") { | ||
| keyAlias = keystoreProperties["keyAlias"] as String | ||
| keyPassword = keystoreProperties["keyPassword"] as String | ||
| storeFile = keystoreProperties["storeFile"]?.let { file(it) } | ||
| storePassword = keystoreProperties["storePassword"] as String | ||
| } | ||
| } | ||
| } | ||
|
|
||
| buildTypes { | ||
| release { | ||
| // TODO: Add your own signing config for the release build. | ||
| // Signing with the debug keys for now, so `flutter run --release` works. | ||
| isMinifyEnabled = false | ||
| isShrinkResources = false | ||
| signingConfig = signingConfigs.getByName("debug") | ||
| signingConfig = signingConfigs.getByName("release") | ||
| signingConfig = if (keystorePropertiesFile.exists()) { | ||
| signingConfigs.getByName("release") | ||
| } else { | ||
| signingConfigs.getByName("debug") | ||
| } |
There was a problem hiding this comment.
Avoid auto-falling back to debug signing for release builds.
This can generate a shippable release artifact signed with debug keys, which is a security and compliance risk. Gate the fallback behind an explicit opt‑in or fail the release build when the keystore is missing.
🔒 Suggested guard for release signing
release {
isMinifyEnabled = false
isShrinkResources = false
- signingConfig = if (keystorePropertiesFile.exists()) {
- signingConfigs.getByName("release")
- } else {
- signingConfigs.getByName("debug")
- }
+ signingConfig = when {
+ keystorePropertiesFile.exists() ->
+ signingConfigs.getByName("release")
+ project.hasProperty("allowDebugSigning") ->
+ signingConfigs.getByName("debug")
+ else ->
+ throw GradleException("Missing key.properties for release signing")
+ }
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@android/app/build.gradle.kts` around lines 47 - 66, The release build
currently falls back to debug signing when keystorePropertiesFile is missing (in
buildTypes -> release -> signingConfig), which risks producing shippable
artifacts signed with debug keys; change this by making the fallback explicit or
failing the build: check keystorePropertiesFile (or a new boolean opt-in
property like allowDebugSigningForRelease) and if missing and opt-in is false
throw a GradleException to fail the release build, otherwise only use
signingConfigs.getByName("release") when the keystore exists or require the
opt-in property to be present before using the debug signing config; update the
logic around signingConfigs, keystorePropertiesFile, and the release
signingConfig assignment accordingly.
| if (reportsCount > 5) { | ||
| Get.offNamed(AppRoutes.userBlockedScreen); | ||
| return; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find the file and check if it exists
fd -t f auth_state_controller.dartRepository: AOSSIE-Org/Resonate
Length of output: 105
🏁 Script executed:
# Get the file size to see if we can read it
wc -l lib/controllers/auth_state_controller.dartRepository: AOSSIE-Org/Resonate
Length of output: 109
🏁 Script executed:
# Read the relevant lines around 301-303 with broader context
sed -n '280,320p' lib/controllers/auth_state_controller.dartRepository: AOSSIE-Org/Resonate
Length of output: 1652
🏁 Script executed:
# Search for reportsCount declaration and all its usages
rg "reportsCount" lib/controllers/auth_state_controller.dart -nRepository: AOSSIE-Org/Resonate
Length of output: 185
🏁 Script executed:
# Search for isUserProfileComplete to understand the initialization pattern
rg "isUserProfileComplete" lib/controllers/auth_state_controller.dart -n -A 2 -B 2Repository: AOSSIE-Org/Resonate
Length of output: 884
🏁 Script executed:
# Get context around reportsCount assignment at line 229
sed -n '200,260p' lib/controllers/auth_state_controller.dartRepository: AOSSIE-Org/Resonate
Length of output: 2227
🏁 Script executed:
# Get the full navigateBasedOnAuthState() method context
sed -n '276,315p' lib/controllers/auth_state_controller.dartRepository: AOSSIE-Org/Resonate
Length of output: 1786
Guard reportsCount access to prevent LateInitializationError for incomplete profiles.
reportsCount is a late int field that is only initialized inside the if (isUserProfileComplete == true) block in setUserProfileData(). In navigateBasedOnAuthState(), if authStatus is already authenticated and isUserProfileComplete == false, the field remains uninitialized when accessed at line 301, causing a crash during navigation.
The fix uses short-circuit evaluation to prevent the access when the profile is incomplete:
Guard before accessing reportsCount
- if (reportsCount > 5) {
+ if (isUserProfileComplete == true && reportsCount > 5) {
Get.offNamed(AppRoutes.userBlockedScreen);
return;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/controllers/auth_state_controller.dart` around lines 301 - 303, The crash
happens because reportsCount (a late int set only in setUserProfileData when
isUserProfileComplete == true) can be uninitialized when
navigateBasedOnAuthState() runs; update the guard before accessing reportsCount
by short-circuiting on isUserProfileComplete (e.g. change the condition to check
isUserProfileComplete == true && reportsCount > 5) so
Get.offNamed(AppRoutes.userBlockedScreen) is only evaluated when the profile is
complete and reportsCount is initialized.
| /// Returns a [Future] that completes as soon as [AuthStateController.authStatus] | ||
| /// leaves [AuthStatus.checking]. | ||
| /// | ||
| /// If the auth check is already resolved (fast local session cache), this | ||
| /// completes on the very next microtask so no extra delay is introduced. | ||
| Future<void> _waitForAuthResolution() async { | ||
| if (authController.authStatus.value != AuthStatus.checking) return; | ||
|
|
||
| final completer = Completer<void>(); | ||
| final worker = ever<AuthStatus>( | ||
| authController.authStatus, | ||
| (AuthStatus status) { | ||
| if (status != AuthStatus.checking && !completer.isCompleted) { | ||
| completer.complete(); | ||
| } | ||
| }, | ||
| ); | ||
| await completer.future; | ||
| worker.dispose(); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
GetX ever() function behavior immediate callback current value
💡 Result:
ever(rx, callback) does not invoke callback immediately when you register it; it only runs when the observed Rx emits an event (i.e., a change/assignment that triggers a notify). (chornthorn.github.io)
One GetX nuance: by default, the first assignment after an Rx is created can still emit an event even if the value is the same (to handle cases like false.obs where a “load” might set false again). You can disable that with rx.firstRebuild = false;. (pub.dev)
If you need an “immediate callback with the current value”, call it yourself right after wiring ever, e.g. callback(rx.value), then rely on ever for subsequent emissions.
Citations:
- 1: https://chornthorn.github.io/getx-docs/docs/state/reactive-state/?utm_source=openai
- 2: https://pub.dev/packages/get/versions/2.14.1?utm_source=openai
🏁 Script executed:
# Check the actual implementation in the codebase
cd lib/views/screens && cat -n splash_screen.dart | sed -n '89,108p'Repository: AOSSIE-Org/Resonate
Length of output: 924
🏁 Script executed:
# Search for other ever() usages in codebase to understand patterns
rg "ever<" --type dart -B 2 -A 3Repository: AOSSIE-Org/Resonate
Length of output: 495
Close the race where authStatus flips before ever() attaches.
GetX's ever() only fires on value changes, not on the current value. If authStatus changes after the initial check (line 95) but before ever() subscribes (line 98), that state transition is missed and the completer may never resolve. Add an immediate post-subscribe check to handle this window:
🛠️ Safer completion with a post-subscribe check
final completer = Completer<void>();
final worker = ever<AuthStatus>(
authController.authStatus,
(AuthStatus status) {
if (status != AuthStatus.checking && !completer.isCompleted) {
completer.complete();
}
},
);
+ if (authController.authStatus.value != AuthStatus.checking &&
+ !completer.isCompleted) {
+ completer.complete();
+ }
await completer.future;
worker.dispose();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/views/screens/splash_screen.dart` around lines 89 - 108, The
_waitForAuthResolution() routine can miss a state flip that occurs between the
initial check and the ever() subscription; update it so that after attaching the
worker (ever on authController.authStatus) you immediately re-check
authController.authStatus and complete the Completer if it's no longer
AuthStatus.checking, then dispose the worker; ensure you still guard with
!completer.isCompleted to avoid double-completion.
Summary
Prevents login screen flicker when an authenticated user launches the app.
Closes #750.
Changes
Result
Authenticated users are now routed directly to the home screen without UI flicker.
Summary by CodeRabbit
Bug Fixes
New Features