autologin

This commit is contained in:
Stein Milder 2022-09-21 14:58:46 +02:00
parent 9d6529538d
commit 7abc81593b
14 changed files with 222 additions and 432 deletions

View file

@ -1,42 +1,15 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../login_config.dart';
import '../model/login_confirmation_result.dart';
import '../model/login_user.dart';
abstract class LoginRepository with ChangeNotifier {
String? _loggedIn = '';
abstract class LoginRepository {
bool loggedIn = false;
String loginError = '';
bool _initialized = false;
bool isInitialized() => _initialized;
/// This function returns true if the user is logged in.
bool isLoggedIn() => _loggedIn != null && _loggedIn != '';
String get user => _loggedIn!;
/// This function sets the logged in user.
void setLoggedIn(String user) => _loggedIn = user;
String getLoginError() => loginError;
Future<bool> login(String username, String password);
Future<void> logout() async {
_loggedIn = null;
}
/// This function returns a map with the username.
Map<String, dynamic> getUser() => {
'username': _loggedIn,
};
@mustCallSuper
Future<void> init() async {
// Auto login here
_initialized = true;
}
Future<LoginUser?> signInWithSocial(SocialLoginBundle bundle);
Future<bool?> userprofileExists();
Future sendLoginEmail(String input);
@ -59,6 +32,6 @@ abstract class LoginRepository with ChangeNotifier {
});
Future<bool> forgotPassword(String email);
Future<bool> isRegistrationRequired(LoginUser user);
Future<void> reLogin();
Future<void> reLogin({required VoidCallback onLoggedIn});
Future<LoginUser?> signInAnonymous();
}

View file

@ -1,17 +0,0 @@
import 'sdk/screen.dart';
import 'sdk/user.dart';
mixin FlutterLoginSdk {
static final UserService _userService = UserService();
static final ScreenService _screenService = ScreenService();
UserService get users => _userService;
ScreenService get screens => _screenService;
static UserService get userService => _userService;
static ScreenService get screenService => _screenService;
void dispose() {
_userService.dispose();
}
}

View file

