Skip to main content

User Flows

This document traces the key user flows in the Visla GPS Android app end-to-end β€” from screen entry through ViewModel intents, use-case orchestration, API calls, and final UI transitions. Each flow includes decision points, error-handling branches, and the exact code components involved.

Architecture context: All flows follow the MVI pattern β€” every user action dispatches an Intent, the ViewModel processes it through a use case, and emits new UiState or one-shot Event objects. See Navigation for the full route table.


1. Authentication Flow​

The authentication flow covers email login, social login (Google/Facebook), registration, email verification, two-factor authentication, and password reset.

Key files:

LayerFiles
ScreensLoginScreen, RegisterScreen, VerificationCodeScreen, TwoFactorScreen, ForgotPasswordScreen, ResetCodeScreen, ResetPasswordScreen
ViewModelsLoginViewModel, RegisterViewModel, VerificationViewModel, TwoFactorViewModel, ForgotPasswordViewModel, ResetCodeViewModel, ResetPasswordViewModel, AuthNavigationViewModel
Use CasesLoginUseCase, LoginWithGoogleUseCase, LoginWithFacebookUseCase, Login2FAUseCase, RegisterUseCase, RequestPasswordResetUseCase, VerifyResetCodeUseCase, ResetPasswordUseCase
DataAuthRepositoryImpl, TokenManager, GoogleAuthManager, FacebookAuthManager

Cross-reference: Authentication & Security Β· Data Layer β†’ Auth

1.1 Email Login​

Step-by-step:

  1. User enters email and password on LoginScreen.
  2. Dispatches LoginIntent.Login.
  3. LoginViewModel.login() sets isLoading = true and calls LoginUseCase.
  4. The use case calls authRepository.login(email, password).
  5. On success β€” tokens are saved via TokenManager.saveTokens(), LoginEvent.Success is emitted, and navigation proceeds to the main screen.

Decision points:

ConditionBranch
2FA requiredServer returns TwoFactorRequiredException with tempToken β†’ sets showTwoFactor = true, renders inline TwoFactorScreen (see Β§1.4)
Email not verifiedResponse message contains "verify your email" β†’ emits LoginEvent.NeedVerification(email) β†’ navigates to VerificationCodeScreen
Terms not acceptedSocial login only β€” emits LoginEvent.NeedTermsAcceptance(name) β†’ shows terms dialog

Error states:

ErrorExceptionRecovery
Wrong credentialsInvalidCredentialsExceptionError message displayed, user retries
Network failureNetworkExceptionError message, user retries
Rate limitedRateLimitExceptionError message, wait and retry
Server errorServerException(statusCode)Error message displayed

1.2 Social Login (Google / Facebook)​

Both social login paths converge to the same post-login handling.

Google:

  1. User taps Sign in with Google β†’ dispatches LoginIntent.LoginWithGoogle(activity).
  2. GoogleAuthManager.signIn(activity) uses the AndroidX Credentials API to obtain a Google ID token.
  3. LoginWithGoogleUseCase(idToken) calls authRepository.loginWithGoogle(idToken).
  4. Backend validates the token, returns access/refresh tokens.
  5. If termsAccepted == false, emits LoginEvent.NeedTermsAcceptance(name).
  6. Otherwise, saves tokens and navigates to main.

Facebook:

  1. User taps Sign in with Facebook β†’ dispatches LoginIntent.LoginWithFacebook(activity).
  2. FacebookAuthManager.login() opens the Facebook login dialog, requesting ["email", "public_profile"] permissions.
  3. On callback success, the access token is passed to LoginWithFacebookUseCase(accessToken).
  4. Same backend flow as Google from step 4 onward.

Error handling: Both paths catch GetCredentialCancellationException (Google) or FacebookException (Facebook) for user cancellation, and surface network/server errors via errorMessage in UI state.

1.3 Registration​

Step-by-step:

  1. User fills in name, email, password, and confirm password. Checks terms checkbox.
  2. Client-side validation: name 2–250 chars, valid email format, password β‰₯ 8 chars, passwords match, terms accepted.
  3. Dispatches RegisterIntent.Register β†’ RegisterUseCase calls authRepository.register().
  4. On success β†’ emits RegisterEvent.Success(email) β†’ navigates to VerificationCodeScreen.

