diff --git a/miner-app/lib/features/setup/rewards_address_setup_screen.dart b/miner-app/lib/features/setup/rewards_address_setup_screen.dart index 1bbcc6ad..5c60d10f 100644 --- a/miner-app/lib/features/setup/rewards_address_setup_screen.dart +++ b/miner-app/lib/features/setup/rewards_address_setup_screen.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:flash/flash_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -82,9 +83,7 @@ class _RewardsAddressSetupScreenState extends State { print('Rewards address saved: $address'); if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Rewards address saved successfully!'))); + context.showSuccessBar(content: Text('Rewards address saved successfully!')); // Navigate to the main mining screen context.go('/miner_dashboard'); } @@ -131,14 +130,6 @@ class _RewardsAddressSetupScreenState extends State { Container( width: 40, // spacer for alignment ), - // const Text( - // 'Scan QR Code', - // style: TextStyle( - // color: Colors.white, - // fontSize: 20, - // fontWeight: FontWeight.bold, - // ), - // ), Positioned( right: 0, top: 0, diff --git a/miner-app/lib/src/services/chain_rpc_client.dart b/miner-app/lib/src/services/chain_rpc_client.dart index d5ee415d..54c23655 100644 --- a/miner-app/lib/src/services/chain_rpc_client.dart +++ b/miner-app/lib/src/services/chain_rpc_client.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; + import 'package:http/http.dart' as http; class ChainInfo { @@ -196,7 +197,8 @@ class ChainRpcClient { /// Execute a JSON-RPC call Future _rpcCall(String method, [List? params]) async { - final request = {'jsonrpc': '2.0', 'id': _requestId++, 'method': method, if (params != null) 'params': params}; + final request = {'jsonrpc': '2.0', 'id': _requestId++, 'method': method}; + if (params != null) request['params'] = params; // Only print RPC calls when debugging connection issues // print('DEBUG: Making RPC call: $method with request: ${json.encode(request)}'); diff --git a/mobile-app/assets/navbar/Lightning.svg b/mobile-app/assets/navbar/Lightning.svg new file mode 100644 index 00000000..174b263c --- /dev/null +++ b/mobile-app/assets/navbar/Lightning.svg @@ -0,0 +1,3 @@ + + + diff --git a/mobile-app/assets/navbar/lightning_icon_off.svg b/mobile-app/assets/navbar/lightning_icon_off.svg new file mode 100644 index 00000000..8d9bf394 --- /dev/null +++ b/mobile-app/assets/navbar/lightning_icon_off.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/mobile-app/assets/navbar/lightning_icon_on.svg b/mobile-app/assets/navbar/lightning_icon_on.svg new file mode 100644 index 00000000..6eb149cf --- /dev/null +++ b/mobile-app/assets/navbar/lightning_icon_on.svg @@ -0,0 +1,4 @@ + + + + diff --git a/mobile-app/assets/quests/quests_top_logo.png b/mobile-app/assets/quests/quests_top_logo.png new file mode 100644 index 00000000..f65ab04a Binary files /dev/null and b/mobile-app/assets/quests/quests_top_logo.png differ diff --git a/mobile-app/lib/features/components/inner_shadow_container.dart b/mobile-app/lib/features/components/inner_shadow_container.dart new file mode 100644 index 00000000..779f3013 --- /dev/null +++ b/mobile-app/lib/features/components/inner_shadow_container.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +class InnerShadowContainer extends StatelessWidget { + final Widget child; + final List shadows; + final BorderRadius borderRadius; + + const InnerShadowContainer({ + super.key, + required this.child, + required this.shadows, + this.borderRadius = BorderRadius.zero, + }); + + factory InnerShadowContainer.standard({required Widget child}) { + return InnerShadowContainer( + shadows: const [ + BoxShadow(color: Color(0x19FFFFFF), offset: Offset(-2, -2), blurRadius: 12, spreadRadius: 2), + BoxShadow(color: Color(0x19FFFFFF), offset: Offset(2, 2), blurRadius: 12, spreadRadius: 2), + ], + borderRadius: BorderRadius.circular(4), + child: child, + ); + } + + @override + Widget build(BuildContext context) { + return CustomPaint(foregroundPainter: _InnerShadowPainter(shadows, borderRadius), child: child); + } +} + +class _InnerShadowPainter extends CustomPainter { + final List shadows; + final BorderRadius borderRadius; + + _InnerShadowPainter(this.shadows, this.borderRadius); + + @override + void paint(Canvas canvas, Size size) { + final rect = Rect.fromLTWH(0, 0, size.width, size.height); + final rrect = borderRadius.toRRect(rect); + + canvas.clipRRect(rrect); + + for (final shadow in shadows) { + final paint = Paint() + ..color = shadow.color + ..maskFilter = MaskFilter.blur(BlurStyle.normal, shadow.blurSigma); + + final outerRect = rect.inflate(shadow.blurRadius + shadow.spreadRadius + 20); + + final path = Path() + ..fillType = PathFillType.evenOdd + ..addRect(outerRect) + ..addRRect(rrect.shift(-shadow.offset)); + + canvas.drawPath(path, paint); + } + } + + @override + bool shouldRepaint(covariant _InnerShadowPainter oldDelegate) { + return oldDelegate.shadows != shadows || oldDelegate.borderRadius != borderRadius; + } +} diff --git a/mobile-app/lib/features/components/scaffold_base.dart b/mobile-app/lib/features/components/scaffold_base.dart index 67b1b7e5..95275ddc 100644 --- a/mobile-app/lib/features/components/scaffold_base.dart +++ b/mobile-app/lib/features/components/scaffold_base.dart @@ -19,6 +19,7 @@ class ScaffoldBase extends StatelessWidget { final double dim; final bool extendBodyBehindAppBar; final bool extendBodyBehindNavBar; + final Color? backgroundColor; // Default constructor - static content const ScaffoldBase({ @@ -30,6 +31,7 @@ class ScaffoldBase extends StatelessWidget { this.decorations, this.dim = 0.25, this.padding = const EdgeInsets.symmetric(horizontal: 24.0), + this.backgroundColor, required Widget this.child, }) : slivers = null, scrollController = null, @@ -46,6 +48,7 @@ class ScaffoldBase extends StatelessWidget { this.decorations, this.dim = 0.25, this.padding = const EdgeInsets.symmetric(horizontal: 24.0), + this.backgroundColor, required ScrollController this.scrollController, this.scrollPhysics = const AlwaysScrollableScrollPhysics(), required Widget this.child, @@ -62,6 +65,7 @@ class ScaffoldBase extends StatelessWidget { this.decorations, this.dim = 0.25, this.padding = const EdgeInsets.symmetric(horizontal: 24.0), + this.backgroundColor, required ScrollController this.scrollController, this.scrollPhysics = const AlwaysScrollableScrollPhysics(), required RefreshCallback this.onRefresh, @@ -73,7 +77,7 @@ class ScaffoldBase extends StatelessWidget { return Scaffold( extendBodyBehindAppBar: extendBodyBehindAppBar, appBar: appBar, - backgroundColor: context.themeColors.background, + backgroundColor: backgroundColor ?? context.themeColors.background, body: Stack( children: [ if (decorations != null) ...decorations!, diff --git a/mobile-app/lib/features/main/screens/navbar.dart b/mobile-app/lib/features/main/screens/navbar.dart index 9f4412cc..1dc0ce2e 100644 --- a/mobile-app/lib/features/main/screens/navbar.dart +++ b/mobile-app/lib/features/main/screens/navbar.dart @@ -107,7 +107,7 @@ class _NavbarState extends ConsumerState { NavItem('assets/navbar/history_icon_off.svg', 'assets/navbar/history_icon_on.svg', 'History'), NavItem('assets/navbar/floating_button.svg', 'assets/navbar/floating_button.svg', 'Send'), NavItem('assets/navbar/settings_icon_off.svg', 'assets/navbar/settings_icon_on.svg', 'Settings'), - NavItem('assets/navbar/qcat_navbar_icon.png', 'assets/navbar/qcat_navbar_icon.png', 'Quests'), + NavItem('assets/navbar/lightning_icon_off.svg', 'assets/navbar/lightning_icon_on.svg', 'Quests'), ]; @override @@ -139,12 +139,8 @@ class _NavbarState extends ConsumerState { // Handle quest screen visibility if (newIndex == 3) { - // quests screen index - make video visible + // quests screen index (_questsScreenKey.currentState as dynamic)?.refreshStatsData(); - (_questsScreenKey.currentState as dynamic)?.setVideoVisibility(true); - } else if (_selectedIndex == 3) { - // leaving quests screen - hide video - (_questsScreenKey.currentState as dynamic)?.setVideoVisibility(false); } setState(() { @@ -215,15 +211,13 @@ class _NavbarState extends ConsumerState { } Widget _buildBody() { - bool playPromoVideo = _selectedIndex == 3; - return IndexedStack( index: _selectedIndex, children: [ const WalletMain(), const TransactionsScreen(), const SettingsScreen(), - QuestsScreen(key: _questsScreenKey, playPromoVideo: playPromoVideo), + QuestsScreen(key: _questsScreenKey), ], ); } diff --git a/mobile-app/lib/features/main/screens/quests/complete_setup_action_sheet.dart b/mobile-app/lib/features/main/screens/quests/complete_setup_action_sheet.dart new file mode 100644 index 00000000..ec0486cf --- /dev/null +++ b/mobile-app/lib/features/main/screens/quests/complete_setup_action_sheet.dart @@ -0,0 +1,522 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; +import 'package:resonance_network_wallet/providers/account_associations_providers.dart'; +import 'package:resonance_network_wallet/providers/opt_in_position_providers.dart'; +import 'package:resonance_network_wallet/shared/extensions/snackbar_extensions.dart'; + +void showCompleteSetupActionSheet(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const CompleteSetupActionSheet(), + ); +} + +class CompleteSetupActionSheet extends ConsumerStatefulWidget { + const CompleteSetupActionSheet({super.key}); + + @override + ConsumerState createState() => _CompleteSetupActionSheetState(); +} + +class _CompleteSetupActionSheetState extends ConsumerState { + final _taskmasterService = TaskmasterService(); + final _ethAddressController = TextEditingController(); + final _xHandleController = TextEditingController(); + final _bioController = TextEditingController(); + + bool _isSubmitting = false; + bool _hasInitialized = false; + String? _originalEthAddress; + String? _originalXUsername; + + bool get _isBioValid => _bioController.text.trim().toLowerCase().contains('@quantusnetwork'); + + @override + void dispose() { + _ethAddressController.dispose(); + _xHandleController.dispose(); + _bioController.dispose(); + super.dispose(); + } + + void _initializeData(AccountAssociations associations) { + if (_hasInitialized) return; + + _originalEthAddress = associations.ethAddress; + _originalXUsername = associations.xUsername; + + if (associations.ethAddress != null) { + _ethAddressController.text = associations.ethAddress!; + } + if (associations.xUsername != null) { + _xHandleController.text = associations.xUsername!; + } + + _hasInitialized = true; + } + + Future _handleLinkAccounts() async { + final ethAddress = _ethAddressController.text.trim(); + final xHandle = _xHandleController.text.trim(); + + setState(() => _isSubmitting = true); + + try { + final optInStatus = ref.read(optInPositionProvider); + final isAlreadyOptedIn = optInStatus.maybeWhen(data: (position) => position.position > 0, orElse: () => false); + + if (!isAlreadyOptedIn) { + await _taskmasterService.optInRewardProgram(); + } + + if (ethAddress.isNotEmpty && ethAddress != _originalEthAddress) { + await _taskmasterService.associateEthAddress(ethAddress); + } + + final normalizedHandle = xHandle.startsWith('@') ? xHandle.substring(1) : xHandle; + if (normalizedHandle.isNotEmpty && normalizedHandle != _originalXUsername) { + await _taskmasterService.associateXHandle(normalizedHandle); + } + + ref.invalidate(accountAssociationsProvider); + ref.invalidate(optInPositionProvider); + + if (mounted) { + context.showSuccessSnackbar(title: 'Success', message: 'Accounts linked successfully!'); + Navigator.pop(context); + } + } catch (e) { + if (mounted) { + context.showErrorSnackbar(title: 'Error', message: e.toString()); + } + } finally { + if (mounted) { + setState(() => _isSubmitting = false); + } + } + } + + @override + Widget build(BuildContext context) { + final associationsAsync = ref.watch(accountAssociationsProvider); + + associationsAsync.whenData((associations) { + _initializeData(associations); + }); + + return Container( + constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.9), + padding: EdgeInsets.only(left: 26, right: 26, top: 40, bottom: 40 + MediaQuery.of(context).viewInsets.bottom), + decoration: ShapeDecoration( + color: context.themeColors.background2, + shape: const RoundedRectangleBorder( + side: BorderSide(width: 1, strokeAlign: BorderSide.strokeAlignOutside, color: Color(0x66F4F6F9)), + borderRadius: BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20)), + ), + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(), + const SizedBox(height: 64), + _buildForm(), + const SizedBox(height: 32), + _buildLinkButton(), + ], + ), + ), + ); + } + + Widget _buildHeader() { + return SizedBox( + width: double.infinity, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('ACCOUNT SETUP', style: context.themeText.paragraph), + const SizedBox(height: 6), + SizedBox( + width: 264, + child: Text( + 'Link your accounts once to participate in referrals and raids.', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.50), + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w400, + ), + ), + ), + ], + ), + ), + const SizedBox(width: 18), + // Close button + GestureDetector( + onTap: () => Navigator.pop(context), + child: Icon(Icons.close, color: context.themeColors.textPrimary, size: 24), + ), + ], + ), + ); + } + + void _showUnlinkConfirmation({required String title, required Future Function() onConfirm}) { + showModalBottomSheet( + context: context, + isDismissible: true, + backgroundColor: context.themeColors.background2, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))), + builder: (sheetContext) => + _UnlinkConfirmationSheet(title: title, onConfirm: onConfirm, dangerColor: context.themeColors.buttonDanger), + ); + } + + Future _handleUnlinkEth() async { + await _taskmasterService.dissociateEthAddress(); + _ethAddressController.clear(); + _originalEthAddress = null; + ref.invalidate(accountAssociationsProvider); + if (mounted) { + context.showSuccessSnackbar(title: 'Success', message: 'ETH address unlinked'); + setState(() {}); + } + } + + Future _handleUnlinkX() async { + await _taskmasterService.dissociateXAccount(); + _xHandleController.clear(); + _originalXUsername = null; + ref.invalidate(accountAssociationsProvider); + if (mounted) { + context.showSuccessSnackbar(title: 'Success', message: 'X account unlinked'); + setState(() {}); + } + } + + Widget _buildForm() { + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildInputSection( + title: 'Connect your wallet', + controller: _ethAddressController, + hintText: 'Enter ETH Address', + canUnlink: _originalEthAddress != null, + onUnlink: () => _showUnlinkConfirmation(title: 'ETH Address', onConfirm: _handleUnlinkEth), + ), + const SizedBox(height: 32), + _buildInputSection( + title: 'Connect your X handle', + controller: _xHandleController, + hintText: 'Enter username', + canUnlink: _originalXUsername != null, + onUnlink: () => _showUnlinkConfirmation(title: 'X Account', onConfirm: _handleUnlinkX), + ), + const SizedBox(height: 32), + _buildBioSection(), + ], + ); + } + + Widget _buildInputSection({ + required String title, + required TextEditingController controller, + required String hintText, + bool canUnlink = false, + VoidCallback? onUnlink, + }) { + return SizedBox( + width: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 323, + child: Text(title, style: context.themeText.smallTitle?.copyWith(color: context.themeColors.textPrimary)), + ), + const SizedBox(height: 15), + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: ShapeDecoration( + color: const Color(0x0AF4F6F9), + shape: RoundedRectangleBorder( + side: const BorderSide(width: 1, color: Color(0x19F4F6F9)), + borderRadius: BorderRadius.circular(4), + ), + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: controller, + autocorrect: false, + textCapitalization: TextCapitalization.none, + style: const TextStyle( + color: Color(0xFFF4F6F9), + fontSize: 14, + fontFamily: 'Fira Code', + fontWeight: FontWeight.w400, + ), + decoration: InputDecoration.collapsed( + hintText: '|$hintText', + hintStyle: const TextStyle( + color: Color(0x7FF4F6F9), + fontSize: 14, + fontFamily: 'Fira Code', + fontWeight: FontWeight.w400, + ), + ), + ), + ), + if (canUnlink) + GestureDetector( + onTap: onUnlink, + child: Icon(Icons.close, size: 18, color: context.themeColors.buttonDanger), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildBioSection() { + return SizedBox( + width: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 323, + child: Text( + 'Verify your X account', + style: context.themeText.smallTitle?.copyWith(color: context.themeColors.textPrimary), + ), + ), + const SizedBox(height: 4), + const SizedBox( + width: 323, + child: Text( + 'To confirm this account belongs to you, please update your X bio to include @QuantusNetwork', + style: TextStyle( + color: Color(0x7FF4F6F9), + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w400, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 15), + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: ShapeDecoration( + color: const Color(0x0AF4F6F9), + shape: RoundedRectangleBorder( + side: const BorderSide(width: 1, color: Color(0x19F4F6F9)), + borderRadius: BorderRadius.circular(4), + ), + ), + child: TextField( + controller: _bioController, + onChanged: (_) => setState(() {}), + autocorrect: false, + textCapitalization: TextCapitalization.none, + style: const TextStyle( + color: Color(0xFFF4F6F9), + fontSize: 14, + fontFamily: 'Fira Code', + fontWeight: FontWeight.w400, + ), + decoration: const InputDecoration.collapsed( + hintText: '|Updated Bio', + hintStyle: TextStyle( + color: Color(0x7FF4F6F9), + fontSize: 14, + fontFamily: 'Fira Code', + fontWeight: FontWeight.w400, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildLinkButton() { + final bool isEnabled = _isBioValid && !_isSubmitting; + + return GestureDetector( + onTap: isEnabled ? _handleLinkAccounts : null, + child: Opacity( + opacity: isEnabled ? 1.0 : 0.40, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: ShapeDecoration( + gradient: const LinearGradient( + begin: Alignment(0.02, 0.50), + end: Alignment(1.00, 0.50), + colors: [Color(0x7F0000FF), Color(0x19ED4CCE), Color(0x7FFFE91F)], + ), + shape: RoundedRectangleBorder( + side: const BorderSide(width: 1, color: Color(0x33F4F6F9)), + borderRadius: BorderRadius.circular(42), + ), + ), + child: Center( + child: _isSubmitting + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(color: context.themeColors.textPrimary, strokeWidth: 2), + ) + : Text( + 'Link Accounts', + style: context.themeText.smallTitle?.copyWith(color: context.themeColors.textPrimary), + ), + ), + ), + ), + ); + } +} + +class _UnlinkConfirmationSheet extends StatefulWidget { + final String title; + final Future Function() onConfirm; + final Color dangerColor; + + const _UnlinkConfirmationSheet({required this.title, required this.onConfirm, required this.dangerColor}); + + @override + State<_UnlinkConfirmationSheet> createState() => _UnlinkConfirmationSheetState(); +} + +class _UnlinkConfirmationSheetState extends State<_UnlinkConfirmationSheet> { + bool _isUnlinking = false; + + Future _handleUnlink() async { + setState(() => _isUnlinking = true); + try { + await widget.onConfirm(); + if (mounted) Navigator.pop(context); + } catch (e) { + if (mounted) { + setState(() => _isUnlinking = false); + context.showErrorSnackbar(title: 'Error', message: 'Error: $e'); + } + } + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Unlink ${widget.title}?', style: context.themeText.smallTitle?.copyWith(color: Colors.white)), + const SizedBox(height: 8), + Text( + 'Are you sure you want to unlink your ${widget.title}?', + style: TextStyle(color: Colors.white.withValues(alpha: 0.5), fontSize: 14, fontFamily: 'Inter'), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + GestureDetector( + onTap: _isUnlinking ? null : _handleUnlink, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: ShapeDecoration( + color: widget.dangerColor, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(42)), + ), + alignment: Alignment.center, + child: _isUnlinking + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2), + ) + : const Text( + 'Unlink', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontFamily: 'Fira Code', + fontWeight: FontWeight.w500, + ), + ), + ), + ), + const SizedBox(height: 12), + GestureDetector( + onTap: _isUnlinking ? null : () => Navigator.pop(context), + child: Opacity( + opacity: _isUnlinking ? 0.5 : 1.0, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: ShapeDecoration( + color: const Color(0x33F4F6F9), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(42)), + ), + alignment: Alignment.center, + child: const Text( + 'Cancel', + style: TextStyle( + color: Color(0xFFF4F6F9), + fontSize: 16, + fontFamily: 'Fira Code', + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} 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 1eccde51..f57ec327 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 @@ -1,20 +1,17 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; -import 'package:resonance_network_wallet/features/components/button.dart'; -import 'package:resonance_network_wallet/features/components/link_text.dart'; -import 'package:resonance_network_wallet/features/components/raid_submission_action_sheet.dart'; +import 'package:resonance_network_wallet/features/components/inner_shadow_container.dart'; import 'package:resonance_network_wallet/features/components/scaffold_base.dart'; -import 'package:resonance_network_wallet/features/components/sphere.dart'; import 'package:resonance_network_wallet/features/components/wallet_app_bar.dart'; -import 'package:resonance_network_wallet/features/main/screens/quests/account_associations_status.dart'; -import 'package:resonance_network_wallet/features/main/screens/quests/optin_position_status.dart'; -import 'package:resonance_network_wallet/features/main/screens/quests/quest_title.dart'; +import 'package:resonance_network_wallet/features/main/screens/quests/quest_constants.dart'; +import 'package:resonance_network_wallet/features/main/screens/quests/raid_submission_action_sheet.dart'; import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; import 'package:resonance_network_wallet/providers/raider_quest_providers.dart'; -import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; -import 'package:resonance_network_wallet/shared/extensions/snackbar_extensions.dart'; +import 'package:url_launcher/url_launcher.dart'; class KingOfTheShillScreen extends ConsumerStatefulWidget { const KingOfTheShillScreen({super.key}); @@ -23,201 +20,368 @@ class KingOfTheShillScreen extends ConsumerStatefulWidget { ConsumerState createState() => _KingOfTheShillScreenState(); } -class _KingOfTheShillScreenState extends ConsumerState with WidgetsBindingObserver { - final _taskmasterService = TaskmasterService(); - final ScrollController _scrollController = ScrollController(); - - @override - void initState() { - super.initState(); +class _KingOfTheShillScreenState extends ConsumerState { + void _showHowItWorksDialog() { + showDialog( + context: context, + barrierColor: Colors.black.withValues(alpha: 0.7), + builder: (context) => GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Scaffold( + backgroundColor: Colors.transparent, + body: Center( + child: GestureDetector( + onTap: () {}, + child: Container( + width: 280, + padding: const EdgeInsets.all(24), + decoration: ShapeDecoration( + color: context.themeColors.background2, + shape: RoundedRectangleBorder( + side: const BorderSide(width: 1, color: Color(0x66F4F6F9)), + borderRadius: BorderRadius.circular(20), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('HOW IT WORKS', style: context.themeText.paragraph), + GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: const Icon(Icons.close, color: Colors.white, size: 24), + ), + ], + ), + const SizedBox(height: 32), + _buildStep('Step 1', 'Find an active raid on X (Twitter)'), + const SizedBox(height: 16), + _buildStep('Step 2', 'Reply to the raid post with your shill'), + const SizedBox(height: 16), + _buildStep('Step 3', 'Submit your reply URL here to get verified and earn rewards'), + ], + ), + ), + ), + ), + ), + ), + ); } - @override - void dispose() { - super.dispose(); + Widget _buildStep(String title, String description) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontFamily: 'Fira Code', + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 2), + Text( + description, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.50), + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w400, + ), + ), + ], + ); } void refreshRaiderSubmissions() { ref.invalidate(raiderSubmissionsProvider); } - String? extractXStatusId(String url) { + Future _openUrl(String url) async { final uri = Uri.tryParse(url); - if (uri == null) return null; - - // Expected path: /{username}/status/{id} - final segments = uri.pathSegments; - - if (segments.length >= 3 && segments[1] == 'status') { - final id = segments[2]; - return RegExp(r'^\d+$').hasMatch(id) ? id : null; - } - - return null; - } - - Future _handleRemoveSubmission(String id) async { - try { - await _taskmasterService.removeRaidSubmission(id); - if (mounted) { - context.showSuccessSnackbar(title: 'Success removed!', message: 'Success removing raid submission!'); - } - ref.invalidate(raiderSubmissionsProvider); - } catch (e) { - print('Failed removing raid submission: $e'); - - if (mounted) { - context.showErrorSnackbar(title: 'Failed removing!', message: e.toString()); - } + if (uri != null && await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); } } @override Widget build(BuildContext context) { final raiderSubmissionsAsync = ref.watch(raiderSubmissionsProvider); - final effectiveSpacing = context.isSmallHeight ? 24.0 : 36.0; - return ScaffoldBase.refreshable( - appBar: WalletAppBar(title: 'King of The Shill'), - onRefresh: () async { - refreshRaiderSubmissions(); - }, - scrollController: _scrollController, - decorations: [ - const Positioned(top: 180, right: -34, child: Sphere(variant: 2, size: 194)), - const Positioned(left: -60, bottom: 0, child: Sphere(variant: 7, size: 240.68)), - ], - slivers: [ - SliverToBoxAdapter( - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.only(top: 11), - child: Column( - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 17, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 96), - const Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [OptinPositionStatus()], + return ScaffoldBase( + appBar: WalletAppBar( + title: 'King of The Shill', + actions: [ + IconButton( + icon: const Icon(Icons.info_outline, color: Colors.white), + onPressed: _showHowItWorksDialog, + ), + ], + ), + child: SafeArea( + top: false, + child: Column( + children: [ + Expanded( + child: Container( + width: double.infinity, + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: context.themeColors.background2, + shape: RoundedRectangleBorder( + side: const BorderSide(width: 1, color: Color(0x7F6734BA)), + borderRadius: BorderRadius.circular(4), + ), + ), + child: InnerShadowContainer.standard( + child: Stack( + children: [ + Positioned.fill( + child: ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 52, sigmaY: 52), + child: Container( + decoration: const BoxDecoration( + gradient: RadialGradient( + center: Alignment(0.77, -0.56), + radius: 2.8, + colors: questKingOfTheShillGradient, + stops: [0.45, 0.54, 0.57], ), - SizedBox(height: effectiveSpacing), - const AccountAssociationsStatus(), - SizedBox(height: effectiveSpacing), - ...raiderSubmissionsAsync.when( - loading: () => [ - Center(child: CircularProgressIndicator(color: context.themeColors.circularLoader)), - ], - error: (error, stackTrace) => [ + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Column( + children: [ + Text('KING OF THE SHILL', style: context.themeText.paragraph), + const SizedBox(height: 8), Text( - 'Error fetching raider submissions.', - style: context.themeText.detail?.copyWith(color: context.themeColors.textError), - ), - const SizedBox(height: 12), - Button( - variant: ButtonVariant.neutral, - label: 'Try again', - onPressed: refreshRaiderSubmissions, + 'Join social raids. Get rewarded for\nverified posts.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.50), + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w400, + ), ), ], - data: (state) { - switch (state) { - case RaiderSubmissionsOk(): - return [ - LinkText( - label: 'Learn more', - url: AppConstants.raidQuestsPageUrl, - textStyle: context.themeText.smallParagraph, - ), - SizedBox(height: effectiveSpacing), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Raid Submissions', style: context.themeText.smallTitle), - const SizedBox(width: 12), - InkWell( - child: Container( - decoration: ShapeDecoration( - color: context.themeColors.buttonNeutral, - shape: RoundedRectangleBorder( - borderRadius: BorderRadiusGeometry.circular(8), - ), - ), - child: const Icon(Icons.add, color: Colors.black), - ), - onTap: () { - showRaidSubmissionActionSheet(context); - }, - ), - ], - ), - const SizedBox(height: 8), - if (state.submissions.isNotEmpty) - Column( - spacing: 4, - children: state.submissions.asMap().entries.map((entry) { - final index = entry.key + 1; - final value = entry.value; - final label = extractXStatusId(value) ?? 'Unknown'; - - return Row( - children: [ - Text('$index. '), - LinkText( - label: label, - url: value, - textStyle: context.themeText.smallParagraph, - ), - InkWell( - child: Icon(Icons.delete, color: context.themeColors.buttonDanger), - onTap: () { - _handleRemoveSubmission(label); - }, - ), - ], - ); - }).toList(), - ) - else - Text( - "You haven't submitted anything yet", - style: context.themeText.smallParagraph, - ), - ]; - - case NoActiveRaid(): - return [ - LinkText( - label: 'Learn more', - url: AppConstants.raidQuestsPageUrl, - textStyle: context.themeText.smallParagraph, - ), - ]; - - case NoTwitterLinked(): - return [Text('Please link your X account', style: context.themeText.smallTitle)]; - } - }, ), - ], - ), + ), + const SizedBox(height: 40), + raiderSubmissionsAsync.when( + loading: () => const Center( + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2), + ), + ), + error: (_, _) => _buildStatsBox(0, 0), + data: (state) { + if (state is RaiderSubmissionsOk) { + return _buildStatsBox(state.submissions.length, 0); + } + return _buildStatsBox(0, 0); + }, + ), + const SizedBox(height: 24), + _buildPastSubmissionsSection(raiderSubmissionsAsync), + const Spacer(), + _buildSubmitSection(), + ], ), - ], + ), + ], + ), + ), + ), + ), + const SizedBox(height: 32), + GestureDetector( + onTap: () => showRaidSubmissionActionSheet(context), + child: InnerShadowContainer.standard( + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: ShapeDecoration( + color: const Color(0x33F4F6F9), + shape: RoundedRectangleBorder( + side: const BorderSide(width: 1, color: Color(0x33F4F6F9)), + borderRadius: BorderRadius.circular(42), ), - SizedBox(height: context.isSmallHeight ? 18 : 40), - ], + ), + alignment: Alignment.center, + child: Text( + 'Add Raid Submission', + style: context.themeText.smallTitle?.copyWith(color: context.themeColors.textPrimary), + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildStatsBox(int submissions, int verifiedPosts) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: ShapeDecoration( + color: context.themeColors.background2.useOpacity(0.4), + shape: RoundedRectangleBorder( + side: const BorderSide(width: 1, color: Color(0x33F4F6F9)), + borderRadius: BorderRadius.circular(8), + ), + shadows: const [BoxShadow(color: Color(0x3F000000), blurRadius: 4, offset: Offset(4, 4))], + ), + child: Column( + children: [ + _buildStatRow('Submissions', '$submissions', Colors.white), + const SizedBox(height: 16), + _buildStatRow('Verified posts', verifiedPosts.toString().padLeft(2, '0'), Colors.white), + const SizedBox(height: 16), + _buildStatRow('Rank', '#-', context.themeColors.pink), + ], + ), + ); + } + + Widget _buildStatRow(String label, String value, Color valueColor) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: const TextStyle( + color: Color(0xFFF4F6F9), + fontSize: 14, + fontFamily: 'Fira Code', + fontWeight: FontWeight.w400, + ), + ), + Text( + value, + style: TextStyle(color: valueColor, fontSize: 14, fontFamily: 'Fira Code', fontWeight: FontWeight.w400), + ), + ], + ); + } + + Widget _buildPastSubmissionsSection(AsyncValue raiderSubmissionsAsync) { + return Column( + children: [ + const Text( + 'Past Submissions', + style: TextStyle(color: Colors.white, fontSize: 14, fontFamily: 'Fira Code', fontWeight: FontWeight.w500), + ), + const SizedBox(height: 12), + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: const BorderSide(width: 1, color: Color(0x33F4F6F9)), + borderRadius: BorderRadius.circular(8), + ), + ), + child: raiderSubmissionsAsync.when( + loading: () => const SizedBox( + height: 80, + child: Center( + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2), ), ), - const QuestTitle(), - ], + ), + error: (_, _) => Text( + 'Failed to load submissions', + style: TextStyle(color: Colors.white.withValues(alpha: 0.5), fontSize: 12, fontFamily: 'Inter'), + ), + data: (state) { + if (state is RaiderSubmissionsOk && state.submissions.isNotEmpty) { + return Column(children: state.submissions.take(4).map((url) => _buildSubmissionRow(url)).toList()); + } + return Text( + 'No submissions yet', + style: TextStyle(color: Colors.white.withValues(alpha: 0.5), fontSize: 12, fontFamily: 'Inter'), + ); + }, + ), + ), + ], + ); + } + + Widget _buildSubmissionRow(String url) { + final displayUrl = url.length > 30 ? '${url.substring(0, 30)}...' : url; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => _openUrl(url), + child: Text( + displayUrl, + style: const TextStyle( + color: Color(0xFFF4F6F9), + fontSize: 12, + fontFamily: 'Inter', + fontWeight: FontWeight.w400, + decoration: TextDecoration.underline, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + ); + } + + Widget _buildSubmitSection() { + return Column( + children: [ + const Text( + 'Submit Your Reply', + style: TextStyle(color: Colors.white, fontSize: 14, fontFamily: 'Fira Code', fontWeight: FontWeight.w500), + ), + const SizedBox(height: 12), + GestureDetector( + onTap: () => showRaidSubmissionActionSheet(context), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: ShapeDecoration( + color: Colors.white.withValues(alpha: 0.24), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Text( + '|https://x.com/....', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.5), + fontSize: 12, + fontFamily: 'Fira Code', + fontWeight: FontWeight.w400, + ), + ), ), ), ], diff --git a/mobile-app/lib/features/main/screens/quests/quest_card.dart b/mobile-app/lib/features/main/screens/quests/quest_card.dart new file mode 100644 index 00000000..a47db688 --- /dev/null +++ b/mobile-app/lib/features/main/screens/quests/quest_card.dart @@ -0,0 +1,165 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:resonance_network_wallet/features/main/screens/quests/quest_constants.dart'; +import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; + +class QuestCard extends StatelessWidget { + final String title; + final String description; + final String actionLabel; + final VoidCallback? onTap; + final VoidCallback? onDisabledTap; + final List gradientColors; + final List? gradientStops; + final AlignmentGeometry gradientCenter; + final double gradientRadius; + final Color borderColor; + final bool isDisabled; + + final double bgRectLeft; + final double bgRectTop; + final double bgRectWidth; + final double bgRectHeight; + + const QuestCard({ + super.key, + required this.title, + required this.description, + required this.actionLabel, + required this.onTap, + this.onDisabledTap, + required this.gradientColors, + this.gradientStops, + required this.gradientCenter, + required this.gradientRadius, + required this.borderColor, + this.isDisabled = false, + this.bgRectLeft = -127, + this.bgRectTop = -198, + this.bgRectWidth = 531, + this.bgRectHeight = 531, + }); + + factory QuestCard.referFriends({required VoidCallback? onTap, VoidCallback? onDisabledTap, bool isDisabled = false}) { + return QuestCard( + title: 'REFER FRIENDS', + description: 'Earn for every friend who joins\nQuantus using your link.', + actionLabel: 'View Referrals', + onTap: onTap, + onDisabledTap: onDisabledTap, + gradientColors: questReferFriendsGradient, + gradientStops: const [0.55, 0.62, 0.68, 0.72], + gradientCenter: const Alignment(0.6, -0.7), + gradientRadius: 1.29, + borderColor: const Color(0x7F6734BA), + isDisabled: isDisabled, + bgRectLeft: -127, + bgRectTop: -198, + ); + } + + factory QuestCard.kingOfTheShill({ + required VoidCallback? onTap, + VoidCallback? onDisabledTap, + bool isDisabled = false, + }) { + return QuestCard( + title: 'KING OF THE SHILL', + description: isDisabled + ? 'Link your X account to participate!' + : 'Participate in social raids and earn rewards for verified posts.', + actionLabel: 'View Raids', + onTap: onTap, + onDisabledTap: onDisabledTap, + gradientColors: questKingOfTheShillGradient, + gradientStops: const [0.55, 0.64, 0.68], + gradientCenter: const Alignment(0.7, -0.7), + gradientRadius: 1.3, + borderColor: const Color(0x7F773F56), + isDisabled: isDisabled, + bgRectLeft: -127, + bgRectTop: -198, + ); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: isDisabled ? onDisabledTap : onTap, + child: Opacity( + opacity: isDisabled ? 0.5 : 1.0, + child: Container( + width: double.infinity, + height: 246, + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: context.themeColors.background2, + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: borderColor), + borderRadius: BorderRadius.circular(4), + ), + ), + child: Stack( + children: [ + Positioned( + left: bgRectLeft, + top: bgRectTop, + child: ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 52, sigmaY: 52), + child: Container( + width: bgRectWidth, + height: bgRectHeight, + decoration: ShapeDecoration( + gradient: RadialGradient( + center: gradientCenter, + radius: gradientRadius, + colors: gradientColors, + stops: gradientStops, + ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: context.themeText.paragraph), + const SizedBox(height: 8), + Text( + description, + style: TextStyle( + color: context.themeColors.textPrimary.withValues(alpha: 0.5), + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w400, + ), + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(actionLabel, style: context.themeText.smallTitle), + const SizedBox(width: 8), + Icon(Icons.arrow_forward, size: 18, color: context.themeColors.textPrimary), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/mobile-app/lib/features/main/screens/quests/quest_constants.dart b/mobile-app/lib/features/main/screens/quests/quest_constants.dart new file mode 100644 index 00000000..c883c132 --- /dev/null +++ b/mobile-app/lib/features/main/screens/quests/quest_constants.dart @@ -0,0 +1,4 @@ +import 'package:flutter/material.dart'; + +const questReferFriendsGradient = [Color(0xFF0C1014), Color(0xFF0000FF), Color(0xFFED4CCE), Color(0xFFFFE91F)]; +const questKingOfTheShillGradient = [Color(0xFF0C1014), Color(0xFFED4CCE), Color(0xFFFFE91F)]; diff --git a/mobile-app/lib/features/components/quests_promo_video.dart b/mobile-app/lib/features/main/screens/quests/quests_promo_video.dart similarity index 100% rename from mobile-app/lib/features/components/quests_promo_video.dart rename to mobile-app/lib/features/main/screens/quests/quests_promo_video.dart diff --git a/mobile-app/lib/features/main/screens/quests/quests_screen.dart b/mobile-app/lib/features/main/screens/quests/quests_screen.dart index 4c296074..4554eaf2 100644 --- a/mobile-app/lib/features/main/screens/quests/quests_screen.dart +++ b/mobile-app/lib/features/main/screens/quests/quests_screen.dart @@ -1,68 +1,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:quantus_sdk/quantus_sdk.dart'; -import 'package:resonance_network_wallet/features/components/button.dart'; -import 'package:resonance_network_wallet/features/components/list_item.dart'; -import 'package:resonance_network_wallet/features/components/loading_text_animation.dart'; -import 'package:resonance_network_wallet/features/components/quests_promo_video.dart'; +import 'package:resonance_network_wallet/features/main/screens/quests/complete_setup_action_sheet.dart'; +import 'package:resonance_network_wallet/features/main/screens/quests/quest_card.dart'; import 'package:resonance_network_wallet/features/components/scaffold_base.dart'; -import 'package:resonance_network_wallet/features/components/sphere.dart'; -import 'package:resonance_network_wallet/features/main/screens/navbar.dart'; -import 'package:resonance_network_wallet/features/main/screens/quests/account_associations_status.dart'; import 'package:resonance_network_wallet/features/main/screens/quests/king_of_the_shill_screen.dart'; import 'package:resonance_network_wallet/features/main/screens/quests/referrals_quest_screen.dart'; -import 'package:resonance_network_wallet/features/main/screens/quests/optin_position_status.dart'; -import 'package:resonance_network_wallet/features/main/screens/quests/quest_title.dart'; +import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; import 'package:resonance_network_wallet/providers/account_associations_providers.dart'; import 'package:resonance_network_wallet/providers/account_stats_providers.dart'; -import 'package:resonance_network_wallet/providers/opt_in_position_providers.dart'; -import 'package:resonance_network_wallet/services/referral_service.dart'; -import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; class QuestsScreen extends ConsumerStatefulWidget { - final bool playPromoVideo; - const QuestsScreen({super.key, required this.playPromoVideo}); + const QuestsScreen({super.key}); @override ConsumerState createState() => _QuestsScreenState(); } class _QuestsScreenState extends ConsumerState { - final ReferralService _referralService = ReferralService(); final ScrollController _scrollController = ScrollController(); - - bool _isRewardProgramParticipant = false; - bool _isLoadingParticipation = true; - bool _isLastPromo = false; - bool _isSubmitting = false; - bool _isVisible = true; - - Future _loadParticipationStatus() async { - try { - final isParticipant = await _referralService.getRewardProgramParticiation(); - setState(() { - _isRewardProgramParticipant = isParticipant; - _isLoadingParticipation = false; - }); - } catch (e) { - debugPrint('Error loading participation status: $e'); - setState(() { - _isLoadingParticipation = false; - }); - } - } - - @override - void initState() { - super.initState(); - _loadParticipationStatus(); - } - - @override - void dispose() { - super.dispose(); - } + bool _showSetupTooltip = true; void refreshStatsData() { ref.invalidate(accountsStatsProvider); @@ -72,166 +29,131 @@ class _QuestsScreenState extends ConsumerState { ref.invalidate(accountAssociationsProvider); } - void setVideoVisibility(bool isVisible) { - if (mounted) { - setState(() { - _isVisible = isVisible; - }); - } - } - - void _setIsFinalVideo(bool isFinalVideo) { - setState(() { - _isLastPromo = isFinalVideo; - }); - } - - Future _handleOptIn(BuildContext context) async { + void _dismissTooltip() { setState(() { - _isSubmitting = true; + _showSetupTooltip = false; }); - - try { - await _referralService.optInRewardProgram(); - ref.invalidate(optInPositionProvider); - - setState(() { - _isSubmitting = false; - }); - - if (mounted) { - Navigator.pushAndRemoveUntil( - this.context, - MaterialPageRoute( - settings: const RouteSettings(name: 'navbar'), - builder: (context) => const Navbar(initialIndex: 3), - ), - (route) => false, - ); - } - } catch (e) { - print('Failed opting in reward program: $e'); - setState(() { - _isSubmitting = false; - }); - } } @override Widget build(BuildContext context) { - // Show loading state for users who haven't opted in to the reward program or if the promo video is not playing yet - if (_isLoadingParticipation || !widget.playPromoVideo) { - return const ScaffoldBase( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 12, - children: [ - QuestTitle(padding: EdgeInsetsGeometry.zero), - LoadingTextAnimation(), - ], - ), - ), - ); - } + final associationsAsync = ref.watch(accountAssociationsProvider); - if (!_isRewardProgramParticipant) { - return Scaffold( - backgroundColor: Colors.black, - body: Stack( - children: [ - QuestsPromoVideo( - isSubmitting: _isSubmitting, - closeSheet: null, // No close button for inline use - setIsFinalVideo: _setIsFinalVideo, - startFromBeginning: true, - showCloseButton: false, - isVisible: _isVisible, - ), - if (_isLastPromo) - Positioned( - bottom: 100, // Move down to avoid video text overlap - left: 0, - right: 0, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Colors.transparent, Colors.black.useOpacity(0.8)], - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Button( - label: "I'm In", - isLoading: _isSubmitting, - variant: ButtonVariant.primary, - onPressed: () { - _handleOptIn(context); - }, - ), - ], - ), - ), - ), - ], - ), - ); - } + final hasEthAddress = associationsAsync.maybeWhen( + data: (associations) => associations.ethAddress != null, + orElse: () => false, + ); + + final hasXUsername = associationsAsync.maybeWhen( + data: (associations) => associations.xUsername != null, + orElse: () => false, + ); + + final showTooltip = !hasEthAddress && _showSetupTooltip; return ScaffoldBase.refreshable( + backgroundColor: context.themeColors.background2, onRefresh: () async { refreshStatsData(); refreshAssociationsData(); }, scrollController: _scrollController, - decorations: [ - const Positioned(top: 180, right: -34, child: Sphere(variant: 2, size: 194)), - const Positioned(left: -60, bottom: 0, child: Sphere(variant: 7, size: 240.68)), - ], slivers: [ SliverToBoxAdapter( child: Column( - mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const QuestTitle(), - const OptinPositionStatus(), - SizedBox(height: context.isSmallHeight ? 18 : 37.0), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 4, - children: [ - Text('Associated Accounts', style: context.themeText.smallParagraph, textAlign: TextAlign.start), - const AccountAssociationsStatus(), - ], - ), + const SizedBox(height: 24), + _buildHeader(context, hasEthAddress, showTooltip), + const SizedBox(height: 48), + _buildQuestCards(context, hasEthAddress, hasXUsername), ], ), ), + ], + ); + } - SliverToBoxAdapter(child: SizedBox(height: context.isSmallHeight ? 18 : 37.0)), - SliverToBoxAdapter(child: Text('Quests', style: context.themeText.smallParagraph)), - const SliverToBoxAdapter(child: SizedBox(height: 4)), - SliverList( - delegate: SliverChildListDelegate([ - ListItem( - title: 'King of The Shill', - onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => const KingOfTheShillScreen())); - }, + Widget _buildHeader(BuildContext context, bool hasEthAddress, bool showTooltip) { + return Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Image.asset('assets/quests/quests_top_logo.png', height: 24, fit: BoxFit.contain), + GestureDetector( + onTap: () => showCompleteSetupActionSheet(context), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: ShapeDecoration( + color: context.themeColors.settingCard, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + ), + child: Text(hasEthAddress ? 'Setup' : 'Complete Setup', style: context.themeText.smallParagraph), + ), ), - const SizedBox(height: 12), - ListItem( - title: 'Referrals', - onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => const ReferralsQuestScreen())); - }, + ], + ), + if (showTooltip) ...[const SizedBox(height: 8), _buildSetupTooltip()], + ], + ); + } + + Widget _buildSetupTooltip() { + return Container( + padding: const EdgeInsets.all(14), + decoration: ShapeDecoration( + color: context.themeColors.background2, + shape: RoundedRectangleBorder( + side: const BorderSide(width: 1, color: Color(0x66F4F6F9)), + borderRadius: BorderRadius.circular(4), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + width: 149, + child: Text( + 'Complete setup to unlock quests.', + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontFamily: 'Fira Code', + fontWeight: FontWeight.w400, + height: 1.35, + ), ), - ]), + ), + const SizedBox(width: 8), + GestureDetector( + onTap: _dismissTooltip, + child: const Icon(Icons.close, color: Colors.white, size: 14), + ), + ], + ), + ); + } + + Widget _buildQuestCards(BuildContext context, bool hasEthAddress, bool hasXUsername) { + return Column( + children: [ + QuestCard.referFriends( + isDisabled: !hasEthAddress, + onDisabledTap: () => showCompleteSetupActionSheet(context), + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => const ReferralsQuestScreen())); + }, + ), + const SizedBox(height: 40), + QuestCard.kingOfTheShill( + isDisabled: !hasXUsername, + onDisabledTap: () => showCompleteSetupActionSheet(context), + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => const KingOfTheShillScreen())); + }, ), ], ); diff --git a/mobile-app/lib/features/components/raid_submission_action_sheet.dart b/mobile-app/lib/features/main/screens/quests/raid_submission_action_sheet.dart similarity index 96% rename from mobile-app/lib/features/components/raid_submission_action_sheet.dart rename to mobile-app/lib/features/main/screens/quests/raid_submission_action_sheet.dart index 70c4aacb..0f7554f5 100644 --- a/mobile-app/lib/features/components/raid_submission_action_sheet.dart +++ b/mobile-app/lib/features/main/screens/quests/raid_submission_action_sheet.dart @@ -75,9 +75,14 @@ class _RaidSubmissionActionSheetState extends ConsumerState createState() => _ReferralsQuestScreenState(); } -class _ReferralsQuestScreenState extends ConsumerState with WidgetsBindingObserver { +class _ReferralsQuestScreenState extends ConsumerState { final ReferralService _referralService = ReferralService(); - final ScrollController _scrollController = ScrollController(); - String? _referralCode; Future _loadReferralCode() async { @@ -59,187 +50,324 @@ class _ReferralsQuestScreenState extends ConsumerState wit } } + void _showHowItWorksDialog() { + showDialog( + context: context, + barrierColor: Colors.black.withValues(alpha: 0.7), + builder: (context) => GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Scaffold( + backgroundColor: Colors.transparent, + body: Center( + child: GestureDetector( + onTap: () {}, + child: Container( + width: 280, + padding: const EdgeInsets.all(24), + decoration: ShapeDecoration( + color: context.themeColors.background2, + shape: RoundedRectangleBorder( + side: const BorderSide(width: 1, color: Color(0x66F4F6F9)), + borderRadius: BorderRadius.circular(20), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('HOW IT WORKS', style: context.themeText.paragraph), + GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: const Icon(Icons.close, color: Colors.white, size: 24), + ), + ], + ), + const SizedBox(height: 32), + _buildStep('Step 1', 'Invite users using your unique code'), + const SizedBox(height: 16), + _buildStep( + 'Step 2', + 'They must create a Quantus Wallet. Referrals without wallet creation won\'t count', + ), + const SizedBox(height: 16), + _buildStep('Step 3', 'Climb the leaderboard. Rank updates automatically'), + ], + ), + ), + ), + ), + ), + ), + ); + } + + Widget _buildStep(String title, String description) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontFamily: 'Fira Code', + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 2), + Text( + description, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.50), + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w400, + ), + ), + ], + ); + } + @override void initState() { super.initState(); _loadReferralCode(); } - @override - void dispose() { - super.dispose(); - } - void refreshStatsData() { ref.invalidate(accountsStatsProvider); - } - - void refreshAssociationsData() { ref.invalidate(accountAssociationsProvider); } @override Widget build(BuildContext context) { final statsAsync = ref.watch(accountsStatsProvider); + final referralsCount = statsAsync.value?.referralCount ?? 0; - return ScaffoldBase.refreshable( - appBar: WalletAppBar(title: 'Referrals Quest'), - padding: const EdgeInsetsGeometry.all(0), - onRefresh: () async { - refreshStatsData(); - refreshAssociationsData(); - }, - scrollController: _scrollController, - decorations: [ - const Positioned(top: 180, right: -34, child: Sphere(variant: 2, size: 194)), - const Positioned(left: -60, bottom: 0, child: Sphere(variant: 7, size: 240.68)), - ], - slivers: [ - SliverToBoxAdapter( - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.only(top: 11), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(right: 44), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 17, - children: [ - _buildDecoration(), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + return ScaffoldBase( + appBar: WalletAppBar( + title: 'Referrals', + actions: [ + IconButton( + icon: const Icon(Icons.info_outline, color: Colors.white), + onPressed: _showHowItWorksDialog, + ), + ], + ), + child: SafeArea( + top: false, + child: Column( + children: [ + Expanded( + child: Container( + width: double.infinity, + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: context.themeColors.background2, + shape: RoundedRectangleBorder( + side: const BorderSide(width: 1, color: Color(0x7F6734BA)), + borderRadius: BorderRadius.circular(4), + ), + ), + child: InnerShadowContainer.standard( + child: Stack( + children: [ + Positioned.fill( + left: -156, + top: 72, + child: Container( + width: 531, + height: 531, + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + gradient: const RadialGradient( + center: Alignment(0.77, -0.77), + radius: 1.8, + colors: questReferFriendsGradient, + stops: [0.45, 0.53, 0.62, 0.65], + ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Column( + children: [ + Text('REFER FRIENDS', style: context.themeText.paragraph), + const SizedBox(height: 8), + Text( + 'Invite friends. Earn rewards. \nClimb the leaderboard.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.50), + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + const SizedBox(height: 40), + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - const SizedBox(height: 96), - const Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [OptinPositionStatus(), SizedBox(width: 71)], + // these colors aren't really great anyway..should be spheres for accounts.. + _buildAvatar([const Color(0xFF0000FF), context.themeColors.background2]), + Transform.translate( + offset: const Offset(-16, 0), + child: _buildAvatar([const Color(0xFF8B0000), context.themeColors.pink]), ), - SizedBox(height: context.isSmallHeight ? 18 : 37.0), - const AccountAssociationsStatus(), - SizedBox(height: context.isSmallHeight ? 18 : 37.0), - ..._buildAccountStats(context, statsAsync), - const SizedBox(height: 16), - LinkText( - label: 'Learn more', - url: AppConstants.shillQuestsPageUrl, - textStyle: context.themeText.smallParagraph, + Transform.translate( + offset: const Offset(-32, 0), + child: _buildAvatar([const Color(0xFFFFD700), context.themeColors.yellow]), ), ], ), - ), - ], - ), - ), - SizedBox(height: context.isSmallHeight ? 18 : 40), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: InkWell( - onTap: _copyReferralCode, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 11), - decoration: ShapeDecoration( - color: context.themeColors.background, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), - ), - child: Row( - spacing: 12.0, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - _referralCode ?? 'Loading...', - style: context.themeText.smallParagraph, - textAlign: TextAlign.center, + const SizedBox(height: 40), + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: ShapeDecoration( + color: context.themeColors.background2.useOpacity(0.4), + shape: RoundedRectangleBorder( + side: const BorderSide(width: 1, color: Color(0x33F4F6F9)), + borderRadius: BorderRadius.circular(8), + ), + shadows: const [ + BoxShadow(color: Color(0x3F000000), blurRadius: 4, offset: Offset(4, 4)), + ], ), - const CopyIcon(), - ], - ), + child: Column( + children: [ + _buildStatRow('Referrals', '$referralsCount', Colors.white), + const SizedBox(height: 16), + _buildStatRow('Rank', '#-', context.themeColors.pink), + ], + ), + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'Your invite code', + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontFamily: 'Fira Code', + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 12), + GestureDetector( + onTap: _copyReferralCode, + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: ShapeDecoration( + color: Colors.white.useOpacity(0.3), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _referralCode ?? 'Loading...', + style: const TextStyle( + color: Color(0xFFF4F6F9), + fontSize: 12, + fontFamily: 'Fira Code', + fontWeight: FontWeight.w400, + ), + ), + const CopyIcon(width: 16, color: Color(0xFFF4F6F9)), + ], + ), + ), + ), + ], + ), + ], ), ), + ], + ), + ), + ), + ), + const SizedBox(height: 32), + GestureDetector( + onTap: _shareReferralLink, + child: InnerShadowContainer.standard( + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: ShapeDecoration( + color: const Color(0x33F4F6F9), + shape: RoundedRectangleBorder( + side: const BorderSide(width: 1, color: Color(0x33F4F6F9)), + borderRadius: BorderRadius.circular(42), ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Button( - variant: ButtonVariant.glassOutline, - label: 'Share Referral Link', - onPressed: _shareReferralLink, - ), - ), - ], + ), + alignment: Alignment.center, + child: Text( + 'Share Link', + style: context.themeText.smallTitle?.copyWith(color: context.themeColors.textPrimary), + ), ), ), - const QuestTitle(), - ], - ), + ), + ], ), - ], + ), ); } - List _buildAccountStats(BuildContext context, AsyncValue statsAsync) { - return statsAsync.when( - data: (stats) => [ - _buildStatCard(context, 'Referrals:', stats.referralCount), - const SizedBox(height: 9), - _buildStatCard(context, 'Sends:', stats.sendCount), - const SizedBox(height: 9), - _buildStatCard(context, 'Reversals:', stats.reversalCount), - const SizedBox(height: 9), - _buildStatCard(context, 'Mining:', stats.miningCount), - ], - loading: () => [ - _buildStatCard(context, 'Referrals:', null), - const SizedBox(height: 9), - _buildStatCard(context, 'Sends:', null), - const SizedBox(height: 9), - _buildStatCard(context, 'Reversals:', null), - const SizedBox(height: 9), - _buildStatCard(context, 'Mining:', null), - ], - error: (error, stack) => [ - Text( - 'Error fetching account stats.', - style: context.themeText.detail?.copyWith(color: context.themeColors.textError), + Widget _buildAvatar(List colors) { + return Container( + width: 64, + height: 64, + decoration: ShapeDecoration( + gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: colors), + shape: OvalBorder( + side: BorderSide( + width: 2.67, + strokeAlign: BorderSide.strokeAlignOutside, + color: context.themeColors.background2, + ), ), - const SizedBox(height: 12), - Button(variant: ButtonVariant.neutral, label: 'Try again', onPressed: refreshStatsData), - ], - ); - } - - Widget _buildStatCard(BuildContext context, String title, int? stat) { - final isLoading = stat == null; - - return BasicCard( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(title, style: context.themeText.smallTitle), - isLoading ? const Skeleton(width: 40, height: 16) : Text('$stat', style: context.themeText.smallTitle), - ], ), ); } - Widget _buildDecoration() { - return Container( - width: 85, - height: context.isSmallHeight ? 415 : 480, - decoration: const ShapeDecoration( - gradient: LinearGradient( - begin: Alignment(0.03, -1.00), - end: Alignment(-0.03, 1), - colors: [Color(0xFF0000FF), Color(0xFFED4CCE), Color(0xFFFFE91F)], + Widget _buildStatRow(String label, String value, Color valueColor) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: const TextStyle( + color: Color(0xFFF4F6F9), + fontSize: 14, + fontFamily: 'Fira Code', + fontWeight: FontWeight.w400, + ), ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only(topRight: Radius.circular(10), bottomRight: Radius.circular(10)), + Text( + value, + textAlign: TextAlign.center, + style: TextStyle(color: valueColor, fontSize: 14, fontFamily: 'Fira Code', fontWeight: FontWeight.w400), ), - ), + ], ); } } diff --git a/mobile-app/lib/features/styles/app_colors_theme.dart b/mobile-app/lib/features/styles/app_colors_theme.dart index f3e9970e..e7de2065 100644 --- a/mobile-app/lib/features/styles/app_colors_theme.dart +++ b/mobile-app/lib/features/styles/app_colors_theme.dart @@ -13,6 +13,7 @@ class AppColorsTheme extends ThemeExtension { final Color pink; final Color yellow; final Color background; + final Color background2; final Color surface; final Color surfaceActive; final Color error; @@ -52,6 +53,7 @@ class AppColorsTheme extends ThemeExtension { required this.pink, required this.yellow, required this.background, + required this.background2, required this.surface, required this.surfaceActive, required this.error, @@ -93,6 +95,7 @@ class AppColorsTheme extends ThemeExtension { pink: const Color(0xFFED4CCE), yellow: const Color(0xFFFFE91F), background: const Color(0xFF0B0F14), + background2: const Color(0xFF0C1014), surface: const Color(0xFF000000), surfaceActive: const Color(0xFFF4F6F9), error: const Color(0xFFFF2D54), @@ -134,6 +137,7 @@ class AppColorsTheme extends ThemeExtension { pink: const Color(0xFFED4CCE), yellow: const Color(0xFFFFE91F), background: const Color(0xFF0B0F14), + background2: const Color(0xFF0C1014), surface: const Color(0xFF000000), surfaceActive: const Color(0xFFF4F6F9), error: const Color(0xFFFF2D54), @@ -174,6 +178,7 @@ class AppColorsTheme extends ThemeExtension { Color? pink, Color? yellow, Color? background, + Color? background2, Color? surface, Color? surfaceActive, Color? error, @@ -213,6 +218,7 @@ class AppColorsTheme extends ThemeExtension { pink: pink ?? this.pink, yellow: yellow ?? this.yellow, background: background ?? this.background, + background2: background2 ?? this.background2, surface: surface ?? this.surface, surfaceActive: surfaceActive ?? this.surfaceActive, error: error ?? this.error, @@ -256,6 +262,7 @@ class AppColorsTheme extends ThemeExtension { pink: Color.lerp(pink, other.pink, t) ?? pink, yellow: Color.lerp(yellow, other.yellow, t) ?? yellow, background: Color.lerp(background, other.background, t) ?? background, + background2: Color.lerp(background2, other.background2, t) ?? background2, surface: Color.lerp(surface, other.surface, t) ?? surface, surfaceActive: Color.lerp(surfaceActive, other.surfaceActive, t) ?? surfaceActive, error: Color.lerp(error, other.error, t) ?? error, diff --git a/mobile-app/lib/services/referral_service.dart b/mobile-app/lib/services/referral_service.dart index d9c6588c..46fa64b4 100644 --- a/mobile-app/lib/services/referral_service.dart +++ b/mobile-app/lib/services/referral_service.dart @@ -151,7 +151,7 @@ class ReferralService { String link = generateReferralLink(referralCode); String message = - "Most L1s aren't ready for quantum threats. This one is.\nI'm on the @QuantusNetwork testnet stacking early points for rewards.\nUse my referral link so we both earn points: $referralCode\n\nDownload the wallet & get in early\n$link"; + "Most L1s aren't ready for quantum threats. This one is.\nI'm on the @QuantusNetwork testnet stacking early points for rewards.\nUse my referral link so we both earn points:\n$referralCode\n\nDownload the wallet & get in early\n\n$link"; return ShareParams( text: message, diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index b9bb1923..8db77fe0 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -100,6 +100,7 @@ flutter: - assets/high_security/step_indicator_icon.svg - assets/high_security/big_red_button_icon.png - assets/high_security/intercept_icon.svg + - assets/quests/quests_top_logo.png fonts: - family: Fira Code