diff --git a/mobile/lib/controllers/cart.dart b/mobile/lib/controllers/cart.dart index ac5f6a0..1baa3c1 100644 --- a/mobile/lib/controllers/cart.dart +++ b/mobile/lib/controllers/cart.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:mobile/models/cart_item.dart'; import 'package:mobile/models/product.dart'; import 'package:mobile/results.dart'; +import 'package:mobile/server/server.dart'; import 'package:path_provider/path_provider.dart'; class ProductIdException implements Exception {} @@ -23,9 +24,10 @@ abstract class CartController extends ChangeNotifier { } class CartControllerMemory extends CartController { + final Server server; final List cart = []; - CartControllerMemory(); + CartControllerMemory({required this.server}); @override List allCartItems() { @@ -114,8 +116,14 @@ class CartControllerMemory extends CartController { notifyListeners(); } - Result pay() { - return const Err("Not implemented"); + Future> purchase(String token) async { + final res = await server.purchaseCart(token, cart); + switch (res) { + case Success(): + return const Ok(null); + case Error(message: final message): + return Err(message); + } } } @@ -125,7 +133,7 @@ class CartControllerCache extends CartControllerMemory { return File("${directory.path}/cart.json").create(); } - CartControllerCache() { + CartControllerCache({required super.server}) { load(); } @@ -137,7 +145,6 @@ class CartControllerCache extends CartControllerMemory { void load() async { final json = await (await _cacheFile).readAsString(); - print("Loading cache: $json"); if (json.isEmpty) { return; } diff --git a/mobile/lib/controllers/session.dart b/mobile/lib/controllers/session.dart index 042d79e..e8e81d3 100644 --- a/mobile/lib/controllers/session.dart +++ b/mobile/lib/controllers/session.dart @@ -6,6 +6,7 @@ import 'package:mobile/server/server.dart'; class SessionController extends ChangeNotifier { final Server server; String? _sessionToken; + User? _user; SessionController({required this.server}); @@ -21,24 +22,53 @@ class SessionController extends ChangeNotifier { } } - Future> user() async { + Future _validateToken() async { final token = _sessionToken; if (token == null) { + return; + } + final res = await server.sessionUser(token); + switch (res) { + case Success(): + return; + case Error(): + _sessionToken = null; + return; + } + } + + User? get user { + loadUser(); + return _user; + } + + Future _notifyIfTokenChanged() async { + final prev = _sessionToken; + _validateToken(); + if (prev != _sessionToken) { notifyListeners(); - return const Err(null); + } + } + + Future loadUser() async { + final token = _sessionToken; + if (token == null) { + _user = null; + return; } final res = await server.sessionUser(token); switch (res) { case Success(data: final user): - return Ok(user); + _user = user; + return; case Error(): - _sessionToken = null; - notifyListeners(); - return const Err(null); + _user = null; + return; } } String? get sessionToken { + _notifyIfTokenChanged(); return _sessionToken; } @@ -50,8 +80,4 @@ class SessionController extends ChangeNotifier { } notifyListeners(); } - - Result pay(int userId, int amount) { - return const Err("not implemented"); - } } diff --git a/mobile/lib/controllers/user.dart b/mobile/lib/controllers/user.dart index caa0db9..2d3cb9a 100644 --- a/mobile/lib/controllers/user.dart +++ b/mobile/lib/controllers/user.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:mobile/controllers/session.dart'; import 'package:mobile/results.dart'; import 'package:mobile/server/server.dart'; class UsersController extends ChangeNotifier { Server server; + SessionController sessionController; - UsersController({required this.server}); + UsersController({required this.server, required this.sessionController}); Future> register( String name, String email, String password) async { @@ -17,4 +19,19 @@ class UsersController extends ChangeNotifier { return Err(message); } } + + Future> addBalance() async { + final token = sessionController.sessionToken; + if (token == null) { + return const Err("No token"); + } + final res = await server.addBalance(token); + notifyListeners(); + switch (res) { + case Success(): + return const Ok(null); + case Error(message: final message): + return Err(message); + } + } } diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 39c91c9..271885f 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -26,17 +26,21 @@ class MyApp extends StatelessWidget { final server = BackendServer(); return MultiProvider( providers: [ + ChangeNotifierProvider( + create: (_) => SessionController(server: server)), ChangeNotifierProvider(create: (_) => RoutingController()), ChangeNotifierProvider( create: (_) => ProductController(server: server)), - ChangeNotifierProvider(create: (_) => CartControllerCache()), + ChangeNotifierProvider( + create: (_) => CartControllerCache(server: server)), ChangeNotifierProvider(create: (_) => ReceiptController()), ChangeNotifierProvider(create: (_) => PayingStateController()), ChangeNotifierProvider(create: (_) => AddToCartStateController()), ChangeNotifierProvider(create: (_) => LocationImageController()), - ChangeNotifierProvider(create: (_) => UsersController(server: server)), ChangeNotifierProvider( - create: (_) => SessionController(server: server)), + create: (context) => UsersController( + server: server, + sessionController: context.read())), ], child: MaterialApp( title: 'Fresh Plaza', diff --git a/mobile/lib/pages/all_products_page.dart b/mobile/lib/pages/all_products_page.dart index 18bd4b0..0f7077d 100644 --- a/mobile/lib/pages/all_products_page.dart +++ b/mobile/lib/pages/all_products_page.dart @@ -94,9 +94,23 @@ class ProductListItem extends StatelessWidget { } } -class AllProductsPage extends StatelessWidget { +class AllProductsPage extends StatefulWidget { const AllProductsPage({super.key}); + @override + State createState() => _AllProductsPageState(); +} + +class _AllProductsPageState extends State { + final seawchContwowwew = TextEditingController(); + + @override + void initState() { + final contwowwew = context.read(); + seawchContwowwew.text = contwowwew.query; + super.initState(); + } + @override Widget build(BuildContext context) { final productRepo = Provider.of(context); @@ -114,6 +128,7 @@ class AllProductsPage extends StatelessWidget { onChanged: (query) { productRepo.searchProducts(query); }, + controller: seawchContwowwew, decoration: const InputDecoration( label: Text("Search"), contentPadding: EdgeInsets.only(top: 20))), @@ -127,6 +142,7 @@ class AllProductsPage extends StatelessWidget { return ListView.builder( shrinkWrap: true, itemBuilder: (_, idx) => ProductListItem( + key: Key(products[idx].name), productId: products[idx].id, name: products[idx].name, price: products[idx].priceDkkCent, diff --git a/mobile/lib/pages/dashboard.dart b/mobile/lib/pages/dashboard.dart index 9e94be3..457ed44 100644 --- a/mobile/lib/pages/dashboard.dart +++ b/mobile/lib/pages/dashboard.dart @@ -8,7 +8,7 @@ import 'package:mobile/controllers/cart.dart'; import 'package:provider/provider.dart'; class Dashboard extends StatelessWidget { - final List pages = []; + final List pages = []; Dashboard({super.key}) { pages.addAll([ diff --git a/mobile/lib/pages/finish_shopping_page.dart b/mobile/lib/pages/finish_shopping_page.dart index 6169311..8cfb456 100644 --- a/mobile/lib/pages/finish_shopping_page.dart +++ b/mobile/lib/pages/finish_shopping_page.dart @@ -3,6 +3,7 @@ import 'package:mobile/controllers/routing.dart'; import 'package:mobile/controllers/cart.dart'; import 'package:mobile/controllers/paying_state.dart'; import 'package:mobile/controllers/receipt.dart'; +import 'package:mobile/controllers/session.dart'; import 'package:mobile/results.dart'; import 'package:mobile/utils/price.dart'; import 'package:mobile/widgets/primary_button.dart'; @@ -56,10 +57,11 @@ class FinishShoppingPage extends StatelessWidget { child: Center( child: PrimaryButton( onPressed: () async { + final session = context.read(); payingStateRepo.next(); await Future.delayed(const Duration(seconds: 1)); - // TODO: implement paying for user - if (cartController.pay() is Err) { + if (cartController.purchase(session.sessionToken!) + is Err) { if (context.mounted) { showDialog( context: context, diff --git a/mobile/lib/pages/home_page.dart b/mobile/lib/pages/home_page.dart index bcc1c66..f934154 100644 --- a/mobile/lib/pages/home_page.dart +++ b/mobile/lib/pages/home_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:mobile/controllers/session.dart'; +import 'package:mobile/controllers/user.dart'; import 'package:mobile/pages/settings_page.dart'; import 'package:mobile/utils/build_if_session_exists.dart'; import 'package:mobile/utils/price.dart'; @@ -10,7 +11,8 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { - final sessionController = context.read(); + final sessionController = context.watch(); + context.watch(); return Column( children: [ Row( @@ -34,7 +36,7 @@ class HomePage extends StatelessWidget { color: Color(0xFFFFFFFF), ), padding: const EdgeInsets.all(10), - child: BuildIfSessionExists( + child: BuildIfSessionUserExists( sessionController: sessionController, placeholder: const CircularProgressIndicator(), builder: (context, user) => Text( @@ -45,7 +47,7 @@ class HomePage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - BuildIfSessionExists( + BuildIfSessionUserExists( sessionController: sessionController, placeholder: const CircularProgressIndicator(), builder: (context, user) => Text( diff --git a/mobile/lib/pages/log_in_page.dart b/mobile/lib/pages/log_in_page.dart index 8867846..2edf715 100644 --- a/mobile/lib/pages/log_in_page.dart +++ b/mobile/lib/pages/log_in_page.dart @@ -74,6 +74,7 @@ class LogInFormState extends State { child: const Text("Log ind")), TextButton( onPressed: () { + setState(() => loginError = false); Navigator.of(context).push( MaterialPageRoute(builder: (context) => const RegisterPage())); }, diff --git a/mobile/lib/pages/register_page.dart b/mobile/lib/pages/register_page.dart index 9764460..0afdaf1 100644 --- a/mobile/lib/pages/register_page.dart +++ b/mobile/lib/pages/register_page.dart @@ -88,6 +88,7 @@ class RegisterFormState extends State { child: const Text("Opret bruger")), TextButton( onPressed: () { + setState(() => registerError = false); Navigator.of(context).pop(); }, child: RichText( diff --git a/mobile/lib/pages/settings_pages/saldo.dart b/mobile/lib/pages/settings_pages/saldo.dart index 600192a..f818743 100644 --- a/mobile/lib/pages/settings_pages/saldo.dart +++ b/mobile/lib/pages/settings_pages/saldo.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:mobile/controllers/session.dart'; +import 'package:mobile/controllers/user.dart'; +import 'package:mobile/results.dart'; import 'package:mobile/utils/build_if_session_exists.dart'; import 'package:mobile/utils/price.dart'; import 'package:provider/provider.dart'; @@ -10,6 +12,7 @@ class SaldoSettingsPage extends StatelessWidget { @override Widget build(BuildContext context) { final sessionController = context.watch(); + final userController = context.watch(); return Scaffold( backgroundColor: Colors.white, body: SafeArea( @@ -25,17 +28,23 @@ class SaldoSettingsPage extends StatelessWidget { ), ], ), - BuildIfSessionExists( - sessionController: sessionController, - placeholder: const CircularProgressIndicator(), - builder: (context, user) => Text( - "Nuværende saldo: ${formatDkkCents(user.balanceDkkCents)}", - style: Theme.of(context).textTheme.bodyLarge), - ), + BuildIfSessionUserExists( + sessionController: sessionController, + placeholder: const CircularProgressIndicator(), + builder: (context, user) { + return Text( + "Nuværende saldo: ${formatDkkCents(user.balanceDkkCents)}", + style: Theme.of(context).textTheme.bodyLarge); + }), ElevatedButton.icon( - onPressed: () { - // TODO: implement add balance - throw Exception("not implemented: Adding funds"); + onPressed: () async { + final res = await userController.addBalance(); + switch (res) { + case Ok(): + print("yay"); + case Err(value: final message): + print("Womp womp fejled er: $message"); + } }, icon: const Icon(Icons.add), label: const Text("Tilføj 100,00 kr"), diff --git a/mobile/lib/server/backend_server.dart b/mobile/lib/server/backend_server.dart index 8386173..4082d90 100644 --- a/mobile/lib/server/backend_server.dart +++ b/mobile/lib/server/backend_server.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:mobile/models/cart_item.dart'; import 'package:mobile/models/product.dart'; import 'package:mobile/models/user.dart'; import 'package:mobile/server/server.dart'; @@ -10,7 +11,7 @@ class BackendServer implements Server { // final _apiUrl = "http://127.0.0.1:8080/api"; Future _post( - {required String endpoint, required Map body}) async { + {required String endpoint, Map? body}) async { final encoded = json.encode(body); return await http.post( Uri.parse("$_apiUrl/$endpoint"), @@ -75,9 +76,6 @@ class BackendServer implements Server { Future> logout(String token) async { final res = await _post( endpoint: "sessions/logout", - body: { - "token": token, - }, ).then((res) => json.decode(res.body)); if (res["ok"]) { @@ -102,11 +100,34 @@ class BackendServer implements Server { } @override - Future> payForCart(String token) async { - final res = await _post( - endpoint: "cart/pay", - body: { - "token": token, + Future> purchaseCart( + String token, List cartItems) async { + final res = await http.post(Uri.parse("$_apiUrl/carts/purchase"), headers: { + "Content-Type": "application/json", + "Session-Token": token + }, body: { + "cart_items": cartItems + .map((cartItem) => + {"product_id": cartItem.product.id, "amount": cartItem.amount}) + .toList() + }).then((res) => json.decode(res.body)); + + if (res["ok"]) { + return Success(data: null); + } else { + return Error(message: res["msg"]); + } + } + + @override + Future> addBalance(String token) async { + print("$_apiUrl/api/users/balance/add"); + final res = await http.post( + Uri.parse("$_apiUrl/users/balance/add"), + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + "Session-Token": token }, ).then((res) => json.decode(res.body)); diff --git a/mobile/lib/server/mock_server.dart b/mobile/lib/server/mock_server.dart index dd4f183..0500231 100644 --- a/mobile/lib/server/mock_server.dart +++ b/mobile/lib/server/mock_server.dart @@ -1,3 +1,4 @@ +import 'package:mobile/models/cart_item.dart'; import 'package:mobile/models/coordinate.dart'; import 'package:mobile/models/product.dart'; import 'package:mobile/models/user.dart'; @@ -122,7 +123,13 @@ class MockServer implements Server { } @override - Future> payForCart(String token) async { + Future> purchaseCart( + String token, List cartItems) async { + return Success(data: null); + } + + @override + Future> addBalance(String token) async { return Success(data: null); } } diff --git a/mobile/lib/server/server.dart b/mobile/lib/server/server.dart index f888d65..c224055 100644 --- a/mobile/lib/server/server.dart +++ b/mobile/lib/server/server.dart @@ -1,3 +1,4 @@ +import 'package:mobile/models/cart_item.dart'; import 'package:mobile/models/product.dart'; import 'package:mobile/models/user.dart'; @@ -18,7 +19,9 @@ abstract class Server { Future> sessionUser(String token); - Future> payForCart(String token); + Future> purchaseCart(String token, List cartItems); + + Future> addBalance(String token); } sealed class Response {} diff --git a/mobile/lib/utils/build_if_session_exists.dart b/mobile/lib/utils/build_if_session_exists.dart index 7705936..20f2503 100644 --- a/mobile/lib/utils/build_if_session_exists.dart +++ b/mobile/lib/utils/build_if_session_exists.dart @@ -1,14 +1,13 @@ import 'package:flutter/material.dart'; import 'package:mobile/controllers/session.dart'; import 'package:mobile/models/user.dart'; -import 'package:mobile/results.dart'; -class BuildIfSessionExists extends StatelessWidget { +class BuildIfSessionUserExists extends StatelessWidget { final SessionController sessionController; final Widget placeholder; final Widget Function(BuildContext, User) builder; - const BuildIfSessionExists( + const BuildIfSessionUserExists( {super.key, required this.sessionController, required this.placeholder, @@ -17,17 +16,13 @@ class BuildIfSessionExists extends StatelessWidget { @override Widget build(BuildContext context) { return FutureBuilder( - future: sessionController.user(), + future: sessionController.loadUser(), builder: (context, snapshot) { - final data = snapshot.data; - if (data == null) { + final user = sessionController.user; + if (user == null) { return placeholder; } - if (data is Ok) { - final user = data.value; - return builder(context, user); - } - return Container(); + return builder(context, user); }); } }