refactor controllers

This commit is contained in:
SFJ 2025-03-18 13:48:08 +01:00
parent ec520c330a
commit 9603651e05
17 changed files with 364 additions and 359 deletions

7
git-authors.toml Normal file
View File

@ -0,0 +1,7 @@
[authors.mtk]
name = "Mikkel Troels Kongsted"
email = "mtkongsted@gmail.com"
[authors.sfj]
name = "SFJ"
email = "simonfromjakobsen@gmail.com"

View File

@ -119,9 +119,9 @@ class CartControllerMemory extends CartController {
Future<Result<Null, String>> purchase(String token) async { Future<Result<Null, String>> purchase(String token) async {
final res = await server.purchaseCart(token, cart); final res = await server.purchaseCart(token, cart);
switch (res) { switch (res) {
case Success<Null>(): case Ok<Null, String>():
return const Ok(null); return const Ok(null);
case Error<Null>(message: final message): case Err<Null, String>(value: final message):
return Err(message); return Err(message);
} }
} }

View File

@ -15,10 +15,10 @@ class ProductController extends ChangeNotifier {
Future<void> fetchProductsFromServer() async { Future<void> fetchProductsFromServer() async {
final res = await server.allProducts(); final res = await server.allProducts();
switch (res) { switch (res) {
case Success<List<Product>>(data: final data): case Ok<List<Product>, String>(value: final data):
products = data; products = data;
notifyListeners(); notifyListeners();
case Error<List<Product>>(): case Err<List<Product>, String>():
return; return;
} }
} }

View File

@ -0,0 +1,163 @@
import 'package:flutter/material.dart';
import 'package:mobile/models/user.dart';
import 'package:mobile/results.dart';
import 'package:mobile/server/server.dart';
class SessionController {
final Server server;
String? _sessionToken;
User? _sessionUser;
final List<_ChangeListener> _sessionChangeListeners = [];
final List<_ChangeListener> _userChangeListeners = [];
SessionController({required this.server});
Future<Result<Null, String>> loginUser(String email, String password) async {
final loginResult = await server.login(email, password);
switch (loginResult) {
case Ok<String, String>(value: final sessionToken):
_sessionToken = sessionToken;
notifySessionChangeListeners();
return const Ok(null);
case Err<String, String>(value: final message):
return Err(message);
}
}
Future<Result<Null, Null>> loadUser() async {
// TODO: retrieve session from cache, if exists
return _loadCurrentUser();
}
Future<Result<Null, Null>> _loadCurrentUser() async {
final sessionUserResult = await _requestWithSession<User>(
(server, sessionToken) => server.sessionUser(sessionToken));
switch (sessionUserResult) {
case Ok<User, String>(value: final sessionUser):
_sessionUser = sessionUser;
notifyUserChangeListeners();
// The mechanism for checking that a user is logged in, only listens on
// the session provider. There we also notify sessions listeners, to
// account for this one specific case. Is this smart? idk.
notifySessionChangeListeners();
return const Ok(null);
case Err<User, String>():
return const Err(null);
}
}
Future<Null> logout() async {
final sessionToken = _sessionToken;
if (sessionToken == null) {
return;
}
await server.logout(sessionToken);
_sessionToken = null;
notifySessionChangeListeners();
}
User get user {
final user = _sessionUser;
if (user == null) {
throw NoUser();
}
return user;
}
bool get hasUser {
return _sessionUser != null;
}
Future<Result<Null, String>> addBalance() async {
final addBalanceResult = await _requestWithSession(
(server, sessionToken) => server.addBalance(sessionToken));
if (addBalanceResult case Err<Null, String>(value: final message)) {
return Err(message);
}
if (await _loadCurrentUser() case Err<Null, Null>()) {
return const Err("could not fetch user");
}
return const Ok(null);
}
/// Package private.
Future<Result<T, String>> _requestWithSession<T>(
Future<Result<T, String>> Function(Server server, String sessionToken)
func) async {
final sessionToken = _sessionToken;
if (sessionToken == null) {
return const Err("unathorized");
}
final result = await func(server, sessionToken);
if (result case Err<T, String>(value: final message)) {
if (message == "unauthorized") {
_sessionToken = null;
_sessionUser = null;
notifySessionChangeListeners();
notifyUserChangeListeners();
return const Err("unathorized");
}
}
return result;
}
/// Package private.
void _addSessionChangeListener(_ChangeListener listener) {
_sessionChangeListeners.add(listener);
}
/// Package private.
void _addUserChangeListener(_ChangeListener listener) {
_userChangeListeners.add(listener);
}
/// Class private.
void notifySessionChangeListeners() {
for (final listener in _sessionChangeListeners) {
listener.notify();
}
}
/// Class private.
void notifyUserChangeListeners() {
for (final listener in _userChangeListeners) {
listener.notify();
}
}
}
abstract class _ChangeListener {
void notify();
}
class NoUser implements Exception {}
class SessionProvider extends ChangeNotifier implements _ChangeListener {
final SessionController controller;
SessionProvider({required this.controller}) {
controller._addSessionChangeListener(this);
}
@override
void notify() {
notifyListeners();
}
}
class CurrentUserProvider extends ChangeNotifier implements _ChangeListener {
final SessionController controller;
CurrentUserProvider({required this.controller}) {
controller._addUserChangeListener(this);
}
@override
void notify() {
notifyListeners();
}
}

View File

@ -1,132 +0,0 @@
import 'package:flutter/material.dart';
import 'package:mobile/models/user.dart';
import 'package:mobile/results.dart';
import 'package:mobile/server/server.dart';
class UserController extends ChangeNotifier {
final Server server;
String? _sessionToken;
User? _user;
Future<Result<User, Null>> userLoad = Future.error(Null);
UserController({required this.server});
/// Make sure a user exists before calling using `.loadUser()`.
User get user {
final user = _user;
if (user == null) {
throw NoUserExcept();
}
return user;
}
Future<Result<Null, Null>> loadUser() async {
if (_sessionToken == null) {
return const Err(null);
}
final userResult = await server.sessionUser(_sessionToken!);
switch (userResult) {
case Success<User>(data: final user):
_user = user;
return const Ok(null);
case Error<User>():
return const Err(null);
}
}
Future<Result<Null, Null>> loadedUser() async {
throw Exception();
}
Future<Result<Null, String>> login(String email, String password) async {
switch (await server.login(email, password)) {
case Success<String>(data: final token):
_sessionToken = token;
notifyListeners();
return const Ok(null);
case Error<String>(message: final message):
notifyListeners();
return Err(message);
}
}
Future<void> _validateToken() async {
final token = _sessionToken;
if (token == null) {
return;
}
final res = await server.sessionUser(token);
switch (res) {
case Success<User>():
return;
case Error<User>():
_sessionToken = null;
return;
}
}
@Deprecated("Use 'user' instead.")
User? get userOld {
loadUserOld();
return _user;
}
Future<void> _notifyIfTokenChanged() async {
final prev = _sessionToken;
_validateToken();
if (prev != _sessionToken) {
notifyListeners();
}
}
@Deprecated("Use 'loadUser' instead.")
Future<void> loadUserOld() async {
final token = _sessionToken;
if (token == null) {
_user = null;
return;
}
final res = await server.sessionUser(token);
switch (res) {
case Success<User>(data: final user):
_user = user;
return;
case Error<User>():
_user = null;
return;
}
}
String? get sessionToken {
_notifyIfTokenChanged();
return _sessionToken;
}
Future<void> logout() async {
final token = _sessionToken;
if (token != null) {
await server.logout(token);
_sessionToken = null;
}
notifyListeners();
}
Future<Result<Null, String>> addBalance() async {
final token = _sessionToken;
if (token == null) {
return const Err("No token");
}
final res = await server.addBalance(token);
notifyListeners();
switch (res) {
case Success<Null>():
return const Ok(null);
case Error<Null>(message: final message):
return Err(message);
}
}
}
class NoUserExcept implements Exception {}

View File

@ -10,9 +10,9 @@ class UsersController {
String name, String email, String password) async { String name, String email, String password) async {
final res = await server.register(name, email, password); final res = await server.register(name, email, password);
switch (res) { switch (res) {
case Success<Null>(): case Ok<Null, String>():
return const Ok(null); return const Ok(null);
case Error<Null>(message: final message): case Err<Null, String>(value: final message):
return Err(message); return Err(message);
} }
} }

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:mobile/controllers/user.dart'; import 'package:mobile/controllers/session.dart';
import 'package:mobile/pages/dashboard.dart'; import 'package:mobile/pages/dashboard.dart';
import 'package:mobile/pages/log_in_page.dart'; import 'package:mobile/pages/log_in_page.dart';
import 'package:mobile/controllers/add_to_cart_state.dart'; import 'package:mobile/controllers/add_to_cart_state.dart';
@ -10,6 +10,7 @@ import 'package:mobile/controllers/paying_state.dart';
import 'package:mobile/controllers/product.dart'; import 'package:mobile/controllers/product.dart';
import 'package:mobile/controllers/receipt.dart'; import 'package:mobile/controllers/receipt.dart';
import 'package:mobile/controllers/users.dart'; import 'package:mobile/controllers/users.dart';
import 'package:mobile/results.dart';
import 'package:mobile/server/backend_server.dart'; import 'package:mobile/server/backend_server.dart';
import 'package:mobile/server/server.dart'; import 'package:mobile/server/server.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -17,29 +18,38 @@ import 'package:mobile/controllers/routing.dart';
void main() { void main() {
final server = BackendServer(); final server = BackendServer();
final users = UsersController(server: server); final usersController = UsersController(server: server);
final sessionController = SessionController(server: server);
final user = UserController(server: server); sessionController.loadUser();
user.loadUser().ignore();
runApp(MyApp( runApp(MyApp(
users: users, usersController: usersController,
sessionController: sessionController,
server: server, server: server,
)); ));
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
final UsersController users; final UsersController usersController;
final SessionController sessionController;
final Server server; final Server server;
const MyApp({super.key, required this.users, required this.server}); const MyApp(
{super.key,
required this.usersController,
required this.sessionController,
required this.server});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiProvider( return MultiProvider(
providers: [ providers: [
ChangeNotifierProvider(create: (_) => UserController(server: server)), ChangeNotifierProvider(
create: (_) => SessionProvider(controller: sessionController)),
ChangeNotifierProvider(
create: (_) => CurrentUserProvider(controller: sessionController)),
ChangeNotifierProvider(create: (_) => RoutingController()), ChangeNotifierProvider(create: (_) => RoutingController()),
ChangeNotifierProvider( ChangeNotifierProvider(
create: (_) => ProductController(server: server)), create: (_) => ProductController(server: server)),
@ -49,7 +59,7 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider(create: (_) => PayingStateController()), ChangeNotifierProvider(create: (_) => PayingStateController()),
ChangeNotifierProvider(create: (_) => AddToCartStateController()), ChangeNotifierProvider(create: (_) => AddToCartStateController()),
ChangeNotifierProvider(create: (_) => LocationImageController()), ChangeNotifierProvider(create: (_) => LocationImageController()),
Provider(create: (_) => users), Provider(create: (_) => usersController),
], ],
child: MaterialApp( child: MaterialApp(
title: 'Fresh Plaza', title: 'Fresh Plaza',
@ -61,12 +71,24 @@ class MyApp extends StatelessWidget {
GoogleFonts.merriweatherTextTheme(Theme.of(context).textTheme), GoogleFonts.merriweatherTextTheme(Theme.of(context).textTheme),
useMaterial3: true, useMaterial3: true,
), ),
home: Consumer<UserController>( home: Consumer<SessionProvider>(
builder: (_, sessionController, __) { builder: (_, provider, ___) {
if (sessionController.sessionToken is String) { if (provider.controller.hasUser) {
return Dashboard(); return Dashboard();
} }
return const LogInPage(); return FutureBuilder(
future: provider.controller.loadUser(),
builder: (_, snapshot) {
final error = snapshot.error;
if (error != null) {
throw error;
}
if (snapshot.data != null &&
snapshot.data is Err<Null, Null>) {
return const LoginPage();
}
return const Scaffold(body: CircularProgressIndicator());
});
}, },
)), )),
); );

View File

@ -3,7 +3,7 @@ import 'package:mobile/controllers/routing.dart';
import 'package:mobile/controllers/cart.dart'; import 'package:mobile/controllers/cart.dart';
import 'package:mobile/controllers/paying_state.dart'; import 'package:mobile/controllers/paying_state.dart';
import 'package:mobile/controllers/receipt.dart'; import 'package:mobile/controllers/receipt.dart';
import 'package:mobile/controllers/user.dart'; import 'package:mobile/controllers/session.dart';
import 'package:mobile/results.dart'; import 'package:mobile/results.dart';
import 'package:mobile/utils/price.dart'; import 'package:mobile/utils/price.dart';
import 'package:mobile/widgets/primary_button.dart'; import 'package:mobile/widgets/primary_button.dart';
@ -57,42 +57,42 @@ class FinishShoppingPage extends StatelessWidget {
child: Center( child: Center(
child: PrimaryButton( child: PrimaryButton(
onPressed: () async { onPressed: () async {
final session = context.read<UserController>(); // final session = context.read<SessionController>();
payingStateRepo.next(); // payingStateRepo.next();
await Future.delayed(const Duration(seconds: 1)); // await Future.delayed(const Duration(seconds: 1));
if (cartController.purchase(session.sessionToken!) // if (cartController.purchase(session.sessionToken!)
is Err) { // is Err) {
if (context.mounted) { // if (context.mounted) {
showDialog<String>( // showDialog<String>(
context: context, // context: context,
builder: (BuildContext context) => // builder: (BuildContext context) =>
AlertDialog( // AlertDialog(
content: const Text( // content: const Text(
'Du har desværre ikke råd til at købe dette'), // 'Du har desværre ikke råd til at købe dette'),
actions: <Widget>[ // actions: <Widget>[
TextButton( // TextButton(
onPressed: () => // onPressed: () =>
Navigator.pop(context, 'OK'), // Navigator.pop(context, 'OK'),
child: const Text('OK'), // child: const Text('OK'),
), // ),
], // ],
), // ),
); // );
} // }
payingStateRepo.reset(); // payingStateRepo.reset();
return; // return;
} // }
receiptRepo.createReceipt(cart); // receiptRepo.createReceipt(cart);
payingStateRepo.next(); // payingStateRepo.next();
await Future.delayed(const Duration(seconds: 1)); // await Future.delayed(const Duration(seconds: 1));
cartController.clearCart(); // cartController.clearCart();
payingStateRepo.reset(); // payingStateRepo.reset();
if (context.mounted) { // if (context.mounted) {
Navigator.pop(context); // Navigator.pop(context);
final RoutingController routing = // final RoutingController routing =
context.read<RoutingController>(); // context.read<RoutingController>();
routing.routeTo(PageSelector.homePage); // routing.routeTo(PageSelector.homePage);
} // }
}, },
child: const Text("Betal"))), child: const Text("Betal"))),
), ),

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mobile/controllers/user.dart'; import 'package:mobile/controllers/session.dart';
import 'package:mobile/pages/settings_page.dart'; import 'package:mobile/pages/settings_page.dart';
import 'package:mobile/utils/build_if_session_exists.dart';
import 'package:mobile/utils/price.dart'; import 'package:mobile/utils/price.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -10,8 +9,6 @@ class HomePage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final userController = context.watch<UserController>();
return Column( return Column(
children: [ children: [
Row( Row(
@ -30,29 +27,33 @@ class HomePage extends StatelessWidget {
), ),
Card( Card(
child: Container( child: Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10)), borderRadius: BorderRadius.all(Radius.circular(10)),
color: Color(0xFFFFFFFF), color: Colors.white,
), ),
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: BuildIfSessionUserExists( child: Consumer<CurrentUserProvider>(
sessionController: userController, builder: (_, provider, ___) {
placeholder: const CircularProgressIndicator(), final user = provider.controller.user;
builder: (context, user) => Text( return Text("Saldo: ${formatDkkCents(user.balanceDkkCents)}",
"Saldo: ${formatDkkCents(user.balanceDkkCents)}", style: Theme.of(context).textTheme.headlineSmall);
style: Theme.of(context).textTheme.headlineSmall))), },
),
),
), ),
Expanded( Expanded(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
BuildIfSessionUserExists( Consumer<CurrentUserProvider>(
sessionController: userController, builder: (_, provider, ___) {
placeholder: const CircularProgressIndicator(), final user = provider.controller.user;
builder: (context, user) => Text( return Text(
"Velkommen ${user.name}", "Velkommen ${user.name}",
style: Theme.of(context).textTheme.headlineMedium, style: Theme.of(context).textTheme.headlineMedium,
)) );
},
)
], ],
), ),
), ),

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mobile/controllers/user.dart'; import 'package:mobile/controllers/session.dart';
import 'package:mobile/pages/register_page.dart'; import 'package:mobile/pages/register_page.dart';
import 'package:mobile/results.dart'; import 'package:mobile/results.dart';
import 'package:mobile/widgets/error_box.dart'; import 'package:mobile/widgets/error_box.dart';
@ -7,30 +7,30 @@ import 'package:mobile/widgets/primary_button.dart';
import 'package:mobile/widgets/primary_input.dart'; import 'package:mobile/widgets/primary_input.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class LogInPage extends StatelessWidget { class LoginPage extends StatelessWidget {
const LogInPage({super.key}); const LoginPage({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Scaffold( return const Scaffold(
body: Row( body: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [LogInForm()])); children: [LoginForm()]));
} }
} }
class LogInForm extends StatefulWidget { class LoginForm extends StatefulWidget {
const LogInForm({super.key}); const LoginForm({super.key});
@override @override
State<StatefulWidget> createState() => LogInFormState(); State<StatefulWidget> createState() => LoginFormState();
} }
class LogInFormState extends State<LogInForm> { class LoginFormState extends State<LoginForm> {
bool loginError = false; bool loginError = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final userController = context.read<UserController>(); final sessionProvider = context.read<SessionProvider>();
final mailController = TextEditingController(); final mailController = TextEditingController();
final passwordController = TextEditingController(); final passwordController = TextEditingController();
@ -63,8 +63,8 @@ class LogInFormState extends State<LogInForm> {
), ),
PrimaryButton( PrimaryButton(
onPressed: () async { onPressed: () async {
final loginResult = await userController.login( final loginResult = await sessionProvider.controller
mailController.text, passwordController.text); .loginUser(mailController.text, passwordController.text);
switch (loginResult) { switch (loginResult) {
case Ok<Null, String>(): case Ok<Null, String>():
setState(() => loginError = false); setState(() => loginError = false);

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mobile/controllers/user.dart'; import 'package:mobile/controllers/session.dart';
import 'package:mobile/pages/settings_pages/saldo.dart'; import 'package:mobile/pages/settings_pages/saldo.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -30,9 +30,11 @@ class SettingsPage extends StatelessWidget {
icon: Icons.door_back_door, icon: Icons.door_back_door,
title: "Log ud", title: "Log ud",
action: (context) async { action: (context) async {
final sessionsController = context.read<UserController>(); final sessionProvider = context.read<SessionProvider>();
Navigator.pop(context); await sessionProvider.controller.logout();
await sessionsController.logout(); if (context.mounted) {
Navigator.pop(context);
}
}), }),
]; ];

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mobile/controllers/user.dart'; import 'package:mobile/controllers/session.dart';
import 'package:mobile/results.dart'; import 'package:mobile/results.dart';
import 'package:mobile/utils/build_if_session_exists.dart'; import 'package:mobile/utils/build_if_session_exists.dart';
import 'package:mobile/utils/price.dart'; import 'package:mobile/utils/price.dart';
@ -10,7 +10,6 @@ class SaldoSettingsPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sessionController = context.watch<UserController>();
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
body: SafeArea( body: SafeArea(
@ -26,22 +25,30 @@ class SaldoSettingsPage extends StatelessWidget {
), ),
], ],
), ),
BuildIfSessionUserExists( Consumer<CurrentUserProvider>(builder: (_, provider, ___) {
sessionController: sessionController, final user = provider.controller.user;
placeholder: const CircularProgressIndicator(), return Text(
builder: (context, user) { "Nuværende saldo: ${formatDkkCents(user.balanceDkkCents)}",
return Text( style: Theme.of(context).textTheme.bodyLarge);
"Nuværende saldo: ${formatDkkCents(user.balanceDkkCents)}", }),
style: Theme.of(context).textTheme.bodyLarge);
}),
ElevatedButton.icon( ElevatedButton.icon(
onPressed: () async { onPressed: () async {
final res = await sessionController.addBalance(); final currentUserProvider = context.read<CurrentUserProvider>();
switch (res) { final res = await currentUserProvider.controller.addBalance();
case Ok<Null, String>(): if (res case Err<Null, String>(value: final message)) {
print("yay"); if (context.mounted) {
case Err<Null, String>(value: final message): showDialog(
print("Womp womp fejled er: $message"); context: context,
builder: (context) => AlertDialog(
content: Text('Serverfejl: $message'),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, 'OK'),
child: const Text('OK'),
),
],
));
}
} }
}, },
icon: const Icon(Icons.add), icon: const Icon(Icons.add),

