Skip to content

Commit

Permalink
Support Mini Wallet (#23)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ulbqb authored Aug 5, 2024
1 parent ab91313 commit 3cd8fe9
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 69 deletions.
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();

_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;
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);
}
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

1 comment on commit 3cd8fe9

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.