From de98358926d0ad77437e3f93466c9bd68376e249 Mon Sep 17 00:00:00 2001 From: Alfreedom <00tango.bromine@icloud.com> Date: Wed, 6 Nov 2024 16:36:30 +0100 Subject: [PATCH 1/6] Wallet activity list --- .../lib/modal/appkit_modal_impl.dart | 2 - .../lib/modal/assets/icons/receive.svg | 3 + .../modal/assets/icons/regular/receive.svg | 3 + .../lib/modal/assets/icons/regular/send.svg | 3 + .../lib/modal/assets/icons/regular/swap.svg | 3 + .../lib/modal/assets/icons/send.svg | 3 + .../modal/assets/icons/swap_horizontal.svg | 4 +- .../lib/modal/assets/icons/swap_vertical.svg | 3 + .../lib/modal/constants/key_constants.dart | 1 + .../lib/modal/pages/account_page.dart | 133 ++++-- .../lib/modal/pages/activity_page.dart | 256 ++++++++++ .../blockchain_service.dart | 38 ++ .../i_blockchain_service.dart | 6 + .../models/wallet_activity.dart | 363 ++++++++++++++ .../lib/modal/utils/core_utils.dart | 14 +- .../modal/widgets/lists/activity_item.dart | 448 ++++++++++++++++++ .../lists/list_items/base_list_item.dart | 13 +- packages/reown_appkit/pubspec.yaml | 11 +- 18 files changed, 1248 insertions(+), 59 deletions(-) create mode 100644 packages/reown_appkit/lib/modal/assets/icons/receive.svg create mode 100644 packages/reown_appkit/lib/modal/assets/icons/regular/receive.svg create mode 100644 packages/reown_appkit/lib/modal/assets/icons/regular/send.svg create mode 100644 packages/reown_appkit/lib/modal/assets/icons/regular/swap.svg create mode 100644 packages/reown_appkit/lib/modal/assets/icons/send.svg create mode 100644 packages/reown_appkit/lib/modal/assets/icons/swap_vertical.svg create mode 100644 packages/reown_appkit/lib/modal/pages/activity_page.dart create mode 100644 packages/reown_appkit/lib/modal/services/blockchain_service/models/wallet_activity.dart create mode 100644 packages/reown_appkit/lib/modal/widgets/lists/activity_item.dart diff --git a/packages/reown_appkit/lib/modal/appkit_modal_impl.dart b/packages/reown_appkit/lib/modal/appkit_modal_impl.dart index f8adf5b..473e9a7 100644 --- a/packages/reown_appkit/lib/modal/appkit_modal_impl.dart +++ b/packages/reown_appkit/lib/modal/appkit_modal_impl.dart @@ -877,8 +877,6 @@ class ReownAppKitModal @override Future buildConnectionUri() async { if (!_isConnected) { - /// TODO Qs: How do I handle SIWE if non-EVM chains are included? - /// TODO Qs: How do I handle switch to Solana from EVM chain? try { if (_siweService.enabled) { final walletRedirect = _explorerService.getWalletRedirect( diff --git a/packages/reown_appkit/lib/modal/assets/icons/receive.svg b/packages/reown_appkit/lib/modal/assets/icons/receive.svg new file mode 100644 index 0000000..c14cde5 --- /dev/null +++ b/packages/reown_appkit/lib/modal/assets/icons/receive.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/reown_appkit/lib/modal/assets/icons/regular/receive.svg b/packages/reown_appkit/lib/modal/assets/icons/regular/receive.svg new file mode 100644 index 0000000..3632cae --- /dev/null +++ b/packages/reown_appkit/lib/modal/assets/icons/regular/receive.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/reown_appkit/lib/modal/assets/icons/regular/send.svg b/packages/reown_appkit/lib/modal/assets/icons/regular/send.svg new file mode 100644 index 0000000..07e9272 --- /dev/null +++ b/packages/reown_appkit/lib/modal/assets/icons/regular/send.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/reown_appkit/lib/modal/assets/icons/regular/swap.svg b/packages/reown_appkit/lib/modal/assets/icons/regular/swap.svg new file mode 100644 index 0000000..02b54a5 --- /dev/null +++ b/packages/reown_appkit/lib/modal/assets/icons/regular/swap.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/reown_appkit/lib/modal/assets/icons/send.svg b/packages/reown_appkit/lib/modal/assets/icons/send.svg new file mode 100644 index 0000000..438c824 --- /dev/null +++ b/packages/reown_appkit/lib/modal/assets/icons/send.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/reown_appkit/lib/modal/assets/icons/swap_horizontal.svg b/packages/reown_appkit/lib/modal/assets/icons/swap_horizontal.svg index e510a19..9162839 100644 --- a/packages/reown_appkit/lib/modal/assets/icons/swap_horizontal.svg +++ b/packages/reown_appkit/lib/modal/assets/icons/swap_horizontal.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/reown_appkit/lib/modal/assets/icons/swap_vertical.svg b/packages/reown_appkit/lib/modal/assets/icons/swap_vertical.svg new file mode 100644 index 0000000..450d259 --- /dev/null +++ b/packages/reown_appkit/lib/modal/assets/icons/swap_vertical.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/reown_appkit/lib/modal/constants/key_constants.dart b/packages/reown_appkit/lib/modal/constants/key_constants.dart index 697e3ee..0735996 100644 --- a/packages/reown_appkit/lib/modal/constants/key_constants.dart +++ b/packages/reown_appkit/lib/modal/constants/key_constants.dart @@ -26,6 +26,7 @@ class KeyConstants { static const Key confirmEmailPage = Key('confirmEmailPage'); static const Key approveSiwePageKey = Key('approveSiwePageKey'); static const Key socialLoginPage = Key('socialLoginPage'); + static const Key activityPageKey = Key('activityPageKey'); // Buttons static const Key helpButtonKey = Key('helpButtonKey'); diff --git a/packages/reown_appkit/lib/modal/pages/account_page.dart b/packages/reown_appkit/lib/modal/pages/account_page.dart index 438a2fb..72664ab 100644 --- a/packages/reown_appkit/lib/modal/pages/account_page.dart +++ b/packages/reown_appkit/lib/modal/pages/account_page.dart @@ -6,6 +6,7 @@ import 'package:get_it/get_it.dart'; import 'package:reown_appkit/modal/constants/key_constants.dart'; import 'package:reown_appkit/modal/constants/style_constants.dart'; +import 'package:reown_appkit/modal/pages/activity_page.dart'; import 'package:reown_appkit/modal/pages/edit_email_page.dart'; import 'package:reown_appkit/modal/pages/upgrade_wallet_page.dart'; import 'package:reown_appkit/modal/services/analytics_service/models/analytics_event.dart'; @@ -91,7 +92,6 @@ class _DefaultAccountView extends StatelessWidget { @override Widget build(BuildContext context) { - final themeData = ReownAppKitModalTheme.getDataOf(context); final themeColors = ReownAppKitModalTheme.colorsOf(context); final isEmailLogin = _service.session?.sessionService.isMagic ?? false; return Column( @@ -135,27 +135,9 @@ class _DefaultAccountView extends StatelessWidget { // visible: !isEmailLogin, // child: _ConnectedWalletButton(), // ), - const SizedBox.square(dimension: kPadding8), _SelectNetworkButton(), - const SizedBox.square(dimension: kPadding8), - AccountListItem( - iconPath: 'lib/modal/assets/icons/disconnect.svg', - trailing: _service.status.isLoading - ? Row( - children: [ - CircularLoader(size: 18.0, strokeWidth: 2.0), - SizedBox.square(dimension: kPadding12), - ], - ) - : const SizedBox.shrink(), - title: 'Disconnect', - titleStyle: themeData.textStyles.paragraph500.copyWith( - color: themeColors.foreground200, - ), - onTap: _service.status.isLoading - ? null - : () => _service.closeModal(disconnectSession: true), - ), + _ActivityButton(), + _DisconnectButton(), ], ); } @@ -322,29 +304,92 @@ class _SelectNetworkButton extends StatelessWidget { final imageId = ReownAppKitModalNetworks.getNetworkIconId(chainId); final tokenImage = GetIt.I().getAssetImageUrl(imageId); final radiuses = ReownAppKitModalTheme.radiusesOf(context); - return AccountListItem( - iconWidget: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: imageId.isEmpty - ? RoundedIcon( - assetPath: 'lib/modal/assets/icons/network.svg', - assetColor: themeColors.inverse100, - borderRadius: radiuses.isSquare() ? 0.0 : null, - ) - : RoundedIcon( - borderRadius: radiuses.isSquare() ? 0.0 : null, - imageUrl: tokenImage, - assetColor: themeColors.background100, - ), - ), - title: service.selectedChain?.name ?? 'Unsupported network', - titleStyle: themeData.textStyles.paragraph500.copyWith( - color: themeColors.foreground100, - ), - onTap: () => widgetStack.instance.push( - ReownAppKitModalSelectNetworkPage(), - event: ClickNetworksEvent(), - ), + return Column( + children: [ + const SizedBox.square(dimension: kPadding8), + AccountListItem( + iconWidget: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: imageId.isEmpty + ? RoundedIcon( + assetPath: 'lib/modal/assets/icons/network.svg', + assetColor: themeColors.inverse100, + borderRadius: radiuses.isSquare() ? 0.0 : null, + ) + : RoundedIcon( + borderRadius: radiuses.isSquare() ? 0.0 : null, + imageUrl: tokenImage, + assetColor: themeColors.background100, + ), + ), + title: service.selectedChain?.name ?? 'Unsupported network', + titleStyle: themeData.textStyles.paragraph500.copyWith( + color: themeColors.foreground100, + ), + onTap: () => widgetStack.instance.push( + ReownAppKitModalSelectNetworkPage(), + event: ClickNetworksEvent(), + ), + ), + ], + ); + } +} + +class _ActivityButton extends StatelessWidget { + @override + Widget build(BuildContext context) { + final themeColors = ReownAppKitModalTheme.colorsOf(context); + return Column( + children: [ + const SizedBox.square(dimension: kPadding8), + AccountListItem( + iconPath: 'lib/modal/assets/icons/swap_horizontal.svg', + iconColor: themeColors.accent100, + iconBGColor: themeColors.accenGlass015, + iconBorderColor: themeColors.accenGlass005, + title: 'Activity', + // titleStyle: themeData.textStyles.paragraph500.copyWith( + // color: themeColors.foreground200, + // ), + onTap: () => widgetStack.instance.push(ActivityPage()), + ), + ], + ); + } +} + +class _DisconnectButton extends StatelessWidget { + @override + Widget build(BuildContext context) { + final service = ModalProvider.of(context).instance; + final themeData = ReownAppKitModalTheme.getDataOf(context); + final themeColors = ReownAppKitModalTheme.colorsOf(context); + return Column( + children: [ + const SizedBox.square(dimension: kPadding8), + AccountListItem( + iconPath: 'lib/modal/assets/icons/disconnect.svg', + iconColor: themeColors.foreground175, + iconBGColor: themeColors.grayGlass010, + iconBorderColor: themeColors.grayGlass005, + trailing: service.status.isLoading + ? Row( + children: [ + CircularLoader(size: 18.0, strokeWidth: 2.0), + SizedBox.square(dimension: kPadding12), + ], + ) + : const SizedBox.shrink(), + title: 'Disconnect', + titleStyle: themeData.textStyles.paragraph500.copyWith( + color: themeColors.foreground200, + ), + onTap: service.status.isLoading + ? null + : () => service.closeModal(disconnectSession: true), + ), + ], ); } } diff --git a/packages/reown_appkit/lib/modal/pages/activity_page.dart b/packages/reown_appkit/lib/modal/pages/activity_page.dart new file mode 100644 index 0000000..c0e72fd --- /dev/null +++ b/packages/reown_appkit/lib/modal/pages/activity_page.dart @@ -0,0 +1,256 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:intl/intl.dart'; +import 'package:reown_appkit/modal/constants/key_constants.dart'; +import 'package:reown_appkit/modal/constants/style_constants.dart'; +import 'package:reown_appkit/modal/i_appkit_modal_impl.dart'; +import 'package:reown_appkit/modal/services/blockchain_service/i_blockchain_service.dart'; +import 'package:reown_appkit/modal/services/blockchain_service/models/wallet_activity.dart'; +import 'package:reown_appkit/modal/widgets/lists/activity_item.dart'; +import 'package:reown_appkit/modal/widgets/miscellaneous/responsive_container.dart'; +import 'package:reown_appkit/modal/widgets/modal_provider.dart'; +import 'package:reown_appkit/modal/widgets/navigation/navbar.dart'; +import 'package:reown_appkit/reown_appkit.dart'; +import 'package:shimmer/shimmer.dart'; + +class ActivityPage extends StatefulWidget { + const ActivityPage() : super(key: KeyConstants.activityPageKey); + @override + State createState() => _ActivityPageState(); +} + +class _ActivityPageState extends State { + IBlockChainService get _blockchainService => GetIt.I(); + IReownCore get _core => _appKitModal.appKit!.core; + late final IReownAppKitModal _appKitModal; + + final _scrollController = ScrollController(); + final List _activities = []; + String? _currentCursor; + bool _isLoadingActivities = false; + bool _hasMoreActivities = true; + String _currentAddress = ''; + String _currentChain = ''; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + _appKitModal = ModalProvider.of(context).instance; + final chainId = _appKitModal.selectedChain!.chainId; + final namespace = ReownAppKitModalNetworks.getNamespaceForChainId( + chainId, + ); + _currentChain = '$namespace:$chainId'; + _currentAddress = _appKitModal.session!.getAddress(namespace)!; + + _scrollController.addListener(() { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 100) { + _loadMoreActivities(); + } + }); + + _fetchActivities(); + }); + } + + Future _fetchActivities() async { + setState(() => _isLoadingActivities = true); + + // Initial API request here + final activityData = await _blockchainService.getActivity( + address: _currentAddress, + cursor: _currentCursor, + ); + final newItems = activityData.data ?? []; + final activityList = newItems.where((data) { + return data.metadata?.chain == _currentChain && + (data.transfers ?? []).isNotEmpty; + }).toList(); + + _activities.addAll(activityList); + _isLoadingActivities = false; + _currentCursor = activityData.next; + _hasMoreActivities = _currentCursor != null; + _core.logger.d( + '[$runtimeType] fetch data, items: ${activityList.length}, cursor: $_currentCursor, _hasMoreActivities: $_hasMoreActivities', + ); + setState(() {}); + } + + Future _loadMoreActivities() async { + if (_isLoadingActivities || !_hasMoreActivities) return; + await _fetchActivities(); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ModalNavbar( + title: 'Activity', + safeAreaLeft: true, + safeAreaRight: true, + safeAreaBottom: false, + body: Container( + constraints: BoxConstraints( + maxHeight: ((_isLoadingActivities && _activities.isEmpty) || + _activities.length <= 4) + ? 340.0 + : ResponsiveData.maxHeightOf(context), + ), + padding: const EdgeInsets.symmetric(horizontal: kPadding12), + child: _ListBuilder( + scrollController: _scrollController, + items: _activities, + caip2chain: _currentChain, + isLoading: _isLoadingActivities, + ), + ), + ); + } +} + +class _ListBuilder extends StatefulWidget { + const _ListBuilder({ + required this.scrollController, + required this.items, + required this.caip2chain, + required this.isLoading, + }); + final ScrollController scrollController; + final List items; + final String caip2chain; + final bool isLoading; + + @override + State<_ListBuilder> createState() => __ListBuilderState(); +} + +class __ListBuilderState extends State<_ListBuilder> { + @override + Widget build(BuildContext context) { + final themeData = ReownAppKitModalTheme.getDataOf(context); + final themeColors = ReownAppKitModalTheme.colorsOf(context); + if (widget.isLoading && widget.items.isEmpty) { + final loadingList = [ + ActivityListItemLoader(), + ActivityListItemLoader(), + ActivityListItemLoader(), + ActivityListItemLoader(), + ActivityListItemLoader(), + ] + .map( + (e) => Shimmer.fromColors( + baseColor: themeColors.grayGlass100, + highlightColor: themeColors.grayGlass025, + child: e, + ), + ) + .toList(); + return ListView.builder( + itemCount: loadingList.length, + padding: const EdgeInsets.only(bottom: 30.0, top: 10.0), + itemBuilder: (_, int index) { + return Container( + width: 1000.0, + padding: const EdgeInsets.only(bottom: 6.0), + child: loadingList[index], + ); + }, + ); + } + + if (widget.items.isEmpty) { + return Center( + child: Text( + 'No activity found', + style: themeData.textStyles.paragraph500.copyWith( + color: themeColors.foreground100, + ), + ), + ); + } + + final groupedByYearMonth = groupBy(widget.items, (Activity obj) { + final monthName = DateFormat.MMMM().format( + obj.metadata!.minedAt!, + ); + return '$monthName ${obj.metadata!.minedAt!.year}'; + }); + + // Flatten the grouped data for the ListView + final groupedActivities = >>[]; + groupedByYearMonth.forEach((yearMonth, objs) { + groupedActivities.add(MapEntry(yearMonth, objs)); + groupedActivities.addAll(objs.map((obj) => MapEntry('', [obj]))); + }); + + return ListView.builder( + controller: widget.scrollController, + // Extra space for loading indicator + itemCount: groupedActivities.length + (widget.isLoading ? 1 : 0), + padding: const EdgeInsets.only(bottom: 30.0), + itemBuilder: (_, int index) { + if (index == groupedActivities.length) { + // Loading indicator + return Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: CircularProgressIndicator.adaptive(), + ), + ); + } + final entry = groupedActivities[index]; + if (entry.key.isNotEmpty) { + // Display the year-month as a header + return Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + entry.key, + style: themeData.textStyles.paragraph400.copyWith( + color: themeColors.foreground200, + ), + ), + ); + } else { + // Display the object details + final activity = entry.value.first; + return ActivityListItem( + activity: activity, + onTap: () {}, + ); + } + }, + ); + } + + // ignore: unused_element + List _removeNFTsFromTransfers(List activities) { + final activityList = activities + .where((data) { + return data.metadata?.chain == widget.caip2chain && + (data.transfers ?? []).isNotEmpty; + }) + .toList() + .map((a) { + final transfers = List.from( + (a.transfers ?? []).where((e) => e.fungibleInfo != null)); + return Activity( + id: a.id, + metadata: a.metadata, + transfers: transfers, + ); + }) + .where((element) { + return (element.transfers ?? []).isNotEmpty; + }); + return activityList.toList(); + } +} diff --git a/packages/reown_appkit/lib/modal/services/blockchain_service/blockchain_service.dart b/packages/reown_appkit/lib/modal/services/blockchain_service/blockchain_service.dart index 60079da..091abfd 100644 --- a/packages/reown_appkit/lib/modal/services/blockchain_service/blockchain_service.dart +++ b/packages/reown_appkit/lib/modal/services/blockchain_service/blockchain_service.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:reown_appkit/modal/services/blockchain_service/models/wallet_activity.dart'; import 'package:reown_appkit/reown_appkit.dart'; import 'package:reown_appkit/modal/constants/string_constants.dart'; import 'package:reown_appkit/modal/services/blockchain_service/models/blockchain_identity.dart'; @@ -104,6 +105,43 @@ class BlockChainService implements IBlockChainService { } } + @override + Future getActivity({ + required String address, + String? cursor, + }) async { + final uri = Uri.parse('$_baseUrl/account/$address/history'); + final queryParams = { + ..._requiredParams, + if (cursor != null) 'cursor': cursor, + }; + final url = uri.replace(queryParameters: queryParams); + final response = await http.get(url, headers: { + ..._requiredHeaders, + 'Content-Type': 'application/json', + }); + _core.logger.d('[$runtimeType] getActivity $url, ${response.statusCode}'); + if (response.statusCode == 200 && response.body.isNotEmpty) { + try { + return ActivityData.fromRawJson(response.body); + } catch (e) { + _core.logger.e('[$runtimeType] getActivity, parse result error => $e'); + throw Exception('Failed to load wallet activity. $e'); + } + } + try { + final errorData = jsonDecode(response.body) as Map; + final reasons = errorData['reasons'] as List; + final reason = reasons.isNotEmpty + ? reasons.first['description'] ?? '' + : response.body; + throw Exception(reason); + } catch (e) { + _core.logger.e('[$runtimeType] getActivity, decode result error => $e'); + rethrow; + } + } + T _parseRpcResultAs(String body) { try { final result = Map.from({ diff --git a/packages/reown_appkit/lib/modal/services/blockchain_service/i_blockchain_service.dart b/packages/reown_appkit/lib/modal/services/blockchain_service/i_blockchain_service.dart index 7744360..de97253 100644 --- a/packages/reown_appkit/lib/modal/services/blockchain_service/i_blockchain_service.dart +++ b/packages/reown_appkit/lib/modal/services/blockchain_service/i_blockchain_service.dart @@ -1,4 +1,5 @@ import 'package:reown_appkit/modal/services/blockchain_service/models/blockchain_identity.dart'; +import 'package:reown_appkit/modal/services/blockchain_service/models/wallet_activity.dart'; abstract class IBlockChainService { Future init(); @@ -11,4 +12,9 @@ abstract class IBlockChainService { required String namespace, required String chainId, }); + + Future getActivity({ + required String address, + String? cursor, + }); } diff --git a/packages/reown_appkit/lib/modal/services/blockchain_service/models/wallet_activity.dart b/packages/reown_appkit/lib/modal/services/blockchain_service/models/wallet_activity.dart new file mode 100644 index 0000000..6f1379b --- /dev/null +++ b/packages/reown_appkit/lib/modal/services/blockchain_service/models/wallet_activity.dart @@ -0,0 +1,363 @@ +import 'dart:convert'; + +class ActivityData { + final List? data; + final String? next; + + ActivityData({ + this.data, + this.next, + }); + + factory ActivityData.fromRawJson(String str) => + ActivityData.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory ActivityData.fromJson(Map json) => ActivityData( + data: json['data'] == null + ? [] + : List.from( + json['data']!.map((x) => Activity.fromJson(x))), + next: json['next'] as String?, + ); + + Map toJson() => { + 'data': data == null + ? [] + : List.from(data!.map((x) => x.toJson())), + 'next': next, + }; +} + +class Activity { + final String? id; + final Metadata? metadata; + final List? transfers; + + Activity({ + this.id, + this.metadata, + this.transfers, + }); + + factory Activity.fromRawJson(String str) => + Activity.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Activity.fromJson(Map json) => Activity( + id: json['id'], + metadata: json['metadata'] == null + ? null + : Metadata.fromJson(json['metadata']), + transfers: json['transfers'] == null + ? [] + : List.from( + json['transfers']!.map((x) => Transfer.fromJson(x))), + ); + + Map toJson() => { + 'id': id, + 'metadata': metadata?.toJson(), + 'transfers': transfers == null + ? [] + : List.from(transfers!.map((x) => x.toJson())), + }; +} + +class Metadata { + final String? operationType; + final String? hash; + final DateTime? minedAt; + final String? sentFrom; + final String? sentTo; + final String? status; + final int? nonce; + final Application? application; + final String? chain; + + Metadata({ + this.operationType, + this.hash, + this.minedAt, + this.sentFrom, + this.sentTo, + this.status, + this.nonce, + this.application, + this.chain, + }); + + factory Metadata.fromRawJson(String str) => + Metadata.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Metadata.fromJson(Map json) => Metadata( + operationType: json['operationType'], + hash: json['hash'], + minedAt: + json['minedAt'] == null ? null : DateTime.parse(json['minedAt']), + sentFrom: json['sentFrom'], + sentTo: json['sentTo'], + status: json['status'], + nonce: json['nonce'], + application: json['application'] == null + ? null + : Application.fromJson(json['application']), + chain: json['chain'], + ); + + Map toJson() => { + 'operationType': operationType, + 'hash': hash, + 'minedAt': minedAt?.toIso8601String(), + 'sentFrom': sentFrom, + 'sentTo': sentTo, + 'status': status, + 'nonce': nonce, + 'application': application?.toJson(), + 'chain': chain, + }; +} + +class Application { + final String? name; + final String? iconUrl; + + Application({ + this.name, + this.iconUrl, + }); + + factory Application.fromRawJson(String str) => + Application.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Application.fromJson(Map json) => Application( + name: json['name'], + iconUrl: json['iconUrl'], + ); + + Map toJson() => { + 'name': name, + 'iconUrl': iconUrl, + }; +} + +class Transfer { + final FungibleInfo? fungibleInfo; + final NftInfo? nftInfo; + final String? direction; + final Quantity? quantity; + final double? value; + final double? price; + + Transfer({ + this.fungibleInfo, + this.nftInfo, + this.direction, + this.quantity, + this.value, + this.price, + }); + + factory Transfer.fromRawJson(String str) => + Transfer.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Transfer.fromJson(Map json) => Transfer( + fungibleInfo: json['fungible_info'] == null + ? null + : FungibleInfo.fromJson(json['fungible_info']), + nftInfo: json['nft_info'] == null + ? null + : NftInfo.fromJson(json['nft_info']), + direction: json['direction'], + quantity: json['quantity'] == null + ? null + : Quantity.fromJson(json['quantity']), + value: json['value']?.toDouble(), + price: json['price']?.toDouble(), + ); + + Map toJson() => { + 'fungible_info': fungibleInfo?.toJson(), + 'nft_info': nftInfo?.toJson(), + 'direction': direction, + 'quantity': quantity?.toJson(), + 'value': value, + 'price': price, + }; +} + +class FungibleInfo { + final String? name; + final String? symbol; + final Icon? icon; + + FungibleInfo({ + this.name, + this.symbol, + this.icon, + }); + + factory FungibleInfo.fromRawJson(String str) => + FungibleInfo.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory FungibleInfo.fromJson(Map json) => FungibleInfo( + name: json['name'], + symbol: json['symbol'], + icon: json['icon'] == null ? null : Icon.fromJson(json['icon']), + ); + + Map toJson() => { + 'name': name, + 'symbol': symbol, + 'icon': icon?.toJson(), + }; +} + +class Icon { + final String? url; + + Icon({ + this.url, + }); + + factory Icon.fromRawJson(String str) => Icon.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Icon.fromJson(Map json) => Icon( + url: json['url'], + ); + + Map toJson() => { + 'url': url, + }; +} + +class NftInfo { + final String? name; + final Content? content; + final Flags? flags; + + NftInfo({ + this.name, + this.content, + this.flags, + }); + + factory NftInfo.fromRawJson(String str) => NftInfo.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory NftInfo.fromJson(Map json) => NftInfo( + name: json['name'], + content: + json['content'] == null ? null : Content.fromJson(json['content']), + flags: json['flags'] == null ? null : Flags.fromJson(json['flags']), + ); + + Map toJson() => { + 'name': name, + 'content': content?.toJson(), + 'flags': flags?.toJson(), + }; +} + +class Content { + final Detail? preview; + final Detail? detail; + + Content({ + this.preview, + this.detail, + }); + + factory Content.fromRawJson(String str) => Content.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Content.fromJson(Map json) => Content( + preview: + json['preview'] == null ? null : Detail.fromJson(json['preview']), + detail: json['detail'] == null ? null : Detail.fromJson(json['detail']), + ); + + Map toJson() => { + 'preview': preview?.toJson(), + 'detail': detail?.toJson(), + }; +} + +class Detail { + final String? url; + final dynamic contentType; + + Detail({ + this.url, + this.contentType, + }); + + factory Detail.fromRawJson(String str) => Detail.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Detail.fromJson(Map json) => Detail( + url: json['url'], + contentType: json['content_type'], + ); + + Map toJson() => { + 'url': url, + 'content_type': contentType, + }; +} + +class Flags { + final bool? isSpam; + + Flags({ + this.isSpam, + }); + + factory Flags.fromRawJson(String str) => Flags.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Flags.fromJson(Map json) => Flags( + isSpam: json['is_spam'], + ); + + Map toJson() => { + 'is_spam': isSpam, + }; +} + +class Quantity { + final String? numeric; + + Quantity({ + this.numeric, + }); + + factory Quantity.fromRawJson(String str) => + Quantity.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Quantity.fromJson(Map json) => Quantity( + numeric: json['numeric'], + ); + + Map toJson() => { + 'numeric': numeric, + }; +} diff --git a/packages/reown_appkit/lib/modal/utils/core_utils.dart b/packages/reown_appkit/lib/modal/utils/core_utils.dart index a7c1453..3722982 100644 --- a/packages/reown_appkit/lib/modal/utils/core_utils.dart +++ b/packages/reown_appkit/lib/modal/utils/core_utils.dart @@ -89,8 +89,18 @@ class CoreUtils { if (chainBalance == 0.0) { return '0.'.padRight(precision + 2, '0'); } - return chainBalance.toStringAsPrecision(precision) - ..replaceAll(RegExp(r'([.]*0+)(?!.*\d)'), ''); + + if (chainBalance.toInt() <= 0) { + return chainBalance.toStringAsPrecision(precision) + ..replaceAll(RegExp(r'([.]*0+)(?!.*\d)'), ''); + } + + return chainBalance.toStringAsFixed(2); + } + + static String formatStringBalance(String stringValue) { + final value = double.tryParse(stringValue) ?? double.parse('0'); + return formatChainBalance(value); } static String getUserAgent() { diff --git a/packages/reown_appkit/lib/modal/widgets/lists/activity_item.dart b/packages/reown_appkit/lib/modal/widgets/lists/activity_item.dart new file mode 100644 index 0000000..9bb0b56 --- /dev/null +++ b/packages/reown_appkit/lib/modal/widgets/lists/activity_item.dart @@ -0,0 +1,448 @@ +import 'dart:ui' as ui; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:intl/intl.dart'; +import 'package:reown_appkit/modal/services/blockchain_service/models/wallet_activity.dart'; +import 'package:reown_appkit/modal/services/explorer_service/i_explorer_service.dart'; +import 'package:reown_appkit/modal/theme/public/appkit_modal_theme.dart'; +import 'package:reown_appkit/modal/utils/core_utils.dart'; +import 'package:reown_appkit/modal/utils/public/appkit_modal_default_networks.dart'; +import 'package:reown_appkit/modal/utils/render_utils.dart'; +import 'package:reown_appkit/modal/widgets/icons/rounded_icon.dart'; +import 'package:reown_appkit/modal/widgets/lists/list_items/base_list_item.dart'; +import 'package:reown_appkit/modal/widgets/modal_provider.dart'; + +class ActivityListItem extends StatelessWidget { + const ActivityListItem({ + super.key, + required this.activity, + this.onTap, + }); + + final Activity activity; + final VoidCallback? onTap; + + Transfer? get _nft { + final transfers = activity.transfers ?? []; + return transfers.firstWhereOrNull((e) => e.nftInfo != null); + } + + bool get _isNFT => _nft != null; + + @override + Widget build(BuildContext context) { + final themeData = ReownAppKitModalTheme.getDataOf(context); + final themeColors = ReownAppKitModalTheme.colorsOf(context); + final appKitModal = ModalProvider.of(context).instance; + final imageId = ReownAppKitModalNetworks.getNetworkIconId( + appKitModal.selectedChain!.chainId, + ); + final tokenImage = GetIt.I().getAssetImageUrl( + imageId, + ); + // + final operationType = OperationType.values.firstWhere( + (e) => + e.toString() == 'OperationType.${activity.metadata!.operationType}', + ); + // + final dateFormatter = DateFormat('d MMM'); + final minedAt = dateFormatter.format(activity.metadata!.minedAt!); + final confirmed = activity.metadata?.status == 'confirmed'; + // + final transfers = activity.transfers ?? []; + final leftIcon = _leftIconImage(transfers); + final rightIcon = _rightIconImage(transfers); + final stops = _iconsStops(leftIcon, rightIcon); + // + return BaseListItem( + onTap: onTap, + backgroundColor: MaterialStateProperty.all( + themeColors.background125, + ), + padding: const EdgeInsets.all(0.0), + child: Row( + children: [ + const SizedBox.square(dimension: 8.0), + Stack( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 8.0, + right: 8.0, + bottom: 8.0, + ), + child: AspectRatio( + aspectRatio: 1.0, + child: Stack( + children: [ + // left-side icon + Positioned( + top: 0, + left: 0, + bottom: 0, + right: 0, + child: _HalfIconImage( + imageUrl: leftIcon, + isLeft: true, + isNFT: _isNFT, + stops: stops, + // placeHolder: + // transfers.first.fungibleInfo?.symbol ?? '', + ), + ), + // right-side icon + Positioned( + top: 0, + right: 0, + bottom: 0, + left: 0, + child: _HalfIconImage( + imageUrl: rightIcon, + isLeft: false, + isNFT: _isNFT, + stops: stops, + // placeHolder: + // transfers.first.fungibleInfo?.symbol ?? '', + ), + ), + ], + ), + ), + ), + Positioned( + bottom: 6, + right: 6, + child: Container( + decoration: BoxDecoration( + color: themeColors.background150, + borderRadius: BorderRadius.all(Radius.circular(30.0)), + ), + padding: const EdgeInsets.all(1.0), + clipBehavior: Clip.antiAlias, + child: RoundedIcon( + imageUrl: tokenImage, + padding: 2.0, + size: 15.0, + ), + ), + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 4.0, right: 8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + WidgetSpan( + child: RoundedIcon( + assetPath: operationType.icon, + assetColor: confirmed + ? themeColors.success100 + : themeColors.foreground200, + circleColor: confirmed + ? themeColors.success100.withOpacity(0.1) + : themeColors.grayGlass010, + borderColor: confirmed + ? themeColors.success100.withOpacity(0.15) + : themeColors.background150, + padding: 4.0, + size: 20.0, + ), + alignment: ui.PlaceholderAlignment.middle, + ), + TextSpan( + text: ' ${operationType.name}', + style: themeData.textStyles.paragraph500.copyWith( + height: 1.3, + color: themeColors.foreground100, + ), + ), + ], + ), + ), + const SizedBox.square(dimension: 2.0), + Text( + _subtitle(operationType), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: themeData.textStyles.small400.copyWith( + height: 1.3, + color: themeColors.foreground200, + ), + ), + ], + ), + ), + ), + Text( + minedAt.toUpperCase(), + style: themeData.textStyles.micro700.copyWith( + color: themeColors.foreground300, + ), + ), + const SizedBox.square(dimension: 8.0), + ], + ), + ); + } + + String _subtitle(OperationType operationType) { + // execute, send, trade, receive, mint + if (_isNFT) { + return '${_nft!.nftInfo?.name ?? ''} → ' + '${RenderUtils.truncate(activity.metadata?.sentTo ?? '')}'; + } + // + final transfers = activity.transfers ?? []; + final transferValue1 = transfers.isNotEmpty + ? CoreUtils.formatStringBalance(transfers.first.quantity?.numeric ?? '') + : null; + final transferSymbol1 = + transfers.isNotEmpty ? transfers.first.fungibleInfo?.symbol : null; + // + final transferValue2 = transfers.isNotEmpty + ? CoreUtils.formatStringBalance(transfers.last.quantity?.numeric ?? '') + : null; + final transferSymbol2 = + transfers.isNotEmpty ? transfers.last.fungibleInfo?.symbol : null; + // + switch (operationType) { + case OperationType.execute: + return '$transferValue1 $transferSymbol1'; + case OperationType.trade: + return '$transferValue1 $transferSymbol1 → ' + '$transferValue2 $transferSymbol2'; + case OperationType.send: + return '$transferValue1 $transferSymbol1 → ' + '${RenderUtils.truncate(activity.metadata?.sentTo ?? '')}'; + case OperationType.receive: + return '$transferValue1 $transferSymbol1 ← ' + '${RenderUtils.truncate(activity.metadata?.sentFrom ?? '')}'; + case OperationType.mint: + return '$transferValue1 $transferSymbol1'; + } + } + + String? _leftIconImage(List transfers) { + if (_isNFT) { + return _nft!.nftInfo?.content?.preview?.url; + } + if (transfers.isNotEmpty) { + return transfers.first.fungibleInfo?.icon?.url; + } + return null; + } + + String? _rightIconImage(List transfers) { + if (_isNFT) { + return _nft!.nftInfo?.content?.preview?.url; + } + if (transfers.isNotEmpty) { + return transfers.last.fungibleInfo?.icon?.url; + } + return null; + } + + List _iconsStops(String? leftIcon, String? rightIcon) { + return leftIcon == null + ? [1.0, 1.0] + : (rightIcon == null || leftIcon == rightIcon) + ? [0.5, 0.5] + : [0.47, 0.47]; + } +} + +class ActivityListItemLoader extends StatelessWidget { + @override + Widget build(BuildContext context) { + final themeColors = ReownAppKitModalTheme.colorsOf(context); + // + return BaseListItem( + backgroundColor: MaterialStateProperty.all( + Colors.transparent, + ), + padding: const EdgeInsets.all(0.0), + child: Row( + children: [ + const SizedBox.square(dimension: 8.0), + Padding( + padding: const EdgeInsets.only( + top: 8.0, + right: 8.0, + bottom: 8.0, + ), + child: AspectRatio( + aspectRatio: 1.0, + child: CircleAvatar( + backgroundColor: themeColors.grayGlass010, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 4.0, right: 8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircleAvatar( + radius: 12.0, + backgroundColor: themeColors.grayGlass010, + ), + Expanded( + child: Container( + height: 16.0, + margin: const EdgeInsets.only(left: 4.0), + decoration: BoxDecoration( + color: themeColors.grayGlass010, + borderRadius: + BorderRadius.all(Radius.circular(30.0)), + ), + child: SizedBox(), + ), + ), + ], + ), + Container( + width: double.infinity, + height: 16.0, + margin: const EdgeInsets.only(top: 4.0), + decoration: BoxDecoration( + color: themeColors.grayGlass010, + borderRadius: BorderRadius.all(Radius.circular(30.0)), + ), + child: SizedBox(), + ), + ], + ), + ), + ), + Container( + width: 36.0, + height: 14.0, + margin: const EdgeInsets.only(left: 4.0), + decoration: BoxDecoration( + color: themeColors.grayGlass010, + borderRadius: BorderRadius.all(Radius.circular(30.0)), + ), + child: SizedBox(), + ), + const SizedBox.square(dimension: 8.0), + ], + ), + ); + } +} + +class _HalfIconImage extends StatelessWidget { + const _HalfIconImage({ + required this.imageUrl, + required this.isNFT, + required this.stops, + required this.isLeft, + // required this.placeHolder, + }); + final String? imageUrl; + final bool isNFT, isLeft; + final List stops; + // final String placeHolder; + + @override + Widget build(BuildContext context) { + final themeColors = ReownAppKitModalTheme.colorsOf(context); + return ShaderMask( + shaderCallback: (bounds) { + return LinearGradient( + begin: isLeft ? Alignment.centerLeft : Alignment.centerRight, + end: isLeft ? Alignment.centerRight : Alignment.centerLeft, + colors: [Colors.white, Colors.transparent], + stops: stops, + ).createShader(bounds); + }, + blendMode: BlendMode.dstIn, + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: isLeft + ? (isNFT ? Radius.circular(8.0) : Radius.circular(100.0)) + : Radius.zero, + bottomLeft: isLeft + ? (isNFT ? Radius.circular(8.0) : Radius.circular(100.0)) + : Radius.zero, + topRight: !isLeft + ? (isNFT ? Radius.circular(8.0) : Radius.circular(100.0)) + : Radius.zero, + bottomRight: !isLeft + ? (isNFT ? Radius.circular(8.0) : Radius.circular(100.0)) + : Radius.zero, + ), + child: CachedNetworkImage( + imageUrl: imageUrl ?? '', + fit: BoxFit.cover, + fadeInDuration: const Duration(milliseconds: 500), + fadeOutDuration: const Duration(milliseconds: 500), + errorWidget: (context, url, error) => Padding( + padding: const EdgeInsets.all(2.0), + child: RoundedIcon( + size: 20.0, + circleColor: themeColors.background275, + borderColor: themeColors.background275, + assetColor: themeColors.foreground200, + padding: 10.0, + ), + ), + ), + ), + ); + } +} + +enum OperationType { + execute, + send, + trade, + receive, + mint; + + String get name { + switch (this) { + case execute: + return 'Executed'; + case send: + return 'Sent'; + case trade: + return 'Swapped'; + case receive: + return 'Received'; + case mint: + return 'Minted'; + } + } + + String get icon { + switch (this) { + case execute: + return 'lib/modal/assets/icons/checkmark.svg'; + case send: + return 'lib/modal/assets/icons/send.svg'; + case trade: + return 'lib/modal/assets/icons/swap_horizontal.svg'; + case receive: + return 'lib/modal/assets/icons/receive.svg'; + case mint: + return 'lib/modal/assets/icons/swap_vertical.svg'; + } + } +} diff --git a/packages/reown_appkit/lib/modal/widgets/lists/list_items/base_list_item.dart b/packages/reown_appkit/lib/modal/widgets/lists/list_items/base_list_item.dart index 1ef4cf4..d8c5310 100644 --- a/packages/reown_appkit/lib/modal/widgets/lists/list_items/base_list_item.dart +++ b/packages/reown_appkit/lib/modal/widgets/lists/list_items/base_list_item.dart @@ -11,6 +11,7 @@ class BaseListItem extends StatelessWidget { this.padding, this.hightlighted = false, this.flexible = false, + this.backgroundColor, }); final Widget? trailing; final VoidCallback? onTap; @@ -18,6 +19,7 @@ class BaseListItem extends StatelessWidget { final EdgeInsets? padding; final bool hightlighted; final bool flexible; + final MaterialStateProperty? backgroundColor; @override Widget build(BuildContext context) { @@ -36,10 +38,13 @@ class BaseListItem extends StatelessWidget { const Size(1000.0, kListItemHeight), ) : null, - backgroundColor: WidgetStateProperty.all( - hightlighted ? themeColors.accenGlass015 : themeColors.grayGlass002, - ), - overlayColor: WidgetStateProperty.all( + backgroundColor: backgroundColor ?? + MaterialStateProperty.all( + hightlighted + ? themeColors.accenGlass015 + : themeColors.grayGlass002, + ), + overlayColor: MaterialStateProperty.all( themeColors.grayGlass005, ), shape: WidgetStateProperty.all( diff --git a/packages/reown_appkit/pubspec.yaml b/packages/reown_appkit/pubspec.yaml index ec9cfab..d242b90 100644 --- a/packages/reown_appkit/pubspec.yaml +++ b/packages/reown_appkit/pubspec.yaml @@ -19,11 +19,12 @@ dependencies: event: ^3.1.0 flutter: sdk: flutter - flutter_svg: ^2.0.16 - freezed_annotation: ^2.4.4 - get_it: ^8.0.3 - http: ^1.2.2 - json_annotation: ^4.9.0 + flutter_svg: ^2.0.10+1 + freezed_annotation: ^2.4.1 + get_it: ^8.0.0 + http: ^1.1.2 + intl: ^0.19.0 + json_annotation: ^4.8.1 plugin_platform_interface: ^2.1.8 qr_flutter_wc: ^0.0.3 reown_core: ^1.1.0-beta02 From 582a618714e65c167e71c3fe2e34f20512869ce4 Mon Sep 17 00:00:00 2001 From: Alfreedom <00tango.bromine@icloud.com> Date: Fri, 15 Nov 2024 15:46:11 +0100 Subject: [PATCH 2/6] wallet features --- .../lib/modal/appkit_modal_impl.dart | 43 +- .../assets/icons/arrow_bottom_circle.svg | 3 + .../lib/modal/assets/icons/arrow_down.svg | 3 + .../lib/modal/assets/icons/chevron_down.svg | 3 + .../lib/modal/assets/icons/paperplane.svg | 3 + .../lib/modal/constants/key_constants.dart | 8 +- .../lib/modal/i_appkit_modal_impl.dart | 3 +- .../models/public/appkit_network_info.dart | 4 +- .../lib/modal/models/send_data.dart | 35 ++ .../lib/modal/pages/account_page.dart | 51 +- .../lib/modal/pages/activity_page.dart | 213 ++++--- .../lib/modal/pages/connect_wallet_page.dart | 22 +- .../lib/modal/pages/preview_send_page.dart | 594 ++++++++++++++++++ .../lib/modal/pages/receive_page.dart | 103 +++ .../lib/modal/pages/select_token_page.dart | 150 +++++ .../lib/modal/pages/send_page.dart | 433 +++++++++++++ .../lib/modal/pages/smart_account_page.dart | 450 +++++++++++++ .../blockchain_service.dart | 299 +++++++-- .../i_blockchain_service.dart | 43 +- .../blockchain_service/models/gas_price.dart | 36 ++ .../models/token_balance.dart | 77 +++ .../lib/modal/utils/core_utils.dart | 13 +- .../modal/widgets/avatars/account_avatar.dart | 4 +- .../modal/widgets/avatars/account_orb.dart | 11 +- .../modal/widgets/buttons/address_button.dart | 71 ++- .../widgets/buttons/address_copy_button.dart | 14 +- .../modal/widgets/buttons/base_button.dart | 4 +- .../modal/widgets/buttons/connect_button.dart | 13 +- .../modal/widgets/buttons/network_button.dart | 176 ++++-- .../widgets/buttons/secondary_button.dart | 6 +- .../widgets/buttons/simple_icon_button.dart | 39 +- .../lib/modal/widgets/circular_loader.dart | 2 +- .../lib/modal/widgets/icons/rounded_icon.dart | 7 +- .../lists/list_items/account_list_item.dart | 3 + .../widgets/miscellaneous/searchbar.dart | 42 +- .../miscellaneous/segmented_control.dart | 76 ++- .../navigation/navbar_action_button.dart | 37 +- .../public/appkit_modal_account_button.dart | 17 +- .../public/appkit_modal_address_button.dart | 81 ++- .../public/appkit_modal_balance_button.dart | 18 +- .../modal/widgets/text/appkit_address.dart | 27 +- .../modal/widgets/text/appkit_balance.dart | 33 +- 42 files changed, 2801 insertions(+), 469 deletions(-) create mode 100644 packages/reown_appkit/lib/modal/assets/icons/arrow_bottom_circle.svg create mode 100644 packages/reown_appkit/lib/modal/assets/icons/arrow_down.svg create mode 100644 packages/reown_appkit/lib/modal/assets/icons/chevron_down.svg create mode 100644 packages/reown_appkit/lib/modal/assets/icons/paperplane.svg create mode 100644 packages/reown_appkit/lib/modal/models/send_data.dart create mode 100644 packages/reown_appkit/lib/modal/pages/preview_send_page.dart create mode 100644 packages/reown_appkit/lib/modal/pages/receive_page.dart create mode 100644 packages/reown_appkit/lib/modal/pages/select_token_page.dart create mode 100644 packages/reown_appkit/lib/modal/pages/send_page.dart create mode 100644 packages/reown_appkit/lib/modal/pages/smart_account_page.dart create mode 100644 packages/reown_appkit/lib/modal/services/blockchain_service/models/gas_price.dart create mode 100644 packages/reown_appkit/lib/modal/services/blockchain_service/models/token_balance.dart diff --git a/packages/reown_appkit/lib/modal/appkit_modal_impl.dart b/packages/reown_appkit/lib/modal/appkit_modal_impl.dart index 473e9a7..469f308 100644 --- a/packages/reown_appkit/lib/modal/appkit_modal_impl.dart +++ b/packages/reown_appkit/lib/modal/appkit_modal_impl.dart @@ -5,7 +5,9 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get_it/get_it.dart'; +import 'package:reown_appkit/modal/pages/smart_account_page.dart'; import 'package:reown_appkit/modal/services/analytics_service/i_analytics_service.dart'; +import 'package:reown_appkit/modal/services/blockchain_service/models/blockchain_identity.dart'; import 'package:reown_appkit/modal/services/explorer_service/i_explorer_service.dart'; import 'package:reown_appkit/modal/services/network_service/i_network_service.dart'; import 'package:reown_appkit/modal/services/siwe_service/i_siwe_service.dart'; @@ -94,15 +96,15 @@ class ReownAppKitModal @override IReownAppKit? get appKit => _appKit; - String? _avatarUrl; + BlockchainIdentity? _blockchainIdentity; @override - String? get avatarUrl => _avatarUrl; + BlockchainIdentity? get blockchainIdentity => _blockchainIdentity; - double? _chainBalance; @Deprecated('Use balanceNotifier') @override String get chainBalance => CoreUtils.formatChainBalance(_chainBalance); + double? _chainBalance; @override final balanceNotifier = ValueNotifier('-.--'); @@ -490,8 +492,9 @@ class ReownAppKitModal return; } + _chainBalance = null; + try { - _chainBalance = null; final formattedBalance = CoreUtils.formatChainBalance(_chainBalance); balanceNotifier.value = '$formattedBalance ${chainInfo.currency}'; @@ -635,14 +638,17 @@ class ReownAppKitModal @override Future openModalView([Widget? startWidget]) { final keyString = startWidget?.key?.toString() ?? ''; + final isMagic = _currentSession?.sessionService.isMagic == true; if (_isConnected) { final connectedKeys = _allowedScreensWhenConnected.map((e) => e.toString()).toList(); if (startWidget == null) { - startWidget = const AccountPage(); + startWidget = + isMagic ? const SmartAccountPage() : const EOAccountPage(); } else { if (!connectedKeys.contains(keyString)) { - startWidget = const AccountPage(); + startWidget = + isMagic ? const SmartAccountPage() : const EOAccountPage(); } } } else { @@ -659,7 +665,8 @@ class ReownAppKitModal KeyConstants.approveTransactionPage, KeyConstants.confirmEmailPage, KeyConstants.selectNetworkPage, - KeyConstants.accountPage, + KeyConstants.eoAccountPage, + KeyConstants.smartAccountPage, KeyConstants.socialLoginPage, ]; @@ -706,7 +713,8 @@ class ReownAppKitModal Widget? showWidget = startWidget; if (_isConnected && showWidget == null) { - showWidget = const AccountPage(); + final isMagic = _currentSession?.sessionService.isMagic == true; + startWidget = isMagic ? const SmartAccountPage() : const EOAccountPage(); } final childWidget = theme == null @@ -1461,7 +1469,7 @@ class ReownAppKitModal ); try { - _chainBalance = await _blockchainService.getBalance( + _chainBalance = await _blockchainService.getTokenBalance( address: _currentSession!.getAddress(namespace)!, namespace: namespace, chainId: _currentSelectedChainId!, @@ -1480,10 +1488,13 @@ class ReownAppKitModal if (namespace == NetworkUtils.eip155) { // Get the avatar, each chainId is just a number in string form. try { + final address = _currentSession!.getAddress(namespace)!; final blockchainId = await _blockchainService.getIdentity( - _currentSession!.getAddress(namespace)!, + address: address, + ); + _blockchainIdentity = BlockchainIdentity.fromJson( + blockchainId.toJson(), ); - _avatarUrl = blockchainId.avatar; } catch (_) {} } _notify(); @@ -1647,6 +1658,7 @@ class ReownAppKitModal _lastChainEmitted = null; _supportsOneClickAuth = false; _status = ReownAppKitModalStatus.initialized; + _blockchainService.dispose(); _notify(); } @@ -1856,12 +1868,15 @@ extension _EmailConnectorExtension on ReownAppKitModal { } } - Future _onMagicErrorEvent(MagicErrorEvent? args) async { - _appKit.core.logger.d('[$runtimeType] _onMagicErrorEvent: $args'); - final errorMessage = args?.error ?? 'Something went wrong'; + Future _onMagicErrorEvent(MagicErrorEvent? event) async { + _appKit.core.logger.d('[$runtimeType] _onMagicErrorEvent: ${event?.error}'); + final errorMessage = event?.error ?? 'Something went wrong'; if (!errorMessage.toLowerCase().contains('user denied')) { onModalError.broadcast(ModalError(errorMessage)); } + if (event is IsConnectedErrorEvent && _currentSession != null) { + await _cleanSession(); + } _notify(); } diff --git a/packages/reown_appkit/lib/modal/assets/icons/arrow_bottom_circle.svg b/packages/reown_appkit/lib/modal/assets/icons/arrow_bottom_circle.svg new file mode 100644 index 0000000..798cc5b --- /dev/null +++ b/packages/reown_appkit/lib/modal/assets/icons/arrow_bottom_circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/reown_appkit/lib/modal/assets/icons/arrow_down.svg b/packages/reown_appkit/lib/modal/assets/icons/arrow_down.svg new file mode 100644 index 0000000..c14cde5 --- /dev/null +++ b/packages/reown_appkit/lib/modal/assets/icons/arrow_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/reown_appkit/lib/modal/assets/icons/chevron_down.svg b/packages/reown_appkit/lib/modal/assets/icons/chevron_down.svg new file mode 100644 index 0000000..c12e1dd --- /dev/null +++ b/packages/reown_appkit/lib/modal/assets/icons/chevron_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/reown_appkit/lib/modal/assets/icons/paperplane.svg b/packages/reown_appkit/lib/modal/assets/icons/paperplane.svg new file mode 100644 index 0000000..541f5fb --- /dev/null +++ b/packages/reown_appkit/lib/modal/assets/icons/paperplane.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/reown_appkit/lib/modal/constants/key_constants.dart b/packages/reown_appkit/lib/modal/constants/key_constants.dart index 0735996..4b9ea91 100644 --- a/packages/reown_appkit/lib/modal/constants/key_constants.dart +++ b/packages/reown_appkit/lib/modal/constants/key_constants.dart @@ -4,7 +4,10 @@ import 'package:flutter/material.dart'; class KeyConstants { static const Key selectNetworkPage = Key('selectNetworkPage'); - static const Key accountPage = Key('accountPage'); + static const Key eoAccountPage = Key('eoAccountPage'); + static const Key smartAccountPage = Key('smartAccountPage'); + static const Key selectTokenPage = Key('selectTokenPage'); + static const Key embeddedWalletPage = Key('embeddedWalletPage'); static const Key addressCopyButton = Key('addressCopyButton'); static const Key disconnectButton = Key('disconnectButton'); static const Key w3mAccountButton = Key('w3mAccountButton'); @@ -13,6 +16,9 @@ class KeyConstants { static const Key upgradeWalletPage = Key('upgradeWalletPage'); static const Key helpPageKey = Key('helpPageKey'); static const Key qrCodePageKey = Key('qrCodePageKey'); + static const Key receivePageKey = Key('receivePageKey'); + static const Key sendPageKey = Key('sendPageKey'); + static const Key previewSendPageKey = Key('previewSendPageKey'); static const Key farcasterQrCodePageKey = Key('farcasterQrCodePageKey'); static const Key walletListShortPageKey = Key('walletListShortPageKey'); static const Key allSocialLoginPageKey = Key('allSocialLoginPageKey'); diff --git a/packages/reown_appkit/lib/modal/i_appkit_modal_impl.dart b/packages/reown_appkit/lib/modal/i_appkit_modal_impl.dart index d1db8e7..716b68e 100644 --- a/packages/reown_appkit/lib/modal/i_appkit_modal_impl.dart +++ b/packages/reown_appkit/lib/modal/i_appkit_modal_impl.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:reown_appkit/modal/services/blockchain_service/models/blockchain_identity.dart'; import 'package:reown_appkit/reown_appkit.dart'; enum ReownAppKitModalStatus { @@ -45,7 +46,7 @@ abstract class IReownAppKitModal with ChangeNotifier { /// The url to the account's avatar image. /// Pass this into a [Image.network] and it will load the avatar image. - String? get avatarUrl; + BlockchainIdentity? get blockchainIdentity; /// Returns the balance of the currently connected wallet on the selected chain. @Deprecated('Use balanceNotifier') diff --git a/packages/reown_appkit/lib/modal/models/public/appkit_network_info.dart b/packages/reown_appkit/lib/modal/models/public/appkit_network_info.dart index 297d975..bc921dd 100644 --- a/packages/reown_appkit/lib/modal/models/public/appkit_network_info.dart +++ b/packages/reown_appkit/lib/modal/models/public/appkit_network_info.dart @@ -19,14 +19,14 @@ class ReownAppKitModalNetworkInfo with _$ReownAppKitModalNetworkInfo { extension AppKitNetworkInfoExtension on ReownAppKitModalNetworkInfo { String get chainHexId => '0x${int.parse(chainId).toRadixString(16)}'; - Map toJson() { + Map toJson({int? decimals}) { return { 'chainId': chainHexId, 'chainName': name, 'nativeCurrency': { 'name': currency, 'symbol': currency, - 'decimals': 18, + 'decimals': decimals ?? 18, }, 'rpcUrls': [ rpcUrl, diff --git a/packages/reown_appkit/lib/modal/models/send_data.dart b/packages/reown_appkit/lib/modal/models/send_data.dart new file mode 100644 index 0000000..e78e802 --- /dev/null +++ b/packages/reown_appkit/lib/modal/models/send_data.dart @@ -0,0 +1,35 @@ +import 'dart:convert'; + +class SendData { + final String? amount; + final String? address; + + SendData({ + this.amount, + this.address, + }); + + SendData copyWith({ + String? amount, + String? address, + }) => + SendData( + amount: amount ?? this.amount, + address: address ?? this.address, + ); + + factory SendData.fromRawJson(String str) => + SendData.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory SendData.fromJson(Map json) => SendData( + amount: json['amount'], + address: json['address'], + ); + + Map toJson() => { + 'amount': amount, + 'address': address, + }; +} diff --git a/packages/reown_appkit/lib/modal/pages/account_page.dart b/packages/reown_appkit/lib/modal/pages/account_page.dart index 72664ab..2b54b7c 100644 --- a/packages/reown_appkit/lib/modal/pages/account_page.dart +++ b/packages/reown_appkit/lib/modal/pages/account_page.dart @@ -26,23 +26,24 @@ import 'package:reown_appkit/modal/widgets/lists/list_items/account_list_item.da import 'package:reown_appkit/modal/widgets/text/appkit_balance.dart'; import 'package:reown_appkit/reown_appkit.dart'; -class AccountPage extends StatefulWidget { - const AccountPage() : super(key: KeyConstants.accountPage); +class EOAccountPage extends StatefulWidget { + const EOAccountPage() : super(key: KeyConstants.eoAccountPage); @override - State createState() => _AccountPageState(); + State createState() => _EOAccountPageState(); } -class _AccountPageState extends State with WidgetsBindingObserver { - IReownAppKitModal? _service; +class _EOAccountPageState extends State + with WidgetsBindingObserver { + IReownAppKitModal? _appKitModal; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addPostFrameCallback((_) { - _service = ModalProvider.of(context).instance; - _service?.addListener(_rebuild); + _appKitModal = ModalProvider.of(context).instance; + _appKitModal?.addListener(_rebuild); _rebuild(); }); } @@ -58,14 +59,14 @@ class _AccountPageState extends State with WidgetsBindingObserver { @override void dispose() { - _service?.removeListener(_rebuild); + _appKitModal?.removeListener(_rebuild); WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override Widget build(BuildContext context) { - if (_service == null) { + if (_appKitModal == null) { return ContentLoading(viewHeight: 400.0); } @@ -78,7 +79,7 @@ class _AccountPageState extends State with WidgetsBindingObserver { body: Padding( padding: const EdgeInsets.symmetric(horizontal: kPadding12), child: _DefaultAccountView( - service: _service!, + appKitModal: _appKitModal!, ), ), ); @@ -86,14 +87,15 @@ class _AccountPageState extends State with WidgetsBindingObserver { } class _DefaultAccountView extends StatelessWidget { - const _DefaultAccountView({required IReownAppKitModal service}) - : _service = service; - final IReownAppKitModal _service; + const _DefaultAccountView({required IReownAppKitModal appKitModal}) + : _appKitMoldal = appKitModal; + final IReownAppKitModal _appKitMoldal; @override Widget build(BuildContext context) { + final themeData = ReownAppKitModalTheme.getDataOf(context); final themeColors = ReownAppKitModalTheme.colorsOf(context); - final isEmailLogin = _service.session?.sessionService.isMagic ?? false; + final isEmailLogin = _appKitMoldal.session?.sessionService.isMagic ?? false; return Column( mainAxisSize: MainAxisSize.min, children: [ @@ -102,20 +104,24 @@ class _DefaultAccountView extends StatelessWidget { const Orb(size: 72.0), const SizedBox.square(dimension: kPadding12), const AddressCopyButton(), - const BalanceText(), + BalanceText( + textStyle: themeData.textStyles.paragraph500.copyWith( + color: themeColors.foreground200, + ), + ), Visibility( - visible: _service.selectedChain?.explorerUrl != null, + visible: _appKitMoldal.selectedChain?.explorerUrl != null, child: Padding( padding: const EdgeInsets.only(top: kPadding12), child: SimpleIconButton( - onTap: () => _service.launchBlockExplorer(), + onTap: () => _appKitMoldal.launchBlockExplorer(), leftIcon: 'lib/modal/assets/icons/compass.svg', rightIcon: 'lib/modal/assets/icons/arrow_top_right.svg', title: 'Block Explorer', - backgroundColor: themeColors.background125, + backgroundColor: themeColors.grayGlass002, foregroundColor: themeColors.foreground150, - overlayColor: WidgetStateProperty.all( - themeColors.background200, + overlayColor: MaterialStateProperty.all( + themeColors.grayGlass002, ), ), ), @@ -136,7 +142,10 @@ class _DefaultAccountView extends StatelessWidget { // child: _ConnectedWalletButton(), // ), _SelectNetworkButton(), - _ActivityButton(), + Visibility( + visible: !isEmailLogin, + child: _ActivityButton(), + ), _DisconnectButton(), ], ); diff --git a/packages/reown_appkit/lib/modal/pages/activity_page.dart b/packages/reown_appkit/lib/modal/pages/activity_page.dart index c0e72fd..6d164c8 100644 --- a/packages/reown_appkit/lib/modal/pages/activity_page.dart +++ b/packages/reown_appkit/lib/modal/pages/activity_page.dart @@ -7,6 +7,7 @@ import 'package:reown_appkit/modal/constants/style_constants.dart'; import 'package:reown_appkit/modal/i_appkit_modal_impl.dart'; import 'package:reown_appkit/modal/services/blockchain_service/i_blockchain_service.dart'; import 'package:reown_appkit/modal/services/blockchain_service/models/wallet_activity.dart'; +import 'package:reown_appkit/modal/widgets/icons/rounded_icon.dart'; import 'package:reown_appkit/modal/widgets/lists/activity_item.dart'; import 'package:reown_appkit/modal/widgets/miscellaneous/responsive_container.dart'; import 'package:reown_appkit/modal/widgets/modal_provider.dart'; @@ -21,9 +22,38 @@ class ActivityPage extends StatefulWidget { } class _ActivityPageState extends State { + @override + Widget build(BuildContext context) { + return ModalNavbar( + title: 'Activity', + safeAreaLeft: true, + safeAreaRight: true, + safeAreaBottom: false, + body: Container( + constraints: BoxConstraints( + maxHeight: ResponsiveData.maxHeightOf(context), + ), + padding: const EdgeInsets.symmetric(horizontal: kPadding12), + child: ActivityListViewBuilder( + appKitModal: ModalProvider.of(context).instance, + ), + ), + ); + } +} + +class ActivityListViewBuilder extends StatefulWidget { + ActivityListViewBuilder({required this.appKitModal, super.key}); + final IReownAppKitModal appKitModal; + + @override + State createState() => + _ActivityListViewBuilderState(); +} + +class _ActivityListViewBuilderState extends State { IBlockChainService get _blockchainService => GetIt.I(); - IReownCore get _core => _appKitModal.appKit!.core; - late final IReownAppKitModal _appKitModal; + IReownCore get _core => widget.appKitModal.appKit!.core; final _scrollController = ScrollController(); final List _activities = []; @@ -36,47 +66,62 @@ class _ActivityPageState extends State { @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) async { - _appKitModal = ModalProvider.of(context).instance; - final chainId = _appKitModal.selectedChain!.chainId; - final namespace = ReownAppKitModalNetworks.getNamespaceForChainId( - chainId, - ); - _currentChain = '$namespace:$chainId'; - _currentAddress = _appKitModal.session!.getAddress(namespace)!; - - _scrollController.addListener(() { - if (_scrollController.position.pixels >= - _scrollController.position.maxScrollExtent - 100) { - _loadMoreActivities(); - } - }); + final chainId = widget.appKitModal.selectedChain!.chainId; + final namespace = ReownAppKitModalNetworks.getNamespaceForChainId( + chainId, + ); + _currentChain = '$namespace:$chainId'; + _currentAddress = widget.appKitModal.session!.getAddress(namespace)!; - _fetchActivities(); + _scrollController.addListener(() { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 100) { + _loadMoreActivities(); + } }); + + // cached items + final cachedItems = _blockchainService.activityData?.data ?? []; + if (cachedItems.isNotEmpty) { + final activityList = cachedItems.where((data) { + return data.metadata?.chain == _currentChain && + (data.transfers ?? []).isNotEmpty; + }).toList(); + _activities.addAll(activityList); + } + setState(() {}); + + _fetchActivities(); } Future _fetchActivities() async { - setState(() => _isLoadingActivities = true); + setState(() => _isLoadingActivities = _activities.isEmpty); - // Initial API request here - final activityData = await _blockchainService.getActivity( - address: _currentAddress, - cursor: _currentCursor, - ); - final newItems = activityData.data ?? []; - final activityList = newItems.where((data) { - return data.metadata?.chain == _currentChain && - (data.transfers ?? []).isNotEmpty; - }).toList(); + try { + final activityData = await _blockchainService.getHistory( + address: _currentAddress, + cursor: _currentCursor, + ); + _activities.clear(); + final newItems = activityData.data ?? []; + final activityList = newItems.where((data) { + return data.metadata?.chain == _currentChain && + (data.transfers ?? []).isNotEmpty; + }).toList(); - _activities.addAll(activityList); - _isLoadingActivities = false; - _currentCursor = activityData.next; - _hasMoreActivities = _currentCursor != null; - _core.logger.d( - '[$runtimeType] fetch data, items: ${activityList.length}, cursor: $_currentCursor, _hasMoreActivities: $_hasMoreActivities', - ); + _activities.addAll(activityList); + _isLoadingActivities = false; + _currentCursor = activityData.next; + _hasMoreActivities = _currentCursor != null; + _core.logger.d( + '[$runtimeType] fetch data, items: ${activityList.length}, cursor: $_currentCursor, _hasMoreActivities: $_hasMoreActivities', + ); + } catch (e) { + _isLoadingActivities = false; + widget.appKitModal.onModalError.broadcast(ModalError( + 'Error fetching activity', + )); + } setState(() {}); } @@ -91,54 +136,11 @@ class _ActivityPageState extends State { super.dispose(); } - @override - Widget build(BuildContext context) { - return ModalNavbar( - title: 'Activity', - safeAreaLeft: true, - safeAreaRight: true, - safeAreaBottom: false, - body: Container( - constraints: BoxConstraints( - maxHeight: ((_isLoadingActivities && _activities.isEmpty) || - _activities.length <= 4) - ? 340.0 - : ResponsiveData.maxHeightOf(context), - ), - padding: const EdgeInsets.symmetric(horizontal: kPadding12), - child: _ListBuilder( - scrollController: _scrollController, - items: _activities, - caip2chain: _currentChain, - isLoading: _isLoadingActivities, - ), - ), - ); - } -} - -class _ListBuilder extends StatefulWidget { - const _ListBuilder({ - required this.scrollController, - required this.items, - required this.caip2chain, - required this.isLoading, - }); - final ScrollController scrollController; - final List items; - final String caip2chain; - final bool isLoading; - - @override - State<_ListBuilder> createState() => __ListBuilderState(); -} - -class __ListBuilderState extends State<_ListBuilder> { @override Widget build(BuildContext context) { final themeData = ReownAppKitModalTheme.getDataOf(context); final themeColors = ReownAppKitModalTheme.colorsOf(context); - if (widget.isLoading && widget.items.isEmpty) { + if (_isLoadingActivities && _activities.isEmpty) { final loadingList = [ ActivityListItemLoader(), ActivityListItemLoader(), @@ -167,18 +169,37 @@ class __ListBuilderState extends State<_ListBuilder> { ); } - if (widget.items.isEmpty) { - return Center( - child: Text( - 'No activity found', - style: themeData.textStyles.paragraph500.copyWith( - color: themeColors.foreground100, + if (_activities.isEmpty) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + RoundedIcon( + assetPath: 'lib/modal/assets/icons/swap_horizontal.svg', + assetColor: themeColors.foreground200, + circleColor: themeColors.grayGlass010, + borderColor: themeColors.grayGlass010, + borderRadius: 8.0, ), - ), + const SizedBox.square(dimension: kPadding8), + Text( + 'No activity yet', + style: themeData.textStyles.paragraph500.copyWith( + color: themeColors.foreground100, + ), + ), + Text( + 'Your next transactions will appear here', + style: themeData.textStyles.small400.copyWith( + color: themeColors.foreground200, + ), + ), + const SizedBox.square(dimension: 30.0), + ], ); } - final groupedByYearMonth = groupBy(widget.items, (Activity obj) { + final groupedByYearMonth = groupBy(_activities, (Activity obj) { final monthName = DateFormat.MMMM().format( obj.metadata!.minedAt!, ); @@ -192,10 +213,20 @@ class __ListBuilderState extends State<_ListBuilder> { groupedActivities.addAll(objs.map((obj) => MapEntry('', [obj]))); }); + // final height = groupedActivities.map((entry) { + // if (entry.key.isNotEmpty) { + // // title + // return 28.0; + // } else { + // // transfers + // return kListItemHeight; + // } + // }).reduce((a, b) => a + b) + + // 30.0; return ListView.builder( - controller: widget.scrollController, + controller: _scrollController, // Extra space for loading indicator - itemCount: groupedActivities.length + (widget.isLoading ? 1 : 0), + itemCount: groupedActivities.length + (_isLoadingActivities ? 1 : 0), padding: const EdgeInsets.only(bottom: 30.0), itemBuilder: (_, int index) { if (index == groupedActivities.length) { @@ -235,7 +266,7 @@ class __ListBuilderState extends State<_ListBuilder> { List _removeNFTsFromTransfers(List activities) { final activityList = activities .where((data) { - return data.metadata?.chain == widget.caip2chain && + return data.metadata?.chain == _currentChain && (data.transfers ?? []).isNotEmpty; }) .toList() diff --git a/packages/reown_appkit/lib/modal/pages/connect_wallet_page.dart b/packages/reown_appkit/lib/modal/pages/connect_wallet_page.dart index 54e7bfa..5ca9511 100644 --- a/packages/reown_appkit/lib/modal/pages/connect_wallet_page.dart +++ b/packages/reown_appkit/lib/modal/pages/connect_wallet_page.dart @@ -37,7 +37,7 @@ class _ConnectWalletPageState extends State ISiweService get _siweService => GetIt.I(); IReownAppKitModal? _service; - SegmentOption _selectedSegment = SegmentOption.mobile; + SegmentOption _selectedSegment = SegmentOption.option1; ModalError? errorEvent; @override @@ -157,7 +157,7 @@ class _ConnectWalletPageState extends State ), ) : errorEvent is WalletNotInstalled && - _selectedSegment == SegmentOption.mobile + _selectedSegment == SegmentOption.option1 ? Text( 'App not installed', textAlign: TextAlign.center, @@ -185,11 +185,11 @@ class _ConnectWalletPageState extends State ), ) : errorEvent is WalletNotInstalled && - _selectedSegment == SegmentOption.mobile + _selectedSegment == SegmentOption.option1 ? SizedBox.shrink() : Text( webOnlyWallet || - _selectedSegment == SegmentOption.browser + _selectedSegment == SegmentOption.option2 ? 'Open and continue in a new browser tab' : 'Accept connection request in the wallet', textAlign: TextAlign.center, @@ -200,7 +200,7 @@ class _ConnectWalletPageState extends State const SizedBox.square(dimension: kPadding16), Visibility( visible: isPortrait && - _selectedSegment != SegmentOption.browser && + _selectedSegment != SegmentOption.option2 && errorEvent == null, child: SimpleIconButton( onTap: () => _service!.connectSelectedWallet(), @@ -213,10 +213,10 @@ class _ConnectWalletPageState extends State Visibility( visible: isPortrait && (webOnlyWallet || - _selectedSegment == SegmentOption.browser), + _selectedSegment == SegmentOption.option2), child: SimpleIconButton( onTap: () => _service!.connectSelectedWallet( - inBrowser: _selectedSegment == SegmentOption.browser, + inBrowser: _selectedSegment == SegmentOption.option2, ), rightIcon: 'lib/modal/assets/icons/arrow_top_right.svg', title: 'Open', @@ -236,7 +236,7 @@ class _ConnectWalletPageState extends State if (isPortrait) const SizedBox.square(dimension: kPadding12), Visibility( visible: !isPortrait && - _selectedSegment != SegmentOption.browser && + _selectedSegment != SegmentOption.option2 && errorEvent == null, child: SimpleIconButton( onTap: () => _service!.connectSelectedWallet(), @@ -249,10 +249,10 @@ class _ConnectWalletPageState extends State Visibility( visible: !isPortrait && (webOnlyWallet || - _selectedSegment == SegmentOption.browser), + _selectedSegment == SegmentOption.option2), child: SimpleIconButton( onTap: () => _service!.connectSelectedWallet( - inBrowser: _selectedSegment == SegmentOption.browser, + inBrowser: _selectedSegment == SegmentOption.option2, ), leftIcon: 'lib/modal/assets/icons/arrow_top_right.svg', title: 'Open', @@ -276,7 +276,7 @@ class _ConnectWalletPageState extends State ), if (!isPortrait) const SizedBox.square(dimension: kPadding8), if (errorEvent is WalletNotInstalled && - _selectedSegment == SegmentOption.mobile) + _selectedSegment == SegmentOption.option1) Column( children: [ if (isPortrait) diff --git a/packages/reown_appkit/lib/modal/pages/preview_send_page.dart b/packages/reown_appkit/lib/modal/pages/preview_send_page.dart new file mode 100644 index 0000000..7679796 --- /dev/null +++ b/packages/reown_appkit/lib/modal/pages/preview_send_page.dart @@ -0,0 +1,594 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get_it/get_it.dart'; +import 'dart:ui' as ui; + +import 'package:reown_appkit/modal/constants/key_constants.dart'; +import 'package:reown_appkit/modal/models/send_data.dart'; +import 'package:reown_appkit/modal/services/blockchain_service/i_blockchain_service.dart'; +import 'package:reown_appkit/modal/services/blockchain_service/models/token_balance.dart'; +import 'package:reown_appkit/modal/services/toast_service/i_toast_service.dart'; +import 'package:reown_appkit/modal/services/toast_service/models/toast_message.dart'; +import 'package:reown_appkit/modal/utils/core_utils.dart'; +import 'package:reown_appkit/modal/utils/render_utils.dart'; +import 'package:reown_appkit/modal/widgets/avatars/account_avatar.dart'; +import 'package:reown_appkit/modal/widgets/buttons/address_button.dart'; +import 'package:reown_appkit/modal/widgets/buttons/network_button.dart'; +import 'package:reown_appkit/modal/widgets/buttons/primary_button.dart'; +import 'package:reown_appkit/modal/widgets/buttons/secondary_button.dart'; +import 'package:reown_appkit/modal/widgets/icons/rounded_icon.dart'; +import 'package:reown_appkit/modal/widgets/lists/list_items/account_list_item.dart'; +import 'package:reown_appkit/modal/widgets/widget_stack/widget_stack_singleton.dart'; +import 'package:reown_appkit/reown_appkit.dart' hide TransactionExtension; +import 'package:reown_appkit/modal/constants/style_constants.dart'; +import 'package:reown_appkit/modal/widgets/modal_provider.dart'; +import 'package:reown_appkit/modal/widgets/navigation/navbar.dart'; + +class PreviewSendPage extends StatefulWidget { + const PreviewSendPage({ + required this.sendData, + required this.sendTokenData, + required this.networkTokenData, + }) : super(key: KeyConstants.previewSendPageKey); + + final SendData sendData; + final TokenBalance sendTokenData; + final TokenBalance? networkTokenData; + + @override + State createState() => _PreviewSendPageState(); +} + +class _PreviewSendPageState extends State { + IBlockChainService get _blockchainService => GetIt.I(); + IToastService get _toastService => GetIt.I(); + Transaction? _transaction; + double _requiredGasInTokens = 0.0; + bool _isSendEnabled = false; + Timer? _gasEstimationTimer; + + bool get _isContractCall => widget.sendTokenData.address != null; + + String get _ownAddress { + final appKitModal = ModalProvider.of(context).instance; + final chainId = widget.sendTokenData.chainId!; + final namespace = NamespaceUtils.getNamespaceFromChain(chainId); + return appKitModal.session!.getAddress(namespace)!; + } + + String _contractData(BigInt sendValue) { + // Keccak256 hash of `transfer(address,uint256)`'s signature + final transferMethodId = 'a9059cbb'; + // Remove '0x' and pad + final paddedReceiver = + widget.sendData.address!.replaceFirst('0x', '').padLeft(64, '0'); + // Amount in hex, padded + final paddedAmount = sendValue.toRadixString(16).padLeft(64, '0'); + // + return '0x$transferMethodId$paddedReceiver$paddedAmount'; + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _constructTransaction(); + await _estimateNetworkCost(); + }); + } + + Future _constructTransaction() async { + final sendValue = CoreUtils.formatStringBalance( + widget.sendData.amount!, + precision: 4, + ); + final bigIntValue = _valueToBigInt(double.parse(sendValue)); + if (_isContractCall) { + final contractAddress = NamespaceUtils.getAccount( + widget.sendTokenData.address!, + ); + // final chainId = widget.sendTokenData.chainId!; + // final allowance = await _blockchainService.checkAllowance( + // senderAddress: _ownAddress, + // receiverAddress: widget.sendData.address!, + // contractAddress: contractAddress, + // caip2Chain: chainId, + // ); + // print('allowance $allowance'); + final data = _contractData(bigIntValue); + _transaction = Transaction( + from: EthereumAddress.fromHex(_ownAddress), + to: EthereumAddress.fromHex(contractAddress), + data: utf8.encode(data), + ); + } else { + _transaction = Transaction( + // from: EthereumAddress.fromHex(address!), + to: EthereumAddress.fromHex(widget.sendData.address!), + value: EtherAmount.fromBigInt(EtherUnit.wei, bigIntValue), + data: utf8.encode('0x'), + ); + } + debugPrint('transaction first: ${jsonEncode(_transaction!.toJson())}'); + setState(() {}); + } + + Future _estimateNetworkCost() async { + try { + final chainId = widget.sendTokenData.chainId!; + final gasPrices = await _blockchainService.gasPrice( + caip2chain: chainId, + ); + final standardGasPrice = gasPrices.standard ?? BigInt.zero; + // + final estimatedGas = await _blockchainService.estimateGas( + transaction: _transaction!.toJson(), + caip2Chain: chainId, + ); + // + final decimals = BigInt.from(10).pow(_getDecimals(true)); + _requiredGasInTokens = (standardGasPrice * estimatedGas) / decimals; + // Add a little buffer of 10% more since this is just an estimation + _requiredGasInTokens = _requiredGasInTokens * 1.1; + // + final sendValue = double.parse(widget.sendData.amount!); + // TODO do this only if sending max + final actualValue = sendValue - _requiredGasInTokens; + final finalValue = _valueToBigInt(actualValue); + + if (_isContractCall) { + final data = _contractData(finalValue); + _transaction = _transaction!.copyWith( + data: utf8.encode(data), + ); + } else { + _transaction = _transaction!.copyWith( + value: EtherAmount.fromBigInt(EtherUnit.wei, finalValue), + ); + } + debugPrint('transaction then: ${jsonEncode(_transaction!.toJson())}'); + setState(() => _isSendEnabled = true); + + _gasEstimationTimer ??= Timer.periodic( + Duration(seconds: 10), + _reEstimateGas, + ); + } on ArgumentError catch (e) { + _toastService.show(ToastMessage( + type: ToastType.error, + text: 'Invald ${e.name ?? 'argument'}', + )); + } catch (e) { + _toastService.show(ToastMessage( + type: ToastType.error, + text: e.toString(), + )); + } + } + + void _reEstimateGas(_) => _estimateNetworkCost(); + + @override + void dispose() { + _gasEstimationTimer?.cancel(); + _gasEstimationTimer = null; + super.dispose(); + } + + int _getDecimals(bool isNetworkToken) { + if (isNetworkToken) { + final decimals = widget.networkTokenData?.quantity?.decimals ?? '18'; + return int.parse(decimals); + } + final decimals = widget.sendTokenData.quantity?.decimals ?? '18'; + return int.parse(decimals); + } + + BigInt _valueToBigInt(double value) { + final amountStr = value.toStringAsFixed(_getDecimals(false)); + // Remove the decimal point and parse as an integer + final normalizedAmountStr = amountStr.replaceAll('.', ''); + return BigInt.parse(normalizedAmountStr); + } + + Future _sendTransaction() async { + try { + final appKitModal = ModalProvider.of(context).instance; + await appKitModal.request( + topic: appKitModal.session!.topic, + chainId: widget.sendTokenData.chainId!, + request: SessionRequestParams( + method: 'eth_sendTransaction', + params: [ + _transaction!.toJson(), + ], + ), + ); + } on ArgumentError catch (e) { + _toastService.show(ToastMessage( + type: ToastType.error, + text: 'Invald ${e.name ?? 'argument'}', + )); + } on Exception catch (e) { + _toastService.show(ToastMessage( + type: ToastType.error, + text: e.toString(), + )); + } + } + + @override + Widget build(BuildContext context) { + final themeColors = ReownAppKitModalTheme.colorsOf(context); + final themeData = ReownAppKitModalTheme.getDataOf(context); + return ModalNavbar( + title: 'Review send', + divider: false, + body: Container( + padding: const EdgeInsets.only( + left: kPadding16, + right: kPadding16, + top: kPadding16, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SendRow( + sendTokenData: widget.sendTokenData, + sendData: widget.sendData, + ), + const SizedBox.square(dimension: kPadding6), + Padding( + padding: const EdgeInsets.symmetric(horizontal: kPadding8), + child: SvgPicture.asset( + colorFilter: ColorFilter.mode( + themeColors.foreground200, + BlendMode.srcIn, + ), + 'lib/modal/assets/icons/arrow_down.svg', + package: 'reown_appkit', + width: 14.0, + height: 14.0, + ), + ), + _ReceiveRow( + sendData: widget.sendData, + ), + const SizedBox.square(dimension: kPadding16), + if (_transaction != null) + _DetailsRow( + transaction: _transaction!, + nativeTokenData: + widget.networkTokenData ?? widget.sendTokenData, + sendData: widget.sendData, + requiredGasInTokens: _requiredGasInTokens, + ), + const SizedBox.square(dimension: kPadding16), + Center( + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + WidgetSpan( + child: RoundedIcon( + assetPath: 'lib/modal/assets/icons/warning.svg', + assetColor: themeColors.foreground250, + circleColor: Colors.transparent, + borderColor: Colors.transparent, + padding: 0.0, + size: 14.0, + ), + alignment: ui.PlaceholderAlignment.middle, + ), + TextSpan( + text: ' Review transaction carefully', + style: themeData.textStyles.small400.copyWith( + color: themeColors.foreground200, + ), + ), + ], + ), + ), + ), + const SizedBox.square(dimension: kPadding16), + _SendButtonRow( + onCancel: () => widgetStack.instance.pop(), + onSend: _isSendEnabled ? _sendTransaction : null, + ), + ], + ), + ), + ); + } +} + +class _SendRow extends StatelessWidget { + const _SendRow({ + required this.sendTokenData, + required this.sendData, + }); + final TokenBalance sendTokenData; + final SendData sendData; + + @override + Widget build(BuildContext context) { + final appKitModal = ModalProvider.of(context).instance; + final themeData = ReownAppKitModalTheme.getDataOf(context); + final themeColors = ReownAppKitModalTheme.colorsOf(context); + final tokenPrice = sendTokenData.price ?? 0.0; + final balanceSend = double.parse(sendData.amount!) * tokenPrice; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: kPadding8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Send', + style: themeData.textStyles.small400.copyWith( + color: themeColors.foreground150, + height: 1.2, + ), + ), + Text( + '\$${CoreUtils.formatChainBalance(balanceSend)}', + style: themeData.textStyles.paragraph400.copyWith( + color: themeColors.foreground100, + height: 1.2, + ), + ), + ], + ), + Expanded(child: SizedBox()), + NetworkButton( + serviceStatus: appKitModal.status, + chainInfo: appKitModal.selectedChain, + iconUrl: sendTokenData.iconUrl, + title: '${CoreUtils.formatStringBalance( + sendData.amount!, + precision: 8, + )} ${sendTokenData.symbol} ', + iconOnRight: true, + onTap: () {}, + ), + ], + ), + ); + } +} + +class _ReceiveRow extends StatelessWidget { + const _ReceiveRow({required this.sendData}); + final SendData sendData; + + @override + Widget build(BuildContext context) { + final appKitModal = ModalProvider.of(context).instance; + final themeData = ReownAppKitModalTheme.getDataOf(context); + final themeColors = ReownAppKitModalTheme.colorsOf(context); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: kPadding8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'To', + style: themeData.textStyles.small400.copyWith( + color: themeColors.foreground150, + height: 1.2, + ), + ), + Expanded(child: SizedBox()), + AddressButton( + service: appKitModal, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox.square(dimension: kPadding12), + Text(RenderUtils.truncate(sendData.address!)), + const SizedBox.square(dimension: 6.0), + SizedBox.square( + dimension: BaseButtonSize.regular.height * 0.55, + child: GradientOrb( + address: sendData.address!, + size: BaseButtonSize.regular.height * 0.55, + ), + ), + const SizedBox.square(dimension: 6.0), + ], + ), + onTap: () {}, + ), + ], + ), + ); + } +} + +class _SendButtonRow extends StatelessWidget { + const _SendButtonRow({ + required this.onCancel, + required this.onSend, + }); + final VoidCallback onCancel; + final VoidCallback? onSend; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + SizedBox( + height: kListItemHeight, + child: SecondaryButton( + title: ' Cancel ', + onTap: onCancel, + ), + ), + const SizedBox.square(dimension: kPadding8), + Expanded( + child: SizedBox( + height: kListItemHeight, + child: PrimaryButton( + title: 'Send', + onTap: onSend, + ), + ), + ), + ], + ); + } +} + +class _DetailsRow extends StatefulWidget { + const _DetailsRow({ + required this.transaction, + required this.nativeTokenData, + required this.sendData, + required this.requiredGasInTokens, + }); + final Transaction transaction; + final TokenBalance nativeTokenData; + final SendData sendData; + final double requiredGasInTokens; + + @override + State<_DetailsRow> createState() => _DetailsRowState(); +} + +class _DetailsRowState extends State<_DetailsRow> { + bool _feesInTokens = false; + + String _formattedFee(double unformatted) { + if (_feesInTokens) { + return '${CoreUtils.formatChainBalance( + widget.requiredGasInTokens, + precision: 8, + )} ${widget.nativeTokenData.symbol}'; + } + final formatted = unformatted.toStringAsFixed(2); + if (double.parse(formatted) < 0.01) { + return '< \$0.01'; + } + return '\$$formatted'; + } + + @override + Widget build(BuildContext context) { + final appKitModal = ModalProvider.of(context).instance; + final themeData = ReownAppKitModalTheme.getDataOf(context); + final themeColors = ReownAppKitModalTheme.colorsOf(context); + final radiuses = ReownAppKitModalTheme.radiusesOf(context); + final chainId = appKitModal.selectedChain!.chainId; + final namespace = ReownAppKitModalNetworks.getNamespaceForChainId( + chainId, + ); + final address = appKitModal.session!.getAddress(namespace); + return Container( + width: double.infinity, + padding: const EdgeInsets.all(kPadding8), + decoration: BoxDecoration( + color: themeColors.grayGlass002, + borderRadius: BorderRadius.all(Radius.circular( + radiuses.radius2XS, + )), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(kPadding8), + child: Text( + 'Details', + style: themeData.textStyles.small400.copyWith( + color: themeColors.foreground100, + ), + ), + ), + const SizedBox.square(dimension: kPadding8), + AccountListItem( + title: 'Network cost', + titleStyle: themeData.textStyles.small400.copyWith( + color: themeColors.foreground150, + ), + trailing: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text( + _formattedFee( + (widget.requiredGasInTokens * widget.nativeTokenData.price!), + ), + style: themeData.textStyles.small400.copyWith( + color: themeColors.foreground100, + ), + ), + ), + onTap: () { + setState(() { + _feesInTokens = !_feesInTokens; + }); + }, + ), + const SizedBox.square(dimension: kPadding8), + AccountListItem( + title: 'Address', + titleStyle: themeData.textStyles.small400.copyWith( + color: themeColors.foreground150, + ), + trailing: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text( + RenderUtils.truncate(address!), + style: themeData.textStyles.small400.copyWith( + color: themeColors.foreground100, + ), + ), + ), + ), + const SizedBox.square(dimension: kPadding8), + AccountListItem( + title: 'Network', + titleStyle: themeData.textStyles.small500.copyWith( + color: themeColors.foreground100, + ), + trailing: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: NetworkButton( + serviceStatus: appKitModal.status, + chainInfo: appKitModal.selectedChain, + iconOnRight: true, + size: BaseButtonSize.small, + onTap: () {}, + ), + ), + ), + ], + ), + ); + } +} + +extension on Transaction { + Map toJson() { + return { + if (from != null) 'from': from!.hexEip55, + if (to != null) 'to': to!.hexEip55, + if (maxGas != null) 'gas': '0x${maxGas!.toRadixString(16)}', + if (gasPrice != null) + 'gasPrice': '0x${gasPrice!.getInWei.toRadixString(16)}', + if (value != null) 'value': '0x${value!.getInWei.toRadixString(16)}', + if (data != null) 'data': utf8.decode(data!), + if (nonce != null) 'nonce': nonce, + if (maxFeePerGas != null) + 'maxFeePerGas': '0x${maxFeePerGas!.getInWei.toRadixString(16)}', + if (maxPriorityFeePerGas != null) + 'maxPriorityFeePerGas': + '0x${maxPriorityFeePerGas!.getInWei.toRadixString(16)}', + }; + } +} diff --git a/packages/reown_appkit/lib/modal/pages/receive_page.dart b/packages/reown_appkit/lib/modal/pages/receive_page.dart new file mode 100644 index 0000000..3a3b8a9 --- /dev/null +++ b/packages/reown_appkit/lib/modal/pages/receive_page.dart @@ -0,0 +1,103 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:get_it/get_it.dart'; + +import 'package:reown_appkit/modal/constants/key_constants.dart'; +import 'package:reown_appkit/modal/services/toast_service/i_toast_service.dart'; +import 'package:reown_appkit/modal/widgets/buttons/address_button.dart'; +import 'package:reown_appkit/reown_appkit.dart'; +import 'package:reown_appkit/modal/constants/style_constants.dart'; +import 'package:reown_appkit/modal/widgets/qr_code_view.dart'; +import 'package:reown_appkit/modal/widgets/miscellaneous/responsive_container.dart'; +import 'package:reown_appkit/modal/widgets/modal_provider.dart'; +import 'package:reown_appkit/modal/widgets/navigation/navbar.dart'; +import 'package:reown_appkit/modal/services/toast_service/models/toast_message.dart'; + +class ReceivePage extends StatelessWidget { + const ReceivePage() : super(key: KeyConstants.receivePageKey); + + @override + Widget build(BuildContext context) { + final appKitModal = ModalProvider.of(context).instance; + final themeData = ReownAppKitModalTheme.getDataOf(context); + final themeColors = ReownAppKitModalTheme.colorsOf(context); + final isPortrait = ResponsiveData.isPortrait(context); + final chainId = appKitModal.selectedChain!.chainId; + final namespace = ReownAppKitModalNetworks.getNamespaceForChainId( + chainId, + ); + return ModalNavbar( + title: 'Receive', + divider: false, + body: SingleChildScrollView( + scrollDirection: isPortrait ? Axis.vertical : Axis.horizontal, + child: Flex( + direction: isPortrait ? Axis.vertical : Axis.horizontal, + children: [ + AddressButton( + service: appKitModal, + assetPath: 'lib/modal/assets/icons/copy.svg', + onTap: () async { + final address = appKitModal.session!.getAddress( + namespace, + )!; + final identityName = + (appKitModal.blockchainIdentity?.name ?? '').isNotEmpty + ? appKitModal.blockchainIdentity!.name! + : null; + + await Clipboard.setData(ClipboardData( + text: identityName ?? address, + )); + GetIt.I().show(ToastMessage( + type: ToastType.success, + text: identityName != null ? 'Name copied' : 'Address copied', + )); + }, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width / 1.5, + child: QRCodeView( + uri: appKitModal.session!.getAddress(namespace)!, + ), + ), + ], + ), + Container( + constraints: BoxConstraints( + maxWidth: isPortrait + ? ResponsiveData.maxWidthOf(context) + : (ResponsiveData.maxHeightOf(context) - + kNavbarHeight - + 32.0), + ), + padding: const EdgeInsets.only( + left: kPadding16, + right: kPadding16, + bottom: kPadding16, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Copy your address or scan this QR code', + textAlign: TextAlign.center, + style: themeData.textStyles.paragraph500.copyWith( + color: themeColors.foreground100, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/packages/reown_appkit/lib/modal/pages/select_token_page.dart b/packages/reown_appkit/lib/modal/pages/select_token_page.dart new file mode 100644 index 0000000..febada4 --- /dev/null +++ b/packages/reown_appkit/lib/modal/pages/select_token_page.dart @@ -0,0 +1,150 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; + +import 'package:reown_appkit/modal/constants/key_constants.dart'; +import 'package:reown_appkit/modal/constants/style_constants.dart'; +import 'package:reown_appkit/modal/services/blockchain_service/i_blockchain_service.dart'; +import 'package:reown_appkit/modal/services/blockchain_service/models/token_balance.dart'; +import 'package:reown_appkit/modal/utils/core_utils.dart'; +import 'package:reown_appkit/modal/widgets/navigation/navbar.dart'; +import 'package:reown_appkit/modal/widgets/modal_provider.dart'; +import 'package:reown_appkit/modal/widgets/icons/rounded_icon.dart'; +import 'package:reown_appkit/modal/widgets/lists/list_items/account_list_item.dart'; +import 'package:reown_appkit/modal/widgets/widget_stack/widget_stack_singleton.dart'; +import 'package:reown_appkit/reown_appkit.dart'; + +class SelectTokenPage extends StatefulWidget { + const SelectTokenPage() : super(key: KeyConstants.selectTokenPage); + + @override + State createState() => _SelectTokenPageState(); +} + +class _SelectTokenPageState extends State { + IBlockChainService get _blockchainService => GetIt.I(); + List _tokens = []; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + try { + final appKitModal = ModalProvider.of(context).instance; + final chainId = appKitModal.selectedChain!.chainId; + final namespace = ReownAppKitModalNetworks.getNamespaceForChainId( + chainId, + ); + final address = appKitModal.session!.getAddress(namespace)!; + + // cached items + final cachedTokens = _blockchainService.tokensList ?? []; + if (cachedTokens.isNotEmpty) { + _tokens = List.from(cachedTokens); + } else { + _tokens = await _blockchainService.getBalance( + address: address, + caip2chain: '$namespace:$chainId', + ); + } + setState(() {}); + } catch (_) {} + }); + } + + @override + Widget build(BuildContext context) { + final themeColors = ReownAppKitModalTheme.colorsOf(context); + final themeData = ReownAppKitModalTheme.getDataOf(context); + final radiuses = ReownAppKitModalTheme.radiusesOf(context); + return ModalNavbar( + title: 'Select token', + safeAreaLeft: true, + safeAreaRight: true, + safeAreaBottom: false, + divider: false, + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: kPadding12), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'Your tokens', + style: themeData.textStyles.paragraph400.copyWith( + color: themeColors.foreground200, + ), + ), + ), + ..._tokens.mapIndexed((index, token) { + return AccountListItem( + padding: const EdgeInsets.all(0.0), + backgroundColor: MaterialStatePropertyAll( + Colors.transparent, + ), + iconWidget: Padding( + padding: const EdgeInsets.only(left: kPadding6), + child: (_tokens[index].iconUrl ?? '').isEmpty + ? RoundedIcon( + assetPath: 'lib/modal/assets/icons/coin.svg', + assetColor: themeColors.inverse100, + borderRadius: radiuses.isSquare() ? 0.0 : null, + ) + : ClipRRect( + borderRadius: radiuses.isSquare() + ? BorderRadius.zero + : BorderRadius.circular(34), + child: CachedNetworkImage( + imageUrl: _tokens[index].iconUrl!, + height: 34, + width: 34, + errorWidget: (context, url, error) { + return RoundedIcon( + assetPath: 'lib/modal/assets/icons/coin.svg', + assetColor: themeColors.inverse100, + borderRadius: radiuses.isSquare() ? 0.0 : null, + ); + }, + ), + ), + ), + title: _tokens[index].name ?? '', + titleStyle: themeData.textStyles.paragraph500.copyWith( + color: themeColors.foreground100, + ), + subtitle: '${CoreUtils.formatStringBalance( + _tokens[index].quantity?.numeric ?? '0.0', + )} ${_tokens[index].symbol ?? ''}', + subtitleStyle: themeData.textStyles.small400.copyWith( + color: themeColors.foreground200, + ), + trailing: Row( + children: [ + Text( + '\$${CoreUtils.formatChainBalance(_tokens[index].value)}', + style: themeData.textStyles.paragraph500.copyWith( + color: themeColors.foreground100, + ), + ), + SizedBox.square(dimension: kPadding6), + ], + ), + onTap: () { + _blockchainService.selectSendToken(_tokens[index]); + widgetStack.instance.pop(); + }, + ); + }), + SizedBox.square( + dimension: MediaQuery.of(context).padding.bottom + kPadding6, + ), + ], + ), + ), + ); + } +} diff --git a/packages/reown_appkit/lib/modal/pages/send_page.dart b/packages/reown_appkit/lib/modal/pages/send_page.dart new file mode 100644 index 0000000..bf10758 --- /dev/null +++ b/packages/reown_appkit/lib/modal/pages/send_page.dart @@ -0,0 +1,433 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get_it/get_it.dart'; + +import 'package:reown_appkit/modal/constants/key_constants.dart'; +import 'package:reown_appkit/modal/models/send_data.dart'; +import 'package:reown_appkit/modal/pages/preview_send_page.dart'; +import 'package:reown_appkit/modal/pages/select_token_page.dart'; +import 'package:reown_appkit/modal/services/blockchain_service/i_blockchain_service.dart'; +import 'package:reown_appkit/modal/services/blockchain_service/models/token_balance.dart'; +import 'package:reown_appkit/modal/utils/core_utils.dart'; +import 'package:reown_appkit/modal/widgets/buttons/network_button.dart'; +import 'package:reown_appkit/modal/widgets/buttons/primary_button.dart'; +import 'package:reown_appkit/modal/widgets/buttons/secondary_button.dart'; +import 'package:reown_appkit/modal/widgets/buttons/simple_icon_button.dart'; +import 'package:reown_appkit/modal/widgets/icons/rounded_icon.dart'; +import 'package:reown_appkit/modal/widgets/miscellaneous/searchbar.dart'; +import 'package:reown_appkit/modal/widgets/widget_stack/widget_stack_singleton.dart'; +import 'package:reown_appkit/reown_appkit.dart'; +import 'package:reown_appkit/modal/constants/style_constants.dart'; +import 'package:reown_appkit/modal/widgets/modal_provider.dart'; +import 'package:reown_appkit/modal/widgets/navigation/navbar.dart'; + +class SendPage extends StatefulWidget { + const SendPage() : super(key: KeyConstants.sendPageKey); + + @override + State createState() => _SendPageState(); +} + +class _SendPageState extends State with WidgetsBindingObserver { + IBlockChainService get _blockchainService => GetIt.I(); + + final _amountController = TextEditingController(); + final _addressController = TextEditingController(); + var _sendData = SendData(); + final _addressFocus = FocusNode(); + late final TokenBalance _selectedToken; + late final TokenBalance? _networkToken; + + @override + void initState() { + super.initState(); + final cachedTokens = _blockchainService.tokensList ?? []; + _selectedToken = _blockchainService.selectedSendToken ?? cachedTokens.last; + final namespace = NamespaceUtils.getNamespaceFromChain( + _selectedToken.chainId!, + ); + + // TODO check this + if (namespace == 'eip155') { + _networkToken = cachedTokens.firstWhereOrNull( + (element) => + element.address == null && + element.chainId == _selectedToken.chainId, + ); + } + + _amountController.addListener(() { + _sendData = _sendData.copyWith(amount: _amountController.text); + setState(() {}); + }); + _addressController.addListener(() { + _sendData = _sendData.copyWith(address: _addressController.text); + setState(() {}); + }); + } + + void _setMaxAmount(String? maxAmount) { + final parsedAmount = CoreUtils.formatStringBalance( + maxAmount ?? '0.00', + precision: _selectedToken.quantity?.decimals.toInt() ?? 18, + ); + _sendData = _sendData.copyWith(amount: parsedAmount); + _amountController.text = _sendData.amount!; + setState(() {}); + } + + void _setAddress(String address) { + _addressController.text = address; + setState(() {}); + } + + void _pasteAddress() async { + final clipboardData = await Clipboard.getData( + Clipboard.kTextPlain, + ); + // await Clipboard.setData(ClipboardData(text: '')); + _setAddress(clipboardData?.text ?? ''); + } + + bool _isValidDecimal(String input) { + final regex = RegExp(r'^-?\d+(\.\d+)?$'); + return regex.hasMatch(input); + } + + bool _isValidAddress(String input) { + final appKitModal = ModalProvider.of(context).instance; + final chainId = appKitModal.selectedChain!.chainId; + final namespace = ReownAppKitModalNetworks.getNamespaceForChainId(chainId); + final regex = RegExp(r'^(0x)?[0-9a-fA-F]{40}$'); + if (namespace == 'eip155') { + return regex.hasMatch(input); + } + return true; + } + + bool get _pasteIsVisible { + return _addressController.text.isEmpty; + } + + bool get _sendButtonEnabled { + if (_sendData.amount == null) return false; + if (_sendData.amount!.toString().isEmpty) return false; + if (!_isValidDecimal(_sendData.amount!)) return false; + + if (_sendData.address == null) return false; + if (_sendData.address!.toString().isEmpty) return false; + if (!_isValidAddress(_sendData.address!)) return false; + + try { + final parsedAmount = double.parse(_sendData.amount!); + if (parsedAmount <= 0.0) { + return false; + } + } catch (_) { + return false; + } + + return true; + } + + @override + Widget build(BuildContext context) { + final appKitModal = ModalProvider.of(context).instance; + final themeData = ReownAppKitModalTheme.getDataOf(context); + final themeColors = ReownAppKitModalTheme.colorsOf(context); + final tokenPrice = _selectedToken.price ?? 0.0; + double balanceSend = 0.0; + if ((_sendData.amount ?? '').isNotEmpty) { + balanceSend = double.parse(_sendData.amount ?? '0.0') * tokenPrice; + } + return ModalNavbar( + title: 'Send', + divider: false, + body: Container( + padding: const EdgeInsets.all(kPadding12), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + alignment: Alignment.center, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + InputAmountWidget( + controller: _amountController, + onSubmitted: (value) { + _addressFocus.requestFocus(); + }, + ), + InputAddressWidget( + controller: _addressController, + focusNode: _addressFocus, + onSubmitted: (value) { + FocusManager.instance.primaryFocus?.unfocus(); + }, + ), + ], + ), + Container( + decoration: BoxDecoration( + color: themeColors.background125, + border: Border.all( + color: themeColors.background200, + width: 1.0, + ), + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + padding: const EdgeInsets.all(6.0), + child: RoundedIcon( + assetPath: 'lib/modal/assets/icons/receive.svg', + assetColor: themeColors.foreground275, + circleColor: themeColors.grayGlass002, + borderColor: themeColors.background125, + size: 40.0, + borderWidth: 6.0, + borderRadius: kPadding12, + ), + ), + Positioned( + top: 18.0, + right: kPadding16, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + NetworkButton( + serviceStatus: appKitModal.status, + chainInfo: appKitModal.selectedChain, + title: ' ${_selectedToken.symbol}', + iconUrl: _selectedToken.iconUrl, + onTap: () { + widgetStack.instance.push(SelectTokenPage()); + }, + ), + const SizedBox.square(dimension: 4.0), + Padding( + padding: const EdgeInsets.only(right: 4.0), + child: GestureDetector( + onTap: () => _setMaxAmount( + _selectedToken.quantity?.numeric, + ), + child: Row( + children: [ + Text( + CoreUtils.formatStringBalance( + _selectedToken.quantity?.numeric ?? '', + ), + style: themeData.textStyles.small400.copyWith( + color: themeColors.foreground200, + ), + ), + Text( + ' Max ', + style: themeData.textStyles.small600.copyWith( + color: themeColors.accent100, + ), + ), + ], + ), + ), + ) + ], + ), + ), + Positioned( + top: 70.0, + left: 20.0, + child: Padding( + padding: const EdgeInsets.only(right: 4.0), + child: Text( + '\$${CoreUtils.formatChainBalance(balanceSend)}', + style: themeData.textStyles.small400.copyWith( + color: themeColors.foreground200, + ), + ), + ), + ), + Positioned( + bottom: 28.0, + right: kPadding16, + child: Visibility( + visible: _pasteIsVisible, + child: SimpleIconButton( + leftIcon: 'lib/modal/assets/icons/copy.svg', + title: 'Paste', + backgroundColor: themeColors.grayGlass002, + foregroundColor: themeColors.foreground125, + iconSize: 14.0, + fontSize: 14.0, + onTap: () => _pasteAddress(), + ), + ), + ), + Container( + margin: const EdgeInsets.only(left: 60.0), + color: themeColors.background125, + height: kPadding8, + child: SizedBox.square(dimension: 10.0), + ), + Container( + margin: const EdgeInsets.only(right: 60.0), + color: themeColors.background125, + height: kPadding8, + child: SizedBox.square(dimension: 10.0), + ), + ], + ), + const SizedBox.square(dimension: kPadding6), + Row( + children: [ + const SizedBox.square(dimension: 4.0), + Expanded( + child: SizedBox( + height: kListItemHeight, + child: _sendButtonEnabled + ? PrimaryButton( + title: 'Review send', + onTap: () { + widgetStack.instance.push(PreviewSendPage( + sendData: _sendData, + sendTokenData: _selectedToken, + networkTokenData: _networkToken, + )); + }) + : SecondaryButton( + title: (_sendData.amount ?? '').isEmpty + ? 'Add amount' + : (_sendData.address ?? '').isEmpty + ? 'Add address' + : _isValidAddress(_sendData.address!) + ? 'Send' + : 'Invalid address', + ), + ), + ), + const SizedBox.square(dimension: 4.0), + ], + ), + ], + ), + ), + ); + } +} + +class InputAmountWidget extends StatefulWidget { + final TextEditingController controller; + final Function(String value)? onSubmitted; + final Function(bool value)? onFocus; + final Widget? suffixIcon; + const InputAmountWidget({ + super.key, + required this.controller, + this.onSubmitted, + this.onFocus, + this.suffixIcon, + }); + + @override + State createState() => _InputAmountWidgetState(); +} + +class _InputAmountWidgetState extends State { + bool hasFocus = false; + + @override + Widget build(BuildContext context) { + final themeData = ReownAppKitModalTheme.getDataOf(context); + final themeColors = ReownAppKitModalTheme.colorsOf(context); + return ModalSearchBar( + height: 100.0, + controller: widget.controller, + initialValue: widget.controller.text, + hint: '0', + prefixIcon: const SizedBox.square(dimension: kPadding16), + suffixIcon: const SizedBox.shrink(), + suffixWidth: 110.0, + noIcons: true, + textInputType: TextInputType.datetime, + textInputAction: TextInputAction.next, + onSubmitted: widget.onSubmitted, + debounce: false, + borderOnFocus: false, + onTextChanged: (_) {}, + onFocusChange: _onFocusChange, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')), + ], + textStyle: themeData.textStyles.title400.copyWith( + color: themeColors.foreground100, + fontSize: 32.0, + height: 1.0, + ), + ); + } + + void _onFocusChange(bool focus) { + if (hasFocus == focus) return; + widget.onFocus?.call(focus); + setState(() => hasFocus = focus); + } +} + +class InputAddressWidget extends StatefulWidget { + final TextEditingController controller; + final Function(String value)? onSubmitted; + final Function(bool value)? onFocus; + final FocusNode? focusNode; + final Widget? suffixIcon; + const InputAddressWidget({ + super.key, + required this.controller, + this.onSubmitted, + this.onFocus, + this.focusNode, + this.suffixIcon, + }); + + @override + State createState() => _InputAddressWidgetState(); +} + +class _InputAddressWidgetState extends State { + bool hasFocus = false; + @override + Widget build(BuildContext context) { + final themeData = ReownAppKitModalTheme.getDataOf(context); + final themeColors = ReownAppKitModalTheme.colorsOf(context); + return ModalSearchBar( + focusNode: widget.focusNode, + height: 100.0, + maxLines: widget.controller.text.isEmpty ? 1 : 2, + controller: widget.controller, + initialValue: widget.controller.text, + hint: 'Type or paste address', + prefixIcon: const SizedBox.square(dimension: kPadding12), + suffixIcon: const SizedBox.square(dimension: kPadding12), + noIcons: true, + textInputType: TextInputType.text, + textInputAction: TextInputAction.done, + onSubmitted: widget.onSubmitted, + debounce: false, + borderOnFocus: false, + onTextChanged: (_) {}, + onFocusChange: _onFocusChange, + textStyle: themeData.textStyles.large500.copyWith( + color: themeColors.foreground100, + fontSize: 18.0, + height: 1.2, + ), + ); + } + + void _onFocusChange(bool focus) { + if (hasFocus == focus) return; + widget.onFocus?.call(focus); + setState(() => hasFocus = focus); + } +} diff --git a/packages/reown_appkit/lib/modal/pages/smart_account_page.dart b/packages/reown_appkit/lib/modal/pages/smart_account_page.dart new file mode 100644 index 0000000..438add3 --- /dev/null +++ b/packages/reown_appkit/lib/modal/pages/smart_account_page.dart @@ -0,0 +1,450 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get_it/get_it.dart'; + +import 'package:reown_appkit/modal/constants/key_constants.dart'; +import 'package:reown_appkit/modal/constants/style_constants.dart'; +import 'package:reown_appkit/modal/pages/activity_page.dart'; +import 'package:reown_appkit/modal/pages/account_page.dart'; +import 'package:reown_appkit/modal/pages/receive_page.dart'; +import 'package:reown_appkit/modal/pages/send_page.dart'; +import 'package:reown_appkit/modal/services/analytics_service/models/analytics_event.dart'; +import 'package:reown_appkit/modal/i_appkit_modal_impl.dart'; +import 'package:reown_appkit/modal/services/blockchain_service/i_blockchain_service.dart'; +import 'package:reown_appkit/modal/services/blockchain_service/models/token_balance.dart'; +import 'package:reown_appkit/modal/utils/core_utils.dart'; +import 'package:reown_appkit/modal/widgets/buttons/address_button.dart'; +import 'package:reown_appkit/modal/widgets/buttons/network_button.dart'; +import 'package:reown_appkit/modal/widgets/miscellaneous/content_loading.dart'; +import 'package:reown_appkit/modal/widgets/miscellaneous/responsive_container.dart'; +import 'package:reown_appkit/modal/widgets/miscellaneous/segmented_control.dart'; +import 'package:reown_appkit/modal/widgets/navigation/navbar.dart'; +import 'package:reown_appkit/modal/widgets/navigation/navbar_action_button.dart'; +import 'package:reown_appkit/modal/widgets/widget_stack/widget_stack_singleton.dart'; +import 'package:reown_appkit/modal/widgets/modal_provider.dart'; +import 'package:reown_appkit/modal/widgets/buttons/simple_icon_button.dart'; +import 'package:reown_appkit/modal/widgets/icons/rounded_icon.dart'; +import 'package:reown_appkit/modal/widgets/lists/list_items/account_list_item.dart'; +import 'package:reown_appkit/reown_appkit.dart'; + +class SmartAccountPage extends StatefulWidget { + const SmartAccountPage() : super(key: KeyConstants.smartAccountPage); + + @override + State createState() => _SmartAccountPageState(); +} + +class _SmartAccountPageState extends State + with WidgetsBindingObserver { + IReownAppKitModal? _appKitModal; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance.addPostFrameCallback((_) { + _appKitModal = ModalProvider.of(context).instance; + _appKitModal?.addListener(_rebuild); + _rebuild(); + }); + } + + void _rebuild() => setState(() {}); + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + _rebuild(); + } + } + + @override + void dispose() { + _appKitModal?.removeListener(_rebuild); + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (_appKitModal == null) { + return ContentLoading(viewHeight: 400.0); + } + + final themeColors = ReownAppKitModalTheme.colorsOf(context); + return ModalNavbar( + title: '', + safeAreaLeft: true, + safeAreaRight: true, + safeAreaBottom: false, + divider: false, + leftAction: NavbarActionButton( + child: GestureDetector( + onTap: () => widgetStack.instance.push( + ReownAppKitModalSelectNetworkPage(), + event: ClickNetworksEvent(), + ), + child: Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Row( + children: [ + const SizedBox.square(dimension: 20.0), + NetworkButton( + serviceStatus: _appKitModal!.status, + chainInfo: _appKitModal!.selectedChain, + size: BaseButtonSize.small, + iconOnly: true, + ), + const SizedBox.square(dimension: 4.0), + SvgPicture.asset( + 'lib/modal/assets/icons/chevron_down.svg', + package: 'reown_appkit', + colorFilter: ColorFilter.mode( + themeColors.foreground200, + BlendMode.srcIn, + ), + width: 14.0, + height: 14.0, + ), + ], + ), + ), + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: kPadding12), + child: _SmartAccountView( + appKitModal: _appKitModal!, + ), + ), + ); + } +} + +class _SmartAccountView extends StatefulWidget { + const _SmartAccountView({required this.appKitModal}); + final IReownAppKitModal appKitModal; + + @override + State<_SmartAccountView> createState() => _SmartAccountViewState(); +} + +class _SmartAccountViewState extends State<_SmartAccountView> { + IBlockChainService get _blockchainService => GetIt.I(); + SegmentOption _selectedSegment = SegmentOption.option1; + List _tokens = []; + + @override + void initState() { + super.initState(); + final chainId = widget.appKitModal.selectedChain!.chainId; + final namespace = ReownAppKitModalNetworks.getNamespaceForChainId( + chainId, + ); + final address = widget.appKitModal.session!.getAddress(namespace)!; + + // cached items + final cachedTokens = _blockchainService.tokensList ?? []; + if (cachedTokens.isNotEmpty) { + _tokens = List.from(cachedTokens); + setState(() {}); + } + + WidgetsBinding.instance.addPostFrameCallback((_) async { + try { + _tokens = await _blockchainService.getBalance( + address: address, + caip2chain: '$namespace:$chainId', + ); + setState(() {}); + } catch (_) {} + }); + } + + String get _sumBalance { + String? tokenBalance = '0.00'; + if (_tokens.isNotEmpty) { + final sum = _tokens.map((e) => e.value).reduce((prev, curr) { + return (prev ?? 0.0) + (curr ?? 0.0); + }) ?? + 0.0; + tokenBalance = CoreUtils.formatChainBalance(sum); + } + return tokenBalance; + } + + @override + Widget build(BuildContext context) { + final themeColors = ReownAppKitModalTheme.colorsOf(context); + final themeData = ReownAppKitModalTheme.getDataOf(context); + final radiuses = ReownAppKitModalTheme.radiusesOf(context); + return ConstrainedBox( + constraints: BoxConstraints( + maxHeight: ResponsiveData.maxHeightOf(context), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AddressButton( + service: widget.appKitModal, + onTap: () { + widgetStack.instance.push(EOAccountPage()); + }, + ), + const SizedBox.square(dimension: kPadding8), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: '\$${_sumBalance.split('.').first}', + style: themeData.textStyles.large400.copyWith( + color: themeColors.foreground100, + fontSize: 38.0, + ), + ), + TextSpan( + text: '.${_sumBalance.split('.').last}', + style: themeData.textStyles.large400.copyWith( + color: themeColors.foreground200, + fontSize: 38.0, + ), + ), + ], + ), + ), + const SizedBox.square(dimension: kPadding16), + Row( + children: [ + Expanded( + child: SimpleIconButton( + rightIcon: 'lib/modal/assets/icons/arrow_bottom_circle.svg', + backgroundColor: themeColors.accenGlass010, + foregroundColor: themeColors.accent100, + borderRadius: kPadding16, + title: '', + size: BaseButtonSize.big, + iconSize: 20.0, + fontSize: 1.0, + onTap: () => widgetStack.instance.push(ReceivePage()), + ), + ), + const SizedBox.square(dimension: kPadding8), + Expanded( + child: SimpleIconButton( + rightIcon: 'lib/modal/assets/icons/paperplane.svg', + backgroundColor: themeColors.accenGlass010, + foregroundColor: themeColors.accent100, + borderRadius: kPadding16, + title: '', + size: BaseButtonSize.big, + iconSize: 20.0, + fontSize: 1.0, + onTap: _tokens.isEmpty + ? null + : () => widgetStack.instance.push(SendPage()), + ), + ), + ], + ), + const SizedBox.square(dimension: kPadding12), + LayoutBuilder( + builder: (context, constraints) { + return SegmentedControl( + width: (constraints.maxWidth / 2) - 2.0, + option1Title: 'Tokens', + option1Icon: '', + option2Title: 'Activity', + option2Icon: '', + onChange: (option) => setState(() { + _selectedSegment = option; + }), + ); + }, + ), + const SizedBox.square(dimension: kPadding12), + Visibility( + visible: _selectedSegment == SegmentOption.option1, + child: _tokens.isNotEmpty + ? Column( + children: [ + ..._tokens.mapIndexed((index, token) { + return AccountListItem( + padding: const EdgeInsets.all(0.0), + backgroundColor: MaterialStatePropertyAll( + Colors.transparent, + ), + iconWidget: Padding( + padding: const EdgeInsets.only(left: kPadding6), + child: (_tokens[index].iconUrl ?? '').isEmpty + ? RoundedIcon( + assetPath: + 'lib/modal/assets/icons/coin.svg', + assetColor: themeColors.inverse100, + borderRadius: + radiuses.isSquare() ? 0.0 : null, + ) + : ClipRRect( + borderRadius: radiuses.isSquare() + ? BorderRadius.zero + : BorderRadius.circular(34), + child: CachedNetworkImage( + imageUrl: _tokens[index].iconUrl!, + height: 34, + width: 34, + errorWidget: (context, url, error) { + return RoundedIcon( + assetPath: + 'lib/modal/assets/icons/coin.svg', + assetColor: themeColors.inverse100, + borderRadius: + radiuses.isSquare() ? 0.0 : null, + ); + }, + ), + ), + ), + title: _tokens[index].name ?? '', + titleStyle: + themeData.textStyles.paragraph500.copyWith( + color: themeColors.foreground100, + ), + subtitle: '${CoreUtils.formatStringBalance( + _tokens[index].quantity?.numeric ?? '0.0', + )} ${_tokens[index].symbol ?? ''}', + subtitleStyle: themeData.textStyles.small400.copyWith( + color: themeColors.foreground200, + ), + trailing: Row( + children: [ + Text( + '\$${CoreUtils.formatChainBalance(_tokens[index].value)}', + style: + themeData.textStyles.paragraph500.copyWith( + color: themeColors.foreground100, + ), + ), + SizedBox.square(dimension: kPadding6), + ], + ), + ); + }), + SizedBox.square( + dimension: + MediaQuery.of(context).padding.bottom + kPadding6, + ), + ], + ) + : _ReceiveFundsEmptyStateButton(), + ), + Visibility( + visible: _selectedSegment == SegmentOption.option2, + child: Expanded( + child: ActivityListViewBuilder( + appKitModal: widget.appKitModal, + ), + ), + ), + // const SizedBox.square(dimension: 20.0), + ], + ), + ); + } +} + +class _ReceiveFundsEmptyStateButton extends StatelessWidget { + @override + Widget build(BuildContext context) { + final themeData = ReownAppKitModalTheme.getDataOf(context); + final themeColors = ReownAppKitModalTheme.colorsOf(context); + return Column( + children: [ + AccountListItem( + padding: const EdgeInsets.symmetric( + horizontal: kPadding8, + vertical: kPadding8, + ), + iconWidget: Padding( + padding: const EdgeInsets.all(4.0), + child: RoundedIcon( + borderRadius: 30.0, + size: 30.0, + assetPath: 'lib/modal/assets/icons/arrow_bottom_circle.svg', + assetColor: themeColors.error100, + circleColor: themeColors.error100.withOpacity(0.15), + borderColor: themeColors.error100.withOpacity(0.15), + ), + ), + title: 'Receive funds', + subtitle: 'Transfer tokens on your wallet', + flexible: true, + titleStyle: themeData.textStyles.small500.copyWith( + color: themeColors.foreground100, + ), + subtitleStyle: themeData.textStyles.tiny400.copyWith( + color: themeColors.foreground150, + ), + onTap: () => widgetStack.instance.push(ReceivePage()), + ), + SizedBox.square( + dimension: MediaQuery.of(context).padding.bottom + kPadding6, + ), + ], + ); + } +} + +// class _ConnectedWalletButton extends StatelessWidget { +// @override +// Widget build(BuildContext context) { +// final service = ModalProvider.of(context).instance; +// final themeData = ReownAppKitModalTheme.getDataOf(context); +// final themeColors = ReownAppKitModalTheme.colorsOf(context); +// final radiuses = ReownAppKitModalTheme.radiusesOf(context); +// String iconImage = ''; +// if ((service.session!.peer?.metadata.icons ?? []).isNotEmpty) { +// iconImage = service.session!.peer?.metadata.icons.first ?? ''; +// } +// final walletInfo = GetIt.I().getConnectedWallet(); +// return Column( +// children: [ +// const SizedBox.square(dimension: kPadding8), +// AccountListItem( +// iconWidget: Padding( +// padding: const EdgeInsets.symmetric(horizontal: 4.0), +// child: iconImage.isEmpty +// ? RoundedIcon( +// assetPath: 'lib/modal/assets/icons/wallet.svg', +// assetColor: themeColors.inverse100, +// borderRadius: radiuses.isSquare() ? 0.0 : null, +// ) +// : ClipRRect( +// borderRadius: radiuses.isSquare() +// ? BorderRadius.zero +// : BorderRadius.circular(34), +// child: CachedNetworkImage( +// imageUrl: iconImage, +// height: 34, +// width: 34, +// errorWidget: (context, url, error) { +// return RoundedIcon( +// assetPath: 'lib/modal/assets/icons/wallet.svg', +// assetColor: themeColors.inverse100, +// borderRadius: radiuses.isSquare() ? 0.0 : null, +// ); +// }, +// ), +// ), +// ), +// title: service.session!.peer?.metadata.name ?? 'Connected Wallet', +// titleStyle: themeData.textStyles.paragraph500.copyWith( +// color: themeColors.foreground100, +// ), +// onTap: +// walletInfo != null ? () => service.launchConnectedWallet() : null, +// ), +// ], +// ); +// } +// } diff --git a/packages/reown_appkit/lib/modal/services/blockchain_service/blockchain_service.dart b/packages/reown_appkit/lib/modal/services/blockchain_service/blockchain_service.dart index 091abfd..55ccf59 100644 --- a/packages/reown_appkit/lib/modal/services/blockchain_service/blockchain_service.dart +++ b/packages/reown_appkit/lib/modal/services/blockchain_service/blockchain_service.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:reown_appkit/modal/services/blockchain_service/models/gas_price.dart'; +import 'package:reown_appkit/modal/services/blockchain_service/models/token_balance.dart'; import 'package:reown_appkit/modal/services/blockchain_service/models/wallet_activity.dart'; import 'package:reown_appkit/reown_appkit.dart'; import 'package:reown_appkit/modal/constants/string_constants.dart'; @@ -26,122 +28,278 @@ class BlockChainService implements IBlockChainService { 'x-sdk-version': CoreConstants.X_SDK_VERSION, }; + ActivityData? _activityData; + @override + ActivityData? get activityData => _activityData; + + List? _tokensList; + @override + List? get tokensList => _tokensList; + + TokenBalance? _selectedToken; + @override + TokenBalance? get selectedSendToken => _selectedToken; + + @override + void selectSendToken(TokenBalance token) => _selectedToken = token; + @override Future init() async { _clientId = await _core.crypto.getClientId(); } @override - Future getIdentity(String address) async { + Future getIdentity({required String address}) async { + final uri = Uri.parse('$_baseUrl/identity/$address'); + final queryParams = {..._requiredParams}; + final url = uri.replace(queryParameters: queryParams); + final response = await http.get(url, headers: _requiredHeaders); + _core.logger.i('[$runtimeType] getIdentity $url => ${response.body}'); + if (response.statusCode == 200 && response.body.isNotEmpty) { + return BlockchainIdentity.fromJson(jsonDecode(response.body)); + } try { - final uri = Uri.parse('$_baseUrl/identity/$address'); - final queryParams = {..._requiredParams}; - final response = await http.get( - uri.replace(queryParameters: queryParams), - headers: _requiredHeaders, - ); - _core.logger.i('[$runtimeType] getIdentity $address => ${response.body}'); - if (response.statusCode == 200 && response.body.isNotEmpty) { - return BlockchainIdentity.fromJson(jsonDecode(response.body)); - } - if (response.statusCode == 400) { - final errorData = jsonDecode(response.body) as Map; - final reasons = errorData['reasons'] as List; - final reason = reasons.isNotEmpty - ? reasons.first['description'] ?? '' - : response.body; - throw Exception(reason); - } else { - throw Exception('Failed to load avatar'); + final reason = _parseResponseError(response.body); + throw Exception(reason); + } catch (e) { + _core.logger.e('[$runtimeType] getIdentity, decode result error => $e'); + rethrow; + } + } + + @override + Future getHistory({ + required String address, + String? cursor, + }) async { + final uri = Uri.parse('$_baseUrl/account/$address/history'); + final queryParams = { + ..._requiredParams, + if (cursor != null) 'cursor': cursor, + }; + final url = uri.replace(queryParameters: queryParams); + final response = await http.get(url, headers: _requiredHeaders); + _core.logger.i('[$runtimeType] getHistory $url => ${response.body}'); + if (response.statusCode == 200 && response.body.isNotEmpty) { + try { + _activityData = ActivityData.fromRawJson(response.body); + return _activityData!; + } catch (e) { + _core.logger.e('[$runtimeType] getHistory, parse result error => $e'); + throw Exception('Failed to load wallet activity. $e'); } + } + try { + final reason = _parseResponseError(response.body); + throw Exception(reason); + } catch (e) { + _core.logger.e('[$runtimeType] getHistory, decode result error => $e'); + rethrow; + } + } + + @override + Future> getBalance({ + required String address, + String? caip2chain, + }) async { + final uri = Uri.parse('$_baseUrl/account/$address/balance'); + final queryParams = { + ..._requiredParams, + 'currency': 'usd', + if (caip2chain != null) 'chainId': caip2chain, + // 'forceUpdate': , + }; + final url = uri.replace(queryParameters: queryParams); + final response = await http.get(url, headers: _requiredHeaders); + _core.logger.i('[$runtimeType] getBalance $url => ${response.body}'); + if (response.statusCode == 200 && response.body.isNotEmpty) { + final result = jsonDecode(response.body) as Map; + final balances = result['balances'] as List; + _tokensList = balances.map((e) => TokenBalance.fromJson(e)).toList(); + return _tokensList!; + } + try { + final reason = _parseResponseError(response.body); + throw Exception(reason); } catch (e) { - _core.logger.e('[$runtimeType] getIdentity $address error => $e'); + _core.logger.e('[$runtimeType] getBalance, decode result error => $e'); rethrow; } } @override - Future getBalance({ + Future getTokenBalance({ required String address, required String namespace, required String chainId, }) async { final uri = Uri.parse(_baseUrl); final queryParams = {..._requiredParams, 'chainId': '$namespace:$chainId'}; + final url = uri.replace(queryParameters: queryParams); + final body = jsonEncode({ + 'id': 1, + 'jsonrpc': '2.0', + 'method': _balanceMetod(namespace), + 'params': [ + address, + if (namespace == NetworkUtils.eip155) 'latest', + ], + }); final response = await http.post( - uri.replace(queryParameters: queryParams), - headers: {..._requiredHeaders, 'Content-Type': 'application/json'}, - body: jsonEncode({ - 'id': 1, - 'jsonrpc': '2.0', - 'method': _balanceMetod(namespace), - 'params': [ - address, - if (namespace == NetworkUtils.eip155) 'latest', - ], - }), + url, + headers: _requiredHeaders, + body: body, ); _core.logger.i( - '[$runtimeType] getBalance $namespace, $chainId, $address => ${response.body}', + '[$runtimeType] getTokenBalance $url, $body => ${response.body}', ); if (response.statusCode == 200 && response.body.isNotEmpty) { try { return _parseBalanceResult(namespace, response.body); } catch (e) { - _core.logger.e('[$runtimeType] getBalance, parse result error => $e'); + _core.logger.e('[$runtimeType] getTokenBalance, parse error => $e'); throw Exception('Failed to load balance. $e'); } } try { - final errorData = jsonDecode(response.body) as Map; - final reasons = errorData['reasons'] as List; - final reason = reasons.isNotEmpty - ? reasons.first['description'] ?? '' - : response.body; + final reason = _parseResponseError(response.body); throw Exception(reason); } catch (e) { - _core.logger.e('[$runtimeType] getBalance, decode result error => $e'); + _core.logger.e('[$runtimeType] getTokenBalance, decode error => $e'); rethrow; } } @override - Future getActivity({ - required String address, - String? cursor, + Future gasPrice({required String caip2chain}) async { + final uri = Uri.parse('$_baseUrl/convert/gas-price'); + final queryParams = {..._requiredParams, 'chainId': caip2chain}; + final url = uri.replace(queryParameters: queryParams); + final response = await http.get(url, headers: _requiredHeaders); + _core.logger.i('[$runtimeType] gasPrice $url => ${response.body}'); + if (response.statusCode == 200 && response.body.isNotEmpty) { + final result = jsonDecode(response.body) as Map; + return GasPrice.fromJson(result); + } + try { + final reason = _parseResponseError(response.body); + throw Exception(reason); + } catch (e) { + _core.logger.e('[$runtimeType] gasPrice, decode result error => $e'); + rethrow; + } + } + + @override + Future estimateGas({ + required Map transaction, + required String caip2Chain, }) async { - final uri = Uri.parse('$_baseUrl/account/$address/history'); - final queryParams = { - ..._requiredParams, - if (cursor != null) 'cursor': cursor, - }; + final uri = Uri.parse(_baseUrl); + final queryParams = {..._requiredParams, 'chainId': caip2Chain}; final url = uri.replace(queryParameters: queryParams); - final response = await http.get(url, headers: { - ..._requiredHeaders, - 'Content-Type': 'application/json', + final body = jsonEncode({ + 'jsonrpc': '2.0', + 'method': 'eth_estimateGas', + 'params': [transaction], + 'id': 1, }); - _core.logger.d('[$runtimeType] getActivity $url, ${response.statusCode}'); + final response = await http.post( + url, + headers: _requiredHeaders, + body: body, + ); + _core.logger.i( + '[$runtimeType] estimateGas $url, $body => ${response.body}', + ); if (response.statusCode == 200 && response.body.isNotEmpty) { try { - return ActivityData.fromRawJson(response.body); + return _parseEstimateGasResult(response.body); + } on JsonRpcError catch (e) { + _core.logger.e('[$runtimeType] estimateGas, parse error => $e'); + if ((e.message ?? '') + .toLowerCase() + .contains('insufficient funds for gas')) { + throw 'Insufficient funds for gas'; + } + throw 'Failed to estimate gas'; } catch (e) { - _core.logger.e('[$runtimeType] getActivity, parse result error => $e'); - throw Exception('Failed to load wallet activity. $e'); + _core.logger.e('[$runtimeType] estimateGas, parse error => $e'); + throw 'Failed to estimate gas.'; } } try { - final errorData = jsonDecode(response.body) as Map; - final reasons = errorData['reasons'] as List; - final reason = reasons.isNotEmpty - ? reasons.first['description'] ?? '' - : response.body; + final reason = _parseResponseError(response.body); throw Exception(reason); } catch (e) { - _core.logger.e('[$runtimeType] getActivity, decode result error => $e'); + _core.logger.e('[$runtimeType] getBalance, decode error => $e'); rethrow; } } + @override + Future checkAllowance({ + required String senderAddress, + required String receiverAddress, + required String contractAddress, + required String caip2Chain, + }) async { + // Keccak-256 of "allowance(address,address)" + final functionSelector = 'dd62ed3e'; + final ownerPadded = senderAddress.replaceFirst('0x', '').padLeft(64, '0'); + final spenderPadded = + receiverAddress.replaceFirst('0x', '').padLeft(64, '0'); + final data = '0x$functionSelector$ownerPadded$spenderPadded'; + // + final uri = Uri.parse(_baseUrl); + final queryParams = {..._requiredParams, 'chainId': caip2Chain}; + final url = uri.replace(queryParameters: queryParams); + final body = jsonEncode({ + 'jsonrpc': '2.0', + 'id': 1, + 'method': 'eth_call', + 'params': [ + {'to': contractAddress, 'data': data}, + 'latest' + ] + }); + final response = await http.post( + url, + headers: _requiredHeaders, + body: body, + ); + _core.logger.i( + '[$runtimeType] checkAllowance $url, $body => ${response.body}', + ); + if (response.statusCode == 200 && response.body.isNotEmpty) { + try { + final namespace = NamespaceUtils.getNamespaceFromChain(caip2Chain); + return _parseBalanceResult(namespace, response.body); + } on JsonRpcError catch (e) { + _core.logger.e('[$runtimeType] checkAllowance, parse error => $e'); + throw 'Failed checking allowance'; + } catch (e) { + _core.logger.e('[$runtimeType] checkAllowance, parse error => $e'); + throw 'Failed checking allowance'; + } + } + try { + final reason = _parseResponseError(response.body); + throw Exception(reason); + } catch (e) { + _core.logger.e('[$runtimeType] checkAllowance, decode error => $e'); + rethrow; + } + } + + @override + void dispose() { + _activityData = null; + _selectedToken = null; + _tokensList?.clear(); + } + T _parseRpcResultAs(String body) { try { final result = Map.from({ @@ -187,4 +345,21 @@ class BlockChainService implements IBlockChainService { } return 0.0; } + + BigInt _parseEstimateGasResult(String gasResult) { + try { + final result = _parseRpcResultAs(gasResult); + return hexToInt(result); + } catch (e) { + rethrow; + } + } + + String _parseResponseError(String responseBody) { + final errorData = jsonDecode(responseBody) as Map; + final reasons = errorData['reasons'] as List; + return reasons.isNotEmpty + ? reasons.first['description'] ?? '' + : responseBody; + } } diff --git a/packages/reown_appkit/lib/modal/services/blockchain_service/i_blockchain_service.dart b/packages/reown_appkit/lib/modal/services/blockchain_service/i_blockchain_service.dart index de97253..a6bb478 100644 --- a/packages/reown_appkit/lib/modal/services/blockchain_service/i_blockchain_service.dart +++ b/packages/reown_appkit/lib/modal/services/blockchain_service/i_blockchain_service.dart @@ -1,20 +1,53 @@ import 'package:reown_appkit/modal/services/blockchain_service/models/blockchain_identity.dart'; +import 'package:reown_appkit/modal/services/blockchain_service/models/gas_price.dart'; +import 'package:reown_appkit/modal/services/blockchain_service/models/token_balance.dart'; import 'package:reown_appkit/modal/services/blockchain_service/models/wallet_activity.dart'; abstract class IBlockChainService { + ActivityData? get activityData; + List? get tokensList; + TokenBalance? get selectedSendToken; + Future init(); /// Gets the name and avatar of a provided address on the given chain - Future getIdentity(String address); + Future getIdentity({ + required String address, + }); + + Future getHistory({ + required String address, + String? cursor, + }); + + Future> getBalance({ + required String address, + String? caip2chain, + }); + + void selectSendToken(TokenBalance token); - Future getBalance({ + Future gasPrice({ + required String caip2chain, + }); + + Future getTokenBalance({ required String address, required String namespace, required String chainId, }); - Future getActivity({ - required String address, - String? cursor, + Future estimateGas({ + required Map transaction, + required String caip2Chain, + }); + + Future checkAllowance({ + required String senderAddress, + required String receiverAddress, + required String contractAddress, + required String caip2Chain, }); + + void dispose(); } diff --git a/packages/reown_appkit/lib/modal/services/blockchain_service/models/gas_price.dart b/packages/reown_appkit/lib/modal/services/blockchain_service/models/gas_price.dart new file mode 100644 index 0000000..11b707c --- /dev/null +++ b/packages/reown_appkit/lib/modal/services/blockchain_service/models/gas_price.dart @@ -0,0 +1,36 @@ +import 'dart:convert'; + +class GasPrice { + final BigInt? standard; + final BigInt? fast; + final BigInt? instant; + + GasPrice({ + this.standard, + this.fast, + this.instant, + }); + + factory GasPrice.fromRawJson(String str) => + GasPrice.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory GasPrice.fromJson(Map json) => GasPrice( + standard: json['standard']?.toString().bigInt(), + fast: json['fast']?.toString().bigInt(), + instant: json['instant']?.toString().bigInt(), + ); + + Map toJson() => { + 'standard': standard, + 'fast': fast, + 'instant': instant, + }; +} + +extension on String? { + BigInt bigInt() { + return BigInt.from(num.parse(this ?? '0')); + } +} diff --git a/packages/reown_appkit/lib/modal/services/blockchain_service/models/token_balance.dart b/packages/reown_appkit/lib/modal/services/blockchain_service/models/token_balance.dart new file mode 100644 index 0000000..e96c376 --- /dev/null +++ b/packages/reown_appkit/lib/modal/services/blockchain_service/models/token_balance.dart @@ -0,0 +1,77 @@ +import 'dart:convert'; + +class TokenBalance { + final String? name; + final String? symbol; + final String? chainId; + final String? address; + final double? value; + final double? price; + final Quantity? quantity; + final String? iconUrl; + + TokenBalance({ + this.name, + this.symbol, + this.chainId, + this.address, + this.value, + this.price, + this.quantity, + this.iconUrl, + }); + + factory TokenBalance.fromRawJson(String str) => + TokenBalance.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory TokenBalance.fromJson(Map json) => TokenBalance( + name: json['name'], + symbol: json['symbol'], + chainId: json['chainId'], + address: json['address'], + value: json['value']?.toDouble(), + price: json['price']?.toDouble(), + quantity: json['quantity'] == null + ? null + : Quantity.fromJson(json['quantity']), + iconUrl: json['iconUrl'], + ); + + Map toJson() => { + 'name': name, + 'symbol': symbol, + 'chainId': chainId, + 'address': address, + 'value': value, + 'price': price, + 'quantity': quantity?.toJson(), + 'iconUrl': iconUrl, + }; +} + +class Quantity { + final String? decimals; + final String? numeric; + + Quantity({ + this.decimals, + this.numeric, + }); + + factory Quantity.fromRawJson(String str) => + Quantity.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Quantity.fromJson(Map json) => Quantity( + decimals: json['decimals'], + numeric: json['numeric'], + ); + + Map toJson() => { + 'decimals': decimals, + 'numeric': numeric, + }; +} diff --git a/packages/reown_appkit/lib/modal/utils/core_utils.dart b/packages/reown_appkit/lib/modal/utils/core_utils.dart index 3722982..e7d0966 100644 --- a/packages/reown_appkit/lib/modal/utils/core_utils.dart +++ b/packages/reown_appkit/lib/modal/utils/core_utils.dart @@ -82,7 +82,7 @@ class CoreUtils { return Uri.parse('${plainAppUrl}wc?uri=$encodedWcUrl'); } - static String formatChainBalance(double? chainBalance, {int precision = 3}) { + static String formatChainBalance(double? chainBalance, {int precision = 4}) { if (chainBalance == null) { return '_.'.padRight(precision + 1, '_'); } @@ -90,17 +90,18 @@ class CoreUtils { return '0.'.padRight(precision + 2, '0'); } - if (chainBalance.toInt() <= 0) { - return chainBalance.toStringAsPrecision(precision) + if (chainBalance.toInt() > 0) { + return chainBalance.toStringAsFixed(precision - 1) ..replaceAll(RegExp(r'([.]*0+)(?!.*\d)'), ''); } - return chainBalance.toStringAsFixed(2); + return chainBalance.toStringAsFixed(precision) + ..replaceAll(RegExp(r'([.]*0+)(?!.*\d)'), ''); } - static String formatStringBalance(String stringValue) { + static String formatStringBalance(String stringValue, {int precision = 4}) { final value = double.tryParse(stringValue) ?? double.parse('0'); - return formatChainBalance(value); + return formatChainBalance(value, precision: precision); } static String getUserAgent() { diff --git a/packages/reown_appkit/lib/modal/widgets/avatars/account_avatar.dart b/packages/reown_appkit/lib/modal/widgets/avatars/account_avatar.dart index 211c368..bc1a4af 100644 --- a/packages/reown_appkit/lib/modal/widgets/avatars/account_avatar.dart +++ b/packages/reown_appkit/lib/modal/widgets/avatars/account_avatar.dart @@ -69,7 +69,7 @@ class _AccountAvatarState extends State { void _modalNotifyListener() { setState(() { try { - _avatarUrl = widget.appKit.avatarUrl; + _avatarUrl = widget.appKit.blockchainIdentity?.avatar; final chainId = widget.appKit.selectedChain?.chainId ?? ''; final namespace = ReownAppKitModalNetworks.getNamespaceForChainId( chainId, @@ -103,7 +103,7 @@ class GradientOrb extends StatelessWidget { borderRadius: BorderRadius.circular(size / 2.0), boxShadow: [ BoxShadow( - color: themeColors.grayGlass025, + color: themeColors.grayGlass005, spreadRadius: 1.0, blurRadius: 0.0, ), diff --git a/packages/reown_appkit/lib/modal/widgets/avatars/account_orb.dart b/packages/reown_appkit/lib/modal/widgets/avatars/account_orb.dart index 2f1492c..5f18472 100644 --- a/packages/reown_appkit/lib/modal/widgets/avatars/account_orb.dart +++ b/packages/reown_appkit/lib/modal/widgets/avatars/account_orb.dart @@ -4,8 +4,13 @@ import 'package:reown_appkit/modal/widgets/avatars/account_avatar.dart'; import 'package:reown_appkit/modal/widgets/modal_provider.dart'; class Orb extends StatelessWidget { - const Orb({super.key, this.size = 70.0}); + const Orb({ + super.key, + this.size = 70.0, + this.border = 8.0, + }); final double size; + final double border; @override Widget build(BuildContext context) { @@ -18,12 +23,12 @@ class Orb extends StatelessWidget { borderRadius: BorderRadius.circular(size / 2), border: Border.all( color: themeColors.grayGlass005, - width: 8.0, + width: border, ), ), child: AccountAvatar( appKit: service, - size: size - 8.0, + size: size - border, ), ); } diff --git a/packages/reown_appkit/lib/modal/widgets/buttons/address_button.dart b/packages/reown_appkit/lib/modal/widgets/buttons/address_button.dart index af411aa..b9956e6 100644 --- a/packages/reown_appkit/lib/modal/widgets/buttons/address_button.dart +++ b/packages/reown_appkit/lib/modal/widgets/buttons/address_button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:reown_appkit/modal/i_appkit_modal_impl.dart'; import 'package:reown_appkit/modal/utils/render_utils.dart'; import 'package:reown_appkit/modal/widgets/avatars/account_avatar.dart'; @@ -10,11 +11,15 @@ class AddressButton extends StatefulWidget { super.key, required this.service, this.size = BaseButtonSize.regular, + this.assetPath, this.onTap, + this.child, }); final IReownAppKitModal service; final BaseButtonSize size; final VoidCallback? onTap; + final String? assetPath; + final Widget? child; @override State createState() => _AddressButtonState(); @@ -53,17 +58,16 @@ class _AddressButtonState extends State { @override Widget build(BuildContext context) { final themeColors = ReownAppKitModalTheme.colorsOf(context); + final identityName = + (widget.service.blockchainIdentity?.name ?? '').isNotEmpty + ? widget.service.blockchainIdentity!.name! + : null; return BaseButton( size: widget.size, onTap: widget.onTap, buttonStyle: ButtonStyle( - backgroundColor: WidgetStateProperty.resolveWith( - (states) { - if (states.contains(WidgetState.disabled)) { - return themeColors.grayGlass005; - } - return themeColors.grayGlass010; - }, + backgroundColor: MaterialStateProperty.resolveWith( + (states) => themeColors.grayGlass002, ), foregroundColor: WidgetStateProperty.resolveWith( (states) { @@ -76,32 +80,47 @@ class _AddressButtonState extends State { shape: WidgetStateProperty.resolveWith( (states) { return RoundedRectangleBorder( - side: states.contains(WidgetState.disabled) - ? BorderSide(color: themeColors.grayGlass005, width: 1.0) - : BorderSide(color: themeColors.grayGlass010, width: 1.0), + side: BorderSide( + color: themeColors.grayGlass002, + width: 1.0, + ), borderRadius: BorderRadius.circular(widget.size.height / 2), ); }, ), ), - overridePadding: WidgetStateProperty.all( - EdgeInsets.only( - left: 6.0, - right: widget.size == BaseButtonSize.small ? 12.0 : 16.0, - ), + overridePadding: MaterialStateProperty.all( + widget.child != null + ? const EdgeInsets.all(0.0) + : EdgeInsets.only( + left: 6.0, + right: widget.size == BaseButtonSize.small ? 12.0 : 16.0, + ), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - AccountAvatar( - appKit: widget.service, - size: widget.size.height * 0.7, - disabled: widget.onTap == null, + child: widget.child ?? + Row( + mainAxisSize: MainAxisSize.min, + children: [ + AccountAvatar( + appKit: widget.service, + size: widget.size.height * 0.6, + disabled: widget.onTap == null, + ), + const SizedBox.square(dimension: 4.0), + Text(identityName ?? RenderUtils.truncate(_address ?? '')), + const SizedBox.square(dimension: 8.0), + SvgPicture.asset( + widget.assetPath ?? 'lib/modal/assets/icons/chevron_down.svg', + package: 'reown_appkit', + colorFilter: ColorFilter.mode( + themeColors.foreground200, + BlendMode.srcIn, + ), + width: 14.0, + height: 14.0, + ), + ], ), - const SizedBox.square(dimension: 4.0), - Text(RenderUtils.truncate(_address ?? '')), - ], - ), ); } } diff --git a/packages/reown_appkit/lib/modal/widgets/buttons/address_copy_button.dart b/packages/reown_appkit/lib/modal/widgets/buttons/address_copy_button.dart index 21c4099..51b59f6 100644 --- a/packages/reown_appkit/lib/modal/widgets/buttons/address_copy_button.dart +++ b/packages/reown_appkit/lib/modal/widgets/buttons/address_copy_button.dart @@ -15,16 +15,16 @@ class AddressCopyButton extends StatelessWidget { @override Widget build(BuildContext context) { - final service = ModalProvider.of(context).instance; + final appKitModal = ModalProvider.of(context).instance; final themeData = ReownAppKitModalTheme.getDataOf(context); final themeColors = ReownAppKitModalTheme.colorsOf(context); return GestureDetector( onTap: () async { - final chainId = service.selectedChain!.chainId; + final chainId = appKitModal.selectedChain!.chainId; final namespace = ReownAppKitModalNetworks.getNamespaceForChainId( chainId, ); - final address = service.session!.getAddress(namespace)!; + final address = appKitModal.session!.getAddress(namespace)!; await Clipboard.setData(ClipboardData(text: address)); GetIt.I().show(ToastMessage( type: ToastType.success, @@ -34,8 +34,8 @@ class AddressCopyButton extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Address( - service: service, + AddressText( + appKitModal: appKitModal, style: themeData.textStyles.title600.copyWith( color: themeColors.foreground100, ), @@ -48,8 +48,8 @@ class AddressCopyButton extends StatelessWidget { themeColors.foreground250, BlendMode.srcIn, ), - width: 20.0, - height: 20.0, + width: 16.0, + height: 16.0, ), ], ), diff --git a/packages/reown_appkit/lib/modal/widgets/buttons/base_button.dart b/packages/reown_appkit/lib/modal/widgets/buttons/base_button.dart index c722e4b..3aceb26 100644 --- a/packages/reown_appkit/lib/modal/widgets/buttons/base_button.dart +++ b/packages/reown_appkit/lib/modal/widgets/buttons/base_button.dart @@ -48,8 +48,8 @@ class BaseButton extends StatelessWidget { Widget build(BuildContext context) { final themeData = ReownAppKitModalTheme.getDataOf(context); final textStyle = size == BaseButtonSize.small - ? themeData.textStyles.small600 - : themeData.textStyles.paragraph600; + ? themeData.textStyles.small500 + : themeData.textStyles.paragraph500; return FilledButton( onPressed: onTap, child: child, diff --git a/packages/reown_appkit/lib/modal/widgets/buttons/connect_button.dart b/packages/reown_appkit/lib/modal/widgets/buttons/connect_button.dart index a5db069..ebb599c 100644 --- a/packages/reown_appkit/lib/modal/widgets/buttons/connect_button.dart +++ b/packages/reown_appkit/lib/modal/widgets/buttons/connect_button.dart @@ -50,10 +50,10 @@ class ConnectButton extends StatelessWidget { backgroundColor: WidgetStateProperty.resolveWith( (states) { if (connecting) { - return themeColors.grayGlass010; + return themeColors.grayGlass002; } - if (states.contains(WidgetState.disabled)) { - return themeColors.grayGlass005; + if (states.contains(MaterialState.disabled)) { + return themeColors.grayGlass002; } return themeColors.accent100; }, @@ -72,8 +72,11 @@ class ConnectButton extends StatelessWidget { shape: WidgetStateProperty.resolveWith( (states) { return RoundedRectangleBorder( - side: (states.contains(WidgetState.disabled) || connecting) - ? BorderSide(color: themeColors.grayGlass010, width: 1.0) + side: (states.contains(MaterialState.disabled) || connecting) + ? BorderSide( + color: themeColors.grayGlass002, + width: 1.0, + ) : BorderSide.none, borderRadius: BorderRadius.circular(borderRadius), ); diff --git a/packages/reown_appkit/lib/modal/widgets/buttons/network_button.dart b/packages/reown_appkit/lib/modal/widgets/buttons/network_button.dart index bae6aa8..e1a9298 100644 --- a/packages/reown_appkit/lib/modal/widgets/buttons/network_button.dart +++ b/packages/reown_appkit/lib/modal/widgets/buttons/network_button.dart @@ -15,15 +15,23 @@ import 'package:reown_appkit/modal/widgets/circular_loader.dart'; class NetworkButton extends StatelessWidget { const NetworkButton({ super.key, + this.title, this.size = BaseButtonSize.regular, this.serviceStatus = ReownAppKitModalStatus.idle, this.chainInfo, this.onTap, + this.iconOnly = false, + this.iconOnRight = false, + this.iconUrl, }); + final String? title; final ReownAppKitModalNetworkInfo? chainInfo; final BaseButtonSize size; final ReownAppKitModalStatus serviceStatus; final VoidCallback? onTap; + final bool iconOnly; + final bool iconOnRight; + final String? iconUrl; String _getImageUrl(ReownAppKitModalNetworkInfo? chainInfo) { if (chainInfo == null) return ''; @@ -43,70 +51,112 @@ class NetworkButton extends StatelessWidget { final imageUrl = _getImageUrl(chainInfo); final radiuses = ReownAppKitModalTheme.radiusesOf(context); final borderRadius = radiuses.isSquare() ? 0.0 : size.height / 2; - return BaseButton( - size: size, - onTap: serviceStatus.isInitialized ? onTap : null, - buttonStyle: ButtonStyle( - backgroundColor: WidgetStateProperty.resolveWith( - (states) { - if (states.contains(WidgetState.disabled)) { - return themeColors.grayGlass002; - } - return themeColors.grayGlass005; - }, - ), - foregroundColor: WidgetStateProperty.resolveWith( - (states) { - if (states.contains(WidgetState.disabled)) { - return themeColors.grayGlass015; - } - return themeColors.foreground100; - }, - ), - shape: WidgetStateProperty.resolveWith( - (states) { - return RoundedRectangleBorder( - side: states.contains(WidgetState.disabled) - ? BorderSide(color: themeColors.grayGlass002, width: 1.0) - : BorderSide(color: themeColors.grayGlass005, width: 1.0), - borderRadius: BorderRadius.circular(borderRadius), - ); - }, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - serviceStatus.isLoading - ? Row( - children: [ - const SizedBox.square(dimension: kPadding6), - CircularLoader( - size: size.height * 0.4, - strokeWidth: size == BaseButtonSize.small ? 1.0 : 1.5, + return iconOnly + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + RoundedIcon( + assetPath: 'lib/modal/assets/icons/network.svg', + imageUrl: iconUrl ?? imageUrl, + size: size.height * 0.7, + assetColor: themeColors.inverse100, + padding: size == BaseButtonSize.small ? 5.0 : 6.0, + ), + ], + ) + : BaseButton( + size: size, + onTap: serviceStatus.isInitialized ? onTap : null, + buttonStyle: ButtonStyle( + backgroundColor: MaterialStateProperty.resolveWith( + (states) => themeColors.grayGlass002, + ), + foregroundColor: MaterialStateProperty.resolveWith( + (states) { + if (states.contains(MaterialState.disabled)) { + return themeColors.grayGlass015; + } + return themeColors.foreground100; + }, + ), + shape: MaterialStateProperty.resolveWith( + (states) { + return RoundedRectangleBorder( + side: BorderSide( + color: themeColors.grayGlass002, + width: 1.0, ), - const SizedBox.square(dimension: kPadding6), - ], - ) - : RoundedIcon( - assetPath: 'lib/modal/assets/icons/network.svg', - imageUrl: imageUrl, - size: size.height * 0.7, - assetColor: themeColors.inverse100, - padding: size == BaseButtonSize.small ? 5.0 : 6.0, + borderRadius: BorderRadius.circular(borderRadius), + ); + }, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (!iconOnRight) + Row( + children: [ + serviceStatus.isLoading + ? Row( + children: [ + const SizedBox.square(dimension: kPadding6), + CircularLoader( + size: size.height * 0.4, + strokeWidth: + size == BaseButtonSize.small ? 1.0 : 1.5, + ), + const SizedBox.square(dimension: kPadding6), + ], + ) + : RoundedIcon( + assetPath: 'lib/modal/assets/icons/network.svg', + imageUrl: iconUrl ?? imageUrl, + size: size.height * 0.55, + assetColor: themeColors.inverse100, + padding: size == BaseButtonSize.small ? 5.0 : 6.0, + ), + const SizedBox.square(dimension: 4.0), + ], + ), + Text( + (title ?? chainInfo?.name) ?? + (size == BaseButtonSize.small + ? UIConstants.selectNetworkShort + : UIConstants.selectNetwork), ), - const SizedBox.square(dimension: 4.0), - Text( - chainInfo?.name ?? - (size == BaseButtonSize.small - ? UIConstants.selectNetworkShort - : UIConstants.selectNetwork), - ), - ], - ), - overridePadding: WidgetStateProperty.all( - const EdgeInsets.only(left: 6.0, right: 16.0), - ), - ); + if (iconOnRight) + Row( + children: [ + const SizedBox.square(dimension: 4.0), + serviceStatus.isLoading + ? Row( + children: [ + const SizedBox.square(dimension: kPadding6), + CircularLoader( + size: size.height * 0.4, + strokeWidth: + size == BaseButtonSize.small ? 1.0 : 1.5, + ), + const SizedBox.square(dimension: kPadding6), + ], + ) + : RoundedIcon( + assetPath: 'lib/modal/assets/icons/network.svg', + imageUrl: iconUrl ?? imageUrl, + size: size.height * 0.55, + assetColor: themeColors.inverse100, + padding: size == BaseButtonSize.small ? 5.0 : 6.0, + ), + ], + ), + ], + ), + overridePadding: MaterialStateProperty.all( + !iconOnRight + ? const EdgeInsets.only(left: 6.0, right: 16.0) + : const EdgeInsets.only(left: 16.0, right: 6.0), + ), + ); } } diff --git a/packages/reown_appkit/lib/modal/widgets/buttons/secondary_button.dart b/packages/reown_appkit/lib/modal/widgets/buttons/secondary_button.dart index 996df21..48073d0 100644 --- a/packages/reown_appkit/lib/modal/widgets/buttons/secondary_button.dart +++ b/packages/reown_appkit/lib/modal/widgets/buttons/secondary_button.dart @@ -20,8 +20,8 @@ class SecondaryButton extends StatelessWidget { child: Text(title), onTap: onTap, buttonStyle: ButtonStyle( - backgroundColor: WidgetStateProperty.resolveWith( - (states) => themeColors.grayGlass001, + backgroundColor: MaterialStateProperty.resolveWith( + (states) => themeColors.grayGlass002, ), foregroundColor: WidgetStateProperty.resolveWith( (states) => themeColors.foreground200, @@ -30,7 +30,7 @@ class SecondaryButton extends StatelessWidget { (states) { return RoundedRectangleBorder( side: BorderSide( - color: themeColors.grayGlass010, + color: themeColors.grayGlass002, width: 1.0, ), borderRadius: radiuses.isSquare() diff --git a/packages/reown_appkit/lib/modal/widgets/buttons/simple_icon_button.dart b/packages/reown_appkit/lib/modal/widgets/buttons/simple_icon_button.dart index 27fcdfa..608d8ab 100644 --- a/packages/reown_appkit/lib/modal/widgets/buttons/simple_icon_button.dart +++ b/packages/reown_appkit/lib/modal/widgets/buttons/simple_icon_button.dart @@ -17,6 +17,7 @@ class SimpleIconButton extends StatelessWidget { this.size = BaseButtonSize.regular, this.overlayColor, this.withBorder = true, + this.borderRadius, }); final VoidCallback? onTap; final String title; @@ -27,23 +28,37 @@ class SimpleIconButton extends StatelessWidget { final BaseButtonSize size; final WidgetStateProperty? overlayColor; final bool withBorder; + final double? borderRadius; @override Widget build(BuildContext context) { final themeColors = ReownAppKitModalTheme.colorsOf(context); final textStyles = ReownAppKitModalTheme.getDataOf(context).textStyles; final radiuses = ReownAppKitModalTheme.radiusesOf(context); - final borderRadius = - radiuses.isSquare() ? 0.0 : (BaseButtonSize.regular.height / 2); + final radius = radiuses.isSquare() + ? 0.0 + : radiuses.isCircular() + ? 100.0 + : borderRadius ?? (size.height / 2); return BaseButton( onTap: onTap, size: size, buttonStyle: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - backgroundColor ?? themeColors.accent100, + backgroundColor: MaterialStateProperty.resolveWith( + (states) { + if (states.contains(MaterialState.disabled)) { + return themeColors.grayGlass005; + } + return backgroundColor ?? themeColors.accent100; + }, ), - foregroundColor: WidgetStateProperty.all( - foregroundColor ?? themeColors.inverse100, + foregroundColor: MaterialStateProperty.resolveWith( + (states) { + if (states.contains(MaterialState.disabled)) { + return themeColors.grayGlass005; + } + return foregroundColor ?? themeColors.inverse100; + }, ), overlayColor: overlayColor, shape: withBorder @@ -51,10 +66,10 @@ class SimpleIconButton extends StatelessWidget { (states) { return RoundedRectangleBorder( side: BorderSide( - color: themeColors.grayGlass010, + color: backgroundColor ?? themeColors.grayGlass010, width: 1.0, ), - borderRadius: BorderRadius.circular(borderRadius), + borderRadius: BorderRadius.circular(radius), ); }, ) @@ -73,7 +88,9 @@ class SimpleIconButton extends StatelessWidget { leftIcon!, package: 'reown_appkit', colorFilter: ColorFilter.mode( - foregroundColor ?? themeColors.inverse100, + onTap == null + ? themeColors.grayGlass025 + : foregroundColor ?? themeColors.inverse100, BlendMode.srcIn, ), width: iconSize ?? 14.0, @@ -97,7 +114,9 @@ class SimpleIconButton extends StatelessWidget { rightIcon!, package: 'reown_appkit', colorFilter: ColorFilter.mode( - foregroundColor ?? themeColors.inverse100, + onTap == null + ? themeColors.grayGlass025 + : foregroundColor ?? themeColors.inverse100, BlendMode.srcIn, ), width: iconSize ?? 14.0, diff --git a/packages/reown_appkit/lib/modal/widgets/circular_loader.dart b/packages/reown_appkit/lib/modal/widgets/circular_loader.dart index 87e104b..e8def9f 100644 --- a/packages/reown_appkit/lib/modal/widgets/circular_loader.dart +++ b/packages/reown_appkit/lib/modal/widgets/circular_loader.dart @@ -20,7 +20,7 @@ class CircularLoader extends StatelessWidget { width: size, height: size, child: CircularProgressIndicator( - color: themeColors.accent100, + color: themeColors.accenGlass080, strokeWidth: strokeWidth ?? 4.0, ), ); diff --git a/packages/reown_appkit/lib/modal/widgets/icons/rounded_icon.dart b/packages/reown_appkit/lib/modal/widgets/icons/rounded_icon.dart index 05a4e8d..217b340 100644 --- a/packages/reown_appkit/lib/modal/widgets/icons/rounded_icon.dart +++ b/packages/reown_appkit/lib/modal/widgets/icons/rounded_icon.dart @@ -14,6 +14,7 @@ class RoundedIcon extends StatelessWidget { this.assetColor, this.circleColor, this.borderColor, + this.borderWidth, this.size = 34.0, this.padding = 8.0, this.borderRadius, @@ -21,7 +22,7 @@ class RoundedIcon extends StatelessWidget { final String? assetPath, imageUrl; final Color? assetColor, circleColor, borderColor; final double size, padding; - final double? borderRadius; + final double? borderRadius, borderWidth; @override Widget build(BuildContext context) { @@ -35,8 +36,8 @@ class RoundedIcon extends StatelessWidget { borderRadius: BorderRadius.all(Radius.circular(radius)), border: Border.fromBorderSide( BorderSide( - color: borderColor ?? themeColors.grayGlass002, - width: 2, + color: borderColor ?? themeColors.grayGlass005, + width: borderWidth ?? 2.0, strokeAlign: BorderSide.strokeAlignOutside, ), ), diff --git a/packages/reown_appkit/lib/modal/widgets/lists/list_items/account_list_item.dart b/packages/reown_appkit/lib/modal/widgets/lists/list_items/account_list_item.dart index ad7028a..992bdcb 100644 --- a/packages/reown_appkit/lib/modal/widgets/lists/list_items/account_list_item.dart +++ b/packages/reown_appkit/lib/modal/widgets/lists/list_items/account_list_item.dart @@ -22,6 +22,7 @@ class AccountListItem extends StatelessWidget { this.hightlighted = false, this.flexible = false, this.padding, + this.backgroundColor, }); final Widget? iconWidget; final String? iconPath; @@ -35,6 +36,7 @@ class AccountListItem extends StatelessWidget { final bool hightlighted; final bool flexible; final EdgeInsets? padding; + final MaterialStateProperty? backgroundColor; @override Widget build(BuildContext context) { @@ -46,6 +48,7 @@ class AccountListItem extends StatelessWidget { hightlighted: hightlighted, padding: padding, flexible: flexible, + backgroundColor: backgroundColor, child: Row( children: [ iconWidget ?? const SizedBox.shrink(), diff --git a/packages/reown_appkit/lib/modal/widgets/miscellaneous/searchbar.dart b/packages/reown_appkit/lib/modal/widgets/miscellaneous/searchbar.dart index d5a8d47..94a0508 100644 --- a/packages/reown_appkit/lib/modal/widgets/miscellaneous/searchbar.dart +++ b/packages/reown_appkit/lib/modal/widgets/miscellaneous/searchbar.dart @@ -17,11 +17,13 @@ class ModalSearchBar extends StatefulWidget { this.iconPath = 'lib/modal/assets/icons/search.svg', this.prefixIcon, this.suffixIcon, + this.suffixWidth, this.textAlign, this.textInputType, this.textInputAction, this.onSubmitted, this.autofocus, + this.maxLines, this.onFocusChange, this.noIcons = false, this.showCursor = true, @@ -31,6 +33,7 @@ class ModalSearchBar extends StatefulWidget { this.width, this.height = kSearchFieldHeight, this.enabled = true, + this.borderOnFocus = true, this.inputFormatters, }); final Function(String) onTextChanged; @@ -41,11 +44,13 @@ class ModalSearchBar extends StatefulWidget { final String initialValue; final Widget? prefixIcon; final Widget? suffixIcon; + final double? suffixWidth; final TextAlign? textAlign; final TextInputType? textInputType; final TextInputAction? textInputAction; final Function(String)? onSubmitted; final bool? autofocus; + final int? maxLines; final Function(bool)? onFocusChange; final bool noIcons; final bool showCursor; @@ -55,6 +60,7 @@ class ModalSearchBar extends StatefulWidget { final double? width; final double height; final bool enabled; + final bool borderOnFocus; final List? inputFormatters; @override @@ -159,7 +165,9 @@ class _ModalSearchBarState extends State void _updateState() { if (_focusNode.hasFocus && !_hasFocus) { _hasFocus = _focusNode.hasFocus; - _animationController.forward(); + if (widget.borderOnFocus) { + _animationController.forward(); + } } if (!_focusNode.hasFocus && _hasFocus) { _hasFocus = _focusNode.hasFocus; @@ -199,12 +207,15 @@ class _ModalSearchBarState extends State ); return DecoratedBoxTransition( - decoration: _decorationTween.animate(_animationController), + decoration: _decorationTween.animate( + _animationController, + ), child: Container( height: widget.height + 8.0, width: widget.width, padding: const EdgeInsets.all(4.0), child: TextFormField( + maxLines: widget.maxLines ?? 1, keyboardType: widget.textInputType ?? TextInputType.text, textInputAction: widget.textInputAction ?? TextInputAction.unspecified, @@ -268,14 +279,20 @@ class _ModalSearchBarState extends State maxWidth: 40.0, minWidth: widget.noIcons ? 0.0 : 40.0, ), - labelStyle: themeData.textStyles.paragraph500.copyWith( - color: themeColors.inverse100, - ), + labelStyle: widget.textStyle?.copyWith( + color: themeColors.inverse100, + ) ?? + themeData.textStyles.paragraph500.copyWith( + color: themeColors.inverse100, + ), hintText: widget.hint, - hintStyle: themeData.textStyles.paragraph500.copyWith( - color: themeColors.foreground275, - height: 1.5, - ), + hintStyle: widget.textStyle?.copyWith( + color: themeColors.foreground275, + ) ?? + themeData.textStyles.paragraph500.copyWith( + color: themeColors.foreground275, + height: 1.5, + ), suffixIcon: widget.suffixIcon ?? (_controller.value.text.isNotEmpty || _focusNode.hasFocus ? Column( @@ -308,14 +325,15 @@ class _ModalSearchBarState extends State suffixIconConstraints: BoxConstraints( maxHeight: widget.height, minHeight: widget.height, - maxWidth: 36.0, - minWidth: widget.noIcons ? 0.0 : 36.0, + maxWidth: widget.suffixWidth ?? 36.0, + minWidth: widget.suffixWidth ?? (widget.noIcons ? 0.0 : 36.0), ), border: unfocusedBorder, errorBorder: unfocusedBorder, enabledBorder: unfocusedBorder, disabledBorder: disabledBorder, - focusedBorder: focusedBorder, + focusedBorder: + widget.borderOnFocus ? focusedBorder : unfocusedBorder, filled: true, fillColor: themeColors.grayGlass002, contentPadding: const EdgeInsets.all(0.0), diff --git a/packages/reown_appkit/lib/modal/widgets/miscellaneous/segmented_control.dart b/packages/reown_appkit/lib/modal/widgets/miscellaneous/segmented_control.dart index 56f4527..9f66820 100644 --- a/packages/reown_appkit/lib/modal/widgets/miscellaneous/segmented_control.dart +++ b/packages/reown_appkit/lib/modal/widgets/miscellaneous/segmented_control.dart @@ -1,26 +1,36 @@ import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:reown_appkit/modal/theme/public/appkit_modal_theme.dart'; enum SegmentOption { - mobile, - browser, + option1, + option2, } class SegmentedControl extends StatefulWidget { const SegmentedControl({ super.key, + this.width, this.onChange, + this.option1Title = 'Mobile', + this.option1Icon = 'lib/modal/assets/icons/mobile.svg', + this.option2Title = 'Browser', + this.option2Icon = 'lib/modal/assets/icons/extension.svg', }); final Function(SegmentOption option)? onChange; + final double? width; + final String option1Title, option2Title; + final String option1Icon, option2Icon; @override State createState() => _SegmentedControlState(); } class _SegmentedControlState extends State { - SegmentOption _selectedSegment = SegmentOption.mobile; + SegmentOption _selectedSegment = SegmentOption.option1; @override Widget build(BuildContext context) { @@ -30,55 +40,59 @@ class _SegmentedControlState extends State { return SizedBox( height: 32.0, child: CustomSlidingSegmentedControl( - initialValue: SegmentOption.mobile, - fixedWidth: 100.0, + initialValue: SegmentOption.option1, + fixedWidth: widget.width ?? 100.0, children: { - SegmentOption.mobile: Row( + SegmentOption.option1: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - SvgPicture.asset( - 'lib/modal/assets/icons/mobile.svg', - package: 'reown_appkit', - colorFilter: ColorFilter.mode( - _selectedSegment == SegmentOption.mobile - ? themeColors.foreground100 - : themeColors.foreground200, - BlendMode.srcIn, + Visibility( + visible: widget.option1Icon.isNotEmpty, + child: SvgPicture.asset( + widget.option1Icon, + package: 'reown_appkit', + colorFilter: ColorFilter.mode( + _selectedSegment == SegmentOption.option1 + ? themeColors.foreground100 + : themeColors.foreground200, + BlendMode.srcIn, + ), + height: 14.0, ), - height: 14.0, ), - const SizedBox.square(dimension: 4.0), Text( - 'Mobile', + ' ${widget.option1Title}', style: themeData.textStyles.small500.copyWith( - color: _selectedSegment == SegmentOption.mobile + color: _selectedSegment == SegmentOption.option1 ? themeColors.foreground100 : themeColors.foreground200, ), ), ], ), - SegmentOption.browser: Row( + SegmentOption.option2: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - SvgPicture.asset( - 'lib/modal/assets/icons/extension.svg', - package: 'reown_appkit', - colorFilter: ColorFilter.mode( - _selectedSegment == SegmentOption.browser - ? themeColors.foreground100 - : themeColors.foreground200, - BlendMode.srcIn, + Visibility( + visible: widget.option2Icon.isNotEmpty, + child: SvgPicture.asset( + widget.option2Icon, + package: 'reown_appkit', + colorFilter: ColorFilter.mode( + _selectedSegment == SegmentOption.option2 + ? themeColors.foreground100 + : themeColors.foreground200, + BlendMode.srcIn, + ), + height: 14.0, ), - height: 14.0, ), - const SizedBox.square(dimension: 4.0), Text( - 'Browser', + ' ${widget.option2Title}', style: themeData.textStyles.small500.copyWith( - color: _selectedSegment == SegmentOption.browser + color: _selectedSegment == SegmentOption.option2 ? themeColors.foreground100 : themeColors.foreground200, ), diff --git a/packages/reown_appkit/lib/modal/widgets/navigation/navbar_action_button.dart b/packages/reown_appkit/lib/modal/widgets/navigation/navbar_action_button.dart index d06fd82..b516e09 100644 --- a/packages/reown_appkit/lib/modal/widgets/navigation/navbar_action_button.dart +++ b/packages/reown_appkit/lib/modal/widgets/navigation/navbar_action_button.dart @@ -6,36 +6,39 @@ import 'package:reown_appkit/modal/theme/public/appkit_modal_theme.dart'; class NavbarActionButton extends StatelessWidget { const NavbarActionButton({ super.key, - required this.asset, - required this.action, + this.action, + this.asset = '', + this.child, this.color, this.dimension = kNavbarHeight, }); final String asset; - final VoidCallback action; + final VoidCallback? action; final Color? color; final double dimension; + final Widget? child; @override Widget build(BuildContext context) { final themeColors = ReownAppKitModalTheme.colorsOf(context); return SizedBox.square( dimension: dimension, - child: IconButton( - onPressed: action, - padding: const EdgeInsets.all(0.0), - visualDensity: VisualDensity.compact, - icon: SvgPicture.asset( - asset, - package: 'reown_appkit', - colorFilter: ColorFilter.mode( - color ?? themeColors.foreground100, - BlendMode.srcIn, + child: child ?? + IconButton( + onPressed: action, + padding: const EdgeInsets.all(0.0), + visualDensity: VisualDensity.compact, + icon: SvgPicture.asset( + asset, + package: 'reown_appkit', + colorFilter: ColorFilter.mode( + color ?? themeColors.foreground100, + BlendMode.srcIn, + ), + width: 18.0, + height: 18.0, + ), ), - width: 18.0, - height: 18.0, - ), - ), ); } } diff --git a/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_account_button.dart b/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_account_button.dart index 5c700b4..6f0d91e 100644 --- a/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_account_button.dart +++ b/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_account_button.dart @@ -130,13 +130,8 @@ class _AppKitModalAccountButtonState extends State { const EdgeInsets.only(left: 4.0, right: 4.0), ), buttonStyle: ButtonStyle( - backgroundColor: WidgetStateProperty.resolveWith( - (states) { - if (states.contains(WidgetState.disabled)) { - return themeColors.grayGlass002; - } - return themeColors.grayGlass005; - }, + backgroundColor: MaterialStateProperty.resolveWith( + (states) => themeColors.grayGlass002, ), foregroundColor: WidgetStateProperty.resolveWith( (states) { @@ -149,9 +144,7 @@ class _AppKitModalAccountButtonState extends State { shape: WidgetStateProperty.resolveWith( (states) { return RoundedRectangleBorder( - side: states.contains(WidgetState.disabled) - ? BorderSide(color: themeColors.grayGlass002, width: 1.0) - : BorderSide(color: themeColors.grayGlass005, width: 1.0), + side: BorderSide(color: themeColors.grayGlass002, width: 1.0), borderRadius: BorderRadius.circular(borderRadius), ); }, @@ -216,8 +209,8 @@ class _BalanceButton extends StatelessWidget { backgroundColor: WidgetStateProperty.all(Colors.transparent), foregroundColor: WidgetStateProperty.resolveWith( (states) { - if (states.contains(WidgetState.disabled)) { - return themeColors.grayGlass015; + if (states.contains(MaterialState.disabled)) { + return themeColors.grayGlass005; } return themeColors.foreground100; }, diff --git a/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_address_button.dart b/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_address_button.dart index acd4fb4..3671382 100644 --- a/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_address_button.dart +++ b/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_address_button.dart @@ -3,6 +3,7 @@ import 'package:reown_appkit/modal/i_appkit_modal_impl.dart'; import 'package:reown_appkit/modal/utils/render_utils.dart'; import 'package:reown_appkit/modal/widgets/avatars/account_avatar.dart'; import 'package:reown_appkit/modal/widgets/buttons/base_button.dart'; +import 'package:reown_appkit/modal/widgets/circular_loader.dart'; import 'package:reown_appkit/reown_appkit.dart'; class AppKitModalAddressButton extends StatelessWidget { @@ -30,6 +31,9 @@ class AppKitModalAddressButton extends StatelessWidget { if ((address ?? '').isEmpty) { return SizedBox.shrink(); } + final identityName = (appKitModal.blockchainIdentity?.name ?? '').isNotEmpty + ? appKitModal.blockchainIdentity!.name! + : null; final themeData = ReownAppKitModalTheme.getDataOf(context); final textStyle = size == BaseButtonSize.small ? themeData.textStyles.small600 @@ -37,6 +41,7 @@ class AppKitModalAddressButton extends StatelessWidget { final themeColors = ReownAppKitModalTheme.colorsOf(context); final radiuses = ReownAppKitModalTheme.radiusesOf(context); final innerBorderRadius = radiuses.isSquare() ? 0.0 : size.height / 2; + // TODO replace with AddressButton() return Padding( padding: EdgeInsets.only( top: size == BaseButtonSize.small ? 4.0 : 0.0, @@ -44,21 +49,16 @@ class AppKitModalAddressButton extends StatelessWidget { ), child: BaseButton( size: size, - onTap: onTap, - overridePadding: WidgetStateProperty.all( + onTap: appKitModal.status.isLoading ? null : onTap, + overridePadding: MaterialStateProperty.all( EdgeInsets.only( left: size == BaseButtonSize.small ? 4.0 : 6.0, right: 8.0, ), ), buttonStyle: ButtonStyle( - backgroundColor: WidgetStateProperty.resolveWith( - (states) { - if (states.contains(WidgetState.disabled)) { - return themeColors.grayGlass002; - } - return themeColors.grayGlass005; - }, + backgroundColor: MaterialStateProperty.resolveWith( + (states) => themeColors.grayGlass002, ), foregroundColor: WidgetStateProperty.resolveWith( (states) { @@ -71,9 +71,10 @@ class AppKitModalAddressButton extends StatelessWidget { shape: WidgetStateProperty.resolveWith( (states) { return RoundedRectangleBorder( - side: states.contains(WidgetState.disabled) - ? BorderSide(color: themeColors.grayGlass002, width: 1.0) - : BorderSide(color: themeColors.grayGlass005, width: 1.0), + side: BorderSide( + color: themeColors.grayGlass002, + width: 1.0, + ), borderRadius: BorderRadius.circular(innerBorderRadius), ); }, @@ -82,28 +83,44 @@ class AppKitModalAddressButton extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(size.iconSize), - border: Border.all( - color: themeColors.grayGlass002, - width: 1.0, - strokeAlign: BorderSide.strokeAlignInside, - ), - ), - child: AccountAvatar( - appKit: appKitModal, - size: size.iconSize * 0.95, - disabled: false, - ), - ), + appKitModal.status.isLoading + ? Row( + children: [ + const SizedBox.square(dimension: 4.0), + CircularLoader( + size: 16.0, + strokeWidth: 1.5, + ), + ], + ) + : Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(size.iconSize), + border: Border.all( + color: themeColors.grayGlass005, + width: 1.0, + strokeAlign: BorderSide.strokeAlignInside, + ), + ), + child: AccountAvatar( + appKit: appKitModal, + size: size.iconSize * 0.95, + disabled: false, + ), + ), const SizedBox.square(dimension: 4.0), - Text( - RenderUtils.truncate( - address!, - length: size == BaseButtonSize.small ? 2 : 4, + ConstrainedBox( + constraints: BoxConstraints(maxWidth: 140.0), + child: Text( + identityName ?? + RenderUtils.truncate( + address!, + length: size == BaseButtonSize.small ? 2 : 4, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textStyle, ), - style: textStyle, ), ], ), diff --git a/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_balance_button.dart b/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_balance_button.dart index f8ac8dd..af0fd93 100644 --- a/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_balance_button.dart +++ b/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_balance_button.dart @@ -59,15 +59,10 @@ class _AppKitModalBalanceButtonState extends State { final themeColors = ReownAppKitModalTheme.colorsOf(context); return BaseButton( size: widget.size, - onTap: widget.onTap, + onTap: widget.appKitModal.status.isLoading ? null : widget.onTap, buttonStyle: ButtonStyle( - backgroundColor: WidgetStateProperty.resolveWith( - (states) { - if (states.contains(WidgetState.disabled)) { - return themeColors.grayGlass002; - } - return themeColors.grayGlass005; - }, + backgroundColor: MaterialStateProperty.resolveWith( + (states) => themeColors.grayGlass002, ), foregroundColor: WidgetStateProperty.resolveWith( (states) { @@ -80,9 +75,10 @@ class _AppKitModalBalanceButtonState extends State { shape: WidgetStateProperty.resolveWith( (states) { return RoundedRectangleBorder( - side: states.contains(WidgetState.disabled) - ? BorderSide(color: themeColors.grayGlass002, width: 1.0) - : BorderSide(color: themeColors.grayGlass005, width: 1.0), + side: BorderSide( + color: themeColors.grayGlass002, + width: 1.0, + ), borderRadius: BorderRadius.circular(widget.size.height / 2), ); }, diff --git a/packages/reown_appkit/lib/modal/widgets/text/appkit_address.dart b/packages/reown_appkit/lib/modal/widgets/text/appkit_address.dart index 9add272..46a696a 100644 --- a/packages/reown_appkit/lib/modal/widgets/text/appkit_address.dart +++ b/packages/reown_appkit/lib/modal/widgets/text/appkit_address.dart @@ -3,33 +3,34 @@ import 'package:reown_appkit/modal/i_appkit_modal_impl.dart'; import 'package:reown_appkit/modal/utils/render_utils.dart'; import 'package:reown_appkit/reown_appkit.dart'; -class Address extends StatefulWidget { - const Address({ +/// Widget to show the address or identity name +class AddressText extends StatefulWidget { + const AddressText({ super.key, - required this.service, + required this.appKitModal, this.style, }); - final IReownAppKitModal service; + final IReownAppKitModal appKitModal; final TextStyle? style; @override - State
createState() => _AddressState(); + State createState() => _AddressTextState(); } -class _AddressState extends State
{ +class _AddressTextState extends State { String? _address; @override void initState() { super.initState(); _modalNotifyListener(); - widget.service.addListener(_modalNotifyListener); + widget.appKitModal.addListener(_modalNotifyListener); } @override void dispose() { - widget.service.removeListener(_modalNotifyListener); + widget.appKitModal.removeListener(_modalNotifyListener); super.dispose(); } @@ -37,8 +38,12 @@ class _AddressState extends State
{ Widget build(BuildContext context) { final themeData = ReownAppKitModalTheme.getDataOf(context); final themeColors = ReownAppKitModalTheme.colorsOf(context); + final identityName = + (widget.appKitModal.blockchainIdentity?.name ?? '').isNotEmpty + ? widget.appKitModal.blockchainIdentity!.name! + : null; return Text( - RenderUtils.truncate(_address ?? ''), + identityName ?? RenderUtils.truncate(_address ?? ''), style: widget.style ?? themeData.textStyles.paragraph600.copyWith( color: themeColors.foreground100, @@ -48,11 +53,11 @@ class _AddressState extends State
{ void _modalNotifyListener() { setState(() { - final chainId = widget.service.selectedChain?.chainId ?? ''; + final chainId = widget.appKitModal.selectedChain?.chainId ?? ''; final namespace = ReownAppKitModalNetworks.getNamespaceForChainId( chainId, ); - _address = widget.service.session?.getAddress(namespace); + _address = widget.appKitModal.session?.getAddress(namespace); }); } } diff --git a/packages/reown_appkit/lib/modal/widgets/text/appkit_balance.dart b/packages/reown_appkit/lib/modal/widgets/text/appkit_balance.dart index d449b1c..0588402 100644 --- a/packages/reown_appkit/lib/modal/widgets/text/appkit_balance.dart +++ b/packages/reown_appkit/lib/modal/widgets/text/appkit_balance.dart @@ -4,15 +4,20 @@ import 'package:reown_appkit/modal/theme/public/appkit_modal_theme.dart'; import 'package:reown_appkit/modal/widgets/modal_provider.dart'; import 'package:reown_appkit/modal/widgets/buttons/base_button.dart'; +/// Widget to show the wallet balance, mainly used in Account Page class BalanceText extends StatefulWidget { const BalanceText({ super.key, this.size = BaseButtonSize.regular, + this.removeCurrency = false, + this.textStyle, this.onTap, }); final BaseButtonSize size; + final bool removeCurrency; final VoidCallback? onTap; + final TextStyle? textStyle; @override State createState() => _BalanceTextState(); @@ -52,10 +57,30 @@ class _BalanceTextState extends State { return ValueListenableBuilder( valueListenable: _appKitModal!.balanceNotifier, builder: (_, balance, __) { - return Text( - balance, - style: themeData.textStyles.paragraph500.copyWith( - color: themeColors.foreground200, + final tokenName = _appKitModal?.selectedChain?.currency ?? ''; + final value = (widget.removeCurrency) + ? '\$ ${balance.replaceAll(tokenName, ' ').trim()}' + : balance; + return RichText( + text: TextSpan( + children: [ + TextSpan( + text: value.split('.').first, + style: widget.textStyle ?? + themeData.textStyles.paragraph500.copyWith( + color: themeColors.foreground200, + ), + ), + TextSpan( + text: '.${value.split('.').last}', + style: widget.textStyle?.copyWith( + color: themeColors.foreground200, + ) ?? + themeData.textStyles.paragraph500.copyWith( + color: themeColors.foreground100, + ), + ), + ], ), ); }, From 90c25d2ca2831f9cb78fe5a6df87b57e6849627a Mon Sep 17 00:00:00 2001 From: Alfreedom <00tango.bromine@icloud.com> Date: Mon, 18 Nov 2024 10:24:31 +0100 Subject: [PATCH 3/6] metada update --- packages/reown_appkit/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/reown_appkit/CHANGELOG.md b/packages/reown_appkit/CHANGELOG.md index a773872..4906f54 100644 --- a/packages/reown_appkit/CHANGELOG.md +++ b/packages/reown_appkit/CHANGELOG.md @@ -5,6 +5,11 @@ - Reconnection and resubscription mechanism, specially for Android 15 that implemented a strict doze mode. ## 1.2.0 +## 1.3.0-alpha01 + +- Wallet Features for Email and Social Login + +## 1.2.0-beta01 - Non-EVM Chains support - Social Logins From bf0e29b67f12046fb8b136f124acd4b4109d8dbc Mon Sep 17 00:00:00 2001 From: Alfreedom <00tango.bromine@icloud.com> Date: Mon, 18 Nov 2024 14:47:56 +0100 Subject: [PATCH 4/6] changes in sample dapp --- packages/reown_appkit/example/base/lib/main.dart | 13 +++++++++++-- .../example/base/lib/pages/connect_page.dart | 16 +++++----------- .../example/base/lib/utils/constants.dart | 4 ---- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/reown_appkit/example/base/lib/main.dart b/packages/reown_appkit/example/base/lib/main.dart index a1581d8..f34f3e6 100644 --- a/packages/reown_appkit/example/base/lib/main.dart +++ b/packages/reown_appkit/example/base/lib/main.dart @@ -73,6 +73,15 @@ class _MyAppState extends State with WidgetsBindingObserver { isDarkMode: _isDarkMode, child: MaterialApp( title: StringConstants.appTitle, + theme: ThemeData( + colorScheme: _isDarkMode + ? ColorScheme.dark( + primary: ReownAppKitModalThemeData().darkColors.accent100, + ) + : ColorScheme.light( + primary: ReownAppKitModalThemeData().lightColors.accent100, + ), + ), home: const MyHomePage(), ), ); @@ -421,13 +430,13 @@ class _MyHomePageState extends State { debugPrint('[SampleDapp] _onSessionAuthResponse $response'); } - void _setState(_) => setState(() {}); - void _relayClientError(ErrorEvent? event) { debugPrint('[SampleDapp] _relayClientError ${event?.error}'); _setState(''); } + void _setState(_) => setState(() {}); + @override void dispose() { // Unregister event handlers diff --git a/packages/reown_appkit/example/base/lib/pages/connect_page.dart b/packages/reown_appkit/example/base/lib/pages/connect_page.dart index 46c2adf..a78b667 100644 --- a/packages/reown_appkit/example/base/lib/pages/connect_page.dart +++ b/packages/reown_appkit/example/base/lib/pages/connect_page.dart @@ -207,18 +207,12 @@ class ConnectPageState extends State { final enabled = snapshot.data != null; return ElevatedButton( style: ButtonStyle( - elevation: WidgetStateProperty.all(0.0), - backgroundColor: WidgetStateProperty.all( - enabled - ? ReownAppKitModalTheme.colorsOf(context).accent080 + backgroundColor: MaterialStateProperty.resolveWith( + (states) => states.contains(MaterialState.disabled) + ? ReownAppKitModalTheme.colorsOf(context) + .grayGlass005 : ReownAppKitModalTheme.colorsOf(context) - .accenGlass010, - ), - foregroundColor: WidgetStateProperty.all( - enabled - ? Colors.white - : ReownAppKitModalTheme.colorsOf(context) - .foreground200, + .background125, ), ), onPressed: enabled diff --git a/packages/reown_appkit/example/base/lib/utils/constants.dart b/packages/reown_appkit/example/base/lib/utils/constants.dart index a17612d..96011b9 100644 --- a/packages/reown_appkit/example/base/lib/utils/constants.dart +++ b/packages/reown_appkit/example/base/lib/utils/constants.dart @@ -32,22 +32,18 @@ class StyleConstants { // Text styles static const TextStyle titleText = TextStyle( - color: Colors.black, fontSize: magic40, fontWeight: FontWeight.w600, ); static const TextStyle subtitleText = TextStyle( - color: Colors.black, fontSize: linear24, fontWeight: FontWeight.w600, ); static const TextStyle paragraph = TextStyle( - color: Colors.black, fontSize: linear16, fontWeight: FontWeight.w600, ); static const TextStyle buttonText = TextStyle( - color: Colors.black, fontSize: magic14, fontWeight: FontWeight.w600, ); From 3d3425a7b09984f53efac29415a51d28dd566b1a Mon Sep 17 00:00:00 2001 From: Alfreedom <00tango.bromine@icloud.com> Date: Tue, 7 Jan 2025 19:24:52 +0100 Subject: [PATCH 5/6] Generated code and fixed deprecations after building with latest flutter --- .../example/base/lib/pages/connect_page.dart | 4 ++-- .../reown_appkit/lib/modal/pages/account_page.dart | 2 +- .../lib/modal/pages/select_token_page.dart | 2 +- .../lib/modal/pages/smart_account_page.dart | 2 +- .../lib/modal/widgets/buttons/address_button.dart | 4 ++-- .../lib/modal/widgets/buttons/connect_button.dart | 4 ++-- .../lib/modal/widgets/buttons/network_button.dart | 10 +++++----- .../lib/modal/widgets/buttons/secondary_button.dart | 2 +- .../lib/modal/widgets/buttons/simple_icon_button.dart | 8 ++++---- .../lib/modal/widgets/lists/activity_item.dart | 4 ++-- .../widgets/lists/list_items/account_list_item.dart | 2 +- .../modal/widgets/lists/list_items/base_list_item.dart | 6 +++--- .../widgets/public/appkit_modal_account_button.dart | 4 ++-- .../widgets/public/appkit_modal_address_button.dart | 4 ++-- .../widgets/public/appkit_modal_balance_button.dart | 2 +- 15 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/reown_appkit/example/base/lib/pages/connect_page.dart b/packages/reown_appkit/example/base/lib/pages/connect_page.dart index a78b667..688758c 100644 --- a/packages/reown_appkit/example/base/lib/pages/connect_page.dart +++ b/packages/reown_appkit/example/base/lib/pages/connect_page.dart @@ -207,8 +207,8 @@ class ConnectPageState extends State { final enabled = snapshot.data != null; return ElevatedButton( style: ButtonStyle( - backgroundColor: MaterialStateProperty.resolveWith( - (states) => states.contains(MaterialState.disabled) + backgroundColor: WidgetStateProperty.resolveWith( + (states) => states.contains(WidgetState.disabled) ? ReownAppKitModalTheme.colorsOf(context) .grayGlass005 : ReownAppKitModalTheme.colorsOf(context) diff --git a/packages/reown_appkit/lib/modal/pages/account_page.dart b/packages/reown_appkit/lib/modal/pages/account_page.dart index 2b54b7c..e3d3813 100644 --- a/packages/reown_appkit/lib/modal/pages/account_page.dart +++ b/packages/reown_appkit/lib/modal/pages/account_page.dart @@ -120,7 +120,7 @@ class _DefaultAccountView extends StatelessWidget { title: 'Block Explorer', backgroundColor: themeColors.grayGlass002, foregroundColor: themeColors.foreground150, - overlayColor: MaterialStateProperty.all( + overlayColor: WidgetStateProperty.all( themeColors.grayGlass002, ), ), diff --git a/packages/reown_appkit/lib/modal/pages/select_token_page.dart b/packages/reown_appkit/lib/modal/pages/select_token_page.dart index febada4..ac67bac 100644 --- a/packages/reown_appkit/lib/modal/pages/select_token_page.dart +++ b/packages/reown_appkit/lib/modal/pages/select_token_page.dart @@ -83,7 +83,7 @@ class _SelectTokenPageState extends State { ..._tokens.mapIndexed((index, token) { return AccountListItem( padding: const EdgeInsets.all(0.0), - backgroundColor: MaterialStatePropertyAll( + backgroundColor: WidgetStatePropertyAll( Colors.transparent, ), iconWidget: Padding( diff --git a/packages/reown_appkit/lib/modal/pages/smart_account_page.dart b/packages/reown_appkit/lib/modal/pages/smart_account_page.dart index 438add3..b06dbc9 100644 --- a/packages/reown_appkit/lib/modal/pages/smart_account_page.dart +++ b/packages/reown_appkit/lib/modal/pages/smart_account_page.dart @@ -272,7 +272,7 @@ class _SmartAccountViewState extends State<_SmartAccountView> { ..._tokens.mapIndexed((index, token) { return AccountListItem( padding: const EdgeInsets.all(0.0), - backgroundColor: MaterialStatePropertyAll( + backgroundColor: WidgetStatePropertyAll( Colors.transparent, ), iconWidget: Padding( diff --git a/packages/reown_appkit/lib/modal/widgets/buttons/address_button.dart b/packages/reown_appkit/lib/modal/widgets/buttons/address_button.dart index b9956e6..be846c2 100644 --- a/packages/reown_appkit/lib/modal/widgets/buttons/address_button.dart +++ b/packages/reown_appkit/lib/modal/widgets/buttons/address_button.dart @@ -66,7 +66,7 @@ class _AddressButtonState extends State { size: widget.size, onTap: widget.onTap, buttonStyle: ButtonStyle( - backgroundColor: MaterialStateProperty.resolveWith( + backgroundColor: WidgetStateProperty.resolveWith( (states) => themeColors.grayGlass002, ), foregroundColor: WidgetStateProperty.resolveWith( @@ -89,7 +89,7 @@ class _AddressButtonState extends State { }, ), ), - overridePadding: MaterialStateProperty.all( + overridePadding: WidgetStateProperty.all( widget.child != null ? const EdgeInsets.all(0.0) : EdgeInsets.only( diff --git a/packages/reown_appkit/lib/modal/widgets/buttons/connect_button.dart b/packages/reown_appkit/lib/modal/widgets/buttons/connect_button.dart index ebb599c..4af531a 100644 --- a/packages/reown_appkit/lib/modal/widgets/buttons/connect_button.dart +++ b/packages/reown_appkit/lib/modal/widgets/buttons/connect_button.dart @@ -52,7 +52,7 @@ class ConnectButton extends StatelessWidget { if (connecting) { return themeColors.grayGlass002; } - if (states.contains(MaterialState.disabled)) { + if (states.contains(WidgetState.disabled)) { return themeColors.grayGlass002; } return themeColors.accent100; @@ -72,7 +72,7 @@ class ConnectButton extends StatelessWidget { shape: WidgetStateProperty.resolveWith( (states) { return RoundedRectangleBorder( - side: (states.contains(MaterialState.disabled) || connecting) + side: (states.contains(WidgetState.disabled) || connecting) ? BorderSide( color: themeColors.grayGlass002, width: 1.0, diff --git a/packages/reown_appkit/lib/modal/widgets/buttons/network_button.dart b/packages/reown_appkit/lib/modal/widgets/buttons/network_button.dart index e1a9298..53ea88c 100644 --- a/packages/reown_appkit/lib/modal/widgets/buttons/network_button.dart +++ b/packages/reown_appkit/lib/modal/widgets/buttons/network_button.dart @@ -68,18 +68,18 @@ class NetworkButton extends StatelessWidget { size: size, onTap: serviceStatus.isInitialized ? onTap : null, buttonStyle: ButtonStyle( - backgroundColor: MaterialStateProperty.resolveWith( + backgroundColor: WidgetStateProperty.resolveWith( (states) => themeColors.grayGlass002, ), - foregroundColor: MaterialStateProperty.resolveWith( + foregroundColor: WidgetStateProperty.resolveWith( (states) { - if (states.contains(MaterialState.disabled)) { + if (states.contains(WidgetState.disabled)) { return themeColors.grayGlass015; } return themeColors.foreground100; }, ), - shape: MaterialStateProperty.resolveWith( + shape: WidgetStateProperty.resolveWith( (states) { return RoundedRectangleBorder( side: BorderSide( @@ -152,7 +152,7 @@ class NetworkButton extends StatelessWidget { ), ], ), - overridePadding: MaterialStateProperty.all( + overridePadding: WidgetStateProperty.all( !iconOnRight ? const EdgeInsets.only(left: 6.0, right: 16.0) : const EdgeInsets.only(left: 16.0, right: 6.0), diff --git a/packages/reown_appkit/lib/modal/widgets/buttons/secondary_button.dart b/packages/reown_appkit/lib/modal/widgets/buttons/secondary_button.dart index 48073d0..f14a7d7 100644 --- a/packages/reown_appkit/lib/modal/widgets/buttons/secondary_button.dart +++ b/packages/reown_appkit/lib/modal/widgets/buttons/secondary_button.dart @@ -20,7 +20,7 @@ class SecondaryButton extends StatelessWidget { child: Text(title), onTap: onTap, buttonStyle: ButtonStyle( - backgroundColor: MaterialStateProperty.resolveWith( + backgroundColor: WidgetStateProperty.resolveWith( (states) => themeColors.grayGlass002, ), foregroundColor: WidgetStateProperty.resolveWith( diff --git a/packages/reown_appkit/lib/modal/widgets/buttons/simple_icon_button.dart b/packages/reown_appkit/lib/modal/widgets/buttons/simple_icon_button.dart index 608d8ab..00608cb 100644 --- a/packages/reown_appkit/lib/modal/widgets/buttons/simple_icon_button.dart +++ b/packages/reown_appkit/lib/modal/widgets/buttons/simple_icon_button.dart @@ -44,17 +44,17 @@ class SimpleIconButton extends StatelessWidget { onTap: onTap, size: size, buttonStyle: ButtonStyle( - backgroundColor: MaterialStateProperty.resolveWith( + backgroundColor: WidgetStateProperty.resolveWith( (states) { - if (states.contains(MaterialState.disabled)) { + if (states.contains(WidgetState.disabled)) { return themeColors.grayGlass005; } return backgroundColor ?? themeColors.accent100; }, ), - foregroundColor: MaterialStateProperty.resolveWith( + foregroundColor: WidgetStateProperty.resolveWith( (states) { - if (states.contains(MaterialState.disabled)) { + if (states.contains(WidgetState.disabled)) { return themeColors.grayGlass005; } return foregroundColor ?? themeColors.inverse100; diff --git a/packages/reown_appkit/lib/modal/widgets/lists/activity_item.dart b/packages/reown_appkit/lib/modal/widgets/lists/activity_item.dart index 9bb0b56..8b89675 100644 --- a/packages/reown_appkit/lib/modal/widgets/lists/activity_item.dart +++ b/packages/reown_appkit/lib/modal/widgets/lists/activity_item.dart @@ -59,7 +59,7 @@ class ActivityListItem extends StatelessWidget { // return BaseListItem( onTap: onTap, - backgroundColor: MaterialStateProperty.all( + backgroundColor: WidgetStateProperty.all( themeColors.background125, ), padding: const EdgeInsets.all(0.0), @@ -267,7 +267,7 @@ class ActivityListItemLoader extends StatelessWidget { final themeColors = ReownAppKitModalTheme.colorsOf(context); // return BaseListItem( - backgroundColor: MaterialStateProperty.all( + backgroundColor: WidgetStateProperty.all( Colors.transparent, ), padding: const EdgeInsets.all(0.0), diff --git a/packages/reown_appkit/lib/modal/widgets/lists/list_items/account_list_item.dart b/packages/reown_appkit/lib/modal/widgets/lists/list_items/account_list_item.dart index 992bdcb..edd27f3 100644 --- a/packages/reown_appkit/lib/modal/widgets/lists/list_items/account_list_item.dart +++ b/packages/reown_appkit/lib/modal/widgets/lists/list_items/account_list_item.dart @@ -36,7 +36,7 @@ class AccountListItem extends StatelessWidget { final bool hightlighted; final bool flexible; final EdgeInsets? padding; - final MaterialStateProperty? backgroundColor; + final WidgetStateProperty? backgroundColor; @override Widget build(BuildContext context) { diff --git a/packages/reown_appkit/lib/modal/widgets/lists/list_items/base_list_item.dart b/packages/reown_appkit/lib/modal/widgets/lists/list_items/base_list_item.dart index d8c5310..09c2fe5 100644 --- a/packages/reown_appkit/lib/modal/widgets/lists/list_items/base_list_item.dart +++ b/packages/reown_appkit/lib/modal/widgets/lists/list_items/base_list_item.dart @@ -19,7 +19,7 @@ class BaseListItem extends StatelessWidget { final EdgeInsets? padding; final bool hightlighted; final bool flexible; - final MaterialStateProperty? backgroundColor; + final WidgetStateProperty? backgroundColor; @override Widget build(BuildContext context) { @@ -39,12 +39,12 @@ class BaseListItem extends StatelessWidget { ) : null, backgroundColor: backgroundColor ?? - MaterialStateProperty.all( + WidgetStateProperty.all( hightlighted ? themeColors.accenGlass015 : themeColors.grayGlass002, ), - overlayColor: MaterialStateProperty.all( + overlayColor: WidgetStateProperty.all( themeColors.grayGlass005, ), shape: WidgetStateProperty.all( diff --git a/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_account_button.dart b/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_account_button.dart index 6f0d91e..c6fd01f 100644 --- a/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_account_button.dart +++ b/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_account_button.dart @@ -130,7 +130,7 @@ class _AppKitModalAccountButtonState extends State { const EdgeInsets.only(left: 4.0, right: 4.0), ), buttonStyle: ButtonStyle( - backgroundColor: MaterialStateProperty.resolveWith( + backgroundColor: WidgetStateProperty.resolveWith( (states) => themeColors.grayGlass002, ), foregroundColor: WidgetStateProperty.resolveWith( @@ -209,7 +209,7 @@ class _BalanceButton extends StatelessWidget { backgroundColor: WidgetStateProperty.all(Colors.transparent), foregroundColor: WidgetStateProperty.resolveWith( (states) { - if (states.contains(MaterialState.disabled)) { + if (states.contains(WidgetState.disabled)) { return themeColors.grayGlass005; } return themeColors.foreground100; diff --git a/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_address_button.dart b/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_address_button.dart index 3671382..6f04efa 100644 --- a/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_address_button.dart +++ b/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_address_button.dart @@ -50,14 +50,14 @@ class AppKitModalAddressButton extends StatelessWidget { child: BaseButton( size: size, onTap: appKitModal.status.isLoading ? null : onTap, - overridePadding: MaterialStateProperty.all( + overridePadding: WidgetStateProperty.all( EdgeInsets.only( left: size == BaseButtonSize.small ? 4.0 : 6.0, right: 8.0, ), ), buttonStyle: ButtonStyle( - backgroundColor: MaterialStateProperty.resolveWith( + backgroundColor: WidgetStateProperty.resolveWith( (states) => themeColors.grayGlass002, ), foregroundColor: WidgetStateProperty.resolveWith( diff --git a/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_balance_button.dart b/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_balance_button.dart index af0fd93..9ff090c 100644 --- a/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_balance_button.dart +++ b/packages/reown_appkit/lib/modal/widgets/public/appkit_modal_balance_button.dart @@ -61,7 +61,7 @@ class _AppKitModalBalanceButtonState extends State { size: widget.size, onTap: widget.appKitModal.status.isLoading ? null : widget.onTap, buttonStyle: ButtonStyle( - backgroundColor: MaterialStateProperty.resolveWith( + backgroundColor: WidgetStateProperty.resolveWith( (states) => themeColors.grayGlass002, ), foregroundColor: WidgetStateProperty.resolveWith( From f1a70dffccbd11f54b603725af91398e387eb71a Mon Sep 17 00:00:00 2001 From: Alfreedom <00tango.bromine@icloud.com> Date: Wed, 8 Jan 2025 11:57:23 +0100 Subject: [PATCH 6/6] Minor changes --- .../reown_appkit/example/modal/pubspec.yaml | 2 +- packages/reown_appkit/pubspec.yaml | 30 +++++++++---------- packages/reown_sign/pubspec.yaml | 6 ++-- packages/reown_walletkit/pubspec.yaml | 18 +++++------ 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/reown_appkit/example/modal/pubspec.yaml b/packages/reown_appkit/example/modal/pubspec.yaml index 8187e2d..cf39a3a 100644 --- a/packages/reown_appkit/example/modal/pubspec.yaml +++ b/packages/reown_appkit/example/modal/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: flutter: sdk: flutter http: ^1.2.2 - intl: ^0.19.0 + intl: ^0.20.1 reown_appkit: path: ../.. shared_preferences: ^2.3.4 diff --git a/packages/reown_appkit/pubspec.yaml b/packages/reown_appkit/pubspec.yaml index d242b90..8a7e93c 100644 --- a/packages/reown_appkit/pubspec.yaml +++ b/packages/reown_appkit/pubspec.yaml @@ -19,20 +19,20 @@ dependencies: event: ^3.1.0 flutter: sdk: flutter - flutter_svg: ^2.0.10+1 - freezed_annotation: ^2.4.1 - get_it: ^8.0.0 - http: ^1.1.2 - intl: ^0.19.0 - json_annotation: ^4.8.1 + flutter_svg: ^2.0.16 + freezed_annotation: ^2.4.4 + get_it: ^8.0.3 + http: ^1.2.2 + intl: ^0.20.1 + json_annotation: ^4.9.0 plugin_platform_interface: ^2.1.8 qr_flutter_wc: ^0.0.3 - reown_core: ^1.1.0-beta02 - # reown_core: - # path: ../reown_core/ - reown_sign: ^1.1.0-beta02 - # reown_sign: - # path: ../reown_sign/ + # reown_core: ^1.1.0-beta02 + reown_core: + path: ../reown_core/ + # reown_sign: ^1.1.0-beta02 + reown_sign: + path: ../reown_sign/ shimmer: ^3.0.0 uuid: ^4.5.1 webview_flutter: ^4.10.0 @@ -51,9 +51,9 @@ dev_dependencies: logger: ^2.5.0 mockito: ^5.4.4 package_info_plus: ^8.1.2 - reown_walletkit: ^1.1.0-beta02 - # reown_walletkit: - # path: ../reown_walletkit/ + # reown_walletkit: ^1.1.0-beta02 + reown_walletkit: + path: ../reown_walletkit/ # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/packages/reown_sign/pubspec.yaml b/packages/reown_sign/pubspec.yaml index 8952b13..213611c 100644 --- a/packages/reown_sign/pubspec.yaml +++ b/packages/reown_sign/pubspec.yaml @@ -15,9 +15,9 @@ dependencies: freezed_annotation: ^2.4.4 http: ^1.2.2 pointycastle: ^3.9.1 - reown_core: ^1.1.0-beta02 - # reown_core: - # path: ../reown_core/ + # reown_core: ^1.1.0-beta02 + reown_core: + path: ../reown_core/ web3dart: ^2.7.3 dev_dependencies: diff --git a/packages/reown_walletkit/pubspec.yaml b/packages/reown_walletkit/pubspec.yaml index b6892e8..7b39580 100644 --- a/packages/reown_walletkit/pubspec.yaml +++ b/packages/reown_walletkit/pubspec.yaml @@ -12,12 +12,12 @@ dependencies: event: ^3.1.0 flutter: sdk: flutter - reown_core: ^1.1.0-beta02 - # reown_core: - # path: ../reown_core/ - reown_sign: ^1.1.0-beta02 - # reown_sign: - # path: ../reown_sign/ + # reown_core: ^1.1.0-beta02 + reown_core: + path: ../reown_core/ + # reown_sign: ^1.1.0-beta02 + reown_sign: + path: ../reown_sign/ dev_dependencies: build_runner: ^2.4.13 @@ -30,9 +30,9 @@ dev_dependencies: logger: ^2.5.0 mockito: ^5.4.4 package_info_plus: ^8.1.2 - reown_appkit: ^1.3.0-beta01 - # reown_appkit: - # path: ../reown_appkit/ + # reown_appkit: ^1.3.0-beta01 + reown_appkit: + path: ../reown_appkit/ platforms: android: