Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

google_sign_in and flutter_redux #14

Closed
JMichaelYang opened this issue Mar 4, 2018 · 10 comments
Closed

google_sign_in and flutter_redux #14

JMichaelYang opened this issue Mar 4, 2018 · 10 comments

Comments

@JMichaelYang
Copy link

JMichaelYang commented Mar 4, 2018

Hi,

First of all, thank you guys for writing such a great library; I am still learning Redux but this package has been a wonderful first introduction.

I am trying to use the google_sign_in package in order to sign users into my app, and it worked fine before I moved to Redux. However, it now throws an error any time I try to sign in to the app. The sign in box that normally pops up now does not, so I think that it might have to do with trying to render the sign in box from Middleware, which doesn't really have a context (?)

Here is the associated code:

SignIn

import 'dart:async';

import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:kickit/data/injectors.dart';
import 'package:kickit/data/profile_store.dart';
import 'package:kickit/database/networking.dart';
import 'package:kickit/database/profile_package.dart';

/// A collection of methods that handle the signing in and signing out of the
/// current user.
abstract class ISignIn {
  /// Signs in the current user, returning a [ProfilePackage] containing the
  /// current user's data.
  Future<ProfilePackage> signIn();

  /// Signs the current user out of Firebase and Google.
  Future<Null> signOut();

  /// Signs the current user out of Firebase and Google and deletes their profile
  /// from the database.
  Future<Null> signOutAndDelete(String uid);
}

/// Performs sign in operations with real network data.
class SignIn extends ISignIn {
  final IProfileStore store;

  SignIn() : store = new ProfileInjector().profileLoader;

  Future<ProfilePackage> signIn() async {
    // Check to see if there is currently a signed in user.
    final GoogleSignInAccount account = googleSignIn.currentUser;

    // Try to sign in without prompting the user.
    if (account == null) {
      account == await googleSignIn.signInSilently();
    }

    // If this doesn't work, prompt the user to sign in.
    if (account == null) {
      account == await googleSignIn.signIn();
    }

    // If this doesn't work, throw an error that should tell the user that
    // they must sign in.
    if (account == null) {
      throw new StateError("The user must log in.");
    }

    final GoogleSignInAuthentication auth = await account.authentication;

    // Authenticate the user with firebase.
    final FirebaseUser user = await firebaseAuth.signInWithGoogle(
        idToken: auth.idToken, accessToken: auth.accessToken);

    if (user == null || user.isAnonymous) {
      throw new StateError("Log in error.");
    }

    final ProfilePackage package = await store.loadProfile(user.uid);

    // No user was found, so create a new one and save it to the database.
    if (package == null) {
      ProfilePackage newPackage =
          new ProfilePackage.fromGoogleSignIn(account, user);
      await store.saveProfile(newPackage);
      return newPackage;
    } else {
      return package;
    }
  }

  Future<Null> signOut() async {
    await googleSignIn.signOut();
    await firebaseAuth.signOut();
  }

  Future<Null> signOutAndDelete(String uid) async {
    await store.deleteProfile(uid);
    await signOut();
  }
}

/// Performs sign in operations with mock data.
class MockSignIn implements ISignIn {
  @override
  Future<ProfilePackage> signIn() {
    return new Future.delayed(delayMedium,
        () => new ProfilePackage("_", "Jaewon Yang", "_", "Test profile."));
  }

  @override
  Future<Null> signOut() {
    return null;
  }

  @override
  Future<Null> signOutAndDelete(String uid) {
    return null;
  }
}

Middleware

import 'package:kickit/actions/sign_in_actions.dart';
import 'package:kickit/data/injectors.dart';
import 'package:kickit/data/sign_in.dart';
import 'package:kickit/models/app_state.dart';
import 'package:kickit/models/profile.dart';
import 'package:kickit/utils/values/internal_strings.dart';
import 'package:kickit/utils/values/keys.dart';
import 'package:redux/redux.dart';

/// Creates all the [Middleware] associated with signing the user in and out.
List<Middleware<AppState>> createSignInMiddleware() {
  final ISignIn signIn = new SignInInjector().signIn;

  final Middleware<AppState> mSignIn = _createSignIn(signIn);
  final Middleware<AppState> mSignOut = _createSignOut(signIn);
  final Middleware<AppState> mSignOutAndDelete =
      _createSignOutAndDelete(signIn);

  return combineTypedMiddleware(
    [
      new MiddlewareBinding<AppState, SignInAction>(mSignIn),
      new MiddlewareBinding<AppState, SignOutAction>(mSignOut),
      new MiddlewareBinding<AppState, SignOutAndDeleteAction>(
          mSignOutAndDelete),
    ],
  );
}

