06 - Stage Walkthrough

Let's trace Stage 1 Registration end-to-end. By following this one stage carefully, you'll understand how every other stage in the app is wired.

Goal

User taps "Proceed" on the registration form → backend creates a lead → app navigates to Stage 2 (OTP). We'll show the frontend code + backend handler in parallel for each step.

Step 1 - App Launches, Lands on Stage 0

main.dart initializes storage and runs EkycApp. The root route / maps to Stage0ArrivalScreen.

flutter_app/lib/main.dartDart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await StorageService.getInstance();  // Singleton init
  runApp(const EkycApp());
}

Step 2 - Stage 0 Calls journeyInit and Navigates to Stage 1

Stage0ArrivalScreen has no UI input - it shows a Lottie loader, calls the backend to create a session, then forwards to Stage 1.

flutter_app/lib/screens/stage0_arrival.dartDart_initJourney
Future<void> _initJourney() async {
  final storage = await StorageService.getInstance();
  final api = ApiService(storage: storage);

  // Check for resume first (existing lead)
  if (storage.hasActiveLead) { ... resume ... }

  // Fresh journey - create session
  final result = await api.journeyInit(deviceType: 'DESKTOP');
  if (result.success) {
    Navigator.pushReplacementNamed(context, AppRoutes.stage1Registration);
  }
}
Backend handler for Stage 0
  • HTTP: GET /api/v1/journey/init
  • Controller: backend/src/MO.Ekyc.Api/Controllers/Common/ArrivalController.cs
  • Service: backend/src/MO.Ekyc.Infrastructure/Services/Common/ArrivalService.cs
  • Backend docs (pending) - placeholder

Step 3 - Stage 1 Screen Builds Its UI

The registration screen shows the RISE logo, a hero illustration, mobile + name fields, a T&C checkbox, and a Proceed button. Form validation is local (regex checks).

flutter_app/lib/screens/stage1_registration.dartDart_isFormValid getter
bool get _isFormValid {
  final mobile = _mobileController.text.trim();
  final name = _nameController.text.trim();
  return RegExp(r'^[6-9]\d{9}$').hasMatch(mobile) &&
      name.length >= 2 &&
      _acceptedTerms;
}

The Proceed button is disabled until _isFormValid is true. Typing triggers setState(() {}) to re-evaluate.

Step 4 - User Taps Proceed → _onProceed()

The button's onPressed handler calls _onProceed(), which validates the form, sets loading state, then calls the API.

flutter_app/lib/screens/stage1_registration.dartDart_onProceed
Future<void> _onProceed() async {
  if (!_formKey.currentState!.validate() || !_acceptedTerms) return;

  setState(() => _isLoading = true);

  try {
    final storage = await StorageService.getInstance();
    final api = ApiService(storage: storage);

    final mobile = _mobileController.text.trim();
    final name = _nameController.text.trim();

    final result = await api.registrationSignup(
      mobileNumber: mobile,
      fullName: name,
      consentAccountOpening: true,
      consentCommunication: true,
      consentTerms: _acceptedTerms,
    );

    if (!mounted) return;

    if (result.success) {
      Navigator.pushReplacementNamed(
        context,
        AppRoutes.stage2Otp,
        arguments: {'mobileNumber': mobile},
      );
    } else {
      ErrorDialog.show(context, message: result.displayMessage);
    }
  } catch (e) {
    ErrorDialog.show(context, message: 'Something went wrong.');
  } finally {
    if (mounted) setState(() => _isLoading = false);
  }
}

Step 5 - ApiService.registrationSignup Delegates to _post

The ApiService method is a thin wrapper that delegates to the generic _post<T> helper. It knows the URL and the fromJson factory for the response type.

flutter_app/lib/services/api_service.dartDartregistrationSignup
Future<ApiResponse<Lead>> registrationSignup({
  required String mobileNumber,
  required String fullName,
  required bool consentAccountOpening,
  required bool consentCommunication,
  required bool consentTerms,
}) async {
  final result = await _post<Lead>(
    ApiConfig.registrationSignup,
    {
      'mobileNumber': mobileNumber,
      'fullName': fullName,
      'sessionId': _storage.sessionId,
      'consentAccountOpening': consentAccountOpening,
      'consentCommunication': consentCommunication,
      'consentTerms': consentTerms,
    },
    fromJson: Lead.fromJson,
  );
  if (result.success && result.data != null) {
    await _storage.setLeadId(result.data!.leadId);
    await _storage.setCurrentStage(1);
  }
  return result;
}

Step 6 - _post Injects Headers and Calls http.post

The generic helper adds JWT auth, leadId injection, session tracking, timeout, error handling.

flutter_app/lib/services/api_service.dartDart_post
Future<ApiResponse<T>> _post<T>(
  String url,
  Map<String, dynamic> body, {
  required T Function(Map<String, dynamic>) fromJson,
}) async {
  try {
    final response = await http.post(
      Uri.parse(url),
      headers: _headers,
      body: jsonEncode(body),
    ).timeout(ApiConfig.apiTimeout);

    await _storage.updateActivity();

    final json = jsonDecode(response.body) as Map<String, dynamic>;
    if (response.statusCode == 200 && json['success'] == true) {
      return ApiResponse.ok(fromJson(json['data']));
    }
    return ApiResponse.fail(json['message'], json['errorCode']);
  } on SocketException {
    return ApiResponse.fail('No internet connection');
  } catch (e) {
    return ApiResponse.fail('Something went wrong.');
  }
}

Step 7 - HTTP Request Hits the Backend

The request reaches POST /api/v1/registration/signup on the backend.

Backend handler for Stage 1
  • HTTP: POST /api/v1/registration/signup
  • Controller: backend/src/MO.Ekyc.Api/Controllers/Stage1/RegistrationController.cs:22
  • Service: backend/src/MO.Ekyc.Infrastructure/Services/Stage1/RegistrationService.cs
  • Writes: leads table, triggers OTP send (real SMS or fixed 1234 in mock mode)
  • Backend docs (pending) - placeholder

Step 8 - Backend Responds with ApiResponse<Lead>

JSONExample backend response
{
  "success": true,
  "message": "Lead created successfully",
  "data": {
    "leadId": "8f2a1c9e-...",
    "mobileNumber": "9876543210",
    "fullName": "John Doe",
    "sessionId": "...",
    "state": "INITIATED"
  },
  "journeyStatus": { "currentStage": 1, "progressPercent": 6 }
}

Step 9 - Lead.fromJson Turns JSON into Dart Object

flutter_app/lib/models/lead.dartDart
class Lead {
  final String leadId;
  final String? mobileNumber;
  final String? fullName;
  final String? state;

  Lead({required this.leadId, this.mobileNumber, this.fullName, this.state});

  factory Lead.fromJson(Map<String, dynamic> json) => Lead(
    leadId: json['leadId'] as String,
    mobileNumber: json['mobileNumber'] as String?,
    fullName: json['fullName'] as String?,
    state: json['state'] as String?,
  );
}

Step 10 - ApiService Saves leadId to Storage

Back in registrationSignup, after the _post returns successfully, the method writes leadId to persistent storage so the whole app can reference it.

Look at Step 5's code again - after the _post call:

if (result.success && result.data != null) {
  await _storage.setLeadId(result.data!.leadId);
  await _storage.setCurrentStage(1);
}

Step 11 - Screen Calls setState and Navigates

Back in _onProceed, the result.success branch runs:

Navigator.pushReplacementNamed(
  context,
  AppRoutes.stage2Otp,
  arguments: {'mobileNumber': mobile},
);

pushReplacementNamed replaces the current route, so pressing back on Stage 2 won't return to Stage 1 - it's a one-way journey.

Step 12 - Stage 2 Screen Mounts

AppRoutes.stage2Otp maps to Stage2OtpScreen in routes.dart. The MaterialApp router mounts it and its initState runs, starting the OTP flow.

flutter_app/lib/config/routes.dartDart
class AppRoutes {
  static const stage0Arrival     = '/';
  static const stage1Registration = '/registration';
  static const stage2Otp          = '/otp';
  // ...

  static Map<int, String> stageRoutes = {
    0: stage0Arrival,
    1: stage1Registration,
    2: stage2Otp,
    // ...
  };
}

Summary Diagram

For a visual sequence diagram of this exact flow, open frontend_architecture.html — Appendix: Functional Flow Sequences and expand the "Stage 1 Registration Flow" collapsible. It shows the same 12 steps as a UML-style sequence diagram.

Apply This to Any Stage

Every stage follows the same basic pattern:

  1. Screen builds UI with local state (loading, form fields)
  2. User interacts
  3. Screen validates locally
  4. Screen calls an ApiService method
  5. ApiService calls _post / _get with URL + fromJson
  6. HTTP hits the matching backend controller
  7. Response parsed into ApiResponse<T>
  8. ApiService may write to storage (setLeadId, setCurrentStage)
  9. Screen checks result.success and navigates or shows an error

Open any of the 16 stage files in lib/screens/ and you will recognize this structure.