Error states:

ErrorExceptionRecovery
Email already existsValidationException (422)Show error, user changes email
Weak passwordWeakPasswordExceptionShow error, user strengthens password
Network failureNetworkExceptionShow error, retry

1.4 Two-Factor Authentication (TOTP)​

When login returns TwoFactorRequiredException, the login screen overlays an inline TwoFactorScreen:

  1. LoginViewModel sets showTwoFactor = true and stores the tempToken.
  2. User enters a 6-digit TOTP code from their authenticator app.
  3. Dispatches LoginIntent.Verify2FA(code) β†’ Login2FAUseCase(tempToken, code).
  4. Backend validates the code against the user's TOTP secret.
  5. On success β€” tokens saved, LoginEvent.Success emitted, navigate to main.
  6. On failure β€” Invalid2FACodeException β†’ clears code input, shows error. User retries.
  7. User can cancel via LoginIntent.CancelTwoFactor β†’ returns to login form.

2FA setup (from Settings) uses TwoFactorSetupViewModel with a multi-step flow: LOADING β†’ SHOW_QR β†’ ENTER_CODE β†’ SUCCESS. The QR code is generated via ZXing's QRCodeWriter from the otpauthUrl. After verification, backup codes are displayed for the user to save.

1.5 Forgot Password​

The password reset flow spans three screens:

Step 1 β€” Request reset:

  1. User enters email on ForgotPasswordScreen.
  2. ForgotPasswordIntent.RequestReset β†’ RequestPasswordResetUseCase.
  3. Backend sends a 6-digit code to the email.
  4. On success β†’ emits ForgotPasswordEvent.NavigateToResetCode(email).

Step 2 β€” Enter code:

  1. User enters the 6-digit code on ResetCodeScreen. Auto-submits when 6 digits are entered.
  2. ResetCodeIntent.Verify β†’ VerifyResetCodeUseCase(email, code).
  3. On success β†’ emits ResetCodeEvent.Success(email, code) β†’ navigates to ResetPasswordScreen.

Step 3 β€” Set new password:

  1. User enters new password + confirmation on ResetPasswordScreen.
  2. Validation: password β‰₯ 8 chars, passwords match.
  3. ResetPasswordIntent.Submit β†’ ResetPasswordUseCase(email, code, password).
  4. On success β†’ emits ResetPasswordEvent.Success β†’ navigates back to LoginScreen.

1.6 Auth State Machine​

AuthNavigationViewModel manages the overall auth navigation state:

Deep link handling:

  • HandleVerificationToken(token) β†’ calls authRepository.verifyEmailByToken(token) β†’ saves tokens β†’ navigates to Main.
  • HandleResetToken(token) β†’ calls authRepository.resolveResetToken(token) β†’ navigates to ResetPasswordScreen(email, code).

2. Device Claiming Flow​

The device claiming flow lets users add a GPS tracker to their account by entering a claim token manually or scanning a QR code.

Key files:

LayerFiles
ScreensAddDeviceScreen, DevicesListScreen, DeviceDetailScreen
ViewModelsAddDeviceViewModel, DevicesViewModel
Use CasesClaimDeviceUseCase, UnclaimDeviceUseCase, DevicesInteractor, GetLicenseStatusUseCase
DataDeviceRepositoryImpl, DeviceApi, DeviceDataStore

Cross-reference: Data Layer β†’ Devices Β· Billing (license checks)

2.1 Flow Diagram​

2.2 Step-by-Step​

  1. License check β€” Before showing the add-device UI, DevicesViewModel dispatches CheckLicense which calls GetLicenseStatusUseCase. If license.available == 0, an upgrade sheet modal is shown instead.

  2. Token input β€” The user either types a token (formatted as B1G8-GNMI) or scans a QR code using ML Kit barcode detection. AddDeviceViewModel.processScannedCode() extracts the token from a URL or uses the raw scanned value.

  3. Claim request β€” AddDeviceIntent.ClaimDevice triggers claimDevice() in the ViewModel:

    • Validates token is non-empty.
    • Re-checks license availability.
    • Calls ClaimDeviceUseCase(token) β†’ deviceRepository.claimDevice(token) β†’ POST /api/devices/claim.
  4. API validation β€” The backend validates the token, checks it hasn't been claimed, and returns the device object.

  5. Device appears β€” On success, the device is added to DeviceDataStore (the single source of truth), the WebSocket reconnects to receive real-time positions for the new device, and the user is navigated to device_detail/{deviceId}.

