feat: enhance UI, branding assets, onboarding flow, and screen layouts

This commit is contained in:
R-Bharathraj
2026-09-18 15:23:30 +05:30
parent cafffd27e0
commit 5122eebc11
68 changed files with 1661 additions and 470 deletions

62
lib/core/permissions.dart Normal file
View 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(),
);

View File

@@ -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()),