Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/app/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ targets:
- lib/src/utils/**.dart
- lib/src/features/*/domain/*_model.dart
- lib/src/features/*/domain/*_entity.dart
options:
checked: true
riverpod_generator:
# configs for the @riverpod generator
generate_for:
Expand Down
11 changes: 4 additions & 7 deletions packages/app/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,16 @@
library;

import 'package:flutter/widgets.dart';
import 'package:shared_preferences/shared_preferences.dart';

import 'src/app/app.dart';
import 'src/app/bootstrap.dart';
import 'src/utils/api.dart';

/// The primary entrypoint of the app.
///
/// This uses [Bootstrap] to launch [App].
Future<void> main() async {
await const App().bootstrap(
(
runApp: runApp,
getSharedPreferences: SharedPreferencesWithCache.create,
),
);
const env = (runApp: runApp, createClient: createClient);

await const App().bootstrap(env);
}
24 changes: 11 additions & 13 deletions packages/app/lib/src/app/bootstrap.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,27 @@ library;
// `riverpod_lint` doesn't recognize that this is the root of the app.
// ignore_for_file: scoped_providers_should_specify_dependencies

import 'package:appwrite/appwrite.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:flutter_web_plugins/url_strategy.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';

import '../features/settings/application/settings_service.dart';
import '../features/settings/data/preferences_repository.dart';
import '../utils/api.dart';

/// The signature of [runApp].
typedef RunApp = void Function(Widget app);

/// The signature of [SharedPreferencesWithCache.create].
typedef GetSharedPreferences = Future<SharedPreferencesWithCache> Function({
required SharedPreferencesWithCacheOptions cacheOptions,
Map<String, Object?>? cache,
});
/// The signature of [createClient]
typedef CreateClient = Client Function();

/// The environment needed to bootstrap the app.
typedef BootstrapEnv = ({
RunApp runApp,
GetSharedPreferences getSharedPreferences,
CreateClient createClient,
});

/// Turn any widget into a flow-blown app.
Expand All @@ -40,7 +38,7 @@ mixin Bootstrap implements Widget {
/// - initializing riverpod's [ProviderScope], and
/// - running the app with [runApp].
Future<void> bootstrap(BootstrapEnv env) async {
final (:runApp, :getSharedPreferences) = env;
final (:runApp, :createClient) = env;

// Don't use hash style routes.
usePathUrlStrategy();
Expand All @@ -49,10 +47,9 @@ mixin Bootstrap implements Widget {
WidgetsFlutterBinding.ensureInitialized();

// Load the user's preferences.
final prefs = await getSharedPreferences(
cacheOptions: const SharedPreferencesWithCacheOptions(),
);
final initialSettings = await loadSettings(prefs);
final client = createClient();
final account = Account(client);
final initialSettings = await loadSettings(account);

// Reset splash screen.
FlutterNativeSplash.remove();
Expand All @@ -65,7 +62,8 @@ mixin Bootstrap implements Widget {
runApp(
ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
clientProvider.overrideWithValue(client),
accountsProvider.overrideWithValue(account),
initialSettingsProvider.overrideWithValue(initialSettings),
],
child: RestorationScope(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ base class AuthService extends _$AuthService {

/// Creates a new user in the Appwrite database.
Future<void> createUser(String name, String email, String password) async {
// Set the state to loading.
state = const AsyncValue.loading();

// Try to create the user. If it fails, set the state to error.
// Note that expected errors are already converted to null.
state = await AsyncValue.guard(
Expand Down
24 changes: 14 additions & 10 deletions packages/app/lib/src/features/auth/data/auth_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ abstract interface class AuthRepository {
/// Throws an [AppwriteException] if
/// - the user already exists, or
/// - the password is too weak.
Future<User> createUser(String name, String email, String password);
Future<User?> createUser(String name, String email, String password);

/// Log the user in.
///
Expand All @@ -38,16 +38,20 @@ final class _AppwriteAuthRepository implements AuthRepository {
final Account account;

@override
Future<User> createUser(String name, String email, String password) async {
final user = await account.create(
userId: ID.unique(),
email: email,
password: password,
name: name,
);
await logInUser(email, password);
Future<User?> createUser(String name, String email, String password) async {
try {
final user = await account.create(
userId: ID.unique(),
email: email,
password: password,
name: name,
);
await logInUser(email, password);

return user;
return user;
} on AppwriteException {
return null;
}
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';

import '../../../../app/router.gr.dart';
import '../../../../gen/assets.gen.dart';
import '../../../../utils/toast.dart';
import '../../application/auth_service.dart';

// TODO(lishaduck): Rename to `LogInPage`.
Expand Down Expand Up @@ -53,10 +54,8 @@ class LoginPage extends HookConsumerWidget {
}
} else {
// TODO(lishaduck): Move this to the guard.
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Invalid username and password'),
),
context.showSnackBar(
content: const Text('Invalid username and password'),
);
}
}
Expand Down Expand Up @@ -84,33 +83,18 @@ class LoginPage extends HookConsumerWidget {
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
// TODO(MattsAttack): Find a better color for this (use `Theme.of(context).<someColor>`).
color: const Color.fromARGB(
255,
34,
29,
43,
),
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(15),
),
child: Form(
key: formKey,
child: Column(
children: [
const DecoratedBox(
// TODO(MattsAttack): Redesign this, it was for testing.
decoration: BoxDecoration(
// color: Colors.white,
// border: Border.all(color: Colors.white),
// borderRadius: BorderRadius.circular(5),
),
child: Text(
'Welcome to Nexus!',
style: TextStyle(
fontSize: 28,
// TODO(MattsAttack): Use `Theme.of(context).<someColor>`.
color: Color.fromARGB(255, 221, 168, 230),
),
Text(
'Welcome to Nexus!',
style: TextStyle(
fontSize: 28,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
const SizedBox(height: 32),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';

import '../../../../app/router.gr.dart';
import '../../../../gen/assets.gen.dart';
import '../../../../utils/toast.dart';
import '../../application/auth_service.dart';

// TODO(lishaduck): Extract most of this out to a widget that can be shared with the log in page.
Expand Down Expand Up @@ -54,10 +55,8 @@ class SignupPage extends HookConsumerWidget {
}
} else {
// TODO(lishaduck): Move this to the guard.
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Invalid username or password'),
),
context.showSnackBar(
content: const Text('Invalid username or password'),
);
}
}
Expand Down Expand Up @@ -85,33 +84,18 @@ class SignupPage extends HookConsumerWidget {
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
// TODO(MattsAttack): Find a better color for this (use `Theme.of(context).<someColor>`).
color: const Color.fromARGB(
255,
34,
29,
43,
),
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(15),
),
child: Form(
key: formKey,
child: Column(
children: [
const DecoratedBox(
// TODO(MattsAttack): Redesign this, it was for testing.
decoration: BoxDecoration(
// color: Colors.white,
// border: Border.all(color: Colors.white),
// borderRadius: BorderRadius.circular(5),
),
child: Text(
'Welcome to Nexus!',
style: TextStyle(
fontSize: 28,
// TODO(MattsAttack): Use `Theme.of(context).<someColor>`.
color: Color.fromARGB(255, 221, 168, 230),
),
Text(
'Welcome to Nexus!',
style: TextStyle(
fontSize: 28,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
const SizedBox(height: 32),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
/// This file provides a service to manage local user settings.
library;

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

Expand Down Expand Up @@ -31,9 +29,7 @@ class SettingsService extends _$SettingsService {
state = state.copyWith(themeMode: mode);

// Persist the changes to a local database or the internet using the PreferencesRepository.
await ref
.read(preferencesRepositoryProvider)
.setString('prefs', json.encode(state));
await ref.read(preferencesRepositoryProvider).update(state);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/// This library provides the ability to fetch and persist the user's settings.
library;

import 'dart:convert';

import 'package:appwrite/appwrite.dart';
import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';

import '../../../utils/api.dart';
import '../domain/settings_model.dart';

part 'preferences_repository.g.dart';
Expand All @@ -17,46 +17,46 @@ abstract interface class PreferencesRepository {
Future<SettingsModel> load();

/// Persist the user's settings to a local database or the internet.
Future<void> setString(String key, String value);
Future<void> update(SettingsModel newPrefs);
}

final class _SharedPreferencesRepository implements PreferencesRepository {
_SharedPreferencesRepository(this.prefs);
final class _AppwritePreferencesRepository implements PreferencesRepository {
_AppwritePreferencesRepository(this.account);

final SharedPreferencesWithCache prefs;
final Account account;

@override
Future<SettingsModel> load() => loadSettings(prefs);
Future<SettingsModel> load() => loadSettings(account);

@override
Future<void> setString(String key, String value) async =>
await prefs.setString(key, value);
Future<void> update(SettingsModel newPrefs) async =>
await account.updatePrefs(prefs: newPrefs.toJson());
}

/// Get the user's preferences.
@Riverpod(keepAlive: true)
PreferencesRepository preferencesRepository(PreferencesRepositoryRef ref) {
final prefs = ref.watch(sharedPreferencesProvider);
final account = ref.watch(accountsProvider);

return _SharedPreferencesRepository(prefs);
return _AppwritePreferencesRepository(account);
}

/// Load a from a local database.
/// Load preferences from Appwrite.
///
/// Returns [defaultSettings] if anything goes wrong.
Future<SettingsModel> loadSettings(
SharedPreferencesWithCache prefs,
Account account,
) async {
final data = prefs.getString('prefs');
final Object? decoded = data == null ? null : json.decode(data);

return decoded is Map<String, Object?>
? SettingsModel.fromJson(decoded)
: defaultSettings;
}

/// Get a [SharedPreferencesWithCache] instance.
@Riverpod(keepAlive: true)
SharedPreferencesWithCache sharedPreferences(SharedPreferencesRef ref) {
throw UnimplementedError();
try {
final data = await account.getPrefs();

return SettingsModel.fromJson(data.data);
} on AppwriteException {
// TODO: Also handle invalid JSON.
return defaultSettings;
} on CheckedFromJsonException {
return defaultSettings;
}
}

/// The default settings, in case the user has none or they are corrupted.
Expand Down
Loading