From 1be87a1628269067478b5c78c3b60a6faffe01f0 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Sat, 5 Aug 2023 00:47:44 +0400 Subject: [PATCH] Add sign in form --- example/config/development.json | 1 + example/lib/src/common/constant/config.dart | 6 +- example/lib/src/common/widget/app.dart | 13 +- .../controller/authentication_controller.dart | 17 +- .../data/authentication_repository.dart | 35 +- .../authentication/model/sign_in_data.dart | 72 +++ .../widget/authentication_scope.dart | 56 ++- .../authentication/widget/sign_in_form.dart | 418 ++++++++++++++++++ .../authentication/widget/sign_in_screen.dart | 24 - .../src/feature/chat/widget/chat_screen.dart | 36 ++ .../initialize_dependencies.dart | 2 +- 11 files changed, 619 insertions(+), 61 deletions(-) create mode 100644 example/lib/src/feature/authentication/model/sign_in_data.dart create mode 100644 example/lib/src/feature/authentication/widget/sign_in_form.dart delete mode 100644 example/lib/src/feature/authentication/widget/sign_in_screen.dart create mode 100644 example/lib/src/feature/chat/widget/chat_screen.dart diff --git a/example/config/development.json b/example/config/development.json index f1cdc5e..9cbc255 100644 --- a/example/config/development.json +++ b/example/config/development.json @@ -1,5 +1,6 @@ { "ENVIRONMENT": "development", "CENTRIFUGE_BASE_URL": "http://localhost:8000", + "CENTRIFUGE_CHANNEL": "chat", "CENTRIFUGE_TIMEOUT": 8000 } diff --git a/example/lib/src/common/constant/config.dart b/example/lib/src/common/constant/config.dart index 31983b0..bf2cc78 100644 --- a/example/lib/src/common/constant/config.dart +++ b/example/lib/src/common/constant/config.dart @@ -20,9 +20,13 @@ abstract final class Config { int.fromEnvironment('CENTRIFUGE_TIMEOUT', defaultValue: 15000)); /// Secret for HMAC token. - static const String passwordMinLength = + static const String centrifugeToken = String.fromEnvironment('CENTRIFUGE_TOKEN_HMAC_SECRET'); + /// Channel by default. + static const String centrifugeChannel = + String.fromEnvironment('CENTRIFUGE_CHANNEL'); + // --- Layout --- // /// Maximum screen layout width for screen with list view. diff --git a/example/lib/src/common/widget/app.dart b/example/lib/src/common/widget/app.dart index 42dceb2..ef9c235 100644 --- a/example/lib/src/common/widget/app.dart +++ b/example/lib/src/common/widget/app.dart @@ -3,7 +3,8 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:spinifyapp/src/common/localization/localization.dart'; import 'package:spinifyapp/src/common/widget/window_scope.dart'; import 'package:spinifyapp/src/feature/authentication/widget/authentication_scope.dart'; -import 'package:spinifyapp/src/feature/authentication/widget/sign_in_screen.dart'; +import 'package:spinifyapp/src/feature/authentication/widget/sign_in_form.dart'; +import 'package:spinifyapp/src/feature/chat/widget/chat_screen.dart'; /// {@template app} /// App widget. @@ -27,17 +28,17 @@ class App extends StatelessWidget { ? ThemeData.dark(useMaterial3: true) : ThemeData.light( useMaterial3: true), // TODO(plugfox): implement theme - home: const Placeholder(), + home: const AuthenticationScope( + signInForm: SignInForm(), + child: ChatScreen(), + ), supportedLocales: Localization.supportedLocales, locale: const Locale('en', 'US'), // TODO(plugfox): implement locale builder: (context, child) => MediaQuery( data: MediaQuery.of(context).copyWith(textScaleFactor: 1), child: WindowScope( title: Localization.of(context).title, - child: AuthenticationScope( - signInScreen: const SignInScreen(), - child: child ?? const SizedBox.shrink(), - ), + child: child ?? const SizedBox.shrink(), ), ), ); diff --git a/example/lib/src/feature/authentication/controller/authentication_controller.dart b/example/lib/src/feature/authentication/controller/authentication_controller.dart index cc4f972..2a46ee1 100644 --- a/example/lib/src/feature/authentication/controller/authentication_controller.dart +++ b/example/lib/src/feature/authentication/controller/authentication_controller.dart @@ -5,6 +5,7 @@ import 'package:spinifyapp/src/common/controller/state_controller.dart'; import 'package:spinifyapp/src/common/util/error_util.dart'; import 'package:spinifyapp/src/feature/authentication/controller/authentication_state.dart'; import 'package:spinifyapp/src/feature/authentication/data/authentication_repository.dart'; +import 'package:spinifyapp/src/feature/authentication/model/sign_in_data.dart'; import 'package:spinifyapp/src/feature/authentication/model/user.dart'; final class AuthenticationController @@ -26,17 +27,29 @@ final class AuthenticationController final IAuthenticationRepository _repository; StreamSubscription? _userSubscription; - void signInAnonymously() => handle( + void signIn(SignInData data) => handle( () async { setState(AuthenticationState.processing( user: state.user, message: 'Logging in...')); - await _repository.signInAnonymously(); + await _repository.signIn(data); }, (error, _) => setState(AuthenticationState.idle( user: state.user, error: ErrorUtil.formatMessage(error))), () => setState(AuthenticationState.idle(user: state.user)), ); + void signOut() => handle( + () async { + setState(AuthenticationState.processing( + user: state.user, message: 'Logging out...')); + await _repository.signOut(); + }, + (error, _) => setState(AuthenticationState.idle( + user: state.user, error: ErrorUtil.formatMessage(error))), + () => setState( + const AuthenticationState.idle(user: User.unauthenticated())), + ); + @override void dispose() { _userSubscription?.cancel(); diff --git a/example/lib/src/feature/authentication/data/authentication_repository.dart b/example/lib/src/feature/authentication/data/authentication_repository.dart index aab590f..94da072 100644 --- a/example/lib/src/feature/authentication/data/authentication_repository.dart +++ b/example/lib/src/feature/authentication/data/authentication_repository.dart @@ -1,30 +1,18 @@ import 'dart:async'; +import 'package:spinifyapp/src/feature/authentication/model/sign_in_data.dart'; import 'package:spinifyapp/src/feature/authentication/model/user.dart'; abstract interface class IAuthenticationRepository { Stream userChanges(); FutureOr getUser(); - Future signInAnonymously(); - - /* Future sendSignInWithEmailLink(String email); - Future signInWithEmailLink(String email, String emailLink); - Future signInWithEmailAndPassword(String email, String password); - Future signInWithFacebook(); - Future signInWithApple(); - Future signInWithTwitter(); - Future signInWithGithub(); - Future signInWithPhoneNumber(String phoneNumber); - Future sendPasswordResetEmail(String email); - Future confirmPasswordReset(String code, String newPassword); - Future signUpWithEmailAndPassword(String email, String password); - Future deleteUser(); - Future isSignedIn(); - Future signInWithGoogle(); - Future signOut(); */ + Future signIn(SignInData data); + Future signOut(); } -class AuthenticationRepositoryFake implements IAuthenticationRepository { +class AuthenticationRepositoryImpl implements IAuthenticationRepository { + AuthenticationRepositoryImpl(); + final StreamController _userController = StreamController.broadcast(); User _user = const User.unauthenticated(); @@ -36,6 +24,13 @@ class AuthenticationRepositoryFake implements IAuthenticationRepository { Stream userChanges() => _userController.stream; @override - Future signInAnonymously() => Future.sync(() => _userController - .add(_user = const User.authenticated(id: 'anonymous-user-id'))); + Future signIn(SignInData data) { + // TODO(plugfox): implement signIn + return Future.sync(() => _userController + .add(_user = const User.authenticated(id: 'anonymous-user-id'))); + } + + @override + Future signOut() => Future.sync( + () => _userController.add(_user = const User.unauthenticated())); } diff --git a/example/lib/src/feature/authentication/model/sign_in_data.dart b/example/lib/src/feature/authentication/model/sign_in_data.dart new file mode 100644 index 0000000..cae7ed1 --- /dev/null +++ b/example/lib/src/feature/authentication/model/sign_in_data.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; + +@immutable +final class SignInData { + SignInData({ + required this.endpoint, + required this.token, + required this.username, + required this.channel, + String? secret, + }) : secret = secret == null || secret.isEmpty ? null : secret; + + final String endpoint; + final String token; + final String username; + final String channel; + final String? secret; + + static final RegExp _urlValidator = RegExp( + r'^(https?:\/\/)?(localhost|((([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|((\d{1,3}\.){3}\d{1,3})))?(:\d+)?(\/[-a-z\d%_.~+]*)*(\?[;&a-z\d%_.~+=-]*)?(\#[-a-z\d_]*)?$', + caseSensitive: false, + multiLine: false, + ); + String? isValidEndpoint() { + if (endpoint.isEmpty) return 'Endpoint is required'; + if (endpoint.length < 6) return 'Endpoint is too short'; + if (endpoint.length > 1024) return 'Endpoint is too long'; + if (!_urlValidator.hasMatch(endpoint)) return 'Endpoint is invalid'; + return null; + } + + String? isValidToken() { + if (token.isEmpty) return 'Token is required'; + if (token.length < 6) return 'Token is too short'; + if (token.length > 64) return 'Token is too long'; + return null; + } + + static final RegExp _usernameValidator = RegExp( + r'\@|[A-Z]|[a-z]|[0-9]|\.|\-|\_|\+', + caseSensitive: false, + multiLine: false, + ); + String? isValidUsername() { + if (username.isEmpty) return 'Username is required'; + if (username.length < 4) return 'Username is too short'; + if (username.length > 64) return 'Username is too long'; + if (!_usernameValidator.hasMatch(username)) return 'Username is invalid'; + return null; + } + + static final RegExp _channelValidator = RegExp( + r'^[a-zA-Z0-9_-]+$', + caseSensitive: false, + multiLine: false, + ); + String? isValidChannel() { + if (channel.isEmpty) return 'Channel is required'; + if (channel.length < 4) return 'Channel is too short'; + if (channel.length > 64) return 'Channel is too long'; + if (!_channelValidator.hasMatch(channel)) return 'Channel is invalid'; + return null; + } + + String? isValidSecret() { + final secret = this.secret; + if (secret == null || secret.isEmpty) return null; + if (secret.length < 4) return 'Secret is too short'; + if (secret.length > 64) return 'Secret is too long'; + return null; + } +} diff --git a/example/lib/src/feature/authentication/widget/authentication_scope.dart b/example/lib/src/feature/authentication/widget/authentication_scope.dart index e2e1b6b..0344648 100644 --- a/example/lib/src/feature/authentication/widget/authentication_scope.dart +++ b/example/lib/src/feature/authentication/widget/authentication_scope.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/widgets.dart'; import 'package:spinifyapp/src/feature/authentication/controller/authentication_controller.dart'; import 'package:spinifyapp/src/feature/authentication/model/user.dart'; @@ -9,13 +11,13 @@ import 'package:spinifyapp/src/feature/dependencies/widget/dependencies_scope.da class AuthenticationScope extends StatefulWidget { /// {@macro authentication_scope} const AuthenticationScope({ - required this.signInScreen, + required this.signInForm, required this.child, super.key, }); - /// Sign In screen for unauthenticated users. - final Widget signInScreen; + /// Sign In form for unauthenticated users. + final Widget signInForm; /// The widget below this widget in the tree. final Widget child; @@ -36,6 +38,7 @@ class AuthenticationScope extends StatefulWidget { class _AuthenticationScopeState extends State { late final AuthenticationController _authenticationController; User _user = const User.unauthenticated(); + bool _showForm = true; @override void initState() { @@ -55,17 +58,56 @@ class _AuthenticationScopeState extends State { void _onAuthenticationControllerChanged() { final user = _authenticationController.state.user; - if (!identical(_user, user)) setState(() => _user = user); + if (!identical(_user, user)) { + if (user.isNotAuthenticated) _showForm = true; + setState(() => _user = user); + } } @override Widget build(BuildContext context) => _InheritedAuthenticationScope( controller: _authenticationController, user: _user, - child: switch (_user) { - UnauthenticatedUser _ => widget.signInScreen, + /* child: switch (_user) { + UnauthenticatedUser _ => widget.signInForm, AuthenticatedUser _ => widget.child, - }, + }, */ + child: ClipRect( + child: StatefulBuilder( + builder: (context, setState) => Stack( + children: [ + Positioned.fill( + child: IgnorePointer( + ignoring: _user.isNotAuthenticated, + child: widget.child, + ), + ), + if (_showForm) + Positioned.fill( + child: IgnorePointer( + ignoring: _user.isAuthenticated, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 350), + onEnd: () => setState(() => _showForm = false), + curve: Curves.easeInOut, + opacity: _user.isNotAuthenticated ? 1 : 0, + child: RepaintBoundary( + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 2.5, + sigmaY: 2.5, + ), + child: Center( + child: widget.signInForm, + ), + ), + ), + ), + ), + ), + ], + )), + ), ); } diff --git a/example/lib/src/feature/authentication/widget/sign_in_form.dart b/example/lib/src/feature/authentication/widget/sign_in_form.dart new file mode 100644 index 0000000..cad2f0a --- /dev/null +++ b/example/lib/src/feature/authentication/widget/sign_in_form.dart @@ -0,0 +1,418 @@ +import 'dart:math' as math; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:spinifyapp/src/common/constant/config.dart'; +import 'package:spinifyapp/src/common/controller/state_consumer.dart'; +import 'package:spinifyapp/src/common/localization/localization.dart'; +import 'package:spinifyapp/src/feature/authentication/controller/authentication_controller.dart'; +import 'package:spinifyapp/src/feature/authentication/model/sign_in_data.dart'; +import 'package:spinifyapp/src/feature/authentication/widget/authentication_scope.dart'; + +/// {@template sign_in_form} +/// SignInScreen widget. +/// {@endtemplate} +class SignInForm extends StatelessWidget implements PreferredSizeWidget { + /// {@macro sign_in_form} + const SignInForm({super.key}); + + /// Width of the sign in form. + static const double width = 480; + + /// Height of the sign in form. + static const double height = 720; + + @override + Size get preferredSize => const Size(width, height); + + @override + Widget build(BuildContext context) => + LayoutBuilder(builder: (context, constraints) { + final space = math.min(constraints.maxHeight - preferredSize.height, + constraints.maxWidth - preferredSize.width); + final padding = switch (space) { + > 32 => 24.0, + > 24 => 16.0, + > 16 => 8.0, + _ => 0.0, + }; + Widget wrap({required Widget child}) => padding > 0 + ? SizedBox( + width: width, + height: height, + child: child, + ) + : SizedBox.expand( + child: child, + ); + return wrap( + child: Card( + elevation: padding > 0 ? 8 : 0, + margin: EdgeInsets.all(padding), + shape: padding > 0 + ? RoundedRectangleBorder( + borderRadius: BorderRadius.circular(padding), + ) + : const RoundedRectangleBorder(borderRadius: BorderRadius.zero), + child: const _SignInForm(), + ), + ); + }); +} + +class _SignInForm extends StatefulWidget { + const _SignInForm(); + + @override + State<_SignInForm> createState() => _SignInFormState(); +} + +/// State for widget _SignInForm. +class _SignInFormState extends State<_SignInForm> { + late final AuthenticationController authenticationController; + final TextEditingController _endpointController = + TextEditingController(text: Config.centrifugeBaseUrl), + _tokenController = TextEditingController(text: Config.centrifugeToken), + _channelController = + TextEditingController(text: Config.centrifugeChannel), + _usernameController = TextEditingController(), + _secretController = TextEditingController(); + + final FocusNode _endpointFocusNode = FocusNode(), + _tokenFocusNode = FocusNode(), + _channelFocusNode = FocusNode(), + _usernameFocusNode = FocusNode(), + _secretFocusNode = FocusNode(); + + final ValueNotifier _endpointError = ValueNotifier(null), + _tokenError = ValueNotifier(null), + _channelError = ValueNotifier(null), + _usernameError = ValueNotifier(null), + _secretError = ValueNotifier(null); + + final ValueNotifier _validNotifier = ValueNotifier(false); + + late final Listenable _observer; + late final List _controllers = [ + _endpointController, + _tokenController, + _channelController, + _usernameController, + _secretController, + ]; + + @override + void initState() { + super.initState(); + authenticationController = AuthenticationScope.controllerOf(context); + _observer = Listenable.merge(_controllers)..addListener(_onChanged); + _onChanged(); + } + + @override + void dispose() { + _observer.removeListener(_onChanged); + for (final controller in _controllers) { + controller.dispose(); + } + _validNotifier.dispose(); + super.dispose(); + } + + late SignInData _data; + + void _onChanged() { + if (!mounted) return; + _data = SignInData( + endpoint: _endpointController.text, + token: _tokenController.text, + channel: _channelController.text, + username: _usernameController.text, + secret: _secretController.text, + ); + _validNotifier.value = _validate(_data); + } + + late final List _validators = + [ + (data) => _endpointError.value = data.isValidEndpoint(), + (data) => _tokenError.value = data.isValidToken(), + (data) => _usernameError.value = data.isValidUsername(), + (data) => _channelError.value = data.isValidChannel(), + (data) => _secretError.value = data.isValidSecret(), + ]; + bool _validate(SignInData data) { + for (final validator in _validators) { + if (validator(data) != null) return false; + } + return true; + } + + void _submit() { + final data = _data; + if (!_validate(data)) return; + authenticationController.signIn(data); + } + + @override + Widget build(BuildContext context) => FocusScope( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: ListView( + padding: const EdgeInsets.all(16), + shrinkWrap: true, + children: [ + SizedBox( + width: double.infinity, + height: 48, + child: Text( + Localization.of(context).signInButton, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .headlineMedium + ?.copyWith(height: 1), + ), + ), + const SizedBox(height: 12), + SignInTextField( + focusNode: _endpointFocusNode, + controller: _endpointController, + error: _endpointError, + autofillHints: const [ + AutofillHints.url, + ], + maxLength: 1024, + keyboardType: TextInputType.url, + labelText: 'Endpoint', + hintText: 'Enter your endpoint', + ), + SignInTextField( + focusNode: _tokenFocusNode, + controller: _tokenController, + error: _tokenError, + maxLength: 64, + autofillHints: const [ + AutofillHints.password, + ], + keyboardType: TextInputType.visiblePassword, + labelText: 'Token', + hintText: 'Enter HMAC secret token', + obscureText: true, + ), + SignInTextField( + focusNode: _channelFocusNode, + controller: _channelController, + error: _channelError, + maxLength: 64, + labelText: 'Channel', + hintText: 'Enter your channel', + autofillHints: const [ + AutofillHints.username, + ], + keyboardType: TextInputType.name, + formatters: [ + FilteringTextInputFormatter.allow( + RegExp(r'^[a-zA-Z0-9_-]+$'), + ), + ], + ), + SignInTextField( + focusNode: _usernameFocusNode, + controller: _usernameController, + error: _usernameError, + maxLength: 64, + labelText: 'Username', + hintText: 'Select your username', + autofillHints: const [ + AutofillHints.username, + ], + keyboardType: TextInputType.name, + formatters: [ + FilteringTextInputFormatter.allow( + /// Allow only letters, numbers, + /// and the following characters: @.-_+ + RegExp(r'\@|[A-Z]|[a-z]|[0-9]|\.|\-|\_|\+'), + ), + ], + ), + SignInTextField( + focusNode: _secretFocusNode, + controller: _secretController, + error: _secretError, + maxLength: 64, + autofillHints: const [ + AutofillHints.password, + ], + keyboardType: TextInputType.visiblePassword, + labelText: 'Secret (optional)', + hintText: 'For private channels only', + obscureText: true, + ), + ], + ), + ), + const Divider(height: 1, thickness: 1, indent: 16, endIndent: 16), + Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: SizedBox( + width: 320, + height: 64, + child: ValueListenableBuilder( + valueListenable: _validNotifier, + builder: (context, valid, _) => AnimatedOpacity( + opacity: valid ? 1 : .5, + duration: const Duration(milliseconds: 350), + child: ElevatedButton( + onPressed: valid ? _submit : null, + style: ElevatedButton.styleFrom( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(24), + bottomLeft: Radius.circular(8), + bottomRight: Radius.circular(24), + topRight: Radius.circular(8), + ), + ), + textStyle: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + child: const Text('Sign In Anonymously'), + ), + ), + ), + ), + ), + ), + ], + ), + ); +} + +class SignInTextField extends StatefulWidget { + const SignInTextField({ + required this.controller, + this.formatters, + this.focusNode, + this.error, + this.autofillHints, + this.labelText, + this.hintText, + this.obscureText = false, + this.keyboardType, + this.maxLength, + super.key, + }); + + final TextEditingController controller; + final List? formatters; + final FocusNode? focusNode; + final ValueListenable? error; + final List? autofillHints; + final String? labelText; + final String? hintText; + final bool obscureText; + final TextInputType? keyboardType; + final int? maxLength; + + @override + State createState() => _SignInTextFieldState(); +} + +class _SignInTextFieldState extends State { + bool _obscurePassword = false; + FocusNode? focusNode; + + @override + void initState() { + super.initState(); + _obscurePassword = widget.obscureText; + focusNode = widget.focusNode?..addListener(_onFocusChanged); + } + + @override + void dispose() { + focusNode?.removeListener(_onFocusChanged); + super.dispose(); + } + + void _onFocusChanged() { + if (focusNode?.hasFocus == false && + mounted && + widget.obscureText && + !_obscurePassword) { + setState(() => _obscurePassword = true); + } + } + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: StateConsumer( + controller: AuthenticationScope.controllerOf(context), + builder: (context, state, _) => AnimatedOpacity( + opacity: state.isIdling ? 1 : .5, + duration: const Duration(milliseconds: 250), + child: ValueListenableBuilder( + valueListenable: widget.error ?? ValueNotifier(null), + builder: (context, error, child) => StatefulBuilder( + builder: (context, setState) => TextField( + focusNode: widget.focusNode, + enabled: state.isIdling, + maxLines: 1, + minLines: 1, + maxLength: widget.maxLength, + controller: widget.controller, + autocorrect: false, + autofillHints: widget.autofillHints, + keyboardType: widget.keyboardType, + inputFormatters: widget.formatters, + obscureText: _obscurePassword, + decoration: InputDecoration( + constraints: const BoxConstraints(maxHeight: 84), + labelText: widget.labelText, + hintText: widget.hintText, + helperText: '', + helperMaxLines: 1, + errorText: error ?? state.error, + errorMaxLines: 1, + suffixIcon: widget.obscureText + ? IconButton( + icon: Icon(_obscurePassword + ? Icons.visibility + : Icons.visibility_off), + onPressed: () => setState( + () => _obscurePassword = !_obscurePassword, + ), + ) + : null, + border: const OutlineInputBorder(), + ), + ), + ), + ), + ), + ), + ); +} + +/* class _UsernameTextFormatter extends TextInputFormatter { + const _UsernameTextFormatter(); + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) => + TextEditingValue( + text: newValue.text.toLowerCase(), + selection: newValue.selection, + ); +} */ diff --git a/example/lib/src/feature/authentication/widget/sign_in_screen.dart b/example/lib/src/feature/authentication/widget/sign_in_screen.dart deleted file mode 100644 index 59fc019..0000000 --- a/example/lib/src/feature/authentication/widget/sign_in_screen.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:spinifyapp/src/feature/authentication/widget/authentication_scope.dart'; - -/// {@template sign_in_screen} -/// SignInScreen widget. -/// {@endtemplate} -class SignInScreen extends StatelessWidget { - /// {@macro sign_in_screen} - const SignInScreen({super.key}); - - @override - Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: const Text('Sign In'), - ), - body: Center( - child: ElevatedButton( - onPressed: () => - AuthenticationScope.controllerOf(context).signInAnonymously(), - child: const Text('Sign In Anonymously'), - ), - ), - ); -} diff --git a/example/lib/src/feature/chat/widget/chat_screen.dart b/example/lib/src/feature/chat/widget/chat_screen.dart new file mode 100644 index 0000000..e63fcdb --- /dev/null +++ b/example/lib/src/feature/chat/widget/chat_screen.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:spinifyapp/src/common/controller/state_consumer.dart'; +import 'package:spinifyapp/src/feature/authentication/widget/authentication_scope.dart'; + +/// {@template chat_screen} +/// ChatScreen widget. +/// {@endtemplate} +class ChatScreen extends StatelessWidget { + /// {@macro chat_screen} + const ChatScreen({super.key}); + + @override + Widget build(BuildContext context) { + final authController = AuthenticationScope.controllerOf(context); + return StateConsumer( + controller: authController, + builder: (context, state, _) => Scaffold( + appBar: AppBar( + title: const Text('Chat'), + actions: [ + IconButton( + onPressed: state.user.isNotAuthenticated + ? null + : () => authController.signOut(), + icon: const Icon(Icons.logout), + ), + const SizedBox(width: 16), + ], + ), + body: const Center( + child: Text('Chat'), + ), + ), + ); + } +} diff --git a/example/lib/src/feature/dependencies/initialization/initialize_dependencies.dart b/example/lib/src/feature/dependencies/initialization/initialize_dependencies.dart index 0eaee93..29755c8 100644 --- a/example/lib/src/feature/dependencies/initialization/initialize_dependencies.dart +++ b/example/lib/src/feature/dependencies/initialization/initialize_dependencies.dart @@ -96,7 +96,7 @@ mixin InitializeDependencies { ( 'Authentication repository', (dependencies) => dependencies.authenticationRepository = - AuthenticationRepositoryFake(), + AuthenticationRepositoryImpl(), ), ( 'Fake delay 1',