View File

@ -4,6 +4,7 @@ import 'package:http/http.dart' as http;
import 'package:mobile/models/cart_item.dart'; import 'package:mobile/models/cart_item.dart';
import 'package:mobile/models/product.dart'; import 'package:mobile/models/product.dart';
import 'package:mobile/models/user.dart'; import 'package:mobile/models/user.dart';
import 'package:mobile/results.dart';
import 'package:mobile/server/server.dart'; import 'package:mobile/server/server.dart';
class BackendServer implements Server { class BackendServer implements Server {
@ -21,24 +22,23 @@ class BackendServer implements Server {
} }
@override @override
Future<Response<List<Product>>> allProducts() async { Future<Result<List<Product>, String>> allProducts() async {
final res = await http final res = await http
.get( .get(
Uri.parse("$_apiUrl/products/all"), Uri.parse("$_apiUrl/products/all"),
) )
.then((res) => json.decode(res.body)); .then((res) => json.decode(res.body));
if (res["ok"]) { if (res["ok"]) {
return Success( return Ok((res["products"] as List<dynamic>)
data: (res["products"] as List<dynamic>) .map(((product) => Product.fromJson(product)))
.map(((product) => Product.fromJson(product))) .toList());
.toList());
} else { } else {
return Error(message: res["msg"]); return Err(res["msg"]);
} }
} }
@override @override
Future<Response<Null>> register( Future<Result<Null, String>> register(
String name, String name,
String email, String email,
String password, String password,
@ -49,14 +49,14 @@ class BackendServer implements Server {
).then((res) => json.decode(res.body)); ).then((res) => json.decode(res.body));
if (res["ok"]) { if (res["ok"]) {
return Success(data: null); return Ok(null);
} else { } else {
return Error(message: res["msg"]); return Err(res["msg"]);
} }
} }
@override @override
Future<Response<String>> login( Future<Result<String, String>> login(
String email, String email,
String password, String password,
) async { ) async {
@ -66,41 +66,41 @@ class BackendServer implements Server {
).then((res) => json.decode(res.body)); ).then((res) => json.decode(res.body));
if (res["ok"]) { if (res["ok"]) {
return Success(data: res["token"]); return Ok(res["token"]);
} else { } else {
return Error(message: res["msg"]); return Err(res["msg"]);
} }
} }
@override @override
Future<Response<Null>> logout(String token) async { Future<Result<Null, String>> logout(String token) async {
final res = await _post( final res = await _post(
endpoint: "sessions/logout", endpoint: "sessions/logout",
).then((res) => json.decode(res.body)); ).then((res) => json.decode(res.body));
if (res["ok"]) { if (res["ok"]) {
return Success(data: null); return Ok(null);
} else { } else {
return Error(message: res["msg"]); return Err(res["msg"]);
} }
} }
@override @override
Future<Response<User>> sessionUser(String token) async { Future<Result<User, String>> sessionUser(String token) async {
("sending request fr with token $token"); ("sending request fr with token $token");
final res = await http.get( final res = await http.get(
Uri.parse("$_apiUrl/sessions/user"), Uri.parse("$_apiUrl/sessions/user"),
headers: {"Session-Token": token}, headers: {"Session-Token": token},
).then((res) => json.decode(res.body)); ).then((res) => json.decode(res.body));
if (res["ok"]) { if (res["ok"]) {
return Success(data: User.fromJson(res["user"])); return Ok(User.fromJson(res["user"]));
} else { } else {
return Error(message: res["msg"]); return Err(res["msg"]);
} }
} }
@override @override
Future<Response<Null>> purchaseCart( Future<Result<Null, String>> purchaseCart(
String token, List<CartItem> cartItems) async { String token, List<CartItem> cartItems) async {
final res = await http.post(Uri.parse("$_apiUrl/carts/purchase"), headers: { final res = await http.post(Uri.parse("$_apiUrl/carts/purchase"), headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -113,14 +113,14 @@ class BackendServer implements Server {
}).then((res) => json.decode(res.body)); }).then((res) => json.decode(res.body));
if (res["ok"]) { if (res["ok"]) {
return Success(data: null); return Ok(null);
} else { } else {
return Error(message: res["msg"]); return Err(res["msg"]);
} }
} }
@override @override
Future<Response<Null>> addBalance(String token) async { Future<Result<Null, String>> addBalance(String token) async {
print("$_apiUrl/api/users/balance/add"); print("$_apiUrl/api/users/balance/add");
final res = await http.post( final res = await http.post(
Uri.parse("$_apiUrl/users/balance/add"), Uri.parse("$_apiUrl/users/balance/add"),
@ -132,9 +132,9 @@ class BackendServer implements Server {
).then((res) => json.decode(res.body)); ).then((res) => json.decode(res.body));
if (res["ok"]) { if (res["ok"]) {
return Success(data: null); return Ok(null);
} else { } else {
return Error(message: res["msg"]); return Err(res["msg"]);
} }
} }
} }

