feat: 1-tap connection

This commit is contained in:
2025-05-26 16:40:18 +01:00
parent f36f02e95a
commit 19d50f2947
13 changed files with 220 additions and 23 deletions

View File

@ -29,6 +29,13 @@ void runZapStream() {
TextTheme(),
),
routerConfig: GoRouter(
redirect: (context, state) {
// redirect back to the wallet settings page
if (state.uri.scheme == "zswc") {
return "/settings/wallet";
}
return null;
},
routes: [
ShellRoute(
observers: [routeObserver],

View File

@ -35,6 +35,7 @@ const defaultRelays = [
"wss://relay.fountain.fm",
];
const searchRelays = ["wss://relay.nostr.band", "wss://search.nos.today"];
const nwcRelays = ["wss://relay.getalby.com/v1"];
final loginData = LoginData();
final RouteObserver<ModalRoute<void>> routeObserver =

View File

@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 28
/// Strings: 1988 (71 per locale)
/// Strings: 1991 (71 per locale)
///
/// Built on 2025-05-26 at 12:50 UTC
/// Built on 2025-05-26 at 15:39 UTC
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import

View File

@ -285,8 +285,10 @@ class TranslationsSettingsWalletEn {
final Translations _root; // ignore: unused_field
// Translations
String get connect_wallet => 'Connect Wallet (NWC nwc://)';
String get connect_wallet => 'Connect Wallet (NWC nostr+walletconnect://)';
String get disconnect_wallet => 'Disconnect Wallet';
String get connect_1tap => '1-Tap Connection';
String get paste => 'Paste URL';
late final TranslationsSettingsWalletErrorEn error = TranslationsSettingsWalletErrorEn.internal(_root);
}
@ -366,6 +368,7 @@ class TranslationsSettingsWalletErrorEn {
// Translations
String get logged_out => 'Cant connect wallet when logged out';
String get nwc_auth_event_not_found => 'No wallet auth event found';
}
/// Flat map(s) containing all translations.
@ -449,9 +452,12 @@ extension on Translations {
case 'settings.profile.nip05': return 'Nostr Address';
case 'settings.profile.lud16': return 'Lightning Address';
case 'settings.profile.error.logged_out': return 'Cant edit profile when logged out';
case 'settings.wallet.connect_wallet': return 'Connect Wallet (NWC nwc://)';
case 'settings.wallet.connect_wallet': return 'Connect Wallet (NWC nostr+walletconnect://)';
case 'settings.wallet.disconnect_wallet': return 'Disconnect Wallet';
case 'settings.wallet.connect_1tap': return '1-Tap Connection';
case 'settings.wallet.paste': return 'Paste URL';
case 'settings.wallet.error.logged_out': return 'Cant connect wallet when logged out';
case 'settings.wallet.error.nwc_auth_event_not_found': return 'No wallet auth event found';
case 'login.username': return 'Username';
case 'login.amber': return 'Login with Amber';
case 'login.key': return 'Login with Key';

View File

@ -120,10 +120,13 @@ settings:
error:
logged_out: Cant edit profile when logged out
wallet:
connect_wallet: Connect Wallet (NWC nwc://)
connect_wallet: Connect Wallet (NWC nostr+walletconnect://)
disconnect_wallet: Disconnect Wallet
connect_1tap: 1-Tap Connection
paste: Paste URL
error:
logged_out: Cant connect wallet when logged out
nwc_auth_event_not_found: No wallet auth event found
login:
username: "Username"
amber: Login with Amber

View File

@ -1,5 +1,14 @@
import 'dart:developer' as developer;
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:ndk/ndk.dart';
import 'package:ndk/shared/nips/nip01/bip340.dart';
import 'package:ndk/shared/nips/nip01/key_pair.dart';
import 'package:protocol_handler/protocol_handler.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:zap_stream_flutter/const.dart';
import 'package:zap_stream_flutter/i18n/strings.g.dart';
import 'package:zap_stream_flutter/login.dart';
@ -13,16 +22,93 @@ class SettingsWalletPage extends StatefulWidget {
State<StatefulWidget> createState() => _Inner();
}
class _Inner extends State<SettingsWalletPage> {
const nwaHandlerUrl = "zswc://handler";
class _Inner extends State<SettingsWalletPage> with ProtocolListener {
late final TextEditingController _uri;
String? _error;
KeyPair? _nwaKey;
@override
void initState() {
_uri = TextEditingController();
protocolHandler.addListener(this);
super.initState();
}
@override
void dispose() {
protocolHandler.removeListener(this);
super.dispose();
}
@override
void onProtocolUrlReceived(String url) async {
developer.log("NWA: $url");
if (url == nwaHandlerUrl && _nwaKey != null) {
final walletInfos = ndk.requests
.query(
filters: [
Filter(kinds: [13194], pTags: [_nwaKey!.publicKey], limit: 5),
],
explicitRelays: nwcRelays,
)
.stream
.timeout(Duration(seconds: 15));
final walletInfo =
(await walletInfos.toList())
.sortedBy((e) => e.createdAt)
.reversed
.firstOrNull;
if (walletInfo == null) {
setState(() {
_error = t.settings.wallet.error.nwc_auth_event_not_found;
});
return;
} else {
final nwcUrl = Uri(
scheme: "nostr+walletconnect",
host: walletInfo.pubKey,
queryParameters: {"relay": nwcRelays, "secret": _nwaKey!.privateKey},
);
_setWallet(WalletConfig(type: WalletType.nwc, data: nwcUrl.toString()));
}
}
}
Future<void> _start1TapFlow() async {
final key = Bip340.generatePrivateKey();
final url = Uri(
scheme: "nostr+walletauth",
host: key.publicKey,
queryParameters: {
"relay": nwcRelays,
"name": "zap.stream",
"request_methods": "pay_invoice",
"icon": "https://zap.stream/logo.png",
"return_to": nwaHandlerUrl,
},
);
setState(() {
_error = null;
_nwaKey = key;
});
await launchUrl(url, mode: LaunchMode.externalApplication);
}
_setWallet(WalletConfig? cfg) {
loginData.value = LoginAccount(
type: loginData.value!.type,
pubkey: loginData.value!.pubkey,
privateKey: loginData.value!.privateKey,
signerRelays: loginData.value!.signerRelays,
wallet: cfg,
);
}
@override
Widget build(BuildContext context) {
final pubkey = ndk.accounts.getPublicKey();
@ -33,11 +119,34 @@ class _Inner extends State<SettingsWalletPage> {
builder: (context, state, child) {
if (state?.wallet == null) {
return Column(
spacing: 8,
spacing: 16,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (Platform.isAndroid) ...[
Text(
t.settings.wallet.connect_1tap,
style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold),
),
BasicButton.text(
t.button.connect,
onTap: (context) {
_start1TapFlow().onError((e, _) {
setState(() {
_error = e.toString();
});
});
},
),
],
Text(
"Paste URL",
style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold),
),
TextField(
controller: _uri,
decoration: InputDecoration(labelText: t.settings.wallet.connect_wallet),
decoration: InputDecoration(
labelText: t.settings.wallet.connect_wallet,
),
),
BasicButton.text(
t.button.connect,
@ -48,13 +157,8 @@ class _Inner extends State<SettingsWalletPage> {
type: WalletType.nwc,
data: _uri.text,
);
loginData.value = LoginAccount(
type: loginData.value!.type,
pubkey: loginData.value!.pubkey,
privateKey: loginData.value!.privateKey,
signerRelays: loginData.value!.signerRelays,
wallet: cfg,
);
_setWallet(cfg);
if (context.mounted) {
context.pop();
}
@ -73,13 +177,7 @@ class _Inner extends State<SettingsWalletPage> {
return BasicButton.text(
t.settings.wallet.disconnect_wallet,
onTap: (context) {
loginData.value = LoginAccount(
type: loginData.value!.type,
pubkey: loginData.value!.pubkey,
privateKey: loginData.value!.privateKey,
signerRelays: loginData.value!.signerRelays,
wallet: null,
);
_setWallet(null);
if (context.mounted) {
context.pop();
}