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/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<CartItem> cart = [];
CartControllerMemory();
CartControllerMemory({required this.server});
@override
List<CartItem> allCartItems() {
@ -114,8 +116,14 @@ class CartControllerMemory extends CartController {
notifyListeners();
}
Result<Null, String> pay() {
return const Err("Not implemented");
Future<Result<Null, String>> purchase(String token) async {
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();
}
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;
}

View File

@ -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<Result<User, Null>> user() async {
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;
}
}
User? get user {
loadUser();
return _user;
}
Future<void> _notifyIfTokenChanged() async {
final prev = _sessionToken;
_validateToken();
if (prev != _sessionToken) {
notifyListeners();
return const Err(null);
}
}
Future<void> loadUser() async {
final token = _sessionToken;
if (token == null) {
_user = null;
return;
}
final res = await server.sessionUser(token);
switch (res) {
case Success<User>(data: final user):
return Ok(user);
_user = user;
return;
case Error<User>():
_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<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: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<Result<Null, String>> register(
String name, String email, String password) async {
@ -17,4 +19,19 @@ class UsersController extends ChangeNotifier {
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();
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<SessionController>())),
],
child: MaterialApp(
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});
@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
Widget build(BuildContext context) {
final productRepo = Provider.of<ProductController>(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,

View File

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

View File

@ -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<SessionController>();
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<String>(
context: context,

View File

@ -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<SessionController>();
final sessionController = context.watch<SessionController>();
context.watch<UsersController>();
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(

View File

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

View File

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

View File

@ -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<SessionController>();
final userController = context.watch<UsersController>();
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<Null, String>():
print("yay");
case Err<Null, String>(value: final message):
print("Womp womp fejled er: $message");
}
},
icon: const Icon(Icons.add),
label: const Text("Tilføj 100,00 kr"),

View File

@ -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<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);
return await http.post(
Uri.parse("$_apiUrl/$endpoint"),
@ -75,9 +76,6 @@ class BackendServer implements Server {
Future<Response<Null>> 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<Response<Null>> payForCart(String token) async {
final res = await _post(
endpoint: "cart/pay",
body: {
"token": token,
Future<Response<Null>> purchaseCart(
String token, List<CartItem> 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<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));

View File

@ -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<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);
}
}

View File

@ -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<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> {}

View File

@ -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<User, Null>) {
final user = data.value;
return builder(context, user);
}
return Container();
return builder(context, user);
});
}
}