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

Feature/1 click auth #292

Merged
merged 13 commits into from
Jun 10, 2024
2 changes: 1 addition & 1 deletion example/dapp/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:name="com.example.dapp.MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:taskAffinity="com.walletconnect.flutterdapp.activity"
Expand Down
1 change: 1 addition & 0 deletions example/dapp/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<key>LSApplicationQueriesSchemes</key>
<array>
<string>wcflutterwallet</string>
<string>walletapp</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
Expand Down
9 changes: 9 additions & 0 deletions example/dapp/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ class _MyHomePageState extends State<MyHomePage> {
_web3App!.onSessionEvent.subscribe(_onSessionEvent);
_web3App!.onSessionUpdate.subscribe(_onSessionUpdate);

_web3App!.core.addLogListener(_logListener);
_web3App!.core.relayClient.onRelayClientConnect.subscribe(_setState);
_web3App!.core.relayClient.onRelayClientDisconnect.subscribe(_setState);
_web3App!.core.relayClient.onRelayClientMessage.subscribe(_onRelayMessage);
Expand Down Expand Up @@ -140,6 +141,7 @@ class _MyHomePageState extends State<MyHomePage> {
_web3App!.onSessionEvent.unsubscribe(_onSessionEvent);
_web3App!.onSessionUpdate.unsubscribe(_onSessionUpdate);

_web3App!.core.removeLogListener(_logListener);
_web3App!.core.relayClient.onRelayClientConnect.unsubscribe(_setState);
_web3App!.core.relayClient.onRelayClientDisconnect.unsubscribe(_setState);
_web3App!.core.relayClient.onRelayClientMessage
Expand All @@ -150,6 +152,13 @@ class _MyHomePageState extends State<MyHomePage> {
super.dispose();
}

void _logListener(LogEvent event) {
debugPrint('[SampleDapp] ${event.level.name}: ${event.message}');
if (event.level == Level.error) {
// TODO send to mixpanel
}
}

@override
Widget build(BuildContext context) {
if (_initializing) {
Expand Down
185 changes: 155 additions & 30 deletions example/dapp/lib/pages/connect_page.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// ignore_for_file: use_build_context_synchronously

import 'dart:async';
import 'dart:convert';

import 'package:fl_toast/fl_toast.dart';
import 'package:flutter/foundation.dart';
Expand Down Expand Up @@ -165,6 +166,47 @@ class ConnectPageState extends State<ConnectPage> {
),
);

children.add(const SizedBox(height: 16.0));

children.add(
ElevatedButton(
onPressed: _selectedChains.isEmpty
? null
: () => _oneClickAuth(
closeModal: () {
if (Navigator.canPop(context)) {
Navigator.of(context).pop();
}
},
),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith<Color>(
(states) {
if (states.contains(MaterialState.disabled)) {
return StyleConstants.grayColor;
}
return StyleConstants.primaryColor;
},
),
minimumSize: MaterialStateProperty.all<Size>(const Size(
1000.0,
StyleConstants.linear48,
)),
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
StyleConstants.linear8,
),
),
),
),
child: const Text(
'1-Click Auth',
style: StyleConstants.buttonText,
),
),
);

children.add(const SizedBox.square(dimension: 12.0));

return Center(
Expand Down Expand Up @@ -275,21 +317,22 @@ class ConnectPageState extends State<ConnectPage> {
if (openApp) {
launchUrlString(uri, mode: LaunchMode.externalApplication);
} else {
_showQrCode(connectResponse);
_showQrCode(connectResponse.uri.toString());
}
} else {
_showQrCode(connectResponse);
_showQrCode(connectResponse.uri.toString());
}

debugPrint('Awaiting session proposal settlement');
final _ = await connectResponse.session.future;

showToast?.call(StringConstants.connectionEstablished);
closeModal?.call();
}

