12 - Code Notes
This code was written with significant AI assistance. Here's what that means for you as a human developer taking over the codebase.
AI-generated code has specific strengths and quirks. It tends to be consistent, pattern-driven, and over-simplified in some places but also over-complete in others. This page lists things that might look weird but are intentional, and things to watch out for.
Where Logic Lives
There are only two places to look for business logic:
- Stage screens (
lib/screens/stage*.dart) - UI state, form validation, event handlers, navigation decisions - ApiService (
lib/services/api_service.dart) - All backend interactions, auth, error handling
That's it. There are no:
- Controllers / Presenters / ViewModels
- BLoCs / Cubits / Providers / Riverpod notifiers
- Event buses / streams / reactive state managers
- Repositories / use cases / interactors
- Base classes / mixins / extension inheritance chains
If you're looking for where something happens, open the relevant stage file. If it's a network call, check api_service.dart. You will never need to trace through 5 files to understand a single behavior.
Deliberate Design Decisions
These choices may surprise you. They were intentional.
1. Single massive ApiService
~1100 lines, ~60 methods, one class. Common BLoC/clean-architecture orthodoxy says split this by feature. We didn't, because:
- Every endpoint is discoverable in one file (
Ctrl+F) - Auth, headers, error handling, stage tracking are centralized in 4 helpers
- Adding a new endpoint takes 5 lines, not 5 files
2. setState everywhere, no state management library
eKYC is a one-way linear journey. No screen needs to react to state changes in another screen. Each stage is self-contained. setState is sufficient.
3. Wrapper widgets, not mixins
Cross-cutting concerns (loading overlay, session timer, back button, progress bar) are independent widgets that take a child. No base class, no mixin chain, no inheritance. See 08 - Widgets.
4. Mock URL detection in screens, not middleware
The backend returns example.com in mock URLs. Flutter detects these inline in Stage 5 and Stage 6, guarded by ApiConfig.isDev. This is intentionally not a network interceptor - it lives where the URL is actually used, making it obvious.
5. Compile-time environment, no runtime config
No JSON config file. No .env. No runtime environment switcher. Just --dart-define=ENV=prod at build time. Dead code paths are tree-shaken out. No way to accidentally ship a dev URL in a prod build.
6. No dependency injection framework
No GetIt, no Riverpod DI, no Provider. Screens create their own ApiService(storage: storage) instance on demand. This is fine because ApiService is stateless and StorageService is a singleton.
7. pushReplacementNamed everywhere, not push
Each stage replaces the previous in the navigator stack. Pressing back on Stage 5 does not return to Stage 4. The journey is one-way. BackButtonHandler handles the real device back button with an exit confirmation.
Things That Look Weird But Aren't Bugs
_post takes a fromJson function parameter
This is a generic type erasure workaround in Dart. Dart can't do T.fromJson(json) at runtime, so we pass the factory explicitly.
DartWhy this signature
Future<ApiResponse<T>> _post<T>(
String url,
Map<String, dynamic> body, {
required T Function(Map<String, dynamic>) fromJson, // <-- this
}) async { ... }
// Caller passes the factory by name (tear-off):
_post<Lead>(url, body, fromJson: Lead.fromJson)
Some screens have an enum _BankViewState but others don't
Only screens with 3+ visual modes (Stage 3, 5, 6, 13) use a view-state enum. Simple screens like Stage 1 Registration just have a form and don't need it. Inconsistent by design.
SharedPreferences initialized async in main()
await StorageService.getInstance() runs before runApp. This is so every screen can assume storage is ready and call getInstance() synchronously afterwards.
Stage 10 redirects to Stage 9
Stage 10 Income Proof used to be a separate screen. It was merged into Stage 9 (Personal Details, F&O section) per the latest BRD. Stage 10 exists as a thin redirect for backwards compatibility.
Razorpay key is in the backend, not the app
Backend returns razorpayKey in the paymentDefaults response. Mobile uses this key with the Razorpay SDK. Web uses the redirect URL flow instead.
flutter_app/lib/screens/stage15_congrats.dartDart
if (kIsWeb || razorpayKey == null || razorpayKey.isEmpty) {
// Web or no key: use redirect URL
await launchUrl(Uri.parse(redirectUrl), ...);
} else {
// Mobile with key: use SDK
_razorpay.open({'key': razorpayKey, ...});
}
Things to Watch Out For
Don't add a state management library
BLoC or Riverpod would require rewriting every screen. The current setState approach is consistent. Stay consistent.
Don't bypass ApiService for direct http calls
All centralized concerns (auth, headers, leadId injection, error handling, activity tracking) live in _post/_get. A rogue direct http.post will break session tracking.
Don't add runtime env switches
The compile-time --dart-define=ENV=prod approach is a security feature. Adding a runtime settings screen that lets users change the backend URL would let attackers point a prod app at a malicious backend.
Don't remove the mock URL detection without checking isDev
The bypass is guarded by ApiConfig.isDev. Removing the guard would let mock URLs short-circuit real providers in prod. Don't touch this code without understanding 02 - Mock Mode.
Don't hardcode asset paths
Always use AppAssets.something. Hardcoded strings like 'assets/images/foo.png' bypass the type system and break silently if the file is renamed.
Don't mix logic across stages
Stage 6 should not import from Stage 7. If you need shared state, put it in StorageService. If you need shared UI, make it a widget in lib/widgets/.
Keep wrapper widgets optional
If you add a new wrapper, don't require screens to use it. Make it opt-in. Follow the existing pattern: class MyWrapper { final Widget child; }.
Refactoring Tips
If you do need to refactor, here are safe starting points:
- Split ApiService by stage: Extract per-stage methods into
ApiServiceStage1,ApiServiceStage2, etc. - still one central class per stage, but smaller files. Useextensionor inheritance. - Extract form validation: Regex patterns for mobile, PAN, email, IFSC are duplicated. Pull into
lib/utils/validators.dart. - Extract stage-display names: The
JourneyProgressBarhas a big switch for stage names. Move toAppRoutes.stageDisplayName(int).
1. The layered architecture (screens / services / models / config).
2. The ApiResponse<T> result type - changing it breaks every screen.
3. The StorageService singleton - changes to initialization order can crash startup.
4. The mock URL detection guard - this is a security boundary.
How to Verify AI-Generated Code Still Works
When you make changes, check these things:
flutter analyze- should return 0 errors, 0 warnings (info-level is OK)- Full journey in mock mode (Stage 0 → Stage 15) - use OTP
1234 - Session timeout works (wait 13 min, see warning)
- Back button shows exit sheet
- Navigation doesn't build up a back stack (pressing back shouldn't return to Stage 1 from Stage 5)