Skip to content

Commit

Permalink
Add sign in form
Browse files Browse the repository at this point in the history
  • Loading branch information
PlugFox committed Aug 4, 2023
1 parent d442fa8 commit 1be87a1
Show file tree
Hide file tree
Showing 11 changed files with 619 additions and 61 deletions.
1 change: 1 addition & 0 deletions example/config/development.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"ENVIRONMENT": "development",
"CENTRIFUGE_BASE_URL": "http://localhost:8000",
"CENTRIFUGE_CHANNEL": "chat",
"CENTRIFUGE_TIMEOUT": 8000
}
6 changes: 5 additions & 1 deletion example/lib/src/common/constant/config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 7 additions & 6 deletions example/lib/src/common/widget/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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(),
),
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,17 +27,29 @@ final class AuthenticationController
final IAuthenticationRepository _repository;
StreamSubscription<AuthenticationState>? _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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<User> userChanges();
FutureOr<User> getUser();
Future<void> signInAnonymously();

/* Future<void> sendSignInWithEmailLink(String email);
Future<void> signInWithEmailLink(String email, String emailLink);
Future<void> signInWithEmailAndPassword(String email, String password);
Future<void> signInWithFacebook();
Future<void> signInWithApple();
Future<void> signInWithTwitter();
Future<void> signInWithGithub();
Future<void> signInWithPhoneNumber(String phoneNumber);
Future<void> sendPasswordResetEmail(String email);
Future<void> confirmPasswordReset(String code, String newPassword);
Future<void> signUpWithEmailAndPassword(String email, String password);
Future<void> deleteUser();
Future<bool> isSignedIn();
Future<void> signInWithGoogle();
Future<void> signOut(); */
Future<void> signIn(SignInData data);
Future<void> signOut();
}

class AuthenticationRepositoryFake implements IAuthenticationRepository {
class AuthenticationRepositoryImpl implements IAuthenticationRepository {
AuthenticationRepositoryImpl();

final StreamController<User> _userController =
StreamController<User>.broadcast();
User _user = const User.unauthenticated();
Expand All @@ -36,6 +24,13 @@ class AuthenticationRepositoryFake implements IAuthenticationRepository {
Stream<User> userChanges() => _userController.stream;

@override
Future<void> signInAnonymously() => Future<void>.sync(() => _userController
.add(_user = const User.authenticated(id: 'anonymous-user-id')));
Future<void> signIn(SignInData data) {
// TODO(plugfox): implement signIn
return Future<void>.sync(() => _userController
.add(_user = const User.authenticated(id: 'anonymous-user-id')));
}

@override
Future<void> signOut() => Future<void>.sync(
() => _userController.add(_user = const User.unauthenticated()));
}
72 changes: 72 additions & 0 deletions example/lib/src/feature/authentication/model/sign_in_data.dart
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand All @@ -36,6 +38,7 @@ class AuthenticationScope extends StatefulWidget {
class _AuthenticationScopeState extends State<AuthenticationScope> {
late final AuthenticationController _authenticationController;
User _user = const User.unauthenticated();
bool _showForm = true;

@override
void initState() {
Expand All @@ -55,17 +58,56 @@ class _AuthenticationScopeState extends State<AuthenticationScope> {

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: <Widget>[
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,
),
),
),
),
),
),
],
)),
),
);
}

Expand Down
Loading

0 comments on commit 1be87a1

Please sign in to comment.