updated branding & added analytics

This commit is contained in:
Prince
2026-06-19 12:44:57 +02:00
parent f3ae5ca791
commit bb9dbac2a3
41 changed files with 181 additions and 110 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 687 B

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 671 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -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,

View File

@@ -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.';

View File

@@ -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,

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
web/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
web/icon-192-maskable.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
web/icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
web/icon-512-maskable.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
web/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

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

View File

@@ -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" }
]
}