2.3 Error States​

ScenarioExceptionUser Experience
Empty tokenEarly return in ViewModelError message set
Token too short (< 6 chars)InvalidClaimTokenException"Token must be at least 6 characters"
Token not foundHTTP 404 β†’ DeviceNotFoundExceptionError displayed, user retries
Already claimedHTTP 409 β†’ DeviceAlreadyClaimedExceptionError displayed
No license slotsManual check before API callUpgrade sheet modal shown
Network errorNetworkException"Network error claiming device"
Session expiredHTTP 401 β†’ AuthenticationExceptionGlobal token refresh via interceptor

2.4 Post-Claim: Real-Time Updates​

After a successful claim, RealTimeDataBridge.reconnect() re-establishes the WebSocket connection. The backend starts streaming position updates, battery level, and signal strength for the new device. These updates flow through DeviceDataStore and are automatically reflected in the devices list and map screens.


3. Subscription Purchase Flow​

The subscription flow handles plan selection, Google Play Billing integration, backend verification, and license activation.

Key files:

LayerFiles
ScreensSubscriptionScreen, SubscriptionManagementScreen
ViewModelsSubscriptionManagementViewModel
BillingBillingManager (implements IBillingManager, PurchasesUpdatedListener)
Use CasesGetLicenseStatusUseCase, VerifyAndroidPurchaseUseCase, DeviceLicenseInteractor
DataBillingRepositoryImpl, BillingApi, SubscriptionRepositoryImpl
ComponentsDeviceSelectorCard, PeriodSelectorCard, PriceSummaryCard, PurchaseButton, LicenseUsageCard, StatusCard, ActionsCard

Cross-reference: Billing & Subscriptions Β· Data Layer β†’ Billing Β· Data Layer β†’ Subscriptions

3.1 Flow Diagram​

3.2 Step-by-Step​

  1. Plan selection β€” User selects device count (1–10) and billing period (Monthly, Semi-Annual at 20% discount, or Annual at 40% discount) on SubscriptionScreen. Product IDs follow the pattern sub_{period}_{devices} (e.g., sub_annual_5).

  2. Product loading β€” BillingManager.loadProducts() queries all 30 product combinations from Google Play. Prices are displayed in the user's local currency via ProductDetails.

  3. Plan modification detection β€” If the user has an existing subscription, the UI detects:

    • isCurrentPlanExact β€” same devices + same period β†’ purchase button disabled.
    • isChangingPlan β€” different selection from current plan.
  4. Purchase initiation β€” User taps the purchase button β†’ BillingManager.purchase(activity, productDetails, userId):

    • Gets offerToken from subscriptionOfferDetails[0].
    • Sets obfuscatedAccountId to the user ID for backend correlation.
    • Calls BillingClient.launchBillingFlow() β†’ Google Play system dialog appears.
  5. Purchase callback β€” onPurchasesUpdated(billingResult, purchases):

    • BillingResponseCode.OK β†’ proceeds to handlePurchase().
    • USER_CANCELED β†’ logged, no error shown.
    • Other codes β†’ error message displayed.
  6. Backend verification β€” handlePurchase() calls the onPurchaseVerified callback which triggers BillingRepositoryImpl.verifyAndroidPurchase():

    • Sends purchaseToken and productId to POST /api/billing/purchases/android/verify.
    • Backend validates the Google Play receipt signature and creates/updates the subscription record.
  7. Acknowledgement β€” BillingManager.acknowledgePurchase() confirms the purchase with Google Play. Purchases must be acknowledged within 3 days or they are automatically refunded.

  8. License activation β€” After verification, SubscriptionManagementViewModel.loadSubscriptionStatus() fetches the updated license from GET /api/devices/license/status. The License entity reflects the new allowed device count, and the user can now claim devices up to that limit.

3.3 Error States​

