feat: add Nostr account login with account switching
Create Release / release (push) Canceled after 0s
Create Release / release (push) Canceled after 0s
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
.env
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
const anonymousPubkeyKey = 'anonymous_pubkey';
|
||||
@@ -0,0 +1,106 @@
|
||||
import 'package:file_transfer/routes.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:ndk/ndk.dart';
|
||||
import 'package:ndk/shared/nips/nip01/bip340.dart';
|
||||
import 'package:ndk_flutter/ndk_flutter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../constants.dart';
|
||||
|
||||
class AccountController extends GetxController {
|
||||
final ndk = Get.find<Ndk>();
|
||||
final ndkFlutter = Get.find<NdkFlutter>();
|
||||
final _anonymousPubkey = RxnString();
|
||||
final _isLoading = true.obs;
|
||||
final _hasRealAccount = false.obs;
|
||||
|
||||
String? get anonymousPubkey => _anonymousPubkey.value;
|
||||
bool get isLoading => _isLoading.value;
|
||||
bool get isLoggedIn => ndk.accounts.getLoggedAccount() != null;
|
||||
String? get pubkey => ndk.accounts.getLoggedAccount()?.pubkey;
|
||||
bool get hasRealAccount => _hasRealAccount.value;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_loadAccountStatus();
|
||||
_listenToAuthChanges();
|
||||
}
|
||||
|
||||
/// Listen to NDK auth state changes to update UI automatically
|
||||
void _listenToAuthChanges() {
|
||||
ndk.accounts.authStateChanges.listen((account) {
|
||||
_updateState();
|
||||
});
|
||||
}
|
||||
|
||||
void _updateState() {
|
||||
// Check if there's any account other than the anonymous one
|
||||
final accounts = ndk.accounts.accounts.values;
|
||||
_hasRealAccount.value = accounts.any(
|
||||
(a) => a.pubkey != _anonymousPubkey.value,
|
||||
);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> _loadAccountStatus() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_anonymousPubkey.value = prefs.getString(anonymousPubkeyKey);
|
||||
_updateState();
|
||||
_isLoading.value = false;
|
||||
}
|
||||
|
||||
/// Handle menu selection from account popup
|
||||
void handleMenuSelection(String? value) {
|
||||
if (value == 'add_account') {
|
||||
Get.toNamed(AppRoutes.login);
|
||||
} else if (value == 'logout') {
|
||||
handleLogout();
|
||||
} else if (value?.startsWith('switch_') == true) {
|
||||
final switchPubkey = value!.substring(7);
|
||||
handleSwitchAccount(switchPubkey);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle logout action
|
||||
Future<void> handleLogout() async {
|
||||
// Logout the current active account
|
||||
ndk.accounts.logout();
|
||||
|
||||
// Switch to another account if available
|
||||
final remainingAccounts = ndk.accounts.accounts.values;
|
||||
if (remainingAccounts.isNotEmpty) {
|
||||
final accountToSwitch = remainingAccounts.first;
|
||||
ndk.accounts.switchAccount(pubkey: accountToSwitch.pubkey);
|
||||
} else {
|
||||
// No accounts left, create a new anonymous account
|
||||
await _createAnonymousAccount();
|
||||
}
|
||||
|
||||
await ndkFlutter.saveAccountsState();
|
||||
updateAuth();
|
||||
}
|
||||
|
||||
/// Handle account switch
|
||||
Future<void> handleSwitchAccount(String pubkey) async {
|
||||
ndk.accounts.switchAccount(pubkey: pubkey);
|
||||
await ndkFlutter.saveAccountsState();
|
||||
updateAuth();
|
||||
}
|
||||
|
||||
/// Create a new anonymous account
|
||||
Future<void> _createAnonymousAccount() async {
|
||||
final keyPair = Bip340.generatePrivateKey();
|
||||
ndk.accounts.loginPrivateKey(
|
||||
pubkey: keyPair.publicKey,
|
||||
privkey: keyPair.privateKey!,
|
||||
);
|
||||
await ndkFlutter.saveAccountsState();
|
||||
// Update the anonymous pubkey in preferences
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(anonymousPubkeyKey, keyPair.publicKey);
|
||||
updateAuth();
|
||||
}
|
||||
|
||||
void updateAuth() => update();
|
||||
}
|
||||
+14
-1
@@ -1,7 +1,10 @@
|
||||
import 'package:file_transfer/constants.dart';
|
||||
import 'package:file_transfer/controllers/account_controller.dart';
|
||||
import 'package:file_transfer/controllers/file_share_controller.dart';
|
||||
import 'package:file_transfer/controllers/home_controller.dart';
|
||||
import 'package:file_transfer/pages/file_share_page.dart';
|
||||
import 'package:file_transfer/pages/home_page.dart';
|
||||
import 'package:file_transfer/pages/login_page.dart';
|
||||
import 'package:file_transfer/routes.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -9,7 +12,9 @@ import 'package:get/get.dart';
|
||||
import 'package:ndk/ndk.dart';
|
||||
import 'package:ndk/shared/nips/nip01/bip340.dart';
|
||||
import 'package:ndk_flutter/ndk_flutter.dart';
|
||||
import 'package:ndk_flutter/l10n/app_localizations.dart' as ndk_flutter;
|
||||
import 'package:toastification/toastification.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -27,16 +32,22 @@ void main() async {
|
||||
|
||||
await ndkFlutter.restoreAccountsState();
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
if (ndk.accounts.accounts.isEmpty) {
|
||||
// First time: create anonymous account
|
||||
final keyPair = Bip340.generatePrivateKey();
|
||||
ndk.accounts.loginPrivateKey(
|
||||
pubkey: keyPair.publicKey,
|
||||
privkey: keyPair.privateKey!,
|
||||
);
|
||||
|
||||
await ndkFlutter.saveAccountsState();
|
||||
await prefs.setString(anonymousPubkeyKey, keyPair.publicKey);
|
||||
}
|
||||
|
||||
// Initialize AccountController after NDK is set up
|
||||
Get.put(AccountController());
|
||||
|
||||
runApp(const MainApp());
|
||||
}
|
||||
|
||||
@@ -50,6 +61,7 @@ class MainApp extends StatelessWidget {
|
||||
title: 'File Transfer',
|
||||
theme: ThemeData.light(),
|
||||
darkTheme: ThemeData.dark(),
|
||||
localizationsDelegates: const [ndk_flutter.AppLocalizations.delegate],
|
||||
getPages: [
|
||||
GetPage(
|
||||
name: AppRoutes.home,
|
||||
@@ -58,6 +70,7 @@ class MainApp extends StatelessWidget {
|
||||
Get.put(HomePageController());
|
||||
}),
|
||||
),
|
||||
GetPage(name: AppRoutes.login, page: () => const LoginPage()),
|
||||
GetPage(
|
||||
name: AppRoutes.fileShare,
|
||||
page: () => const FileSharePage(),
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:file_transfer/controllers/account_controller.dart';
|
||||
import 'package:file_transfer/controllers/home_controller.dart';
|
||||
import 'package:file_transfer/routes.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dropzone/flutter_dropzone.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:ndk/ndk.dart';
|
||||
import 'package:ndk_flutter/ndk_flutter.dart';
|
||||
|
||||
class UploadView extends GetView<HomePageController> {
|
||||
const UploadView({super.key});
|
||||
@@ -12,9 +16,251 @@ class UploadView extends GetView<HomePageController> {
|
||||
Widget build(BuildContext context) {
|
||||
final isSmallScreen = MediaQuery.of(context).size.width < 600;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final accountController = Get.find<AccountController>();
|
||||
final ndkFlutter = Get.find<NdkFlutter>();
|
||||
final ndk = Get.find<Ndk>();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('File Transfer')),
|
||||
appBar: AppBar(
|
||||
title: const Text('File Transfer'),
|
||||
actions: [
|
||||
GetBuilder<AccountController>(
|
||||
builder: (_) {
|
||||
if (accountController.isLoading) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (!accountController.hasRealAccount) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: TextButton.icon(
|
||||
onPressed: () => Get.toNamed(AppRoutes.login),
|
||||
icon: const Icon(Icons.login),
|
||||
label: const Text('Login'),
|
||||
),
|
||||
);
|
||||
}
|
||||
final pubkey = accountController.pubkey;
|
||||
if (pubkey == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
final accounts = ndk.accounts.accounts.values;
|
||||
final currentPubkey = ndk.accounts
|
||||
.getLoggedAccount()
|
||||
?.pubkey;
|
||||
|
||||
if (isSmallScreen) {
|
||||
showModalBottomSheet<String>(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
builder: (ctx) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
for (final account in accounts)
|
||||
ListTile(
|
||||
leading: SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: account.pubkey == currentPubkey
|
||||
? Stack(
|
||||
children: [
|
||||
NPicture(
|
||||
ndkFlutter: ndkFlutter,
|
||||
pubkey: account.pubkey,
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: colorScheme.surface,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: NPicture(
|
||||
ndkFlutter: ndkFlutter,
|
||||
pubkey: account.pubkey,
|
||||
),
|
||||
),
|
||||
title:
|
||||
account.pubkey ==
|
||||
accountController.anonymousPubkey
|
||||
? const Text('Anonymous')
|
||||
: NName(
|
||||
ndkFlutter: ndkFlutter,
|
||||
pubkey: account.pubkey,
|
||||
style: TextStyle(
|
||||
fontWeight:
|
||||
account.pubkey == currentPubkey
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: account.pubkey == currentPubkey
|
||||
? const Text('Current')
|
||||
: null,
|
||||
onTap: () {
|
||||
Navigator.pop(ctx);
|
||||
accountController.handleSwitchAccount(
|
||||
account.pubkey,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add_circle_outline),
|
||||
title: const Text('Add account'),
|
||||
onTap: () {
|
||||
Navigator.pop(ctx);
|
||||
Get.toNamed(AppRoutes.login);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.logout,
|
||||
color: colorScheme.error,
|
||||
),
|
||||
title: Text(
|
||||
'Logout',
|
||||
style: TextStyle(color: colorScheme.error),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(ctx);
|
||||
accountController.handleLogout();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
showMenu<String>(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(
|
||||
MediaQuery.of(context).size.width,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(
|
||||
color: colorScheme.outlineVariant,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
items: [
|
||||
for (final account in accounts)
|
||||
PopupMenuItem<String>(
|
||||
value: 'switch_${account.pubkey}',
|
||||
child: ListTile(
|
||||
leading: SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: account.pubkey == currentPubkey
|
||||
? Stack(
|
||||
children: [
|
||||
NPicture(
|
||||
ndkFlutter: ndkFlutter,
|
||||
pubkey: account.pubkey,
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: colorScheme.surface,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: NPicture(
|
||||
ndkFlutter: ndkFlutter,
|
||||
pubkey: account.pubkey,
|
||||
),
|
||||
),
|
||||
title:
|
||||
account.pubkey ==
|
||||
accountController.anonymousPubkey
|
||||
? const Text('Anonymous')
|
||||
: NName(
|
||||
ndkFlutter: ndkFlutter,
|
||||
pubkey: account.pubkey,
|
||||
style: TextStyle(
|
||||
fontWeight:
|
||||
account.pubkey == currentPubkey
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: account.pubkey == currentPubkey
|
||||
? const Text('Current')
|
||||
: null,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
const PopupMenuItem(
|
||||
value: 'add_account',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.add_circle_outline),
|
||||
title: Text('Add account'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'logout',
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
Icons.logout,
|
||||
color: colorScheme.error,
|
||||
),
|
||||
title: Text(
|
||||
'Logout',
|
||||
style: TextStyle(color: colorScheme.error),
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
),
|
||||
],
|
||||
).then(accountController.handleMenuSelection);
|
||||
}
|
||||
},
|
||||
child: NPicture(ndkFlutter: ndkFlutter),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: kIsWeb
|
||||
? _buildWebDropzone(context, isSmallScreen, colorScheme)
|
||||
: _buildDesktopDropzone(context, isSmallScreen, colorScheme),
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:file_transfer/routes.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:ndk_flutter/ndk_flutter.dart';
|
||||
|
||||
class LoginPage extends StatelessWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
void _navigateBack(BuildContext context) {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
Get.offAllNamed(AppRoutes.home);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ndkFlutter = Get.find<NdkFlutter>();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Connect to Nostr'),
|
||||
leading: IconButton(
|
||||
icon: const BackButtonIcon(),
|
||||
onPressed: () => _navigateBack(context),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: NLogin(
|
||||
ndkFlutter: ndkFlutter,
|
||||
enablePubkeyLogin: false,
|
||||
onLoggedIn: () => _navigateBack(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
class AppRoutes {
|
||||
static const home = '/';
|
||||
static const login = '/login';
|
||||
static const fileShare = '/f/:nevent/:encodedPrivateKey';
|
||||
|
||||
static String fileShareRoute(String nevent, String encodedPrivateKey) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import desktop_drop
|
||||
import file_picker
|
||||
import file_saver
|
||||
import flutter_secure_storage_darwin
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
@@ -16,5 +17,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
|
||||
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
|
||||
@@ -636,6 +636,62 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.28.0"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.21"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.6"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
||||
+2
-1
@@ -1,7 +1,7 @@
|
||||
name: file_transfer
|
||||
description: "Zero SPOF file transfer over Nostr. E2E encrypted, decentralized, and private."
|
||||
publish_to: 'none'
|
||||
version: 0.1.0+1
|
||||
version: 0.2.0
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.0
|
||||
@@ -24,6 +24,7 @@ dependencies:
|
||||
toastification: ^3.0.3
|
||||
file_transfer_sdk:
|
||||
path: packages/file_transfer_sdk
|
||||
shared_preferences: ^2.5.4
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user