Android parity 1/3: forms come out right and complete - #33
Conversation
…etting lost Correctness fixes from the iOS parity survey (designs/future/android-parity.md §4 1a): - The first crew member is sent as "Pilot", as iOS does; "PIC" was being printed on the gendec and LSGS forms. - A local flight shows both its arrival and its departure forms. Only web forms are filtered by direction; document forms show on both sides. - The flight editor edits a draft: Save stores flight, crew and passengers together, Back with changes asks Save / Discard, and + no longer leaves a "???? -> ????" row when abandoned. - The OAuth state nonce survives process death (10-minute TTL). A 401 clears the token and returns to sign-in, and Settings offers sign-in after skipping it. - Web forms: Back walks the page history first; cookies, storage and cache are cleared on the way out. - Generated forms and exports are cleared on start and, 15 minutes after a share, on returning to the app. - Move my data keeps trip extra fields; aircraft chips scroll, people chips wrap, and the aircraft editor scrolls above the keyboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pXA3ei7MugJjuxWmZ12Y2
- Settings > Delete account calls DELETE /auth/account after a confirm, then signs out. Local people, aircraft and flights are kept. Play requires the in-app deletion. - People, aircraft and flights delete with a swipe or from the edit screen's overflow menu, and offer Undo in a snackbar. Deletion stays a tombstone; Undo clears it and bumps updatedAt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pXA3ei7MugJjuxWmZ12Y2
- Nature, Reason for Visit and Responsible Person on the flight. The responsible person is the form's contact, keeps `contact` in step with their phone as iOS does, and fills the telephone / e-mail extras. - Connecting flight (the next or previous leg through this airport within 14 days) and return flight (has_return_flight) are sent to the forms that ask for them. The matching is FlightLegs in :core-logic, unit-tested. - Each form shows its own extra fields (choice, person, text) and sends them. An untouched choice sends the option it shows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pXA3ei7MugJjuxWmZ12Y2
…hts fold away - Create Return Flight, Create Next Leg and Duplicate Flight, as on iOS: the aircraft, people, nature, reason and responsible person carry over. The current flight is stored first; the new leg opens as an unsaved draft. - Moving the departure moves an arrival on the same UTC day with it, keeping its time of day. FlightLegs.arrivalFollowing, unit-tested. - Past flights sit under a collapsed "Past Flights" row, and every row shows the aircraft registration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pXA3ei7MugJjuxWmZ12Y2
- Email next to Generate: the form is generated while /email-text writes the covering text, then a mail app opens addressed to the form's email.to / send_to and cc, with the file attached. With no mail app it falls back to the share sheet. - Settings > Languages you speak. The body is in the airport's language when the pilot speaks it, English otherwise; stored as on iOS. - Each side lists its primary form first, then the airport's web forms, then the other forms folded under "Other forms". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pXA3ei7MugJjuxWmZ12Y2
… is recreated Skipping the redirect whenever there was saved state could drop a callback that arrives as the creating intent after process death. Remember the one already handled instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pXA3ei7MugJjuxWmZ12Y2
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 37454865 | Triggered | Generic Password | bf2af5b | app/android/app/src/androidTest/kotlin/aero/flyfun/forms/data/DataTransferTest.kt | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
…a literal GitGuardian flagged the literal as a generic password. It only ever encrypted an in-memory test database, so nothing needs rotating; this stops the new line tripping the scanner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pXA3ei7MugJjuxWmZ12Y2
|
Reviewed the diff against
/** Also keeps `contact` in step, as iOS `setResponsiblePerson` does for older builds reading it. */
fun setResponsiblePerson(person: PersonEntity?) = edit {
it.copy(
flight = it.flight.copy(responsiblePersonId = person?.id, contact = person?.phone),
responsiblePerson = person,
)
}Per 🤖 Generated with Claude Code |
Code reviewReviewed each commit (1a-1e, the sign-in redirect fix, and the test fix) against the intent logged in 1. Delete-account failure can silently sign the user out with no visible error (medium-high confidence)
suspend fun deleteAccount(): Result<Unit> = runCatching {
val response = api.auth.deleteAccount()
if (!response.isSuccessful) error("The server returned \${response.code()}. Your account was not deleted.")
tokens.clear()
}But if (response.code == 401 && token != null) tokens.clearIfCurrent(token)If the token happens to be expired/revoked when the pilot taps Delete account, the interceptor clears it first, Suggest checking 2. New-leg draft (Return/Next Leg/Duplicate) can be silently lost to process death (medium confidence)
private fun createLeg(shape: ...) = viewModelScope.launch {
save().join()
val current = _detail.value ?: return@launch
...
show(current.copy(flight = shape(from, common), isNew = true), unsaved = true)
}The screen's Worth confirming this is an accepted edge case, or navigating to the new draft's own route (e.g. Everything elseCrew-role rename to "Pilot", local-flight form-side filtering, the persisted OAuth |
…on had expired Follow-up to #33's review. A 401 from DELETE /auth/account is caught by the shared interceptor, which drops the token, so the sign-in screen replaced Settings before its error could show and the pilot never learnt the account was still there. AuthService now holds a sign-in notice that the sign-in screen shows until the next sign-in. Also from the review, in the parity tracker: PR 1 merged with its unit and instrumented tests run, the draft-vs-process-death limit accepted, and the responsible person's `contact` kept as iOS stores it (phone), with designs/ios-app.md corrected to match the code. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Closes #29. PR 1 of 3 in
designs/future/android-parity.md§4. One commit per section; the tracker's items are ticked and the decisions logged in §8 in the same commits.What changes
1a Correctness fixes
"Pilot"(was"PIC", printed on the gendec and LSGS forms).flight/new, which no longer leaves a "???? → ????" row when abandoned.statenonce survives process death (SharedPreferences, 10-minute TTL). A 401 to an authenticated request clears the token and returns to sign-in, and Settings offers sign-in after "Enter data without signing in".cacheDir/formsare cleared on cold start and, on returning to the app, once older than 15 minutes.1b Delete account, delete with Undo
DELETE /auth/account) after a confirm. Local data is kept.updatedAt.1c Flight fields
has_return_flight), found byFlightLegsin:core-logic.1d Leg actions
1e Email export
/email-textruns alongside generation, then a mail app opens (ACTION_SENDwith amailto:selector) with to/cc fromemail/send_to. With no mail app it falls back to the share sheet.Plus a follow-up fix so each sign-in redirect is handled exactly once however the activity is recreated.
Deliberate departures from iOS (logged in §8)
Verification: not complete, hence draft
The session that wrote this could not reach
dl.google.com(Google Maven + Android SDK), so./gradlew :app:…was never run and nothing was driven on the emulator. What was checked::core-logic:test: 116 tests pass, 36 new (FormSides,FlightLegs,EmailText,TripExtras).app/src/test(FormRequestBuilderTest,ApiTypesTest, 20 tests) passes on the JVM.app/src/main, exceptMainActivityandscan/, compiles against Compose Multiplatform 1.6 desktop plus stubs for the Android APIs.SwipeToDismissBox(onDismiss = …)is newer than that; it was confirmed present in material3 1.4 sources.Before marking ready:
./gradlew :app:testDebugUnitTest :core-logic:testand:app:connectedDebugAndroidTest🤖 Generated with Claude Code
https://claude.ai/code/session_011pXA3ei7MugJjuxWmZ12Y2
Generated by Claude Code