Skip to content

Commit

Permalink
Add login screen, fix guards, check initial state
Browse files Browse the repository at this point in the history
  • Loading branch information
PlugFox committed Nov 29, 2023
1 parent a0b9bf4 commit 24166f8
Show file tree
Hide file tree
Showing 10 changed files with 749 additions and 44 deletions.
106 changes: 106 additions & 0 deletions example/lib/src/common/constant/config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// ignore_for_file: avoid_classes_with_only_static_members

/// Config for app.
abstract final class Config {
// --- ENVIRONMENT --- //

/// Environment flavor.
/// e.g. development, staging, production
static final EnvironmentFlavor environment = EnvironmentFlavor.from(
const String.fromEnvironment('ENVIRONMENT', defaultValue: 'development'));

// --- API --- //

/// Base url for api.
/// e.g. https://api.vexus.io
static const String apiBaseUrl = String.fromEnvironment('API_BASE_URL',
defaultValue: 'https://api.domain.tld');

/// Timeout in milliseconds for opening url.
/// [Dio] will throw the [DioException] with [DioExceptionType.connectTimeout] type when time out.
/// e.g. 15000
static const Duration apiConnectTimeout = Duration(
milliseconds:
int.fromEnvironment('API_CONNECT_TIMEOUT', defaultValue: 15000));

/// Timeout in milliseconds for receiving data from url.
/// [Dio] will throw the [DioException] with [DioExceptionType.receiveTimeout] type when time out.
/// e.g. 10000
static const Duration apiReceiveTimeout = Duration(
milliseconds:
int.fromEnvironment('API_RECEIVE_TIMEOUT', defaultValue: 10000));

/// Cache lifetime.
/// Refetch data from url when cache is expired.
/// e.g. 1 hour
static const Duration cacheLifetime = Duration(hours: 1);

// --- DATABASE --- //

/// Whether to drop database on start.
/// e.g. true
static const bool dropDatabase =
bool.fromEnvironment('DROP_DATABASE', defaultValue: false);

/// Database file name by default.
/// e.g. sqlite means "sqlite.db" for native platforms and "sqlite" for web platform.
static const String databaseName =
String.fromEnvironment('DATABASE_NAME', defaultValue: 'sqlite');

// --- AUTHENTICATION --- //

/// Minimum length of password.
/// e.g. 8
static const int passwordMinLength =
int.fromEnvironment('PASSWORD_MIN_LENGTH', defaultValue: 8);

/// Maximum length of password.
/// e.g. 32
static const int passwordMaxLength =
int.fromEnvironment('PASSWORD_MAX_LENGTH', defaultValue: 32);

// --- LAYOUT --- //

/// Maximum screen layout width for screen with list view.
static const int maxScreenLayoutWidth =
int.fromEnvironment('MAX_LAYOUT_WIDTH', defaultValue: 768);
}

/// Environment flavor.
/// e.g. development, staging, production
enum EnvironmentFlavor {
/// Development
development('development'),

/// Staging
staging('staging'),

/// Production
production('production');

/// {@nodoc}
const EnvironmentFlavor(this.value);

/// {@nodoc}
factory EnvironmentFlavor.from(String? value) =>
switch (value?.trim().toLowerCase()) {
'development' || 'debug' || 'develop' || 'dev' => development,
'staging' || 'profile' || 'stage' || 'stg' => staging,
'production' || 'release' || 'prod' || 'prd' => production,
_ => const bool.fromEnvironment('dart.vm.product')
? production
: development,
};

/// development, staging, production
final String value;

/// Whether the environment is development.
bool get isDevelopment => this == development;

/// Whether the environment is staging.
bool get isStaging => this == staging;

/// Whether the environment is production.
bool get isProduction => this == production;
}
70 changes: 70 additions & 0 deletions example/lib/src/common/router/authentication_guard.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'dart:async';

import 'package:example/src/feature/authentication/model/user.dart';
import 'package:octopus/octopus.dart';

/// A router guard that checks if the user is authenticated.
class AuthenticationGuard extends OctopusGuard {
AuthenticationGuard({
required FutureOr<User> Function() getUser,
required Set<String> routes,
required OctopusState signinNavigation,
required OctopusState homeNavigation,
super.refresh,
}) : _getUser = getUser,
_routes = routes,
_lastNavigation = homeNavigation,
_signinNavigation = signinNavigation;

/// Get the current user.
final FutureOr<User> Function() _getUser;

/// Routes names that stand for the authentication routes.
final Set<String> _routes;

/// The navigation to use when the user is not authenticated.
final OctopusState _signinNavigation;

/// The navigation to use when the user is authenticated.
OctopusState _lastNavigation;

@override
FutureOr<OctopusState?> call(
List<OctopusState> history,
OctopusState state,
) async {
final user = await _getUser(); // Get the current user.
final isAuthNav =
state.children.any((child) => _routes.contains(child.name));
if (isAuthNav) {
// New state is an authentication navigation.
if (user.isAuthenticated) {
// User authenticated.
// Remove any navigation that is an authentication navigation.
state.removeWhere((child) => _routes.contains(child.name));
// Restore the last navigation when the user is authenticated
// if the state contains only the authentication routes.
return state.isEmpty ? _lastNavigation : state;
} else {
// User not authenticated.
// Remove any navigation that is not an authentication navigation.
state.removeWhere((child) => !_routes.contains(child.name));
// Add the signin navigation if the state is empty.
// Or return the state if it contains the signin navigation.
return state.isEmpty ? _signinNavigation : state;
}
} else {
// New state is not an authentication navigation.
if (user.isAuthenticated) {
// User authenticated.
// Save the current navigation as the last navigation.
_lastNavigation = state;
return super.call(history, state);
} else {
// User not authenticated.
// Replace the current navigation with the signin navigation.
return _signinNavigation;
}
}
}
}
37 changes: 34 additions & 3 deletions example/lib/src/common/widget/app.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import 'package:example/src/common/model/dependencies.dart';
import 'package:example/src/common/router/authentication_guard.dart';
import 'package:example/src/common/router/routes.dart';
import 'package:example/src/common/widget/router_state_observer.dart';
import 'package:example/src/feature/authentication/widget/authentication_scope.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:octopus/octopus.dart';
Expand All @@ -17,11 +20,36 @@ class App extends StatefulWidget {

class _AppState extends State<App> {
late final Octopus router;
late final ValueNotifier<List<({Object error, StackTrace stackTrace})>>
errorsObserver;

@override
void initState() {
final dependencies = Dependencies.of(context);
errorsObserver =
ValueNotifier<List<({Object error, StackTrace stackTrace})>>(
<({Object error, StackTrace stackTrace})>[],
);
router = Octopus(
routes: Routes.values,
defaultRoute: Routes.home,
guards: <IOctopusGuard>[
AuthenticationGuard(
getUser: () => dependencies.authenticationController.state.user,
routes: <String>{
Routes.signin.name,
Routes.signup.name,
},
signinNavigation: OctopusState.single(Routes.signin.node()),
homeNavigation: OctopusState.single(Routes.home.node()),
refresh: dependencies.authenticationController,
),
],
onError: (error, stackTrace) =>
errorsObserver.value = <({Object error, StackTrace stackTrace})>[
(error: error, stackTrace: stackTrace),
...errorsObserver.value,
],
/* observers: <NavigatorObserver>[
HeroController(),
], */
Expand Down Expand Up @@ -50,9 +78,12 @@ class _AppState extends State<App> {
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.noScaling,
),
child: RouterStateObserver(
listenable: router.stateObserver,
child: child!,
child: AuthenticationScope(
child: RouterStateObserver(
octopus: router,
errorsObserver: errorsObserver,
child: child!,
),
),
),
);
Expand Down
Loading

0 comments on commit 24166f8

Please sign in to comment.