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 { const BleProvisionSheet({super.key}); @override ConsumerState createState() => _BleProvisionSheetState(); } class _BleProvisionSheetState extends ConsumerState { final _provisioner = BleProvisioner(); final _ssidController = TextEditingController(); final _wifiPasswordController = TextEditingController(); _Step _step = _Step.provision; BleScanResult? _selected; bool _provisioning = false; String? _error; @override void dispose() { _provisioner.dispose(); _ssidController.dispose(); _wifiPasswordController.dispose(); super.dispose(); } Future _provision() async { if (_selected == null) return; setState(() { _provisioning = true; _error = null; }); try { await _provisioner.provision( _selected!.deviceId, ssid: _ssidController.text.trim(), wifiPassword: _wifiPasswordController.text, ); if (mounted) setState(() => _step = _Step.done); } catch (e) { 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( expand: false, initialChildSize: 0.6, maxChildSize: 0.9, builder: (context, scrollController) => Padding( padding: EdgeInsets.fromLTRB( 24, 16, 24, MediaQuery.of(context).viewInsets.bottom + 24, ), 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, ), ), ); } } 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'), ), ], ); } }