DEV Community

ROCI
ROCI

Posted on

Why My Flutter App Was Spamming Google Sign-In Prompts (And How I Fixed It)

As an indie developer building an offline-first task and calendar app (ROCIs Tasks) using Flutter with native Android Kotlin widgets, nothing gives you sudden micro-heart attacks quite like user bug reports about authentication loops.

Recently, I hit a nasty regression: users were getting hammered with multiple Google sign-in prompts. Tap the Google Tasks sync toggle? Prompt. Tap 'Create Task'? Another prompt. It felt less like a productivity app and more like an aggressive security guard.

Here is the breakdown of why this happened and how I fixed it.

The Mystery: Death by a Thousand Prompts

Users trying to sync their tasks were seeing back-to-back sign-in overlays. Even worse, generic email/password users were occasionally getting swept into the credential flow. That is a textbook way to tank your app's retention on Google Play.

Root Cause Analysis

Digging into the codebase revealed three distinct culprits conspiring against my users:

  1. Decoupled Auth in Google Sign-In v7: Authentication (ID token) and Authorization (Access token/scopes) are now separate steps. My code was calling attemptLightweightAuthentication() inside getGoogleAccessToken() whenever the cache missed. On Android, this doesn't always fail silently—it can trigger a Credential Manager overlay chooser.
  2. The Amnesiac In-Memory Session: The authenticated GoogleSignInAccount object was never stored in memory. Every single token request started completely from scratch.
  3. Redundant Scopes: My mobile build was requesting the Google Calendar scope (https://www.googleapis.com/auth/calendar). But on mobile, ROCIs Tasks uses device_calendar for native OS calendar integration. Asking for the web calendar scope via Google Sign-In triggered redundant, scary permission consent prompts.

The Solution & Architecture

To clean this up, I refactored the AuthService with a strict caching and platform-aware scope strategy.

First, I introduced an in-memory session cache and hooked into the authentication event stream on startup:

class AuthService {
  GoogleSignInAccount? _googleUser;

  Future<void> initialize() async {
    _googleSignIn.authenticationEvents.listen((event) {
      if (event is GoogleSignInAccountEvent) {
        _googleUser = event.user;
      }
    });
    await _restoreGoogleSignInSession();
  }

  // Smart startup restoration - only for users who actually linked Google Tasks
  Future<void> _restoreGoogleSignInSession() async {
    if (_userHasLinkedGoogleTasks) {
      _googleUser = await _googleSignIn.attemptLightweightAuthentication();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Next, I split the requested scopes by platform. Web needs the Calendar API; mobile handles it natively via the OS device calendar:

List<String> getScopesForPlatform() {
  if (kIsWeb) {
    return [
      'email',
      'profile',
      'https://www.googleapis.com/auth/tasks',
      'https://www.googleapis.com/auth/calendar',
    ];
  } else {
    // Mobile uses device_calendar for native OS integration
    return [
      'email',
      'profile',
      'https://www.googleapis.com/auth/tasks',
    ];
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, I unified SharedPreferences token caching across platforms so that access tokens survive app restarts without forcing unnecessary network calls.

Key Lessons for Other Devs

  • Never assume silent auth is silent: Modern Android credential managers can surface UI when you least expect it. Cache your user state locally.
  • Audit your OAuth scopes: Don't request web-only API scopes on mobile if your app handles things natively via platform channels.
  • Respect your users' intent: Generic email/password accounts should never touch Google OAuth logic on startup.

Wrapping Up

Building ROCIs Tasks as a solo indie dev means obsessing over details like smooth auth flows so the app feels as native and polished as possible. If you want to check out an offline-first task manager with native Kotlin widgets built in Flutter, take a look at ROCIs Tasks on Google Play or visit tasks.rocisapps.com. Happy coding!

Top comments (0)