Skip to content

feat(onboarding): add invite claim and server-driven feature tour - #125

Merged
Quick104 merged 7 commits into
mainfrom
feat/invite-claim-and-onboarding
Jul 28, 2026
Merged

feat(onboarding): add invite claim and server-driven feature tour#125
Quick104 merged 7 commits into
mainfrom
feat/invite-claim-and-onboarding

Conversation

@Quick104

@Quick104 Quick104 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Android companion to Silo-Server/silo-server#501 (emailed invitations + server-driven onboarding).

  • Invite deep linksilo://invite?server=…&token=… (host registered in the manifest) opens InviteClaimScreen with the server URL and single-use token already bound, skipping the "which server?" step entirely. The invitee sets only a password; their email address becomes their username. On success the server is registered through the existing AuthRepository.setServerUrl path and tokens persist — identical post-conditions to a manual login. Expired/used/revoked links render an explanatory card, not an error toast.
  • Server-driven feature tourOnboardingTourScreen renders GET /onboarding/flow?surface=phone: one step per page (Aurora treatment), skip always reachable, unknown step kinds dropped at load per the forward-compat contract. Progress and completion post per profile, so finishing on the phone silences web and TV. setting_choice steps write through the existing profile-update path (quality_preference, subtitle_mode, …).
  • Gate — profile selection now routes Home entry through the tour screen, which checks /onboarding/state and immediately hands off to Home when the profile has already completed or skipped. Any state/flow error also skips rather than blocking first run.

Shared additions: InvitationModels, OnboardingModels, OnboardingApi, OnboardingRepository, invitation lookup/accept on AuthApi/AuthRepository, Koin wiring.

Out of scope (follow-up): androidTvApp surface=tv renderer; https App Links (need the server to host assetlinks.json — custom scheme works today).

Verification

$ ./gradlew :shared:compileDebugKotlinAndroid :androidApp:compileDebugKotlinAndroid
BUILD SUCCESSFUL
$ ./gradlew :shared:testDebugUnitTest :androidApp:testDebugUnitTest
BUILD SUCCESSFUL

Not yet exercised against a device/emulator — the server endpoints themselves were verified end-to-end in the server PR (browser + curl). Flagging that a manual emulator pass of the deep link (adb shell am start -a android.intent.action.VIEW -d "silo://invite?server=…&token=…") is the right pre-merge check.

AI Disclosure

  • Tool(s): Claude Code
  • Model(s): claude-fable-5
  • Involvement: fully AI-generated
  • Adversarial review: reviewed the diff before commit. Found and fixed: (1) the claim VM originally took a ServerRegistry directly and called a method that doesn't exist on it — replaced with the established AuthRepository.setServerUrl path which upserts + switches + restores token scope; (2) the tour screen initially used invented modifier helpers (clickableOption, border16) — replaced with real clickable/border; (3) UpdateProfileRequest is typed per field, so the manifest's string key maps through an explicit whitelist and unknown keys no-op instead of failing the tour. Known gap: no emulator run yet (stated above).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added emailed-invitation claim flow with silo://invite deep links (password setup + completion routing).
    • Added server-driven onboarding tour with step persistence, skip, and “done” handling (including onboarding settings).
  • Improvements
    • Standardized audio/subtitle/metadata language pickers using shared language-tag mappings, with legacy migration.
    • Added cleartext HTTP consent prompt during invitation claims.
    • Improved deep-link and start-route handling for invite-claim and onboarding completion.
  • Bug Fixes
    • Prevented onboarding from being bypassed after warm starts or server/profile switching.
  • Tests
    • Added language-tag mapping and migration tests.

Quick104 and others added 2 commits July 27, 2026 18:01
playback.audio_language and the profile's subtitle_language are BCP 47
language tags in the server's settings contract. The phone put the display
label on the wire verbatim — "English", not "en" — and the TV did the same
for audio while doing it correctly for subtitles.

That was already broken before the server started enforcing it: the same
string is handed to ExoPlayer as preferredAudioLanguage, and
setPreferredAudioLanguage("English") never matches a track tagged eng, so
choosing an audio language on Android has silently been a no-op. It also
meant Android and Apple wrote different vocabularies to the same key —
Apple has always sent codes, so a language picked on an iPhone read as
"Default" on the phone and vice versa.

Now that the server validates the tag, the flusher's PUT 400s and only
logs, so the setting would stop persisting entirely after a server upgrade.

Replaces the four drifted option lists with one table in shared, so a
language cannot be added to one surface and missed on the others, and
translates values already on devices on read rather than re-sending a
label the server will reject.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Companion to silo-server#501.

- silo://invite?server=…&token=… deep link opens InviteClaimScreen with
  the server and single-use token already bound — no "which server?"
  step. The invitee sets only a password; their email address is their
  username. On success the server is registered and tokens persist,
  identical post-conditions to a manual login.
