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

@ -31,6 +31,12 @@
<data android:scheme="http" android:host="zap.stream" />
<data android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="zswc" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->

View File

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Zap Stream Flutter</string>
<string>zap.stream</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@ -76,5 +76,18 @@
<string>vi</string>
<string>zh</string>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleURLSchemes</key>
<array>
<string>zswc</string>
</array>
</dict>
</array>
</dict>
</plist>

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

View File

@ -14,6 +14,7 @@ import flutter_secure_storage_macos
import objectbox_flutter_libs
import package_info_plus
import path_provider_foundation
import protocol_handler_macos
import share_plus
import shared_preferences_foundation
import sqflite_darwin
@ -31,6 +32,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ObjectboxFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "ObjectboxFlutterLibsPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ProtocolHandlerMacosPlugin.register(with: registry.registrar(forPlugin: "ProtocolHandlerMacosPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))

View File

@ -896,6 +896,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.9.1"
protocol_handler:
dependency: "direct main"
description:
name: protocol_handler
sha256: dc2e2dcb1e0e313c3f43827ec3fa6d98adee6e17edc0c3923ac67efee87479a9
url: "https://pub.dev"
source: hosted
version: "0.2.0"
protocol_handler_android:
dependency: transitive
description:
name: protocol_handler_android
sha256: "82eb860ca42149e400328f54b85140329a1766d982e94705b68271f6ca73895c"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
protocol_handler_ios:
dependency: transitive
description:
name: protocol_handler_ios
sha256: "0d3a56b8c1926002cb1e32b46b56874759f4dcc8183d389b670864ac041b6ec2"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
protocol_handler_macos:
dependency: transitive
description:
name: protocol_handler_macos
sha256: "6eb8687a84e7da3afbc5660ce046f29d7ecf7976db45a9dadeae6c87147dd710"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
protocol_handler_platform_interface:
dependency: transitive
description:
name: protocol_handler_platform_interface
sha256: "53776b10526fdc25efdf1abcf68baf57fdfdb75342f4101051db521c9e3f3e5b"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
protocol_handler_windows:
dependency: transitive
description:
name: protocol_handler_windows
sha256: d8f3a58938386aca2c76292757392f4d059d09f11439d6d896d876ebe997f2c4
url: "https://pub.dev"
source: hosted
version: "0.2.0"
provider:
dependency: transitive
description:
@ -1373,6 +1421,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.13.0"
win32_registry:
dependency: transitive
description:
name: win32_registry
sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852"
url: "https://pub.dev"
source: hosted
version: "1.1.5"
xdg_directories:
dependency: transitive
description:

View File

@ -44,6 +44,7 @@ dependencies:
http: ^1.4.0
flutter_local_notifications: ^19.2.1
flutter_dotenv: ^5.2.1
protocol_handler: ^0.2.0
dependency_overrides:
ndk:

View File

@ -11,6 +11,7 @@
#include <firebase_core/firebase_core_plugin_c_api.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <objectbox_flutter_libs/objectbox_flutter_libs_plugin.h>
#include <protocol_handler_windows/protocol_handler_windows_plugin_c_api.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
@ -25,6 +26,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
ObjectboxFlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ObjectboxFlutterLibsPlugin"));
ProtocolHandlerWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ProtocolHandlerWindowsPluginCApi"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(

View File

@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
firebase_core
flutter_secure_storage_windows
objectbox_flutter_libs
protocol_handler_windows
share_plus
url_launcher_windows
)