fix: update credential gates without an app restart - #49
Merged
Conversation
Screens read hasApiKey/hasYouCamKey once in their ViewModel's init. Both platforms keep every tab's screen alive — Android in a HorizontalPager, iOS in a paged TabView — so connecting YouCam or Claude on Settings left Try-It, Wardrobe and Gaps showing their locked state until the process restarted. The platform secret stores have no change notification, so the shared repository owns the signal: a revision StateFlow bumped after every successful credential write, which the new flows re-read through. The ViewModels collect those instead of sampling once. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adding YouCam credentials (or a Claude key) on the Settings screen did not unlock the screens gated on them until the app was restarted.
TryItState.hasApiKey/hasYouCamKeywere read once in the ViewModel'sinit, and both platforms keep every tab's screen alive — Android in a singleHorizontalPagercomposition with an Activity-scopedkoinViewModel(), iOS in a pagedTabViewholding a@StateObject. Soinitnever ran again and the flag stayed stale.WardrobeViewModelandGapsViewModelsampledisAiAvailable()the same way.The platform secret stores (Keystore, Keychain) have no change notification, so
SettingsRepositoryImplnow owns the signal: a private revisionStateFlowbumped after every successful credential write, which the new observable gates re-read through. Because the repository is a Koinsingle, a write from Settings reaches every other screen's collector. All the reactivity lives incommonMain— theexpect/actualSecretStorestays a plain get/set, and no UI changed on either platform since both already render from state.Changes
SettingsRepository: addedhasApiKeyFlow(),hasYouCamCredentialsFlow()andisAiAvailableFlow()alongside the existing one-shot suspend checksSettingsRepositoryImpl: privatecredentialRevisionMutableStateFlow, bumped insidewithContext(dispatcher)after each successful save/clear so a failed write does not emit;isAiAvailableFlow()combines it with the DataStoreisOnDeviceAiEnabled()flow and re-queries on-device availability per emissionTryItViewModel:observeCredentials()combines both credential flows into state;loadCredentialState()kept for the share-sheet path, which still needs a synchronous read so it does not race the collector's first emissionWardrobeViewModel:refreshAiAvailability()→observeAiAvailability()GapsViewModel: collects availability and re-runsloadGaps()on change, so gaining a key switches it from heuristic to AI recommendations immediatelyFakeSettingsRepository: mirrors the revision tick via property setters, so existing tests that assignapiKey/youCamClientIddrive the flows tooTest plan
./gradlew :shared:allTests— passes; includes the new regression tests, and covers the iOS Simulator target compile./gradlew :composeApp:assembleDebug— passesChecklist
./gradlew detektpasses🤖 Generated with Claude Code