From ef5ffbf42b3d4b313a2171ce3eca2498c20f2d73 Mon Sep 17 00:00:00 2001 From: shiki-tak Date: Sat, 13 Jul 2024 15:01:15 +0900 Subject: [PATCH 1/3] feat: connect and sign from LIFF --- flutter_bird_app/.gitignore | 3 +- .../controller/authentication_service.dart | 213 +++++++++---- .../controller/flutter_bird_controller.dart | 119 ++++++-- flutter_bird_app/lib/main.dart | 35 ++- .../lib/model/wallet_provider.dart | 126 ++++++-- .../lib/view/authentication_popup.dart | 289 +++++++++++------- flutter_bird_app/lib/view/main_menu_view.dart | 18 +- flutter_bird_app/pubspec.yaml | 4 + flutter_bird_app/web/index.html | 1 + 9 files changed, 573 insertions(+), 235 deletions(-) diff --git a/flutter_bird_app/.gitignore b/flutter_bird_app/.gitignore index e064682..b3b63f4 100644 --- a/flutter_bird_app/.gitignore +++ b/flutter_bird_app/.gitignore @@ -33,7 +33,6 @@ pubspec.lock # Web related -lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols @@ -50,3 +49,5 @@ app.*.map.json /analysis_options.yaml .env + +netlify.toml diff --git a/flutter_bird_app/lib/controller/authentication_service.dart b/flutter_bird_app/lib/controller/authentication_service.dart index 2033ad5..5ec98a9 100644 --- a/flutter_bird_app/lib/controller/authentication_service.dart +++ b/flutter_bird_app/lib/controller/authentication_service.dart @@ -5,17 +5,17 @@ import 'dart:math' as math; import 'package:eth_sig_util/eth_sig_util.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:http/http.dart' as http; import 'package:nonce/nonce.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; -import '../../model/account.dart'; -import '../../model/wallet_provider.dart'; +import '../model/account.dart'; +import '../model/wallet_provider.dart'; -/// Manages the authentication process and communication with crypto wallets abstract class AuthenticationService { + Future initialize(bool isInLiff); + List get availableWallets; Account? get authenticatedAccount; @@ -26,69 +26,117 @@ abstract class AuthenticationService { bool get isAuthenticated; - String? get webQrData; + bool get isConnected; - requestAuthentication({WalletProvider? walletProvider}); + String? get webQrData; - unauthenticate(); + WalletProvider? get lastUsedWallet; + Future requestAuthentication({WalletProvider? walletProvider}); + Future unauthenticate(); } class AuthenticationServiceImpl implements AuthenticationService { - @override - late final List availableWallets; - + final bool isInLiff; final int operatingChain; + final Function() onAuthStatusChanged; + WalletProvider? _lastUsedWallet; + + static const String projectId = String.fromEnvironment('WALLET_CONNECT_PROJECT_ID'); + + AuthenticationServiceImpl({ + required this.isInLiff, + required this.operatingChain, + required this.onAuthStatusChanged, + }); + + List _availableWallets = []; Web3App? _connector; - Function() onAuthStatusChanged; + bool _isInitialized = false; @override - String get operatingChainName => operatingChain == 1001 ? 'Klaytn Testnet' : 'Chain $operatingChain'; + List get availableWallets => _availableWallets; @override Account? get authenticatedAccount => _authenticatedAccount; Account? _authenticatedAccount; @override - bool get isOnOperatingChain => currentChain == operatingChain; - - SessionData? get currentSession => _connector?.sessions.getAll().firstOrNull; + String get operatingChainName => operatingChain == 1001 ? 'Klaytn Testnet' : 'Chain $operatingChain'; - int? get currentChain => int.tryParse(currentSession?.namespaces['eip155']?.accounts.first.split(':')[1] ?? ''); + @override + bool get isOnOperatingChain => currentChain == operatingChain; @override bool get isAuthenticated => isConnected && authenticatedAccount != null; + @override + String? get webQrData => _webQrData; + String? _webQrData; + + WalletProvider? get lastUsedWallet => _lastUsedWallet; + + SessionData? get currentSession => _connector?.sessions.getAll().firstOrNull; bool get isConnected => currentSession != null; - // The data to display in a QR Code for connections on Desktop / Browser. + int? get currentChain => int.tryParse(currentSession?.namespaces['eip155']?.accounts.first.split(':')[1] ?? ''); + @override - String? webQrData; + Future initialize(bool isInLiff) async { + if (_isInitialized) { + return; + } + + await _createConnector(); + await _clearSessions(); - AuthenticationServiceImpl({ - required this.operatingChain, - required this.onAuthStatusChanged, - }) { - if (kIsWeb) { - requestAuthentication(); + if (!kIsWeb || isInLiff) { + await _loadWallets(); } else { - _loadWallets(); + print('AuthenticationServiceImpl: Skipping wallet loading for Web'); } + + _isInitialized = true; } - /// Loads all WalletConnect compatible wallets - _loadWallets() async { - final walletResponse = await http.get(Uri.parse('https://registry.walletconnect.org/data/wallets.json')); - final walletData = json.decode(walletResponse.body); - availableWallets = walletData.entries.map((data) => WalletProvider.fromJson(data.value)).toList(); + // Update to support WalletConnect v2 + Future _loadWallets() async { + try { + final response = await http.get( + Uri.parse('https://explorer-api.walletconnect.com/v3/wallets?projectId=${projectId}&entries=5&page=1'), + headers: {'Accept': 'application/json'}, + ); + + if (response.statusCode == 200) { + final Map responseData = json.decode(response.body); + final Map walletsData = responseData['listings']; + + _availableWallets = walletsData.entries.map((entry) { + try { + return WalletProvider.fromJson(entry.value); + } catch (e, stackTrace) { + print('Error creating WalletProvider from data: ${entry.value}'); + print('Error: $e'); + print('Stack trace: $stackTrace'); + return null; + } + }).where((wallet) => wallet != null).cast().toList(); + + } else { + throw Exception('Failed to load wallets: ${response.statusCode}'); + } + } catch (e) { + log('Error loading wallets: $e'); + } } - /// Prompts user to authenticate with a wallet @override - requestAuthentication({WalletProvider? walletProvider}) async { + Future requestAuthentication({WalletProvider? walletProvider}) async { + await _updateConnectionStatus(); // Create fresh connector await _createConnector(walletProvider: walletProvider); - // Create a new session + _lastUsedWallet = walletProvider; + if (!isConnected) { try { ConnectResponse resp = await _connector!.connect( @@ -103,14 +151,20 @@ class AuthenticationServiceImpl implements AuthenticationService { Uri? uri = resp.uri; if (uri != null) { - if (kIsWeb) { - webQrData = uri.toString(); + // Web + if (kIsWeb && !isInLiff) { + _webQrData = uri.toString(); onAuthStatusChanged(); + // LIFF + } else if(kIsWeb && isInLiff) { + _launchWallet(wallet: walletProvider, uri: uri.toString()); + // Native } else { _launchWallet(wallet: walletProvider, uri: uri.toString()); } } + await resp.session.future; onAuthStatusChanged(); } catch (e) { log('Error during connect: $e', name: 'AuthenticationService'); @@ -118,22 +172,49 @@ class AuthenticationServiceImpl implements AuthenticationService { } } - /// Send request to the users wallet to sign a message - /// User will be authenticated if the signature could be verified + // 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; + + String? address = currentSession?.namespaces['eip155']?.accounts.first.split(':').last; + if (address == null) return false; + + return _verifySignature(walletProvider: _lastUsedWallet, address: address); + } + + // To maintain consistency during testing, delete the session before opening the app each time. + // These(_clearSessions, _updateConnectionStatus) may not be necessary for user convenience. + Future _clearSessions() async { + if (_connector != null) { + final sessions = _connector!.sessions.getAll(); + for (var session in sessions) { + await _connector!.disconnectSession( + topic: session.topic, + reason: Errors.getSdkError(Errors.USER_DISCONNECTED), + ); + } + } + } + + Future _updateConnectionStatus() async { + final sessions = _connector?.sessions.getAll(); + } + Future _verifySignature({WalletProvider? walletProvider, String? address}) async { if (address == null || currentChain == null || !isOnOperatingChain) return false; + // Native if (!kIsWeb) { - // Launch wallet app if on mobile - // Delay to make sure FlutterBird is in foreground before launching wallet app again await Future.delayed(const Duration(seconds: 1)); - // v2 doesn't have a uri property in currentSession so you need to get the proper URI. + _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}'); } + - log('Signing message...', name: 'AuthenticationService'); - - // Let Crypto Wallet sign custom message String nonce = Nonce.generate(32, math.Random.secure()); String messageText = 'Please sign this message to authenticate with Flutter Bird.\nChallenge: $nonce'; final String signature = await _connector!.request( @@ -159,48 +240,47 @@ class AuthenticationServiceImpl implements AuthenticationService { } @override - unauthenticate() async { + Future unauthenticate() async { if (currentSession != null) { await _connector?.disconnectSession(topic: currentSession!.topic, reason: Errors.getSdkError(Errors.USER_DISCONNECTED)); } _authenticatedAccount = null; _connector = null; - webQrData = null; + _webQrData = null; } - /// Creates a WalletConnect Instance Future _createConnector({WalletProvider? walletProvider}) async { - // Create WalletConnect Connector try { _connector = await Web3App.createInstance( - projectId: dotenv.env['WALLET_CONNECT_PROJECT_ID']!, + projectId: projectId, metadata: const PairingMetadata( name: 'Flutter Bird', description: 'WalletConnect Developer App', - url: 'https://flutter-bird.netlify.app', + url: 'https://dynamic-tartufo-87d5f8.netlify.app', // FIXME: real url icons: [ 'https://raw.githubusercontent.com/Tonnanto/flutter-bird/v1.0/flutter_bird_app/assets/icon.png', ], ), ); - // Subscribe to events _connector?.onSessionConnect.subscribe((SessionConnect? session) async { - 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(); + 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(); + } }); _connector?.onSessionUpdate.subscribe((SessionUpdate? payload) async { log('session_update: ' + payload.toString(), name: 'AuthenticationService'); - webQrData = null; + _webQrData = null; onAuthStatusChanged(); }); _connector?.onSessionDelete.subscribe((SessionDelete? session) { log('disconnect: ' + session.toString(), name: 'AuthenticationService'); - webQrData = null; + _webQrData = null; _authenticatedAccount = null; onAuthStatusChanged(); }); @@ -218,22 +298,23 @@ class AuthenticationServiceImpl implements AuthenticationService { return; } - if (wallet.universal != null && await canLaunchUrl(Uri.parse(wallet.universal!))) { + if (wallet.mobile.universal != null && await canLaunchUrl(Uri.parse(wallet.mobile.universal!))) { await launchUrl( - _convertToWcUri(appLink: wallet.universal!, wcUri: uri), + _convertToWcUri(appLink: wallet.mobile.universal!, wcUri: uri), mode: LaunchMode.externalApplication, ); - } else if (wallet.native != null && await canLaunchUrl(Uri.parse(wallet.native!))) { + } else if (wallet.mobile.native != null && await canLaunchUrl(Uri.parse(wallet.mobile.native!))) { await launchUrl( - _convertToWcUri(appLink: wallet.native!, wcUri: uri), + _convertToWcUri(appLink: wallet.mobile.native!, wcUri: uri), ); } else { - if (Platform.isIOS && wallet.iosLink != null) { - await launchUrl(Uri.parse(wallet.iosLink!)); - } else if (Platform.isAndroid && wallet.androidLink != null) { - await launchUrl(Uri.parse(wallet.androidLink!)); + if (Platform.isIOS && wallet.appUrls.ios != null) { + await launchUrl(Uri.parse(wallet.appUrls.ios!)); + } else if (Platform.isAndroid && wallet.appUrls.android != null) { + await launchUrl(Uri.parse(wallet.appUrls.android!)); } } + } Uri _convertToWcUri({ diff --git a/flutter_bird_app/lib/controller/flutter_bird_controller.dart b/flutter_bird_app/lib/controller/flutter_bird_controller.dart index ea3f4b2..7580c9a 100644 --- a/flutter_bird_app/lib/controller/flutter_bird_controller.dart +++ b/flutter_bird_app/lib/controller/flutter_bird_controller.dart @@ -22,8 +22,14 @@ class FlutterBirdController extends ChangeNotifier { bool get isAuthenticated => _authenticationService.isAuthenticated; + bool get isConnected => _authenticationService.isConnected; + + WalletProvider? get lastUsedWallet => (_authenticationService as AuthenticationServiceImpl).lastUsedWallet; + String? get currentAddressShort => - '${authenticatedAccount?.address.substring(0, 8)}...${authenticatedAccount?.address.substring(36)}'; + authenticatedAccount?.address != null + ? '${authenticatedAccount!.address.substring(0, 8)}...${authenticatedAccount!.address.substring(36)}' + : null; String? get webQrData => _authenticationService.webQrData; bool _loadingSkins = false; @@ -32,44 +38,99 @@ class FlutterBirdController extends ChangeNotifier { List? skins; String? skinOwnerAddress; - init() { - // Setting Up Web3 Connection - const String skinContractAddress = flutterBirdSkinsContractAddress; - String rpcUrl = klaytnBaobabProviderUrl; + // Error handling + String? lastError; - _authenticationService = AuthenticationServiceImpl( - operatingChain: chainId, - onAuthStatusChanged: () async { - notifyListeners(); - authorizeUser(); - }); - _authorizationService = AuthorizationServiceImpl(contractAddress: skinContractAddress, rpcUrl: rpcUrl); + Future init(bool isInLiff) 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); + _authorizationService = AuthorizationServiceImpl(contractAddress: skinContractAddress, rpcUrl: rpcUrl); + + } catch (e) { + lastError = 'Initialization error: $e'; + printDebugInfo('Initialization error: $e'); + notifyListeners(); + } } - requestAuthentication({WalletProvider? walletProvider}) { - _authenticationService.requestAuthentication(walletProvider: walletProvider); + Future verifySignature() async { + try { + bool result = await (_authenticationService as AuthenticationServiceImpl).verifySignature(); + if (result) { + await authorizeUser(); + } + notifyListeners(); + } catch (e) { + lastError = 'Signature verification error: $e'; + printDebugInfo('Signature verification error: $e'); + notifyListeners(); + rethrow; + } } - unauthenticate() { - _authenticationService.unauthenticate(); - notifyListeners(); + Future requestAuthentication({WalletProvider? walletProvider}) async { + try { + await _authenticationService.requestAuthentication(walletProvider: walletProvider); + } catch (e) { + lastError = 'Authentication error: $e'; + printDebugInfo('Authentication error: $e'); + notifyListeners(); + } + } + + void unauthenticate() { + try { + _authenticationService.unauthenticate(); + notifyListeners(); + } catch (e) { + lastError = 'Unauthentication error: $e'; + printDebugInfo('Unauthentication error: $e'); + notifyListeners(); + } } /// Loads a users owned skins - authorizeUser({bool forceReload = false}) async { - // Reload skins only if address changed - if (!_loadingSkins && (forceReload || skinOwnerAddress != authenticatedAccount?.address)) { - _loadingSkins = true; - await _authorizationService.authorizeUser(authenticatedAccount?.address, onSkinsUpdated: (skins) { - skins?.sort( - (a, b) => a.tokenId.compareTo(b.tokenId), - ); - this.skins = skins; + Future authorizeUser({bool forceReload = false}) async { + try { + // Reload skins only if address changed + if (!_loadingSkins && (forceReload || skinOwnerAddress != authenticatedAccount?.address)) { + _loadingSkins = true; + await _authorizationService.authorizeUser(authenticatedAccount?.address, onSkinsUpdated: (skins) { + skins?.sort( + (a, b) => a.tokenId.compareTo(b.tokenId), + ); + this.skins = skins; + notifyListeners(); + }); + skinOwnerAddress = authenticatedAccount?.address; + _loadingSkins = false; notifyListeners(); - }); - skinOwnerAddress = authenticatedAccount?.address; - _loadingSkins = false; + } + } catch (e) { + lastError = 'Authorization error: $e'; + printDebugInfo('Authorization error: $e'); notifyListeners(); } } + + void printDebugInfo(String message) { + print('FlutterBirdController: $message'); + print('isAuthenticated: $isAuthenticated'); + print('isOnOperatingChain: $isOnOperatingChain'); + print('authenticatedAccount: ${authenticatedAccount?.address}'); + print('availableWallets: ${availableWallets.length}'); + print('webQrData: $webQrData'); + } } diff --git a/flutter_bird_app/lib/main.dart b/flutter_bird_app/lib/main.dart index 2192e64..54ca939 100644 --- a/flutter_bird_app/lib/main.dart +++ b/flutter_bird_app/lib/main.dart @@ -1,30 +1,51 @@ +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; 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(); + if (const String.fromEnvironment('FLUTTER_ENV') != 'production') { + await dotenv.load(fileName: ".env"); + } -void main() { - dotenv.load(fileName: ".env"); - runApp(const MyApp()); + print("Starting application..."); + const String liffId = String.fromEnvironment('LIFF_ID'); + + final String? os = FlutterLineLiff().os; + final bool isInClient = FlutterLineLiff().isInClient; + runApp(MyApp(isInClient: isInClient)); } class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); + final bool isInClient; + + const MyApp({Key? key, required this.isInClient}) : super(key: key); - // This widget is the root of your application. @override Widget build(BuildContext context) { return ChangeNotifierProvider( - create: (BuildContext context) => FlutterBirdController()..init(), + create: (BuildContext context) { + return FlutterBirdController()..init(isInClient); + }, child: MaterialApp( title: 'Flutter Bird', theme: ThemeData( primarySwatch: Colors.blue, ), - home: const MainMenuView(title: 'Flutter Bird'), + home: Builder( + builder: (context) { + return MainMenuView( + title: 'Flutter Bird', + isInLiff: isInClient, + ); + }, + ), ), ); } } + diff --git a/flutter_bird_app/lib/model/wallet_provider.dart b/flutter_bird_app/lib/model/wallet_provider.dart index 29c0115..e1a2289 100644 --- a/flutter_bird_app/lib/model/wallet_provider.dart +++ b/flutter_bird_app/lib/model/wallet_provider.dart @@ -1,35 +1,119 @@ -/// Represents a compatible Wallet Provider class WalletProvider { final String id; final String name; - final String? imageUrl; - final String? iosLink; - final String? androidLink; - final String? native; - final String? universal; + final String imageId; + final ImageUrls imageUrl; + final String? description; + final String? homepage; + final List chains; + final List versions; + final List sdks; + final AppUrls appUrls; + final MobileInfo mobile; + final DesktopInfo desktop; WalletProvider({ required this.id, required this.name, + required this.imageId, required this.imageUrl, - required this.iosLink, - required this.androidLink, - required this.native, - required this.universal, + this.description, + this.homepage, + required this.chains, + required this.versions, + required this.sdks, + required this.appUrls, + required this.mobile, + required this.desktop, }); - static WalletProvider fromJson(Map json) { - Map appMap = json['app'] as Map; - Map imageMap = json['image_url'] as Map; - Map mobileMap = json['mobile'] as Map; + factory WalletProvider.fromJson(Map json) { return WalletProvider( - id: json['id'] as String, - name: json['name'] as String, - imageUrl: imageMap['md'] as String?, - iosLink: appMap['ios'] as String?, - androidLink: appMap['android'] as String?, - native: mobileMap['native'] as String?, - universal: mobileMap['universal'] as String?, + id: json['id'], + name: json['name'], + imageId: json['image_id'], + imageUrl: ImageUrls.fromJson(json['image_url']), + description: json['description'], + homepage: json['homepage'], + chains: List.from(json['chains'] ?? []), + versions: List.from(json['versions'] ?? []), + sdks: List.from(json['sdks'] ?? []), + appUrls: AppUrls.fromJson(json['app'] ?? {}), + mobile: MobileInfo.fromJson(json['mobile'] ?? {}), + desktop: DesktopInfo.fromJson(json['desktop'] ?? {}), + ); + } +} + +class MobileInfo { + final String? native; + final String? universal; + + MobileInfo({this.native, this.universal}); + + factory MobileInfo.fromJson(Map json) { + return MobileInfo( + native: json['native'], + universal: json['universal'], + ); + } +} + +class DesktopInfo { + final String? native; + final String? universal; + + DesktopInfo({this.native, this.universal}); + + factory DesktopInfo.fromJson(Map json) { + return DesktopInfo( + native: json['native'], + universal: json['universal'], + ); + } +} + +class ImageUrls { + final String sm; + final String md; + final String lg; + + ImageUrls({required this.sm, required this.md, required this.lg}); + + factory ImageUrls.fromJson(Map json) { + return ImageUrls( + sm: json['sm'] ?? '', + md: json['md'] ?? '', + lg: json['lg'] ?? '', + ); + } +} + +class AppUrls { + final String? browser; + final String? ios; + final String? android; + final String? mac; + final String? windows; + final String? linux; + + AppUrls({ + this.browser, + this.ios, + this.android, + this.mac, + this.windows, + this.linux, + }); + + factory AppUrls.fromJson(Map json) { + return AppUrls( + browser: json['browser'], + ios: json['ios'], + android: json['android'], + mac: json['mac'], + windows: json['windows'], + linux: json['linux'], ); } } diff --git a/flutter_bird_app/lib/view/authentication_popup.dart b/flutter_bird_app/lib/view/authentication_popup.dart index 809118e..44afa5a 100644 --- a/flutter_bird_app/lib/view/authentication_popup.dart +++ b/flutter_bird_app/lib/view/authentication_popup.dart @@ -7,7 +7,8 @@ import '../controller/flutter_bird_controller.dart'; import '../model/wallet_provider.dart'; class AuthenticationPopup extends StatefulWidget { - const AuthenticationPopup({Key? key}) : super(key: key); + final bool isInLiff; + const AuthenticationPopup({Key? key, required this.isInLiff}) : super(key: key); @override State createState() => _AuthenticationPopupState(); @@ -15,82 +16,129 @@ class AuthenticationPopup extends StatefulWidget { class _AuthenticationPopupState extends State { String? uri; + String? errorMessage; @override void initState() { super.initState(); } + void _setErrorMessage(String message) { + setState(() { + errorMessage = message; + }); + } + @override Widget build(BuildContext context) { return Consumer( - builder: (context, web3Service, child) => Scaffold( + builder: (context, flutterBirdController, child) => Scaffold( backgroundColor: Colors.transparent, body: Stack( children: [ _buildBackground(), - _buildBody(web3Service), + _buildBody(flutterBirdController), + if (errorMessage != null) _buildErrorOverlay(), ], ), ), ); } - _buildBody(FlutterBirdController flutterBirdController) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox( - height: 80, + Widget _buildErrorOverlay() { + return Container( + color: Colors.black.withOpacity(0.8), + child: Center( + child: Container( + padding: const EdgeInsets.all(20), + margin: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), ), - Expanded( - flex: (flutterBirdController.isAuthenticated || kIsWeb) ? 0 : 1, - child: Container( - constraints: const BoxConstraints( - minHeight: 240, - maxWidth: 340, - ), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(24)), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), - child: flutterBirdController.isAuthenticated - ? _buildAuthenticatedView(flutterBirdController) - : _buildUnauthenticatedView(flutterBirdController), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Error', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + const SizedBox(height: 10), + Text(errorMessage ?? '', style: const TextStyle(fontSize: 16)), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + setState(() { + errorMessage = null; + }); + }, + child: const Text('Close'), ), - ), + ], ), - const SizedBox( - height: 80, + ), + ), + ); + } + + Widget _buildBody(FlutterBirdController flutterBirdController) { + return Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 80), + child: Container( + constraints: BoxConstraints( + minHeight: 240, + maxWidth: 340, + maxHeight: MediaQuery.of(context).size.height - 160, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), + child: flutterBirdController.isAuthenticated + ? _buildAuthenticatedView(flutterBirdController) + : _buildUnauthenticatedView(flutterBirdController), + ), ), - ], + ), ), ); } - _buildUnauthenticatedView(FlutterBirdController flutterBirdController) { + Widget _buildUnauthenticatedView(FlutterBirdController flutterBirdController) { if (kIsWeb && flutterBirdController.webQrData == null) { // Generates QR Data flutterBirdController.requestAuthentication(); } return Column( + mainAxisSize: MainAxisSize.min, children: [ _buildAuthenticationStatusView(flutterBirdController), - if (!kIsWeb) _buildWalletSelector(flutterBirdController), - if (flutterBirdController.webQrData != null && kIsWeb) _buildQRView(flutterBirdController.webQrData!) + if (!flutterBirdController.isConnected) + if (!kIsWeb && !widget.isInLiff) + Flexible( + child: _buildWalletSelector(flutterBirdController), + ), + if (!flutterBirdController.isConnected) + if (kIsWeb && widget.isInLiff) + Flexible( + child: _buildWalletSelector(flutterBirdController), + ), + if (flutterBirdController.webQrData != null && kIsWeb && !widget.isInLiff) + _buildQRView(flutterBirdController.webQrData!) ], ); } - _buildAuthenticatedView(FlutterBirdController flutterBirdController) => Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildAuthenticationStatusView(flutterBirdController), - _buildConnectButton(flutterBirdController), - ], - ); + Widget _buildAuthenticatedView(FlutterBirdController flutterBirdController) => Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildAuthenticationStatusView(flutterBirdController), + _buildConnectButton(flutterBirdController), + ], + ); - _buildAuthenticationStatusView(FlutterBirdController flutterBirdController) { + Widget _buildAuthenticationStatusView(FlutterBirdController flutterBirdController) { String statusText = 'Not Authenticated'; if (flutterBirdController.isAuthenticated) { statusText = flutterBirdController.isOnOperatingChain ? 'Authenticated' : '\nAuthenticated on wrong chain'; @@ -102,89 +150,113 @@ class _AuthenticationPopupState extends State { style: Theme.of(context).textTheme.titleLarge, ), if (!flutterBirdController.isOnOperatingChain) - const SizedBox( - height: 16, - ), + const SizedBox(height: 16,), if (!flutterBirdController.isOnOperatingChain) Text( 'Connect a wallet on ${flutterBirdController.operatingChainName}', style: Theme.of(context).textTheme.bodyLarge, ), if (flutterBirdController.isAuthenticated) - const SizedBox( - height: 16, - ), + const SizedBox(height: 16,), if (flutterBirdController.isAuthenticated) Text( 'Wallet address:\n' + (flutterBirdController.authenticatedAccount?.address ?? ''), style: Theme.of(context).textTheme.bodyLarge, - ) + ), + if (flutterBirdController.isConnected && !flutterBirdController.isAuthenticated) + Column( + children: [ + const SizedBox(height: 16), + Text( + "Please sign to authenticate your wallet.", + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + ElevatedButton( + onPressed: () async { + try { + await flutterBirdController.verifySignature(); + } catch (e) { + _setErrorMessage('Error during signature verification: $e'); + } + }, + child: Text('Sign Message'), + ), + ], + ), ], ); } - _buildConnectButton(FlutterBirdController web3Service) { + Widget _buildConnectButton(FlutterBirdController flutterBirdController) { return ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: web3Service.isAuthenticated ? Colors.redAccent : Colors.green, - ), - onPressed: () async { - if (web3Service.isAuthenticated) { - web3Service.unauthenticate(); - } else { - web3Service.requestAuthentication(); - } - }, - child: SizedBox( - height: 40, - child: Center( - child: Text( - web3Service.isAuthenticated ? 'Disconnect' : 'Connect', - style: Theme.of(context).textTheme.labelLarge?.copyWith(color: Colors.white), - ), + style: ElevatedButton.styleFrom( + backgroundColor: flutterBirdController.isAuthenticated ? Colors.redAccent : Colors.green, + ), + onPressed: () async { + if (flutterBirdController.isAuthenticated) { + flutterBirdController.unauthenticate(); + } else { + flutterBirdController.requestAuthentication(); + } + }, + child: SizedBox( + height: 40, + child: Center( + child: Text( + flutterBirdController.isAuthenticated ? 'Disconnect' : 'Connect', + style: Theme.of(context).textTheme.labelLarge?.copyWith(color: Colors.white), ), - )); + ), + ) + ); } - _buildWalletSelector(FlutterBirdController flutterBirdController) { - return Expanded( - child: ListView.separated( - shrinkWrap: true, - itemCount: flutterBirdController.availableWallets.length, - itemBuilder: (BuildContext context, int index) { - WalletProvider wallet = flutterBirdController.availableWallets[index]; - return ListTile( - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - wallet.name, - overflow: TextOverflow.ellipsis, - ), - ), - SizedBox( - height: 40, - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: wallet.imageUrl == null ? Container() : Image.network(wallet.imageUrl!), - ), - ), - ], + Widget _buildWalletSelector(FlutterBirdController flutterBirdController) { + if (flutterBirdController.availableWallets.isEmpty) { + return Center(child: CircularProgressIndicator()); + } + return ListView.separated( + shrinkWrap: true, + physics: ClampingScrollPhysics(), + itemCount: flutterBirdController.availableWallets.length, + itemBuilder: (BuildContext context, int index) { + WalletProvider wallet = flutterBirdController.availableWallets[index]; + return ListTile( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + wallet.name, + overflow: TextOverflow.ellipsis, + ), ), - trailing: const Icon(Icons.chevron_right_rounded), - onTap: () { - flutterBirdController.requestAuthentication(walletProvider: wallet); - }); - }, - separatorBuilder: (BuildContext context, int index) => const SizedBox( - height: 4, - ), - ), + SizedBox( + height: 40, + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: wallet.imageUrl.sm == '' ? Container() : Image.network(wallet.imageUrl.sm), + ), + ), + ], + ), + trailing: const Icon(Icons.chevron_right_rounded), + onTap: () { + try { + flutterBirdController.requestAuthentication(walletProvider: wallet); + } catch (e) { + _setErrorMessage('Error connecting to wallet: $e'); + } + }, + ); + }, + separatorBuilder: (BuildContext context, int index) => const SizedBox(height: 4), ); } - _buildQRView(String data) { + Widget _buildQRView(String data) { return QrImageView( data: data, version: QrVersions.auto, @@ -192,13 +264,14 @@ class _AuthenticationPopupState extends State { ); } - _buildBackground() => Positioned.fill( - child: GestureDetector( - onTap: () { - Navigator.of(context).pop(); - }, - child: Container( - color: Colors.black54, - ), - )); + Widget _buildBackground() => Positioned.fill( + child: GestureDetector( + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + color: Colors.black54, + ), + ), + ); } diff --git a/flutter_bird_app/lib/view/main_menu_view.dart b/flutter_bird_app/lib/view/main_menu_view.dart index 5ef0532..315d1cd 100644 --- a/flutter_bird_app/lib/view/main_menu_view.dart +++ b/flutter_bird_app/lib/view/main_menu_view.dart @@ -15,9 +15,10 @@ import 'widgets/bird.dart'; import 'widgets/flappy_text.dart'; class MainMenuView extends StatefulWidget { - const MainMenuView({Key? key, required this.title}) : super(key: key); - final String title; + final bool isInLiff; + + const MainMenuView({Key? key, required this.title, required this.isInLiff}) : super(key: key); @override State createState() => _MainMenuViewState(); @@ -75,6 +76,7 @@ class _MainMenuViewState extends State with AutomaticKeepAliveClie worldDimensions = Size(min(maxWidth, screenDimensions.width), screenDimensions.height * 3 / 4); birdSize = worldDimensions.height / 8; + try { return Scaffold( body: Consumer(builder: (context, web3Service, child) { web3Service.authorizeUser(); @@ -104,6 +106,16 @@ class _MainMenuViewState extends State with AutomaticKeepAliveClie ); }), ); + } catch(e, stackTrace) { + print("Error in MainMenuView: $e"); + print("StackTrace: $stackTrace"); + return Scaffold( + body: Center( + child: Text("An error occurred. Please try again."), + ), + ); + } + } Widget _buildMenu(FlutterBirdController web3Service) => Column( @@ -317,7 +329,7 @@ class _MainMenuViewState extends State with AutomaticKeepAliveClie Navigator.of(context).push(PageRouteBuilder( opaque: false, pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { - return const AuthenticationPopup(); + return AuthenticationPopup(isInLiff: widget.isInLiff); }, 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 acef346..9a75847 100644 --- a/flutter_bird_app/pubspec.yaml +++ b/flutter_bird_app/pubspec.yaml @@ -10,6 +10,9 @@ environment: dependencies: flutter: sdk: flutter + package_info_plus: ^5.0.1 + shared_preferences_android: ^2.0.0 + url_launcher_android: ^6.0.0 flutter_dotenv: ^5.1.0 web3dart: 2.7.3 @@ -20,6 +23,7 @@ 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 130aed0..0483585 100644 --- a/flutter_bird_app/web/index.html +++ b/flutter_bird_app/web/index.html @@ -31,6 +31,7 @@ flappy_bird +