|
| 1 | +import 'dart:async'; |
| 2 | + |
| 3 | +import 'package:flutter/foundation.dart'; |
| 4 | +import 'package:google_sign_in/google_sign_in.dart'; |
| 5 | + |
| 6 | +/// Owns the one and only `GoogleSignIn.instance.initialize()` call in the |
| 7 | +/// process. |
| 8 | +/// |
| 9 | +/// `GoogleSignIn.instance` is a process-wide singleton, and google_sign_in v7 |
| 10 | +/// documents that `initialize()` must be called *exactly once* — but the Dart |
| 11 | +/// wrapper does not enforce it. The enforcement lives in the web |
| 12 | +/// implementation, whose `init()` throws |
| 13 | +/// `StateError('init() has already been called ...')` on the second call. The |
| 14 | +/// Android/iOS implementations swallow a repeat init silently, so a second |
| 15 | +/// `initialize()` is a latent bug on every platform and a hard failure only on |
| 16 | +/// web. |
| 17 | +/// |
| 18 | +/// The app has two natural doors into Google auth — the web shell's sign-in |
| 19 | +/// screen ([WebSession.initGoogleWeb]) and the lazy scope request in |
| 20 | +/// [AuthService] — and each used to keep its own private `bool` guard over the |
| 21 | +/// shared singleton. A new web user opened both doors in one page session: |
| 22 | +/// signing in with Google initialized it once, then publishing a website whose |
| 23 | +/// contact form is backed by a Google Form asked for the Drive scope, which |
| 24 | +/// initialized it again and threw. Returning users never hit it, because a |
| 25 | +/// session restored from storage skips the sign-in screen entirely and leaves |
| 26 | +/// the scope request as the *first* initialize. |
| 27 | +/// |
| 28 | +/// Routing both doors through this class turns "initialize exactly once" from a |
| 29 | +/// convention that two files have to agree on into an invariant that holds by |
| 30 | +/// construction. |
| 31 | +class GoogleSignInBootstrap { |
| 32 | + GoogleSignInBootstrap._(); |
| 33 | + |
| 34 | + static bool _done = false; |
| 35 | + static Future<void>? _inFlight; |
| 36 | + static String? _usedClientId; |
| 37 | + static String? _usedServerClientId; |
| 38 | + |
| 39 | + /// Whether `GoogleSignIn.instance` has been initialized. |
| 40 | + static bool get isInitialized => _done; |
| 41 | + |
| 42 | + /// Initializes `GoogleSignIn.instance` unless that has already happened. |
| 43 | + /// |
| 44 | + /// Safe to call from anywhere, any number of times, including concurrently: |
| 45 | + /// the first caller does the work and every other caller awaits that same |
| 46 | + /// future. First call wins for [clientId]/[serverClientId] — the plugin has |
| 47 | + /// no reconfigure API, so a later, different value could not be applied even |
| 48 | + /// if we wanted to. |
| 49 | + static Future<void> ensureInitialized({ |
| 50 | + String? clientId, |
| 51 | + String? serverClientId, |
| 52 | + }) { |
| 53 | + if (_done) { |
| 54 | + assert(() { |
| 55 | + if (_usedClientId != clientId || |
| 56 | + _usedServerClientId != serverClientId) { |
| 57 | + debugPrint( |
| 58 | + 'GoogleSignInBootstrap: ignoring a repeat initialize() with ' |
| 59 | + 'different parameters (clientId=$clientId, ' |
| 60 | + 'serverClientId=$serverClientId); the singleton is already ' |
| 61 | + 'configured with clientId=$_usedClientId, ' |
| 62 | + 'serverClientId=$_usedServerClientId. Callers on one platform ' |
| 63 | + 'are expected to agree on these.', |
| 64 | + ); |
| 65 | + } |
| 66 | + return true; |
| 67 | + }()); |
| 68 | + return Future<void>.value(); |
| 69 | + } |
| 70 | + return _inFlight ??= _initialize(clientId, serverClientId); |
| 71 | + } |
| 72 | + |
| 73 | + static Future<void> _initialize( |
| 74 | + String? clientId, |
| 75 | + String? serverClientId, |
| 76 | + ) async { |
| 77 | + try { |
| 78 | + await GoogleSignIn.instance.initialize( |
| 79 | + clientId: clientId, |
| 80 | + serverClientId: serverClientId, |
| 81 | + ); |
| 82 | + } on StateError catch (e) { |
| 83 | + // The double-init guard is the only StateError `initialize()` raises, so |
| 84 | + // arriving here means the singleton is already configured — by a code |
| 85 | + // path this class does not know about, or by the previous run of a hot |
| 86 | + // restart, which resets Dart state but not the GIS SDK already loaded |
| 87 | + // into the page. Either way it is initialized, which is all the caller |
| 88 | + // asked for. Note this is deliberately matched by TYPE, not by message: |
| 89 | + // the wording of that error changes between plugin versions. |
| 90 | + debugPrint('GoogleSignInBootstrap: initialize() reports an existing ' |
| 91 | + 'initialization; continuing. ($e)'); |
| 92 | + } catch (e) { |
| 93 | + // Something transient — e.g. the GIS script failed to load. Drop the |
| 94 | + // cached future so the next caller retries instead of inheriting the |
| 95 | + // failure forever. |
| 96 | + _inFlight = null; |
| 97 | + rethrow; |
| 98 | + } |
| 99 | + _done = true; |
| 100 | + _usedClientId = clientId; |
| 101 | + _usedServerClientId = serverClientId; |
| 102 | + } |
| 103 | + |
| 104 | + /// Test-only: forget that initialization happened. |
| 105 | + @visibleForTesting |
| 106 | + static void resetForTest() { |
| 107 | + _done = false; |
| 108 | + _inFlight = null; |
| 109 | + _usedClientId = null; |
| 110 | + _usedServerClientId = null; |
| 111 | + } |
| 112 | +} |
0 commit comments