From 1738cde246f4b7428f915c781e3476e37cfe73c0 Mon Sep 17 00:00:00 2001 From: Chralu Date: Mon, 2 Dec 2024 14:31:29 +0100 Subject: [PATCH] feat: :sparkles: Bridge web view saves its state. A reload button is used to trigger dapp reload. --- lib/application/dapps.dart | 9 +- lib/application/dapps.g.dart | 21 +-- lib/domain/models/settings.dart | 12 +- lib/infrastructure/rpc/awc_webview.dart | 36 ++++- lib/ui/views/main/bloc/providers.dart | 8 +- lib/ui/views/main/components/main_appbar.dart | 153 ++++++++++-------- lib/ui/views/main/home_page.dart | 10 +- lib/ui/views/sheets/dapp_sheet.dart | 55 ++++++- 8 files changed, 199 insertions(+), 105 deletions(-) diff --git a/lib/application/dapps.dart b/lib/application/dapps.dart index 9bd5b2894..a16a4fcd5 100644 --- a/lib/application/dapps.dart +++ b/lib/application/dapps.dart @@ -1,4 +1,5 @@ import 'package:aewallet/application/api_service.dart'; +import 'package:aewallet/application/settings/settings.dart'; import 'package:aewallet/domain/models/dapp.dart'; import 'package:aewallet/infrastructure/repositories/dapps_repository.dart'; import 'package:aewallet/model/available_networks.dart'; @@ -16,11 +17,15 @@ DAppsRepositoryImpl _dAppsRepository( @riverpod Future _getDApp( Ref ref, - AvailableNetworks network, String code, ) async { final apiService = ref.watch(apiServiceProvider); - return ref.watch(_dAppsRepositoryProvider).getDApp(network, code, apiService); + final networkSettings = ref.watch( + SettingsProviders.settings.select((settings) => settings.network), + ); + return ref + .watch(_dAppsRepositoryProvider) + .getDApp(networkSettings.network, code, apiService); } @riverpod diff --git a/lib/application/dapps.g.dart b/lib/application/dapps.g.dart index 76a30aa7c..f0ab0bdbe 100644 --- a/lib/application/dapps.g.dart +++ b/lib/application/dapps.g.dart @@ -24,7 +24,7 @@ final _dAppsRepositoryProvider = @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef _DAppsRepositoryRef = AutoDisposeProviderRef; -String _$getDAppHash() => r'70f3139f239d37e2aaf093b1007b668aeab29d90'; +String _$getDAppHash() => r'e708424d60dbef17da84a12ed02a67dd493a28a2'; /// Copied from Dart SDK class _SystemHash { @@ -58,11 +58,9 @@ class _GetDAppFamily extends Family> { /// See also [_getDApp]. _GetDAppProvider call( - AvailableNetworks network, String code, ) { return _GetDAppProvider( - network, code, ); } @@ -72,7 +70,6 @@ class _GetDAppFamily extends Family> { covariant _GetDAppProvider provider, ) { return call( - provider.network, provider.code, ); } @@ -96,12 +93,10 @@ class _GetDAppFamily extends Family> { class _GetDAppProvider extends AutoDisposeFutureProvider { /// See also [_getDApp]. _GetDAppProvider( - AvailableNetworks network, String code, ) : this._internal( (ref) => _getDApp( ref as _GetDAppRef, - network, code, ), from: _getDAppProvider, @@ -112,7 +107,6 @@ class _GetDAppProvider extends AutoDisposeFutureProvider { : _$getDAppHash, dependencies: _GetDAppFamily._dependencies, allTransitiveDependencies: _GetDAppFamily._allTransitiveDependencies, - network: network, code: code, ); @@ -123,11 +117,9 @@ class _GetDAppProvider extends AutoDisposeFutureProvider { required super.allTransitiveDependencies, required super.debugGetCreateSourceHash, required super.from, - required this.network, required this.code, }) : super.internal(); - final AvailableNetworks network; final String code; @override @@ -143,7 +135,6 @@ class _GetDAppProvider extends AutoDisposeFutureProvider { dependencies: null, allTransitiveDependencies: null, debugGetCreateSourceHash: null, - network: network, code: code, ), ); @@ -156,15 +147,12 @@ class _GetDAppProvider extends AutoDisposeFutureProvider { @override bool operator ==(Object other) { - return other is _GetDAppProvider && - other.network == network && - other.code == code; + return other is _GetDAppProvider && other.code == code; } @override int get hashCode { var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, network.hashCode); hash = _SystemHash.combine(hash, code.hashCode); return _SystemHash.finish(hash); @@ -174,9 +162,6 @@ class _GetDAppProvider extends AutoDisposeFutureProvider { @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element mixin _GetDAppRef on AutoDisposeFutureProviderRef { - /// The parameter `network` of this provider. - AvailableNetworks get network; - /// The parameter `code` of this provider. String get code; } @@ -185,8 +170,6 @@ class _GetDAppProviderElement extends AutoDisposeFutureProviderElement with _GetDAppRef { _GetDAppProviderElement(super.provider); - @override - AvailableNetworks get network => (origin as _GetDAppProvider).network; @override String get code => (origin as _GetDAppProvider).code; } diff --git a/lib/domain/models/settings.dart b/lib/domain/models/settings.dart index b98a83862..af060fb07 100644 --- a/lib/domain/models/settings.dart +++ b/lib/domain/models/settings.dart @@ -7,7 +7,13 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'settings.freezed.dart'; -enum MainScreenTab { accountTab, transactionTab, swapTab, earnTab, bridgeTab } +enum MainScreenTab { + accountTab, + transactionTab, + swapTab, + earnTab, + bridgeTab, +} @freezed class Settings with _$Settings { @@ -42,4 +48,8 @@ class Settings with _$Settings { ); const Settings._(); + + MainScreenTab get mainScreenTab => MainScreenTab.values.firstWhere( + (value) => value.index == mainScreenCurrentPage, + ); } diff --git a/lib/infrastructure/rpc/awc_webview.dart b/lib/infrastructure/rpc/awc_webview.dart index d608a898b..96d2aa7ac 100644 --- a/lib/infrastructure/rpc/awc_webview.dart +++ b/lib/infrastructure/rpc/awc_webview.dart @@ -65,6 +65,35 @@ enum EVMWallet { final String scheme; } +/// Keeps track of all alive WebviewControllers +class AWCWebviewControllers { + const AWCWebviewControllers._(); + + static final _logger = Logger('AWCWebviewControllers'); + static final _controllers = {}; + + static InAppWebViewController? find(Uri uri) => _controllers[uri]; + + static void register(Uri uri, InAppWebViewController controller) { + final alreadyRegisteredController = find(uri); + if (alreadyRegisteredController != null && + controller == alreadyRegisteredController) { + return; + } + dispose(uri); + _controllers[uri] = controller; + _logger.info('Registered controller for $uri'); + } + + static void dispose(Uri uri) { + final controller = find(uri); + if (controller == null) return; + controller.dispose(); + _controllers.remove(uri); + _logger.info('Disposed controller for $uri'); + } +} + class AWCWebview extends StatefulWidget { const AWCWebview({super.key, required this.uri}); @@ -96,11 +125,13 @@ class AWCWebview extends StatefulWidget { class _AWCWebviewState extends State with WidgetsBindingObserver { AWCJsonRPCServer? _peerServer; - InAppWebViewController? _controller; WebviewMessagePortStreamChannel? _channel; bool _loaded = false; late final FocusNode _focusNode; + InAppWebViewController? get _controller => + AWCWebviewControllers.find(widget.uri); + @override void initState() { if (kDebugMode && @@ -119,6 +150,7 @@ class _AWCWebviewState extends State with WidgetsBindingObserver { _peerServer?.close(); _focusNode.dispose(); WidgetsBinding.instance.removeObserver(this); + AWCWebviewControllers.dispose(widget.uri); AWCWebview._logger.info('AWC webview disposed.'); super.dispose(); @@ -153,7 +185,7 @@ class _AWCWebviewState extends State with WidgetsBindingObserver { transparentBackground: true, ), onLoadStop: (controller, _) async { - _controller = controller; + AWCWebviewControllers.register(widget.uri, controller); await _initMessageChannelRPC(controller); await _maintainWebviewFocus(controller); setState(() { diff --git a/lib/ui/views/main/bloc/providers.dart b/lib/ui/views/main/bloc/providers.dart index aa6200c7c..74a847afd 100644 --- a/lib/ui/views/main/bloc/providers.dart +++ b/lib/ui/views/main/bloc/providers.dart @@ -105,9 +105,11 @@ Future homePage(Ref ref) async { final mainTabControllerProvider = StateNotifierProvider.autoDispose( - (ref) { - return TabControllerNotifier(); -}); + (ref) { + return TabControllerNotifier(); + }, + name: 'TabControllerNotifier', +); class TabControllerNotifier extends StateNotifier { TabControllerNotifier() : super(null); diff --git a/lib/ui/views/main/components/main_appbar.dart b/lib/ui/views/main/components/main_appbar.dart index 39117ceae..da9ad9041 100644 --- a/lib/ui/views/main/components/main_appbar.dart +++ b/lib/ui/views/main/components/main_appbar.dart @@ -11,10 +11,10 @@ import 'package:aewallet/ui/views/aeswap_swap/layouts/components/swap_icon_refre import 'package:aewallet/ui/views/main/components/main_appbar_account.dart'; import 'package:aewallet/ui/views/main/components/main_appbar_basic.dart'; import 'package:aewallet/ui/views/main/components/main_appbar_transactions.dart'; +import 'package:aewallet/ui/views/sheets/dapp_sheet.dart'; import 'package:aewallet/ui/widgets/components/icon_network_warning.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_gen/gen_l10n/localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -30,11 +30,87 @@ class MainAppBar extends ConsumerWidget implements PreferredSizeWidget { Widget build(BuildContext context, WidgetRef ref) { final localizations = AppLocalizations.of(context)!; final preferences = ref.watch(SettingsProviders.settings); - final connectivityStatusProvider = ref.watch(connectivityStatusProviders); - if (preferences.mainScreenCurrentPage == 4) { - return const _MainAppbarForWebView(); - } + final tab = preferences.mainScreenTab; + + return switch (tab) { + MainScreenTab.accountTab => _MainAppBar( + key: const Key('account'), + actions: [ + _BalanceVisibilityButton(preferences: preferences), + ], + title: const MainAppBarAccount(), + ), + MainScreenTab.transactionTab => _MainAppBar( + key: const Key('transaction'), + actions: [ + _BalanceVisibilityButton(preferences: preferences), + ], + title: const MainAppBarTransactions(), + ), + MainScreenTab.swapTab => _MainAppBar( + key: const Key('swap'), + actions: const [ + SwapTokenIconRefresh(), + ], + title: MainAppBarBasic(header: localizations.swapHeader), + ), + MainScreenTab.earnTab => _MainAppBar( + key: const Key('earn'), + actions: const [], + title: MainAppBarBasic(header: localizations.aeSwapEarnHeader), + ), + MainScreenTab.bridgeTab => _MainAppBar( + key: const Key('bridge'), + actions: [ + DAppSheetIconRefresh(dappKey: 'aeBridge'), + ], + title: MainAppBarBasic( + header: localizations.aeBridgeHeader, + ), + ), + }; + } +} + +class _BalanceVisibilityButton extends ConsumerWidget { + const _BalanceVisibilityButton({ + required this.preferences, + }); + + final Settings preferences; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return IconButton( + icon: Icon( + preferences.showBalances ? Symbols.visibility : Symbols.visibility_off, + weight: IconSize.weightM, + opticalSize: IconSize.opticalSizeM, + grade: IconSize.gradeM, + ), + onPressed: () async { + final preferencesNotifier = + ref.read(SettingsProviders.settings.notifier); + await preferencesNotifier.setShowBalances(!preferences.showBalances); + }, + ); + } +} + +class _MainAppBar extends ConsumerWidget { + const _MainAppBar({ + super.key, + required this.actions, + required this.title, + }); + + final List actions; + final Widget title; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final connectivityStatusProvider = ref.watch(connectivityStatusProviders); return AppBar( flexibleSpace: ClipRRect( @@ -51,43 +127,11 @@ class MainAppBar extends ConsumerWidget implements PreferredSizeWidget { automaticallyImplyLeading: false, leading: const _MenuButton(), actions: [ - if (preferences.mainScreenCurrentPage == - MainScreenTab.accountTab.index || - preferences.mainScreenCurrentPage == - MainScreenTab.transactionTab.index) - IconButton( - icon: Icon( - preferences.showBalances - ? Symbols.visibility - : Symbols.visibility_off, - weight: IconSize.weightM, - opticalSize: IconSize.opticalSizeM, - grade: IconSize.gradeM, - ), - onPressed: () async { - final preferencesNotifier = - ref.read(SettingsProviders.settings.notifier); - await preferencesNotifier - .setShowBalances(!preferences.showBalances); - }, - ) - else if (preferences.mainScreenCurrentPage == - MainScreenTab.swapTab.index) - const SwapTokenIconRefresh(), + ...actions, if (connectivityStatusProvider == ConnectivityStatus.isDisconnected) const IconNetworkWarning(), ], - title: preferences.mainScreenCurrentPage == MainScreenTab.accountTab.index - ? const MainAppBarAccount() - : preferences.mainScreenCurrentPage == - MainScreenTab.transactionTab.index - ? const MainAppBarTransactions() - : preferences.mainScreenCurrentPage == MainScreenTab.swapTab.index - ? MainAppBarBasic(header: localizations.swapHeader) - : preferences.mainScreenCurrentPage == - MainScreenTab.earnTab.index - ? MainAppBarBasic(header: localizations.aeSwapEarnHeader) - : MainAppBarBasic(header: localizations.aeBridgeHeader), + title: title, backgroundColor: Colors.transparent, elevation: 0, centerTitle: true, @@ -96,37 +140,6 @@ class MainAppBar extends ConsumerWidget implements PreferredSizeWidget { } } -/// AppBar containing only the menu button. -/// Useful for webview screens. -class _MainAppbarForWebView extends ConsumerWidget { - const _MainAppbarForWebView(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final localizations = AppLocalizations.of(context)!; - return SafeArea( - child: Stack( - alignment: Alignment.center, - children: [ - SizedBox( - height: kToolbarHeight, - child: Align( - child: Text( - localizations.aeBridgeHeader, - style: ArchethicThemeStyles.textStyleSize24W700Primary, - ).animate().fade(duration: const Duration(milliseconds: 300)), - ), - ), - const Positioned( - left: 5, - child: _MenuButton(), - ), - ], - ), - ); - } -} - class _MenuButton extends ConsumerWidget { const _MenuButton(); diff --git a/lib/ui/views/main/home_page.dart b/lib/ui/views/main/home_page.dart index 191fea125..db5807ef8 100755 --- a/lib/ui/views/main/home_page.dart +++ b/lib/ui/views/main/home_page.dart @@ -159,8 +159,8 @@ class _HomePageState extends ConsumerState child: TabBarView( physics: const NeverScrollableScrollPhysics(), controller: tabController, - children: const [ - Stack( + children: [ + const Stack( alignment: Alignment.topCenter, children: [ AccountTab(), @@ -170,9 +170,9 @@ class _HomePageState extends ConsumerState ), ], ), - TransactionsTab(), - SwapTab(), - EarnTab(), + const TransactionsTab(), + const SwapTab(), + const EarnTab(), DAppSheet(dappKey: 'aeBridge'), ], ), diff --git a/lib/ui/views/sheets/dapp_sheet.dart b/lib/ui/views/sheets/dapp_sheet.dart index 6f76f67b2..4faf32bf8 100755 --- a/lib/ui/views/sheets/dapp_sheet.dart +++ b/lib/ui/views/sheets/dapp_sheet.dart @@ -11,13 +11,57 @@ import 'package:aewallet/ui/widgets/components/app_button_tiny.dart'; import 'package:aewallet/ui/widgets/components/loading_list_header.dart'; import 'package:aewallet/ui/widgets/components/scrollbar.dart'; import 'package:aewallet/util/universal_platform.dart'; +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart' + as aedappfm; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:url_launcher/url_launcher.dart'; +class DAppSheetIconRefresh extends ConsumerWidget { + factory DAppSheetIconRefresh({required String dappKey}) => + DAppSheetIconRefresh._( + dappKey: dappKey, + key: Key(dappKey), + ); + const DAppSheetIconRefresh._({ + required this.dappKey, + super.key, + }); + + final String dappKey; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return IconButton( + icon: const Icon( + aedappfm.Iconsax.refresh, + size: 16, + color: Colors.white, + ), + onPressed: () async { + final dapp = await ref.read( + DAppsProviders.getDApp(dappKey).future, + ); + if (dapp == null) return; + final webviewController = + AWCWebviewControllers.find(Uri.parse(dapp.url)); + if (webviewController == null) return; + + if (await webviewController.isLoading()) return; + await webviewController.reload(); + }, + ); + } +} + class DAppSheet extends ConsumerStatefulWidget { - const DAppSheet({ + factory DAppSheet({required String dappKey}) => DAppSheet._( + dappKey: dappKey, + key: Key(dappKey), + ); + + const DAppSheet._({ required this.dappKey, super.key, }); @@ -31,12 +75,16 @@ class DAppSheet extends ConsumerStatefulWidget { ConsumerState createState() => DAppSheetState(); } -class DAppSheetState extends ConsumerState { +class DAppSheetState extends ConsumerState + with AutomaticKeepAliveClientMixin { String? aeBridgeUrl; bool? featureFlags; static const applicationCode = 'aeWallet'; static const featureCode = 'bridge'; + @override + bool get wantKeepAlive => true; + @override void initState() { Future.delayed(Duration.zero, () async { @@ -61,7 +109,7 @@ class DAppSheetState extends ConsumerState { DApp? dapp; if (connectivityStatusProvider == ConnectivityStatus.isConnected) { dapp = await ref.read( - DAppsProviders.getDApp(networkSettings.network, dappKey).future, + DAppsProviders.getDApp(dappKey).future, ); setState(() { @@ -75,6 +123,7 @@ class DAppSheetState extends ConsumerState { @override Widget build(BuildContext context) { + super.build(context); final localizations = AppLocalizations.of(context)!; if (featureFlags != null && featureFlags == false) {