Skip to content

Commit 0709ea4

Browse files
authored
Feature/onboarding (#75)
closes #57, closes #47 This pull request includes multiple changes to the Android app and Flutter codebase, focusing on improving health data permissions, onboarding flow, and theme management. The most important changes include updating permissions in the Android manifest, enhancing the authorization wrapper, adding onboarding providers and routes, updating theme management, and extending localization for onboarding and permissions. ### Permissions Updates: * [`android/app/src/main/AndroidManifest.xml`](diffhunk://#diff-63272c98a6330850888e633b43ba4c9decee6431a115e0d2731564838eaa1d1dL16-R23): Removed `READ_SPEED` and `WRITE_SPEED` permissions and added `MANAGE_HEALTH_DATA` permission. Added an intent filter for requesting historical health data permissions. [[1]](diffhunk://#diff-63272c98a6330850888e633b43ba4c9decee6431a115e0d2731564838eaa1d1dL16-R23) [[2]](diffhunk://#diff-63272c98a6330850888e633b43ba4c9decee6431a115e0d2731564838eaa1d1dR55-R59) ### Authorization Wrapper Enhancements: * [`lib/core/authorizationWrapper.dart`](diffhunk://#diff-7a413f9bfaab163aa36bfbe36da5029ef3bcb07ea2000a8907b2b55b1c40e2e4R3-R5): Integrated onboarding status check and enhanced UI for different authorization states, including error handling and permission requests. [[1]](diffhunk://#diff-7a413f9bfaab163aa36bfbe36da5029ef3bcb07ea2000a8907b2b55b1c40e2e4R3-R5) [[2]](diffhunk://#diff-7a413f9bfaab163aa36bfbe36da5029ef3bcb07ea2000a8907b2b55b1c40e2e4R17-R135) ### Onboarding Flow: * [`lib/core/navigation_host.dart`](diffhunk://#diff-02bf8d0c46b3059f94aa10aabbc55a5ce32a145dd7524f49b635fe42304a96deR3-R7): Added onboarding routes and providers to manage the onboarding flow and ensure the initial location is set based on onboarding completion status. [[1]](diffhunk://#diff-02bf8d0c46b3059f94aa10aabbc55a5ce32a145dd7524f49b635fe42304a96deR3-R7) [[2]](diffhunk://#diff-02bf8d0c46b3059f94aa10aabbc55a5ce32a145dd7524f49b635fe42304a96deL13-R36) [[3]](diffhunk://#diff-02bf8d0c46b3059f94aa10aabbc55a5ce32a145dd7524f49b635fe42304a96deR66) ### Theme Management: * [`lib/data/repositories/profile_repository_impl.dart`](diffhunk://#diff-f54b2ecf8ed7418352d6d20b69beb298f64b8d7fd9615633ee136d3a31f72741R23-R24): Changed theme management from a simple dark mode boolean to an `AppThemeMode` enum, allowing for system, light, and dark modes. [[1]](diffhunk://#diff-f54b2ecf8ed7418352d6d20b69beb298f64b8d7fd9615633ee136d3a31f72741R23-R24) [[2]](diffhunk://#diff-f54b2ecf8ed7418352d6d20b69beb298f64b8d7fd9615633ee136d3a31f72741L46-R62) * [`lib/domain/repositories/profile_repository.dart`](diffhunk://#diff-536199512adad702fb94653f50545728e0e1c873de8c84c10e6dfa77b4c54812L2-R13): Updated the profile repository to handle the new `AppThemeMode` enum. [[1]](diffhunk://#diff-536199512adad702fb94653f50545728e0e1c873de8c84c10e6dfa77b4c54812L2-R13) [[2]](diffhunk://#diff-536199512adad702fb94653f50545728e0e1c873de8c84c10e6dfa77b4c54812L18-R26) ### Localization: * `lib/l10n/app_de.arb` and `lib/l10n/app_en.arb`: Added and updated localization strings for onboarding, permissions, and theme mode. [[1]](diffhunk://#diff-36252c65ab82cbff4774b4983cb9027a2bef4cb738d5ea656c0b903939b3871aL6-R41) [[2]](diffhunk://#diff-36252c65ab82cbff4774b4983cb9027a2bef4cb738d5ea656c0b903939b3871aL272-R331) [[3]](diffhunk://#diff-9796fde3771f42a3a759ccc941731d83f96037a661e47dde27ce81d3447a69c2R41-R44)
2 parents 8fd67ce + 25eb87f commit 0709ea4

30 files changed

Lines changed: 2446 additions & 368 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@ app.*.map.json
4545
/android/app/debug
4646
/android/app/profile
4747
/android/app/release
48+
/android/app/.cxx

android/app/src/main/AndroidManifest.xml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,14 @@
1313
<uses-permission android:name="android.permission.health.READ_EXERCISE" />
1414
<uses-permission android:name="android.permission.health.WRITE_EXERCISE" />
1515
<uses-permission android:name="android.permission.health.READ_HEART_RATE" />
16-
<uses-permission android:name="android.permission.health.READ_SPEED" />
17-
<uses-permission android:name="android.permission.health.WRITE_SPEED" />
1816
<uses-permission android:name="android.permission.health.READ_DISTANCE" />
1917
<uses-permission android:name="android.permission.health.WRITE_DISTANCE" />
2018
<uses-permission android:name="androidx.health.permission.Distance.READ" />
2119
<uses-permission android:name="androidx.health.permission.Distance.WRITE" />
2220
<uses-permission android:name="android.permission.health.READ_TOTAL_CALORIES_BURNED" />
2321
<uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
2422
<uses-permission android:name="android.permission.health.READ_HEALTH_DATA_HISTORY" />
23+
<uses-permission android:name="android.permission.MANAGE_HEALTH_DATA" />
2524

2625
<application
2726
android:name="${applicationName}"
@@ -53,6 +52,11 @@
5352
<intent-filter>
5453
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
5554
</intent-filter>
55+
<!-- Intention to request historical health data permissions -->
56+
<intent-filter>
57+
<action android:name="android.health.action.REQUEST_HEALTH_PERMISSIONS" />
58+
<category android:name="android.intent.category.DEFAULT" />
59+
</intent-filter>
5660
</activity>
5761
<activity-alias
5862
android:name="ViewPermissionUsageActivity"

lib/core/authorizationWrapper.dart

Lines changed: 0 additions & 28 deletions
This file was deleted.

lib/core/health_authorized_view_model.dart

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'dart:async';
22

3-
import 'package:flutter/material.dart';
43
import 'package:health/health.dart';
4+
import 'package:logging/logging.dart';
55
import 'package:riverpod/riverpod.dart';
66

77
final healthViewModelProvider =
@@ -12,38 +12,90 @@ final healthViewModelProvider =
1212
class HealthAuthViewModel extends StateNotifier<HealthAuthViewModelState> {
1313
final Ref ref;
1414
final _health = Health();
15-
final neededPermissions = [
15+
final log = Logger('HealthAuthViewModel');
16+
17+
final requiredReadPermissions = [
1618
HealthDataType.STEPS,
1719
HealthDataType.DISTANCE_DELTA,
1820
HealthDataType.SLEEP_ASLEEP,
1921
HealthDataType.WORKOUT,
20-
HealthDataType.HEART_RATE
22+
HealthDataType.HEART_RATE,
23+
HealthDataType.ACTIVE_ENERGY_BURNED,
24+
HealthDataType.TOTAL_CALORIES_BURNED
25+
];
26+
27+
final optionalWritePermissions = [
28+
HealthDataType.STEPS,
29+
HealthDataType.DISTANCE_DELTA,
30+
HealthDataType.WORKOUT
2131
];
2232

2333
HealthAuthViewModel(this.ref)
2434
: super(HealthAuthViewModelState.notAuthorized) {
25-
_health.configure();
35+
try {
36+
_health.configure();
37+
} catch (e) {
38+
log.severe('Failed to configure health package: $e');
39+
}
2640
}
2741

28-
Future<void> authorize() async {
29-
bool? hasPermissions = await _health.hasPermissions(neededPermissions);
30-
if (hasPermissions == null || !hasPermissions) {
31-
try {
32-
bool authorized = await _health.requestAuthorization(neededPermissions,
33-
permissions: [HealthDataAccess.READ_WRITE]);
34-
if (authorized) {
35-
state = HealthAuthViewModelState.authorized;
36-
} else {
37-
state = HealthAuthViewModelState.authorizationNotGranted;
42+
/// Request REQUIRED READ-Permissions
43+
Future<void> authorizeReadAccess() async {
44+
try {
45+
bool? hasPermissions =
46+
await _health.hasPermissions(requiredReadPermissions);
47+
48+
if (hasPermissions == null || !hasPermissions) {
49+
try {
50+
List<HealthDataAccess> accessPermissions = List.filled(
51+
requiredReadPermissions.length, HealthDataAccess.READ);
52+
53+
bool authorized = await _health.requestAuthorization(
54+
requiredReadPermissions,
55+
permissions: accessPermissions);
56+
57+
if (authorized) {
58+
state = HealthAuthViewModelState.authorized;
59+
} else {
60+
state = HealthAuthViewModelState.authorizationNotGranted;
61+
}
62+
} catch (error) {
63+
log.severe('Health permission authorization error: $error');
64+
state = HealthAuthViewModelState.error;
3865
}
39-
} catch (error) {
40-
debugPrint("Exception in authorize: $error");
41-
state = HealthAuthViewModelState.error;
66+
} else {
67+
state = HealthAuthViewModelState.authorized;
4268
}
43-
} else {
44-
state = HealthAuthViewModelState.authorized;
69+
} catch (error) {
70+
log.severe('Health permission error: $error');
71+
state = HealthAuthViewModelState.error;
4572
}
4673
}
74+
75+
/// Request OPTIONAL WRITE-Permissions
76+
/// You can only request WRITE when READ is already granted
77+
Future<bool> authorizeWriteAccess() async {
78+
if (state != HealthAuthViewModelState.authorized) {
79+
return false;
80+
}
81+
82+
try {
83+
List<HealthDataAccess> accessPermissions =
84+
List.filled(optionalWritePermissions.length, HealthDataAccess.WRITE);
85+
86+
bool authorized = await _health.requestAuthorization(
87+
optionalWritePermissions,
88+
permissions: accessPermissions);
89+
return authorized;
90+
} catch (error) {
91+
log.severe('Health write permission error: $error');
92+
return false;
93+
}
94+
}
95+
96+
Future<void> authorize() async {
97+
await authorizeReadAccess();
98+
}
4799
}
48100

49101
enum HealthAuthViewModelState {

lib/core/navigation_host.dart

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import 'package:flutter/material.dart';
22
import 'package:go_router/go_router.dart';
3+
import 'package:hooks_riverpod/hooks_riverpod.dart';
34
import 'package:movetopia/presentation/common/navigator.dart';
5+
import 'package:movetopia/presentation/onboarding/providers/onboarding_provider.dart';
6+
import 'package:movetopia/presentation/onboarding/routes.dart';
7+
import 'package:movetopia/presentation/onboarding/screen/authorization_problem_screen.dart';
8+
import 'package:movetopia/presentation/onboarding/screen/onboarding_screen.dart';
49
import 'package:movetopia/presentation/tracking/routes.dart';
510

611
import '../presentation/activities/routes.dart';
@@ -10,36 +15,59 @@ import '../presentation/today/routes.dart';
1015

1116
final _rootNavigatorKey = GlobalKey<NavigatorState>();
1217

13-
final navigationRoutes = GoRouter(
14-
navigatorKey: _rootNavigatorKey,
15-
initialLocation: todayPath,
16-
routes: <RouteBase>[
17-
StatefulShellRoute.indexedStack(
18-
builder: (BuildContext context, GoRouterState state,
19-
StatefulNavigationShell navigationShell) {
20-
return MoveTopiaNavigator(navigationShell: navigationShell);
21-
},
22-
branches: <StatefulShellBranch>[
23-
StatefulShellBranch(
24-
navigatorKey: TodayRoutes.navigatorKey,
25-
routes: TodayRoutes.routes,
26-
),
27-
StatefulShellBranch(
28-
navigatorKey: ActivitiesRoutes.navigatorKey,
29-
routes: ActivitiesRoutes.routes,
30-
),
31-
StatefulShellBranch(
32-
navigatorKey: ChallengesRoutes.navigatorKey,
33-
routes: ChallengesRoutes.routes,
34-
),
35-
StatefulShellBranch(
36-
navigatorKey: ProfileRoutes.navigatorKey,
37-
routes: ProfileRoutes.routes,
38-
),
39-
StatefulShellBranch(
40-
navigatorKey: TrackingRoutes.navigatorKey,
41-
routes: TrackingRoutes.routes)
42-
],
43-
),
44-
],
45-
);
18+
final navigationRoutesProvider = Provider<GoRouter>((ref) {
19+
final hasCompletedOnboardingAsync = ref.watch(hasCompletedOnboardingProvider);
20+
21+
// Standardmäßig starten wir mit dem Today-Screen
22+
String initialLocation = todayPath;
23+
24+
// Nur wenn das Onboarding nicht abgeschlossen ist, navigieren wir dorthin
25+
if (hasCompletedOnboardingAsync is AsyncData &&
26+
hasCompletedOnboardingAsync.value == false) {
27+
initialLocation = onboardingPath;
28+
}
29+
30+
return GoRouter(
31+
navigatorKey: _rootNavigatorKey,
32+
initialLocation: initialLocation,
33+
routes: <RouteBase>[
34+
// Onboarding und Autorisierungsprobleme als separate Routen
35+
GoRoute(
36+
path: onboardingPath,
37+
builder: (context, state) => const OnboardingScreen(),
38+
),
39+
GoRoute(
40+
path: authorizationProblemPath,
41+
builder: (context, state) => const AuthorizationProblemScreen(),
42+
),
43+
44+
// Haupt-Navigation mit StatefulShell
45+
StatefulShellRoute.indexedStack(
46+
builder: (context, state, navigationShell) {
47+
return MoveTopiaNavigator(navigationShell: navigationShell);
48+
},
49+
branches: <StatefulShellBranch>[
50+
StatefulShellBranch(
51+
navigatorKey: TodayRoutes.navigatorKey,
52+
routes: TodayRoutes.routes,
53+
),
54+
StatefulShellBranch(
55+
navigatorKey: ActivitiesRoutes.navigatorKey,
56+
routes: ActivitiesRoutes.routes,
57+
),
58+
StatefulShellBranch(
59+
navigatorKey: ChallengesRoutes.navigatorKey,
60+
routes: ChallengesRoutes.routes,
61+
),
62+
StatefulShellBranch(
63+
navigatorKey: ProfileRoutes.navigatorKey,
64+
routes: ProfileRoutes.routes,
65+
),
66+
StatefulShellBranch(
67+
navigatorKey: TrackingRoutes.navigatorKey,
68+
routes: TrackingRoutes.routes)
69+
],
70+
),
71+
],
72+
);
73+
});

lib/data/repositories/profile_repository_impl.dart

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ class ProfileRepositoryImpl implements ProfileRepository {
2020
await prefs.setString(key, value);
2121
} else if (value is DateTime) {
2222
await prefs.setString(key, value.toIso8601String());
23+
} else if (value is AppThemeMode) {
24+
await prefs.setInt(key, value.index);
2325
} else {
2426
throw Exception('Unsupported type');
2527
}
@@ -43,14 +45,21 @@ class ProfileRepositoryImpl implements ProfileRepository {
4345
}
4446

4547
@override
46-
Future<bool> getIsDarkMode() async {
47-
final value = await loadSetting(isDarkModeKey);
48-
return value != null ? value as bool : false; // Default mode
48+
Future<AppThemeMode> getThemeMode() async {
49+
final value = await loadSetting(themeModeKey);
50+
if (value != null) {
51+
final index = value as int;
52+
// Stelle sicher, dass der Index gültig ist
53+
if (index >= 0 && index < AppThemeMode.values.length) {
54+
return AppThemeMode.values[index];
55+
}
56+
}
57+
return AppThemeMode.system; // Standardeinstellung
4958
}
5059

5160
@override
52-
Future<void> saveIsDarkMode(bool isDarkMode) async {
53-
await saveSetting(isDarkModeKey, isDarkMode);
61+
Future<void> saveThemeMode(AppThemeMode themeMode) async {
62+
await saveSetting(themeModeKey, themeMode);
5463
}
5564

5665
@override

lib/domain/repositories/profile_repository.dart

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
const stepGoalKey = 'stepGoal';
2-
const isDarkModeKey = 'isDarkMode';
2+
const themeModeKey = 'themeMode';
33
const userEPKey = 'userEP';
44
const userLevelKey = 'userLevel';
55
const installationDateKey = 'installationDate';
66
const lastUpdatedKey = 'lastUpdated';
77

8+
enum AppThemeMode {
9+
system,
10+
light,
11+
dark,
12+
}
13+
814
abstract class ProfileRepository {
915
Future<void> saveSetting(String key, dynamic value);
1016

@@ -15,9 +21,9 @@ abstract class ProfileRepository {
1521

1622
Future<void> saveStepGoal(int stepGoal);
1723

18-
Future<bool> getIsDarkMode();
24+
Future<AppThemeMode> getThemeMode();
1925

20-
Future<void> saveIsDarkMode(bool isDarkMode);
26+
Future<void> saveThemeMode(AppThemeMode themeMode);
2127

2228
Future<int> getUserEP();
2329

0 commit comments

Comments
 (0)