updated branding & added analytics
BIN
android/app/src/main/play_store_512.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 687 B After Width: | Height: | Size: 696 B |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 671 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 99 KiB |
@@ -51,15 +51,19 @@ class _ScaffoldWithNavBarState extends State<ScaffoldWithNavBar> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: NavigationRail(
|
child: NavigationRail(
|
||||||
extended: true,
|
extended: true,
|
||||||
leading: Padding(
|
leading: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 12.0,
|
vertical: 12.0,
|
||||||
|
horizontal: 8
|
||||||
),
|
),
|
||||||
child: Image(
|
child: Image(
|
||||||
image: AssetImage('assets/velocity.png'),
|
image: AssetImage('assets/velocity.png'),
|
||||||
width: 150,
|
width: 150,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
labelType: NavigationRailLabelType.none,
|
labelType: NavigationRailLabelType.none,
|
||||||
indicatorColor: Theme.of(
|
indicatorColor: Theme.of(
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ class AccountProvider extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
return ApiResponse.failure(_accountError!);
|
return ApiResponse.failure(_accountError!);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
|
logger.e(e);
|
||||||
logger.e(s);
|
logger.e(s);
|
||||||
_isLoadingAccount = false;
|
_isLoadingAccount = false;
|
||||||
_accountError = 'An unexpected error occurred. Please try again.';
|
_accountError = 'An unexpected error occurred. Please try again.';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:qpay/screens/accounts/account_provider.dart';
|
import 'package:qpay/screens/accounts/account_provider.dart';
|
||||||
import 'package:qpay/screens/accounts/models/account_model.dart';
|
import 'package:qpay/screens/accounts/models/account_model.dart';
|
||||||
@@ -21,19 +22,21 @@ class AccountBalanceWidget extends StatefulWidget {
|
|||||||
class _AccountBalanceWidgetState extends State<AccountBalanceWidget> {
|
class _AccountBalanceWidgetState extends State<AccountBalanceWidget> {
|
||||||
late AccountProvider _accountProvider;
|
late AccountProvider _accountProvider;
|
||||||
|
|
||||||
|
bool _isLoggedIn = false;
|
||||||
|
String? _phoneNumber;
|
||||||
|
|
||||||
AccountModel? _usdAccount;
|
AccountModel? _usdAccount;
|
||||||
AccountModel? _zwgAccount;
|
AccountModel? _zwgAccount;
|
||||||
bool _isLoadingUsd = false;
|
bool _isLoadingUsd = false;
|
||||||
bool _isLoadingZwG = false;
|
bool _isLoadingZwG = false;
|
||||||
String? _usdError;
|
String? _usdError;
|
||||||
String? _zwgError;
|
String? _zwgError;
|
||||||
String? _phoneNumber;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_accountProvider = AccountProvider();
|
_accountProvider = AccountProvider();
|
||||||
_loadBalances();
|
_loadAuthStateAndBalances();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -42,11 +45,21 @@ class _AccountBalanceWidgetState extends State<AccountBalanceWidget> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadBalances() async {
|
Future<void> _loadAuthStateAndBalances() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
|
// Determine login state from persisted token
|
||||||
|
if (prefs.getString("token") != null) {
|
||||||
|
setState(() {
|
||||||
|
_isLoggedIn = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_phoneNumber = prefs.getString('phone');
|
_phoneNumber = prefs.getString('phone');
|
||||||
|
|
||||||
if (_phoneNumber == null || _phoneNumber!.isEmpty) return;
|
if (_phoneNumber == null || _phoneNumber!.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoadingUsd = true;
|
_isLoadingUsd = true;
|
||||||
@@ -84,6 +97,67 @@ class _AccountBalanceWidgetState extends State<AccountBalanceWidget> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If both fetches failed, mark as not logged in
|
||||||
|
if (mounted &&
|
||||||
|
!usdResult.isSuccess &&
|
||||||
|
!zwgResult.isSuccess &&
|
||||||
|
_isLoggedIn) {
|
||||||
|
setState(() {
|
||||||
|
_isLoggedIn = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _refreshBalances() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
_phoneNumber = prefs.getString('phone');
|
||||||
|
|
||||||
|
if (_phoneNumber == null || _phoneNumber!.isEmpty) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoadingUsd = true;
|
||||||
|
_isLoadingZwG = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
final usdResult = await _accountProvider.fetchAccount(
|
||||||
|
'USD$_phoneNumber',
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoadingUsd = false;
|
||||||
|
if (usdResult.isSuccess && usdResult.data != null) {
|
||||||
|
_usdAccount = usdResult.data;
|
||||||
|
_usdError = null;
|
||||||
|
} else {
|
||||||
|
_usdError = usdResult.error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final zwgResult = await _accountProvider.fetchAccount(
|
||||||
|
'ZWG$_phoneNumber',
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoadingZwG = false;
|
||||||
|
if (zwgResult.isSuccess && zwgResult.data != null) {
|
||||||
|
_zwgAccount = zwgResult.data;
|
||||||
|
_zwgError = null;
|
||||||
|
} else {
|
||||||
|
_zwgError = zwgResult.error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted &&
|
||||||
|
!usdResult.isSuccess &&
|
||||||
|
!zwgResult.isSuccess &&
|
||||||
|
_isLoggedIn) {
|
||||||
|
setState(() {
|
||||||
|
_isLoggedIn = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -91,6 +165,10 @@ class _AccountBalanceWidgetState extends State<AccountBalanceWidget> {
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final isDark = theme.brightness == Brightness.dark;
|
final isDark = theme.brightness == Brightness.dark;
|
||||||
|
|
||||||
|
if (!_isLoggedIn) {
|
||||||
|
return _buildLoginPrompt(theme, isDark);
|
||||||
|
}
|
||||||
|
|
||||||
if (_phoneNumber == null || _phoneNumber!.isEmpty) {
|
if (_phoneNumber == null || _phoneNumber!.isEmpty) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
@@ -143,7 +221,7 @@ class _AccountBalanceWidgetState extends State<AccountBalanceWidget> {
|
|||||||
onPressed: _isLoadingUsd || _isLoadingZwG
|
onPressed: _isLoadingUsd || _isLoadingZwG
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
_loadBalances();
|
_refreshBalances();
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.refresh_rounded,
|
Icons.refresh_rounded,
|
||||||
@@ -176,6 +254,70 @@ class _AccountBalanceWidgetState extends State<AccountBalanceWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// Login Prompt
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
Widget _buildLoginPrompt(ThemeData theme, bool isDark) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 18),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
theme.colorScheme.primary.withValues(alpha: 0.12),
|
||||||
|
theme.colorScheme.primary.withValues(alpha: 0.04),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.colorScheme.primary.withValues(alpha: 0.15),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Register or Login for more features',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: isDark ? Colors.white : Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.push('/onboarding/landing');
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: theme.colorScheme.primary,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Get Started',
|
||||||
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildCompactCard({
|
Widget _buildCompactCard({
|
||||||
required ThemeData theme,
|
required ThemeData theme,
|
||||||
required bool isDark,
|
required bool isDark,
|
||||||
|
|||||||
@@ -393,11 +393,6 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (!_isLoggedIn) ...[
|
|
||||||
_buildLoginPrompt(theme, isDark),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
],
|
|
||||||
if (_isLoggedIn)
|
|
||||||
AccountBalanceWidget(
|
AccountBalanceWidget(
|
||||||
selectedCurrency: _selectedCurrency,
|
selectedCurrency: _selectedCurrency,
|
||||||
onCurrencySelected: _onCurrencySelected,
|
onCurrencySelected: _onCurrencySelected,
|
||||||
@@ -669,67 +664,6 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────────────
|
|
||||||
// Login Prompt
|
|
||||||
// ──────────────────────────────────────────────────────────────
|
|
||||||
Widget _buildLoginPrompt(ThemeData theme, bool isDark) {
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 18),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
colors: [
|
|
||||||
theme.colorScheme.primary.withValues(alpha: 0.12),
|
|
||||||
theme.colorScheme.primary.withValues(alpha: 0.04),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
border: Border.all(
|
|
||||||
color: theme.colorScheme.primary.withValues(alpha: 0.15),
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Register or Login for more features',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: isDark ? Colors.white : Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
context.push('/onboarding/landing');
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: theme.colorScheme.primary,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
elevation: 0,
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
'Get Started',
|
|
||||||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────
|
||||||
// Logout Dialog
|
// Logout Dialog
|
||||||
// ──────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|||||||
BIN
web/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
web/favicon.ico
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
web/icon-192-maskable.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
web/icon-192.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
web/icon-512-maskable.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
web/icon-512.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 22 KiB |
@@ -14,6 +14,14 @@
|
|||||||
This is a placeholder for base href that will be replaced by the value of
|
This is a placeholder for base href that will be replaced by the value of
|
||||||
the `--base-href` argument provided to `flutter build`.
|
the `--base-href` argument provided to `flutter build`.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<!-- Privacy-friendly analytics by Plausible -->
|
||||||
|
<script async src="https://plausible.velocityafrica.net/js/pa-NOCPVB_4rsj4DCjfHp8YT.js"></script>
|
||||||
|
<script>
|
||||||
|
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||||
|
plausible.init()
|
||||||
|
</script>
|
||||||
|
|
||||||
<base href="$FLUTTER_BASE_HREF">
|
<base href="$FLUTTER_BASE_HREF">
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
@@ -26,12 +34,11 @@
|
|||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
<meta name="apple-mobile-web-app-title" content="qpay">
|
<meta name="apple-mobile-web-app-title" content="qpay">
|
||||||
<link rel="apple-touch-icon" href="icons/velocity-icon-black.png">
|
|
||||||
|
|
||||||
<!-- Favicon -->
|
<link rel="icon" href="/favicon.ico" sizes="any">
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||||
|
|
||||||
<title>Velocity</title>
|
<title>Velocity Pay</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
// If there's a pending transaction ID stored before redirecting to the
|
// If there's a pending transaction ID stored before redirecting to the
|
||||||
|
|||||||
@@ -9,27 +9,10 @@
|
|||||||
"orientation": "portrait-primary",
|
"orientation": "portrait-primary",
|
||||||
"prefer_related_applications": false,
|
"prefer_related_applications": false,
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{ "src": "/favicon.ico", "type": "image/x-icon", "sizes": "16x16 32x32" },
|
||||||
"src": "icons/Icon-192.png",
|
{ "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" },
|
||||||
"sizes": "192x192",
|
{ "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" },
|
||||||
"type": "image/png"
|
{ "src": "/icon-192-maskable.png", "type": "image/png", "sizes": "192x192", "purpose": "maskable" },
|
||||||
},
|
{ "src": "/icon-512-maskable.png", "type": "image/png", "sizes": "512x512", "purpose": "maskable" }
|
||||||
{
|
|
||||||
"src": "icons/Icon-512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/Icon-maskable-192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/Icon-maskable-512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||