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,13 +51,17 @@ class _ScaffoldWithNavBarState extends State<ScaffoldWithNavBar> {
|
||||
Expanded(
|
||||
child: NavigationRail(
|
||||
extended: true,
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12.0,
|
||||
),
|
||||
child: Image(
|
||||
image: AssetImage('assets/velocity.png'),
|
||||
width: 150,
|
||||
leading: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12.0,
|
||||
horizontal: 8
|
||||
),
|
||||
child: Image(
|
||||
image: AssetImage('assets/velocity.png'),
|
||||
width: 150,
|
||||
),
|
||||
),
|
||||
),
|
||||
labelType: NavigationRailLabelType.none,
|
||||
|
||||
@@ -59,6 +59,7 @@ class AccountProvider extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
return ApiResponse.failure(_accountError!);
|
||||
} catch (e, s) {
|
||||
logger.e(e);
|
||||
logger.e(s);
|
||||
_isLoadingAccount = false;
|
||||
_accountError = 'An unexpected error occurred. Please try again.';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:qpay/screens/accounts/account_provider.dart';
|
||||
import 'package:qpay/screens/accounts/models/account_model.dart';
|
||||
@@ -21,19 +22,21 @@ class AccountBalanceWidget extends StatefulWidget {
|
||||
class _AccountBalanceWidgetState extends State<AccountBalanceWidget> {
|
||||
late AccountProvider _accountProvider;
|
||||
|
||||
bool _isLoggedIn = false;
|
||||
String? _phoneNumber;
|
||||
|
||||
AccountModel? _usdAccount;
|
||||
AccountModel? _zwgAccount;
|
||||
bool _isLoadingUsd = false;
|
||||
bool _isLoadingZwG = false;
|
||||
String? _usdError;
|
||||
String? _zwgError;
|
||||
String? _phoneNumber;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_accountProvider = AccountProvider();
|
||||
_loadBalances();
|
||||
_loadAuthStateAndBalances();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -42,11 +45,21 @@ class _AccountBalanceWidgetState extends State<AccountBalanceWidget> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadBalances() async {
|
||||
Future<void> _loadAuthStateAndBalances() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// Determine login state from persisted token
|
||||
if (prefs.getString("token") != null) {
|
||||
setState(() {
|
||||
_isLoggedIn = true;
|
||||
});
|
||||
}
|
||||
|
||||
_phoneNumber = prefs.getString('phone');
|
||||
|
||||
if (_phoneNumber == null || _phoneNumber!.isEmpty) return;
|
||||
if (_phoneNumber == null || _phoneNumber!.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_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
|
||||
@@ -91,6 +165,10 @@ class _AccountBalanceWidgetState extends State<AccountBalanceWidget> {
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
if (!_isLoggedIn) {
|
||||
return _buildLoginPrompt(theme, isDark);
|
||||
}
|
||||
|
||||
if (_phoneNumber == null || _phoneNumber!.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
@@ -143,7 +221,7 @@ class _AccountBalanceWidgetState extends State<AccountBalanceWidget> {
|
||||
onPressed: _isLoadingUsd || _isLoadingZwG
|
||||
? null
|
||||
: () {
|
||||
_loadBalances();
|
||||
_refreshBalances();
|
||||
},
|
||||
icon: Icon(
|
||||
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({
|
||||
required ThemeData theme,
|
||||
required bool isDark,
|
||||
|
||||
@@ -393,15 +393,10 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!_isLoggedIn) ...[
|
||||
_buildLoginPrompt(theme, isDark),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
if (_isLoggedIn)
|
||||
AccountBalanceWidget(
|
||||
selectedCurrency: _selectedCurrency,
|
||||
onCurrencySelected: _onCurrencySelected,
|
||||
),
|
||||
AccountBalanceWidget(
|
||||
selectedCurrency: _selectedCurrency,
|
||||
onCurrencySelected: _onCurrencySelected,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -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
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
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
|
||||
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">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
@@ -26,12 +34,11 @@
|
||||
<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-title" content="qpay">
|
||||
<link rel="apple-touch-icon" href="icons/velocity-icon-black.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
<link rel="icon" href="/favicon.ico" sizes="any">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
|
||||
<title>Velocity</title>
|
||||
<title>Velocity Pay</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<script type="text/javascript">
|
||||
// If there's a pending transaction ID stored before redirecting to the
|
||||
|
||||
@@ -8,28 +8,11 @@
|
||||
"description": "Payments... but faster",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/Icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
"icons": [
|
||||
{ "src": "/favicon.ico", "type": "image/x-icon", "sizes": "16x16 32x32" },
|
||||
{ "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" },
|
||||
{ "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" },
|
||||
{ "src": "/icon-192-maskable.png", "type": "image/png", "sizes": "192x192", "purpose": "maskable" },
|
||||
{ "src": "/icon-512-maskable.png", "type": "image/png", "sizes": "512x512", "purpose": "maskable" }
|
||||
]
|
||||
}
|
||||