Future<void> _showQrCode(ConnectResponse response) async {
Future<void> _showQrCode(String uri) async {
// Show the QR code
debugPrint('Showing QR Code: ${response.uri}');
debugPrint('Showing QR Code: $uri');
_shouldDismissQrCode = true;
if (kIsWeb) {
await showDialog(
Expand All @@ -307,7 +350,7 @@ class ConnectPageState extends State<ConnectPage> {
child: Padding(
padding: const EdgeInsets.all(20.0),
child: _QRCodeView(
uri: response.uri.toString(),
uri: uri,
),
),
),
Expand All @@ -328,19 +371,12 @@ class ConnectPageState extends State<ConnectPage> {
context,
MaterialPageRoute(
fullscreenDialog: true,
builder: (context) => QRCodeScreen(response: response),
builder: (context) => QRCodeScreen(uri: uri),
),
);
}

void _onSessionConnect(SessionConnect? event) async {
if (event == null) return;

if (_shouldDismissQrCode && Navigator.canPop(context)) {
_shouldDismissQrCode = false;
Navigator.pop(context);
}

void _requestAuth(SessionConnect? event) async {
final shouldAuth = await showDialog(
context: context,
barrierDismissible: false,
Expand Down Expand Up @@ -368,49 +404,138 @@ class ConnectPageState extends State<ConnectPage> {
if (!shouldAuth) return;

try {
final scheme = event.session.peer.metadata.redirect?.native ?? '';
launchUrlString(scheme, mode: LaunchMode.externalApplication);

final pairingTopic = event.session.pairingTopic;
final pairingTopic = event?.session.pairingTopic;
// Send off an auth request now that the pairing/session is established
debugPrint('Requesting authentication');
final authRes = await widget.web3App.requestAuth(
final authResponse = await widget.web3App.requestAuth(
pairingTopic: pairingTopic,
params: AuthRequestParams(
chainId: _selectedChains[0].chainId,
domain: Constants.domain,
aud: Constants.aud,
// statement: 'Welcome to example flutter app',
statement: 'Welcome to example flutter app',
),
);

debugPrint('Awaiting authentication response');
final authResponse = await authRes.completer.future;
final scheme = event?.session.peer.metadata.redirect?.native;
launchUrlString(
scheme ?? 'wcflutterwallet://',
mode: LaunchMode.externalApplication,
);

debugPrint('[SampleDapp] Awaiting authentication response');
final response = await authResponse.completer.future;
debugPrint('[SampleDapp] response ${jsonEncode(response.toJson())}');

if (authResponse.error != null) {
debugPrint('Authentication failed: ${authResponse.error}');
if (response.result != null) {
showPlatformToast(
child: const Text(StringConstants.authFailed),
child: const Text(StringConstants.authSucceeded),
context: context,
);
} else {
final error = response.error ?? response.jsonRpcError;
showPlatformToast(
child: const Text(StringConstants.authSucceeded),
child: Text(error.toString()),
context: context,
);
}
} catch (e) {
debugPrint('[SampleDapp] auth $e');
showPlatformToast(
child: const Text(StringConstants.connectionFailed),
context: context,
);
}
}

void _oneClickAuth({VoidCallback? closeModal}) async {
final methods = optionalNamespaces['eip155']?.methods ?? [];
final authResponse = await widget.web3App.authenticate(
params: OCARequestParams(
chains: _selectedChains.map((e) => e.chainId).toList(),
domain: Constants.domain,
nonce: AuthUtils.generateNonce(),
uri: Constants.aud,
statement: 'Welcome to example flutter app',
methods: methods,
),
);

final encodedUri = Uri.encodeComponent(authResponse.uri.toString());
final uri = 'wcflutterwallet://wc?uri=$encodedUri';

if (await canLaunchUrlString(uri)) {
final openApp = await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: const Text('Do you want to open with Web3Wallet Flutter'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Show QR'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Open'),
),
],
);
},
);
if (openApp) {
launchUrlString(uri, mode: LaunchMode.externalApplication);
} else {
_showQrCode(authResponse.uri.toString());
}
} else {
_showQrCode(authResponse.uri.toString());
}

try {
debugPrint('[SampleDapp] Awaiting 1-CA session');
final response = await authResponse.completer.future;
debugPrint('[SampleDapp] response ${jsonEncode(response.toJson())}');

if (response.session != null) {
showPlatformToast(
child: const Text(
'${StringConstants.authSucceeded} and '
'${StringConstants.connectionEstablished}',
),
context: context,
);
} else {
final error = response.error ?? response.jsonRpcError;
showPlatformToast(
child: Text(error.toString()),
context: context,
);
}
} catch (e) {
debugPrint('[SampleDapp] 1-CA $e');
showPlatformToast(
child: const Text(StringConstants.connectionFailed),
context: context,
);
}
closeModal?.call();
}

void _onSessionConnect(SessionConnect? event) async {
if (event == null) return;

if (_shouldDismissQrCode && Navigator.canPop(context)) {
_shouldDismissQrCode = false;
Navigator.pop(context);
}

_requestAuth(event);
}
}

class QRCodeScreen extends StatefulWidget {
const QRCodeScreen({super.key, required this.response});
final ConnectResponse response;
const QRCodeScreen({super.key, required this.uri});
final String uri;

@override
State<QRCodeScreen> createState() => _QRCodeScreenState();
Expand All @@ -423,7 +548,7 @@ class _QRCodeScreenState extends State<QRCodeScreen> {
child: Scaffold(
appBar: AppBar(title: const Text(StringConstants.scanQrCode)),
body: _QRCodeView(
uri: widget.response.uri!.toString(),
uri: widget.uri,
),
),
);
Expand Down
2 changes: 1 addition & 1 deletion example/dapp/lib/pages/sessions_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class SessionsPageState extends State<SessionsPage> {
(session) => ExpansionPanel(
canTapOnHeader: true,
isExpanded: _selectedSession == session.topic,
backgroundColor: Colors.black12,
backgroundColor: Colors.blue.withOpacity(0.2),
headerBuilder: (context, isExpanded) {
return SessionItem(
key: ValueKey(session.topic),
Expand Down
2 changes: 1 addition & 1 deletion example/dapp/lib/widgets/auth_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class AuthItem extends StatelessWidget {
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(12.0),
color: Colors.green.withOpacity(0.2),
color: Colors.blue.withOpacity(0.2),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expand Down
13 changes: 11 additions & 2 deletions example/dapp/lib/widgets/pairing_item.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
import 'package:walletconnect_flutter_v2_dapp/utils/constants.dart';

Expand All @@ -14,11 +15,19 @@ class PairingItem extends StatelessWidget {

@override
Widget build(BuildContext context) {
final expiryTimestamp = DateTime.fromMillisecondsSinceEpoch(
pairing.expiry * 1000,
);
final dateFormat = DateFormat.yMd().add_jm();
final expiryDate = dateFormat.format(expiryTimestamp);
final inDays = expiryTimestamp.difference(DateTime.now()).inDays + 1;
return InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(12.0),
color: Colors.blue.withOpacity(0.2),
color: pairing.active
? Colors.blue.withOpacity(0.2)
: Colors.red.withOpacity(0.2),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expand All @@ -27,7 +36,7 @@ class PairingItem extends StatelessWidget {
style: StyleConstants.paragraph,
),
Text(
pairing.peerMetadata?.url ?? 'Unknown',
pairing.peerMetadata?.url ?? 'Expiry: $expiryDate ($inDays days)',
),
],
),
Expand Down
Loading