@ -2,10 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_login/backend/login_repository.dart';
import '../default_translation.dart';
import '../plugins/login/login_email_password.dart';
import 'flutter_login_sdk.dart';
import 'login_config.dart';
import 'plugins/login/choose_login.dart';
import 'widgets/custom_navigator.dart';
export '../plugins/form/form.dart';
export '../plugins/login/email_password_form.dart';
export '../plugins/login/login_email_password.dart';
@ -15,8 +13,8 @@ export 'model/login_confirmation_result.dart';
export 'model/login_user.dart';
export 'plugins/settings/control.dart' show Control;
class FlutterLogin extends InheritedWidget with FlutterLoginSdk {
FlutterLogin({
class FlutterLogin extends InheritedWidget {
const FlutterLogin({
required this.config,
required this.repository,
required Widget child,
@ -24,14 +22,6 @@ class FlutterLogin extends InheritedWidget with FlutterLoginSdk {
Key? key,
}) : super(key: key, child: child);
FlutterLogin.from({
required FlutterLogin appShell,
required Widget child,
Key? key,
}) : config = appShell.config,
repository = appShell.repository,
app = appShell.app,
super(child: child, key: key);
static Function(Object?) logError = (error) {};
static Map<String, Map<String, String>> get defaultTranslations =>
defaultTranslation;
@ -41,21 +31,21 @@ class FlutterLogin extends InheritedWidget with FlutterLoginSdk {
final Widget app;
static FlutterLogin of(BuildContext context) {
var inheritedAppshell =
var inheritedLogin =
context.dependOnInheritedWidgetOfExactType<FlutterLogin>();
if (inheritedAppshell == null) {
if (inheritedLogin == null) {
throw FlutterError(
'You are retrieving an flutter login from a context that does not contain an flutter login. Make sure you keep the flutter login in your inheritence tree',
);
}
return inheritedAppshell;
return inheritedLogin;
}
@override
bool updateShouldNotify(FlutterLogin oldWidget) => config != oldWidget.config;
}
extension AppShellRetrieval on BuildContext {
extension LoginRetrieval on BuildContext {
static LoginRepository? _cachedBackend;
FlutterLogin login() => FlutterLogin.of(this);
LoginRepository loginRepository() {
@ -77,7 +67,7 @@ extension StringFormat on String {
String format(List<dynamic> params) => _interpolate(this, params);
}
extension AppShellTranslate on BuildContext {
extension LoginTranslate on BuildContext {
String? _getDefaultTranslation(String key, List arguments) {
var locale = Localizations.localeOf(this);
var code = locale.countryCode ?? 'nl';
@ -86,8 +76,11 @@ extension AppShellTranslate on BuildContext {
return translationMap?[key]?.toString().format(arguments);
}
String translate(String key,
{String? defaultValue, List<dynamic> arguments = const []}) {
String translate(
String key, {
String? defaultValue,
List<dynamic> arguments = const [],
}) {
dynamic translateFunction = login().config.translate;
if (translateFunction == null) {
return _getDefaultTranslation(key, arguments) ?? defaultValue ?? key;
@ -100,53 +93,64 @@ extension AppShellTranslate on BuildContext {
}
}
class LoginMain extends StatelessWidget {
LoginMain({
class LoginMain extends StatefulWidget {
const LoginMain({
required this.child,
super.key,
});
final Widget child;
Widget _login(context) {
return Builder(
builder: (context) {
if (context.login().users.isLoggedIn(context)) {
return child;
}
return FlutterLogin.of(context)
.config
.loginOptions
.loginMethod
.contains(LoginMethod.LoginInteractiveWithSocial) ||
FlutterLogin.of(context)
.config
.loginOptions
.loginMethod
.contains(LoginMethod.LoginInteractiveWithPhoneNumber)
? ChooseLogin(
child: child,
)
: EmailPasswordLogin(
onPressedForgotPassword: FlutterLogin.of(context)
.config
.loginOptions
.onPressForgotPassword,
child: child,
);
},
);
}
@override
CustomNavigator build(BuildContext context) => CustomNavigator(
pageRoute: PageRoutes.materialPageRoute,
home: _login(context),
);
State<LoginMain> createState() => _LoginMainState();
}
class AppShellException implements Exception {
AppShellException(
class _LoginMainState extends State<LoginMain> {
bool _checkedIfLoggedIn = false;
bool _isLoggedIn = false;
@override
Widget build(BuildContext context) {
if (!_checkedIfLoggedIn) {
context.loginRepository().reLogin(
onLoggedIn: () => setState(
() {
_isLoggedIn = true;
},
),
);
_checkedIfLoggedIn = true;
}
return _isLoggedIn
? widget.child
: Builder(
builder: (context) => FlutterLogin.of(context)
.config
.loginOptions
.loginMethod
.contains(LoginMethod.LoginInteractiveWithSocial) ||
FlutterLogin.of(context)
.config
.loginOptions
.loginMethod
.contains(LoginMethod.LoginInteractiveWithPhoneNumber)
? ChooseLogin(
child: widget.child,
)
: EmailPasswordLogin(
onPressedForgotPassword: FlutterLogin.of(context)
.config
.loginOptions
.onPressForgotPassword,
child: widget.child,
),
);
}
}
class LoginException implements Exception {
LoginException(
this.error, [
this.stackTrace = StackTrace.empty,
]);
@ -158,7 +162,7 @@ class AppShellException implements Exception {
@override
String toString() {
return 'Unhandled error occurred in Appshell: $error\n'
return 'Unhandled error occurred in login: $error\n'
'$stackTrace';
}
}

View file

@ -337,7 +337,7 @@ class LoginConfig extends StatefulWidget {
}
class LoginConfigState extends State<LoginConfig> with WidgetsBindingObserver {
FlutterLogin? appShell;
FlutterLogin? login;
late final ConfigData configData;
late LoginRepository repository;
@ -352,7 +352,6 @@ class LoginConfigState extends State<LoginConfig> with WidgetsBindingObserver {
if (widget.repository != null) {
repository = widget.repository!;
repository.init();
}
WidgetsBinding.instance.addObserver(this);
@ -383,7 +382,7 @@ class LoginConfigState extends State<LoginConfig> with WidgetsBindingObserver {
!configData.loginOptions.socialOptions.forceAppleSignin))) {
// check if apple login is removed by developer
if (configData.loginOptions.socialOptions.socialLogins.isEmpty) {
throw AppShellException(
throw LoginException(
'If you enable LoginMethod.LoginInteractiveWithSocial you must provide atleast 1 social login option!');
}
}
@ -391,7 +390,7 @@ class LoginConfigState extends State<LoginConfig> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
appShell = FlutterLogin(
login = FlutterLogin(
config: widget.config ?? ConfigData.example(),
repository: repository,
app: widget.child,
@ -406,21 +405,10 @@ class LoginConfigState extends State<LoginConfig> with WidgetsBindingObserver {
if (isFlutterDefaultTheme(context)) {
return Theme(
data: defaultTheme,
child: appShell!,
child: login!,
);
}
return appShell!;
}
@override
void dispose() {
super.dispose();
repository.dispose();
return login!;
}
}
class AppshellNoDisplayError {
AppshellNoDisplayError(this.error);
Object error;
}

View file

@ -14,7 +14,8 @@ class ChooseLogin extends Login {
static String? finalEmail;
static String? finalPassword;
createState() => ChooseLoginState();
@override
ChooseLoginState createState() => ChooseLoginState();
}
class ChooseLoginState extends LoginState<ChooseLogin> {

View file

@ -47,139 +47,122 @@ class ForgotPasswordState extends State<ForgotPassword> {
context.translate('forgot_password.text.title'),
),
),
body: context.login().screens.getAppshellScreenWrapper(
context,
backgroundImg:
context.login().config.loginOptions.backgroundImage,
child: Column(
body: Column(
children: [
Expanded(
child: ListView(
children: [
Expanded(
child: ListView(
Container(
alignment: Alignment.topLeft,
padding: const EdgeInsets.only(
top: 27,
left: 5,
),
child: context.login().config.appTheme.buttons.backButton(
context: context,
),
),
Padding(
padding: const EdgeInsets.all(30.0),
child: Text(
context.translate('forgot_password.text.body'),
style: Theme.of(context).textTheme.subtitle1,
textAlign: TextAlign.left,
),
),
Padding(
padding: const EdgeInsets.only(left: 30, right: 30),
child: Column(
children: [
Container(
alignment: Alignment.topLeft,
padding: const EdgeInsets.only(
top: 27,
left: 5,
),
child: context
.login()
.config
.appTheme
.buttons
.backButton(
context: context,
),
const Padding(
padding: EdgeInsets.only(bottom: 20),
),
Padding(
padding: const EdgeInsets.all(30.0),
child: Text(
context.translate('forgot_password.text.body'),
style: Theme.of(context).textTheme.subtitle1,
textAlign: TextAlign.left,
),
),
Padding(
padding: const EdgeInsets.only(left: 30, right: 30),
child: Column(
children: [
const Padding(
padding: EdgeInsets.only(bottom: 20),
context.login().config.appTheme.inputs.textField(
title: context.translate(
'forgot_password.input.email',
),
context.login().config.appTheme.inputs.textField(
title: context.translate(
'forgot_password.input.email',
),
validators: [
EmailValidator(
errorMessage: context.translate(
'forgot_password.error.invalid_email',
),
)
],
onChange: (value, valid) {
setState(() {
showError = false;
});
if (valid) {
email = value;
}
},
validators: [
EmailValidator(
errorMessage: context.translate(
'forgot_password.error.invalid_email',
),
if (showError) ...[
Text(
context.translate(
'forgot_password.error.email_does_not_exist',
),
style: Theme.of(context)
.textTheme
.bodyText2!
.copyWith(
color: Theme.of(context).errorColor,
),
),
)
],
const Padding(
padding: EdgeInsets.only(bottom: 30),
),
],
onChange: (value, valid) {
setState(() {
showError = false;
});
if (valid) {
email = value;
}
},
),
if (showError) ...[
Text(
context.translate(
'forgot_password.error.email_does_not_exist',
),
style:
Theme.of(context).textTheme.bodyText2!.copyWith(
color: Theme.of(context).errorColor,
),
),
],
const Padding(
padding: EdgeInsets.only(bottom: 30),
),
],
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 30),
padding: const EdgeInsets.only(bottom: 40.0),
child: context
.login()
.config
.appTheme
.buttons
.primaryButton(
context: context,
child: Text(
context.translate('forgot_password.button.submit'),
style: Theme.of(context).textTheme.button,
),
onPressed: () async {
if (email != null) {
setState(() {
showError = false;
});
var result = await context
.loginRepository()
.forgotPassword(email!);
if (result) {
showAlert(
title: context.translate(
'forgot_password.dialog.text.title',
),
text: context.translate(
'forgot_password.dialog.text.body',
arguments: [email],
),
buttonTitle: context.translate(
'forgot_password.dialog.text.button',
),
buttonAction: () {
Navigator.pop(context);
},
);
} else {
setState(() {
showError = true;
});
}
}
},
),
),
],
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 30),
padding: const EdgeInsets.only(bottom: 40.0),
child: context.login().config.appTheme.buttons.primaryButton(
context: context,
child: Text(
context.translate('forgot_password.button.submit'),
style: Theme.of(context).textTheme.button,
),
onPressed: () async {
if (email != null) {
setState(() {
showError = false;
});
var result = await context
.loginRepository()
.forgotPassword(email!);
if (result) {
showAlert(
title: context.translate(
'forgot_password.dialog.text.title',
),
text: context.translate(
'forgot_password.dialog.text.body',
arguments: [email],
),
buttonTitle: context.translate(
'forgot_password.dialog.text.button',
),
buttonAction: () {
Navigator.pop(context);
},
);
} else {
setState(() {
showError = true;
});
}
}
},
),
),
],
),
),
);
}

View file

@ -15,30 +15,16 @@ class LoginAwaitEmailScreen extends StatefulWidget {
final Widget? child;
@override
_LoginAwaitEmailScreenState createState() => _LoginAwaitEmailScreenState();
LoginAwaitEmailScreenState createState() => LoginAwaitEmailScreenState();
}
class _LoginAwaitEmailScreenState extends State<LoginAwaitEmailScreen>
class LoginAwaitEmailScreenState extends State<LoginAwaitEmailScreen>
with NavigateWidgetMixin {
@override
void initState() {
context.loginRepository().addListener(registrateOrMainScreen);
super.initState();
}
Future<void> registrateOrMainScreen() async {
var data =
await (context.loginRepository().userprofileExists() as FutureOr<bool>);
if (context.login().config.registrationOptions.registrationMode ==
RegistrationMode.Disabled ||
data) {
context.loginRepository().setLoggedIn(EmailPasswordLogin.finalEmail!);
widget.loginComplete!();
} else {
debugPrint('Register');
}
}
void navigateToEmailPage(BuildContext context) {
navigateFadeTo(
context,

View file

@ -87,7 +87,7 @@ class EmailLoginState extends LoginState<EmailPasswordLogin> {
_loading = false;
});
} else {
context.loginRepository().setLoggedIn(EmailPasswordLogin.finalEmail!);
context.loginRepository().loggedIn = true;
navigateFadeToReplace(
context,
(context) => widget.child,

View file

@ -14,7 +14,6 @@ class LoginImage extends StatelessWidget {
if (config.loginImage == '') {
image = const AssetImage(
'assets/images/login.png',
package: 'appshell',
);
} else if (split.length < 2) {
image = AssetImage(config.loginImage);

View file

@ -36,43 +36,38 @@ abstract class LoginState<L extends Login> extends State<L>
@override
Widget build(BuildContext context) => Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
body: context.login().screens.getAppshellScreenWrapper(
context,
backgroundImg:
context.login().config.loginOptions.backgroundImage,
child: SizedBox(
height: MediaQuery.of(context).size.height,
child: Stack(
children: [
SingleChildScrollView(
child: buildLoginPage(context),
),
if (widget.allowExit) ...[
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(top: 30, right: 10),
child: SizedBox(
height: 48,
width: 48,
child: GestureDetector(
key: const Key('navigateToSettings'),
child: const Icon(
Icons.close,
size: 24,
),
onTap: () {
Navigator.of(context).pop();
},
),
),
),
),
],
],
),
body: SizedBox(
height: MediaQuery.of(context).size.height,
child: Stack(
children: [
SingleChildScrollView(
child: buildLoginPage(context),
),
),
if (widget.allowExit) ...[
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(top: 30, right: 10),
child: SizedBox(
height: 48,
width: 48,
child: GestureDetector(
key: const Key('navigateToSettings'),
child: const Icon(
Icons.close,
size: 24,
),
onTap: () {
Navigator.of(context).pop();
},
),
),
),
),
],
],
),
),
);
Widget buildLoginPage(BuildContext context);

View file

@ -5,10 +5,10 @@ class Resend extends StatefulWidget {
const Resend({super.key});
@override
_ConfirmationState createState() => _ConfirmationState();
ConfirmationState createState() => ConfirmationState();
}
class _ConfirmationState extends State<Resend> {
class ConfirmationState extends State<Resend> {
@override
void initState() {
super.initState();

View file

@ -1,40 +0,0 @@
import 'package:flutter/material.dart';
import '../flutter_login_view.dart';
class ScreenService {
late bool shouldShowIntroductionScreen;
late bool shouldShowPolicyPage;
Widget getAppshellScreenWrapper(
BuildContext context, {
required Widget child,
String? backgroundImg,
}) {
var bgImage =
backgroundImg ?? context.login().config.appOptions.backgroundImage;
if (bgImage.isNotEmpty) {
late AssetImage image;
var split = bgImage.split(';');
image = split.length < 2
? AssetImage(bgImage)
: AssetImage(
split.first,
package: split.last,
);
return Container(
height: MediaQuery.of(context).size.height,
decoration: BoxDecoration(
image: DecorationImage(
image: image,
fit: BoxFit.cover,
),
),
child: child,
);
} else {
return child;
}
}
}

View file

@ -1,84 +0,0 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../flutter_login_view.dart';
class UserService extends ChangeNotifier {
late Map<String, dynamic>? _currentProfile = {};
set profile(Map<String, dynamic> profile) {
_currentProfile = profile;
notifyListeners();
}
Future<void> checkAutoLogin(FlutterLogin login) async {
debugPrint('checking autologin');
if (login.config.loginOptions.loginMode != LoginMode.NoLogin) {
await Future.delayed(const Duration(milliseconds: 100), () async {
if (login.config.loginOptions.loginMode == LoginMode.LoginAutomatic) {
if (login.config.loginOptions.loginEmail == null ||
login.config.loginOptions.loginEmail == '') {
throw Exception('No login account for automatic login provided!');
}
if (login.config.loginOptions.loginPassword == null ||
login.config.loginOptions.loginPassword == '') {
throw Exception(
'No login password for automatic login provided!',
);
}
await login.repository.login(login.config.loginOptions.loginEmail!,
login.config.loginOptions.loginPassword!);
} else if (login.config.loginOptions.loginMode ==
LoginMode.LoginAnonymous) {
await login.repository.signInAnonymous();
} else {
var prefs = await SharedPreferences.getInstance();
var autoLoginMode = login.config.loginOptions.autoLoginMode;
if ((autoLoginMode != AutoLoginMode.alwaysOff &&
(prefs.getBool('autoLogin') ?? false) == true) ||
autoLoginMode == AutoLoginMode.alwaysOn) {
await login.repository.reLogin();
}
}
});
}
}
void addProfileListener(
void Function(Map<String, dynamic>) onProfileChanged,
) {
addListener(() {
onProfileChanged.call(_currentProfile!);
});
}
bool isLoggedIn(BuildContext context) =>
context.loginRepository().isLoggedIn();
Future<void> logout(BuildContext context) =>
SharedPreferences.getInstance().then(
(value) {
value
.setBool('autoLogin', false)
.then((value) => context.loginRepository().logout());
},
);
}
class UserProfile {
late Map<String, dynamic> rawFields;
String? photoUrl;
@mustCallSuper
void init(Map<String, dynamic> raw) {
rawFields = raw;
photoUrl = raw['photo'];
}
bool isProfileComplete({List<String> requiredFields = const []}) {
return !requiredFields.any((element) => rawFields[element] == null);
}
dynamic getValue(String key) {
return rawFields[key];
}
}

View file

@ -24,10 +24,10 @@ class CustomNavigator extends StatefulWidget {
final List<NavigatorObserver> navigatorObservers;
@override
_CustomNavigatorState createState() => _CustomNavigatorState();
CustomNavigatorState createState() => CustomNavigatorState();
}
class _CustomNavigatorState extends State<CustomNavigator>
class CustomNavigatorState extends State<CustomNavigator>
implements WidgetsBindingObserver {
GlobalKey<NavigatorState>? _navigator;
@ -134,9 +134,11 @@ class _CustomNavigatorState extends State<CustomNavigator>
return result;
}
didChangeAppLifecycleState(AppLifecycleState state) {}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {}
noSuchMethod(Invocation invocation) {
@override
void noSuchMethod(Invocation invocation) {
var name = invocation.memberName.toString();
debugPrint(
'Expected a method to be called with name $name, '
@ -147,9 +149,9 @@ class _CustomNavigatorState extends State<CustomNavigator>
class PageRoutes {
static final materialPageRoute =
(<T>(RouteSettings settings, WidgetBuilder builder) =>
MaterialPageRoute<T>(settings: settings, builder: builder));
<T>(RouteSettings settings, WidgetBuilder builder) =>
MaterialPageRoute<T>(settings: settings, builder: builder);
static final cupertinoPageRoute =
(<T>(RouteSettings settings, WidgetBuilder builder) =>
CupertinoPageRoute<T>(settings: settings, builder: builder));
<T>(RouteSettings settings, WidgetBuilder builder) =>
CupertinoPageRoute<T>(settings: settings, builder: builder);
}