feat: enhance UI, branding assets, onboarding flow, and screen layouts
This commit is contained in:
62
lib/core/permissions.dart
Normal file
62
lib/core/permissions.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
/// The three things the app asks for, in the order it asks.
|
||||
enum AppPermission { location, notifications, camera }
|
||||
|
||||
extension AppPermissionX on AppPermission {
|
||||
Permission get _platform => switch (this) {
|
||||
// While-in-use only: clock-in needs to know you are at the venue, and
|
||||
// nothing here justifies background tracking.
|
||||
AppPermission.location => Permission.locationWhenInUse,
|
||||
AppPermission.notifications => Permission.notification,
|
||||
AppPermission.camera => Permission.camera,
|
||||
};
|
||||
}
|
||||
|
||||
/// Thin wrapper over `permission_handler`.
|
||||
///
|
||||
/// Exists so the rest of the app never imports the plugin directly, and so the
|
||||
/// platforms it has no implementation for — web, desktop — degrade to "granted"
|
||||
/// instead of throwing. On the web the browser prompts at the point of use
|
||||
/// anyway, so asking up front is neither possible nor needed.
|
||||
class PermissionService {
|
||||
const PermissionService();
|
||||
|
||||
bool get _supported =>
|
||||
!kIsWeb &&
|
||||
(defaultTargetPlatform == TargetPlatform.android ||
|
||||
defaultTargetPlatform == TargetPlatform.iOS);
|
||||
|
||||
/// Shows the platform dialog. Returns whether the permission ended up
|
||||
/// granted — which is not the same as whether the user tapped Allow, since
|
||||
/// it may already have been permanently denied.
|
||||
Future<bool> request(AppPermission permission) async {
|
||||
if (!_supported) return true;
|
||||
final status = await permission._platform.request();
|
||||
return status.isGranted || status.isLimited;
|
||||
}
|
||||
|
||||
Future<bool> isGranted(AppPermission permission) async {
|
||||
if (!_supported) return true;
|
||||
final status = await permission._platform.status;
|
||||
return status.isGranted || status.isLimited;
|
||||
}
|
||||
|
||||
/// True once the OS will no longer show a dialog, so the only way back is
|
||||
/// the system settings page.
|
||||
Future<bool> isPermanentlyDenied(AppPermission permission) async {
|
||||
if (!_supported) return false;
|
||||
return permission._platform.status.then((s) => s.isPermanentlyDenied);
|
||||
}
|
||||
|
||||
Future<void> openSettings() async {
|
||||
if (!_supported) return;
|
||||
await openAppSettings();
|
||||
}
|
||||
}
|
||||
|
||||
final permissionServiceProvider = Provider<PermissionService>(
|
||||
(ref) => const PermissionService(),
|
||||
);
|
||||
@@ -10,6 +10,7 @@ import '../screens/onboarding_screens.dart';
|
||||
import '../screens/pay_screens.dart';
|
||||
import '../screens/profile_detail_screens.dart';
|
||||
import '../screens/profile_screen.dart';
|
||||
import '../screens/requirements_screen.dart';
|
||||
import '../screens/settings_screens.dart';
|
||||
import '../screens/shift_detail_screen.dart';
|
||||
import '../screens/shifts_screen.dart';
|
||||
@@ -60,27 +61,30 @@ final router = GoRouter(
|
||||
_detailRoute('/phone', (_) => const PhoneScreen()),
|
||||
_detailRoute('/otp', (_) => const OtpScreen()),
|
||||
|
||||
// Branch order is the tab order, and it matches the prototype: Today sits
|
||||
// in the middle, with the two browsing tabs to its left and the two
|
||||
// personal ones to its right. KrowNavBar's items must stay in step.
|
||||
StatefulShellRoute.indexedStack(
|
||||
builder: (_, _, shell) => HomeShell(navigationShell: shell),
|
||||
branches: [
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(path: '/shifts', builder: (_, _) => const ShiftsScreen()),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [GoRoute(path: '/pay', builder: (_, _) => const PayScreen())],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(path: '/today', builder: (_, _) => const TodayScreen()),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(path: '/shifts', builder: (_, _) => const ShiftsScreen()),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(path: '/clock', builder: (_, _) => const ClockScreen()),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [GoRoute(path: '/pay', builder: (_, _) => const PayScreen())],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(path: '/profile', builder: (_, _) => const ProfileScreen()),
|
||||
@@ -92,6 +96,12 @@ final router = GoRouter(
|
||||
_detailRoute(
|
||||
'/shift/:id',
|
||||
(state) => ShiftDetailScreen(shiftId: state.pathParameters['id']!),
|
||||
routes: [
|
||||
_detailRoute(
|
||||
'requirements',
|
||||
(state) => RequirementsScreen(shiftId: state.pathParameters['id']!),
|
||||
),
|
||||
],
|
||||
),
|
||||
_detailRoute('/availability', (_) => const AvailabilityScreen()),
|
||||
_detailRoute('/timecards', (_) => const TimecardsScreen()),
|
||||
|
||||
Reference in New Issue
Block a user