Add account UI and secure session storage
This commit is contained in:
@@ -24,7 +24,8 @@ android {
|
||||
applicationId = "com.example.flutter_app"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
// flutter_secure_storage 10.x requires Android API 23 or newer.
|
||||
minSdk = 23
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
abstract interface class SessionTokenStore {
|
||||
Future<String?> read();
|
||||
|
||||
Future<void> write(String token);
|
||||
|
||||
Future<void> delete();
|
||||
}
|
||||
|
||||
class SecureSessionTokenStore implements SessionTokenStore {
|
||||
SecureSessionTokenStore({FlutterSecureStorage? storage})
|
||||
: _storage = storage ?? const FlutterSecureStorage();
|
||||
|
||||
static const _key = 'sovereignai.account.access_token';
|
||||
|
||||
final FlutterSecureStorage _storage;
|
||||
|
||||
@override
|
||||
Future<String?> read() => _storage.read(key: _key);
|
||||
|
||||
@override
|
||||
Future<void> write(String token) => _storage.write(key: _key, value: token);
|
||||
|
||||
@override
|
||||
Future<void> delete() => _storage.delete(key: _key);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:http_parser/http_parser.dart';
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../auth/session_token_store.dart';
|
||||
import '../../features/chat/presentation/cubit/chat_state.dart';
|
||||
|
||||
const _uuid = Uuid();
|
||||
@@ -18,13 +19,25 @@ class AgentRunResult {
|
||||
}
|
||||
|
||||
class ApiRepository {
|
||||
ApiRepository({required String baseUrl}) : _baseUrl = baseUrl;
|
||||
ApiRepository({required String baseUrl, SessionTokenStore? sessionTokenStore})
|
||||
: _baseUrl = baseUrl,
|
||||
_sessionTokenStore = sessionTokenStore ?? SecureSessionTokenStore();
|
||||
|
||||
String _baseUrl;
|
||||
final SessionTokenStore _sessionTokenStore;
|
||||
String? _accessToken;
|
||||
Future<String>? _loadingLocalSession;
|
||||
Future<void>? _restoringSession;
|
||||
Map<String, dynamic>? _currentUser;
|
||||
bool _sessionRestored = false;
|
||||
http.Client? _activeRequestClient;
|
||||
String get baseUrl => _baseUrl;
|
||||
Map<String, dynamic>? get currentUser => _currentUser;
|
||||
String? get accountEmail {
|
||||
final user = _currentUser;
|
||||
if (user == null || user['mode'] != 'account') return null;
|
||||
return user['email'] as String?;
|
||||
}
|
||||
|
||||
void cancelActiveRequest() {
|
||||
_activeRequestClient?.close();
|
||||
@@ -37,6 +50,44 @@ class ApiRepository {
|
||||
_baseUrl = value.replaceFirst(RegExp(r'/+$'), '');
|
||||
_accessToken = null;
|
||||
_loadingLocalSession = null;
|
||||
_currentUser = null;
|
||||
_sessionRestored = false;
|
||||
_restoringSession = null;
|
||||
}
|
||||
|
||||
Future<void> initializeSession() async {
|
||||
if (_sessionRestored) return;
|
||||
final pending = _restoringSession;
|
||||
if (pending != null) return pending;
|
||||
final restoring = _restoreAccountSession();
|
||||
_restoringSession = restoring;
|
||||
try {
|
||||
await restoring;
|
||||
} finally {
|
||||
_restoringSession = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _restoreAccountSession() async {
|
||||
try {
|
||||
final token = await _sessionTokenStore.read();
|
||||
if (token == null || token.isEmpty) return;
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/v1/auth/me'),
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
);
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
await _sessionTokenStore.delete();
|
||||
return;
|
||||
}
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
_accessToken = token;
|
||||
_currentUser = data['user'] as Map<String, dynamic>?;
|
||||
} catch (_) {
|
||||
// Local single-user mode remains available if secure storage is absent.
|
||||
} finally {
|
||||
_sessionRestored = true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getModelName() async {
|
||||
@@ -64,6 +115,7 @@ class ApiRepository {
|
||||
}
|
||||
|
||||
Future<String> _localSessionToken() async {
|
||||
await initializeSession();
|
||||
final cached = _accessToken;
|
||||
if (cached != null) return cached;
|
||||
final existing = _loadingLocalSession;
|
||||
@@ -86,6 +138,7 @@ class ApiRepository {
|
||||
_checkStatus(response);
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
_accessToken = data['access_token'] as String;
|
||||
_currentUser = data['user'] as Map<String, dynamic>?;
|
||||
return _accessToken!;
|
||||
}
|
||||
|
||||
@@ -116,20 +169,56 @@ class ApiRepository {
|
||||
);
|
||||
_checkStatus(response);
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
_accessToken = data['access_token'] as String;
|
||||
final token = data['access_token'] as String;
|
||||
try {
|
||||
await _sessionTokenStore.write(token);
|
||||
} catch (error) {
|
||||
try {
|
||||
await http.post(
|
||||
Uri.parse('$_baseUrl/v1/auth/logout'),
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
);
|
||||
} catch (_) {
|
||||
// The short-lived server session expires automatically if revocation fails.
|
||||
}
|
||||
throw Exception(
|
||||
'تعذر حفظ جلسة الحساب في التخزين الآمن على هذا الجهاز: $error',
|
||||
);
|
||||
}
|
||||
_accessToken = token;
|
||||
_currentUser = data['user'] as Map<String, dynamic>?;
|
||||
_sessionRestored = true;
|
||||
return data;
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
await initializeSession();
|
||||
final token = _accessToken;
|
||||
if (token == null) return;
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/v1/auth/logout'),
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
);
|
||||
_checkStatus(response);
|
||||
_accessToken = null;
|
||||
_loadingLocalSession = null;
|
||||
Object? logoutError;
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/v1/auth/logout'),
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
);
|
||||
_checkStatus(response);
|
||||
} catch (error) {
|
||||
logoutError = error;
|
||||
} finally {
|
||||
try {
|
||||
await _sessionTokenStore.delete();
|
||||
} finally {
|
||||
_accessToken = null;
|
||||
_currentUser = null;
|
||||
_loadingLocalSession = null;
|
||||
_sessionRestored = true;
|
||||
}
|
||||
}
|
||||
if (logoutError != null) {
|
||||
throw Exception(
|
||||
'أُزيل الرمز من الجهاز، لكن تعذر تأكيد إلغاء جلسة الخادم: $logoutError',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _checkStatus(http.Response response) {
|
||||
|
||||
@@ -423,6 +423,28 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reloadForAccountChange() async {
|
||||
if (state.isSending) return;
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: const [],
|
||||
conversations: const [],
|
||||
clearActiveConversation: true,
|
||||
isLoadingHistory: true,
|
||||
isWorkspaceMode: false,
|
||||
clearSelectedWorkspacePath: true,
|
||||
availableWorkspaceFiles: const [],
|
||||
selectedWorkspaceFiles: const [],
|
||||
clearSelectedSkill: true,
|
||||
requestStatus: RequestStatus.idle,
|
||||
clearAgentProgress: true,
|
||||
clearPendingFileChangeProposal: true,
|
||||
clearError: true,
|
||||
),
|
||||
);
|
||||
await Future.wait([_loadHistory(), _loadAgentSkills()]);
|
||||
}
|
||||
|
||||
Future<void> _saveCurrent(List<ChatMessage> messages) async {
|
||||
if (messages.isEmpty) return;
|
||||
final firstQuestion = messages.firstWhere(
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../../audio/presentation/cubit/voice_cubit.dart';
|
||||
import '../../../audio/presentation/cubit/voice_state.dart';
|
||||
import '../../../../core/notifications/completion_notification_service.dart';
|
||||
import '../../../../core/network/api_repository.dart';
|
||||
import '../cubit/chat_cubit.dart';
|
||||
import '../cubit/chat_state.dart';
|
||||
|
||||
@@ -423,6 +424,232 @@ class _ChatPageState extends State<ChatPage> {
|
||||
controller.dispose();
|
||||
}
|
||||
|
||||
Future<void> _manageAccount() async {
|
||||
final cubit = context.read<ChatCubit>();
|
||||
final api = context.read<ApiRepository>();
|
||||
if (cubit.state.isSending) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('انتظر حتى ينتهي الطلب قبل تبديل الحساب.'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (api.accountEmail != null) {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: AlertDialog(
|
||||
title: const Text('تسجيل الخروج'),
|
||||
content: Text('هل تريد تسجيل الخروج من ${api.accountEmail}؟'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('إلغاء'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('تسجيل الخروج'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
try {
|
||||
await api.logout();
|
||||
await cubit.reloadForAccountChange();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('تم تسجيل الخروج والعودة إلى الحساب المحلي.'),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(error.toString().replaceFirst('Exception: ', '')),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final emailController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
var register = false;
|
||||
var validationError = '';
|
||||
final credentials = await showDialog<(bool, String, String)>(
|
||||
context: context,
|
||||
builder:
|
||||
(dialogContext) => StatefulBuilder(
|
||||
builder:
|
||||
(context, setDialogState) => Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: AlertDialog(
|
||||
title: Text(register ? 'إنشاء حساب' : 'تسجيل الدخول'),
|
||||
content: SizedBox(
|
||||
width: 360,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: emailController,
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autofillHints: const [AutofillHints.email],
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'البريد الإلكتروني',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: passwordController,
|
||||
obscureText: true,
|
||||
autofillHints: [
|
||||
register
|
||||
? AutofillHints.newPassword
|
||||
: AutofillHints.password,
|
||||
],
|
||||
onSubmitted: (_) {
|
||||
final email = emailController.text.trim();
|
||||
final password = passwordController.text;
|
||||
if (email.isEmpty || password.isEmpty) {
|
||||
setDialogState(
|
||||
() =>
|
||||
validationError =
|
||||
'أدخل البريد وكلمة المرور.',
|
||||
);
|
||||
} else if (register && password.length < 12) {
|
||||
setDialogState(
|
||||
() =>
|
||||
validationError =
|
||||
'كلمة المرور يجب أن تكون 12 محرفًا على الأقل.',
|
||||
);
|
||||
} else {
|
||||
Navigator.pop(dialogContext, (
|
||||
register,
|
||||
email,
|
||||
password,
|
||||
));
|
||||
}
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'كلمة المرور',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
if (register) ...[
|
||||
const SizedBox(height: 8),
|
||||
const Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
'استخدم 12 محرفًا على الأقل.',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (validationError.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
validationError,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
TextButton(
|
||||
onPressed:
|
||||
() => setDialogState(() {
|
||||
register = !register;
|
||||
validationError = '';
|
||||
}),
|
||||
child: Text(
|
||||
register
|
||||
? 'لديك حساب؟ سجّل الدخول'
|
||||
: 'ليس لديك حساب؟ أنشئ حسابًا',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: const Text('إلغاء'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final email = emailController.text.trim();
|
||||
final password = passwordController.text;
|
||||
if (email.isEmpty || password.isEmpty) {
|
||||
setDialogState(
|
||||
() =>
|
||||
validationError = 'أدخل البريد وكلمة المرور.',
|
||||
);
|
||||
} else if (register && password.length < 12) {
|
||||
setDialogState(
|
||||
() =>
|
||||
validationError =
|
||||
'كلمة المرور يجب أن تكون 12 محرفًا على الأقل.',
|
||||
);
|
||||
} else {
|
||||
Navigator.pop(dialogContext, (
|
||||
register,
|
||||
email,
|
||||
password,
|
||||
));
|
||||
}
|
||||
},
|
||||
child: Text(register ? 'إنشاء الحساب' : 'دخول'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
emailController.dispose();
|
||||
passwordController.dispose();
|
||||
if (credentials == null) return;
|
||||
|
||||
try {
|
||||
if (credentials.$1) {
|
||||
await api.registerAccount(
|
||||
email: credentials.$2,
|
||||
password: credentials.$3,
|
||||
);
|
||||
} else {
|
||||
await api.login(email: credentials.$2, password: credentials.$3);
|
||||
}
|
||||
await cubit.reloadForAccountChange();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
credentials.$1
|
||||
? 'تم إنشاء الحساب وتخزين الجلسة بأمان.'
|
||||
: 'تم تسجيل الدخول وتخزين الجلسة بأمان.',
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(error.toString().replaceFirst('Exception: ', '')),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _toggleRecording(VoiceState voice) async {
|
||||
final cubit = context.read<VoiceCubit>();
|
||||
if (voice.status == VoiceStatus.recording) {
|
||||
@@ -1081,6 +1308,18 @@ class _ChatPageState extends State<ChatPage> {
|
||||
),
|
||||
),
|
||||
SizedBox(width: compact ? 2 : 8),
|
||||
IconButton(
|
||||
onPressed: _manageAccount,
|
||||
tooltip:
|
||||
context.read<ApiRepository>().accountEmail == null
|
||||
? 'تسجيل الدخول أو إنشاء حساب'
|
||||
: 'الحساب: ${context.read<ApiRepository>().accountEmail} · تسجيل الخروج',
|
||||
color:
|
||||
context.read<ApiRepository>().accountEmail == null
|
||||
? _ink
|
||||
: _green,
|
||||
icon: const Icon(Icons.account_circle_outlined),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _settings,
|
||||
tooltip: 'إعداد الاتصال',
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||
#include <local_notifier/local_notifier_plugin.h>
|
||||
#include <record_linux/record_linux_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
@@ -15,6 +16,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) local_notifier_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "LocalNotifierPlugin");
|
||||
local_notifier_plugin_register_with_registrar(local_notifier_registrar);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
flutter_secure_storage_linux
|
||||
local_notifier
|
||||
record_linux
|
||||
url_launcher_linux
|
||||
|
||||
@@ -7,6 +7,7 @@ import Foundation
|
||||
|
||||
import file_selector_macos
|
||||
import flutter_local_notifications
|
||||
import flutter_secure_storage_darwin
|
||||
import local_notifier
|
||||
import path_provider_foundation
|
||||
import record_macos
|
||||
@@ -16,6 +17,7 @@ import url_launcher_macos
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
||||
LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
|
||||
|
||||
@@ -270,6 +270,54 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.0.0"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_secure_storage
|
||||
sha256: fe638107c5f69119156ada2db5a57734385fac3f64430bd7252a00d3ead2ca4b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.3.4"
|
||||
flutter_secure_storage_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_darwin
|
||||
sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.2"
|
||||
flutter_secure_storage_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_linux
|
||||
sha256: caa75bd78f017422912e3a904933113b2428eb79f981ac37acd2dd2a700458ea
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
flutter_secure_storage_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_platform_interface
|
||||
sha256: "64951127f001f546891c86f414972bf49ef2c4c8a4c92c65f5790cc0c9100045"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
flutter_secure_storage_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_web
|
||||
sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
flutter_secure_storage_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_windows
|
||||
sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
||||
@@ -46,6 +46,7 @@ dependencies:
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
flutter_secure_storage: 10.3.4
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <local_notifier/local_notifier_plugin.h>
|
||||
#include <record_windows/record_windows_plugin_c_api.h>
|
||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||
@@ -15,6 +16,8 @@
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
LocalNotifierPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("LocalNotifierPlugin"));
|
||||
RecordWindowsPluginCApiRegisterWithRegistrar(
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_windows
|
||||
flutter_secure_storage_windows
|
||||
local_notifier
|
||||
record_windows
|
||||
share_plus
|
||||
|
||||
Reference in New Issue
Block a user