View File

@ -2,13 +2,14 @@ import 'package:mobile/models/cart_item.dart';
import 'package:mobile/models/coordinate.dart'; import 'package:mobile/models/coordinate.dart';
import 'package:mobile/models/product.dart'; import 'package:mobile/models/product.dart';
import 'package:mobile/models/user.dart'; import 'package:mobile/models/user.dart';
import 'package:mobile/results.dart';
import 'package:mobile/server/server.dart'; import 'package:mobile/server/server.dart';
class MockServer implements Server { class MockServer implements Server {
@override @override
Future<Response<List<Product>>> allProducts() async { Future<Result<List<Product>, String>> allProducts() async {
var nextId = 0; var nextId = 0;
return Success(data: <Product>[ return Ok(<Product>[
Product( Product(
id: nextId++, id: nextId++,
name: "Minimælk", name: "Minimælk",
@ -91,45 +92,44 @@ class MockServer implements Server {
} }
@override @override
Future<Response<Null>> register( Future<Result<Null, String>> register(
String name, String name,
String email, String email,
String password, String password,
) async { ) async {
return Success(data: null); return Ok(null);
} }
@override @override
Future<Response<String>> login( Future<Result<String, String>> login(
String email, String email,
String password, String password,
) async { ) async {
return Success(data: "asdsadasdsad"); return Ok("asdsadasdsad");
} }
@override @override
Future<Response<Null>> logout(String token) async { Future<Result<Null, String>> logout(String token) async {
return Success(data: null); return Ok(null);
} }
@override @override
Future<Response<User>> sessionUser(String token) async { Future<Result<User, String>> sessionUser(String token) async {
return Success( return Ok(User(
data: User( id: 0,
id: 0, email: "test@test.com",
email: "test@test.com", name: "testuser",
name: "testuser", balanceDkkCents: 10000));
balanceDkkCents: 10000));
} }
@override @override
Future<Response<Null>> purchaseCart( Future<Result<Null, String>> purchaseCart(
String token, List<CartItem> cartItems) async { String token, List<CartItem> cartItems) async {
return Success(data: null); return Ok(null);
} }
@override @override
Future<Response<Null>> addBalance(String token) async { Future<Result<Null, String>> addBalance(String token) async {
return Success(data: null); return Ok(null);
} }
} }

View File

@ -1,37 +1,27 @@
import 'package:mobile/models/cart_item.dart'; import 'package:mobile/models/cart_item.dart';
import 'package:mobile/models/product.dart'; import 'package:mobile/models/product.dart';
import 'package:mobile/models/user.dart'; import 'package:mobile/models/user.dart';
import 'package:mobile/results.dart';
abstract class Server { abstract class Server {
Future<Response<List<Product>>> allProducts(); Future<Result<List<Product>, String>> allProducts();
Future<Response<Null>> register( Future<Result<Null, String>> register(
String name, String name,
String email, String email,
String password, String password,
); );
Future<Response<String>> login( Future<Result<String, String>> login(
String email, String email,
String password, String password,
); );
Future<Response<Null>> logout(String token); Future<Result<Null, String>> logout(String token);
Future<Response<User>> sessionUser(String token); Future<Result<User, String>> sessionUser(String token);
Future<Response<Null>> purchaseCart(String token, List<CartItem> cartItems); Future<Result<Null, String>> purchaseCart(
String token, List<CartItem> cartItems);
Future<Response<Null>> addBalance(String token); Future<Result<Null, String>> addBalance(String token);
}
sealed class Response<Data> {}
class Success<Data> extends Response<Data> {
Data data;
Success({required this.data});
}
class Error<Data> extends Response<Data> {
String message;
Error({required this.message});
} }

View File

@ -1,28 +1,3 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mobile/controllers/user.dart'; import 'package:mobile/controllers/session.dart';
import 'package:mobile/models/user.dart'; import 'package:mobile/models/user.dart';
class BuildIfSessionUserExists extends StatelessWidget {
final UserController sessionController;
final Widget placeholder;
final Widget Function(BuildContext, User) builder;
const BuildIfSessionUserExists(
{super.key,
required this.sessionController,
required this.placeholder,
required this.builder});
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: sessionController.loadUserOld(),
builder: (context, snapshot) {
final user = sessionController.userOld;
if (user == null) {
return placeholder;
}
return builder(context, user);
});
}
}

View File

@ -1,30 +0,0 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}