From 3cd8fe96ad3945f9d94f6b8d7a9c57754f800c31 Mon Sep 17 00:00:00 2001 From: Shogo Hyodo Date: Mon, 5 Aug 2024 16:20:53 +0900 Subject: [PATCH] Support Mini Wallet (#23) * Remove liff flag * Disable open wc scheme link * Make default scheme native on opening wallet * Fix _convertToWcUri * Support Mini Wallet * Support Mini Wallet browser * Add comments * Display QR code only when PC * Add comments --- .../controller/authentication_service.dart | 91 ++++++++++--------- .../controller/flutter_bird_controller.dart | 5 +- flutter_bird_app/lib/main.dart | 11 +-- .../lib/view/authentication_popup.dart | 24 ++--- flutter_bird_app/lib/view/main_menu_view.dart | 7 +- flutter_bird_app/pubspec.yaml | 1 - flutter_bird_app/web/index.html | 1 - 7 files changed, 71 insertions(+), 69 deletions(-) diff --git a/flutter_bird_app/lib/controller/authentication_service.dart b/flutter_bird_app/lib/controller/authentication_service.dart index de7effd..ff1c013 100644 --- a/flutter_bird_app/lib/controller/authentication_service.dart +++ b/flutter_bird_app/lib/controller/authentication_service.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:developer'; import 'dart:io'; import 'dart:math' as math; +import 'dart:html' as html; import 'package:eth_sig_util/eth_sig_util.dart'; import 'package:flutter/foundation.dart'; @@ -16,7 +17,7 @@ import '../model/wallet_provider.dart'; /// Manages the authentication process and communication with crypto wallets abstract class AuthenticationService { - Future initialize(bool isInLiff); + Future initialize(); List get availableWallets; @@ -38,7 +39,6 @@ abstract class AuthenticationService { } class AuthenticationServiceImpl implements AuthenticationService { - final bool isInLiff; final int operatingChain; final Function() onAuthStatusChanged; WalletProvider? _lastUsedWallet; @@ -46,7 +46,6 @@ class AuthenticationServiceImpl implements AuthenticationService { String projectId = dotenv.env['WALLET_CONNECT_PROJECT_ID'] ?? ''; AuthenticationServiceImpl({ - required this.isInLiff, required this.operatingChain, required this.onAuthStatusChanged, }); @@ -83,19 +82,14 @@ class AuthenticationServiceImpl implements AuthenticationService { int? get currentChain => int.tryParse(currentSession?.namespaces['eip155']?.accounts.first.split(':')[1] ?? ''); @override - Future initialize(bool isInLiff) async { + Future initialize() async { if (_isInitialized) { return; } await _createConnector(); await _clearSessions(); - - if (!kIsWeb || isInLiff) { - await _loadWallets(); - } else { - print('AuthenticationServiceImpl: Skipping wallet loading for Web'); - } + await _loadWallets(); _isInitialized = true; } @@ -123,6 +117,23 @@ class AuthenticationServiceImpl implements AuthenticationService { } }).where((wallet) => wallet != null).cast().toList(); + final miniWalletProvider = WalletProvider( + id: '', + name: 'Mini Wallet', + imageId: '', + imageUrl: ImageUrls( + sm: 'https://walletconnect-app-demo.vercel.app/wallet.png', + md: 'https://walletconnect-app-demo.vercel.app/wallet.png', + lg: 'https://walletconnect-app-demo.vercel.app/wallet.png'), + chains: ['eip155:1'], + versions: [], + sdks: [], + appUrls: AppUrls(), + mobile: MobileInfo( + native: null, + universal: 'https://liff.line.me/2005811776-v1GJy55G'), + desktop: DesktopInfo()); + _availableWallets.insert(0, miniWalletProvider); } else { throw Exception('Failed to load wallets: ${response.statusCode}'); } @@ -154,11 +165,9 @@ class AuthenticationServiceImpl implements AuthenticationService { Uri? uri = resp.uri; if (uri != null) { // Web - if (kIsWeb && !isInLiff) { + if (kIsWeb) { webQrData = uri.toString(); onAuthStatusChanged(); - // LIFF - } else if(kIsWeb && isInLiff) { _launchWallet(wallet: walletProvider, uri: uri.toString()); // Native } else { @@ -174,8 +183,6 @@ class AuthenticationServiceImpl implements AuthenticationService { } } - // Since the LIFF browser does not automatically transition to the wallet - // after connecting to the wallet, execute verifySignature() directly. Future verifySignature() async { if (currentChain == null || !isOnOperatingChain) return false; @@ -233,16 +240,8 @@ class AuthenticationServiceImpl implements AuthenticationService { Future _verifySignature({WalletProvider? walletProvider, String? address}) async { if (address == null || currentChain == null || !isOnOperatingChain) return false; - // Native - if (!kIsWeb) { - await Future.delayed(const Duration(seconds: 1)); - _launchWallet(wallet: walletProvider, uri: 'wc:${currentSession!.topic}@2?relay-protocol=irn&symKey=${currentSession!.relay.protocol}'); - // LIFF - } else if(isInLiff) { - await Future.delayed(const Duration(seconds: 1)); - _launchWallet(wallet: walletProvider, uri: 'wc:${currentSession!.topic}@2?relay-protocol=irn&symKey=${currentSession!.relay.protocol}'); - } - + await Future.delayed(const Duration(seconds: 1)); + _launchWallet(wallet: walletProvider, uri: 'wc:${currentSession!.topic}@2?relay-protocol=irn&symKey=${currentSession!.relay.protocol}'); String nonce = Nonce.generate(32, math.Random.secure()); String messageText = 'Please sign this message to authenticate with Flutter Bird.\nChallenge: $nonce'; @@ -293,14 +292,12 @@ class AuthenticationServiceImpl implements AuthenticationService { ); _connector?.onSessionConnect.subscribe((SessionConnect? session) async { - if (!isInLiff) { - log('connected: ' + session.toString(), name: 'AuthenticationService'); - String? address = session?.session.namespaces['eip155']?.accounts.first.split(':').last; - webQrData = null; - final authenticated = await _verifySignature(walletProvider: walletProvider, address: address); - if (authenticated) log('authenticated successfully: ' + session.toString(), name: 'AuthenticationService'); - onAuthStatusChanged(); - } + log('connected: ' + session.toString(), name: 'AuthenticationService'); + String? address = session?.session.namespaces['eip155']?.accounts.first.split(':').last; + webQrData = null; + final authenticated = await _verifySignature(walletProvider: walletProvider, address: address); + if (authenticated) log('authenticated successfully: ' + session.toString(), name: 'AuthenticationService'); + onAuthStatusChanged(); }); _connector?.onSessionUpdate.subscribe((SessionUpdate? payload) async { log('session_update: ' + payload.toString(), name: 'AuthenticationService'); @@ -323,18 +320,25 @@ class AuthenticationServiceImpl implements AuthenticationService { required String uri, }) async { if (wallet == null) { - launchUrl(Uri.parse(uri)); + print('Error: Wallet is null'); return; } - if (wallet.mobile.universal != null && await canLaunchUrl(Uri.parse(wallet.mobile.universal!))) { + // This process is for Mini Wallet. Mini Wallet Browser gives link of this app a 'is_line' variable, so app can detect opening this in Mini Wallet. + final isLine = Uri.parse(html.window.location.href).queryParameters['is_line'] != null; + if (wallet.name == 'Mini Wallet' && isLine) { + html.window.parent?.postMessage({'type': 'display_uri', 'data': uri}, "*", []); + return; + } + + if (wallet.mobile.native != null) { await launchUrl( - _convertToWcUri(appLink: wallet.mobile.universal!, wcUri: uri), - mode: LaunchMode.externalApplication, + _convertToWcUri(appLink: wallet.mobile.native!, wcUri: uri), ); - } else if (wallet.mobile.native != null && await canLaunchUrl(Uri.parse(wallet.mobile.native!))) { + } else if (wallet.mobile.universal != null && await canLaunchUrl(Uri.parse(wallet.mobile.universal!))) { await launchUrl( - _convertToWcUri(appLink: wallet.mobile.native!, wcUri: uri), + _convertToWcUri(appLink: wallet.mobile.universal!, wcUri: uri), + mode: LaunchMode.externalApplication, ); } else { if (Platform.isIOS && wallet.appUrls.ios != null) { @@ -349,6 +353,11 @@ class AuthenticationServiceImpl implements AuthenticationService { Uri _convertToWcUri({ required String appLink, required String wcUri, - }) => - Uri.parse('$appLink/wc?uri=${Uri.encodeComponent(wcUri)}'); + }) { + // avoid generating invalid link like 'metamask:///wc?..', 'https://metamask.app.link//wc?...' + if (appLink[appLink.length - 1] == '/') { + appLink = appLink.substring(0, appLink.length - 1); + } + return Uri.parse('$appLink/wc?uri=${Uri.encodeComponent(wcUri)}'); + } } diff --git a/flutter_bird_app/lib/controller/flutter_bird_controller.dart b/flutter_bird_app/lib/controller/flutter_bird_controller.dart index e626270..e9bba1b 100644 --- a/flutter_bird_app/lib/controller/flutter_bird_controller.dart +++ b/flutter_bird_app/lib/controller/flutter_bird_controller.dart @@ -47,21 +47,20 @@ class FlutterBirdController extends ChangeNotifier { // Error handling String? lastError; - Future init(bool isInLiff) async { + Future init() async { try { // Setting Up Web3 Connection const String skinContractAddress = flutterBirdSkinsContractAddress; String rpcUrl = klaytnBaobabProviderUrl; _authenticationService = AuthenticationServiceImpl( - isInLiff: isInLiff, operatingChain: chainId, onAuthStatusChanged: () async { notifyListeners(); authorizeUser(); }); - await (_authenticationService as AuthenticationServiceImpl).initialize(isInLiff); + await (_authenticationService as AuthenticationServiceImpl).initialize(); _authorizationService = AuthorizationServiceImpl(contractAddress: skinContractAddress, rpcUrl: rpcUrl); final String abiJsonString = await rootBundle.loadString('assets/FlutterBirdSkins.json'); diff --git a/flutter_bird_app/lib/main.dart b/flutter_bird_app/lib/main.dart index 58c63de..6f7f5e7 100644 --- a/flutter_bird_app/lib/main.dart +++ b/flutter_bird_app/lib/main.dart @@ -4,7 +4,6 @@ import 'package:flutter_bird/controller/flutter_bird_controller.dart'; import 'package:flutter_bird/view/main_menu_view.dart'; import 'package:provider/provider.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:flutter_line_liff/flutter_line_liff.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -17,20 +16,17 @@ void main() async { print("Stack trace: $stackTrace"); } - final bool isInClient = FlutterLineLiff().isInClient; - runApp(MyApp(isInClient: isInClient)); + runApp(MyApp()); } class MyApp extends StatelessWidget { - final bool isInClient; - - const MyApp({Key? key, required this.isInClient}) : super(key: key); + const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (BuildContext context) { - return FlutterBirdController()..init(isInClient); + return FlutterBirdController()..init(); }, child: MaterialApp( title: 'Flutter Bird', @@ -41,7 +37,6 @@ class MyApp extends StatelessWidget { builder: (context) { return MainMenuView( title: 'Flutter Bird', - isInLiff: isInClient, ); }, ), diff --git a/flutter_bird_app/lib/view/authentication_popup.dart b/flutter_bird_app/lib/view/authentication_popup.dart index 44afa5a..563a536 100644 --- a/flutter_bird_app/lib/view/authentication_popup.dart +++ b/flutter_bird_app/lib/view/authentication_popup.dart @@ -1,3 +1,5 @@ +import 'dart:html' as html; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -7,8 +9,7 @@ import '../controller/flutter_bird_controller.dart'; import '../model/wallet_provider.dart'; class AuthenticationPopup extends StatefulWidget { - final bool isInLiff; - const AuthenticationPopup({Key? key, required this.isInLiff}) : super(key: key); + const AuthenticationPopup({Key? key}) : super(key: key); @override State createState() => _AuthenticationPopupState(); @@ -29,6 +30,13 @@ class _AuthenticationPopupState extends State { }); } + bool _isMobileWeb() { + final userAgent = html.window.navigator.userAgent.toLowerCase(); + return userAgent.contains('mobile') || + userAgent.contains('android') || + userAgent.contains('ios'); + } + @override Widget build(BuildContext context) { return Consumer( @@ -114,17 +122,11 @@ class _AuthenticationPopupState extends State { mainAxisSize: MainAxisSize.min, children: [ _buildAuthenticationStatusView(flutterBirdController), - if (!flutterBirdController.isConnected) - if (!kIsWeb && !widget.isInLiff) - Flexible( - child: _buildWalletSelector(flutterBirdController), - ), - if (!flutterBirdController.isConnected) - if (kIsWeb && widget.isInLiff) - Flexible( + if (!flutterBirdController.isConnected && _isMobileWeb()) + Flexible( child: _buildWalletSelector(flutterBirdController), ), - if (flutterBirdController.webQrData != null && kIsWeb && !widget.isInLiff) + if (flutterBirdController.webQrData != null && !_isMobileWeb()) _buildQRView(flutterBirdController.webQrData!) ], ); diff --git a/flutter_bird_app/lib/view/main_menu_view.dart b/flutter_bird_app/lib/view/main_menu_view.dart index 234d068..a2d726e 100644 --- a/flutter_bird_app/lib/view/main_menu_view.dart +++ b/flutter_bird_app/lib/view/main_menu_view.dart @@ -18,10 +18,9 @@ import 'widgets/bird.dart'; import 'widgets/flappy_text.dart'; class MainMenuView extends StatefulWidget { - final String title; - final bool isInLiff; + const MainMenuView({Key? key, required this.title}) : super(key: key); - const MainMenuView({Key? key, required this.title, required this.isInLiff}) : super(key: key); + final String title; @override State createState() => _MainMenuViewState(); @@ -376,7 +375,7 @@ class _MainMenuViewState extends State with AutomaticKeepAliveClie Navigator.of(context).push(PageRouteBuilder( opaque: false, pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { - return AuthenticationPopup(isInLiff: widget.isInLiff); + return const AuthenticationPopup(); }, transitionDuration: const Duration(milliseconds: 150), transitionsBuilder: (context, animation, secondaryAnimation, child) { diff --git a/flutter_bird_app/pubspec.yaml b/flutter_bird_app/pubspec.yaml index 11de355..47eb090 100644 --- a/flutter_bird_app/pubspec.yaml +++ b/flutter_bird_app/pubspec.yaml @@ -23,7 +23,6 @@ dependencies: provider: 6.0.3 qr_flutter: 4.1.0 http: 1.2.0 - flutter_line_liff: ^0.0.3 dev_dependencies: flutter_launcher_icons: 0.13.1 diff --git a/flutter_bird_app/web/index.html b/flutter_bird_app/web/index.html index 0483585..130aed0 100644 --- a/flutter_bird_app/web/index.html +++ b/flutter_bird_app/web/index.html @@ -31,7 +31,6 @@ flappy_bird -