diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b8d2d84 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,8 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + ] +} \ No newline at end of file diff --git a/assets/.env b/assets/.env index 8e8e1bf..f248619 100644 --- a/assets/.env +++ b/assets/.env @@ -1,6 +1,7 @@ -# BASE_URL=https://peakapi.qantra.co.zw/api -BASE_URL=http://192.168.100.8:6950/api -# BASE_URL=http://192.168.1.164:6950/api -# BASE_URL=http://192.168.120.160:6950/api -# BASE_URL=http://10.10.2.92:6950/api -# BASE_URL=http://172.20.5.105:6950/api +; BASE_URL=https://peakapi.qantra.co.zw/api + BASE_URL=http://192.168.100.8:6950/api +; BASE_URL=http://192.168.1.164:6950/api +; BASE_URL=http://192.168.120.160:6950/api +; BASE_URL=http://10.10.2.92:6950/api +; BASE_URL=http://172.20.5.105:6950/api +; BASE_URL=http://10.10.3.92:6950/api \ No newline at end of file diff --git a/assets/apple.png b/assets/apple.png new file mode 100644 index 0000000..2da6b5c Binary files /dev/null and b/assets/apple.png differ diff --git a/assets/google.png b/assets/google.png new file mode 100644 index 0000000..1a0f3f0 Binary files /dev/null and b/assets/google.png differ diff --git a/assets/landing.png b/assets/landing.png new file mode 100644 index 0000000..69aaeda Binary files /dev/null and b/assets/landing.png differ diff --git a/assets/password.png b/assets/password.png new file mode 100644 index 0000000..8debbf6 Binary files /dev/null and b/assets/password.png differ diff --git a/assets/united-states.png b/assets/united-states.png new file mode 100644 index 0000000..82b720f Binary files /dev/null and b/assets/united-states.png differ diff --git a/assets/verify.png b/assets/verify.png new file mode 100644 index 0000000..0ec9b93 Binary files /dev/null and b/assets/verify.png differ diff --git a/assets/zimbabwe.png b/assets/zimbabwe.png new file mode 100644 index 0000000..76b41dc Binary files /dev/null and b/assets/zimbabwe.png differ diff --git a/lib/main.dart b/lib/main.dart index eb2b470..74f0c57 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,11 @@ import 'package:provider/provider.dart'; import 'package:go_router/go_router.dart'; import 'package:qpay/screens/confirm/confirm_screen.dart'; import 'package:qpay/screens/gateway/gateway_screen.dart'; +import 'package:qpay/screens/onboarding/landing/landing_screen.dart'; +import 'package:qpay/screens/onboarding/login/login_screen.dart'; +import 'package:qpay/screens/onboarding/password/password_screen.dart'; +import 'package:qpay/screens/onboarding/register/register_screen.dart'; +import 'package:qpay/screens/onboarding/verify/verify_screen.dart'; import 'package:qpay/screens/pay/pay_screen.dart'; import 'package:qpay/screens/receipt/receipt_screen.dart'; import 'package:qpay/screens/recipient/recipients_screen.dart'; @@ -65,6 +70,11 @@ class _MyAppState extends State { routes: [ ShellRoute( builder: (context, state, child) { + // If the current location is an onboarding route, don't show the navbar + final location = GoRouterState.of(context).uri.toString(); + if (location.startsWith('/onboarding/')) { + return child; + } return ScaffoldWithNavBar(child: child); }, routes: [ @@ -102,6 +112,26 @@ class _MyAppState extends State { path: '/profile', builder: (context, state) => const ProfileScreen(), ), + GoRoute( + path: '/onboarding/landing', + builder: (context, state) => const LandingScreen(), + ), + GoRoute( + path: '/onboarding/register', + builder: (context, state) => const RegisterScreen(), + ), + GoRoute( + path: '/onboarding/verify', + builder: (context, state) => const VerifyScreen(), + ), + GoRoute( + path: '/onboarding/password', + builder: (context, state) => const PasswordScreen(), + ), + GoRoute( + path: '/onboarding/login', + builder: (context, state) => const LoginScreen(), + ), ], ), ], diff --git a/lib/screens/confirm/confirm_controller.dart b/lib/screens/confirm/confirm_controller.dart index 8f4b844..2ca9390 100644 --- a/lib/screens/confirm/confirm_controller.dart +++ b/lib/screens/confirm/confirm_controller.dart @@ -62,11 +62,12 @@ class ConfirmController extends ChangeNotifier { .formData = transactionController.model.formData.copyWith( type: "REQUEST", trace: transactionController.model.confirmationData['trace'], + id: transactionController.model.confirmationData['id'], authType: transactionController.model.selectedPaymentProcessor.authType, ); dynamic response = await http.post( - '/transaction', + '/public/transaction', transactionController.model.formData, ); logger.i(response.toString()); @@ -107,12 +108,11 @@ class ConfirmController extends ChangeNotifier { Future pollTransaction(String uid) async { while (!model.isCancelled) { - // Check again after delay in case it was cancelled during the delay if (model.isCancelled) break; try { - dynamic response = await http.get('/transaction/poll/$uid'); + dynamic response = await http.get('/public/transaction/poll/$uid'); logger.i(response.toString()); if (response['status'] == 'SUCCESS') { diff --git a/lib/screens/confirm/confirm_screen.dart b/lib/screens/confirm/confirm_screen.dart index 270115b..d38469f 100644 --- a/lib/screens/confirm/confirm_screen.dart +++ b/lib/screens/confirm/confirm_screen.dart @@ -200,72 +200,99 @@ class _ConfirmScreenState extends State ], ), const SizedBox(height: 10), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Our Charge", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + if (controller + .transactionController + .model + .confirmationData["charge"] != + 0) + Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Our Charge", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + controller + .transactionController + .model + .confirmationData["charge"] + .toString(), + style: TextStyle(fontSize: 16), + ), + ], ), - ), - Text( - controller - .transactionController - .model - .confirmationData["charge"] - .toString(), - style: TextStyle(fontSize: 16), - ), - ], - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Gateway Charge", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + const SizedBox(height: 10), + ], + ), + if (controller + .transactionController + .model + .confirmationData["gatewayCharge"] != + 0) + Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Gateway Charge", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + controller + .transactionController + .model + .confirmationData["gatewayCharge"] + .toString(), + style: TextStyle(fontSize: 16), + ), + ], ), - ), - Text( - controller - .transactionController - .model - .confirmationData["gatewayCharge"] - .toString(), - style: TextStyle(fontSize: 16), - ), - ], - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Tax", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + const SizedBox(height: 10), + ], + ), + if (controller + .transactionController + .model + .confirmationData["tax"] != + 0) + Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Tax", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + controller + .transactionController + .model + .confirmationData["tax"] + .toString(), + style: TextStyle(fontSize: 16), + ), + ], ), - ), - Text( - controller - .transactionController - .model - .confirmationData["tax"] - .toString(), - style: TextStyle(fontSize: 16), - ), - ], - ), - const SizedBox(height: 10), + const SizedBox(height: 10), + ], + ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -287,11 +314,28 @@ class _ConfirmScreenState extends State ), ], ), + const SizedBox(height: 5), + Text( + "NB: Your payment provider may charge additional fees.", + style: TextStyle( + fontSize: 11, + color: Colors.red, + ), + ), ], ), ), const SizedBox(height: 20), _buildPaymentProcessorButton(), + const SizedBox(height: 20), + Center( + child: OutlinedButton( + child: Text('Cancel'), + onPressed: () { + context.go('/'); + }, + ), + ), ], ), ), diff --git a/lib/screens/gateway/gateway_controller.dart b/lib/screens/gateway/gateway_controller.dart index d6001b8..c4f43b0 100644 --- a/lib/screens/gateway/gateway_controller.dart +++ b/lib/screens/gateway/gateway_controller.dart @@ -58,7 +58,7 @@ class GatewayController extends ChangeNotifier { if (model.isCancelled) break; try { - dynamic response = await http.get('/transaction/poll/$uid'); + dynamic response = await http.get('/public/transaction/poll/$uid'); logger.i(response.toString()); if (response['status'] == 'SUCCESS') { @@ -85,7 +85,7 @@ class GatewayController extends ChangeNotifier { } try { - dynamic response = await http.get('/transaction/poll/$uid'); + dynamic response = await http.get('/public/transaction/poll/$uid'); logger.i(response.toString()); if (response['status'] == 'SUCCESS') { diff --git a/lib/screens/history/history_controller.dart b/lib/screens/history/history_controller.dart index c5602ae..708b619 100644 --- a/lib/screens/history/history_controller.dart +++ b/lib/screens/history/history_controller.dart @@ -37,7 +37,7 @@ class HistoryController extends ChangeNotifier { try { params['type'] = 'REQUEST'; Map response = await http.get( - '/transaction?${buildQueryParameters(params)}', + '/public/transaction?${buildQueryParameters(params)}', ); if (params['page'] == '0') { diff --git a/lib/screens/home/home_controller.dart b/lib/screens/home/home_controller.dart index 062d6c5..0931e1b 100644 --- a/lib/screens/home/home_controller.dart +++ b/lib/screens/home/home_controller.dart @@ -11,6 +11,7 @@ class HomeScreenModel { List categories = []; List providers = []; List filterableProviders = []; + List accounts = []; BillProvider? selectedProvider; dynamic selectedCategory; dynamic account = {}; @@ -20,6 +21,26 @@ class HomeScreenModel { String? errorMessage; } +class Account { + late String name; + late String description; + late String image; + late String currency; + late String balance; + late String availableBalance; + late String accountNumber; + late String accountType; + late String accountHolderName; + + Account({ + required this.name, + required this.description, + required this.image, + required this.currency, + required this.balance, + }); +} + @freezed abstract class Category with _$Category { const factory Category({ @@ -76,12 +97,17 @@ class HomeController extends ChangeNotifier { }); } + Future getAccounts() async { + model.accounts = getFakeAccounts(); + notifyListeners(); + } + Future getProviders() async { try { model.isLoading = true; notifyListeners(); - List response = await http.get('/providers'); + List response = await http.get('/public/providers'); model.providers = response.map((e) => BillProvider.fromJson(e)).toList(); model.filterableProviders = model.providers; @@ -116,7 +142,7 @@ class HomeController extends ChangeNotifier { try { Map response = await http.get( - '/transaction?sort=createdAt,desc&size=3&page=0&userId=$userId&type=REQUEST', + '/public/transaction?sort=createdAt,desc&size=3&page=0&userId=$userId&type=REQUEST', ); PageableModel pageableModel = PageableModel.fromJson(response); @@ -146,7 +172,7 @@ class HomeController extends ChangeNotifier { // Simulate fetching categories try { - List response = await http.get('/categories'); + List response = await http.get('/public/categories'); model.categories = response.map((e) => Category.fromJson(e)).toList(); if (model.categories.isNotEmpty) { @@ -246,4 +272,23 @@ class HomeController extends ChangeNotifier { ), ]; } + + List getFakeAccounts() { + return [ + Account( + name: 'Vusumuzi Khoza', + description: 'USD Wallet', + image: 'united-states.png', + currency: 'USD', + balance: '4.02', + ), + Account( + name: 'Vusumuzi Khoza', + description: 'ZWG Wallet', + image: 'zimbabwe.png', + currency: 'ZWL', + balance: '36.00', + ), + ]; + } } diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart index 7b94215..3dbb25b 100644 --- a/lib/screens/home/home_screen.dart +++ b/lib/screens/home/home_screen.dart @@ -80,6 +80,7 @@ class _HomeScreenState extends State Future setupDataAndTransitions() async { homeController = HomeController(context); + await homeController.getAccounts(); await homeController.getCategories(); await homeController.getProviders(); @@ -108,20 +109,7 @@ class _HomeScreenState extends State } getBoxDecoration(String? clientId) { - // BillProvider? provider = homeController.model.selectedProvider; - // - // if (provider != null && clientId != provider.clientId) { - // return BoxDecoration( - // border: Border.all(color: Colors.black87.withAlpha(20)), - // borderRadius: BorderRadius.circular(12), - // ); - // } - return BoxDecoration( - // border: Border.all( - // color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.7), - // width: 3, - // ), border: Border.all(color: Colors.black87.withAlpha(20)), borderRadius: BorderRadius.circular(12), gradient: LinearGradient( @@ -161,12 +149,45 @@ class _HomeScreenState extends State surfaceTintColor: Colors.transparent, floating: true, title: Image(image: AssetImage('assets/peak.png'), width: 85), - actions: [ - // IconButton( - // icon: const Icon(Icons.notifications_outlined), - // onPressed: () {}, - // ), - ], + leading: IconButton( + icon: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + border: Border.all(color: Colors.black87.withAlpha(20)), + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.1), + Theme.of( + context, + ).colorScheme.tertiary.withValues(alpha: 0.05), + ], + stops: const [0.3, 0.7], + begin: Alignment.topRight, + end: Alignment.bottomLeft, + ), + ), + child: Center( + child: Text( + 'VK', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + onPressed: () {}, + ), + // actions: [ + // IconButton( + // icon: const Icon(Icons.notifications_outlined), + // onPressed: () {}, + // ), + // ], ), SliverToBoxAdapter( child: Padding( @@ -177,29 +198,8 @@ class _HomeScreenState extends State return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (_isLoggedIn) - AlignTransition( - alignment: _alignAnimation, - child: Skeletonizer( - enabled: _loading, - child: Column( - children: [ - Text( - "Vusumuzi Khoza", - style: TextStyle(fontSize: 15), - ), - Text( - "USD 4.02", - style: TextStyle( - fontSize: 30, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 20), - ], - ), - ), - ), + if (!_isLoggedIn) _buildLogin(homeController), + if (_isLoggedIn) _buildAccounts(homeController), _buildFilterChips(homeController), SizedBox(height: 10), Column( @@ -209,7 +209,7 @@ class _HomeScreenState extends State ), ], ), - const SizedBox(height: 20), + const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, @@ -273,6 +273,174 @@ class _HomeScreenState extends State ); } + Widget _buildLogin(HomeController controller) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + 'Register or Login for more', + style: TextStyle(fontSize: 16), + ), + InkWell( + onTap: () { + _showFeaturesPopup(context); + }, + borderRadius: BorderRadius.circular(5), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 5, vertical: 2), + child: Text( + 'features', + style: TextStyle( + fontSize: 16, + decoration: TextDecoration.underline, + decorationColor: Theme.of(context).colorScheme.primary, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ), + ], + ), + InkWell( + onTap: () { + context.push('/onboarding/landing'); + }, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Text('Get Started', style: TextStyle(fontSize: 12)), + ), + ), + ], + ), + SizedBox(height: 10), + Divider( + color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.2), + ), + ], + ); + } + + Widget _buildAccounts(HomeController controller) { + return Column( + children: [ + Row( + children: [ + ...controller.model.accounts.map( + (e) => Row( + children: [ + Skeletonizer( + enabled: controller.model.isLoading, + child: SlideTransition( + position: _alignListAnimations[0], + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () { + // context.push("/wallet"); + }, + child: Container( + width: 150, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 12, + ), + decoration: BoxDecoration( + border: Border.all( + color: Colors.black87.withAlpha(20), + ), + borderRadius: BorderRadius.circular(12), + gradient: LinearGradient( + colors: [ + Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.1), + Theme.of( + context, + ).colorScheme.tertiary.withValues(alpha: 0.05), + ], + stops: const [0.3, 0.7], + begin: Alignment.topRight, + end: Alignment.bottomLeft, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset('assets/${e.image}', width: 20), + SizedBox(height: 5), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + e.currency, + style: TextStyle( + fontSize: 22, + color: Colors.black, + fontWeight: FontWeight.w100, + ), + ), + SizedBox(width: 5), + Text( + e.balance, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 22, + color: Colors.black, + ), + ), + ], + ), + SizedBox(height: 5), + InkWell( + onTap: () { + // context.push("/withdraw"); + }, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary + .withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + 'Add Money', + style: TextStyle(fontSize: 12), + ), + ), + ), + ], + ), + ), + ), + ), + ), + SizedBox(width: 10), + ], + ), + ), + ], + ), + SizedBox(height: 10), + ], + ); + } + Widget _buildTransactionItem(Map e, BuildContext context) { return SlideTransition( position: _alignListAnimations[0], @@ -463,4 +631,197 @@ class _HomeScreenState extends State ), ); } + + void _showFeaturesPopup(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + elevation: 0, + backgroundColor: Colors.transparent, + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header with icon and title + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: Image(image: AssetImage("assets/peak.png"), width: 45), + ), + const SizedBox(height: 16), + Text( + 'Unlock Additional Features', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Register or login to access these great features', + style: TextStyle( + fontSize: 14, + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + + // Features list + _buildFeatureItem( + Icons.account_balance_wallet, + 'Dual Currency Wallets', + 'USD and ZWG wallets for prepayments that can be utilized later', + ), + const SizedBox(height: 16), + _buildFeatureItem( + Icons.history, + 'Transaction History', + 'All transactions are saved for reference from any device', + ), + const SizedBox(height: 16), + _buildFeatureItem( + Icons.people, + 'Recipient Management', + 'Save and manage recipient details for future use', + ), + const SizedBox(height: 16), + _buildFeatureItem( + Icons.schedule, + 'Smart Payments', + 'Schedule payments and split bills with friends', + ), + + const SizedBox(height: 24), + + // Action buttons + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () => Navigator.of(context).pop(), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + 'Maybe Later', + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.6), + fontSize: 16, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + context.push('/onboarding/landing'); + }, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + backgroundColor: + Theme.of(context).colorScheme.primary, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + 'Get Started', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildFeatureItem(IconData icon, String title, String description) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + size: 20, + color: Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 8), + Text( + description, + style: TextStyle( + fontSize: 14, + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.7), + height: 1.4, + ), + ), + ], + ), + ), + ], + ); + } } diff --git a/lib/screens/onboarding/landing/landing_screen.dart b/lib/screens/onboarding/landing/landing_screen.dart new file mode 100644 index 0000000..eaf6586 --- /dev/null +++ b/lib/screens/onboarding/landing/landing_screen.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:qpay/screens/onboarding/login/login_controller.dart'; + +class LandingScreen extends StatefulWidget { + const LandingScreen({super.key}); + + @override + State createState() => _LandingScreenState(); +} + +class _LandingScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(25), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: 75), + Text( + 'Welcome to Peak', + style: TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + SizedBox(height: 5), + Text( + 'The fastest way to pay for local goods and services', + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + Image.asset('assets/landing.png', width: 300), + Image.asset('assets/peak.png', width: 100), + SizedBox(height: 5), + Text( + 'Register or login to get started', + style: TextStyle(fontSize: 16), + ), + SizedBox(height: 10), + ], + ), + ), + ), + bottomNavigationBar: SizedBox( + height: 130, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 25), + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () { + context.go('/onboarding/register'); + }, + child: Text('Register', style: TextStyle(fontSize: 16)), + ), + ), + SizedBox(width: 16), + Expanded( + child: TextButton( + onPressed: () { + context.go('/onboarding/login'); + }, + child: Text('Login', style: TextStyle(fontSize: 16)), + ), + ), + ], + ), + ), + SizedBox(height: 5), + TextButton( + onPressed: () { + context.go('/'); + }, + child: Text('Continue as guest?', style: TextStyle(fontSize: 14)), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/onboarding/login/login_controller.dart b/lib/screens/onboarding/login/login_controller.dart new file mode 100644 index 0000000..823fbaf --- /dev/null +++ b/lib/screens/onboarding/login/login_controller.dart @@ -0,0 +1,80 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:qpay/http/http.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class LoginScreenModel { + String username = ""; + String password = ""; + String status = "PENDING"; + + bool isLoading = false; + String? errorMessage; +} + +class LoginController extends ChangeNotifier { + final Http http = Http(); + final model = LoginScreenModel(); + late SharedPreferences prefs; + BuildContext context; + + LoginController(this.context); + + void _showErrorSnackBar(String? message) { + WidgetsBinding.instance.addPostFrameCallback((_) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message.toString()), + behavior: SnackBarBehavior.floating, + backgroundColor: Colors.deepOrange, + ), + ); + }); + } + + Future login(String username, String password) async { + try { + model.isLoading = true; + notifyListeners(); + + prefs = await SharedPreferences.getInstance(); + + dynamic response = await http.post('/auth/login', { + 'username': username, + 'password': password, + }); + logger.i(response.toString()); + + if (response.containsKey('token')) { + model.status = 'SUCCESS'; + + prefs.setString('token', response['token']); + prefs.setString('username', username); + prefs.setString('email', response['email']); + prefs.setString('firstName', response['firstName']); + prefs.setString('lastName', response['lastName']); + prefs.setString('phone', response['phone']); + + return model; + } else { + model.errorMessage = + response['errorMessage'] ?? + "Problem with the transaction. Please try again or contact support"; + _showErrorSnackBar(model.errorMessage); + } + } on DioException catch (e, s) { + logger.e(s); + _showErrorSnackBar("Network error. Please try again or contact support"); + } catch (e, s) { + logger.e(s); + _showErrorSnackBar( + "Problem processing that transaction. Please try again or contact support", + ); + } + + model.isLoading = false; + notifyListeners(); + + return model; + } +} diff --git a/lib/screens/onboarding/login/login_screen.dart b/lib/screens/onboarding/login/login_screen.dart new file mode 100644 index 0000000..97c4a41 --- /dev/null +++ b/lib/screens/onboarding/login/login_screen.dart @@ -0,0 +1,231 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:qpay/screens/onboarding/login/login_controller.dart'; + +class LoginScreen extends StatefulWidget { + const LoginScreen({super.key}); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + final _formKey = GlobalKey(); + final _usernameController = TextEditingController(); + final _passwordController = TextEditingController(); + + late LoginController _loginController; + + @override + void initState() { + super.initState(); + _loginController = LoginController(context); + } + + @override + void dispose() { + _loginController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(25), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: 75), + Text( + 'Login', + style: TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + SizedBox(height: 5), + Text( + 'Welcome back to Peak', + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + SizedBox(height: 40), + // Username Field + TextFormField( + controller: _usernameController, + decoration: InputDecoration( + labelText: 'Phone Number', + prefixIcon: Icon(Icons.person_outline), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a phone number'; + } + return null; + }, + ), + SizedBox(height: 20), + // Password Field + TextFormField( + controller: _passwordController, + decoration: InputDecoration( + labelText: 'Password', + prefixIcon: Icon(Icons.lock_outline), + suffixIcon: Icon(Icons.visibility_outlined), + ), + obscureText: true, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a password'; + } + return null; + }, + ), + SizedBox(height: 20), + // Forgot Password Link + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () { + // TODO: Add forgot password logic + }, + child: Text( + 'Forgot Password?', + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontSize: 14, + ), + ), + ), + ), + SizedBox(height: 40), + // Divider with "or" text + Row( + children: [ + Expanded(child: Divider(color: Colors.grey.shade300)), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'or continue with', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 14, + ), + ), + ), + Expanded(child: Divider(color: Colors.grey.shade300)), + ], + ), + SizedBox(height: 30), + // Social Login Buttons + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () { + // TODO: Add Google login logic + }, + icon: Image.asset( + 'assets/google.png', + width: 20, + height: 20, + ), + label: Text('Google', style: TextStyle(fontSize: 16)), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 16), + side: BorderSide(color: Colors.grey.shade300), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + SizedBox(width: 16), + Expanded( + child: OutlinedButton.icon( + onPressed: () { + // TODO: Add Apple login logic + }, + icon: Image.asset( + 'assets/apple.png', + width: 20, + height: 20, + ), + label: Text('Apple', style: TextStyle(fontSize: 16)), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 16), + side: BorderSide(color: Colors.grey.shade300), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + ], + ), + SizedBox(height: 40), + ], + ), + ), + ), + ), + bottomNavigationBar: SizedBox( + height: 130, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 25), + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () async { + // TODO: Add login logic + if (_formKey.currentState!.validate()) { + LoginScreenModel model = await _loginController.login( + _usernameController.text, + _passwordController.text, + ); + + if (model.status == 'SUCCESS') { + context.go('/'); + } + } + }, + child: Text('Login', style: TextStyle(fontSize: 16)), + ), + ), + SizedBox(width: 16), + Expanded( + child: TextButton( + onPressed: () { + context.go('/onboarding/register'); + }, + child: Text( + 'Create Account', + style: TextStyle(fontSize: 16), + ), + ), + ), + ], + ), + ), + SizedBox(height: 5), + TextButton( + onPressed: () { + context.go('/'); + }, + child: Text('Continue as guest?', style: TextStyle(fontSize: 14)), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/onboarding/password/password_screen.dart b/lib/screens/onboarding/password/password_screen.dart new file mode 100644 index 0000000..54943c0 --- /dev/null +++ b/lib/screens/onboarding/password/password_screen.dart @@ -0,0 +1,255 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class PasswordScreen extends StatefulWidget { + const PasswordScreen({super.key}); + + @override + State createState() => _PasswordScreenState(); +} + +class _PasswordScreenState extends State { + final TextEditingController _passwordController = TextEditingController(); + final TextEditingController _confirmPasswordController = + TextEditingController(); + final FocusNode _passwordFocusNode = FocusNode(); + final FocusNode _confirmPasswordFocusNode = FocusNode(); + bool _obscurePassword = true; + bool _obscureConfirmPassword = true; + + @override + void dispose() { + _passwordController.dispose(); + _confirmPasswordController.dispose(); + _passwordFocusNode.dispose(); + _confirmPasswordFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(25), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: 75), + Text( + 'Set New Password', + style: TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + SizedBox(height: 5), + Text( + 'Create a strong password to secure your account', + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + Image.asset('assets/password.png', width: 300), + // Password Input + TextField( + controller: _passwordController, + focusNode: _passwordFocusNode, + obscureText: _obscurePassword, + decoration: InputDecoration( + labelText: 'New Password', + hintText: 'Enter your new password', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + width: 2, + ), + ), + contentPadding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility + : Icons.visibility_off, + color: Colors.grey.shade600, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + ), + ), + SizedBox(height: 20), + // Confirm Password Input + TextField( + controller: _confirmPasswordController, + focusNode: _confirmPasswordFocusNode, + obscureText: _obscureConfirmPassword, + decoration: InputDecoration( + labelText: 'Confirm Password', + hintText: 'Confirm your new password', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + width: 2, + ), + ), + contentPadding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + suffixIcon: IconButton( + icon: Icon( + _obscureConfirmPassword + ? Icons.visibility + : Icons.visibility_off, + color: Colors.grey.shade600, + ), + onPressed: () { + setState(() { + _obscureConfirmPassword = !_obscureConfirmPassword; + }); + }, + ), + ), + ), + SizedBox(height: 20), + // Password Requirements + Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Password Requirements:', + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: Colors.grey.shade700, + ), + ), + SizedBox(height: 8), + _buildRequirement('At least 8 characters', true), + _buildRequirement('Contains uppercase letter', true), + _buildRequirement('Contains lowercase letter', true), + _buildRequirement('Contains number', true), + _buildRequirement('Contains special character', false), + ], + ), + ), + SizedBox(height: 40), + ], + ), + ), + ), + bottomNavigationBar: SizedBox( + height: 130, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 25), + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () { + // TODO: Add password update logic + String password = _passwordController.text; + String confirmPassword = + _confirmPasswordController.text; + print('Password: $password'); + print('Confirm Password: $confirmPassword'); + + context.go('/onboarding/login'); + }, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + 'Update Password', + style: TextStyle(fontSize: 16), + ), + ), + ), + SizedBox(width: 16), + Expanded( + child: OutlinedButton( + onPressed: () { + context.go('/onboarding/login'); + }, + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 16), + side: BorderSide(color: Colors.grey.shade300), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + 'Back to Login', + style: TextStyle(fontSize: 16), + ), + ), + ), + ], + ), + ), + SizedBox(height: 20), + TextButton( + onPressed: () { + context.go('/'); + }, + child: Text('Continue as guest?', style: TextStyle(fontSize: 14)), + ), + ], + ), + ), + ); + } + + Widget _buildRequirement(String text, bool isMet) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 2), + child: Row( + children: [ + Icon( + isMet ? Icons.check_circle : Icons.circle_outlined, + size: 16, + color: isMet ? Colors.green : Colors.grey.shade600, + ), + SizedBox(width: 8), + Text( + text, + style: TextStyle( + fontSize: 12, + color: isMet ? Colors.grey.shade600 : Colors.grey.shade400, + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/onboarding/register/register_screen.dart b/lib/screens/onboarding/register/register_screen.dart new file mode 100644 index 0000000..f85bd67 --- /dev/null +++ b/lib/screens/onboarding/register/register_screen.dart @@ -0,0 +1,194 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class RegisterScreen extends StatefulWidget { + const RegisterScreen({super.key}); + + @override + State createState() => _RegisterScreenState(); +} + +class _RegisterScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(25), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: 75), + Text( + 'Registration', + style: TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + SizedBox(height: 5), + Text( + 'Join Peak and start making fast payments', + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + SizedBox(height: 40), + // First Name Field + TextField( + decoration: InputDecoration( + labelText: 'First Name', + prefixIcon: Icon(Icons.person_outline), + ), + ), + SizedBox(height: 20), + // Last Name Field + TextField( + decoration: InputDecoration( + labelText: 'Last Name', + prefixIcon: Icon(Icons.person_outline), + ), + ), + SizedBox(height: 20), + // Phone Field + TextField( + decoration: InputDecoration( + labelText: 'Phone Number', + prefixIcon: Icon(Icons.phone_outlined), + ), + keyboardType: TextInputType.phone, + ), + SizedBox(height: 20), + // Email Field + TextField( + decoration: InputDecoration( + labelText: 'Email Address', + prefixIcon: Icon(Icons.email_outlined), + ), + keyboardType: TextInputType.emailAddress, + ), + SizedBox(height: 20), + // Password Field + TextField( + decoration: InputDecoration( + labelText: 'Password', + prefixIcon: Icon(Icons.lock_outline), + suffixIcon: Icon(Icons.visibility_outlined), + ), + obscureText: true, + ), + SizedBox(height: 40), + // Divider with "or" text + Row( + children: [ + Expanded(child: Divider(color: Colors.grey.shade300)), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'or continue with', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 14, + ), + ), + ), + Expanded(child: Divider(color: Colors.grey.shade300)), + ], + ), + SizedBox(height: 30), + // Social Login Buttons + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () { + // TODO: Add Google login logic + }, + icon: Image.asset( + 'assets/google.png', + width: 20, + height: 20, + ), + label: Text('Google', style: TextStyle(fontSize: 16)), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 16), + side: BorderSide(color: Colors.grey.shade300), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + SizedBox(width: 16), + Expanded( + child: OutlinedButton.icon( + onPressed: () { + // TODO: Add Apple login logic + }, + icon: Image.asset( + 'assets/apple.png', + width: 20, + height: 20, + ), + label: Text('Apple', style: TextStyle(fontSize: 16)), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 16), + side: BorderSide(color: Colors.grey.shade300), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + ], + ), + SizedBox(height: 40), + ], + ), + ), + ), + bottomNavigationBar: SizedBox( + height: 130, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 25), + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () { + context.go('/onboarding/verify'); + }, + child: Text('Submit', style: TextStyle(fontSize: 16)), + ), + ), + SizedBox(width: 16), + Expanded( + child: TextButton( + onPressed: () { + context.go('/onboarding/login'); + }, + child: Text( + 'Go to Login', + style: TextStyle(fontSize: 16), + ), + ), + ), + ], + ), + ), + SizedBox(height: 5), + TextButton( + onPressed: () { + context.go('/'); + }, + child: Text('Continue as guest?', style: TextStyle(fontSize: 14)), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/onboarding/verify/verify_screen.dart b/lib/screens/onboarding/verify/verify_screen.dart new file mode 100644 index 0000000..98a73e2 --- /dev/null +++ b/lib/screens/onboarding/verify/verify_screen.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class VerifyScreen extends StatefulWidget { + const VerifyScreen({super.key}); + + @override + State createState() => _VerifyScreenState(); +} + +class _VerifyScreenState extends State { + final List _codeControllers = List.generate( + 6, + (index) => TextEditingController(), + ); + final List _focusNodes = List.generate(6, (index) => FocusNode()); + + @override + void dispose() { + for (var controller in _codeControllers) { + controller.dispose(); + } + for (var node in _focusNodes) { + node.dispose(); + } + super.dispose(); + } + + void _onCodeChanged(String value, int index) { + if (value.length == 1 && index < 5) { + _focusNodes[index + 1].requestFocus(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(25), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: 75), + Text( + 'Verify Your Account', + style: TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + SizedBox(height: 5), + Text( + 'Enter the 6-digit code sent to your phone/email', + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + Image.asset('assets/verify.png', width: 300), + // Verification Code Input + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate( + 6, + (index) => SizedBox( + width: 50, + child: TextField( + controller: _codeControllers[index], + focusNode: _focusNodes[index], + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + maxLength: 1, + decoration: InputDecoration( + counterText: '', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + width: 2, + ), + ), + contentPadding: EdgeInsets.symmetric(vertical: 16), + ), + onChanged: (value) => _onCodeChanged(value, index), + ), + ), + ), + ), + SizedBox(height: 30), + // Resend Code Section + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Didn't receive the code? ", + style: TextStyle(color: Colors.grey.shade600, fontSize: 16), + ), + TextButton( + onPressed: () { + // TODO: Add resend code logic + }, + child: Text( + 'Resend Code', + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + SizedBox(height: 20), + // Timer for resend (optional) + Text( + 'Resend available in 2:30', + style: TextStyle(color: Colors.grey.shade500, fontSize: 14), + ), + SizedBox(height: 40), + ], + ), + ), + ), + bottomNavigationBar: SizedBox( + height: 130, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 25), + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () { + context.go('/onboarding/password'); + }, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + 'Verify Code', + style: TextStyle(fontSize: 16), + ), + ), + ), + SizedBox(width: 16), + Expanded( + child: OutlinedButton( + onPressed: () { + context.go('/onboarding/login'); + }, + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 16), + side: BorderSide(color: Colors.grey.shade300), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + 'Back to Login', + style: TextStyle(fontSize: 16), + ), + ), + ), + ], + ), + ), + SizedBox(height: 20), + TextButton( + onPressed: () { + context.go('/'); + }, + child: Text('Continue as guest?', style: TextStyle(fontSize: 14)), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/pay/pay_controller.dart b/lib/screens/pay/pay_controller.dart index 3adae13..01fd76c 100644 --- a/lib/screens/pay/pay_controller.dart +++ b/lib/screens/pay/pay_controller.dart @@ -112,7 +112,7 @@ class PayController extends ChangeNotifier { ); dynamic response = await http.post( - '/transaction', + '/public/transaction', transactionController.model.formData, ); logger.i(response.toString()); @@ -148,7 +148,7 @@ class PayController extends ChangeNotifier { model.isLoading = true; List response = await http.get( - '/providers/$providerId/products', + '/public/providers/$providerId/products', ); model.products = response.map((e) => BillProduct.fromJson(e)).toList(); } catch (e) { @@ -168,7 +168,7 @@ class PayController extends ChangeNotifier { model.paymentProcessors = getFakePaymentProcessors(); notifyListeners(); - List response = await http.get('/payment-processors'); + List response = await http.get('/public/payment-processors'); model.paymentProcessors = response.map((e) => PaymentProcessor.fromJson(e)).toList(); diff --git a/lib/screens/receipt/receipt_controller.dart b/lib/screens/receipt/receipt_controller.dart index c7168e0..573b4a0 100644 --- a/lib/screens/receipt/receipt_controller.dart +++ b/lib/screens/receipt/receipt_controller.dart @@ -50,7 +50,7 @@ class ReceiptController extends ChangeNotifier { // fetch provider & update tran controller String providerId = model.receiptData?['billClientId'] ?? ''; Map provider = await http.get( - '/providers/client/$providerId', + '/public/providers/client/$providerId', ); transactionController.model.selectedProvider = hc.BillProvider.fromJson( provider, @@ -72,7 +72,7 @@ class ReceiptController extends ChangeNotifier { if (productUid.isNotEmpty) { Map product = await http.get( - '/providers/$providerUid/products/$productUid', + '/public/providers/$providerUid/products/$productUid', ); transactionController.model.selectedProduct = pc.BillProduct.fromJson( product, @@ -96,7 +96,7 @@ class ReceiptController extends ChangeNotifier { Future getReceiptData(String id) async { setLoading(true); try { - Map response = await http.get('/transaction/$id'); + Map response = await http.get('/public/transaction/$id'); model.receiptData = response; } catch (e) { _showErrorSnackBar( diff --git a/lib/screens/receipt/receipt_screen.dart b/lib/screens/receipt/receipt_screen.dart index 9c10d0e..f7aa8b6 100644 --- a/lib/screens/receipt/receipt_screen.dart +++ b/lib/screens/receipt/receipt_screen.dart @@ -212,7 +212,7 @@ class _ReceiptScreenState extends State Text( receiptController .model - .receiptData?["totalAmount"] + .receiptData?["amount"] .toStringAsFixed(2) ?? "", style: TextStyle(fontSize: 26), @@ -447,6 +447,37 @@ class _ReceiptScreenState extends State ), ), const SizedBox(height: 10), + Skeletonizer( + enabled: + receiptController + .model + .isLoading, + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text( + "Debit Reference", + style: TextStyle( + fontSize: 16, + fontWeight: + FontWeight.bold, + ), + ), + Text( + transactionController + .model + .receiptData?["debitRef"] ?? + "", + style: TextStyle( + fontSize: 16, + ), + ), + ], + ), + ), + const SizedBox(height: 10), Skeletonizer( enabled: receiptController @@ -613,90 +644,123 @@ class _ReceiptScreenState extends State ], ), const SizedBox(height: 10), - Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Text( - "Our Charge", - style: TextStyle( - fontSize: 16, - fontWeight: - FontWeight.bold, + if (transactionController + .model + .receiptData?["charge"] != + 0) + Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text( + "Our Charge", + style: TextStyle( + fontSize: 16, + fontWeight: + FontWeight + .bold, + ), + ), + Text( + transactionController + .model + .receiptData?["charge"] + .toStringAsFixed( + 2, + ) ?? + "0.00", + style: TextStyle( + fontSize: 16, + ), + ), + ], ), - ), - Text( - transactionController - .model - .receiptData?["charge"] - .toStringAsFixed( - 2, - ) ?? - "0.00", - style: TextStyle( - fontSize: 16, + const SizedBox( + height: 10, ), - ), - ], - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Text( - "Gateway Charge", - style: TextStyle( - fontSize: 16, - fontWeight: - FontWeight.bold, + ], + ), + if (transactionController + .model + .receiptData?["gatewayCharge"] != + 0) + Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text( + "Gateway Charge", + style: TextStyle( + fontSize: 16, + fontWeight: + FontWeight + .bold, + ), + ), + Text( + transactionController + .model + .receiptData?["gatewayCharge"] + .toStringAsFixed( + 2, + ) ?? + "0.00", + style: TextStyle( + fontSize: 16, + ), + ), + ], ), - ), - Text( - transactionController - .model - .receiptData?["gatewayCharge"] - .toStringAsFixed( - 2, - ) ?? - "0.00", - style: TextStyle( - fontSize: 16, + const SizedBox( + height: 10, ), - ), - ], - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Text( - "Tax", - style: TextStyle( - fontSize: 16, - fontWeight: - FontWeight.bold, + ], + ), + if (transactionController + .model + .receiptData?["tax"] != + 0) + Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text( + "Tax", + style: TextStyle( + fontSize: 16, + fontWeight: + FontWeight + .bold, + ), + ), + Text( + transactionController + .model + .receiptData?["tax"] + .toStringAsFixed( + 2, + ) ?? + "0.00", + style: TextStyle( + fontSize: 16, + ), + ), + ], ), - ), - Text( - transactionController - .model - .receiptData?["tax"] - .toStringAsFixed( - 2, - ) ?? - "0.00", - style: TextStyle( - fontSize: 16, + const SizedBox( + height: 10, ), - ), - ], - ), - const SizedBox(height: 10), + ], + ), Row( mainAxisAlignment: MainAxisAlignment diff --git a/lib/screens/recipient/recipients_controller.dart b/lib/screens/recipient/recipients_controller.dart index f42bbcd..f01c512 100644 --- a/lib/screens/recipient/recipients_controller.dart +++ b/lib/screens/recipient/recipients_controller.dart @@ -67,7 +67,7 @@ class RecipientsController extends ChangeNotifier { try { List response = await http.get( - '/recipients/search?${buildQueryParameters(params ?? {})}', + '/public/recipients/search?${buildQueryParameters(params ?? {})}', ); model.recipients.clear(); model.recipients.addAll(response.map((e) => Recipient.fromJson(e))); diff --git a/lib/screens/transaction_controller.dart b/lib/screens/transaction_controller.dart index 973ca8a..0fee1e9 100644 --- a/lib/screens/transaction_controller.dart +++ b/lib/screens/transaction_controller.dart @@ -15,6 +15,7 @@ class TransactionModel { BillProvider? selectedProvider; Map? receiptData; FormData formData = FormData( + id: '', type: '', billClientId: '', debitRef: '', @@ -49,6 +50,7 @@ abstract class FormData with _$FormData { const factory FormData({ required String type, // CONFIRM, REQUEST, REVERSE required String billClientId, + required String id, required String debitRef, required String debitCurrency, required String amount, diff --git a/lib/screens/transaction_controller.freezed.dart b/lib/screens/transaction_controller.freezed.dart index 602c635..c204d16 100644 --- a/lib/screens/transaction_controller.freezed.dart +++ b/lib/screens/transaction_controller.freezed.dart @@ -17,7 +17,7 @@ T _$identity(T value) => value; mixin _$FormData { String get type;// CONFIRM, REQUEST, REVERSE - String get billClientId; String get debitRef; String get debitCurrency; String get amount; String get creditAccount; String? get creditPhone; String? get creditName; String? get creditEmail; String get billName; String? get errorMessage; String get status; String get paymentProcessorLabel; String get paymentProcessorName; String get paymentProcessorImage; String get providerImage; String get providerLabel; String get userId; String? get debitPhone; String? get debitAccount; String? get productUid; String? get trace; String? get authType; String? get charge; String? get gatewayCharge; String? get tax; String? get totalAmount; + String get billClientId; String get id; String get debitRef; String get debitCurrency; String get amount; String get creditAccount; String? get creditPhone; String? get creditName; String? get creditEmail; String get billName; String? get errorMessage; String get status; String get paymentProcessorLabel; String get paymentProcessorName; String get paymentProcessorImage; String get providerImage; String get providerLabel; String get userId; String? get debitPhone; String? get debitAccount; String? get productUid; String? get trace; String? get authType; String? get charge; String? get gatewayCharge; String? get tax; String? get totalAmount; /// Create a copy of FormData /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -30,16 +30,16 @@ $FormDataCopyWith get copyWith => _$FormDataCopyWithImpl(thi @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is FormData&&(identical(other.type, type) || other.type == type)&&(identical(other.billClientId, billClientId) || other.billClientId == billClientId)&&(identical(other.debitRef, debitRef) || other.debitRef == debitRef)&&(identical(other.debitCurrency, debitCurrency) || other.debitCurrency == debitCurrency)&&(identical(other.amount, amount) || other.amount == amount)&&(identical(other.creditAccount, creditAccount) || other.creditAccount == creditAccount)&&(identical(other.creditPhone, creditPhone) || other.creditPhone == creditPhone)&&(identical(other.creditName, creditName) || other.creditName == creditName)&&(identical(other.creditEmail, creditEmail) || other.creditEmail == creditEmail)&&(identical(other.billName, billName) || other.billName == billName)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.status, status) || other.status == status)&&(identical(other.paymentProcessorLabel, paymentProcessorLabel) || other.paymentProcessorLabel == paymentProcessorLabel)&&(identical(other.paymentProcessorName, paymentProcessorName) || other.paymentProcessorName == paymentProcessorName)&&(identical(other.paymentProcessorImage, paymentProcessorImage) || other.paymentProcessorImage == paymentProcessorImage)&&(identical(other.providerImage, providerImage) || other.providerImage == providerImage)&&(identical(other.providerLabel, providerLabel) || other.providerLabel == providerLabel)&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.debitPhone, debitPhone) || other.debitPhone == debitPhone)&&(identical(other.debitAccount, debitAccount) || other.debitAccount == debitAccount)&&(identical(other.productUid, productUid) || other.productUid == productUid)&&(identical(other.trace, trace) || other.trace == trace)&&(identical(other.authType, authType) || other.authType == authType)&&(identical(other.charge, charge) || other.charge == charge)&&(identical(other.gatewayCharge, gatewayCharge) || other.gatewayCharge == gatewayCharge)&&(identical(other.tax, tax) || other.tax == tax)&&(identical(other.totalAmount, totalAmount) || other.totalAmount == totalAmount)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is FormData&&(identical(other.type, type) || other.type == type)&&(identical(other.billClientId, billClientId) || other.billClientId == billClientId)&&(identical(other.id, id) || other.id == id)&&(identical(other.debitRef, debitRef) || other.debitRef == debitRef)&&(identical(other.debitCurrency, debitCurrency) || other.debitCurrency == debitCurrency)&&(identical(other.amount, amount) || other.amount == amount)&&(identical(other.creditAccount, creditAccount) || other.creditAccount == creditAccount)&&(identical(other.creditPhone, creditPhone) || other.creditPhone == creditPhone)&&(identical(other.creditName, creditName) || other.creditName == creditName)&&(identical(other.creditEmail, creditEmail) || other.creditEmail == creditEmail)&&(identical(other.billName, billName) || other.billName == billName)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.status, status) || other.status == status)&&(identical(other.paymentProcessorLabel, paymentProcessorLabel) || other.paymentProcessorLabel == paymentProcessorLabel)&&(identical(other.paymentProcessorName, paymentProcessorName) || other.paymentProcessorName == paymentProcessorName)&&(identical(other.paymentProcessorImage, paymentProcessorImage) || other.paymentProcessorImage == paymentProcessorImage)&&(identical(other.providerImage, providerImage) || other.providerImage == providerImage)&&(identical(other.providerLabel, providerLabel) || other.providerLabel == providerLabel)&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.debitPhone, debitPhone) || other.debitPhone == debitPhone)&&(identical(other.debitAccount, debitAccount) || other.debitAccount == debitAccount)&&(identical(other.productUid, productUid) || other.productUid == productUid)&&(identical(other.trace, trace) || other.trace == trace)&&(identical(other.authType, authType) || other.authType == authType)&&(identical(other.charge, charge) || other.charge == charge)&&(identical(other.gatewayCharge, gatewayCharge) || other.gatewayCharge == gatewayCharge)&&(identical(other.tax, tax) || other.tax == tax)&&(identical(other.totalAmount, totalAmount) || other.totalAmount == totalAmount)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hashAll([runtimeType,type,billClientId,debitRef,debitCurrency,amount,creditAccount,creditPhone,creditName,creditEmail,billName,errorMessage,status,paymentProcessorLabel,paymentProcessorName,paymentProcessorImage,providerImage,providerLabel,userId,debitPhone,debitAccount,productUid,trace,authType,charge,gatewayCharge,tax,totalAmount]); +int get hashCode => Object.hashAll([runtimeType,type,billClientId,id,debitRef,debitCurrency,amount,creditAccount,creditPhone,creditName,creditEmail,billName,errorMessage,status,paymentProcessorLabel,paymentProcessorName,paymentProcessorImage,providerImage,providerLabel,userId,debitPhone,debitAccount,productUid,trace,authType,charge,gatewayCharge,tax,totalAmount]); @override String toString() { - return 'FormData(type: $type, billClientId: $billClientId, debitRef: $debitRef, debitCurrency: $debitCurrency, amount: $amount, creditAccount: $creditAccount, creditPhone: $creditPhone, creditName: $creditName, creditEmail: $creditEmail, billName: $billName, errorMessage: $errorMessage, status: $status, paymentProcessorLabel: $paymentProcessorLabel, paymentProcessorName: $paymentProcessorName, paymentProcessorImage: $paymentProcessorImage, providerImage: $providerImage, providerLabel: $providerLabel, userId: $userId, debitPhone: $debitPhone, debitAccount: $debitAccount, productUid: $productUid, trace: $trace, authType: $authType, charge: $charge, gatewayCharge: $gatewayCharge, tax: $tax, totalAmount: $totalAmount)'; + return 'FormData(type: $type, billClientId: $billClientId, id: $id, debitRef: $debitRef, debitCurrency: $debitCurrency, amount: $amount, creditAccount: $creditAccount, creditPhone: $creditPhone, creditName: $creditName, creditEmail: $creditEmail, billName: $billName, errorMessage: $errorMessage, status: $status, paymentProcessorLabel: $paymentProcessorLabel, paymentProcessorName: $paymentProcessorName, paymentProcessorImage: $paymentProcessorImage, providerImage: $providerImage, providerLabel: $providerLabel, userId: $userId, debitPhone: $debitPhone, debitAccount: $debitAccount, productUid: $productUid, trace: $trace, authType: $authType, charge: $charge, gatewayCharge: $gatewayCharge, tax: $tax, totalAmount: $totalAmount)'; } @@ -50,7 +50,7 @@ abstract mixin class $FormDataCopyWith<$Res> { factory $FormDataCopyWith(FormData value, $Res Function(FormData) _then) = _$FormDataCopyWithImpl; @useResult $Res call({ - String type, String billClientId, String debitRef, String debitCurrency, String amount, String creditAccount, String? creditPhone, String? creditName, String? creditEmail, String billName, String? errorMessage, String status, String paymentProcessorLabel, String paymentProcessorName, String paymentProcessorImage, String providerImage, String providerLabel, String userId, String? debitPhone, String? debitAccount, String? productUid, String? trace, String? authType, String? charge, String? gatewayCharge, String? tax, String? totalAmount + String type, String billClientId, String id, String debitRef, String debitCurrency, String amount, String creditAccount, String? creditPhone, String? creditName, String? creditEmail, String billName, String? errorMessage, String status, String paymentProcessorLabel, String paymentProcessorName, String paymentProcessorImage, String providerImage, String providerLabel, String userId, String? debitPhone, String? debitAccount, String? productUid, String? trace, String? authType, String? charge, String? gatewayCharge, String? tax, String? totalAmount }); @@ -67,10 +67,11 @@ class _$FormDataCopyWithImpl<$Res> /// Create a copy of FormData /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? billClientId = null,Object? debitRef = null,Object? debitCurrency = null,Object? amount = null,Object? creditAccount = null,Object? creditPhone = freezed,Object? creditName = freezed,Object? creditEmail = freezed,Object? billName = null,Object? errorMessage = freezed,Object? status = null,Object? paymentProcessorLabel = null,Object? paymentProcessorName = null,Object? paymentProcessorImage = null,Object? providerImage = null,Object? providerLabel = null,Object? userId = null,Object? debitPhone = freezed,Object? debitAccount = freezed,Object? productUid = freezed,Object? trace = freezed,Object? authType = freezed,Object? charge = freezed,Object? gatewayCharge = freezed,Object? tax = freezed,Object? totalAmount = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? billClientId = null,Object? id = null,Object? debitRef = null,Object? debitCurrency = null,Object? amount = null,Object? creditAccount = null,Object? creditPhone = freezed,Object? creditName = freezed,Object? creditEmail = freezed,Object? billName = null,Object? errorMessage = freezed,Object? status = null,Object? paymentProcessorLabel = null,Object? paymentProcessorName = null,Object? paymentProcessorImage = null,Object? providerImage = null,Object? providerLabel = null,Object? userId = null,Object? debitPhone = freezed,Object? debitAccount = freezed,Object? productUid = freezed,Object? trace = freezed,Object? authType = freezed,Object? charge = freezed,Object? gatewayCharge = freezed,Object? tax = freezed,Object? totalAmount = freezed,}) { return _then(_self.copyWith( type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as String,billClientId: null == billClientId ? _self.billClientId : billClientId // ignore: cast_nullable_to_non_nullable +as String,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,debitRef: null == debitRef ? _self.debitRef : debitRef // ignore: cast_nullable_to_non_nullable as String,debitCurrency: null == debitCurrency ? _self.debitCurrency : debitCurrency // ignore: cast_nullable_to_non_nullable as String,amount: null == amount ? _self.amount : amount // ignore: cast_nullable_to_non_nullable @@ -107,12 +108,13 @@ as String?, @JsonSerializable() class _FormData implements FormData { - const _FormData({required this.type, required this.billClientId, required this.debitRef, required this.debitCurrency, required this.amount, required this.creditAccount, required this.creditPhone, required this.creditName, required this.creditEmail, required this.billName, required this.errorMessage, required this.status, required this.paymentProcessorLabel, required this.paymentProcessorName, required this.paymentProcessorImage, required this.providerImage, required this.providerLabel, required this.userId, this.debitPhone, this.debitAccount, this.productUid, this.trace, this.authType, this.charge, this.gatewayCharge, this.tax, this.totalAmount}); + const _FormData({required this.type, required this.billClientId, required this.id, required this.debitRef, required this.debitCurrency, required this.amount, required this.creditAccount, required this.creditPhone, required this.creditName, required this.creditEmail, required this.billName, required this.errorMessage, required this.status, required this.paymentProcessorLabel, required this.paymentProcessorName, required this.paymentProcessorImage, required this.providerImage, required this.providerLabel, required this.userId, this.debitPhone, this.debitAccount, this.productUid, this.trace, this.authType, this.charge, this.gatewayCharge, this.tax, this.totalAmount}); factory _FormData.fromJson(Map json) => _$FormDataFromJson(json); @override final String type; // CONFIRM, REQUEST, REVERSE @override final String billClientId; +@override final String id; @override final String debitRef; @override final String debitCurrency; @override final String amount; @@ -152,16 +154,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _FormData&&(identical(other.type, type) || other.type == type)&&(identical(other.billClientId, billClientId) || other.billClientId == billClientId)&&(identical(other.debitRef, debitRef) || other.debitRef == debitRef)&&(identical(other.debitCurrency, debitCurrency) || other.debitCurrency == debitCurrency)&&(identical(other.amount, amount) || other.amount == amount)&&(identical(other.creditAccount, creditAccount) || other.creditAccount == creditAccount)&&(identical(other.creditPhone, creditPhone) || other.creditPhone == creditPhone)&&(identical(other.creditName, creditName) || other.creditName == creditName)&&(identical(other.creditEmail, creditEmail) || other.creditEmail == creditEmail)&&(identical(other.billName, billName) || other.billName == billName)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.status, status) || other.status == status)&&(identical(other.paymentProcessorLabel, paymentProcessorLabel) || other.paymentProcessorLabel == paymentProcessorLabel)&&(identical(other.paymentProcessorName, paymentProcessorName) || other.paymentProcessorName == paymentProcessorName)&&(identical(other.paymentProcessorImage, paymentProcessorImage) || other.paymentProcessorImage == paymentProcessorImage)&&(identical(other.providerImage, providerImage) || other.providerImage == providerImage)&&(identical(other.providerLabel, providerLabel) || other.providerLabel == providerLabel)&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.debitPhone, debitPhone) || other.debitPhone == debitPhone)&&(identical(other.debitAccount, debitAccount) || other.debitAccount == debitAccount)&&(identical(other.productUid, productUid) || other.productUid == productUid)&&(identical(other.trace, trace) || other.trace == trace)&&(identical(other.authType, authType) || other.authType == authType)&&(identical(other.charge, charge) || other.charge == charge)&&(identical(other.gatewayCharge, gatewayCharge) || other.gatewayCharge == gatewayCharge)&&(identical(other.tax, tax) || other.tax == tax)&&(identical(other.totalAmount, totalAmount) || other.totalAmount == totalAmount)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _FormData&&(identical(other.type, type) || other.type == type)&&(identical(other.billClientId, billClientId) || other.billClientId == billClientId)&&(identical(other.id, id) || other.id == id)&&(identical(other.debitRef, debitRef) || other.debitRef == debitRef)&&(identical(other.debitCurrency, debitCurrency) || other.debitCurrency == debitCurrency)&&(identical(other.amount, amount) || other.amount == amount)&&(identical(other.creditAccount, creditAccount) || other.creditAccount == creditAccount)&&(identical(other.creditPhone, creditPhone) || other.creditPhone == creditPhone)&&(identical(other.creditName, creditName) || other.creditName == creditName)&&(identical(other.creditEmail, creditEmail) || other.creditEmail == creditEmail)&&(identical(other.billName, billName) || other.billName == billName)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.status, status) || other.status == status)&&(identical(other.paymentProcessorLabel, paymentProcessorLabel) || other.paymentProcessorLabel == paymentProcessorLabel)&&(identical(other.paymentProcessorName, paymentProcessorName) || other.paymentProcessorName == paymentProcessorName)&&(identical(other.paymentProcessorImage, paymentProcessorImage) || other.paymentProcessorImage == paymentProcessorImage)&&(identical(other.providerImage, providerImage) || other.providerImage == providerImage)&&(identical(other.providerLabel, providerLabel) || other.providerLabel == providerLabel)&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.debitPhone, debitPhone) || other.debitPhone == debitPhone)&&(identical(other.debitAccount, debitAccount) || other.debitAccount == debitAccount)&&(identical(other.productUid, productUid) || other.productUid == productUid)&&(identical(other.trace, trace) || other.trace == trace)&&(identical(other.authType, authType) || other.authType == authType)&&(identical(other.charge, charge) || other.charge == charge)&&(identical(other.gatewayCharge, gatewayCharge) || other.gatewayCharge == gatewayCharge)&&(identical(other.tax, tax) || other.tax == tax)&&(identical(other.totalAmount, totalAmount) || other.totalAmount == totalAmount)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hashAll([runtimeType,type,billClientId,debitRef,debitCurrency,amount,creditAccount,creditPhone,creditName,creditEmail,billName,errorMessage,status,paymentProcessorLabel,paymentProcessorName,paymentProcessorImage,providerImage,providerLabel,userId,debitPhone,debitAccount,productUid,trace,authType,charge,gatewayCharge,tax,totalAmount]); +int get hashCode => Object.hashAll([runtimeType,type,billClientId,id,debitRef,debitCurrency,amount,creditAccount,creditPhone,creditName,creditEmail,billName,errorMessage,status,paymentProcessorLabel,paymentProcessorName,paymentProcessorImage,providerImage,providerLabel,userId,debitPhone,debitAccount,productUid,trace,authType,charge,gatewayCharge,tax,totalAmount]); @override String toString() { - return 'FormData(type: $type, billClientId: $billClientId, debitRef: $debitRef, debitCurrency: $debitCurrency, amount: $amount, creditAccount: $creditAccount, creditPhone: $creditPhone, creditName: $creditName, creditEmail: $creditEmail, billName: $billName, errorMessage: $errorMessage, status: $status, paymentProcessorLabel: $paymentProcessorLabel, paymentProcessorName: $paymentProcessorName, paymentProcessorImage: $paymentProcessorImage, providerImage: $providerImage, providerLabel: $providerLabel, userId: $userId, debitPhone: $debitPhone, debitAccount: $debitAccount, productUid: $productUid, trace: $trace, authType: $authType, charge: $charge, gatewayCharge: $gatewayCharge, tax: $tax, totalAmount: $totalAmount)'; + return 'FormData(type: $type, billClientId: $billClientId, id: $id, debitRef: $debitRef, debitCurrency: $debitCurrency, amount: $amount, creditAccount: $creditAccount, creditPhone: $creditPhone, creditName: $creditName, creditEmail: $creditEmail, billName: $billName, errorMessage: $errorMessage, status: $status, paymentProcessorLabel: $paymentProcessorLabel, paymentProcessorName: $paymentProcessorName, paymentProcessorImage: $paymentProcessorImage, providerImage: $providerImage, providerLabel: $providerLabel, userId: $userId, debitPhone: $debitPhone, debitAccount: $debitAccount, productUid: $productUid, trace: $trace, authType: $authType, charge: $charge, gatewayCharge: $gatewayCharge, tax: $tax, totalAmount: $totalAmount)'; } @@ -172,7 +174,7 @@ abstract mixin class _$FormDataCopyWith<$Res> implements $FormDataCopyWith<$Res> factory _$FormDataCopyWith(_FormData value, $Res Function(_FormData) _then) = __$FormDataCopyWithImpl; @override @useResult $Res call({ - String type, String billClientId, String debitRef, String debitCurrency, String amount, String creditAccount, String? creditPhone, String? creditName, String? creditEmail, String billName, String? errorMessage, String status, String paymentProcessorLabel, String paymentProcessorName, String paymentProcessorImage, String providerImage, String providerLabel, String userId, String? debitPhone, String? debitAccount, String? productUid, String? trace, String? authType, String? charge, String? gatewayCharge, String? tax, String? totalAmount + String type, String billClientId, String id, String debitRef, String debitCurrency, String amount, String creditAccount, String? creditPhone, String? creditName, String? creditEmail, String billName, String? errorMessage, String status, String paymentProcessorLabel, String paymentProcessorName, String paymentProcessorImage, String providerImage, String providerLabel, String userId, String? debitPhone, String? debitAccount, String? productUid, String? trace, String? authType, String? charge, String? gatewayCharge, String? tax, String? totalAmount }); @@ -189,10 +191,11 @@ class __$FormDataCopyWithImpl<$Res> /// Create a copy of FormData /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? billClientId = null,Object? debitRef = null,Object? debitCurrency = null,Object? amount = null,Object? creditAccount = null,Object? creditPhone = freezed,Object? creditName = freezed,Object? creditEmail = freezed,Object? billName = null,Object? errorMessage = freezed,Object? status = null,Object? paymentProcessorLabel = null,Object? paymentProcessorName = null,Object? paymentProcessorImage = null,Object? providerImage = null,Object? providerLabel = null,Object? userId = null,Object? debitPhone = freezed,Object? debitAccount = freezed,Object? productUid = freezed,Object? trace = freezed,Object? authType = freezed,Object? charge = freezed,Object? gatewayCharge = freezed,Object? tax = freezed,Object? totalAmount = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? billClientId = null,Object? id = null,Object? debitRef = null,Object? debitCurrency = null,Object? amount = null,Object? creditAccount = null,Object? creditPhone = freezed,Object? creditName = freezed,Object? creditEmail = freezed,Object? billName = null,Object? errorMessage = freezed,Object? status = null,Object? paymentProcessorLabel = null,Object? paymentProcessorName = null,Object? paymentProcessorImage = null,Object? providerImage = null,Object? providerLabel = null,Object? userId = null,Object? debitPhone = freezed,Object? debitAccount = freezed,Object? productUid = freezed,Object? trace = freezed,Object? authType = freezed,Object? charge = freezed,Object? gatewayCharge = freezed,Object? tax = freezed,Object? totalAmount = freezed,}) { return _then(_FormData( type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as String,billClientId: null == billClientId ? _self.billClientId : billClientId // ignore: cast_nullable_to_non_nullable +as String,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,debitRef: null == debitRef ? _self.debitRef : debitRef // ignore: cast_nullable_to_non_nullable as String,debitCurrency: null == debitCurrency ? _self.debitCurrency : debitCurrency // ignore: cast_nullable_to_non_nullable as String,amount: null == amount ? _self.amount : amount // ignore: cast_nullable_to_non_nullable diff --git a/lib/screens/transaction_controller.g.dart b/lib/screens/transaction_controller.g.dart index 01206c3..19be559 100644 --- a/lib/screens/transaction_controller.g.dart +++ b/lib/screens/transaction_controller.g.dart @@ -9,6 +9,7 @@ part of 'transaction_controller.dart'; _FormData _$FormDataFromJson(Map json) => _FormData( type: json['type'] as String, billClientId: json['billClientId'] as String, + id: json['id'] as String, debitRef: json['debitRef'] as String, debitCurrency: json['debitCurrency'] as String, amount: json['amount'] as String, @@ -39,6 +40,7 @@ _FormData _$FormDataFromJson(Map json) => _FormData( Map _$FormDataToJson(_FormData instance) => { 'type': instance.type, 'billClientId': instance.billClientId, + 'id': instance.id, 'debitRef': instance.debitRef, 'debitCurrency': instance.debitCurrency, 'amount': instance.amount, diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index be90ccc..9581ce5 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; class AppTheme { - static const _primaryColor = Color(0xFFD4AF37); // Gold + static const _primaryColor = Color(0xFF9D6711); // #9D6711 static const _secondaryColor = Color(0xFF8B0000); // Dark Red static const _tertiaryColor = Color(0xFF000000); // Black static const _backgroundColor = Color(0xFFFFFBFE); @@ -56,19 +56,19 @@ class AppTheme { fillColor: _surfaceColor, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, + borderSide: BorderSide(color: Colors.grey.shade300, width: 1), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, + borderSide: BorderSide(color: Colors.grey.shade300, width: 1), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide(color: _primaryColor), + borderSide: const BorderSide(color: _primaryColor, width: 2), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide(color: _errorColor), + borderSide: const BorderSide(color: _errorColor, width: 1), ), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), ),