ScenarioSourceRecovery
Play Store connection failedBillingManager.setupBillingClient()Retry connection
Products won't loadloadProducts() β†’ HttpExceptionError message, retry
Offer token missingpurchase() validationValidationException shown
Purchase cancelledUSER_CANCELEDNo action needed
Purchase errorOther BillingResponseCodeError message displayed
Backend verification failedverifyAndroidPurchase() β†’ NetworkExceptionError message, can retry
Network error during verifyIOException"Network error verifying purchase"

3.4 Subscription Management​

The SubscriptionManagementScreen (route: subscription_management) displays the current subscription status:

  • Active subscription: StatusCard shows plan details, provider badge (Google Play / Apple / Stripe), and renewal date. LicenseUsageCard shows active/allowed/available device slots with a progress bar. ActionsCard offers Modify Plan (opens plan selector with current values) and Cancel Subscription (opens Google Play subscriptions page).
  • No subscription: Shows NoSubscriptionContent with a button to navigate to plan selection.

Restore purchases: The RestorePurchasesButton calls BillingManager.restorePurchases() β†’ queryPurchases() to recover purchases after app reinstall.

3.5 Navigation Routes​


4. Sharing Flow​

The sharing flow enables device owners to invite other users to view their GPS trackers, and lets invitees accept or decline access.

Key files:

LayerFiles
ScreensSharingScreen, InvitesListScreen
ViewModelsSharingViewModel, InvitesListViewModel
Use CasesShareDeviceUseCase, GetDeviceSharesUseCase, RevokeShareUseCase, LeaveDeviceUseCase, GetMyInvitesUseCase, AcceptInviteUseCase, CancelInviteUseCase
DataSharingRepositoryImpl, SharingApi, SharingDataStore, InviteDataStore

Cross-reference: Data Layer β†’ Sharing Β· Real-Time (WebSocket invite events)

4.1 Flow Diagram​

4.2 Sending an Invite (Device Owner)​

Permissions model (SharePermissions):

PermissionDefaultDescription
positiontrueView device location
eventstrueView device events
geofencestrueView geofences
notificationstrueReceive notifications
commandsfalseSend commands to device

Step-by-step:

  1. Owner navigates to SharingScreen for a device (route: sharing/{deviceId}/{deviceName}/{isOwner}).
  2. SharingIntent.SetDeviceId(id) triggers loadShares() β†’ fetches active shares and pending invites via GetDeviceSharesUseCase β†’ GET /api/sharing/devices/{id}/shares.
  3. Owner taps the invite button β†’ SharingIntent.ShowInviteSheet β†’ bottom sheet slides up.
  4. Owner enters invitee email and toggles permissions via UpdateInviteEmail and UpdatePerm* intents.
  5. Owner taps "Send" β†’ SharingIntent.SendInvite:
    • Validates email format (must contain @).
    • Builds SharePermissions from toggle states.
    • Calls ShareDeviceUseCase(deviceId, email, permissions) β†’ POST /api/sharing/devices/{id}/share.
  6. On success β†’ snackbar confirmation, invite appears in pendingInvites list, bottom sheet closes.

4.3 Accepting / Declining an Invite​

  1. Invitee navigates to InvitesListScreen (route: invites, accessible from Settings).
  2. InvitesListIntent.LoadInvites β†’ GetMyInvitesUseCase() β†’ GET /api/sharing/invites.
  3. Each InviteCard shows device model, inviter name, and permission chips.

Accept:

  1. User taps "Accept" β†’ InvitesListIntent.AcceptInvite(invite).
  2. Sets processingInviteToken to show spinner on that card.
  3. AcceptInviteUseCase(token) β†’ POST /api/sharing/accept.
  4. On success β†’ invite removed from InviteDataStore, device appears in user's device list, success message shown.

Decline:

  1. User taps "Decline" β†’ InvitesListIntent.DeclineInvite(invite).
  2. CancelInviteUseCase(token) β†’ DELETE /api/sharing/invites/{token}.
  3. On success β†’ invite removed from InviteDataStore.

4.4 Revoking Access / Leaving a Device​

Revoke (owner):

  1. Owner taps revoke icon on a ShareCard β†’ SharingIntent.ShowRevokeConfirmation(share).
  2. Confirmation dialog appears.
  3. Owner confirms β†’ SharingIntent.ConfirmRevoke β†’ RevokeShareUseCase(deviceId, userId) β†’ DELETE /api/sharing/devices/{id}/share/{userId}.
  4. On success β†’ share removed from SharingDataStore, snackbar shown.

