Mobile app
Cross-platform Flutter 3.22+ client. Source repo at /home/kushal/apps/sanchalan_setu/attendance_mobile/. Targets Android (primary, BYOD majority) + iOS.
Source layout
attendance_mobile/
├── pubspec.yaml # deps: dio, riverpod, go_router, hive, geolocator, ...
├── analysis_options.yaml
├── scripts/
│ ├── bootstrap.sh # idempotent: flutter create + pub get + manifest patches
│ ├── patch_android_manifest.py # adds permissions + sanchalan-attendance:// intent filter
│ └── patch_ios_plist.py # NSLocationWhenInUseUsage + URL scheme
├── lib/
│ ├── main.dart # entry: Hive init, runApp
│ ├── app.dart # router + bottom-nav shell
│ ├── config.dart # API_BASE, AUTH_MODE, deep-link scheme — all dart-define
│ ├── theme.dart # Rajya-Sabha-blue Material 3 theme
│ ├── api/ # Dio clients per backend domain
│ │ ├── api_client.dart # auth-injecting interceptor
│ │ ├── device_api.dart
│ │ ├── leave_api.dart
│ │ ├── profile_api.dart
│ │ ├── punch_api.dart
│ │ └── tour_api.dart
│ ├── auth/
│ │ ├── pkce.dart # code_verifier + code_challenge generation
│ │ ├── auth_repo.dart # SSO mobile flow (browser + deep-link)
│ │ ├── token_store.dart # flutter_secure_storage wrapper
│ │ └── login_screen.dart
│ ├── device/
│ │ ├── device_id.dart # stable UUIDv4 + sha256-salted installation hash
│ │ ├── keypair.dart # P-256 keypair, persisted, signs in raw r||s
│ │ └── attestation.dart # rooted/emulator/mock-location signals
│ ├── geo/
│ │ └── location_service.dart # high-accuracy fix + Wi-Fi SSID
│ ├── punch/
│ │ ├── punch_signer.dart
│ │ ├── offline_queue.dart # Hive box, 48h max-age, replay-on-reconnect
│ │ └── punch_screen.dart
│ ├── attendance/
│ │ ├── today_screen.dart
│ │ └── month_screen.dart # calendar grid (NIC-style)
│ ├── leave/
│ │ ├── leave_home_screen.dart
│ │ └── leave_apply_screen.dart
│ ├── tour/
│ │ └── tour_screens.dart # list + apply
│ ├── holidays/
│ │ └── holidays_screen.dart
│ ├── devices/
│ │ └── devices_screen.dart
│ ├── profile/
│ │ └── profile_screen.dart
│ └── dashboard/
│ └── dashboard_screen.dart # NIC-style home
└── test/
├── pkce_test.dart
└── punch_signer_test.dart
Screen graph
flowchart LR
Login["/login
SSO Parichay"] --> Shell subgraph Shell["bottom-nav shell"] Home["/home
Dashboard"] Punch["/punch"] Today["/today"] Month["/month
(calendar grid)"] Leave["/leave"] end Home --> Tour["/tour"] Home --> Hol["/holidays"] Home --> Dev["/devices"] Home --> Prof["/profile"] Leave --> LeaveApply["/leave/apply"] Tour --> TourApply["/tour/apply"] Prof --> Login
SSO Parichay"] --> Shell subgraph Shell["bottom-nav shell"] Home["/home
Dashboard"] Punch["/punch"] Today["/today"] Month["/month
(calendar grid)"] Leave["/leave"] end Home --> Tour["/tour"] Home --> Hol["/holidays"] Home --> Dev["/devices"] Home --> Prof["/profile"] Leave --> LeaveApply["/leave/apply"] Tour --> TourApply["/tour/apply"] Prof --> Login
Punch signing flow
sequenceDiagram
participant U as User
participant App as Flutter app
participant SS as flutter_secure_storage
participant Geo as Geolocator
participant Be as backend
U->>App: tap "Punch IN"
App->>SS: read keypair (or generate P-256 on first use)
App->>Geo: getCurrentPosition (high accuracy)
Geo-->>App: lat, lng, accuracy, ssid, mocked
App->>App: nonce = hex(rand(16))
punched_at = now (UTC iso)
msg = nonce + device_uuid + punched_at
sig = ECDSA P-256 SHA-256 raw r||s std-base64 App->>Be: POST /api/v1/punch (Bearer)
{device_uuid, punch_type:IN, punched_at,
lat, lng, accuracy_m, ssid_seen,
nonce, signature, anti_spoof} alt network up Be-->>App: 201 + verdict App->>U: show verdict pill else network down App->>App: OfflineQueue.enqueue(payload) App->>U: show "queued, will sync" Note over App: drainOffline fires on connectivity event end
punched_at = now (UTC iso)
msg = nonce + device_uuid + punched_at
sig = ECDSA P-256 SHA-256 raw r||s std-base64 App->>Be: POST /api/v1/punch (Bearer)
{device_uuid, punch_type:IN, punched_at,
lat, lng, accuracy_m, ssid_seen,
nonce, signature, anti_spoof} alt network up Be-->>App: 201 + verdict App->>U: show verdict pill else network down App->>App: OfflineQueue.enqueue(payload) App->>U: show "queued, will sync" Note over App: drainOffline fires on connectivity event end
Build steps
# pre-reqs: Flutter 3.22+, Android Studio (SDK API 34+), Xcode 15+ for iOS
cd /home/kushal/apps/sanchalan_setu/attendance_mobile
./scripts/bootstrap.sh # generates android/, ios/, runs pub get, patches manifests
flutter run \
--dart-define=API_BASE=https://attendance.rajyasabha.digital/api \
--dart-define=IMEI_SALT_FRONTEND=$(openssl rand -hex 16) \
--dart-define=AUTH_MODE=sso
flutter build apk --release \
--dart-define=API_BASE=https://attendance.rajyasabha.digital/api \
--dart-define=IMEI_SALT_FRONTEND=<provisioned-from-vault>
Permissions requested
| Platform | Permission | Why |
|---|---|---|
| Android | INTERNET | API calls. |
ACCESS_FINE_LOCATION | Geofence + Wi-Fi SSID join. | |
ACCESS_COARSE_LOCATION | Fallback when fine permission denied. | |
ACCESS_WIFI_STATE | Read SSID for indoor geofence allowlist. | |
ACCESS_NETWORK_STATE | Connectivity_plus offline-queue trigger. | |
USE_BIOMETRIC | Future: biometric prompt before punch. | |
| iOS | NSLocationWhenInUseUsageDescription + URL scheme | Same as above; sanchalan-attendance:// for SSO callback. |
What's deliberately not in the app
- Raw IMEI — Android 10+ blocks it for non-system apps. We hash
(android_id || fingerprint || serial)with a frontend salt before sending; backend re-salts and stores the digest. - Aadhaar AUA/KUA — separate kernel service per
/infra/ §17(sensitive VLAN). - Bundled biometric SDK — would bloat APK; rely on platform biometric prompt for the punch action (TODO).
- Hardcoded backend URL — passed via
--dart-defineat build time so dev/uat/prod APKs differ only in env.