firebase / flutterfire

🔥 A collection of Firebase plugins for Flutter apps.
https://firebase.google.com/docs/flutter/setup
BSD 3-Clause "New" or "Revised" License
8.63k stars 3.95k forks source link

🐛 [firebase_auth] Sign in with Apple on iOS doesn't trigger userChanges() if reauthenticate was cancelled before the sign in #12063

Open anasqadrei opened 9 months ago

anasqadrei commented 9 months ago

Bug report

Describe the bug

I'm trying to implement delete user functionality on my app. Delete requires that user reauthenticates before deletion. It works fine except in case user changes their mind last minute and cancel the reauthentication. The cancellation is also fine except when they sign out then try to sign in again. Sign out works fine but sign in doesn't do anything. The three methods authStateChanges, idTokenChanges and userChanges don't get triggered. That happens once. It works if the user try to sign in for the second time. That's a bad user experience anyway.

Steps to reproduce

Steps to reproduce the behavior:

  1. Create a flutter project, setup flutter fire with an iOS app, add firebase_core and firebase_auth dependencies and include the code below in main.dart
  2. Run the app on a physical iOS device
  3. Click on Sign in with Apple button and follow iOS instructions
  4. It should sign you in and displays your firebase UID, delete user button and a sign out button
  5. Sign out
  6. Sign in again
  7. Tap the delete button. It will require reauthentication
  8. Cancel the reauthentication with the X button. It will log a user cancellation error which is fine
  9. Sign out
  10. Sign in
  11. Nothing will ever happen
  12. Sign in again
  13. You will be signed in and shown your firebase UID

Expected behavior

Step 11 above should be a successful sign in and your firebase UID should be shown

Sample project

Here is a main.dart

import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

import 'firebase_options.dart';

late final FirebaseApp app;
late final FirebaseAuth auth;

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // We store the app and auth to make testing with a named instance easier.
  app = await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  auth = FirebaseAuth.instanceFor(app: app);

  runApp(const AuthExampleApp());
}

// App
class AuthExampleApp extends StatelessWidget {
  const AuthExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Test Firebase Apple Sign in App',
        home: Scaffold(body: SafeArea(
          child: LayoutBuilder(builder: (context, constraints) {
            return Center(
              child: StreamBuilder<User?>(
                // stream: auth.authStateChanges(),
                // stream: auth.idTokenChanges(),
                stream: auth.userChanges(),
                builder: (context, snapshot) {
                  if (snapshot.hasData) {
                    return const ProfilePage();
                  }
                  return const AuthGate();
                },
              ),
            );
          }),
        )));
  }
}

// Authentication
class AuthGate extends StatefulWidget {
  const AuthGate({super.key});

  static String? appleAuthorizationCode;

  @override
  State<AuthGate> createState() => _AuthGateState();
}

class _AuthGateState extends State<AuthGate> {
  @override
  Widget build(BuildContext context) {
    // Sign in button
    return ElevatedButton(
        onPressed: _signInWithApple, child: const Text('Sign in with Apple'));
  }

  Future<void> _signInWithApple() async {
    try {
      final appleProvider = AppleAuthProvider();
      appleProvider.addScope('email');

      final userCred = await auth.signInWithProvider(appleProvider);
      AuthGate.appleAuthorizationCode =
          userCred.additionalUserInfo?.authorizationCode;
    } on Exception catch (e) {
      print(e);
    }
  }
}

// Profile Page
class ProfilePage extends StatefulWidget {
  const ProfilePage({super.key});

  @override
  State<ProfilePage> createState() => _ProfilePageState();
}

class _ProfilePageState extends State<ProfilePage> {
  late User user;

