diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index b6707dc..587f5a0 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -31,6 +31,12 @@
+
+
+
+
+
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 48ade1a..d092642 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -5,7 +5,7 @@
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
- Zap Stream Flutter
+ zap.stream
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
@@ -76,5 +76,18 @@
vi
zh
+ CFBundleURLTypes
+
+
+ CFBundleTypeRole
+ Editor
+ CFBundleURLName
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleURLSchemes
+
+ zswc
+
+
+
diff --git a/lib/app.dart b/lib/app.dart
index b9a9a4d..8bd6d94 100644
--- a/lib/app.dart
+++ b/lib/app.dart
@@ -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],
diff --git a/lib/const.dart b/lib/const.dart
index 28f9f03..2fca126 100644
--- a/lib/const.dart
+++ b/lib/const.dart
@@ -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> routeObserver =
diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart
index da34753..e4d442a 100644
--- a/lib/i18n/strings.g.dart
+++ b/lib/i18n/strings.g.dart
@@ -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
diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart
index 6187254..10c1b62 100644
--- a/lib/i18n/strings_en.g.dart
+++ b/lib/i18n/strings_en.g.dart
@@ -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';
diff --git a/lib/i18n/translated/en.i18n.yaml b/lib/i18n/translated/en.i18n.yaml
index e473cd8..c1c814e 100644
--- a/lib/i18n/translated/en.i18n.yaml
+++ b/lib/i18n/translated/en.i18n.yaml
@@ -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
diff --git a/lib/pages/settings_wallet.dart b/lib/pages/settings_wallet.dart
index aeb89ab..5d34a5a 100644
--- a/lib/pages/settings_wallet.dart
+++ b/lib/pages/settings_wallet.dart
@@ -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 createState() => _Inner();
}
-class _Inner extends State {
+const nwaHandlerUrl = "zswc://handler";
+
+class _Inner extends State 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 _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 {
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 {
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 {
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();
}
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index 8a74080..172e45c 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -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"))
diff --git a/pubspec.lock b/pubspec.lock
index 2ab4e91..c5de158 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -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:
diff --git a/pubspec.yaml b/pubspec.yaml
index 8a6dd43..1c2e184 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -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:
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index bdb6e1a..1827ee1 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -11,6 +11,7 @@
#include
#include
#include
+#include
#include
#include
@@ -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(
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index e1a7c66..75a3233 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -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
)