diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SovereignWalletStartupScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SovereignWalletStartupScreen.kt index 924f6002..98e8bb28 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SovereignWalletStartupScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/SovereignWalletStartupScreen.kt @@ -46,13 +46,15 @@ fun SovereignWalletStartupScreen( // to completion here: everything downstream reads the node's key manager. An intro screen has // to be a step inside this flow, not a detour around it. + val listWalletState by sovereignWalletViewModel.listWalletState.collectAsState() + Box( modifier = Modifier .fillMaxSize() .imePadding(), contentAlignment = Alignment.Center ) { - when (sovereignWalletViewModel.listWalletState.value) { + when (listWalletState) { is ListWalletState.Init -> { LoadingDataIndicator( text = "Decrypting..." diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/SovereignWalletViewModel.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/SovereignWalletViewModel.kt index 90fc6a6a..5229bd03 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/SovereignWalletViewModel.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/SovereignWalletViewModel.kt @@ -65,7 +65,16 @@ class SovereignWalletViewModel( // We might end up only using the machankuraWalletRepository in the future where we send the walletId in each request. ): ViewModel() { private val log = Logger.withTag("SovereignWalletViewModel") - val listWalletState = mutableStateOf(ListWalletState.Init) + + // A StateFlow, not a `mutableStateOf`, and it has to stay one. This view model is built by + // `viewModel()` during composition, so a Compose state created in its constructor is created + // inside that composition's snapshot -- and a write from another thread that lands before the + // composition is applied is discarded. `listAvailableWallets` runs from `init` and, on a device + // with no seed file, answers in about four milliseconds, which is squarely inside that window: + // the write to Success was lost and the app sat on "Decrypting..." for ever. A StateFlow has no + // snapshot to belong to, so the same write from the same thread at the same moment is seen. + private val _listWalletState = MutableStateFlow(ListWalletState.Init) + val listWalletState = _listWalletState.asStateFlow() private val _availableWallets = MutableStateFlow>(emptyMap()) val availableWallets = _availableWallets.asStateFlow() @@ -119,33 +128,33 @@ class SovereignWalletViewModel( fun listAvailableWallets(onDone: () -> Unit) { viewModelScope.launch(Dispatchers.IO + CoroutineExceptionHandler { _, e -> // log.error("error when initialising startup-view: ", e) - listWalletState.value = ListWalletState.Error.Generic(e) + _listWalletState.value = ListWalletState.Error.Generic(e) }) { when (val result = loadAndDecryptSeed(phoenixGlobal)) { is DecryptSeedResult.Failure.SerializationError -> { log.error {"cannot deserialize seed file: "} - listWalletState.value = ListWalletState.Error.Serialization + _listWalletState.value = ListWalletState.Error.Serialization } is DecryptSeedResult.Failure.DecryptionError -> { log.e("cannot decrypt seed file: ", throwable = result.cause) - listWalletState.value = ListWalletState.Error.DecryptionError.GeneralException(result.cause) + _listWalletState.value = ListWalletState.Error.DecryptionError.GeneralException(result.cause) } is DecryptSeedResult.Failure.KeyStoreFailure -> { log.e("key store failure: ", throwable = result.cause) - listWalletState.value = ListWalletState.Error.DecryptionError.KeystoreFailure(result.cause) + _listWalletState.value = ListWalletState.Error.DecryptionError.KeystoreFailure(result.cause) } is DecryptSeedResult.Failure.SeedFileUnreadable -> { log.e("aborting, unreadable seed file") - listWalletState.value = ListWalletState.Error.Generic(null) + _listWalletState.value = ListWalletState.Error.Generic(null) } is DecryptSeedResult.Failure.SeedInvalid -> { log.e("aborting, seed is invalid") - listWalletState.value = ListWalletState.Error.Generic(null) + _listWalletState.value = ListWalletState.Error.Generic(null) } is DecryptSeedResult.Failure.SeedFileNotFound -> { - listWalletState.value = ListWalletState.Success + _listWalletState.value = ListWalletState.Success _availableWallets.value = emptyMap() } @@ -164,7 +173,7 @@ class SovereignWalletViewModel( } } _availableWallets.value = result.userWalletsMap - listWalletState.value = ListWalletState.Success + _listWalletState.value = ListWalletState.Success viewModelScope.launch(Dispatchers.Main) { onDone() }