  @override
  void initState() {
    user = auth.currentUser!;
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
        child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        // User UID
        Text(user.uid),
        // Delete user button
        ElevatedButton(
            onPressed: () async {
              try {
                await user.reauthenticateWithProvider(AppleAuthProvider());
                await user.delete();
              } on Exception catch (e) {
                print(e);
              }
            },
            child: const Text('Delete user')),
        // Sign out button
        ElevatedButton(
            onPressed: () async => await auth.signOut(),
            child: const Text('Sign out')),
      ],
    ));
  }
}

Additional context

I feel the problem is in reauthenticateWithProvider somewhere


Flutter doctor

Run flutter doctor and paste the output below:

Click To Expand ``` Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 3.13.8, on macOS 14.1.2 23B92 darwin-arm64, locale en-AU) [✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0) [✓] Xcode - develop for iOS and macOS (Xcode 15.1) [✓] Chrome - develop for the web [✓] Android Studio (version 2022.3) [✓] VS Code (version 1.85.1) [✓] Connected device (4 available) [✓] Network resources • No issues found! ```

Flutter dependencies

Run flutter pub deps -- --style=compact and paste the output below:

Click To Expand ``` Dart SDK 3.1.4 Flutter SDK 3.13.8 flutter_firebase_apple_signin 0.1.0 dependencies: - firebase_auth 4.15.3 [firebase_auth_platform_interface firebase_auth_web firebase_core firebase_core_platform_interface flutter meta] - firebase_core 2.24.2 [firebase_core_platform_interface firebase_core_web flutter meta] - flutter 0.0.0 [characters collection material_color_utilities meta vector_math web sky_engine] dev dependencies: - flutter_lints 2.0.3 [lints] - flutter_test 0.0.0 [flutter test_api matcher path fake_async clock stack_trace vector_math async boolean_selector characters collection material_color_utilities meta source_span stream_channel string_scanner term_glyph web] transitive dependencies: - _flutterfire_internals 1.3.16 [collection firebase_core firebase_core_platform_interface flutter meta] - async 2.11.0 [collection meta] - boolean_selector 2.1.1 [source_span string_scanner] - characters 1.3.0 - clock 1.1.1 - collection 1.17.2 - fake_async 1.3.1 [clock collection] - firebase_auth_platform_interface 7.0.9 [_flutterfire_internals collection firebase_core flutter meta plugin_platform_interface] - firebase_auth_web 5.8.12 [firebase_auth_platform_interface firebase_core firebase_core_web flutter flutter_web_plugins http_parser js meta] - firebase_core_platform_interface 5.0.0 [collection flutter flutter_test meta plugin_platform_interface] - firebase_core_web 2.10.0 [firebase_core_platform_interface flutter flutter_web_plugins js meta] - flutter_web_plugins 0.0.0 [flutter characters collection material_color_utilities meta vector_math web] - http_parser 4.0.2 [collection source_span string_scanner typed_data] - js 0.6.7 [meta] - lints 2.1.1 - matcher 0.12.16 [async meta stack_trace term_glyph test_api] - material_color_utilities 0.5.0 [collection] - meta 1.9.1 - path 1.8.3 - plugin_platform_interface 2.1.7 [meta] - sky_engine 0.0.99 - source_span 1.10.0 [collection path term_glyph] - stack_trace 1.11.0 [path] - stream_channel 2.1.1 [async] - string_scanner 1.2.0 [source_span] - term_glyph 1.2.1 - test_api 0.6.0 [async boolean_selector collection meta source_span stack_trace stream_channel string_scanner term_glyph] - typed_data 1.3.2 [collection] - vector_math 2.1.4 - web 0.1.4-beta ```

danagbemava-nc commented 9 months ago

Reproducible using the code sample and the steps provided above.

See the recording below. The video ends at step 11 because the it doesn't take more than 5 seconds for the UID to show up when the user is authenticated, but it does in this case. If I do continue to step 13 it doesn't take long for the data to show up.

https://github.com/firebase/flutterfire/assets/88313112/d8eda6ee-450a-46d8-bb9e-8d7c51c2268a

PS: I'm unable to confirm whether this reproduces on android as well, at the moment.

cc @russellwheatley