Leave (non-owner):

  1. Non-owner user sees "Leave device" in SharingScreen.
  2. Taps leave β†’ SharingIntent.LeaveDevice β†’ LeaveDeviceUseCase(deviceId) β†’ DELETE /api/sharing/devices/{id}/leave.
  3. On success β†’ SharingEvent.Left emitted β†’ navigates back, device removed from user's list.

4.5 Real-Time Updates​

Both SharingDataStore and InviteDataStore support WebSocket-driven updates:

  • New invite received: InviteDataStore.addInvite(ShareInviteReceivedEvent) β†’ invite appears in list without polling.
  • Invite accepted (owner side): SharingDataStore.onShareAccepted(ShareAcceptedEvent) β†’ pending invite moves to active shares.

4.6 Error States​

ScenarioExceptionRecovery
Invalid emailInvalidEmailException or ValidationExceptionError in snackbar, fix email
Blank invite tokenValidationExceptionShould not occur (token from API)
Network errorNetworkExceptionError message, retry
Permission deniedHTTP 403Error message
Invite expiredHTTP 404/410Error message, invite removed

5. Geofence Creation Flow​

The geofence flow provides a two-phase editor: draw a shape on the map (circle or polygon), then configure details (name, alerts). Geofences can be created, edited, and deleted.

Key files:

LayerFiles
ScreensGeofenceEditorScreen, GeofencesListScreen
ViewModelsGeofenceEditorViewModel, GeofencesListViewModel
ComponentsDrawShapePhase (GeofenceMapPhase.kt), EnterDetailsPhase (GeofenceDetailsForm.kt), ShapeTypeButton
Use CasesCreateGeofenceUseCase, UpdateGeofenceUseCase, DeleteGeofenceUseCase, GetGeofenceByIdUseCase, FetchGeofencesUseCase
DataGeofenceRepositoryImpl, GeofenceApi, GeofenceDataStore
GeometryWktBuilder, GeoConstants

Cross-reference: Data Layer β†’ Geofences Β· Navigation

5.1 Flow Diagram​

5.2 Phase 1 β€” Drawing the Shape​

The DrawShapePhase component renders a full-screen Mapbox map with shape-drawing controls.

Circle mode:

  1. User taps the map β†’ GeofenceEditorIntent.SetCircleCenter(lat, lng).
  2. A circle preview is rendered using generateCirclePoints() β€” 64 points computed via spherical geometry around the center.
  3. User adjusts radius with a slider (range: 50–5,000 meters) β†’ GeofenceEditorIntent.UpdateRadius(radius).

Polygon mode:

  1. User taps the map to add vertices β†’ GeofenceEditorIntent.AddPolygonPoint(lat, lng).
  2. Lines connect the points, each numbered sequentially (1, 2, 3...).
  3. Undo removes the last point β†’ RemoveLastPolygonPoint.
  4. Clear removes all points β†’ ClearPolygonPoints.
  5. Minimum 3 points required.

Confirm: User taps "Confirm" β†’ GeofenceEditorIntent.ConfirmShape:

  • Validates hasValidShape (circle: center set; polygon: β‰₯ 3 points).
  • Transitions phase from DRAW_SHAPE to ENTER_DETAILS.

The map also shows the device's current position (loaded via DeviceDataStore or PositionRepository fallback) and centers on it when creating a new geofence.

5.3 Phase 2 β€” Configuration & Save​

The EnterDetailsPhase component collects metadata:

  1. Name (required) β†’ GeofenceEditorIntent.UpdateName(name).
  2. Description (optional) β†’ GeofenceEditorIntent.UpdateDescription(desc).
  3. Alert on enter toggle β†’ GeofenceEditorIntent.UpdateAlertOnEnter(enabled).
  4. Alert on exit toggle β†’ GeofenceEditorIntent.UpdateAlertOnExit(enabled).

