Skip to content

Commit

Permalink
feat(ui)!: add email verification and allow to unlink social provider…
Browse files Browse the repository at this point in the history
…s from profile screen (#8358)

* implement unlink providers

* export only ProfileScreen

* implement email verification

* drop unused parameter

* fix analyzer

* Update packages/flutterfire_ui/lib/src/auth/email_verification.dart

* Update packages/flutterfire_ui/example/lib/main.dart

* properly fix pr review comment
  • Loading branch information
lesnitsky committed Mar 31, 2022
1 parent 1972779 commit 89f9704
Show file tree
Hide file tree
Showing 15 changed files with 794 additions and 207 deletions.
Expand Up @@ -28,7 +28,8 @@
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="adjustResize"
android:exported="true">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
Expand Down
3 changes: 1 addition & 2 deletions packages/flutterfire_ui/example/ios/Podfile
@@ -1,5 +1,4 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'
platform :ios, '11.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
Expand Down
Expand Up @@ -160,7 +160,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1020;
LastUpgradeCheck = 1300;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
Expand Down
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
LastUpgradeVersion = "1300"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
88 changes: 66 additions & 22 deletions packages/flutterfire_ui/example/lib/main.dart
Expand Up @@ -10,15 +10,16 @@ import 'init.dart'
import 'config.dart';
import 'decorations.dart';

final actionCodeSettings = ActionCodeSettings(
url: 'https://reactnativefirebase.page.link',
handleCodeInApp: true,
androidMinimumVersion: '1',
androidPackageName:
'io.flutter.plugins.flutterfire_ui.flutterfire_ui_example',
iOSBundleId: 'io.flutter.plugins.flutterfireui.flutterfireUIExample',
);
final emailLinkProviderConfig = EmailLinkProviderConfiguration(
actionCodeSettings: ActionCodeSettings(
url: 'https://reactnativefirebase.page.link',
handleCodeInApp: true,
androidMinimumVersion: '12',
androidPackageName:
'io.flutter.plugins.flutterfire_ui.flutterfire_ui_example',
iOSBundleId: 'io.flutter.plugins.flutterfireui.flutterfireUIExample',
),
actionCodeSettings: actionCodeSettings,
);

Future<void> main() async {
Expand Down Expand Up @@ -53,9 +54,28 @@ class LabelOverrides extends DefaultLocalizations {
}

class FirebaseAuthUIExample extends StatelessWidget {
String get initialRoute {
final auth = FirebaseAuth.instance;

if (auth.currentUser == null) {
return '/';
}

if (!auth.currentUser!.emailVerified) {
return '/verify-email';
}

return '/profile';
}

@override
Widget build(BuildContext context) {
final auth = FirebaseAuth.instance;
final buttonStyle = ButtonStyle(
padding: MaterialStateProperty.all(const EdgeInsets.all(12)),
shape: MaterialStateProperty.all(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);

return MaterialApp(
theme: ThemeData(
Expand All @@ -64,8 +84,11 @@ class FirebaseAuthUIExample extends StatelessWidget {
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
),
elevatedButtonTheme: ElevatedButtonThemeData(style: buttonStyle),
textButtonTheme: TextButtonThemeData(style: buttonStyle),
outlinedButtonTheme: OutlinedButtonThemeData(style: buttonStyle),
),
initialRoute: auth.currentUser == null ? '/' : '/profile',
initialRoute: initialRoute,
routes: {
'/': (context) {
return SignInScreen(
Expand All @@ -81,7 +104,11 @@ class FirebaseAuthUIExample extends StatelessWidget {
Navigator.pushNamed(context, '/phone');
}),
AuthStateChangeAction<SignedIn>((context, state) {
Navigator.pushReplacementNamed(context, '/profile');
if (!state.user!.emailVerified) {
Navigator.pushNamed(context, '/verify-email');
} else {
Navigator.pushReplacementNamed(context, '/profile');
}
}),
EmailLinkSignInAction((context) {
Navigator.pushReplacementNamed(context, '/email-link-sign-in');
Expand Down Expand Up @@ -114,6 +141,22 @@ class FirebaseAuthUIExample extends StatelessWidget {
},
);
},
'/verify-email': (context) {
return EmailVerificationScreen(
headerBuilder: headerIcon(Icons.verified),
sideBuilder: sideIcon(Icons.verified),
actionCodeSettings: actionCodeSettings,
actions: [
EmailVerified(() {
Navigator.pushReplacementNamed(context, '/profile');
}),
Cancel((context) {
FlutterFireUIAuth.signOut(context: context);
Navigator.pushReplacementNamed(context, '/');
}),
],
);
},
'/phone': (context) {
return PhoneInputScreen(
actions: [
Expand All @@ -139,7 +182,7 @@ class FirebaseAuthUIExample extends StatelessWidget {
return SMSCodeInputScreen(
actions: [
AuthStateChangeAction<SignedIn>((context, state) {
Navigator.of(context).pushReplacementNamed('/profile');
Navigator.of(context).pushReplacementNamed('/');
})
],
flowKey: arguments?['flowKey'],
Expand All @@ -148,15 +191,6 @@ class FirebaseAuthUIExample extends StatelessWidget {
sideBuilder: sideIcon(Icons.sms_outlined),
);
},
'/profile': (context) {
return ProfileScreen(
actions: [
SignedOutAction((context) {
Navigator.pushReplacementNamed(context, '/');
}),
],
);
},
'/forgot-password': (context) {
final arguments = ModalRoute.of(context)?.settings.arguments
as Map<String, dynamic>?;
Expand All @@ -172,7 +206,7 @@ class FirebaseAuthUIExample extends StatelessWidget {
return EmailLinkSignInScreen(
actions: [
AuthStateChangeAction<SignedIn>((context, state) {
Navigator.pushReplacementNamed(context, '/profile');
Navigator.pushReplacementNamed(context, '/');
}),
],
config: emailLinkProviderConfig,
Expand All @@ -181,6 +215,16 @@ class FirebaseAuthUIExample extends StatelessWidget {
sideBuilder: sideIcon(Icons.link),
);
},
'/profile': (context) {
return ProfileScreen(
actions: [
SignedOutAction((context) {
Navigator.pushReplacementNamed(context, '/');
}),
],
actionCodeSettings: actionCodeSettings,
);
},
},
title: 'Firebase UI demo',
debugShowCheckedModeBanner: false,
Expand Down
4 changes: 3 additions & 1 deletion packages/flutterfire_ui/lib/auth.dart
Expand Up @@ -76,15 +76,17 @@ export 'src/auth/screens/phone_input_screen.dart';
export 'src/auth/screens/sms_code_input_screen.dart';
export 'src/auth/screens/sign_in_screen.dart';
export 'src/auth/screens/register_screen.dart';
export 'src/auth/screens/profile_screen.dart';
export 'src/auth/screens/profile_screen.dart' show ProfileScreen;
export 'src/auth/screens/forgot_password_screen.dart';
export 'src/auth/screens/universal_email_sign_in_screen.dart';
export 'src/auth/screens/email_link_sign_in_screen.dart';
export 'src/auth/screens/email_verification_screen.dart';

export 'src/auth/navigation/phone_verification.dart';
export 'src/auth/navigation/forgot_password.dart';
export 'src/auth/navigation/authentication.dart';
export 'src/auth/actions.dart';
export 'src/auth/email_verification.dart';

export 'src/auth/configs/email_provider_configuration.dart';
export 'src/auth/configs/phone_provider_configuration.dart';
Expand Down
6 changes: 6 additions & 0 deletions packages/flutterfire_ui/lib/src/auth/actions.dart
Expand Up @@ -28,6 +28,12 @@ class SignedOutAction extends FlutterFireUIAction {
SignedOutAction(this.callback);
}

class Cancel extends FlutterFireUIAction {
final void Function(BuildContext context) callback;

Cancel(this.callback);
}

class FlutterFireUIActions extends InheritedWidget {
final List<FlutterFireUIAction> actions;

Expand Down
Expand Up @@ -9,12 +9,10 @@ import '../flows/email_flow.dart';
const EMAIL_PROVIDER_ID = 'password';

class EmailProviderConfiguration extends ProviderConfiguration {
final ActionCodeSettings? actionCodeSettings;

@override
String get providerId => EMAIL_PROVIDER_ID;

const EmailProviderConfiguration({this.actionCodeSettings});
const EmailProviderConfiguration();

@override
AuthFlow createFlow(FirebaseAuth? auth, AuthAction? action) {
Expand Down
75 changes: 75 additions & 0 deletions packages/flutterfire_ui/lib/src/auth/email_verification.dart
@@ -0,0 +1,75 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_dynamic_links/firebase_dynamic_links.dart';
import 'package:flutter/material.dart';

enum EmailVerificationState {
unresolved,
unverified,
dismissed,
sending,
pending,
sent,
verified,
failed,
}

class EmailVerificationService extends ValueNotifier<EmailVerificationState> {
final FirebaseAuth auth;

EmailVerificationService(this.auth)
: super(EmailVerificationState.unresolved);

User get user => auth.currentUser!;
EmailVerificationState get state => value;

Exception? error;

bool _isMobile(TargetPlatform platform) {
return platform == TargetPlatform.android || platform == TargetPlatform.iOS;
}

Future<void> reload() async {
await user.reload();
if (user.emailVerified) {
value = EmailVerificationState.verified;
} else {
value = EmailVerificationState.unverified;
}
}

void dismiss() {
value = EmailVerificationState.dismissed;
}

Future<void> sendVerificationEmail(
TargetPlatform platform,
ActionCodeSettings? actionCodeSettings,
) async {
value = EmailVerificationState.sending;
try {
await user.sendEmailVerification(actionCodeSettings);
} on Exception catch (e) {
error = e;
value = EmailVerificationState.failed;
return;
}

if (_isMobile(platform)) {
value = EmailVerificationState.pending;
final linkData = await FirebaseDynamicLinks.instance.onLink.first;

try {
final code = linkData.link.queryParameters['oobCode']!;
await auth.checkActionCode(code);
await auth.applyActionCode(code);
await user.reload();
value = EmailVerificationState.verified;
} on Exception catch (err) {
error = err;
value = EmailVerificationState.failed;
}
} else {
value = EmailVerificationState.sent;
}
}
}
32 changes: 0 additions & 32 deletions packages/flutterfire_ui/lib/src/auth/flows/email_flow.dart
Expand Up @@ -14,21 +14,10 @@ class UserCreated extends AuthState {
UserCreated(this.credential);
}

class AwaitingEmailVerification extends AuthState {}

class EmailVerificationFailed extends AuthState {
final Exception exception;

EmailVerificationFailed(this.exception);
}

class EmailVerified extends AuthState {}

class SigningUp extends AuthState {}

abstract class EmailFlowController extends AuthController {
void setEmailAndPassword(String email, String password);
Future<void> verifyEmail([ActionCodeSettings? actionCodeSettings]);
}

class EmailFlow extends AuthFlow implements EmailFlowController {
Expand Down Expand Up @@ -57,27 +46,6 @@ class EmailFlow extends AuthFlow implements EmailFlowController {
setCredential(credential);
}

@override
Future<void> verifyEmail([ActionCodeSettings? actionCodeSettings]) async {
final settings = actionCodeSettings ?? config.actionCodeSettings;

value = AwaitingEmailVerification();
await auth.currentUser!.sendEmailVerification(settings);
final linkData =
await (dynamicLinks ?? FirebaseDynamicLinks.instance).onLink.first;

try {
final code = linkData.link.queryParameters['oobCode']!;
await auth.checkActionCode(code);
await auth.applyActionCode(code);
await auth.currentUser!.reload();

value = EmailVerified();
} on Exception catch (e) {
value = EmailVerificationFailed(e);
}
}

@override
Future<void> onCredentialReceived(AuthCredential credential) async {
try {
Expand Down

0 comments on commit 89f9704

Please sign in to comment.