diff --git a/mobile-app/lib/features/components/card_info.dart b/mobile-app/lib/features/components/card_info.dart index 498f3586..50e8b323 100644 --- a/mobile-app/lib/features/components/card_info.dart +++ b/mobile-app/lib/features/components/card_info.dart @@ -36,7 +36,7 @@ class CardInfo extends StatelessWidget { width: context.isTablet ? 660 : 251, child: Text(text, style: context.themeText.tag?.copyWith(color: textColor)), ), - if (icon != null) icon!, + ?icon, ], ), ), diff --git a/mobile-app/lib/features/main/screens/quests/king_of_the_shill_screen.dart b/mobile-app/lib/features/main/screens/quests/king_of_the_shill_screen.dart index f57ec327..49eeb246 100644 --- a/mobile-app/lib/features/main/screens/quests/king_of_the_shill_screen.dart +++ b/mobile-app/lib/features/main/screens/quests/king_of_the_shill_screen.dart @@ -21,6 +21,33 @@ class KingOfTheShillScreen extends ConsumerStatefulWidget { } class _KingOfTheShillScreenState extends ConsumerState { + final TaskmasterService _taskmasterService = TaskmasterService(); + int? _rank; + int? _totalImpressions; + + @override + void initState() { + super.initState(); + final asyncValue = ref.read(raiderSubmissionsProvider); + if (asyncValue.value is RaiderSubmissionsOk) { + _loadRaidStats((asyncValue.value as RaiderSubmissionsOk).activeRaid.id); + } + } + + Future _loadRaidStats(int raidId) async { + try { + final stats = await _taskmasterService.getRaidStats(raidId); + if (mounted) { + setState(() { + _rank = stats.rank; + _totalImpressions = stats.totalImpressions; + }); + } + } catch (e) { + debugPrint('Error loading raid stats: $e'); + } + } + void _showHowItWorksDialog() { showDialog( context: context, @@ -114,6 +141,13 @@ class _KingOfTheShillScreenState extends ConsumerState { Widget build(BuildContext context) { final raiderSubmissionsAsync = ref.watch(raiderSubmissionsProvider); + ref.listen(raiderSubmissionsProvider, (prev, next) { + final state = next.value; + if (state is RaiderSubmissionsOk && _rank == null) { + _loadRaidStats(state.activeRaid.id); + } + }); + return ScaffoldBase( appBar: WalletAppBar( title: 'King of The Shill', @@ -189,12 +223,12 @@ class _KingOfTheShillScreenState extends ConsumerState { child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2), ), ), - error: (_, _) => _buildStatsBox(0, 0), + error: (_, _) => _buildStatsBox(0, _totalImpressions, _rank), data: (state) { if (state is RaiderSubmissionsOk) { - return _buildStatsBox(state.submissions.length, 0); + return _buildStatsBox(state.submissions.length, _totalImpressions, _rank); } - return _buildStatsBox(0, 0); + return _buildStatsBox(0, _totalImpressions, _rank); }, ), const SizedBox(height: 24), @@ -237,7 +271,7 @@ class _KingOfTheShillScreenState extends ConsumerState { ); } - Widget _buildStatsBox(int submissions, int verifiedPosts) { + Widget _buildStatsBox(int submissions, int? totalImpressions, int? rank) { return Container( width: double.infinity, padding: const EdgeInsets.all(16), @@ -251,17 +285,39 @@ class _KingOfTheShillScreenState extends ConsumerState { ), child: Column( children: [ - _buildStatRow('Submissions', '$submissions', Colors.white), + _buildStatRow( + 'Submissions', + Text( + '$submissions', + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontFamily: 'Fira Code', + fontWeight: FontWeight.w400, + ), + ), + ), const SizedBox(height: 16), - _buildStatRow('Verified posts', verifiedPosts.toString().padLeft(2, '0'), Colors.white), + _buildStatRow('Total impressions', _buildLoadingOrValue(totalImpressions, Colors.white)), const SizedBox(height: 16), - _buildStatRow('Rank', '#-', context.themeColors.pink), + _buildStatRow('Rank', _buildLoadingOrValue(rank, context.themeColors.pink, isRank: true)), ], ), ); } - Widget _buildStatRow(String label, String value, Color valueColor) { + Widget _buildLoadingOrValue(int? value, Color color, {bool isRank = false}) { + if (value == null) { + return SizedBox(width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 2, color: color)); + } + final text = isRank ? (value > 0 ? '#$value' : '#-') : '$value'; + return Text( + text, + style: TextStyle(color: color, fontSize: 14, fontFamily: 'Fira Code', fontWeight: FontWeight.w400), + ); + } + + Widget _buildStatRow(String label, Widget valueWidget) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -274,10 +330,7 @@ class _KingOfTheShillScreenState extends ConsumerState { fontWeight: FontWeight.w400, ), ), - Text( - value, - style: TextStyle(color: valueColor, fontSize: 14, fontFamily: 'Fira Code', fontWeight: FontWeight.w400), - ), + valueWidget, ], ); } diff --git a/mobile-app/lib/features/main/screens/quests/referrals_quest_screen.dart b/mobile-app/lib/features/main/screens/quests/referrals_quest_screen.dart index 849421a0..3b49944a 100644 --- a/mobile-app/lib/features/main/screens/quests/referrals_quest_screen.dart +++ b/mobile-app/lib/features/main/screens/quests/referrals_quest_screen.dart @@ -23,7 +23,9 @@ class ReferralsQuestScreen extends ConsumerStatefulWidget { class _ReferralsQuestScreenState extends ConsumerState { final ReferralService _referralService = ReferralService(); + final TaskmasterService _taskmasterService = TaskmasterService(); String? _referralCode; + int? _rank; Future _loadReferralCode() async { try { @@ -31,11 +33,22 @@ class _ReferralsQuestScreenState extends ConsumerState { setState(() { _referralCode = myReferralCode; }); + _loadRank(myReferralCode); } catch (e) { - debugPrint('Error loading account data: $e'); + debugPrint('Error loading referral code: $e'); + } + } + + Future _loadRank(String referralCode) async { + try { + final rankData = await _taskmasterService.getReferralRank(referralCode); if (mounted) { - setState(() {}); + setState(() { + _rank = rankData.rank; + }); } + } catch (e) { + debugPrint('Error loading rank: $e'); } } @@ -145,7 +158,7 @@ class _ReferralsQuestScreenState extends ConsumerState { @override Widget build(BuildContext context) { final statsAsync = ref.watch(accountsStatsProvider); - final referralsCount = statsAsync.value?.referralCount ?? 0; + final referralsCount = statsAsync.value?.referralCount; return ScaffoldBase( appBar: WalletAppBar( @@ -248,9 +261,12 @@ class _ReferralsQuestScreenState extends ConsumerState { ), child: Column( children: [ - _buildStatRow('Referrals', '$referralsCount', Colors.white), + _buildStatRow('Referrals', _buildLoadingOrValue(referralsCount, Colors.white)), const SizedBox(height: 16), - _buildStatRow('Rank', '#-', context.themeColors.pink), + _buildStatRow( + 'Rank', + _buildLoadingOrValue(_rank, context.themeColors.pink, isRank: true), + ), ], ), ), @@ -349,7 +365,7 @@ class _ReferralsQuestScreenState extends ConsumerState { ); } - Widget _buildStatRow(String label, String value, Color valueColor) { + Widget _buildStatRow(String label, Widget valueWidget) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -362,12 +378,20 @@ class _ReferralsQuestScreenState extends ConsumerState { fontWeight: FontWeight.w400, ), ), - Text( - value, - textAlign: TextAlign.center, - style: TextStyle(color: valueColor, fontSize: 14, fontFamily: 'Fira Code', fontWeight: FontWeight.w400), - ), + valueWidget, ], ); } + + Widget _buildLoadingOrValue(int? value, Color color, {bool isRank = false}) { + if (value == null) { + return SizedBox(width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 2, color: color)); + } + final text = isRank ? (value > 0 ? '#$value' : '#-') : '$value'; + return Text( + text, + textAlign: TextAlign.center, + style: TextStyle(color: color, fontSize: 14, fontFamily: 'Fira Code', fontWeight: FontWeight.w400), + ); + } } diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index 09fb807d..e42d4218 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -2,7 +2,7 @@ name: resonance_network_wallet description: A Flutter wallet for the Quantus blockchain. publish_to: "none" -version: 1.1.5+66 +version: 1.1.5+67 environment: sdk: ">=3.8.0 <4.0.0" diff --git a/quantus_sdk/lib/quantus_sdk.dart b/quantus_sdk/lib/quantus_sdk.dart index b417ce73..8de791c5 100644 --- a/quantus_sdk/lib/quantus_sdk.dart +++ b/quantus_sdk/lib/quantus_sdk.dart @@ -33,6 +33,8 @@ export 'src/models/transaction_event.dart'; export 'src/models/transaction_state.dart'; export 'src/models/raider_submissions.dart'; export 'src/models/raid_quest.dart'; +export 'src/models/referral_rank.dart'; +export 'src/models/raid_stats.dart'; // note we have to hide some things here because they're exported by substrate service // should probably expise all of crypto.dart through substrateservice instead export 'src/rust/api/crypto.dart' hide crystalAlice, crystalCharlie, crystalBob; diff --git a/quantus_sdk/lib/src/models/raid_stats.dart b/quantus_sdk/lib/src/models/raid_stats.dart new file mode 100644 index 00000000..b3054542 --- /dev/null +++ b/quantus_sdk/lib/src/models/raid_stats.dart @@ -0,0 +1,35 @@ +import 'package:flutter/foundation.dart'; + +@immutable +class RaidStats { + final int raidId; + final int rank; + final int totalSubmissions; + final int totalImpressions; + final int totalReplies; + final int totalRetweets; + final int totalLikes; + + const RaidStats({ + required this.raidId, + required this.rank, + required this.totalSubmissions, + required this.totalImpressions, + required this.totalReplies, + required this.totalRetweets, + required this.totalLikes, + }); + + factory RaidStats.fromJson(Map json) { + final data = json['data'] as Map; + return RaidStats( + raidId: data['raid_id'] as int, + rank: data['rank'] as int, + totalSubmissions: data['total_submissions'] as int, + totalImpressions: data['total_impressions'] as int, + totalReplies: data['total_replies'] as int, + totalRetweets: data['total_retweets'] as int, + totalLikes: data['total_likes'] as int, + ); + } +} diff --git a/quantus_sdk/lib/src/models/referral_rank.dart b/quantus_sdk/lib/src/models/referral_rank.dart new file mode 100644 index 00000000..121a4e94 --- /dev/null +++ b/quantus_sdk/lib/src/models/referral_rank.dart @@ -0,0 +1,18 @@ +import 'package:flutter/foundation.dart'; + +@immutable +class ReferralRank { + final int rank; + final int referralsCount; + + const ReferralRank({required this.rank, required this.referralsCount}); + + factory ReferralRank.fromJson(Map json) { + final data = json['data'] as List; + if (data.isEmpty) { + return const ReferralRank(rank: 0, referralsCount: 0); + } + final first = data[0] as Map; + return ReferralRank(rank: first['rank'] as int, referralsCount: first['address']['referrals_count'] as int); + } +} diff --git a/quantus_sdk/lib/src/services/taskmaster_service.dart b/quantus_sdk/lib/src/services/taskmaster_service.dart index 6e2d200b..9fac8836 100644 --- a/quantus_sdk/lib/src/services/taskmaster_service.dart +++ b/quantus_sdk/lib/src/services/taskmaster_service.dart @@ -464,29 +464,30 @@ class TaskmasterService { } } - Future getAccountAssociations() async { - final activeAccount = await getMainAccount(); - print('getAccountAssociations ${activeAccount.accountId}'); - final accountAssociationsEndpoint = Uri.parse('${AppConstants.taskMasterEndpoint}/addresses/associations'); - + Future _authenticatedGet(Uri uri, T Function(Map) fromJson) async { try { - final http.Response response = await _authenticatedHttpClient.get(accountAssociationsEndpoint); + final response = await _authenticatedHttpClient.get(uri); if (response.statusCode != 200) { - throw Exception( - 'Account Associations http request failed with status: ${response.statusCode}. Body: ${response.body}', - ); + throw Exception('HTTP request failed with status: ${response.statusCode}. Body: ${response.body}'); } final json = jsonDecode(response.body) as Map; - return AccountAssociations.fromJson(json); + return fromJson(json); } catch (e, stackTrace) { - print('Error fetching miner stats: $e'); + print('Error fetching data from $uri: $e'); print(stackTrace); rethrow; } } + Future getAccountAssociations() async { + final activeAccount = await getMainAccount(); + print('getAccountAssociations ${activeAccount.accountId}'); + final accountAssociationsEndpoint = Uri.parse('${AppConstants.taskMasterEndpoint}/addresses/associations'); + return _authenticatedGet(accountAssociationsEndpoint, AccountAssociations.fromJson); + } + Future submitAddress() async { await ensureIsLoggedIn(); } @@ -561,24 +562,23 @@ class TaskmasterService { Future getOptInPosition() async { final Uri uri = Uri.parse('${AppConstants.taskMasterEndpoint}/addresses/my-position'); - - try { - final http.Response response = await _authenticatedHttpClient.get(uri); - - if (response.statusCode != 200) { - throw Exception('HTTP request failed with status: ${response.statusCode}. Body: ${response.body}'); - } - - final json = jsonDecode(response.body) as Map; - return OptedInPosition.fromJson(json); - } catch (e, stackTrace) { - print('Error fetching address stats: $e'); - print(stackTrace); - rethrow; - } + return _authenticatedGet(uri, OptedInPosition.fromJson); } Future logout() async { _clearToken(); } + + Future getReferralRank(String referralCode) async { + final Uri uri = Uri.parse('${AppConstants.taskMasterEndpoint}/addresses/leaderboard?referral_code=$referralCode'); + return _authenticatedGet(uri, ReferralRank.fromJson); + } + + Future getRaidStats(int raidId) async { + final activeAccount = await getMainAccount(); + final Uri uri = Uri.parse( + '${AppConstants.taskMasterEndpoint}/raid-quests/raiders/${activeAccount.accountId}/leaderboards/$raidId', + ); + return _authenticatedGet(uri, RaidStats.fromJson); + } }