add balance

This commit is contained in:
Mikkel Troels Kongsted 2025-03-17 14:50:56 +01:00
parent 974f057dc1
commit e7526452dd
15 changed files with 169 additions and 58 deletions

View File

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
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/results.dart'; import 'package:mobile/results.dart';
import 'package:mobile/server/server.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
class ProductIdException implements Exception {} class ProductIdException implements Exception {}
@ -23,9 +24,10 @@ abstract class CartController extends ChangeNotifier {
} }
class CartControllerMemory extends CartController { class CartControllerMemory extends CartController {
final Server server;
final List<CartItem> cart = []; final List<CartItem> cart = [];
CartControllerMemory(); CartControllerMemory({required this.server});
@override @override
List<CartItem> allCartItems() { List<CartItem> allCartItems() {
@ -114,8 +116,14 @@ class CartControllerMemory extends CartController {
notifyListeners(); notifyListeners();
} }
Result<Null, String> pay() { Future<Result<Null, String>> purchase(String token) async {
return const Err("Not implemented"); final res = await server.purchaseCart(token, cart);
switch (res) {
case Success<Null>():
return const Ok(null);
case Error<Null>(message: final message):
return Err(message);
}
} }
} }
@ -125,7 +133,7 @@ class CartControllerCache extends CartControllerMemory {
return File("${directory.path}/cart.json").create(); return File("${directory.path}/cart.json").create();
} }
CartControllerCache() { CartControllerCache({required super.server}) {
load(); load();
} }
@ -137,7 +145,6 @@ class CartControllerCache extends CartControllerMemory {
void load() async { void load() async {
final json = await (await _cacheFile).readAsString(); final json = await (await _cacheFile).readAsString();
print("Loading cache: $json");
if (json.isEmpty) { if (json.isEmpty) {
return; return;
} }

View File

@ -6,6 +6,7 @@ import 'package:mobile/server/server.dart';
class SessionController extends ChangeNotifier { class SessionController extends ChangeNotifier {
final Server server; final Server server;
String? _sessionToken; String? _sessionToken;
User? _user;
SessionController({required this.server}); SessionController({required this.server});
@ -21,24 +22,53 @@ class SessionController extends ChangeNotifier {
} }
} }
Future<Result<User, Null>> user() async { Future<void> _validateToken() async {
final token = _sessionToken; final token = _sessionToken;
if (token == null) { if (token == null) {
return;
}
final res = await server.sessionUser(token);
switch (res) {
case Success<User>():
return;
case Error<User>():
_sessionToken = null;
return;
}
}
User? get user {
loadUser();
return _user;
}
Future<void> _notifyIfTokenChanged() async {
final prev = _sessionToken;
_validateToken();
if (prev != _sessionToken) {
notifyListeners(); notifyListeners();
return const Err(null); }
}
Future<void> loadUser() async {
final token = _sessionToken;
if (token == null) {
_user = null;
return;
} }
final res = await server.sessionUser(token); final res = await server.sessionUser(token);
switch (res) { switch (res) {
case Success<User>(data: final user): case Success<User>(data: final user):
return Ok(user); _user = user;
return;
case Error<User>(): case Error<User>():
_sessionToken = null; _user = null;
notifyListeners(); return;
return const Err(null);
} }
} }
String? get sessionToken { String? get sessionToken {
_notifyIfTokenChanged();
return _sessionToken; return _sessionToken;
} }
@ -50,8 +80,4 @@ class SessionController extends ChangeNotifier {
} }
notifyListeners(); notifyListeners();
} }
Result<int, String> pay(int userId, int amount) {
return const Err("not implemented");
}
} }

View File

@ -1,11 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mobile/controllers/session.dart';
import 'package:mobile/results.dart'; import 'package:mobile/results.dart';
import 'package:mobile/server/server.dart'; import 'package:mobile/server/server.dart';
class UsersController extends ChangeNotifier { class UsersController extends ChangeNotifier {
Server server; Server server;
SessionController sessionController;
UsersController({required this.server}); UsersController({required this.server, required this.sessionController});
Future<Result<Null, String>> register( Future<Result<Null, String>> register(
String name, String email, String password) async { String name, String email, String password) async {
@ -17,4 +19,19 @@ class UsersController extends ChangeNotifier {
return Err(message); return Err(message);
} }
} }
Future<Result<Null, String>> 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<Null>():
return const Ok(null);
case Error<Null>(message: final message):
return Err(message);
}
}
} }

View File

@ -26,17 +26,21 @@ class MyApp extends StatelessWidget {
final server = BackendServer(); final server = BackendServer();
return MultiProvider( return MultiProvider(
providers: [ providers: [
ChangeNotifierProvider(
create: (_) => SessionController(server: server)),
ChangeNotifierProvider(create: (_) => RoutingController()), ChangeNotifierProvider(create: (_) => RoutingController()),
ChangeNotifierProvider( ChangeNotifierProvider(
create: (_) => ProductController(server: server)), create: (_) => ProductController(server: server)),
ChangeNotifierProvider(create: (_) => CartControllerCache()), ChangeNotifierProvider(
create: (_) => CartControllerCache(server: server)),
ChangeNotifierProvider(create: (_) => ReceiptController()), ChangeNotifierProvider(create: (_) => ReceiptController()),
ChangeNotifierProvider(create: (_) => PayingStateController()), ChangeNotifierProvider(create: (_) => PayingStateController()),
ChangeNotifierProvider(create: (_) => AddToCartStateController()), ChangeNotifierProvider(create: (_) => AddToCartStateController()),
ChangeNotifierProvider(create: (_) => LocationImageController()), ChangeNotifierProvider(create: (_) => LocationImageController()),
ChangeNotifierProvider(create: (_) => UsersController(server: server)),
ChangeNotifierProvider( ChangeNotifierProvider(
create: (_) => SessionController(server: server)), create: (context) => UsersController(
server: server,
sessionController: context.read<SessionController>())),
], ],
child: MaterialApp( child: MaterialApp(
title: 'Fresh Plaza', title: 'Fresh Plaza',

View File

@ -94,9 +94,23 @@ class ProductListItem extends StatelessWidget {
} }
} }
class AllProductsPage extends StatelessWidget { class AllProductsPage extends StatefulWidget {
const AllProductsPage({super.key}); const AllProductsPage({super.key});
@override
State<AllProductsPage> createState() => _AllProductsPageState();
}
class _AllProductsPageState extends State<AllProductsPage> {
final seawchContwowwew = TextEditingController();
@override
void initState() {
final contwowwew = context.read<ProductController>();
seawchContwowwew.text = contwowwew.query;
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final productRepo = Provider.of<ProductController>(context); final productRepo = Provider.of<ProductController>(context);
@ -114,6 +128,7 @@ class AllProductsPage extends StatelessWidget {
onChanged: (query) { onChanged: (query) {
productRepo.searchProducts(query); productRepo.searchProducts(query);
}, },
controller: seawchContwowwew,
decoration: const InputDecoration( decoration: const InputDecoration(
label: Text("Search"), label: Text("Search"),
contentPadding: EdgeInsets.only(top: 20))), contentPadding: EdgeInsets.only(top: 20))),
@ -127,6 +142,7 @@ class AllProductsPage extends StatelessWidget {
return ListView.builder( return ListView.builder(
shrinkWrap: true, shrinkWrap: true,
itemBuilder: (_, idx) => ProductListItem( itemBuilder: (_, idx) => ProductListItem(
key: Key(products[idx].name),
productId: products[idx].id, productId: products[idx].id,
name: products[idx].name, name: products[idx].name,
price: products[idx].priceDkkCent, price: products[idx].priceDkkCent,

View File

@ -8,7 +8,7 @@ import 'package:mobile/controllers/cart.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class Dashboard extends StatelessWidget { class Dashboard extends StatelessWidget {
final List<StatelessWidget> pages = []; final List<Widget> pages = [];
Dashboard({super.key}) { Dashboard({super.key}) {
pages.addAll([ pages.addAll([

View File

@ -3,6 +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/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';
@ -56,10 +57,11 @@ class FinishShoppingPage extends StatelessWidget {
child: Center( child: Center(
child: PrimaryButton( child: PrimaryButton(
onPressed: () async { onPressed: () async {
final session = context.read<SessionController>();
payingStateRepo.next(); payingStateRepo.next();
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
// TODO: implement paying for user if (cartController.purchase(session.sessionToken!)
if (cartController.pay() is Err) { is Err) {
if (context.mounted) { if (context.mounted) {
showDialog<String>( showDialog<String>(
context: context, context: context,

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mobile/controllers/session.dart'; import 'package:mobile/controllers/session.dart';
import 'package:mobile/controllers/user.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/build_if_session_exists.dart';
import 'package:mobile/utils/price.dart'; import 'package:mobile/utils/price.dart';
@ -10,7 +11,8 @@ class HomePage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sessionController = context.read<SessionController>(); final sessionController = context.watch<SessionController>();
context.watch<UsersController>();
return Column( return Column(
children: [ children: [
Row( Row(
@ -34,7 +36,7 @@ class HomePage extends StatelessWidget {
color: Color(0xFFFFFFFF), color: Color(0xFFFFFFFF),
), ),
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: BuildIfSessionExists( child: BuildIfSessionUserExists(
sessionController: sessionController, sessionController: sessionController,
placeholder: const CircularProgressIndicator(), placeholder: const CircularProgressIndicator(),
builder: (context, user) => Text( builder: (context, user) => Text(
@ -45,7 +47,7 @@ class HomePage extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
BuildIfSessionExists( BuildIfSessionUserExists(
sessionController: sessionController, sessionController: sessionController,
placeholder: const CircularProgressIndicator(), placeholder: const CircularProgressIndicator(),
builder: (context, user) => Text( builder: (context, user) => Text(

View File

@ -74,6 +74,7 @@ class LogInFormState extends State<LogInForm> {
child: const Text("Log ind")), child: const Text("Log ind")),
TextButton( TextButton(
onPressed: () { onPressed: () {
setState(() => loginError = false);
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const RegisterPage())); MaterialPageRoute(builder: (context) => const RegisterPage()));
}, },

View File

@ -88,6 +88,7 @@ class RegisterFormState extends State<RegisterForm> {
child: const Text("Opret bruger")), child: const Text("Opret bruger")),
TextButton( TextButton(
onPressed: () { onPressed: () {
setState(() => registerError = false);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: RichText( child: RichText(

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mobile/controllers/session.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/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,6 +12,7 @@ class SaldoSettingsPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sessionController = context.watch<SessionController>(); final sessionController = context.watch<SessionController>();
final userController = context.watch<UsersController>();
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
body: SafeArea( body: SafeArea(
@ -25,17 +28,23 @@ class SaldoSettingsPage extends StatelessWidget {
), ),
], ],
), ),
BuildIfSessionExists( BuildIfSessionUserExists(
sessionController: sessionController, sessionController: sessionController,
placeholder: const CircularProgressIndicator(), placeholder: const CircularProgressIndicator(),
builder: (context, user) => Text( builder: (context, user) {
return Text(
"Nuværende saldo: ${formatDkkCents(user.balanceDkkCents)}", "Nuværende saldo: ${formatDkkCents(user.balanceDkkCents)}",
style: Theme.of(context).textTheme.bodyLarge), style: Theme.of(context).textTheme.bodyLarge);
), }),
ElevatedButton.icon( ElevatedButton.icon(
onPressed: () { onPressed: () async {
// TODO: implement add balance final res = await userController.addBalance();
throw Exception("not implemented: Adding funds"); switch (res) {
case Ok<Null, String>():
print("yay");
case Err<Null, String>(value: final message):
print("Womp womp fejled er: $message");
}
}, },
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
label: const Text("Tilføj 100,00 kr"), label: const Text("Tilføj 100,00 kr"),

View File

@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
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/server/server.dart'; import 'package:mobile/server/server.dart';
@ -10,7 +11,7 @@ class BackendServer implements Server {
// final _apiUrl = "http://127.0.0.1:8080/api"; // final _apiUrl = "http://127.0.0.1:8080/api";
Future<http.Response> _post( Future<http.Response> _post(
{required String endpoint, required Map<String, dynamic> body}) async { {required String endpoint, Map<String, dynamic>? body}) async {
final encoded = json.encode(body); final encoded = json.encode(body);
return await http.post( return await http.post(
Uri.parse("$_apiUrl/$endpoint"), Uri.parse("$_apiUrl/$endpoint"),
@ -75,9 +76,6 @@ class BackendServer implements Server {
Future<Response<Null>> logout(String token) async { Future<Response<Null>> logout(String token) async {
final res = await _post( final res = await _post(
endpoint: "sessions/logout", endpoint: "sessions/logout",
body: {
"token": token,
},
).then((res) => json.decode(res.body)); ).then((res) => json.decode(res.body));
if (res["ok"]) { if (res["ok"]) {
@ -102,11 +100,34 @@ class BackendServer implements Server {
} }
@override @override
Future<Response<Null>> payForCart(String token) async { Future<Response<Null>> purchaseCart(
final res = await _post( String token, List<CartItem> cartItems) async {
endpoint: "cart/pay", final res = await http.post(Uri.parse("$_apiUrl/carts/purchase"), headers: {
body: { "Content-Type": "application/json",
"token": token, "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<Response<Null>> 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)); ).then((res) => json.decode(res.body));

View File

@ -1,3 +1,4 @@
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';
@ -122,7 +123,13 @@ class MockServer implements Server {
} }
@override @override
Future<Response<Null>> payForCart(String token) async { Future<Response<Null>> purchaseCart(
String token, List<CartItem> cartItems) async {
return Success(data: null);
}
@override
Future<Response<Null>> addBalance(String token) async {
return Success(data: null); return Success(data: null);
} }
} }

View File

@ -1,3 +1,4 @@
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';
@ -18,7 +19,9 @@ abstract class Server {
Future<Response<User>> sessionUser(String token); Future<Response<User>> sessionUser(String token);
Future<Response<Null>> payForCart(String token); Future<Response<Null>> purchaseCart(String token, List<CartItem> cartItems);
Future<Response<Null>> addBalance(String token);
} }
sealed class Response<Data> {} sealed class Response<Data> {}

View File

@ -1,14 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mobile/controllers/session.dart'; import 'package:mobile/controllers/session.dart';
import 'package:mobile/models/user.dart'; import 'package:mobile/models/user.dart';
import 'package:mobile/results.dart';
class BuildIfSessionExists extends StatelessWidget { class BuildIfSessionUserExists extends StatelessWidget {
final SessionController sessionController; final SessionController sessionController;
final Widget placeholder; final Widget placeholder;
final Widget Function(BuildContext, User) builder; final Widget Function(BuildContext, User) builder;
const BuildIfSessionExists( const BuildIfSessionUserExists(
{super.key, {super.key,
required this.sessionController, required this.sessionController,
required this.placeholder, required this.placeholder,
@ -17,17 +16,13 @@ class BuildIfSessionExists extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder( return FutureBuilder(
future: sessionController.user(), future: sessionController.loadUser(),
builder: (context, snapshot) { builder: (context, snapshot) {
final data = snapshot.data; final user = sessionController.user;
if (data == null) { if (user == null) {
return placeholder; return placeholder;
} }
if (data is Ok<User, Null>) {
final user = data.value;
return builder(context, user); return builder(context, user);
}
return Container();
}); });
} }
} }