Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Mini Wallet #23

Merged
merged 9 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 50 additions & 41 deletions flutter_bird_app/lib/controller/authentication_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -16,7 +17,7 @@ import '../model/wallet_provider.dart';

/// Manages the authentication process and communication with crypto wallets
abstract class AuthenticationService {
Future<void> initialize(bool isInLiff);
Future<void> initialize();

List<WalletProvider> get availableWallets;

Expand All @@ -38,15 +39,13 @@ abstract class AuthenticationService {
}

class AuthenticationServiceImpl implements AuthenticationService {
final bool isInLiff;
final int operatingChain;
final Function() onAuthStatusChanged;
WalletProvider? _lastUsedWallet;

String projectId = dotenv.env['WALLET_CONNECT_PROJECT_ID'] ?? '';

AuthenticationServiceImpl({
required this.isInLiff,
required this.operatingChain,
required this.onAuthStatusChanged,
});
Expand Down Expand Up @@ -83,19 +82,14 @@ class AuthenticationServiceImpl implements AuthenticationService {
int? get currentChain => int.tryParse(currentSession?.namespaces['eip155']?.accounts.first.split(':')[1] ?? '');

@override
Future<void> initialize(bool isInLiff) async {
Future<void> initialize() async {
if (_isInitialized) {
return;
}

await _createConnector();
await _clearSessions();

if (!kIsWeb || isInLiff) {
await _loadWallets();
} else {
print('AuthenticationServiceImpl: Skipping wallet loading for Web');
}
await _loadWallets();
Mdaiki0730 marked this conversation as resolved.
Show resolved Hide resolved

_isInitialized = true;
}
Expand Down Expand Up @@ -123,6 +117,23 @@ class AuthenticationServiceImpl implements AuthenticationService {
}
}).where((wallet) => wallet != null).cast<WalletProvider>().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}');
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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<bool> verifySignature() async {
if (currentChain == null || !isOnOperatingChain) return false;

Expand Down Expand Up @@ -233,16 +240,8 @@ class AuthenticationServiceImpl implements AuthenticationService {
Future<bool> _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';
Expand Down Expand Up @@ -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');
Expand All @@ -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;
Mdaiki0730 marked this conversation as resolved.
Show resolved Hide resolved
Mdaiki0730 marked this conversation as resolved.
Show resolved Hide resolved
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) {
Expand All @@ -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);
}
Mdaiki0730 marked this conversation as resolved.
Show resolved Hide resolved
return Uri.parse('$appLink/wc?uri=${Uri.encodeComponent(wcUri)}');
}
}
5 changes: 2 additions & 3 deletions flutter_bird_app/lib/controller/flutter_bird_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,20 @@ class FlutterBirdController extends ChangeNotifier {
// Error handling
String? lastError;

Future<void> init(bool isInLiff) async {
Future<void> 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');
Expand Down
11 changes: 3 additions & 8 deletions flutter_bird_app/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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',
Expand All @@ -41,7 +37,6 @@ class MyApp extends StatelessWidget {
builder: (context) {
return MainMenuView(
title: 'Flutter Bird',
isInLiff: isInClient,
);
},
),
Expand Down
24 changes: 13 additions & 11 deletions flutter_bird_app/lib/view/authentication_popup.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:html' as html;

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
Expand All @@ -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<AuthenticationPopup> createState() => _AuthenticationPopupState();
Expand All @@ -29,6 +30,13 @@ class _AuthenticationPopupState extends State<AuthenticationPopup> {
});
}

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<FlutterBirdController>(
Expand Down Expand Up @@ -114,17 +122,11 @@ class _AuthenticationPopupState extends State<AuthenticationPopup> {
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!)
],
);
Expand Down
7 changes: 3 additions & 4 deletions flutter_bird_app/lib/view/main_menu_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<MainMenuView> createState() => _MainMenuViewState();
Expand Down Expand Up @@ -376,7 +375,7 @@ class _MainMenuViewState extends State<MainMenuView> with AutomaticKeepAliveClie
Navigator.of(context).push(PageRouteBuilder(
opaque: false,
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return AuthenticationPopup(isInLiff: widget.isInLiff);
return const AuthenticationPopup();
},
transitionDuration: const Duration(milliseconds: 150),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
Expand Down
1 change: 0 additions & 1 deletion flutter_bird_app/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion flutter_bird_app/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@

<title>flappy_bird</title>
<link rel="manifest" href="manifest.json">
<script charset="utf-8" src="https://static.line-scdn.net/liff/edge/versions/2.20.3/sdk.js"></script>
</head>
<body>
<!-- This script installs service_worker.js to provide PWA functionality to
Expand Down
Loading