8000 feat(app): first-run onboarding flow by shroominic · Pull Request #9 · shroominic/OpenGlucose · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 95 additions & 7 deletions openhealth/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@ import 'package:openglucose/src/app_controller.dart';
import 'package:openglucose/src/dashboard_chart.dart';
import 'package:openglucose/src/display_preferences.dart';
import 'package:openglucose/src/driver_factory.dart';
import 'package:openglucose/src/onboarding/onboarding_flow.dart';
import 'package:openglucose/src/onboarding/onboarding_store.dart';
import 'package:openglucose/src/session_presentation.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:shared_preferences/shared_preferences.dart';

/// When built with `--dart-define=OG_DEMO=true`, the app runs the in-memory
/// demo driver (see [buildDefaultDriver]) and auto-connects on launch so it
/// lands directly on the populated dashboard — used for simulator/feature
/// verification. Defaults to false; production builds are unaffected.
const bool kOgDemo = bool.fromEnvironment('OG_DEMO', defaultValue: false);

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();

Expand All @@ -28,14 +36,41 @@ Future<void> main() async {
runApp(const _BootstrapApp());
}

Future<CgmAppController> _bootstrap() async {
Future<_BootstrapResult> _bootstrap() async {
final preferences = await SharedPreferences.getInstance();
final controller = CgmAppController(
preferences: preferences,
driver: buildDefaultDriver(),
);
await controller.initialize();
return controller;
if (kOgDemo) {
// In demo mode (simulator/feature verification) the demo driver only
// surfaces its sensor after a scan, so auto-scan and auto-connect to land
// directly on the populated dashboard. Strictly gated behind OG_DEMO and
// skipped when a previous session was already restored from preferences.
unawaited(_autoConnectDemoSensor(controller));
}
return (controller: controller, preferences: preferences);
}

typedef _BootstrapResult = ({
CgmAppController controller,
SharedPreferences preferences,
});

/// Scans with the demo driver and connects to the first discovered sensor so
/// OG_DEMO builds open straight onto the populated dashboard.
Future<void> _autoConnectDemoSensor(CgmAppController controller) async {
if (controller.snapshot != null) {
// A persisted session is already (re)connecting; don't interfere.
return;
}
await controller.scan();
final sensors = controller.sensors;
if (sensors.isEmpty) {
return;
}
await controller.connect(sensors.first);
}

class _BootstrapApp extends StatefulWidget {
Expand All @@ -46,11 +81,11 @@ class _BootstrapApp extends StatefulWidget {
}

class _BootstrapAppState extends State<_BootstrapApp> {
late final Future<CgmAppController> _future = _bootstrap();
late final Future<_BootstrapResult> _future = _bootstrap();

@override
Widget build(BuildContext context) {
return FutureBuilder<CgmAppController>(
return FutureBuilder<_BootstrapResult>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
Expand All @@ -59,7 +94,11 @@ class _BootstrapAppState extends State<_BootstrapApp> {
if (snapshot.hasError) {
return _SplashApp(error: snapshot.error);
}
return OpenGlucoseApp(controller: snapshot.data!);
final result = snapshot.data!;
return OpenGlucoseApp(
controller: result.controller,
preferences: result.preferences,
);
},
);
}
Expand Down Expand Up @@ -134,9 +173,14 @@ class _SpinningLogoState extends State<_SpinningLogo>
}

class OpenGlucoseApp extends StatelessWidget {
const OpenGlucoseApp({super.key, required this.controller});
const OpenGlucoseApp({
super.key,
required this.controller,
required this.preferences,
});

final CgmAppController controller;
final SharedPreferences preferences;

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -177,10 +221,54 @@ class OpenGlucoseApp extends StatelessWidget {
),
),
),
home: CgmHomePage(controller: controller),
// --- TASK-007 onboarding gate ---
// First-run only: show the skippable onboarding flow, then hand off to
// the existing scan/connect home. Persisted via OnboardingStore; once
// completed/skipped the gate falls straight through on later launches.
home: _OnboardingGate(
store: OnboardingStore(preferences),
unit: controller.displayPreferences.unit,
home: CgmHomePage(controller: controller),
),
// --- end TASK-007 onboarding gate ---
);
}
}

// --- TASK-007 onboarding gate ---
/// Chooses between first-run onboarding and the main app. Kept deliberately
/// small and self-contained so it merges cleanly with other `main.dart` edits.
class _OnboardingGate extends StatefulWidget {
const _OnboardingGate({
required this.store,
required this.unit,
required this.home,
});

final OnboardingStore store;
final GlucoseUnit unit;
final Widget home;

@override
State<_OnboardingGate> createState() => _OnboardingGateState();
}

class _OnboardingGateState extends State<_OnboardingGate> {
late bool _showOnboarding = !widget.store.isCompleted;

@override
Widget build(BuildContext context) {
if (!_showOnboarding) {
return widget.home;
}
return OnboardingFlow(
store: widget.store,
unit: widget.unit,
onFinished: () => setState(() => _showOnboarding = false),
);
}
}
// --- end TASK-007 onboarding gate ---

class CgmHomePage extends StatefulWidget {
const CgmHomePage({super.key, required this.controller});
Expand Down
11 changes: 11 additions & 0 deletions openhealth/lib/src/driver_factory_io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ import 'package:cgm_aidex/cgm_aidex.dart';
import 'package:cgm_ble_flutter/cgm_ble_flutter.dart';
import 'package:cgm_core/cgm_core.dart';

import 'demo_driver.dart';

/// When built with `--dart-define=OG_DEMO=true`, native/simulator builds use the
/// in-memory [DemoCgmDriver] instead of the real BLE driver, so the app can be
/// exercised in the iOS simulator (which has no Bluetooth). Defaults to false,
/// so production builds are unchanged and keep using the real Aidex driver.
const bool kOgDemo = bool.fromEnvironment('OG_DEMO', defaultValue: false);

CgmDriver buildPlatformDriver() {
if (kOgDemo) {
return DemoCgmDriver();
}
return AidexSensorDriver(const FlutterBluePlusTransport());
}
Loading
0