diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index bf9ace9..d381cd6 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -7,9 +7,9 @@ import 'package:flux/core/layout/app_shell.dart'; import 'package:flux/core/routes/routes.dart'; import 'package:flux/core/widgets/image_upload/blocs/image_upload_cubit.dart'; import 'package:flux/core/widgets/image_upload/ui/image_upload_screen.dart'; -import 'package:flux/core/widgets/set_password_screen.dart'; import 'package:flux/core/widgets/image_upload/ui/upload_success_screen.dart'; import 'package:flux/features/auth/ui/auth_screen.dart'; +import 'package:flux/features/auth/ui/set_password_screen.dart'; import 'package:flux/features/company/bloc/company_settings_cubit.dart'; import 'package:flux/features/company/ui/company_settings_screen.dart'; import 'package:flux/features/customers/blocs/customer_form_cubit.dart'; diff --git a/lib/core/widgets/set_password_screen.dart b/lib/core/widgets/set_password_screen.dart deleted file mode 100644 index 0dc103d..0000000 --- a/lib/core/widgets/set_password_screen.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_cubit.dart'; -import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/core/widgets/flux_text_field.dart'; -import 'package:get_it/get_it.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; -import 'package:go_router/go_router.dart'; - -class SetPasswordScreen extends StatefulWidget { - const SetPasswordScreen({super.key}); - - @override - State createState() => _SetPasswordScreenState(); -} - -class _SetPasswordScreenState extends State { - final _passwordCtrl = TextEditingController(); - bool _isLoading = false; - - @override - void dispose() { - _passwordCtrl.dispose(); - super.dispose(); - } - - Future _savePassword() async { - final newPassword = _passwordCtrl.text.trim(); - if (newPassword.length < 6) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.setPasswordScreenAtLeast6Chars)), - ); - return; - } - - setState(() => _isLoading = true); - - try { - // 1. Aggiorniamo la password dell'utente (che Supabase ha già loggato grazie al link della mail) - await GetIt.I.get().auth.updateUser( - UserAttributes(password: newPassword), - ); - - // 2. Finito! Lo mandiamo alla home o facciamo ricaricare la sessione al SessionCubit - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.setPasswordScreenPasswordSetWelcome), - ), - ); - context.go('/'); // Rimandiamo al router principale - } - } on AuthException catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.authError(e.message))), - ); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.commonError(e.toString()))), - ); - } - } finally { - if (mounted) setState(() => _isLoading = false); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(context.l10n.setPasswordScreenWelcomeInFlux), - automaticallyImplyLeading: - false, // Non può tornare indietro, deve mettere la password! - actions: [ - IconButton.filled( - onPressed: () => context.read().signOut(), - icon: Icon(Icons.logout), - ), - ], - ), - body: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Icon(Icons.lock_reset, size: 80, color: Colors.blueAccent), - const SizedBox(height: 24), - Text( - context.l10n.setPasswordScreenSetPassword, - textAlign: TextAlign.center, - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Text( - context.l10n.setPasswordInviteAcceptedChoosePassword, - textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey), - ), - const SizedBox(height: 32), - FluxTextField( - controller: _passwordCtrl, - label: context.l10n.commonNewPassword, - icon: Icons.lock, - isPassword: true, - ), - const SizedBox(height: 32), - ElevatedButton( - onPressed: _isLoading ? null : _savePassword, - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - ), - child: _isLoading - ? const CircularProgressIndicator(color: Colors.white) - : Text( - context.l10n.setPasswordScreenSaveAndStart, - style: TextStyle(fontSize: 16), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/features/auth/ui/set_password_screen.dart b/lib/features/auth/ui/set_password_screen.dart new file mode 100644 index 0000000..df24a20 --- /dev/null +++ b/lib/features/auth/ui/set_password_screen.dart @@ -0,0 +1,207 @@ +import 'package:flutter/material.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +class SetPasswordScreen extends StatefulWidget { + const SetPasswordScreen({super.key}); + + @override + State createState() => _SetPasswordScreenState(); +} + +class _SetPasswordScreenState extends State { + final _formKey = GlobalKey(); + final _passwordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); + bool _isLoading = false; + bool _obscurePassword = true; + String? _errorMessage; + + @override + void dispose() { + _passwordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + Future _submitNewPassword() async { + if (!_formKey.currentState!.validate()) return; + + setState(() { + _isLoading = true; + _errorMessage = null; + }); + + try { + // 🥷 LA MOFFA DEL NINJA: Aggiorniamo la password dell'utente corrente + // che si è loggato in automatico grazie al token nell'URL + await Supabase.instance.client.auth.updateUser( + UserAttributes(password: _passwordController.text.trim()), + ); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Password impostata con successo! Benvenuto in FLUX.', + ), + backgroundColor: Colors.green, + ), + ); + + // Non serve forzare i redirect qui! + // Il SessionCubit nel costruttore ha un listener su 'onAuthStateChange'. + // Sentirà che l'utente ora è perfettamente valido, richiamerà 'initializeSession()' + // e lo scaricherà automaticamente nella pagina di Onboarding corretta. + } + } on AuthException catch (e) { + setState(() { + _errorMessage = e.message; + }); + } catch (e) { + setState(() { + _errorMessage = "Si è verificato un errore imprevisto. Riprova."; + }); + } finally { + if (mounted) { + setState(() => _isLoading = false); + } + } + } + + @override + Widget build(BuildContext context) { + // Rendiamo la schermata responsive ed elegante per il Web (Cloudflare) + final size = MediaQuery.of(context).size; + final isWebContainer = size.width > 600; + + return Scaffold( + body: Center( + child: SingleChildScrollView( + child: Container( + width: isWebContainer ? 450 : size.width, + padding: const EdgeInsets.all(32.0), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Logo o Brand FLUX + Text( + 'FLUX', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + letterSpacing: -1, + ), + ), + const SizedBox(height: 8), + Text( + 'Configura la tua chiave di accesso per iniziare a collaborare.', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey[600], fontSize: 14), + ), + const SizedBox(height: 32), + + if (_errorMessage != null) ...[ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + _errorMessage!, + style: const TextStyle(color: Colors.red, fontSize: 13), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 16), + ], + + // Campo Password + TextFormField( + controller: _passwordController, + obscureText: _obscurePassword, + decoration: InputDecoration( + labelText: 'Nuova Password', + prefixIcon: const Icon(Icons.lock_outline), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility_off + : Icons.visibility, + ), + onPressed: () => setState( + () => _obscurePassword = !_obscurePassword, + ), + ), + border: const OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Inserisci una password'; + } + if (value.length < 6) { + return 'La password deve avere almeno 6 caratteri'; + } + return null; + }, + ), + const SizedBox(height: 16), + + // Campo Conferma Password + TextFormField( + controller: _confirmPasswordController, + obscureText: _obscurePassword, + decoration: const InputDecoration( + labelText: 'Conferma Password', + prefixIcon: Icon(Icons.lock_reset_rounded), + border: OutlineInputBorder(), + ), + validator: (value) { + if (value != _passwordController.text) { + return 'Le password non coincidono'; + } + return null; + }, + ), + const SizedBox(height: 24), + + // Bottone di Invio + ElevatedButton( + onPressed: _isLoading ? null : _submitNewPassword, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + backgroundColor: Colors.black, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: _isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.white, + ), + ), + ) + : const Text( + 'Conferma e Accedi', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/master_data/staff/blocs/staff_cubit.dart b/lib/features/master_data/staff/blocs/staff_cubit.dart index 6681415..9de3514 100644 --- a/lib/features/master_data/staff/blocs/staff_cubit.dart +++ b/lib/features/master_data/staff/blocs/staff_cubit.dart @@ -117,9 +117,9 @@ class StaffCubit extends Cubit { } } - Future resetPasswordOrResendInviteLink(String email) async { + Future resetPassword(String email) async { try { - await _repository.resetPasswordOrResendInviteLink(email); + await _repository.resetPassword(email); emit(state.copyWith(status: StaffStatus.emailSent, error: null)); } catch (e) { emit(state.copyWith(status: StaffStatus.error, error: e.toString())); diff --git a/lib/features/master_data/staff/data/staff_repository.dart b/lib/features/master_data/staff/data/staff_repository.dart index 006e8a2..b495d3f 100644 --- a/lib/features/master_data/staff/data/staff_repository.dart +++ b/lib/features/master_data/staff/data/staff_repository.dart @@ -104,7 +104,8 @@ class StaffRepository { } } - Future resetPasswordOrResendInviteLink(String email) async { + Future resetPassword(String email) async { + //TODO modificare per fare il reset della password tramite edge function, così da poter personalizzare l'email e il link di reset try { await _supabase.auth.resetPasswordForEmail( email, diff --git a/lib/features/master_data/staff/ui/staff_screen.dart b/lib/features/master_data/staff/ui/staff_screen.dart index aecd9c4..886450e 100644 --- a/lib/features/master_data/staff/ui/staff_screen.dart +++ b/lib/features/master_data/staff/ui/staff_screen.dart @@ -190,16 +190,30 @@ class _StaffScreenState extends State { icon: const Icon(Icons.more_vert), onSelected: (value) { if (value == 'invite_reset') { - context.read().resetPasswordOrResendInviteLink( - member.email!, - ); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Operazione richiesta, controlla l\'email!', + if (!member.hasJoined) { + context.read().inviteStaffMember( + member: member, + selectedStoreIds: + member.assignedStores?.map((s) => s.id!).toList() ?? + [], + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Invito reinviato, controlla l\'email!', + ), ), - ), - ); + ); + } else { + context.read().resetPassword(member.email!); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Operazione richiesta, controlla l\'email!', + ), + ), + ); + } } }, itemBuilder: (context) => [ diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index 9f93d17..8b92348 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -2,8 +2,6 @@ - com.apple.developer.aps-environment - development com.apple.security.app-sandbox com.apple.security.cs.allow-jit diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index 7b18ed8..87f17a6 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -2,8 +2,6 @@ - com.apple.developer.aps-environment - development com.apple.security.app-sandbox com.apple.security.cs.allow-jit diff --git a/pubspec.yaml b/pubspec.yaml index 303b24f..497794c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flux description: "Gestione attività negozio di telefonia" publish_to: 'none' -version: 1.1.8+26 +version: 1.1.9+27 environment: sdk: ^3.11.3