/// Creates the [Middleware] associated with a sign in attempt.
Middleware<AppState> _createSignIn(ISignIn signIn) {
  return (Store<AppState> store, action, NextDispatcher next) {
    signIn.signIn().then(
      (user) {
        store.dispatch(
          new SignedInAction(
            new Profile.fromPackage(user),
          ),
        );
        Keys.navigatorKey.currentState
            .pushReplacementNamed(InternalStrings.mainScreenRoute);
      },
    ).catchError((_) => store.dispatch(new FailedSignInAction()));

    next(action);
  };
}

/// Creates the [Middleware] associated with a sign out attempt.
Middleware<AppState> _createSignOut(ISignIn signIn) {
  return (Store<AppState> store, action, NextDispatcher next) {
    signIn.signOut().then(
      (_) {
        store.dispatch(new SignedOutAction());
        Keys.navigatorKey.currentState
            .pushReplacementNamed(InternalStrings.splashScreenRoute);
      },
    );

    next(action);
  };
}

/// Creates the [Middleware] associated with a sign out and delete attempt.
Middleware<AppState> _createSignOutAndDelete(ISignIn signIn) {
  return (Store<AppState> store, action, NextDispatcher next) {
    signIn.signOutAndDelete(store.state.profile.uid).then(
      (_) {
        store.dispatch(new SignedOutAction());
        Keys.navigatorKey.currentState
            .pushReplacementNamed(InternalStrings.splashScreenRoute);
      },
    );

    next(action);
  };
}

Any help would be appreciated!

@brianegan
Copy link
Owner

brianegan commented Mar 6, 2018

Hey, ermagerd, so sorry I missed this notification! I'll take a look this evening and report back :)

Thanks for the kind words and submitting this issue!

@JMichaelYang
Copy link
Author

No problem at all, thanks for being available to answer these questions!

@brianegan
Copy link
Owner

brianegan commented Mar 7, 2018

Hey there! Reading through the code, I don't see anything obviously wrong... You mentioned an error: Could you describe what error you're running into? Are you getting an error from the Flutter framework itself or from Firebase / parsing?

Just to be sure: You're providing the navigatorKey to your MaterialApp as well, correct? Once that's in place, it should work without a context unless you're calling this action before the MaterialApp is in place and has constructed the NavigatorState.

@brianegan
Copy link
Owner

Coded up a quick example showing navigation with a Middleware if this helps: https://github.com/brianegan/flutter_redux/blob/simple-navigation-example/example/lib/main.dart#L20

@JMichaelYang
Copy link
Author

JMichaelYang commented Mar 7, 2018

Hi, thanks for the quick response!

The issue is that the google_sign_in plugin is unable to run its signIn() method found in my signIn() method in the SignIn class. The method simply returns null without prompting the user to sign in, which it was able to do before I switched to Redux. I don't know if this is a problem that is under the scope of your plugin or not, but I figured I might as well try and ask.

The prompt that google_sign_in should provide is not something that I created myself, before it would just overlay over whatever I currently had on screen without the need for a Navigator or BuildContext. To be honest I don't really know how it worked.

Something that could possibly help: do you know of any examples where someone has successfully used the google_sign_in plugin in conjunction with your flutter_redux plugin?

Thanks again.

@brianegan
Copy link
Owner

brianegan commented Mar 7, 2018

No worries! I work on both redux and this plugin, so happy to cover both scopes :)

google_sign_in shouldn't need context or anything like that. Generally, it will open a native view on Android or iOS rather than displaying flutter widgets.

I tried a simplified test case to see if google sign in works within Middleware. When you tap the Button, it should launch google sign in from within the middleware. This worked from my end, does it work for you?

Hrm, there are some projects I know of that use them in combination, but they're private repos or companies :(

import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:redux/redux.dart';

void main() => runApp(new MyApp());

final _googleSignIn = new GoogleSignIn(
  scopes: [
    'email',
    'https://www.googleapis.com/auth/contacts.readonly',
  ],
);

void signInMiddleware(
  Store<String> store,
  dynamic action,
  NextDispatcher next,
) {
  next(action);

  try {
    _googleSignIn.signIn();
  } catch (error) {
    print(error);
  }
}

final store = new Store<String>((state, action) => action.toString(),
    middleware: [signInMiddleware]);

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              'You have pushed the button this many times:',
            ),
            new Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: () {
          store.dispatch("Sign in");
        },
        tooltip: 'Increment',
        child: new Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

@JMichaelYang
Copy link
Author

I was somehow able to fix the problem by making my GoogleSignIn instance a non-static field within the SignIn class rather than a static field in a different class (do you have any insight into why this worked?).

I have, however, run into another problem. It seems that calling the googleSignIn.signIn() method works, but for some reason it does not return the signed in account. I must call googleSignIn.signInSilently() afterwards in order to get a reference to the signed in account.

I found this out because I had to press my "sign in" button twice. The first time gave me the correct prompt, but set the account to null which then threw an error (which I caught). The second time pressing the button worked with a silent sign in (no prompt).

Any idea why this might be happening? Attached is the updated relevant code:

SignIn

/// Performs sign in operations with real network data.
class SignIn extends ISignIn {
  final IProfileStore store;
  final GoogleSignIn googleSignIn = new GoogleSignIn(scopes: [
    "email",
    "https://www.googleapis.com/auth/contacts.readonly",
  ]);

  SignIn() : store = new ProfileInjector().profileLoader;

  Future<ProfilePackage> signIn() async {
    // Check to see if there is currently a signed in user.
    final GoogleSignInAccount account = googleSignIn.currentUser;

    // Try to sign in without prompting the user.
    if (account == null) {
      account == await googleSignIn.signInSilently();
    }

    // If this doesn't work, prompt the user to sign in.
    if (account == null) {
      account == await googleSignIn.signIn();
    }

    // If this doesn't work, throw an error that should tell the user that
    // they must sign in.
    if (account == null) {
      throw new StateError("The user must log in.");
    }

    final GoogleSignInAuthentication auth = await account.authentication;

    // Authenticate the user with firebase.
    final FirebaseUser user = await Networking.firebaseAuth
        .signInWithGoogle(idToken: auth.idToken, accessToken: auth.accessToken);

    if (user == null || user.isAnonymous) {
      throw new StateError("Log in error.");
    }

    final ProfilePackage package = await store.loadProfile(user.uid);

    // No user was found, so create a new one and save it to the database.
    if (package == null) {
      ProfilePackage newPackage =
          new ProfilePackage.fromGoogleSignIn(account, user);
      await store.saveProfile(newPackage);
      return newPackage;
    } else {
      return package;
    }
  }

  Future<Null> signOut() async {
    await googleSignIn.signOut();
    await Networking.firebaseAuth.signOut();
  }

  Future<Null> signOutAndDelete(String uid) async {
    await store.deleteProfile(uid);
    await signOut();
  }
}

Middleware for good measure

/// Creates the [Middleware] associated with a sign in attempt.
Middleware<AppState> _createSignIn(ISignIn signIn) {
  return (Store<AppState> store, action, NextDispatcher next) {
    signIn.signIn().then(
      (user) {
        store.dispatch(
          new SignedInAction(
            new Profile.fromPackage(user),
          ),
        );
        Keys.navigatorKey.currentState
            .pushReplacementNamed(InternalStrings.mainScreenRoute);
      },
    ).catchError(
      (_) {
        store.dispatch(new FailedSignInAction());
      },
    );

    next(action);
  };
}

@brianegan
Copy link
Owner

brianegan commented Mar 8, 2018

"I was somehow able to fix the problem by making my GoogleSignIn instance a non-static field within the SignIn class rather than a static field in a different class (do you have any insight into why this worked?)."

Dart is a bit weird about statics... if you use them in one file it will create the object, then if you use them in another file after importing it will recreate the object :( Don't ask me why :(

Because of that, I generally avoid statics unless I'm hacking something together quickly for an idea.

"I have, however, run into another problem. It seems that calling the googleSignIn.signIn() method works, but for some reason it does not return the signed in account"

Unfortunately, I'm not sure why that would be the case. Did it work before you introduced Redux? If not, you'd probably have to file a bug against the Flutter repo.

@JMichaelYang
Copy link
Author

Dart is a bit weird about statics... if you use them in one file it will create the object, then if you use them in another file after importing it will recreate the object :( Don't ask me why :(

Wow, that's good to know, thank you for that insight. I'll be sure to rearrange the rest of my code to avoid them as much as possible.

Unfortunately, I'm not sure why that would be the case. Did it work before you introduced Redux? If not, you'd probably have to file a bug against the Flutter repo.

It did work before, I'll go ahead and mess around with it to see if I can't figure out why it's broken.

Thank you again for your help, you're a lifesaver.

I will go ahead and mark this as closed.

@brianegan
Copy link
Owner

Sure thing, thanks for the Qs! Best of luck with the app :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants