Skip to content

Commit

Permalink
Migrate to the new firebase_core (#120)
Browse files Browse the repository at this point in the history
* Migrate to the new firebase_core

* Only show profile card and favourite websites card if user is authenticated

* Fix delete() being called twice when deleting account

* Improve position of progress indicator on signup screen

* Stop setting firebase displayName

We weren't using it anywhere anyway.

* Fix "Something went wrong" toast on signup

* Check if user data exists in website provider

* Fix data == null checks

Data is now a function, so we need to check if data() is null

* Fix formatting

* Fix integration tests failing because Firebase.initializeApp() was not called

* Fix failing settings test when account is initially not verified

* Fix request permissions page not opening from dialog

It was probably a weird race condition between the Navigator and the
async isVerified call.

* Fix last failing integration test
  • Loading branch information
IoanaAlexandru authored Feb 28, 2021
1 parent 3381c3f commit ac3bc79
Show file tree
Hide file tree
Showing 28 changed files with 443 additions and 386 deletions.
1 change: 1 addition & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ analyzer:
exclude:
- build/**
- lib/generated/**
- test/firebase_mock.dart

linter:
rules:
Expand Down
125 changes: 44 additions & 81 deletions lib/authentication/service/auth_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@ import 'package:acs_upb_mobile/resources/storage/storage_provider.dart';
import 'package:acs_upb_mobile/resources/validator.dart';
import 'package:acs_upb_mobile/widgets/toast.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_auth/firebase_auth.dart' hide User;
import 'package:firebase_auth/firebase_auth.dart' as firebase_auth show User;
import 'package:flutter/material.dart';

extension DatabaseUser on User {
static User fromSnap(DocumentSnapshot snap) {
final data = snap.data();
return User(
uid: snap.documentID,
firstName: snap.data['name']['first'],
lastName: snap.data['name']['last'],
classes: List.from(snap.data['class'] ?? []),
permissionLevel: snap.data['permissionLevel']);
uid: snap.id,
firstName: data['name']['first'],
lastName: data['name']['last'],
classes: List.from(data['class'] ?? []),
permissionLevel: data['permissionLevel']);
}

Map<String, dynamic> toData() {
Expand All @@ -31,19 +33,18 @@ extension DatabaseUser on User {

class AuthProvider with ChangeNotifier {
AuthProvider() {
_userAuthSub = FirebaseAuth.instance.onAuthStateChanged.listen((newUser) {
print('AuthProvider - FirebaseAuth - onAuthStateChanged - $newUser');
_firebaseUser = newUser;
_userAuthSub = FirebaseAuth.instance.authStateChanges().listen((newUser) {
print('AuthProvider - FirebaseAuth - authStateChanges - $newUser');
_currentUser = null;
_fetchUser();
notifyListeners();
}, onError: (dynamic e) {
print('AuthProvider - FirebaseAuth - onAuthStateChanged - $e');
print('AuthProvider - FirebaseAuth - authStateChanges - $e');
});
}

FirebaseUser _firebaseUser;
StreamSubscription<FirebaseUser> _userAuthSub;
firebase_auth.User get _firebaseUser => FirebaseAuth.instance.currentUser;
StreamSubscription<firebase_auth.User> _userAuthSub;
User _currentUser;

@override
Expand Down Expand Up @@ -98,45 +99,23 @@ class AuthProvider with ChangeNotifier {

var isAnonymousUser = true;
for (final info in _firebaseUser.providerData) {
if (info.providerId == 'facebook.com' ||
info.providerId == 'google.com' ||
info.providerId == 'password') {
if (info.providerId == 'password') {
isAnonymousUser = false;
break;
}
}
return isAnonymousUser;
}

/// Check the memory cache to see if there is a user authenticated
bool get isVerifiedFromCache {
/// Check if the user verified their e-mail
Future<bool> get isVerified async {
assert(_firebaseUser != null);
return !isAnonymous && _firebaseUser.isEmailVerified;
}

/// Check the network to see if there is a user authenticated
Future<bool> get isVerifiedFromService async {
if (isAnonymous) {
return false;
}

await _firebaseUser.reload();
_firebaseUser = await FirebaseAuth.instance.currentUser();
return _firebaseUser.isEmailVerified;
}

/// Check the memory cache to see if there is a user authenticated
bool get isAuthenticatedFromCache {
return _firebaseUser != null;
return !isAnonymous && _firebaseUser.emailVerified;
}

/// Check the filesystem to see if there is a user authenticated.
///
/// This method is `async` and should only be necessary on app startup, since
/// for everything else, the [AuthProvider] will notify its listeners and
/// update the cache if the authentication state changes.
Future<bool> get isAuthenticatedFromService async {
_firebaseUser = await FirebaseAuth.instance.currentUser();
/// Check if there is a user authenticated
bool get isAuthenticated {
return _firebaseUser != null;
}

Expand All @@ -146,7 +125,7 @@ class AuthProvider with ChangeNotifier {

String get email => _firebaseUser.email;

bool isOldFormat(Map<String, dynamic> userData) =>
bool _isOldFormat(Map<String, dynamic> userData) =>
userData['class'] != null && userData['class'] is Map;

/// Change the `class` of the user data in Firebase to the new format.
Expand All @@ -155,32 +134,33 @@ class AuthProvider with ChangeNotifier {
/// where the key is the name of the level in the filter tree.
/// In the new format, the class is simply a `List<String>` that contains the
/// name of the nodes.
Future<void> migrateToNewClassFormat(Map<String, dynamic> userData) async {
Future<void> _migrateToNewClassFormat(Map<String, dynamic> userData) async {
final classes = ['degree', 'domain', 'year', 'series', 'group', 'subgroup']
.map((key) => userData['class'][key].toString())
.where((s) => s != 'null')
.toList();

userData['class'] = classes;

await Firestore.instance
await FirebaseFirestore.instance
.collection('users')
.document(_firebaseUser.uid)
.updateData(userData);
.doc(_firebaseUser.uid)
.update(userData);
}

Future<User> _fetchUser() async {
if (isAnonymous) {
return null;
}
final snapshot = await Firestore.instance
final snapshot = await FirebaseFirestore.instance
.collection('users')
.document(_firebaseUser.uid)
.doc(_firebaseUser.uid)
.get();
if (snapshot.data == null) return null;
final data = snapshot.data();
if (data == null) return null;

if (isOldFormat(snapshot.data)) {
await migrateToNewClassFormat(snapshot.data);
if (_isOldFormat(data)) {
await _migrateToNewClassFormat(data);
}

_currentUser = DatabaseUser.fromSnap(snapshot);
Expand Down Expand Up @@ -237,7 +217,7 @@ class AuthProvider with ChangeNotifier {
}

final List<String> providers = await FirebaseAuth.instance
.fetchSignInMethodsForEmail(email: email)
.fetchSignInMethodsForEmail(email)
.catchError((dynamic e) {
_errorHandler(e, context);
return null;
Expand Down Expand Up @@ -265,19 +245,18 @@ class AuthProvider with ChangeNotifier {
}

Future<void> signOut() async {
await FirebaseAuth.instance.signOut();
if (isAnonymous) {
if (isAuthenticated && isAnonymous) {
await delete();
}
await FirebaseAuth.instance.signOut();
}

Future<bool> delete({BuildContext context}) async {
_firebaseUser ??= await FirebaseAuth.instance.currentUser();
assert(_firebaseUser != null);

try {
final DocumentReference ref =
Firestore.instance.collection('users').document(_firebaseUser.uid);
FirebaseFirestore.instance.collection('users').doc(_firebaseUser.uid);
await ref.delete();

await _firebaseUser.delete();
Expand All @@ -296,8 +275,7 @@ class AuthProvider with ChangeNotifier {
{String email, BuildContext context}) async {
List<String> providers = [];
try {
providers =
await FirebaseAuth.instance.fetchSignInMethodsForEmail(email: email);
providers = await FirebaseAuth.instance.fetchSignInMethodsForEmail(email);
} catch (e) {
_errorHandler(e, context);
return false;
Expand All @@ -312,8 +290,7 @@ class AuthProvider with ChangeNotifier {
Future<bool> canSignUpWithEmail({String email, BuildContext context}) async {
List<String> providers = [];
try {
providers =
await FirebaseAuth.instance.fetchSignInMethodsForEmail(email: email);
providers = await FirebaseAuth.instance.fetchSignInMethodsForEmail(email);
} catch (e) {
_errorHandler(e, context);
return false;
Expand Down Expand Up @@ -378,33 +355,24 @@ class AuthProvider with ChangeNotifier {
}

// Create user
final AuthResult res = await FirebaseAuth.instance
final UserCredential credential = await FirebaseAuth.instance
.createUserWithEmailAndPassword(email: email, password: password);

// Update display name
final userUpdateInfo = UserUpdateInfo()
..displayName = '$firstName $lastName';
await res.user.updateProfile(userUpdateInfo);

// Update user with updated info
await _firebaseUser?.reload();
_firebaseUser = await FirebaseAuth.instance.currentUser();

// Create document in 'users'
_currentUser = User(
uid: res.user.uid,
uid: credential.user.uid,
firstName: firstName,
lastName: lastName,
classes: classes,
);

final DocumentReference ref =
Firestore.instance.collection('users').document(_currentUser.uid);
await ref.setData(_currentUser.toData());
FirebaseFirestore.instance.collection('users').doc(_currentUser.uid);
await ref.set(_currentUser.toData());

// Try to set the default from the user data
if (_currentUser.classes != null) {
await ref.updateData({'filter_nodes': _currentUser.classes});
await ref.update({'filter_nodes': _currentUser.classes});
}
// Send verification e-mail
await _firebaseUser.sendEmailVerification();
Expand Down Expand Up @@ -453,15 +421,10 @@ class AuthProvider with ChangeNotifier {
..lastName = lastName
..classes = classes;

await Firestore.instance
await FirebaseFirestore.instance
.collection('users')
.document(_currentUser.uid)
.updateData(_currentUser.toData());

// Update display name
final userUpdateInfo = UserUpdateInfo()
..displayName = '$firstName $lastName';
await _firebaseUser.updateProfile(userUpdateInfo);
.doc(_currentUser.uid)
.update(_currentUser.toData());

notifyListeners();
return true;
Expand Down
24 changes: 17 additions & 7 deletions lib/authentication/view/edit_profile_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'dart:ui';

import 'package:acs_upb_mobile/authentication/model/user.dart';
import 'package:acs_upb_mobile/authentication/service/auth_provider.dart';

import 'package:acs_upb_mobile/generated/l10n.dart';
import 'package:acs_upb_mobile/pages/filter/view/filter_dropdown.dart';
import 'package:acs_upb_mobile/resources/storage/storage_provider.dart';
Expand All @@ -18,9 +17,9 @@ import 'package:acs_upb_mobile/widgets/toast.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image/image.dart' as im;
import 'package:preferences/preference_title.dart';
import 'package:provider/provider.dart';
import 'package:image/image.dart' as im;

class EditProfilePage extends StatefulWidget {
const EditProfilePage({Key key}) : super(key: key);
Expand All @@ -40,10 +39,15 @@ class _EditProfilePageState extends State<EditProfilePage> {
Uint8List uploadedImage;
ImageProvider imageWidget;

// Whether the user verified their email; this can be true, false or null if
// the async check hasn't completed yet.
bool isVerified;

@override
void initState() {
super.initState();
final authProvider = Provider.of<AuthProvider>(context, listen: false);
authProvider.isVerified.then((value) => setState(() => isVerified = value));
authProvider.getProfilePictureURL(context: context).then((value) =>
setState(() => {if (value != null) imageWidget = NetworkImage(value)}));
}
Expand Down Expand Up @@ -259,10 +263,16 @@ class _EditProfilePageState extends State<EditProfilePage> {
final authProvider = Provider.of<AuthProvider>(context);
final emailDomain = S.of(context).stringEmailDomain;
final User user = authProvider.currentUserFromCache;

if (user == null) {
// TODO(AdrianMargineanu): Show error page if user is not authenticated
return Container();
}

lastNameController.text = user.lastName;
firstNameController.text = user.firstName;
Uint8List imageAsPNG;
if (!authProvider.isVerifiedFromCache) {
if (isVerified == false) {
emailController.text = authProvider.email.split('@')[0];
}
final path = user.classes;
Expand All @@ -284,7 +294,7 @@ class _EditProfilePageState extends State<EditProfilePage> {

if (formKey.currentState.validate()) {
bool result = true;
if (!authProvider.isVerifiedFromCache &&
if (isVerified == false &&
emailController.text + emailDomain != authProvider.email) {
await showDialog(
context: context,
Expand Down Expand Up @@ -362,7 +372,7 @@ class _EditProfilePageState extends State<EditProfilePage> {
return null;
},
),
if (!authProvider.isVerifiedFromCache)
if (isVerified == false)
TextFormField(
decoration: InputDecoration(
prefixIcon: const Icon(Icons.alternate_email),
Expand Down Expand Up @@ -406,12 +416,12 @@ class AccountNotVerifiedWarning extends StatelessWidget {
Widget build(BuildContext context) {
final authProvider = Provider.of<AuthProvider>(context);

if (!authProvider.isAuthenticatedFromCache || authProvider.isAnonymous) {
if (!authProvider.isAuthenticated || authProvider.isAnonymous) {
return Container();
}

return FutureBuilder(
future: authProvider.isVerifiedFromService,
future: authProvider.isVerified,
builder: (context, snap) {
if (!snap.hasData || snap.data) {
return Container();
Expand Down
4 changes: 2 additions & 2 deletions lib/authentication/view/sign_up_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class _SignUpViewState extends State<SignUpView> {
return formItems;
}
final emailDomain = S.of(context).stringEmailDomain;
final authProvider = Provider.of<AuthProvider>(context);
final authProvider = Provider.of<AuthProvider>(context, listen: false);

return formItems = <FormCardField>[
FormCardField(
Expand Down Expand Up @@ -155,7 +155,7 @@ class _SignUpViewState extends State<SignUpView> {
}

FormCard _buildForm(BuildContext context) {
final authProvider = Provider.of<AuthProvider>(context);
final authProvider = Provider.of<AuthProvider>(context, listen: false);

return FormCard(
title: S.of(context).actionSignUp,
Expand Down
Loading

0 comments on commit ac3bc79

Please sign in to comment.