- OnboardingTourScreen renders the server's /onboarding/flow manifest
  (surface=phone): one step per page, skip always reachable, unknown
  step kinds dropped at load (the forward-compat contract). Progress
  and completion post per profile, so finishing here silences web/TV.
  setting_choice steps write through the existing profile-update path.
- Profile selection now routes Home entry through the tour gate, which
  immediately hands off to Home when state is already done.

TV (androidTvApp) intentionally not covered here — the manifest
contract already carries surface=tv for a follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 3 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0819bcf6-dae6-403a-b482-bd5205c563f2

📥 Commits

Reviewing files that changed from the base of the PR and between 2ad5140 and 69eb935.

📒 Files selected for processing (2)
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/auth/InviteClaimViewModel.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/onboarding/OnboardingTourViewModel.kt
📝 Walkthrough

Walkthrough

This PR adds emailed-invitation claiming, server-driven onboarding tours with persisted progress, shared BCP 47 language handling across Android and TV, and configurable Aurora layout behavior.

Changes

Invitation claim flow

Layer / File(s) Summary
Invitation contracts and API
shared/src/commonMain/kotlin/org/siloserver/silo/model/auth/InvitationModels.kt, shared/src/commonMain/kotlin/org/siloserver/silo/network/api/AuthApi.kt, shared/src/commonMain/kotlin/org/siloserver/silo/repository/AuthRepository.kt
Adds invitation lookup and acceptance models/endpoints, session persistence, and post-acceptance server/profile state updates.
Invitation deep-link routing
androidApp/src/androidMain/AndroidManifest.xml, androidApp/src/androidMain/kotlin/org/siloserver/silo/android/..., androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/navigation/*
Parses silo://invite links into typed claim routes and permits the destination before authentication.
Invitation lookup and claim UI
androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/auth/InviteClaim*
Handles lookup, retries, invalid invitations, cleartext consent, password validation, submission, errors, and completion.
Invitation dependency wiring
androidApp/src/androidMain/kotlin/org/siloserver/silo/android/di/AndroidModule.kt
Registers the invitation ViewModel and required Android dependencies.

Server-driven onboarding tour

Layer / File(s) Summary
Onboarding contracts and repository
shared/src/commonMain/kotlin/org/siloserver/silo/model/onboarding/*, shared/src/commonMain/kotlin/org/siloserver/silo/network/api/OnboardingApi.kt, shared/src/commonMain/kotlin/org/siloserver/silo/repository/OnboardingRepository.kt, shared/src/commonMain/kotlin/org/siloserver/silo/di/*
Adds serializable flow/state/progress models, API methods, repository operations, and Koin bindings.
Onboarding progress and setting persistence
androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/onboarding/OnboardingTourViewModel.kt, OnboardingTourLocalCache.kt
Loads and filters tour steps, resumes progress, persists choices, records progress, and handles completion or skipping.
Onboarding screen and navigation gate
androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/onboarding/OnboardingTourScreen.kt, androidApp/src/androidMain/kotlin/org/siloserver/silo/android/MainActivity.kt, androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/navigation/AppNavigation.kt
Adds the pager-based tour UI and routes startup, server switching, and profile selection through the onboarding gate.

Shared language settings

Layer / File(s) Summary
Language mapping and migration
shared/src/commonMain/kotlin/org/siloserver/silo/model/settings/LanguageOptions.kt, shared/src/commonTest/kotlin/org/siloserver/silo/model/settings/LanguageOptionsTest.kt
Centralizes wire tags, labels, unset handling, legacy migration, and conversion tests.
Android language settings integration
android-shared/src/androidMain/kotlin/org/siloserver/silo/common/settings/AndroidPlayerSettingsStore.kt, androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/settings/*
Android storage, ViewModel state, and dropdowns use migrated wire values and shared mappings.
TV language settings integration
androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/settings/*
TV language options and profile initialization use shared mapping and migration logic.

Aurora layout configuration

Layer / File(s) Summary
Aurora modifier and screen layout
androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/components/aurora/AuroraChrome.kt, AuroraScreen.kt
Glass elevation is configurable, and AuroraScreen supports optional scrolling, configurable horizontal padding, and safe drawing insets.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

Invitation claim

sequenceDiagram
  participant User
  participant MainActivity
  participant InviteClaimScreen
  participant AuthRepository
  participant AuthApi
  User->>MainActivity: Open silo://invite link
  MainActivity->>InviteClaimScreen: Navigate with server and token
  InviteClaimScreen->>AuthRepository: Lookup invitation
  AuthRepository->>AuthApi: GET invitation
  AuthApi-->>InviteClaimScreen: Invitation details
  User->>InviteClaimScreen: Submit password
  InviteClaimScreen->>AuthRepository: Accept invitation
  AuthRepository->>AuthApi: POST acceptance
  AuthRepository-->>InviteClaimScreen: Successful login response
Loading

Onboarding tour

sequenceDiagram
  participant MainActivity
  participant OnboardingTourScreen
  participant OnboardingTourViewModel
  participant OnboardingRepository
  participant OnboardingApi
  MainActivity->>OnboardingTourScreen: Navigate when tour is incomplete
  OnboardingTourScreen->>OnboardingTourViewModel: load()
  OnboardingTourViewModel->>OnboardingRepository: Fetch state and flow
  OnboardingRepository->>OnboardingApi: GET onboarding endpoints
  OnboardingApi-->>OnboardingTourViewModel: Flow and progress
  OnboardingTourScreen->>OnboardingTourViewModel: Advance, skip, or finish
  OnboardingTourViewModel->>OnboardingRepository: Post progress
Loading

Possibly related PRs

  • Silo-Server/silo-android#119: Updates the same Android and TV language settings paths with shared LanguageOptions conversion and legacy migration.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: invite claiming and a server-driven onboarding tour.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/invite-claim-and-onboarding

Comment @coderabbitai help to get the list of available commands.

- Route warm-start invite deep links and keep pre-auth claim navigation
- Distinguish unreachable server from dead invite, with retry
- Cache tour completion per server+profile and gate warm starts on it
- Persist tour setting choices on advance; survive navigation with NonCancellable
- Migrate legacy language display labels to wire codes on phone and TV
@Quick104
Quick104 marked this pull request as ready for review July 28, 2026 17:29

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 56f385b1ff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +89 to +92
client.post("${serverUrl.trimEnd('/')}/api/v1/invitations/$token/accept") {
skipSiloAuth()
contentType(ContentType.Application.Json)
setBody(AcceptInvitationRequest(password = password))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add cleartext consent before accepting HTTP invites

When an invite targets an unapproved http:// origin, the lookup GET succeeds because read-only skipSiloAuth requests may bypass consent, but this credential-bearing POST is rejected by AuthInterceptorImpl with CleartextOriginNotApprovedException. The exception becomes a generic NetworkError, and the invite screen provides no consent prompt, so first-time invitees on HTTP/LAN servers cannot claim their accounts unless they previously approved the origin through an unrelated setup flow.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed in bb6b902. InviteClaimViewModel checks cleartextConsentStore.requiresApproval(serverUrl) before the claim POST and surfaces pendingCleartextOrigin, so the screen collects the same consent server setup does, then retries.

Comment on lines +124 to +127
if (result is ApiResult.Success) {
setServerUrl(serverUrl)
}
return persistSession(result)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Clear stale profile scope before storing the claimed account

When the invite targets a server that is already registered with a signed-in account, setServerUrl() reloads that server slot's existing profile ID and profile token, while persistSession() overwrites only the access and refresh tokens. The newly claimed account therefore reaches profile selection with headers for the previous account's profile still attached by AuthInterceptorImpl, which can make subsequent profile requests fail or operate with mismatched identity state; clear the target slot's profile state before saving the new session.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed in bb6b902. acceptInvitation clears the target slot's profile id and profile token (and the registry's) after a successful claim, before persistSession, so the new account doesn't inherit the previous one's profile headers.

if (persistCurrentChoice) {
persistChoiceIfAny(current.steps.getOrNull(current.currentIndex))
}
markDoneLocally()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cache completion only after the server accepts it

If connectivity drops when the user taps Done or Skip, this marks the profile permanently done locally before posting progress, and the POST result is ignored. Future launches then short-circuit without consulting the server, so the completion is never retried and other clients continue showing the tour despite this client claiming that completion is synchronized per profile; only mark the cache after a successful response or retain a retryable pending completion.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed in bb6b902. finish() posts inside withContext(NonCancellable) and only calls localCache.markDone when the result is ApiResult.Success, so a lost POST leaves the cache unset and the next launch re-consults the server.

val flowDeferred = async { onboardingRepository.getFlow(surface = "phone") }
when (val state = stateDeferred.await()) {
is ApiResult.Success -> {
if (state.data.done) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resume from the server-reported onboarding step

When a profile has unfinished progress from another device or a process death, the successful state response's lastStep is ignored and applyFlow() always initializes currentIndex to zero. Although every advance posts the newly displayed step, reopening the tour restarts it from the beginning instead of restoring that progress; carry the returned state into flow application and select the matching step.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed in bb6b902. The state response's lastStep is carried into applyFlow as resumeStep and resolved to startIndex, so the tour resumes at the recorded step. Verified on device: the tour opened mid-flow rather than at step one.

Comment on lines +56 to +57
is ApiResult.Error -> _uiState.update {
it.copy(isLoadingInvitation = false, invitationInvalid = true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Offer retry for transient invitation lookup errors

Any HTTP error is treated as proof that the invitation is expired, including transient server failures such as 429, 500, or 503. In those cases the screen shows the permanent-invalid card with no retry action even though the token was never rejected; only terminal invitation statuses should set invitationInvalid, while retryable HTTP responses should use the same retry state as network failures.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed in bb6b902. Only TERMINAL_LOOKUP_CODES (400/404/409/410) set invitationInvalid; other statuses set lookupFailed, which offers retry via onRetryLookup.

"quality_preference" -> UpdateProfileRequest(qualityPreference = value)
"subtitle_language" -> UpdateProfileRequest(subtitleLanguage = value)
"subtitle_mode" -> UpdateProfileRequest(subtitleMode = value)
"auto_skip_intro" -> UpdateProfileRequest(autoSkipIntro = value.toBoolean())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mirror the intro-skip choice into player settings

For an auto_skip_intro tour step, this updates only the profile field, while Android playback reads PlayerSettingsStore.autoSkipIntroFlow and never consults that profile value. Unlike the special-cased quality choice, the selected intro-skip behavior therefore remains unchanged for playback and in the Settings screen during the current session; write it through playerSettingsStore.setAutoSkipIntro() as well.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed in bb6b902. persistChoiceIfAny mirrors auto_skip_intro and auto_skip_credits into playerSettingsStore alongside quality_preference, since Android playback reads the local store rather than the profile field.

if (spec.key == "quality_preference") {
playerSettingsStore.setPreferredQuality(value)
}
profileRepository.updateActiveProfile(request)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle failed onboarding setting writes before advancing

The result of the profile update is discarded, so a validation error or transient network failure silently loses the user's setting even though the tour has already advanced and may subsequently be marked complete. This is especially unrecoverable for the final setting step because navigation immediately removes the tour; check the result and retry, retain pending work, or keep the user on the step instead of reporting successful completion.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partly addressed in bb6b902: the choice is mirrored into playerSettingsStore first, so the setting the tour showed does take effect on this device even when the profile PUT fails. Keeping the user on the step for a failed best-effort PUT would trap them in a tour they cannot leave, which is the worse failure here; Settings re-syncs from the server later.

Comment on lines +51 to +52
viewModelScope.launch {
when (val result = authRepository.lookupInvitation(serverUrl, token)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cancel stale invitation lookups when the link changes

If a second invite deep link replaces the current one while its first lookup is still in flight, load() launches another request without cancelling or generation-checking the previous job. The older response can then arrive last and overwrite the UI with the first invite's email and server name while the stored serverUrl and token point at the second invite, causing the user to submit a password to an account other than the one displayed; cancel the prior lookup or ignore results whose inputs are no longer current.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed in bb6b902. load() bumps a lookupGeneration and the in-flight lookup returns early when it no longer matches, so a superseded invite's response can't paint over the current one.

Comment on lines +81 to +84
if (state.isLoading || state.steps.isEmpty()) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(color = Color(0xFFF3EFE9))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep Skip available while onboarding loads

While either onboarding request is slow, this branch renders only a spinner and provides no Skip action even though the shared HTTP client permits requests to remain pending for up to 60 seconds. Because profile selection clears the previous back stack before opening this gate, a stalled server can block access to Home for the full timeout; render the skip control during loading or apply a much shorter fail-open timeout.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed in bb6b902. The loading branch renders Skip above the spinner, and finish() handles a blank tourId by letting the user through with server state left not-done.

Comment on lines +53 to +54
fun label(wire: String?, unsetLabel: String): String =
TAGS.firstOrNull { it.first == wire }?.second ?: unsetLabel

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Display preserved language tags instead of showing them as unset

Valid tags outside the ten picker entries are deliberately preserved by migrateLegacyValue()—for example nl, hi, or pt-BR synced from another client—but this formatter labels every such active value as Default for audio or Off for subtitles. Playback can therefore continue applying a preference that both phone and TV settings claim is disabled; expose the stored tag with a fallback label or include it as a selected option rather than mapping it to the unset state.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed in bb6b902. LanguageOptions.label echoes a preserved out-of-picker tag back as itself and only falls back to the unset label for values that aren't tags at all.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
shared/src/commonMain/kotlin/org/siloserver/silo/network/api/OnboardingApi.kt (1)

22-24: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer Ktor's parameter() over string interpolation for the query string.

surface is safe today since callers only pass the literal "phone"/"tv", but building the query string via raw interpolation bypasses Ktor's automatic encoding and is easy to get wrong if this ever takes a dynamic value.

♻️ Proposed fix
-    suspend fun getFlow(surface: String): ApiResult<OnboardingFlow> = safeApiCall {
-        client.get("/api/v1/onboarding/flow?surface=$surface")
-    }
+    suspend fun getFlow(surface: String): ApiResult<OnboardingFlow> = safeApiCall {
+        client.get("/api/v1/onboarding/flow") {
+            url { parameter("surface", surface) }
+        }
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@shared/src/commonMain/kotlin/org/siloserver/silo/network/api/OnboardingApi.kt`
around lines 22 - 24, Update the getFlow method to pass surface through Ktor’s
parameter() query API instead of interpolating it into the request URL. Keep the
existing endpoint path and safeApiCall behavior unchanged while ensuring the
query value is encoded by Ktor.
androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/onboarding/OnboardingTourViewModel.kt (1)

45-51: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add focused unit tests for OnboardingTourViewModel. Cover step filtering, default seeding, and complete/skip persistence so this first-run flow doesn’t regress across devices.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/onboarding/OnboardingTourViewModel.kt`
around lines 45 - 51, Add focused unit tests for OnboardingTourViewModel
covering step filtering, default step seeding, and persistence when completing
or skipping the tour. Mock or fake OnboardingRepository, ProfileRepository,
PlayerSettingsStore, TokenManager, and OnboardingTourLocalCache as needed, and
verify the expected state and saved values across first-run and repeat-device
scenarios.

Source: Path instructions

androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/navigation/InviteClaimRouteParser.kt (1)

41-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate route-encoding logic vs. Route.InviteClaim.

This hand-rolls the same "invite_claim?server=...&token=..." string that Route.InviteClaim(server, token).route (Routes.kt) already builds via Uri.encode. Two independent encoders for one logical route can silently drift if the route format changes.

♻️ Proposed refactor: reuse `Route.InviteClaim`
-    val server = params["server"]?.takeIf { it.isNotBlank() } ?: return null
-    val token = params["token"]?.takeIf { it.isNotBlank() } ?: return null
-
-    return "invite_claim?server=${server.routeEncode()}&token=${token.routeEncode()}"
+    val server = params["server"]?.takeIf { it.isNotBlank() } ?: return null
+    val token = params["token"]?.takeIf { it.isNotBlank() } ?: return null
+
+    return Route.InviteClaim(server, token).route
 }
-
-private fun String.routeEncode(): String =
-    URLEncoder.encode(this, Charsets.UTF_8.name()).replace("+", "%20")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/navigation/InviteClaimRouteParser.kt`
around lines 41 - 45, Update the invite-claim route construction in
InviteClaimRouteParser to reuse Route.InviteClaim(server, token).route instead
of manually assembling the query string and calling routeEncode. Remove the
redundant routeEncode helper if it is no longer used, preserving the existing
parsed server and token values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/onboarding/OnboardingTourViewModel.kt`:
- Around line 102-112: The completion requests in finish() and applyFlow() mark
the tour locally complete without checking the onboardingRepository response.
Inspect the returned ApiResult from complete/skip, only call markDoneLocally()
after confirmed success, and handle or log failures so unsuccessful server
updates remain observable and retryable rather than permanently bypassing future
server checks.

In
`@shared/src/commonMain/kotlin/org/siloserver/silo/model/settings/LanguageOptions.kt`:
- Around line 53-54: Update LanguageOptions.label to distinguish blank or
malformed values from nonblank tags absent in TAGS, displaying preserved unknown
tags or an “Other” representation instead of unsetLabel; retain existing labels
for picker entries. Update LanguageOptionsTest unknown-tag expectations and add
coverage for a preserved out-of-picker tag in
shared/src/commonTest/kotlin/org/siloserver/silo/model/settings/LanguageOptionsTest.kt
lines 40-46.
- Around line 29-40: Rename the non-constant LanguageOptions.TAGS property to
tags and update all three references in
shared/src/commonTest/kotlin/org/siloserver/silo/model/settings/LanguageOptionsTest.kt
at lines 15-16, 25-28, and 33-36. Also rename AudioLanguages and
SubtitleLanguages to camelCase in
androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/settings/TvSettingsScreen.kt
at lines 2034-2036, updating their usages consistently.

In `@shared/src/commonMain/kotlin/org/siloserver/silo/network/api/AuthApi.kt`:
- Around line 71-95: Update lookupInvitation and acceptInvitation to append the
invitation token as an encoded URL path segment instead of interpolating it
directly into the request path. Preserve the existing endpoints, authentication
settings, request body, and response handling while ensuring reserved token
characters cannot alter the path or query.

---

Nitpick comments:
In
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/navigation/InviteClaimRouteParser.kt`:
- Around line 41-45: Update the invite-claim route construction in
InviteClaimRouteParser to reuse Route.InviteClaim(server, token).route instead
of manually assembling the query string and calling routeEncode. Remove the
redundant routeEncode helper if it is no longer used, preserving the existing
parsed server and token values.

In
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/onboarding/OnboardingTourViewModel.kt`:
- Around line 45-51: Add focused unit tests for OnboardingTourViewModel covering
step filtering, default step seeding, and persistence when completing or
skipping the tour. Mock or fake OnboardingRepository, ProfileRepository,
PlayerSettingsStore, TokenManager, and OnboardingTourLocalCache as needed, and
verify the expected state and saved values across first-run and repeat-device
scenarios.

In
`@shared/src/commonMain/kotlin/org/siloserver/silo/network/api/OnboardingApi.kt`:
- Around line 22-24: Update the getFlow method to pass surface through Ktor’s
parameter() query API instead of interpolating it into the request URL. Keep the
existing endpoint path and safeApiCall behavior unchanged while ensuring the
query value is encoded by Ktor.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3d06fc4f-483b-4b83-aaed-f9b75a789ab6

📥 Commits

Reviewing files that changed from the base of the PR and between 57a4ec8 and 56f385b.

📒 Files selected for processing (27)
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/settings/AndroidPlayerSettingsStore.kt
  • androidApp/src/androidMain/AndroidManifest.xml
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/MainActivity.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/di/AndroidModule.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/navigation/AppNavigation.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/navigation/InviteClaimRouteParser.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/navigation/Routes.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/auth/InviteClaimScreen.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/auth/InviteClaimViewModel.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/onboarding/OnboardingTourLocalCache.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/onboarding/OnboardingTourScreen.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/onboarding/OnboardingTourViewModel.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/settings/PlaybackSettings.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/settings/SettingsViewModel.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/settings/SubtitleSettings.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/settings/TvSettingsScreen.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/settings/TvSettingsViewModel.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/di/NetworkModule.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/di/RepositoryModule.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/model/auth/InvitationModels.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/model/onboarding/OnboardingModels.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/model/settings/LanguageOptions.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/network/api/AuthApi.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/network/api/OnboardingApi.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/repository/AuthRepository.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/repository/OnboardingRepository.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/model/settings/LanguageOptionsTest.kt

- Ask for cleartext consent before the credential-bearing claim POST
  (read-only lookup may pass the gate; the POST would be rejected as an
  opaque network error on unapproved http:// origins)
- Clear the previous account's profile id/token when a claim adopts an
  already-registered server slot
- Cache tour completion locally only after the server acks the POST, so
  a lost completion is retried instead of silently diverging
- Resume the tour from the server-reported last step
- Treat only 400/404/409/410 lookup responses as terminal; 429/5xx get
  the retry card, and stale lookups are generation-checked so a slow
  response can't paint a superseded invite
- Keep Skip reachable while the tour manifest loads (no tour id yet =
  finish locally without posting)
- Mirror auto-skip tour choices into the player settings store the app
  actually reads, like quality
- Swallow malformed percent-encoding in warm deep-link parsers instead
  of throwing from onNewIntent
- Show preserved out-of-table language tags as themselves rather than
  claiming the preference is off

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/auth/InviteClaimViewModel.kt (1)

115-147: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Gate duplicate claim attempts before launching.

Both the IME action and “Create account” call onClaimClick, but isSubmitting is set only after the coroutine begins and the cleartext check completes. Rapid duplicate actions can therefore issue multiple one-time claim POSTs. Add a synchronous attempt-in-flight guard and clear it on cancellation or every terminal outcome.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/auth/InviteClaimViewModel.kt`
around lines 115 - 147, Add a synchronous attempt-in-flight guard to
onClaimClick before launching its coroutine, returning immediately for duplicate
actions and setting the guard before launch. Clear the guard on coroutine
cancellation and every terminal outcome, including validation failure,
cleartext-consent suspension, successful submission, and claim errors; ensure
onConfirmCleartext also participates in the same guard so only one claim POST
can be active.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/auth/InviteClaimViewModel.kt`:
- Around line 62-66: Update the early-return identity check in load to require
both the existing serverUrl and token to match the incoming values, while
retaining the non-null invitation condition. Continue updating serverUrl and
token and performing the lookup when either value differs.

---

Outside diff comments:
In
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/auth/InviteClaimViewModel.kt`:
- Around line 115-147: Add a synchronous attempt-in-flight guard to onClaimClick
before launching its coroutine, returning immediately for duplicate actions and
setting the guard before launch. Clear the guard on coroutine cancellation and
every terminal outcome, including validation failure, cleartext-consent
suspension, successful submission, and claim errors; ensure onConfirmCleartext
also participates in the same guard so only one claim POST can be active.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a83ba86-8a75-4171-9093-1f908e453584

📥 Commits

Reviewing files that changed from the base of the PR and between 56f385b and bb6b902.

📒 Files selected for processing (10)
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/di/AndroidModule.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/navigation/DeviceLoginRouteParser.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/navigation/InviteClaimRouteParser.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/auth/InviteClaimScreen.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/auth/InviteClaimViewModel.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/onboarding/OnboardingTourScreen.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/onboarding/OnboardingTourViewModel.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/model/settings/LanguageOptions.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/repository/AuthRepository.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/model/settings/LanguageOptionsTest.kt
🚧 Files skipped from review as they are similar to previous changes (8)
  • shared/src/commonTest/kotlin/org/siloserver/silo/model/settings/LanguageOptionsTest.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/di/AndroidModule.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/auth/InviteClaimScreen.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/onboarding/OnboardingTourScreen.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/navigation/InviteClaimRouteParser.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/model/settings/LanguageOptions.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/repository/AuthRepository.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/onboarding/OnboardingTourViewModel.kt

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb6b9025ac

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


private fun key(serverId: String?, profileId: String?): String? {
if (serverId.isNullOrBlank() || profileId.isNullOrBlank()) return null
return "done:$serverId:$profileId"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope completion cache to the active tour

When the server publishes a new tour_id or resets this profile's onboarding state, this permanent server/profile-only key still returns true, so both cold startup and load() skip the server state and flow endpoints forever. Completed users therefore never see later server-driven tours; store the completed tour/version or periodically revalidate the server rather than treating one completion as permanent for the profile.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid, but not folded into this PR — the fix is bigger than it looks. The stored value is a Boolean on installs already in the field, so re-reading that key as a tour id throws ClassCastException without a migration, and the cold-start gate in MainActivity.resolveStartDestination checks the cache before any tour id is known, so it needs a separate answer. Today a new tour id is not permanently suppressed for most users: the cache is only written on a confirmed complete/skip, and the gate still consults the server whenever it is unset. Worth doing deliberately with the migration rather than as a drive-by here.

Comment on lines +160 to +162
when (val result = authRepository.acceptInvitation(serverUrl, token, current.password)) {
is ApiResult.Success -> {
_uiState.update { it.copy(isSubmitting = false, claimSuccess = true) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore stale invitation claim responses

When a second warm invite replaces the route while the first claim POST is still in flight, load() invalidates only lookup responses; this submission still updates the shared UI and active server when it returns. The first response can consequently navigate away from the second invite or race a second submission and leave credentials for the wrong server active, so capture the lookup generation/target when submitting and discard results after it changes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 69eb935. submitClaim captures lookupGeneration (plus the server/token it is claiming) and discards its result if the target changed while the POST was in flight.

): ApiResult<User> {
val result = authApi.acceptInvitation(serverUrl, token, password)
if (result is ApiResult.Success) {
setServerUrl(serverUrl)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist claim tokens before refreshing server metadata

After the single-use accept POST succeeds, setServerUrl() synchronously runs the best-effort health/name refresh before persistSession() saves the returned credentials. If that health request stalls for its six-second timeout and the invite screen is popped or the process is killed, the coroutine is cancelled with the invitation already consumed but no new tokens stored, leaving the user on the switched server with an unusable invite; switch scopes and persist the session before performing the optional metadata refresh.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed. setServerUrl must run before persistSession — persisting writes into the active server's slot, so saving first would put the claimed account's tokens on the previous server. The window is real but narrow, and reordering as suggested trades a rare lost-token case for a wrong-slot one. If the health refresh is the concern, the fix is to make that refresh non-blocking inside setServerUrl, which affects every caller and belongs in its own change.

Comment on lines +193 to +195
if (result is ApiResult.Success) {
localCache.markDone(tokenManager.getCurrentServerId(), tokenManager.getProfileId())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cache completion against the profile that finished

When the completion or skip POST remains in flight after navigation to Home and the user switches profile or server before it returns, these token-manager lookups resolve the new active identity rather than the profile whose request just succeeded. The new profile can then be marked locally done and bypass its own tour, while the original profile is not cached; capture the server and profile IDs before launching the non-cancellable request and use that snapshot on acknowledgement.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 69eb935. Both finish() and applyFlow's empty-steps branch snapshot the server and profile ids before the non-cancellable POST and mark that snapshot on acknowledgement, rather than re-reading the token manager after Home may have switched identity.

Quick104 and others added 3 commits July 28, 2026 19:37
The tour rendered its chrome — pips, Skip, Back/Next — over an empty
screen. AuroraScreen wraps content in a verticalScroll, which measures
children with unbounded height, so the tour's weight(1f) body had no
space to divide and collapsed to zero. Verified on an S26 Ultra: the
region between the pips and the buttons measured 0px tall.

AuroraScreen takes a `scrollable` flag for screens that lay themselves
out against the display instead of scrolling as one block, and applies
safeDrawing insets so a full-height screen's first and last rows clear
the system bars under edge-to-edge.

The steps are now a HorizontalPager, so they can be swiped as cards as
well as driven by the buttons. Both routes move through one path in the
ViewModel, so a step reached by swiping records progress and commits its
setting_choice exactly like a tapped one; advancing commits, going back
does not. The pager runs full-bleed so the next card peeks in from the
edge rather than being clipped at the gutter, and neighbours scale and
dim to read as depth.

auroraGlass takes an `elevation`, defaulting to the previous 60dp. The
tour cards pass 0: their fill is translucent, so at full height the drop
shadow's own outline showed through the glass as a faint hard-edged box
that tracked the card across a swipe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Percent-encode the invitation token into the lookup and accept paths.
It arrives from an emailed link, so a '/' or '?' in it would re-shape
the request rather than be carried as a path segment.

Send the onboarding flow's `surface` through Ktor's parameter() instead
of interpolating it into the query string, so encoding is the client's
job rather than the caller's.

Rename LanguageOptions.TAGS to `tags`, and the TV settings screen's
AudioLanguages/SubtitleLanguages to camelCase, per the repo's convention
for non-constant properties.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tour's completion POST outlives the screen: finished=true navigates
to Home, where the user can switch profile or server before it returns.
Reading the token manager on acknowledgement therefore cached "done"
against whichever profile was active by then, letting it skip a tour it
had never seen while the profile that actually finished stayed unmarked.
Snapshot the server and profile ids before the request and mark those.

An invite is identified by server and token together. The claim screen's
early-return compared only the token, so a second link carrying the same
token on a different server kept the first server and submitted the
password there. It also let a superseded claim POST drive the UI on
return; the submission now pins the lookup generation and discards its
result once the target has moved on.

Left alone: scoping the local done-cache to a tour id. Its stored value
is a Boolean on installs in the field, so reading it as a string would
throw, and the cold-start gate in MainActivity has no tour id to check
against — worth doing deliberately rather than folded in here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Quick104
Quick104 merged commit 9c1f860 into main Jul 28, 2026
2 checks passed
@Quick104
Quick104 deleted the feat/invite-claim-and-onboarding branch July 28, 2026 19:54

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 69eb9352b7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// the IME — the activity draws edge to edge, so without this the
// first and last rows of a full-height screen sit under system
// chrome.
.windowInsetsPadding(WindowInsets.safeDrawing),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore IME padding to Aurora forms

On edge-to-edge devices, WindowInsets.safeDrawing does not include the software keyboard, so replacing the previous imePadding() leaves the lower fields and submit buttons obscured when the IME opens. This affects every scrollable Aurora form, including Login, Signup, Server Setup, and Invite Claim; retain imePadding() in addition to the safe-drawing insets.

Useful? React with 👍 / 👎.

// what this device plays back with, and Settings re-syncs
// from the server later. A rejected PUT here shouldn't trap
// the user in a tour they can't leave.
profileRepository.updateActiveProfile(request)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pin the profile before persisting the final tour choice

When the user finishes on a quality or auto-skip choice, the local-store write can suspend before this call; meanwhile finished=true navigates to Home, where the user can switch profiles. updateActiveProfile() then resolves whichever profile is active at that later moment, potentially applying the completed tour's choice to another profile. Capture the profile ID when scheduling the write and update that profile explicitly.

Useful? React with 👍 / 👎.

Comment on lines +184 to +186
viewModelScope.launch {
current.steps.getOrNull(index)?.let {
onboardingRepository.recordStep(current.tourId, it.id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize onboarding progress updates

Each rapid Next tap or settled swipe launches an independent progress POST, so variable network latency can make an earlier step arrive after a later one. The server's last_step can consequently regress, causing the next launch or another client to resume at the wrong page; serialize or coalesce these writes so only monotonically newer progress can win.

Useful? React with 👍 / 👎.

Comment on lines +127 to +130
val defaults = steps
.filter { it.kind == "setting_choice" }
.mapNotNull { step -> step.setting?.default?.let { step.id to it } }
.toMap()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Initialize tour choices from the active settings

When a profile already has a non-default setting—such as one configured before the tour was introduced or synchronized from another device—this always preselects the manifest default instead of the active value. Merely advancing past the card then persists that default and silently overwrites the user's existing preference; use the current profile/player setting when available and reserve the manifest default for genuinely unset values.

Useful? React with 👍 / 👎.

Quick104 added a commit that referenced this pull request Jul 29, 2026
Two places collided.

Settings screens (AndroidPlayerSettingsStore, PlaybackSettings,
SettingsViewModel, SubtitleSettings, TvSettingsScreen, TvSettingsViewModel).
Main's PR #125 (invite claim + onboarding tour) landed on the same files this
branch moved onto the canonical settings contract. Resolved as a union: the
canonical-settings plumbing wins for settings behavior — ProfileSettingsController
resolves the profile-scoped keys instead of reading preference columns off
GET /profiles, the two-axis QualityPresets picker replaces the single
defaultQuality label, per-key optimistic writes replace the whole-triple
UpdateProfileRequest PUT, and the SERVER_UPGRADE_REQUIRED notice stays — while
main's onboarding/tour additions come across untouched. Main's language-row
migration in the profile-load path is dropped as dead code on this branch, not
as a reverted intent: those values now come from the effective-values endpoint,
which never carried the legacy display labels. The DataStore read still runs
LanguageOptions.migrateLegacyValue, so the on-device legacy rows main was
protecting are still translated.

LanguageOptions. Main revised the file this branch introduced (TAGS -> tags,
preservable-tag echo in label(), canonicalSubtitleLanguage-backed
migrateLegacyValue). Main's revision is kept wholesale and our call sites are
adapted to it: TvSettingsScreen's AudioLanguages/SubtitleLanguages become
audioLanguages/subtitleLanguages, and SubtitleSettings collapses its two
identical option lists into main's single hoisted languageOptionLabels.
migrateLegacyValue is now wider, not narrower — a valid tag outside the picker
table ("nl", "pt-BR", "eng") passes through instead of being erased, and "Off"
and "Default" still clear. Nothing the flusher tests assert changed.

Verified: :shared:testDebugUnitTest (960 tests) and
:android-shared:testDebugUnitTest (1021 tests) pass with no failures or skips;
:androidApp:compileDebugKotlin, :androidTvApp:compileDebugKotlin,
:androidApp:compileDebugUnitTestKotlin and :androidTvApp:compileDebugUnitTestKotlin
all build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant