Cw 497 wallet connect for desktop (#1134)

* feat: Implement WalletConnect for Desktop

* feat: WalletConnect for Desktop

* fix: Properly handle and dispose textEditingController for URI

* chore: Move BottomSheetListener to Sidebar for desktop app

* Remove unused variable and imports

* Update desktop_settings_page.dart

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
Adegoke David
2023-10-19 19:25:44 +03:00
committed by GitHub
co-authored by Omar Hatem
parent 8ca9fa0a82
commit 374110db54
35 changed files with 344 additions and 129 deletions
@@ -1,10 +1,8 @@
import 'dart:async';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/release_notes/release_notes_screen.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart';
import 'package:cake_wallet/src/screens/yat_emoji_id.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
@@ -21,14 +19,12 @@ import 'package:shared_preferences/shared_preferences.dart';
class DesktopDashboardPage extends StatelessWidget {
DesktopDashboardPage({
required this.balancePage,
required this.bottomSheetService,
required this.dashboardViewModel,
required this.addressListViewModel,
required this.desktopKey,
});
final BalancePage balancePage;
final BottomSheetService bottomSheetService;
final DashboardViewModel dashboardViewModel;
final WalletAddressListViewModel addressListViewModel;
final GlobalKey<NavigatorState> desktopKey;
@@ -40,34 +36,31 @@ class DesktopDashboardPage extends StatelessWidget {
Widget build(BuildContext context) {
_setEffects(context);
return BottomSheetListener(
bottomSheetService: bottomSheetService,
child: Container(
color: Theme.of(context).colorScheme.background,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 400,
child: balancePage,
),
Flexible(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 500),
child: Navigator(
key: desktopKey,
initialRoute: Routes.desktop_actions,
onGenerateRoute: (settings) => Router.createRoute(settings),
onGenerateInitialRoutes: (NavigatorState navigator, String initialRouteName) {
return [
navigator.widget.onGenerateRoute!(RouteSettings(name: initialRouteName))!
];
},
),
return Container(
color: Theme.of(context).colorScheme.background,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 400,
child: balancePage,
),
Flexible(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 500),
child: Navigator(
key: desktopKey,
initialRoute: Routes.desktop_actions,
onGenerateRoute: (settings) => Router.createRoute(settings),
onGenerateInitialRoutes: (NavigatorState navigator, String initialRouteName) {
return [
navigator.widget.onGenerateRoute!(RouteSettings(name: initialRouteName))!
];
},
),
),
],
),
),
],
),
);
}
@@ -1,3 +1,4 @@
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
@@ -7,6 +8,7 @@ import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sideba
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart';
import 'package:flutter/cupertino.dart';
@@ -16,6 +18,7 @@ import 'package:cake_wallet/router.dart' as Router;
import 'package:mobx/mobx.dart';
class DesktopSidebarWrapper extends BasePage {
final BottomSheetService bottomSheetService;
final Widget child;
final DesktopSidebarViewModel desktopSidebarViewModel;
final DashboardViewModel dashboardViewModel;
@@ -23,6 +26,7 @@ class DesktopSidebarWrapper extends BasePage {
DesktopSidebarWrapper({
required this.child,
required this.bottomSheetService,
required this.desktopSidebarViewModel,
required this.dashboardViewModel,
required this.desktopNavigatorKey,
@@ -67,63 +71,75 @@ class DesktopSidebarWrapper extends BasePage {
Widget body(BuildContext context) {
_setEffects();
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Observer(builder: (_) {
return SideMenu(
width: sideMenuWidth,
topItems: [
SideMenuItem(
imagePath: 'assets/images/wallet_outline.png',
isSelected: desktopSidebarViewModel.currentPage == SidebarItem.dashboard,
onTap: () {
desktopSidebarViewModel.onPageChange(SidebarItem.dashboard);
desktopNavigatorKey.currentState
?.pushNamedAndRemoveUntil(Routes.desktop_actions, (route) => false);
},
),
SideMenuItem(
onTap: () {
if (desktopSidebarViewModel.currentPage == SidebarItem.transactions) {
return BottomSheetListener(
bottomSheetService: bottomSheetService,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Observer(builder: (_) {
return SideMenu(
width: sideMenuWidth,
topItems: [
SideMenuItem(
imagePath: 'assets/images/wallet_outline.png',
isSelected: desktopSidebarViewModel.currentPage == SidebarItem.dashboard,
onTap: () {
desktopSidebarViewModel.onPageChange(SidebarItem.dashboard);
desktopNavigatorKey.currentState
?.pushNamedAndRemoveUntil(Routes.desktop_actions, (route) => false);
desktopSidebarViewModel.resetSidebar();
} else {
desktopSidebarViewModel.onPageChange(SidebarItem.transactions);
desktopNavigatorKey.currentState?.pushNamed(Routes.transactionsPage);
}
},
isSelected: desktopSidebarViewModel.currentPage == SidebarItem.transactions,
imagePath: desktopSidebarViewModel.currentPage == SidebarItem.transactions
? selectedIconPath
: unselectedIconPath,
),
],
bottomItems: [
SideMenuItem(
imagePath: 'assets/images/support_icon.png',
isSelected: desktopSidebarViewModel.currentPage == SidebarItem.support,
onTap: () => desktopSidebarViewModel.onPageChange(SidebarItem.support)),
SideMenuItem(
imagePath: 'assets/images/settings_outline.png',
isSelected: desktopSidebarViewModel.currentPage == SidebarItem.settings,
onTap: () => desktopSidebarViewModel.onPageChange(SidebarItem.settings),
),
],
);
}),
Expanded(
child: PageView(
controller: pageController,
physics: NeverScrollableScrollPhysics(),
children: [
child,
Container(
color: Theme.of(context).colorScheme.background,
padding: EdgeInsets.all(20),
child: Navigator(
initialRoute: Routes.support,
},
),
SideMenuItem(
onTap: () {
if (desktopSidebarViewModel.currentPage == SidebarItem.transactions) {
desktopNavigatorKey.currentState
?.pushNamedAndRemoveUntil(Routes.desktop_actions, (route) => false);
desktopSidebarViewModel.resetSidebar();
} else {
desktopSidebarViewModel.onPageChange(SidebarItem.transactions);
desktopNavigatorKey.currentState?.pushNamed(Routes.transactionsPage);
}
},
isSelected: desktopSidebarViewModel.currentPage == SidebarItem.transactions,
imagePath: desktopSidebarViewModel.currentPage == SidebarItem.transactions
? selectedIconPath
: unselectedIconPath,
),
],
bottomItems: [
SideMenuItem(
imagePath: 'assets/images/support_icon.png',
isSelected: desktopSidebarViewModel.currentPage == SidebarItem.support,
onTap: () => desktopSidebarViewModel.onPageChange(SidebarItem.support)),
SideMenuItem(
imagePath: 'assets/images/settings_outline.png',
isSelected: desktopSidebarViewModel.currentPage == SidebarItem.settings,
onTap: () => desktopSidebarViewModel.onPageChange(SidebarItem.settings),
),
],
);
}),
Expanded(
child: PageView(
controller: pageController,
physics: NeverScrollableScrollPhysics(),
children: [
child,
Container(
color: Theme.of(context).colorScheme.background,
padding: EdgeInsets.all(20),
child: Navigator(
initialRoute: Routes.support,
onGenerateRoute: (settings) => Router.createRoute(settings),
onGenerateInitialRoutes: (NavigatorState navigator, String initialRouteName) {
return [
navigator.widget.onGenerateRoute!(RouteSettings(name: initialRouteName))!
];
},
),
),
Navigator(
initialRoute: Routes.desktop_settings_page,
onGenerateRoute: (settings) => Router.createRoute(settings),
onGenerateInitialRoutes: (NavigatorState navigator, String initialRouteName) {
return [
@@ -131,20 +147,11 @@ class DesktopSidebarWrapper extends BasePage {
];
},
),
),
Navigator(
initialRoute: Routes.desktop_settings_page,
onGenerateRoute: (settings) => Router.createRoute(settings),
onGenerateInitialRoutes: (NavigatorState navigator, String initialRouteName) {
return [
navigator.widget.onGenerateRoute!(RouteSettings(name: initialRouteName))!
];
},
),
],
],
),
),
),
],
],
),
);
}
@@ -85,7 +85,7 @@ class ConnectionSyncPage extends BasePage {
);
},
),
if (dashboardViewModel.wallet.type == WalletType.ethereum && DeviceInfo.instance.isMobile) ...[
if (dashboardViewModel.wallet.type == WalletType.ethereum) ...[
WalletConnectTile(
onTap: () async {
Navigator.of(context).push(
@@ -12,6 +12,7 @@ final _settingsNavigatorKey = GlobalKey<NavigatorState>();
class DesktopSettingsPage extends StatefulWidget {
const DesktopSettingsPage({super.key});
@override
State<DesktopSettingsPage> createState() => _DesktopSettingsPageState();
}
@@ -2,8 +2,10 @@ import 'dart:developer';
import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/enter_wallet_connect_uri_widget.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
@@ -36,7 +38,13 @@ class WCPairingsWidget extends BasePage {
String get title => S.current.walletConnect;
Future<void> _onScanQrCode(BuildContext context, Web3Wallet web3Wallet) async {
final String? uri = await presentQRScanner();
final String? uri;
if (DeviceInfo.instance.isMobile) {
uri = await presentQRScanner();
} else {
uri = await _showEnterWalletConnectURIPopUp(context);
}
if (uri == null) return _invalidUriToast(context, S.current.nullURIError);
@@ -51,6 +59,16 @@ class WCPairingsWidget extends BasePage {
}
}
Future<String?> _showEnterWalletConnectURIPopUp(BuildContext context) async {
final walletConnectURI = await showPopUp<String>(
context: context,
builder: (BuildContext context) {
return EnterWalletConnectURIWrapperWidget();
},
);
return walletConnectURI;
}
Future<void> _invalidUriToast(BuildContext context, String message) async {
await showPopUp<void>(
context: context,
@@ -0,0 +1,140 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/base_alert_dialog.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class EnterWalletConnectURIWrapperWidget extends StatefulWidget {
const EnterWalletConnectURIWrapperWidget({super.key});
@override
State<EnterWalletConnectURIWrapperWidget> createState() =>
_EnterWallectConnectURIWrapperWidgetState();
}
class _EnterWallectConnectURIWrapperWidgetState extends State<EnterWalletConnectURIWrapperWidget> {
late final TextEditingController controller;
@override
void initState() {
super.initState();
controller = TextEditingController();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _EnterWalletConnectURIWidget(
controller: controller,
);
}
}
class _EnterWalletConnectURIWidget extends BaseAlertDialog {
_EnterWalletConnectURIWidget({
required this.controller,
});
final TextEditingController controller;
@override
String get titleText => S.current.enterWalletConnectURI;
Future<void> _pasteWalletConnectURI() async {
final clipboard = await Clipboard.getData('text/plain');
final totpURI = clipboard?.text ?? '';
if (totpURI.isNotEmpty) {
controller.text = totpURI;
}
}
@override
Widget content(BuildContext context) {
return Card(
margin: EdgeInsets.zero,
child: Column(
children: [
SizedBox(height: 8),
Text(
S.current.copyWalletConnectLink,
style: Theme.of(context).textTheme.bodySmall,
),
SizedBox(height: 16),
TextField(
controller: controller,
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
decoration: InputDecoration(
suffixIcon: Container(
width: 24,
height: 24,
padding: EdgeInsets.only(top: 0),
child: Semantics(
label: S.of(context).paste,
child: InkWell(
onTap: () => _pasteWalletConnectURI(),
child: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(6)),
),
child: Image.asset(
'assets/images/paste_ios.png',
color:
Theme.of(context).extension<SendPageTheme>()!.textFieldButtonIconColor,
),
),
),
),
),
hintText: S.current.enterWalletConnectURI,
border: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).extension<SendPageTheme>()!.textFieldBorderColor,
),
),
hintStyle: TextStyle(
color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
),
],
),
);
}
@override
Widget actionButtons(BuildContext context) {
return Container(
width: 300,
height: 52,
padding: EdgeInsets.only(left: 12, right: 12),
color: Theme.of(context).dialogBackgroundColor,
child: ButtonTheme(
minWidth: double.infinity,
child: TextButton(
onPressed: () {
Navigator.pop(context, controller.text);
},
child: Text(
S.current.confirm,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Theme.of(context).primaryColor,
decoration: TextDecoration.none,
),
),
),
),
);
}
}