diff --git a/lib/components/pages/settings.dart b/lib/components/pages/settings.dart index 8debed7..30d70c7 100644 --- a/lib/components/pages/settings.dart +++ b/lib/components/pages/settings.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:http/http.dart' as http; +import 'package:mileograph_flutter/services/api_service.dart'; import 'package:mileograph_flutter/services/endpoint_service.dart'; import 'package:mileograph_flutter/services/data_service.dart'; import 'package:provider/provider.dart'; @@ -17,16 +18,27 @@ class SettingsPage extends StatefulWidget { class _SettingsPageState extends State { late final TextEditingController _endpointController; bool _saving = false; + bool _changingPassword = false; + final _passwordFormKey = GlobalKey(); + late final TextEditingController _currentPasswordController; + late final TextEditingController _newPasswordController; + late final TextEditingController _confirmPasswordController; @override void initState() { super.initState(); final endpoint = context.read().baseUrl; _endpointController = TextEditingController(text: endpoint); + _currentPasswordController = TextEditingController(); + _newPasswordController = TextEditingController(); + _confirmPasswordController = TextEditingController(); } @override void dispose() { + _currentPasswordController.dispose(); + _newPasswordController.dispose(); + _confirmPasswordController.dispose(); _endpointController.dispose(); super.dispose(); } @@ -125,6 +137,40 @@ class _SettingsPageState extends State { } } + Future _changePassword() async { + final messenger = ScaffoldMessenger.of(context); + final formState = _passwordFormKey.currentState; + if (formState == null || !formState.validate()) return; + + FocusScope.of(context).unfocus(); + setState(() => _changingPassword = true); + try { + final api = context.read(); + await api.post('/user/password/change', { + 'old_password': _currentPasswordController.text, + 'new_password': _newPasswordController.text, + }); + if (!mounted) return; + messenger.showSnackBar( + const SnackBar(content: Text('Password updated successfully.')), + ); + formState.reset(); + _currentPasswordController.clear(); + _newPasswordController.clear(); + _confirmPasswordController.clear(); + } catch (e) { + if (mounted) { + messenger.showSnackBar( + SnackBar(content: Text('Failed to change password: $e')), + ); + } + } finally { + if (mounted) { + setState(() => _changingPassword = false); + } + } + } + @override Widget build(BuildContext context) { final endpointService = context.watch(); @@ -149,7 +195,7 @@ class _SettingsPageState extends State { }, ), ), - body: Padding( + body: SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -205,6 +251,97 @@ class _SettingsPageState extends State { 'Current: ${endpointService.baseUrl}', style: Theme.of(context).textTheme.labelSmall, ), + const SizedBox(height: 32), + Text( + 'Account', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 8), + Text( + 'Change your password for this account.', + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 12), + Form( + key: _passwordFormKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + controller: _currentPasswordController, + decoration: const InputDecoration( + labelText: 'Current password', + border: OutlineInputBorder(), + ), + obscureText: true, + enableSuggestions: false, + autocorrect: false, + autofillHints: const [AutofillHints.password], + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your current password.'; + } + return null; + }, + ), + const SizedBox(height: 12), + TextFormField( + controller: _newPasswordController, + decoration: const InputDecoration( + labelText: 'New password', + border: OutlineInputBorder(), + ), + obscureText: true, + enableSuggestions: false, + autocorrect: false, + autofillHints: const [AutofillHints.newPassword], + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a new password.'; + } + return null; + }, + ), + const SizedBox(height: 12), + TextFormField( + controller: _confirmPasswordController, + decoration: const InputDecoration( + labelText: 'Confirm new password', + border: OutlineInputBorder(), + ), + obscureText: true, + enableSuggestions: false, + autocorrect: false, + autofillHints: const [AutofillHints.newPassword], + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please confirm the new password.'; + } + if (value != _newPasswordController.text) { + return 'New passwords do not match.'; + } + return null; + }, + ), + const SizedBox(height: 16), + FilledButton.icon( + onPressed: _changingPassword ? null : _changePassword, + icon: _changingPassword + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.lock_reset), + label: Text( + _changingPassword ? 'Updating...' : 'Change password', + ), + ), + ], + ), + ), ], ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 7211412..6dff7b9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.4.1+1 +version: 0.4.2+1 environment: sdk: ^3.8.1