Save (create mode):

  1. User taps "Create" β†’ GeofenceEditorIntent.Save.
  2. ViewModel validates canSave (name non-empty + valid shape + correct phase).
  3. buildWktArea() converts the shape to WKT format via WktBuilder:
    • Circle: CIRCLE(lat lng, radiusMeters) β€” e.g., CIRCLE(45.0 9.0, 200).
    • Polygon: POLYGON((lng1 lat1, lng2 lat2, ...)) β€” auto-closes if needed.
  4. Constructs GeofenceCreateData(name, description, area, deviceId, attributes).
  5. CreateGeofenceUseCase(data) validates name and area format, then calls geofenceRepository.create().
  6. Repository calls POST /api/geofences, maps the response to a domain entity, and adds it to GeofenceDataStore.
  7. GeofenceEditorEvent.Saved emitted β†’ navigate back to list.
  8. The list auto-updates via StateFlow observation on GeofenceDataStore.geofences.

Save (edit mode):

  • Same flow but uses UpdateGeofenceUseCase(id, GeofenceUpdateData) β†’ PUT /api/geofences/{id}.
  • GeofenceDataStore.updateGeofence() updates the cached entity.

5.4 Edit Mode​

When editing an existing geofence (route includes ?id={geofenceId}):

  1. loadGeofence(id) calls GetGeofenceByIdUseCase β†’ populates all state fields.
  2. The editor opens directly in ENTER_DETAILS phase (shape already defined).
  3. User can tap "Back to drawing" β†’ GeofenceEditorIntent.BackToDrawing β†’ returns to DRAW_SHAPE with existing shape loaded.
  4. Save button shows "Save changes" instead of "Create".

5.5 Delete Flow​

  1. User taps delete on a GeofenceCard β†’ GeofencesListIntent.ShowDeleteConfirmation(geofence).
  2. Confirmation AlertDialog appears.
  3. User confirms β†’ GeofencesListIntent.ConfirmDelete:
    • geofenceRepository.delete(id) β†’ DELETE /api/geofences/{id}.
    • GeofenceDataStore.removeGeofence(id) updates the reactive cache.
  4. GeofencesListEvent.DeleteSuccess(name) β†’ snackbar shown.
  5. List auto-updates via StateFlow.

5.6 Geometry Constants​

MAX_LATITUDE       = 90.0
MAX_LONGITUDE = 180.0
MIN_POLYGON_POINTS = 3
EARTH_RADIUS_METERS = 6,371,000 (WGS 84)
POSITION_THRESHOLD = 0.001 (invalid coordinate detection)
Radius range = 50–5,000 meters (UI slider)
Circle resolution = 64 points

5.7 Error States​

ScenarioExceptionRecovery
Empty namecanSave = falseSave button disabled
No shape drawncanSave = falseSave button disabled, must confirm shape first
Polygon < 3 pointsConfirmShape validation failsError message, add more points
Invalid WKT areaInvalidGeofenceAreaExceptionError message displayed
Invalid coordinatesValidationExceptionError message
Geofence not found (edit)HTTP 404 β†’ GeofenceNotFoundExceptionError message, navigate back
Network errorNetworkExceptionError message, retry save
Delete failedNetworkExceptionError snackbar

5.8 Device Assignment​

Geofences are implicitly assigned to the device being viewed when created. The deviceId is passed as a navigation argument and included in the GeofenceCreateData. The backend links the geofence to that device. Geofence.deviceIds tracks all assigned devices β€” a single geofence can be associated with multiple devices.


Common Patterns Across Flows​

Intent β†’ ViewModel β†’ UseCase β†’ Repository​

Every flow follows the same layered architecture:

Error Handling Pipeline​

HTTP Error Mapping​

HTTP CodeDomain Exception
401InvalidCredentialsException or AuthenticationException
403AuthenticationException(serverDetail)
404Context-specific (DeviceNotFoundException, GeofenceNotFoundException)
409DeviceAlreadyClaimedException or NetworkException
422ValidationException
429RateLimitException
OtherNetworkException or ServerException(statusCode)

DataStore β†’ Reactive UI Updates​

All flows use DataStore classes (DeviceDataStore, GeofenceDataStore, SharingDataStore, InviteDataStore) as the single source of truth. These expose StateFlow properties that ViewModels observe. When any operation (create, update, delete) modifies the DataStore, all observing screens update automatically β€” no manual refresh is needed.

See Data Layer β†’ DataStores for the complete DataStore architecture.