diff --git a/lib/features/ble_provision/ble_provision_sheet.dart b/lib/features/ble_provision/ble_provision_sheet.dart index 8ff3500..cf40282 100644 --- a/lib/features/ble_provision/ble_provision_sheet.dart +++ b/lib/features/ble_provision/ble_provision_sheet.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import '../../data/sources/ble/ble_provisioner.dart'; +enum _Step { provision, done } + // Shared bottom sheet used by onboarding and the main sensor screens. // Flow: scan → select device → enter WiFi credentials → provision → place on map. class BleProvisionSheet extends ConsumerStatefulWidget { @@ -17,6 +20,7 @@ class _BleProvisionSheetState extends ConsumerState { final _ssidController = TextEditingController(); final _wifiPasswordController = TextEditingController(); + _Step _step = _Step.provision; BleScanResult? _selected; bool _provisioning = false; String? _error; @@ -41,15 +45,24 @@ class _BleProvisionSheetState extends ConsumerState { ssid: _ssidController.text.trim(), wifiPassword: _wifiPasswordController.text, ); - // TODO: poll localiserd until sensor appears, then prompt placement on map. - if (mounted) Navigator.of(context).pop(); + if (mounted) setState(() => _step = _Step.done); } catch (e) { - setState(() => _error = e.toString()); + if (mounted) setState(() => _error = e.toString()); } finally { if (mounted) setState(() => _provisioning = false); } } + void _reset() { + setState(() { + _step = _Step.provision; + _selected = null; + _error = null; + _ssidController.clear(); + _wifiPasswordController.clear(); + }); + } + @override Widget build(BuildContext context) { return DraggableScrollableSheet( @@ -63,62 +76,163 @@ class _BleProvisionSheetState extends ConsumerState { 24, MediaQuery.of(context).viewInsets.bottom + 24, ), - child: ListView( - controller: scrollController, - children: [ - Text('Add sensor', - style: Theme.of(context).textTheme.titleLarge), - const SizedBox(height: 16), - - // Scan results - // TODO: StreamBuilder on _provisioner.scan() — show a list of - // BleScanResult tiles; tapping one sets _selected. - const Text('Nearby ESP32 devices'), - const SizedBox(height: 8), - const Placeholder(fallbackHeight: 120), - const SizedBox(height: 24), - - if (_selected != null) ...[ - Text('Selected: ${_selected!.name}'), - const SizedBox(height: 16), - TextField( - controller: _ssidController, - decoration: const InputDecoration( - labelText: 'WiFi SSID', - border: OutlineInputBorder(), - ), + child: _step == _Step.done + ? _DoneStep( + onGoToFloorPlan: () { + final router = GoRouter.of(context); + Navigator.of(context).pop(); + router.go('/floorplan'); + }, + onAddAnother: _reset, + onDone: () => Navigator.of(context).pop(), + ) + : _ProvisionStep( + scrollController: scrollController, + provisioner: _provisioner, + selected: _selected, + onSelected: (r) => setState(() => _selected = r), + ssidController: _ssidController, + wifiPasswordController: _wifiPasswordController, + provisioning: _provisioning, + error: _error, + onProvision: _provision, ), - const SizedBox(height: 12), - TextField( - controller: _wifiPasswordController, - decoration: const InputDecoration( - labelText: 'WiFi password', - border: OutlineInputBorder(), - ), - obscureText: true, - ), - const SizedBox(height: 16), - ], - - if (_error != null) - Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Text(_error!, - style: TextStyle( - color: Theme.of(context).colorScheme.error)), - ), - - FilledButton( - onPressed: (_selected == null || _provisioning) ? null : _provision, - child: _provisioning - ? const SizedBox.square( - dimension: 20, - child: CircularProgressIndicator(strokeWidth: 2)) - : const Text('Provision & add'), - ), - ], - ), ), ); } } + +class _ProvisionStep extends StatelessWidget { + const _ProvisionStep({ + required this.scrollController, + required this.provisioner, + required this.selected, + required this.onSelected, + required this.ssidController, + required this.wifiPasswordController, + required this.provisioning, + required this.error, + required this.onProvision, + }); + + final ScrollController scrollController; + final BleProvisioner provisioner; + final BleScanResult? selected; + final ValueChanged onSelected; + final TextEditingController ssidController; + final TextEditingController wifiPasswordController; + final bool provisioning; + final String? error; + final VoidCallback onProvision; + + @override + Widget build(BuildContext context) { + return ListView( + controller: scrollController, + children: [ + Text('Add sensor', style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 16), + + // Scan results + // TODO: StreamBuilder on provisioner.scan() — show a list of + // BleScanResult tiles; tapping one calls onSelected. + const Text('Nearby ESP32 devices'), + const SizedBox(height: 8), + const Placeholder(fallbackHeight: 120), + const SizedBox(height: 24), + + if (selected != null) ...[ + Text('Selected: ${selected!.name}'), + const SizedBox(height: 16), + TextField( + controller: ssidController, + decoration: const InputDecoration( + labelText: 'WiFi SSID', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextField( + controller: wifiPasswordController, + decoration: const InputDecoration( + labelText: 'WiFi password', + border: OutlineInputBorder(), + ), + obscureText: true, + ), + const SizedBox(height: 16), + ], + + if (error != null) + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Text(error!, + style: + TextStyle(color: Theme.of(context).colorScheme.error)), + ), + + FilledButton( + onPressed: (selected == null || provisioning) ? null : onProvision, + child: provisioning + ? const SizedBox.square( + dimension: 20, + child: CircularProgressIndicator(strokeWidth: 2)) + : const Text('Provision & add'), + ), + ], + ); + } +} + +class _DoneStep extends StatelessWidget { + const _DoneStep({ + required this.onGoToFloorPlan, + required this.onAddAnother, + required this.onDone, + }); + + final VoidCallback onGoToFloorPlan; + final VoidCallback onAddAnother; + final VoidCallback onDone; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Icon(Icons.check_circle_outline, size: 56), + const SizedBox(height: 16), + Text( + 'Sensor provisioned!', + style: Theme.of(context).textTheme.titleLarge, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'The sensor will appear in the list once it connects to the network. ' + 'Open the floor plan to place it.', + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + FilledButton.icon( + icon: const Icon(Icons.map_outlined), + label: const Text('Place on floor plan'), + onPressed: onGoToFloorPlan, + ), + const SizedBox(height: 12), + OutlinedButton.icon( + icon: const Icon(Icons.add), + label: const Text('Add another sensor'), + onPressed: onAddAnother, + ), + const SizedBox(height: 8), + TextButton( + onPressed: onDone, + child: const Text('Done'), + ), + ], + ); + } +}