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 newUiStateor one-shotEventobjects. 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:
| Layer | Files |
|---|---|
| Screens | LoginScreen, RegisterScreen, VerificationCodeScreen, TwoFactorScreen, ForgotPasswordScreen, ResetCodeScreen, ResetPasswordScreen |
| ViewModels | LoginViewModel, RegisterViewModel, VerificationViewModel, TwoFactorViewModel, ForgotPasswordViewModel, ResetCodeViewModel, ResetPasswordViewModel, AuthNavigationViewModel |
| Use Cases | LoginUseCase, LoginWithGoogleUseCase, LoginWithFacebookUseCase, Login2FAUseCase, RegisterUseCase, RequestPasswordResetUseCase, VerifyResetCodeUseCase, ResetPasswordUseCase |
| Data | AuthRepositoryImpl, TokenManager, GoogleAuthManager, FacebookAuthManager |
Cross-reference: Authentication & Security Β· Data Layer β Auth
1.1 Email Loginβ
Step-by-step:
- User enters email and password on
LoginScreen. - Dispatches
LoginIntent.Login. LoginViewModel.login()setsisLoading = trueand callsLoginUseCase.- The use case calls
authRepository.login(email, password). - On success β tokens are saved via
TokenManager.saveTokens(),LoginEvent.Successis emitted, and navigation proceeds to the main screen.
Decision points:
| Condition | Branch |
|---|---|
| 2FA required | Server returns TwoFactorRequiredException with tempToken β sets showTwoFactor = true, renders inline TwoFactorScreen (see Β§1.4) |
| Email not verified | Response message contains "verify your email" β emits LoginEvent.NeedVerification(email) β navigates to VerificationCodeScreen |
| Terms not accepted | Social login only β emits LoginEvent.NeedTermsAcceptance(name) β shows terms dialog |
Error states:
| Error | Exception | Recovery |
|---|---|---|
| Wrong credentials | InvalidCredentialsException | Error message displayed, user retries |
| Network failure | NetworkException | Error message, user retries |
| Rate limited | RateLimitException | Error message, wait and retry |
| Server error | ServerException(statusCode) | Error message displayed |
1.2 Social Login (Google / Facebook)β
Both social login paths converge to the same post-login handling.
Google:
- User taps Sign in with Google β dispatches
LoginIntent.LoginWithGoogle(activity). GoogleAuthManager.signIn(activity)uses the AndroidX Credentials API to obtain a Google ID token.LoginWithGoogleUseCase(idToken)callsauthRepository.loginWithGoogle(idToken).- Backend validates the token, returns access/refresh tokens.
- If
termsAccepted == false, emitsLoginEvent.NeedTermsAcceptance(name). - Otherwise, saves tokens and navigates to main.
Facebook:
- User taps Sign in with Facebook β dispatches
LoginIntent.LoginWithFacebook(activity). FacebookAuthManager.login()opens the Facebook login dialog, requesting["email", "public_profile"]permissions.- On callback success, the access token is passed to
LoginWithFacebookUseCase(accessToken). - 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:
- User fills in name, email, password, and confirm password. Checks terms checkbox.
- Client-side validation: name 2β250 chars, valid email format, password β₯ 8 chars, passwords match, terms accepted.
- Dispatches
RegisterIntent.RegisterβRegisterUseCasecallsauthRepository.register(). - On success β emits
RegisterEvent.Success(email)β navigates toVerificationCodeScreen.
Error states:
| Error | Exception | Recovery |
|---|---|---|
| Email already exists | ValidationException (422) | Show error, user changes email |
| Weak password | WeakPasswordException | Show error, user strengthens password |
| Network failure | NetworkException | Show error, retry |
1.4 Two-Factor Authentication (TOTP)β
When login returns TwoFactorRequiredException, the login screen overlays an inline TwoFactorScreen:
LoginViewModelsetsshowTwoFactor = trueand stores thetempToken.- User enters a 6-digit TOTP code from their authenticator app.
- Dispatches
LoginIntent.Verify2FA(code)βLogin2FAUseCase(tempToken, code). - Backend validates the code against the user's TOTP secret.
- On success β tokens saved,
LoginEvent.Successemitted, navigate to main. - On failure β
Invalid2FACodeExceptionβ clears code input, shows error. User retries. - User can cancel via
LoginIntent.CancelTwoFactorβ returns to login form.
2FA setup (from Settings) uses
TwoFactorSetupViewModelwith a multi-step flow:LOADING β SHOW_QR β ENTER_CODE β SUCCESS. The QR code is generated via ZXing'sQRCodeWriterfrom theotpauthUrl. 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:
- User enters email on
ForgotPasswordScreen. ForgotPasswordIntent.RequestResetβRequestPasswordResetUseCase.- Backend sends a 6-digit code to the email.
- On success β emits
ForgotPasswordEvent.NavigateToResetCode(email).
Step 2 β Enter code:
- User enters the 6-digit code on
ResetCodeScreen. Auto-submits when 6 digits are entered. ResetCodeIntent.VerifyβVerifyResetCodeUseCase(email, code).- On success β emits
ResetCodeEvent.Success(email, code)β navigates toResetPasswordScreen.
Step 3 β Set new password:
- User enters new password + confirmation on
ResetPasswordScreen. - Validation: password β₯ 8 chars, passwords match.
ResetPasswordIntent.SubmitβResetPasswordUseCase(email, code, password).- On success β emits
ResetPasswordEvent.Successβ navigates back toLoginScreen.
1.6 Auth State Machineβ
AuthNavigationViewModel manages the overall auth navigation state:
Deep link handling:
HandleVerificationToken(token)β callsauthRepository.verifyEmailByToken(token)β saves tokens β navigates to Main.HandleResetToken(token)β callsauthRepository.resolveResetToken(token)β navigates toResetPasswordScreen(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:
| Layer | Files |
|---|---|
| Screens | AddDeviceScreen, DevicesListScreen, DeviceDetailScreen |
| ViewModels | AddDeviceViewModel, DevicesViewModel |
| Use Cases | ClaimDeviceUseCase, UnclaimDeviceUseCase, DevicesInteractor, GetLicenseStatusUseCase |
| Data | DeviceRepositoryImpl, DeviceApi, DeviceDataStore |
Cross-reference: Data Layer β Devices Β· Billing (license checks)
2.1 Flow Diagramβ
2.2 Step-by-Stepβ
-
License check β Before showing the add-device UI,
DevicesViewModeldispatchesCheckLicensewhich callsGetLicenseStatusUseCase. Iflicense.available == 0, an upgrade sheet modal is shown instead. -
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. -
Claim request β
AddDeviceIntent.ClaimDevicetriggersclaimDevice()in the ViewModel:- Validates token is non-empty.
- Re-checks license availability.
- Calls
ClaimDeviceUseCase(token)βdeviceRepository.claimDevice(token)βPOST /api/devices/claim.
-
API validation β The backend validates the token, checks it hasn't been claimed, and returns the device object.
-
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 todevice_detail/{deviceId}.
2.3 Error Statesβ
| Scenario | Exception | User Experience |
|---|---|---|
| Empty token | Early return in ViewModel | Error message set |
| Token too short (< 6 chars) | InvalidClaimTokenException | "Token must be at least 6 characters" |
| Token not found | HTTP 404 β DeviceNotFoundException | Error displayed, user retries |
| Already claimed | HTTP 409 β DeviceAlreadyClaimedException | Error displayed |
| No license slots | Manual check before API call | Upgrade sheet modal shown |
| Network error | NetworkException | "Network error claiming device" |
| Session expired | HTTP 401 β AuthenticationException | Global 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:
| Layer | Files |
|---|---|
| Screens | SubscriptionScreen, SubscriptionManagementScreen |
| ViewModels | SubscriptionManagementViewModel |
| Billing | BillingManager (implements IBillingManager, PurchasesUpdatedListener) |
| Use Cases | GetLicenseStatusUseCase, VerifyAndroidPurchaseUseCase, DeviceLicenseInteractor |
| Data | BillingRepositoryImpl, BillingApi, SubscriptionRepositoryImpl |
| Components | DeviceSelectorCard, 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β
-
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 patternsub_{period}_{devices}(e.g.,sub_annual_5). -
Product loading β
BillingManager.loadProducts()queries all 30 product combinations from Google Play. Prices are displayed in the user's local currency viaProductDetails. -
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.
-
Purchase initiation β User taps the purchase button β
BillingManager.purchase(activity, productDetails, userId):- Gets
offerTokenfromsubscriptionOfferDetails[0]. - Sets
obfuscatedAccountIdto the user ID for backend correlation. - Calls
BillingClient.launchBillingFlow()β Google Play system dialog appears.
- Gets
-
Purchase callback β
onPurchasesUpdated(billingResult, purchases):BillingResponseCode.OKβ proceeds tohandlePurchase().USER_CANCELEDβ logged, no error shown.- Other codes β error message displayed.
-
Backend verification β
handlePurchase()calls theonPurchaseVerifiedcallback which triggersBillingRepositoryImpl.verifyAndroidPurchase():- Sends
purchaseTokenandproductIdtoPOST /api/billing/purchases/android/verify. - Backend validates the Google Play receipt signature and creates/updates the subscription record.
- Sends
-
Acknowledgement β
BillingManager.acknowledgePurchase()confirms the purchase with Google Play. Purchases must be acknowledged within 3 days or they are automatically refunded. -
License activation β After verification,
SubscriptionManagementViewModel.loadSubscriptionStatus()fetches the updated license fromGET /api/devices/license/status. TheLicenseentity reflects the newalloweddevice count, and the user can now claim devices up to that limit.
3.3 Error Statesβ
| Scenario | Source | Recovery |
|---|---|---|
| Play Store connection failed | BillingManager.setupBillingClient() | Retry connection |
| Products won't load | loadProducts() β HttpException | Error message, retry |
| Offer token missing | purchase() validation | ValidationException shown |
| Purchase cancelled | USER_CANCELED | No action needed |
| Purchase error | Other BillingResponseCode | Error message displayed |
| Backend verification failed | verifyAndroidPurchase() β NetworkException | Error message, can retry |
| Network error during verify | IOException | "Network error verifying purchase" |
3.4 Subscription Managementβ
The SubscriptionManagementScreen (route: subscription_management) displays the current subscription status:
- Active subscription:
StatusCardshows plan details, provider badge (Google Play / Apple / Stripe), and renewal date.LicenseUsageCardshows active/allowed/available device slots with a progress bar.ActionsCardoffers Modify Plan (opens plan selector with current values) and Cancel Subscription (opens Google Play subscriptions page). - No subscription: Shows
NoSubscriptionContentwith 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:
| Layer | Files |
|---|---|
| Screens | SharingScreen, InvitesListScreen |
| ViewModels | SharingViewModel, InvitesListViewModel |
| Use Cases | ShareDeviceUseCase, GetDeviceSharesUseCase, RevokeShareUseCase, LeaveDeviceUseCase, GetMyInvitesUseCase, AcceptInviteUseCase, CancelInviteUseCase |
| Data | SharingRepositoryImpl, 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):
| Permission | Default | Description |
|---|---|---|
position | true | View device location |
events | true | View device events |
geofences | true | View geofences |
notifications | true | Receive notifications |
commands | false | Send commands to device |
Step-by-step:
- Owner navigates to
SharingScreenfor a device (route:sharing/{deviceId}/{deviceName}/{isOwner}). SharingIntent.SetDeviceId(id)triggersloadShares()β fetches active shares and pending invites viaGetDeviceSharesUseCaseβGET /api/sharing/devices/{id}/shares.- Owner taps the invite button β
SharingIntent.ShowInviteSheetβ bottom sheet slides up. - Owner enters invitee email and toggles permissions via
UpdateInviteEmailandUpdatePerm*intents. - Owner taps "Send" β
SharingIntent.SendInvite:- Validates email format (must contain
@). - Builds
SharePermissionsfrom toggle states. - Calls
ShareDeviceUseCase(deviceId, email, permissions)βPOST /api/sharing/devices/{id}/share.
- Validates email format (must contain
- On success β snackbar confirmation, invite appears in
pendingInviteslist, bottom sheet closes.
4.3 Accepting / Declining an Inviteβ
- Invitee navigates to
InvitesListScreen(route:invites, accessible from Settings). InvitesListIntent.LoadInvitesβGetMyInvitesUseCase()βGET /api/sharing/invites.- Each
InviteCardshows device model, inviter name, and permission chips.
Accept:
- User taps "Accept" β
InvitesListIntent.AcceptInvite(invite). - Sets
processingInviteTokento show spinner on that card. AcceptInviteUseCase(token)βPOST /api/sharing/accept.- On success β invite removed from
InviteDataStore, device appears in user's device list, success message shown.
Decline:
- User taps "Decline" β
InvitesListIntent.DeclineInvite(invite). CancelInviteUseCase(token)βDELETE /api/sharing/invites/{token}.- On success β invite removed from
InviteDataStore.
4.4 Revoking Access / Leaving a Deviceβ
Revoke (owner):
- Owner taps revoke icon on a
ShareCardβSharingIntent.ShowRevokeConfirmation(share). - Confirmation dialog appears.
- Owner confirms β
SharingIntent.ConfirmRevokeβRevokeShareUseCase(deviceId, userId)βDELETE /api/sharing/devices/{id}/share/{userId}. - On success β share removed from
SharingDataStore, snackbar shown.
Leave (non-owner):
- Non-owner user sees "Leave device" in
SharingScreen. - Taps leave β
SharingIntent.LeaveDeviceβLeaveDeviceUseCase(deviceId)βDELETE /api/sharing/devices/{id}/leave. - On success β
SharingEvent.Leftemitted β 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β
| Scenario | Exception | Recovery |
|---|---|---|
| Invalid email | InvalidEmailException or ValidationException | Error in snackbar, fix email |
| Blank invite token | ValidationException | Should not occur (token from API) |
| Network error | NetworkException | Error message, retry |
| Permission denied | HTTP 403 | Error message |
| Invite expired | HTTP 404/410 | Error 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:
| Layer | Files |
|---|---|
| Screens | GeofenceEditorScreen, GeofencesListScreen |
| ViewModels | GeofenceEditorViewModel, GeofencesListViewModel |
| Components | DrawShapePhase (GeofenceMapPhase.kt), EnterDetailsPhase (GeofenceDetailsForm.kt), ShapeTypeButton |
| Use Cases | CreateGeofenceUseCase, UpdateGeofenceUseCase, DeleteGeofenceUseCase, GetGeofenceByIdUseCase, FetchGeofencesUseCase |
| Data | GeofenceRepositoryImpl, GeofenceApi, GeofenceDataStore |
| Geometry | WktBuilder, 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:
- User taps the map β
GeofenceEditorIntent.SetCircleCenter(lat, lng). - A circle preview is rendered using
generateCirclePoints()β 64 points computed via spherical geometry around the center. - User adjusts radius with a slider (range: 50β5,000 meters) β
GeofenceEditorIntent.UpdateRadius(radius).
Polygon mode:
- User taps the map to add vertices β
GeofenceEditorIntent.AddPolygonPoint(lat, lng). - Lines connect the points, each numbered sequentially (1, 2, 3...).
- Undo removes the last point β
RemoveLastPolygonPoint. - Clear removes all points β
ClearPolygonPoints. - Minimum 3 points required.
Confirm: User taps "Confirm" β GeofenceEditorIntent.ConfirmShape:
- Validates
hasValidShape(circle: center set; polygon: β₯ 3 points). - Transitions
phasefromDRAW_SHAPEtoENTER_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:
- Name (required) β
GeofenceEditorIntent.UpdateName(name). - Description (optional) β
GeofenceEditorIntent.UpdateDescription(desc). - Alert on enter toggle β
GeofenceEditorIntent.UpdateAlertOnEnter(enabled). - Alert on exit toggle β
GeofenceEditorIntent.UpdateAlertOnExit(enabled).
Save (create mode):
- User taps "Create" β
GeofenceEditorIntent.Save. - ViewModel validates
canSave(name non-empty + valid shape + correct phase). buildWktArea()converts the shape to WKT format viaWktBuilder:- Circle:
CIRCLE(lat lng, radiusMeters)β e.g.,CIRCLE(45.0 9.0, 200). - Polygon:
POLYGON((lng1 lat1, lng2 lat2, ...))β auto-closes if needed.
- Circle:
- Constructs
GeofenceCreateData(name, description, area, deviceId, attributes). CreateGeofenceUseCase(data)validates name and area format, then callsgeofenceRepository.create().- Repository calls
POST /api/geofences, maps the response to a domain entity, and adds it toGeofenceDataStore. GeofenceEditorEvent.Savedemitted β navigate back to list.- The list auto-updates via
StateFlowobservation onGeofenceDataStore.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}):
loadGeofence(id)callsGetGeofenceByIdUseCaseβ populates all state fields.- The editor opens directly in
ENTER_DETAILSphase (shape already defined). - User can tap "Back to drawing" β
GeofenceEditorIntent.BackToDrawingβ returns toDRAW_SHAPEwith existing shape loaded. - Save button shows "Save changes" instead of "Create".
5.5 Delete Flowβ
- User taps delete on a
GeofenceCardβGeofencesListIntent.ShowDeleteConfirmation(geofence). - Confirmation
AlertDialogappears. - User confirms β
GeofencesListIntent.ConfirmDelete:geofenceRepository.delete(id)βDELETE /api/geofences/{id}.GeofenceDataStore.removeGeofence(id)updates the reactive cache.
GeofencesListEvent.DeleteSuccess(name)β snackbar shown.- 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β
| Scenario | Exception | Recovery |
|---|---|---|
| Empty name | canSave = false | Save button disabled |
| No shape drawn | canSave = false | Save button disabled, must confirm shape first |
| Polygon < 3 points | ConfirmShape validation fails | Error message, add more points |
| Invalid WKT area | InvalidGeofenceAreaException | Error message displayed |
| Invalid coordinates | ValidationException | Error message |
| Geofence not found (edit) | HTTP 404 β GeofenceNotFoundException | Error message, navigate back |
| Network error | NetworkException | Error message, retry save |
| Delete failed | NetworkException | Error 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 Code | Domain Exception |
|---|---|
| 401 | InvalidCredentialsException or AuthenticationException |
| 403 | AuthenticationException(serverDetail) |
| 404 | Context-specific (DeviceNotFoundException, GeofenceNotFoundException) |
| 409 | DeviceAlreadyClaimedException or NetworkException |
| 422 | ValidationException |
| 429 | RateLimitException |
| Other | NetworkException 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.