Message visibility based on foreground/background
This commit is contained in:
parent
c45d672995
commit
1315ba04ec
|
@ -19,6 +19,7 @@ import co.electriccoin.zcash.spackle.StrictModeCompat
|
||||||
import co.electriccoin.zcash.spackle.Twig
|
import co.electriccoin.zcash.spackle.Twig
|
||||||
import co.electriccoin.zcash.ui.common.provider.ApplicationStateProvider
|
import co.electriccoin.zcash.ui.common.provider.ApplicationStateProvider
|
||||||
import co.electriccoin.zcash.ui.common.repository.FlexaRepository
|
import co.electriccoin.zcash.ui.common.repository.FlexaRepository
|
||||||
|
import co.electriccoin.zcash.ui.common.repository.HomeMessageCacheRepository
|
||||||
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
|
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
@ -31,6 +32,7 @@ class ZcashApplication : CoroutineApplication() {
|
||||||
private val flexaRepository by inject<FlexaRepository>()
|
private val flexaRepository by inject<FlexaRepository>()
|
||||||
private val applicationStateProvider: ApplicationStateProvider by inject()
|
private val applicationStateProvider: ApplicationStateProvider by inject()
|
||||||
private val getAvailableCrashReporters: CrashReportersProvider by inject()
|
private val getAvailableCrashReporters: CrashReportersProvider by inject()
|
||||||
|
private val homeMessageCacheRepository: HomeMessageCacheRepository by inject()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
@ -68,6 +70,7 @@ class ZcashApplication : CoroutineApplication() {
|
||||||
configureAnalytics()
|
configureAnalytics()
|
||||||
|
|
||||||
flexaRepository.init()
|
flexaRepository.init()
|
||||||
|
homeMessageCacheRepository.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun configureLogging() {
|
private fun configureLogging() {
|
||||||
|
|
|
@ -8,6 +8,8 @@ import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository
|
||||||
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepositoryImpl
|
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepositoryImpl
|
||||||
import co.electriccoin.zcash.ui.common.repository.FlexaRepository
|
import co.electriccoin.zcash.ui.common.repository.FlexaRepository
|
||||||
import co.electriccoin.zcash.ui.common.repository.FlexaRepositoryImpl
|
import co.electriccoin.zcash.ui.common.repository.FlexaRepositoryImpl
|
||||||
|
import co.electriccoin.zcash.ui.common.repository.HomeMessageCacheRepository
|
||||||
|
import co.electriccoin.zcash.ui.common.repository.HomeMessageCacheRepositoryImpl
|
||||||
import co.electriccoin.zcash.ui.common.repository.KeystoneProposalRepository
|
import co.electriccoin.zcash.ui.common.repository.KeystoneProposalRepository
|
||||||
import co.electriccoin.zcash.ui.common.repository.KeystoneProposalRepositoryImpl
|
import co.electriccoin.zcash.ui.common.repository.KeystoneProposalRepositoryImpl
|
||||||
import co.electriccoin.zcash.ui.common.repository.ShieldFundsRepository
|
import co.electriccoin.zcash.ui.common.repository.ShieldFundsRepository
|
||||||
|
@ -36,4 +38,5 @@ val repositoryModule =
|
||||||
singleOf(::TransactionFilterRepositoryImpl) bind TransactionFilterRepository::class
|
singleOf(::TransactionFilterRepositoryImpl) bind TransactionFilterRepository::class
|
||||||
singleOf(::ZashiProposalRepositoryImpl) bind ZashiProposalRepository::class
|
singleOf(::ZashiProposalRepositoryImpl) bind ZashiProposalRepository::class
|
||||||
singleOf(::ShieldFundsRepositoryImpl) bind ShieldFundsRepository::class
|
singleOf(::ShieldFundsRepositoryImpl) bind ShieldFundsRepository::class
|
||||||
|
singleOf(::HomeMessageCacheRepositoryImpl) bind HomeMessageCacheRepository::class
|
||||||
}
|
}
|
||||||
|
|
|
@ -185,7 +185,7 @@ val useCaseModule =
|
||||||
factoryOf(::GetKeystoneStatusUseCase)
|
factoryOf(::GetKeystoneStatusUseCase)
|
||||||
factoryOf(::GetCoinbaseStatusUseCase)
|
factoryOf(::GetCoinbaseStatusUseCase)
|
||||||
factoryOf(::GetFlexaStatusUseCase)
|
factoryOf(::GetFlexaStatusUseCase)
|
||||||
factoryOf(::GetHomeMessageUseCase)
|
singleOf(::GetHomeMessageUseCase)
|
||||||
factoryOf(::OnUserSavedWalletBackupUseCase)
|
factoryOf(::OnUserSavedWalletBackupUseCase)
|
||||||
factoryOf(::RemindWalletBackupLaterUseCase)
|
factoryOf(::RemindWalletBackupLaterUseCase)
|
||||||
factoryOf(::RemindShieldFundsLaterUseCase)
|
factoryOf(::RemindShieldFundsLaterUseCase)
|
||||||
|
|
|
@ -34,6 +34,7 @@ import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT
|
import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.WHATS_NEW
|
import co.electriccoin.zcash.ui.NavigationTargets.WHATS_NEW
|
||||||
import co.electriccoin.zcash.ui.common.compose.LocalNavController
|
import co.electriccoin.zcash.ui.common.compose.LocalNavController
|
||||||
|
import co.electriccoin.zcash.ui.common.datasource.MessageAvailabilityDataSource
|
||||||
import co.electriccoin.zcash.ui.common.provider.ApplicationStateProvider
|
import co.electriccoin.zcash.ui.common.provider.ApplicationStateProvider
|
||||||
import co.electriccoin.zcash.ui.common.provider.isInForeground
|
import co.electriccoin.zcash.ui.common.provider.isInForeground
|
||||||
import co.electriccoin.zcash.ui.design.LocalKeyboardManager
|
import co.electriccoin.zcash.ui.design.LocalKeyboardManager
|
||||||
|
@ -141,6 +142,7 @@ internal fun MainActivity.Navigation() {
|
||||||
val flexaViewModel = koinViewModel<FlexaViewModel>()
|
val flexaViewModel = koinViewModel<FlexaViewModel>()
|
||||||
val navigationRouter = koinInject<NavigationRouter>()
|
val navigationRouter = koinInject<NavigationRouter>()
|
||||||
val sheetStateManager = LocalSheetStateManager.current
|
val sheetStateManager = LocalSheetStateManager.current
|
||||||
|
val messageAvailabilityDataSource = koinInject<MessageAvailabilityDataSource>()
|
||||||
|
|
||||||
// Helper properties for triggering the system security UI from callbacks
|
// Helper properties for triggering the system security UI from callbacks
|
||||||
val (exportPrivateDataAuthentication, setExportPrivateDataAuthentication) =
|
val (exportPrivateDataAuthentication, setExportPrivateDataAuthentication) =
|
||||||
|
@ -153,14 +155,16 @@ internal fun MainActivity.Navigation() {
|
||||||
navController,
|
navController,
|
||||||
flexaViewModel,
|
flexaViewModel,
|
||||||
keyboardManager,
|
keyboardManager,
|
||||||
sheetStateManager
|
sheetStateManager,
|
||||||
|
messageAvailabilityDataSource
|
||||||
) {
|
) {
|
||||||
NavigatorImpl(
|
NavigatorImpl(
|
||||||
activity = this@Navigation,
|
activity = this@Navigation,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
flexaViewModel = flexaViewModel,
|
flexaViewModel = flexaViewModel,
|
||||||
keyboardManager = keyboardManager,
|
keyboardManager = keyboardManager,
|
||||||
sheetStateManager = sheetStateManager
|
sheetStateManager = sheetStateManager,
|
||||||
|
messageAvailabilityDataSource = messageAvailabilityDataSource
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import androidx.activity.ComponentActivity
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.NavOptionsBuilder
|
import androidx.navigation.NavOptionsBuilder
|
||||||
import androidx.navigation.serialization.generateHashCode
|
import androidx.navigation.serialization.generateHashCode
|
||||||
|
import co.electriccoin.zcash.ui.common.datasource.MessageAvailabilityDataSource
|
||||||
import co.electriccoin.zcash.ui.design.KeyboardManager
|
import co.electriccoin.zcash.ui.design.KeyboardManager
|
||||||
import co.electriccoin.zcash.ui.design.SheetStateManager
|
import co.electriccoin.zcash.ui.design.SheetStateManager
|
||||||
import co.electriccoin.zcash.ui.screen.ExternalUrl
|
import co.electriccoin.zcash.ui.screen.ExternalUrl
|
||||||
|
@ -25,6 +26,7 @@ class NavigatorImpl(
|
||||||
private val flexaViewModel: FlexaViewModel,
|
private val flexaViewModel: FlexaViewModel,
|
||||||
private val keyboardManager: KeyboardManager,
|
private val keyboardManager: KeyboardManager,
|
||||||
private val sheetStateManager: SheetStateManager,
|
private val sheetStateManager: SheetStateManager,
|
||||||
|
private val messageAvailabilityDataSource: MessageAvailabilityDataSource,
|
||||||
) : Navigator {
|
) : Navigator {
|
||||||
override suspend fun executeCommand(command: NavigationCommand) {
|
override suspend fun executeCommand(command: NavigationCommand) {
|
||||||
keyboardManager.close()
|
keyboardManager.close()
|
||||||
|
@ -85,6 +87,7 @@ class NavigatorImpl(
|
||||||
throw UnsupportedOperationException("External url can be opened as last screen only")
|
throw UnsupportedOperationException("External url can be opened as last screen only")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
messageAvailabilityDataSource.onThirdPartyUiShown()
|
||||||
WebBrowserUtil.startActivity(activity, route.url)
|
WebBrowserUtil.startActivity(activity, route.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,6 +128,7 @@ class NavigatorImpl(
|
||||||
throw UnsupportedOperationException("External url can be opened as last screen only")
|
throw UnsupportedOperationException("External url can be opened as last screen only")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
messageAvailabilityDataSource.onThirdPartyUiShown()
|
||||||
WebBrowserUtil.startActivity(activity, route.url)
|
WebBrowserUtil.startActivity(activity, route.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,6 +153,10 @@ class NavigatorImpl(
|
||||||
else -> navController.executeNavigation(route = route)
|
else -> navController.executeNavigation(route = route)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command.routes.lastOrNull() in listOf(ExternalUrl, co.electriccoin.zcash.ui.screen.flexa.Flexa) ) {
|
||||||
|
messageAvailabilityDataSource.onThirdPartyUiShown()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun NavHostController.executeNavigation(
|
private fun NavHostController.executeNavigation(
|
||||||
|
@ -171,11 +179,11 @@ class NavigatorImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createFlexaFlow(flexaViewModel: FlexaViewModel) {
|
private fun createFlexaFlow(flexaViewModel: FlexaViewModel) {
|
||||||
|
messageAvailabilityDataSource.onThirdPartyUiShown()
|
||||||
Flexa
|
Flexa
|
||||||
.buildSpend()
|
.buildSpend()
|
||||||
.onTransactionRequest { result ->
|
.onTransactionRequest { result -> flexaViewModel.createTransaction(result) }
|
||||||
flexaViewModel.createTransaction(result)
|
.build()
|
||||||
}.build()
|
|
||||||
.open(activity)
|
.open(activity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,42 +5,75 @@ import co.electriccoin.zcash.ui.common.provider.ApplicationStateProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
interface MessageAvailabilityDataSource {
|
interface MessageAvailabilityDataSource {
|
||||||
val canShowMessage: Boolean
|
val canShowMessage: Boolean
|
||||||
fun observe(): StateFlow<Boolean>
|
fun observe(): Flow<Boolean>
|
||||||
fun onMessageShown()
|
fun onMessageShown()
|
||||||
|
fun onThirdPartyUiShown()
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessageAvailabilityDataSourceImpl(
|
class MessageAvailabilityDataSourceImpl(
|
||||||
private val applicationStateProvider: ApplicationStateProvider
|
applicationStateProvider: ApplicationStateProvider
|
||||||
): MessageAvailabilityDataSource {
|
) : MessageAvailabilityDataSource {
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||||
|
|
||||||
private val state = MutableStateFlow(true)
|
private val state = MutableStateFlow(
|
||||||
|
MessageAvailabilityData(
|
||||||
|
isAppInForeground = true,
|
||||||
|
isThirdPartyUiShown = false,
|
||||||
|
hasMessageBeenShown = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
override val canShowMessage: Boolean
|
override val canShowMessage: Boolean
|
||||||
get() = state.value
|
get() = state.value.canShowMessage
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launch {
|
applicationStateProvider.state
|
||||||
applicationStateProvider.state.collect {
|
.onEach { event ->
|
||||||
if (it == Lifecycle.Event.ON_START) {
|
if (event == Lifecycle.Event.ON_START) {
|
||||||
state.update { true }
|
state.update {
|
||||||
|
it.copy(
|
||||||
|
isAppInForeground = true,
|
||||||
|
hasMessageBeenShown = if (it.isThirdPartyUiShown) it.hasMessageBeenShown else false,
|
||||||
|
isThirdPartyUiShown = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (event == Lifecycle.Event.ON_STOP) {
|
||||||
|
state.update {
|
||||||
|
it.copy(
|
||||||
|
isAppInForeground = it.isThirdPartyUiShown,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun observe(): StateFlow<Boolean> = state.asStateFlow()
|
override fun observe(): Flow<Boolean> = state.map { it.canShowMessage }.distinctUntilChanged()
|
||||||
|
|
||||||
override fun onMessageShown() {
|
override fun onMessageShown() {
|
||||||
state.update { false }
|
state.update { it.copy(hasMessageBeenShown = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onThirdPartyUiShown() {
|
||||||
|
state.update { it.copy(isThirdPartyUiShown = true) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class MessageAvailabilityData(
|
||||||
|
val isAppInForeground: Boolean,
|
||||||
|
val isThirdPartyUiShown: Boolean,
|
||||||
|
val hasMessageBeenShown: Boolean
|
||||||
|
) {
|
||||||
|
val canShowMessage = isAppInForeground && !hasMessageBeenShown
|
||||||
|
}
|
||||||
|
|
|
@ -10,12 +10,7 @@ import co.electriccoin.zcash.ui.common.viewmodel.SynchronizerError
|
||||||
// TODO [#292]: Should be moved to SDK-EXT-UI module.
|
// TODO [#292]: Should be moved to SDK-EXT-UI module.
|
||||||
// TODO [#292]: https://github.com/Electric-Coin-Company/zashi-android/issues/292
|
// TODO [#292]: https://github.com/Electric-Coin-Company/zashi-android/issues/292
|
||||||
data class WalletSnapshot(
|
data class WalletSnapshot(
|
||||||
val isZashi: Boolean,
|
|
||||||
val status: Synchronizer.Status,
|
val status: Synchronizer.Status,
|
||||||
val processorInfo: CompactBlockProcessor.ProcessorInfo,
|
|
||||||
val orchardBalance: WalletBalance,
|
|
||||||
val saplingBalance: WalletBalance?,
|
|
||||||
val transparentBalance: Zatoshi,
|
|
||||||
val progress: PercentDecimal,
|
val progress: PercentDecimal,
|
||||||
val synchronizerError: SynchronizerError?
|
val synchronizerError: SynchronizerError?
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
package co.electriccoin.zcash.ui.common.repository
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||||
|
import co.electriccoin.zcash.ui.common.datasource.MessageAvailabilityDataSource
|
||||||
|
import co.electriccoin.zcash.ui.common.viewmodel.SynchronizerError
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
||||||
|
interface HomeMessageCacheRepository {
|
||||||
|
/**
|
||||||
|
* Last message that was shown. Null if no message has been shown yet.
|
||||||
|
*/
|
||||||
|
var lastShownMessage: HomeMessageData?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last message that was shown. Null if no message has been shown yet or if last message was null.
|
||||||
|
*/
|
||||||
|
var lastMessage: HomeMessageData?
|
||||||
|
|
||||||
|
fun init()
|
||||||
|
|
||||||
|
fun reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
class HomeMessageCacheRepositoryImpl(
|
||||||
|
private val messageAvailabilityDataSource: MessageAvailabilityDataSource
|
||||||
|
) : HomeMessageCacheRepository {
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||||
|
|
||||||
|
override var lastShownMessage: HomeMessageData? = null
|
||||||
|
override var lastMessage: HomeMessageData? = null
|
||||||
|
|
||||||
|
override fun init() {
|
||||||
|
messageAvailabilityDataSource
|
||||||
|
.observe()
|
||||||
|
.onEach { canShowMessage ->
|
||||||
|
if (canShowMessage) {
|
||||||
|
lastShownMessage = null
|
||||||
|
lastMessage = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reset() {
|
||||||
|
lastShownMessage = null
|
||||||
|
lastMessage = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface HomeMessageData {
|
||||||
|
|
||||||
|
val priority: Int
|
||||||
|
|
||||||
|
data class Error(val synchronizerError: SynchronizerError) : RuntimeMessage()
|
||||||
|
data object Disconnected : RuntimeMessage()
|
||||||
|
data class Restoring(val progress: Float) : RuntimeMessage()
|
||||||
|
data class Syncing(val progress: Float) : RuntimeMessage()
|
||||||
|
data object Updating : RuntimeMessage()
|
||||||
|
|
||||||
|
data object Backup : Prioritized {
|
||||||
|
override val priority: Int = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ShieldFunds(val zatoshi: Zatoshi) : Prioritized {
|
||||||
|
override val priority: Int = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
data object EnableCurrencyConversion : Prioritized {
|
||||||
|
override val priority: Int = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message which always is shown.
|
||||||
|
*/
|
||||||
|
sealed class RuntimeMessage : HomeMessageData {
|
||||||
|
override val priority: Int = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message which always is displayed only if previous message was lower priority.
|
||||||
|
*/
|
||||||
|
sealed interface Prioritized : HomeMessageData
|
|
@ -1,5 +1,7 @@
|
||||||
package co.electriccoin.zcash.ui.common.repository
|
package co.electriccoin.zcash.ui.common.repository
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
|
import cash.z.ecc.android.sdk.model.AccountUuid
|
||||||
import cash.z.ecc.android.sdk.model.TransactionId
|
import cash.z.ecc.android.sdk.model.TransactionId
|
||||||
import cash.z.ecc.android.sdk.model.TransactionOutput
|
import cash.z.ecc.android.sdk.model.TransactionOutput
|
||||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||||
|
@ -8,6 +10,7 @@ import cash.z.ecc.android.sdk.model.TransactionState.Expired
|
||||||
import cash.z.ecc.android.sdk.model.TransactionState.Pending
|
import cash.z.ecc.android.sdk.model.TransactionState.Pending
|
||||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||||
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
|
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
|
||||||
|
import co.electriccoin.zcash.ui.common.model.ZashiAccount
|
||||||
import co.electriccoin.zcash.ui.common.provider.SynchronizerProvider
|
import co.electriccoin.zcash.ui.common.provider.SynchronizerProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -20,6 +23,7 @@ import kotlinx.coroutines.flow.WhileSubscribed
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
@ -38,6 +42,8 @@ import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
interface TransactionRepository {
|
interface TransactionRepository {
|
||||||
|
val zashiTransactions: Flow<List<Transaction>?>
|
||||||
|
|
||||||
val currentTransactions: Flow<List<Transaction>?>
|
val currentTransactions: Flow<List<Transaction>?>
|
||||||
|
|
||||||
suspend fun getMemos(transaction: Transaction): List<String>
|
suspend fun getMemos(transaction: Transaction): List<String>
|
||||||
|
@ -52,21 +58,49 @@ interface TransactionRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
class TransactionRepositoryImpl(
|
class TransactionRepositoryImpl(
|
||||||
accountDataSource: AccountDataSource,
|
private val accountDataSource: AccountDataSource,
|
||||||
private val synchronizerProvider: SynchronizerProvider,
|
private val synchronizerProvider: SynchronizerProvider,
|
||||||
) : TransactionRepository {
|
) : TransactionRepository {
|
||||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||||
|
|
||||||
|
override val zashiTransactions: Flow<List<Transaction>?> =
|
||||||
|
observeTransactions(
|
||||||
|
accountFlow = accountDataSource.zashiAccount.map { it?.sdkAccount?.accountUuid }.distinctUntilChanged()
|
||||||
|
).stateIn(
|
||||||
|
scope = scope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5.seconds, Duration.ZERO),
|
||||||
|
initialValue = null
|
||||||
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
override val currentTransactions: Flow<List<Transaction>?> =
|
override val currentTransactions: Flow<List<Transaction>?> = accountDataSource.selectedAccount
|
||||||
|
.distinctUntilChangedBy { it?.sdkAccount?.accountUuid }
|
||||||
|
.flatMapLatest { selected ->
|
||||||
|
if (selected is ZashiAccount) {
|
||||||
|
zashiTransactions
|
||||||
|
} else {
|
||||||
|
observeTransactions(
|
||||||
|
accountFlow = accountDataSource.selectedAccount.map { it?.sdkAccount?.accountUuid }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.stateIn(
|
||||||
|
scope = scope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5.seconds, Duration.ZERO),
|
||||||
|
initialValue = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private fun TransactionRepositoryImpl.observeTransactions(accountFlow: Flow<AccountUuid?>) =
|
||||||
combine(
|
combine(
|
||||||
synchronizerProvider.synchronizer,
|
synchronizerProvider.synchronizer,
|
||||||
accountDataSource.selectedAccount.map { it?.sdkAccount }
|
accountFlow
|
||||||
) { synchronizer, account ->
|
) { synchronizer, account ->
|
||||||
synchronizer to account
|
synchronizer to account
|
||||||
}.distinctUntilChanged()
|
}.distinctUntilChanged()
|
||||||
.flatMapLatest { (synchronizer, account) ->
|
.flatMapLatest { (synchronizer, accountUuid) ->
|
||||||
if (synchronizer == null || account == null) {
|
if (synchronizer == null || accountUuid == null) {
|
||||||
flowOf(null)
|
flowOf(null)
|
||||||
} else {
|
} else {
|
||||||
channelFlow<List<Transaction>?> {
|
channelFlow<List<Transaction>?> {
|
||||||
|
@ -74,144 +108,9 @@ class TransactionRepositoryImpl(
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
synchronizer
|
synchronizer
|
||||||
.getTransactions(account.accountUuid)
|
.getTransactions(accountUuid)
|
||||||
.mapLatest { transactions ->
|
.mapLatest { transactions ->
|
||||||
transactions
|
createTransactions(transactions = transactions, synchronizer = synchronizer)
|
||||||
.map { transaction ->
|
|
||||||
when (transaction.transactionState) {
|
|
||||||
Expired ->
|
|
||||||
when {
|
|
||||||
transaction.isShielding ->
|
|
||||||
ShieldTransaction.Failed(
|
|
||||||
timestamp =
|
|
||||||
createTimestamp(transaction) ?: Instant.now(),
|
|
||||||
transactionOutputs =
|
|
||||||
synchronizer.getTransactionOutputs
|
|
||||||
(transaction),
|
|
||||||
amount = transaction.totalSpent,
|
|
||||||
id = transaction.txId,
|
|
||||||
memoCount = transaction.memoCount,
|
|
||||||
fee = transaction.netValue,
|
|
||||||
overview = transaction
|
|
||||||
)
|
|
||||||
|
|
||||||
transaction.isSentTransaction ->
|
|
||||||
SendTransaction.Failed(
|
|
||||||
timestamp =
|
|
||||||
createTimestamp(transaction) ?: Instant.now(),
|
|
||||||
transactionOutputs =
|
|
||||||
synchronizer.getTransactionOutputs
|
|
||||||
(transaction),
|
|
||||||
amount = transaction.netValue,
|
|
||||||
id = transaction.txId,
|
|
||||||
memoCount = transaction.memoCount,
|
|
||||||
fee = transaction.feePaid,
|
|
||||||
overview = transaction
|
|
||||||
)
|
|
||||||
|
|
||||||
else ->
|
|
||||||
ReceiveTransaction.Failed(
|
|
||||||
timestamp =
|
|
||||||
createTimestamp(transaction) ?: Instant.now(),
|
|
||||||
transactionOutputs =
|
|
||||||
synchronizer.getTransactionOutputs(transaction),
|
|
||||||
amount = transaction.netValue,
|
|
||||||
id = transaction.txId,
|
|
||||||
memoCount = transaction.memoCount,
|
|
||||||
overview = transaction
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Confirmed ->
|
|
||||||
when {
|
|
||||||
transaction.isShielding ->
|
|
||||||
ShieldTransaction.Success(
|
|
||||||
timestamp =
|
|
||||||
createTimestamp(transaction) ?: Instant.now(),
|
|
||||||
transactionOutputs =
|
|
||||||
synchronizer.getTransactionOutputs
|
|
||||||
(transaction),
|
|
||||||
amount = transaction.totalSpent,
|
|
||||||
id = transaction.txId,
|
|
||||||
memoCount = transaction.memoCount,
|
|
||||||
fee = transaction.netValue,
|
|
||||||
overview = transaction
|
|
||||||
)
|
|
||||||
|
|
||||||
transaction.isSentTransaction ->
|
|
||||||
SendTransaction.Success(
|
|
||||||
timestamp =
|
|
||||||
createTimestamp(transaction) ?: Instant.now(),
|
|
||||||
transactionOutputs =
|
|
||||||
synchronizer.getTransactionOutputs
|
|
||||||
(transaction),
|
|
||||||
amount = transaction.netValue,
|
|
||||||
id = transaction.txId,
|
|
||||||
memoCount = transaction.memoCount,
|
|
||||||
fee = transaction.feePaid,
|
|
||||||
overview = transaction
|
|
||||||
)
|
|
||||||
|
|
||||||
else ->
|
|
||||||
ReceiveTransaction.Success(
|
|
||||||
timestamp =
|
|
||||||
createTimestamp(transaction) ?: Instant.now(),
|
|
||||||
transactionOutputs =
|
|
||||||
synchronizer.getTransactionOutputs
|
|
||||||
(transaction),
|
|
||||||
amount = transaction.netValue,
|
|
||||||
id = transaction.txId,
|
|
||||||
memoCount = transaction.memoCount,
|
|
||||||
overview = transaction
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Pending ->
|
|
||||||
when {
|
|
||||||
transaction.isShielding ->
|
|
||||||
ShieldTransaction.Pending(
|
|
||||||
timestamp = createTimestamp(transaction),
|
|
||||||
transactionOutputs =
|
|
||||||
synchronizer.getTransactionOutputs
|
|
||||||
(transaction),
|
|
||||||
amount = transaction.totalSpent,
|
|
||||||
id = transaction.txId,
|
|
||||||
memoCount = transaction.memoCount,
|
|
||||||
fee = transaction.netValue,
|
|
||||||
overview = transaction
|
|
||||||
)
|
|
||||||
|
|
||||||
transaction.isSentTransaction ->
|
|
||||||
SendTransaction.Pending(
|
|
||||||
timestamp = createTimestamp(transaction),
|
|
||||||
transactionOutputs =
|
|
||||||
synchronizer.getTransactionOutputs
|
|
||||||
(transaction),
|
|
||||||
amount = transaction.netValue,
|
|
||||||
id = transaction.txId,
|
|
||||||
memoCount = transaction.memoCount,
|
|
||||||
fee = transaction.feePaid,
|
|
||||||
overview = transaction
|
|
||||||
)
|
|
||||||
|
|
||||||
else ->
|
|
||||||
ReceiveTransaction.Pending(
|
|
||||||
timestamp = createTimestamp(transaction),
|
|
||||||
transactionOutputs =
|
|
||||||
synchronizer.getTransactionOutputs
|
|
||||||
(transaction),
|
|
||||||
amount = transaction.netValue,
|
|
||||||
id = transaction.txId,
|
|
||||||
memoCount = transaction.memoCount,
|
|
||||||
overview = transaction
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> error("Unexpected transaction stat")
|
|
||||||
}
|
|
||||||
}.sortedByDescending { transaction ->
|
|
||||||
transaction.timestamp ?: Instant.now()
|
|
||||||
}
|
|
||||||
}.collect {
|
}.collect {
|
||||||
send(it)
|
send(it)
|
||||||
}
|
}
|
||||||
|
@ -222,11 +121,147 @@ class TransactionRepositoryImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.stateIn(
|
}
|
||||||
scope = scope,
|
|
||||||
started = SharingStarted.WhileSubscribed(5.seconds, Duration.ZERO),
|
private suspend fun createTransactions(
|
||||||
initialValue = null
|
transactions: List<TransactionOverview>,
|
||||||
)
|
synchronizer: Synchronizer
|
||||||
|
) = transactions
|
||||||
|
.map { transaction ->
|
||||||
|
when (transaction.transactionState) {
|
||||||
|
Expired ->
|
||||||
|
when {
|
||||||
|
transaction.isShielding ->
|
||||||
|
ShieldTransaction.Failed(
|
||||||
|
timestamp =
|
||||||
|
createTimestamp(transaction) ?: Instant.now(),
|
||||||
|
transactionOutputs =
|
||||||
|
synchronizer.getTransactionOutputs
|
||||||
|
(transaction),
|
||||||
|
amount = transaction.totalSpent,
|
||||||
|
id = transaction.txId,
|
||||||
|
memoCount = transaction.memoCount,
|
||||||
|
fee = transaction.netValue,
|
||||||
|
overview = transaction
|
||||||
|
)
|
||||||
|
|
||||||
|
transaction.isSentTransaction ->
|
||||||
|
SendTransaction.Failed(
|
||||||
|
timestamp =
|
||||||
|
createTimestamp(transaction) ?: Instant.now(),
|
||||||
|
transactionOutputs =
|
||||||
|
synchronizer.getTransactionOutputs
|
||||||
|
(transaction),
|
||||||
|
amount = transaction.netValue,
|
||||||
|
id = transaction.txId,
|
||||||
|
memoCount = transaction.memoCount,
|
||||||
|
fee = transaction.feePaid,
|
||||||
|
overview = transaction
|
||||||
|
)
|
||||||
|
|
||||||
|
else ->
|
||||||
|
ReceiveTransaction.Failed(
|
||||||
|
timestamp =
|
||||||
|
createTimestamp(transaction) ?: Instant.now(),
|
||||||
|
transactionOutputs =
|
||||||
|
synchronizer.getTransactionOutputs(transaction),
|
||||||
|
amount = transaction.netValue,
|
||||||
|
id = transaction.txId,
|
||||||
|
memoCount = transaction.memoCount,
|
||||||
|
overview = transaction
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Confirmed ->
|
||||||
|
when {
|
||||||
|
transaction.isShielding ->
|
||||||
|
ShieldTransaction.Success(
|
||||||
|
timestamp =
|
||||||
|
createTimestamp(transaction) ?: Instant.now(),
|
||||||
|
transactionOutputs =
|
||||||
|
synchronizer.getTransactionOutputs
|
||||||
|
(transaction),
|
||||||
|
amount = transaction.totalSpent,
|
||||||
|
id = transaction.txId,
|
||||||
|
memoCount = transaction.memoCount,
|
||||||
|
fee = transaction.netValue,
|
||||||
|
overview = transaction
|
||||||
|
)
|
||||||
|
|
||||||
|
transaction.isSentTransaction ->
|
||||||
|
SendTransaction.Success(
|
||||||
|
timestamp =
|
||||||
|
createTimestamp(transaction) ?: Instant.now(),
|
||||||
|
transactionOutputs =
|
||||||
|
synchronizer.getTransactionOutputs
|
||||||
|
(transaction),
|
||||||
|
amount = transaction.netValue,
|
||||||
|
id = transaction.txId,
|
||||||
|
memoCount = transaction.memoCount,
|
||||||
|
fee = transaction.feePaid,
|
||||||
|
overview = transaction
|
||||||
|
)
|
||||||
|
|
||||||
|
else ->
|
||||||
|
ReceiveTransaction.Success(
|
||||||
|
timestamp =
|
||||||
|
createTimestamp(transaction) ?: Instant.now(),
|
||||||
|
transactionOutputs =
|
||||||
|
synchronizer.getTransactionOutputs
|
||||||
|
(transaction),
|
||||||
|
amount = transaction.netValue,
|
||||||
|
id = transaction.txId,
|
||||||
|
memoCount = transaction.memoCount,
|
||||||
|
overview = transaction
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Pending ->
|
||||||
|
when {
|
||||||
|
transaction.isShielding ->
|
||||||
|
ShieldTransaction.Pending(
|
||||||
|
timestamp = createTimestamp(transaction),
|
||||||
|
transactionOutputs =
|
||||||
|
synchronizer.getTransactionOutputs
|
||||||
|
(transaction),
|
||||||
|
amount = transaction.totalSpent,
|
||||||
|
id = transaction.txId,
|
||||||
|
memoCount = transaction.memoCount,
|
||||||
|
fee = transaction.netValue,
|
||||||
|
overview = transaction
|
||||||
|
)
|
||||||
|
|
||||||
|
transaction.isSentTransaction ->
|
||||||
|
SendTransaction.Pending(
|
||||||
|
timestamp = createTimestamp(transaction),
|
||||||
|
transactionOutputs =
|
||||||
|
synchronizer.getTransactionOutputs
|
||||||
|
(transaction),
|
||||||
|
amount = transaction.netValue,
|
||||||
|
id = transaction.txId,
|
||||||
|
memoCount = transaction.memoCount,
|
||||||
|
fee = transaction.feePaid,
|
||||||
|
overview = transaction
|
||||||
|
)
|
||||||
|
|
||||||
|
else ->
|
||||||
|
ReceiveTransaction.Pending(
|
||||||
|
timestamp = createTimestamp(transaction),
|
||||||
|
transactionOutputs =
|
||||||
|
synchronizer.getTransactionOutputs
|
||||||
|
(transaction),
|
||||||
|
amount = transaction.netValue,
|
||||||
|
id = transaction.txId,
|
||||||
|
memoCount = transaction.memoCount,
|
||||||
|
overview = transaction
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> error("Unexpected transaction stat")
|
||||||
|
}
|
||||||
|
}.sortedByDescending { transaction ->
|
||||||
|
transaction.timestamp ?: Instant.now()
|
||||||
|
}
|
||||||
|
|
||||||
private fun createTimestamp(transaction: TransactionOverview): Instant? =
|
private fun createTimestamp(transaction: TransactionOverview): Instant? =
|
||||||
transaction.blockTimeEpochSeconds?.let {
|
transaction.blockTimeEpochSeconds?.let {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import android.app.Application
|
||||||
import cash.z.ecc.android.sdk.SdkSynchronizer
|
import cash.z.ecc.android.sdk.SdkSynchronizer
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
import cash.z.ecc.android.sdk.WalletInitMode
|
import cash.z.ecc.android.sdk.WalletInitMode
|
||||||
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
|
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.FastestServersResult
|
import cash.z.ecc.android.sdk.model.FastestServersResult
|
||||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||||
|
@ -23,7 +22,6 @@ import co.electriccoin.zcash.ui.common.model.OnboardingState
|
||||||
import co.electriccoin.zcash.ui.common.model.WalletAccount
|
import co.electriccoin.zcash.ui.common.model.WalletAccount
|
||||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||||
import co.electriccoin.zcash.ui.common.model.ZashiAccount
|
|
||||||
import co.electriccoin.zcash.ui.common.provider.GetDefaultServersProvider
|
import co.electriccoin.zcash.ui.common.provider.GetDefaultServersProvider
|
||||||
import co.electriccoin.zcash.ui.common.provider.PersistableWalletProvider
|
import co.electriccoin.zcash.ui.common.provider.PersistableWalletProvider
|
||||||
import co.electriccoin.zcash.ui.common.provider.SynchronizerProvider
|
import co.electriccoin.zcash.ui.common.provider.SynchronizerProvider
|
||||||
|
@ -143,6 +141,7 @@ class WalletRepositoryImpl(
|
||||||
OnboardingState.NEEDS_WARN,
|
OnboardingState.NEEDS_WARN,
|
||||||
OnboardingState.NEEDS_BACKUP,
|
OnboardingState.NEEDS_BACKUP,
|
||||||
OnboardingState.NONE -> SecretState.NONE
|
OnboardingState.NONE -> SecretState.NONE
|
||||||
|
|
||||||
OnboardingState.READY -> SecretState.READY
|
OnboardingState.READY -> SecretState.READY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,13 +189,11 @@ class WalletRepositoryImpl(
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
override val currentWalletSnapshot: StateFlow<WalletSnapshot?> =
|
override val currentWalletSnapshot: StateFlow<WalletSnapshot?> =
|
||||||
combine(synchronizer, currentAccount) { synchronizer, currentAccount ->
|
synchronizer.flatMapLatest { synchronizer ->
|
||||||
synchronizer to currentAccount
|
if (synchronizer == null) {
|
||||||
}.flatMapLatest { (synchronizer, currentAccount) ->
|
|
||||||
if (synchronizer == null || currentAccount == null) {
|
|
||||||
flowOf(null)
|
flowOf(null)
|
||||||
} else {
|
} else {
|
||||||
toWalletSnapshot(synchronizer, currentAccount)
|
toWalletSnapshot(synchronizer)
|
||||||
}
|
}
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
|
@ -355,29 +352,14 @@ private fun Synchronizer.toCommonError(): Flow<SynchronizerError?> =
|
||||||
|
|
||||||
// No good way around needing magic numbers for the indices
|
// No good way around needing magic numbers for the indices
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
private fun toWalletSnapshot(
|
private fun toWalletSnapshot(synchronizer: Synchronizer) = combine(
|
||||||
synchronizer: Synchronizer,
|
synchronizer.status, // 0
|
||||||
account: WalletAccount
|
synchronizer.progress, // 1
|
||||||
) = combine(
|
synchronizer.toCommonError() // 2
|
||||||
// 0
|
|
||||||
synchronizer.status,
|
|
||||||
// 1
|
|
||||||
synchronizer.processorInfo,
|
|
||||||
// 2
|
|
||||||
synchronizer.progress,
|
|
||||||
// 3
|
|
||||||
synchronizer.toCommonError()
|
|
||||||
) { flows ->
|
) { flows ->
|
||||||
val progressPercentDecimal = (flows[2] as PercentDecimal)
|
|
||||||
|
|
||||||
WalletSnapshot(
|
WalletSnapshot(
|
||||||
isZashi = account is ZashiAccount,
|
|
||||||
status = flows[0] as Synchronizer.Status,
|
status = flows[0] as Synchronizer.Status,
|
||||||
processorInfo = flows[1] as CompactBlockProcessor.ProcessorInfo,
|
progress = flows[1] as PercentDecimal,
|
||||||
orchardBalance = account.unified.balance,
|
synchronizerError = flows[2] as SynchronizerError?
|
||||||
saplingBalance = account.sapling?.balance,
|
|
||||||
transparentBalance = account.transparent.balance,
|
|
||||||
progress = progressPercentDecimal,
|
|
||||||
synchronizerError = flows[3] as SynchronizerError?
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,22 @@
|
||||||
package co.electriccoin.zcash.ui.common.usecase
|
package co.electriccoin.zcash.ui.common.usecase
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
import co.electriccoin.zcash.spackle.Twig
|
||||||
|
import co.electriccoin.zcash.ui.common.datasource.MessageAvailabilityDataSource
|
||||||
import co.electriccoin.zcash.ui.common.datasource.WalletBackupAvailability
|
import co.electriccoin.zcash.ui.common.datasource.WalletBackupAvailability
|
||||||
import co.electriccoin.zcash.ui.common.datasource.WalletBackupDataSource
|
import co.electriccoin.zcash.ui.common.datasource.WalletBackupDataSource
|
||||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||||
|
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||||
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository
|
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository
|
||||||
|
import co.electriccoin.zcash.ui.common.repository.HomeMessageCacheRepository
|
||||||
|
import co.electriccoin.zcash.ui.common.repository.HomeMessageData
|
||||||
|
import co.electriccoin.zcash.ui.common.repository.ReceiveTransaction
|
||||||
|
import co.electriccoin.zcash.ui.common.repository.RuntimeMessage
|
||||||
import co.electriccoin.zcash.ui.common.repository.ShieldFundsData
|
import co.electriccoin.zcash.ui.common.repository.ShieldFundsData
|
||||||
import co.electriccoin.zcash.ui.common.repository.ShieldFundsRepository
|
import co.electriccoin.zcash.ui.common.repository.ShieldFundsRepository
|
||||||
|
import co.electriccoin.zcash.ui.common.repository.TransactionRepository
|
||||||
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
||||||
import co.electriccoin.zcash.ui.common.viewmodel.SynchronizerError
|
|
||||||
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
@ -18,69 +25,117 @@ import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class GetHomeMessageUseCase(
|
class GetHomeMessageUseCase(
|
||||||
private val walletRepository: WalletRepository,
|
walletRepository: WalletRepository,
|
||||||
private val walletBackupDataSource: WalletBackupDataSource,
|
walletBackupDataSource: WalletBackupDataSource,
|
||||||
private val exchangeRateRepository: ExchangeRateRepository,
|
exchangeRateRepository: ExchangeRateRepository,
|
||||||
private val shieldFundsRepository: ShieldFundsRepository,
|
shieldFundsRepository: ShieldFundsRepository,
|
||||||
|
transactionRepository: TransactionRepository,
|
||||||
|
private val messageAvailabilityDataSource: MessageAvailabilityDataSource,
|
||||||
|
private val cache: HomeMessageCacheRepository,
|
||||||
) {
|
) {
|
||||||
|
private val backupFlow = combine(
|
||||||
|
transactionRepository.zashiTransactions,
|
||||||
|
walletBackupDataSource.observe()
|
||||||
|
) { transactions, backup ->
|
||||||
|
if (backup is WalletBackupAvailability.Available && transactions.orEmpty().any { it is ReceiveTransaction }) {
|
||||||
|
backup
|
||||||
|
} else {
|
||||||
|
WalletBackupAvailability.Unavailable
|
||||||
|
}
|
||||||
|
}.distinctUntilChanged()
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
fun observe(): Flow<HomeMessageData?> = combine(
|
private val flow = combine(
|
||||||
walletRepository.currentWalletSnapshot.filterNotNull(),
|
walletRepository.currentWalletSnapshot.filterNotNull(),
|
||||||
walletRepository.walletRestoringState,
|
walletRepository.walletRestoringState.filterNotNull(),
|
||||||
walletBackupDataSource.observe(),
|
backupFlow,
|
||||||
exchangeRateRepository.state.map { it == ExchangeRateState.OptIn }.distinctUntilChanged(),
|
exchangeRateRepository.state.map { it == ExchangeRateState.OptIn }.distinctUntilChanged(),
|
||||||
shieldFundsRepository.availability
|
shieldFundsRepository.availability
|
||||||
) { walletSnapshot, walletStateInformation, backup, isCCAvailable, shieldFunds ->
|
) { walletSnapshot, walletStateInformation, backup, isCCAvailable, shieldFunds ->
|
||||||
when {
|
createMessage(walletSnapshot, walletStateInformation, backup, shieldFunds, isCCAvailable)
|
||||||
walletSnapshot.synchronizerError != null -> {
|
}.debounce(.5.seconds)
|
||||||
HomeMessageData.Error(walletSnapshot.synchronizerError)
|
.map { message -> prioritizeMessage(message) }
|
||||||
}
|
|
||||||
|
|
||||||
walletSnapshot.status == Synchronizer.Status.DISCONNECTED -> {
|
fun observe(): Flow<HomeMessageData?> = flow
|
||||||
HomeMessageData.Disconnected
|
|
||||||
}
|
|
||||||
|
|
||||||
walletSnapshot.status in listOf(
|
private fun createMessage(
|
||||||
Synchronizer.Status.INITIALIZING,
|
walletSnapshot: WalletSnapshot,
|
||||||
Synchronizer.Status.SYNCING,
|
walletStateInformation: WalletRestoringState,
|
||||||
Synchronizer.Status.STOPPED
|
backup: WalletBackupAvailability,
|
||||||
) -> {
|
shieldFunds: ShieldFundsData,
|
||||||
val progress = walletSnapshot.progress.decimal * 100f
|
isCCAvailable: Boolean
|
||||||
val result = when {
|
) = when {
|
||||||
walletStateInformation == WalletRestoringState.RESTORING -> {
|
walletSnapshot.synchronizerError != null -> HomeMessageData.Error(walletSnapshot.synchronizerError)
|
||||||
HomeMessageData.Restoring(
|
|
||||||
progress = progress,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
walletSnapshot.status == Synchronizer.Status.DISCONNECTED -> HomeMessageData.Disconnected
|
||||||
HomeMessageData.Syncing(progress = progress)
|
|
||||||
}
|
walletSnapshot.status in listOf(
|
||||||
|
Synchronizer.Status.INITIALIZING,
|
||||||
|
Synchronizer.Status.SYNCING,
|
||||||
|
Synchronizer.Status.STOPPED
|
||||||
|
) -> {
|
||||||
|
val progress = walletSnapshot.progress.decimal * 100f
|
||||||
|
val result = when {
|
||||||
|
walletStateInformation == WalletRestoringState.RESTORING -> {
|
||||||
|
HomeMessageData.Restoring(
|
||||||
|
progress = progress,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
HomeMessageData.Syncing(progress = progress)
|
||||||
}
|
}
|
||||||
result
|
|
||||||
}
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
shieldFunds is ShieldFundsData.Available -> HomeMessageData.ShieldFunds(shieldFunds.amount)
|
backup is WalletBackupAvailability.Available -> HomeMessageData.Backup
|
||||||
|
|
||||||
backup is WalletBackupAvailability.Available -> HomeMessageData.Backup
|
shieldFunds is ShieldFundsData.Available -> HomeMessageData.ShieldFunds(shieldFunds.amount)
|
||||||
|
|
||||||
isCCAvailable -> HomeMessageData.EnableCurrencyConversion
|
isCCAvailable -> HomeMessageData.EnableCurrencyConversion
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prioritizeMessage(message: HomeMessageData?): HomeMessageData? {
|
||||||
|
val isSameMessageUpdate = message?.priority == cache.lastMessage?.priority // same but updated
|
||||||
|
val someMessageBeenShown = cache.lastShownMessage != null // has any message been shown while app in fg
|
||||||
|
val hasNoMessageBeenShownLately = cache.lastMessage == null // has no message been shown
|
||||||
|
val isHigherPriorityMessage = (message?.priority ?: 0) > (cache.lastShownMessage?.priority ?: 0)
|
||||||
|
val result = when {
|
||||||
|
message == null -> null
|
||||||
|
message is RuntimeMessage -> message
|
||||||
|
isSameMessageUpdate -> message
|
||||||
|
isHigherPriorityMessage -> if (hasNoMessageBeenShownLately) {
|
||||||
|
if (someMessageBeenShown) null else message
|
||||||
|
} else {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}.debounce(.5.seconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed interface HomeMessageData {
|
if (result != null) {
|
||||||
data object EnableCurrencyConversion : HomeMessageData
|
messageAvailabilityDataSource.onMessageShown()
|
||||||
data class ShieldFunds(val zatoshi: Zatoshi) : HomeMessageData
|
cache.lastShownMessage = result
|
||||||
data object Backup : HomeMessageData
|
}
|
||||||
data object Disconnected : HomeMessageData
|
cache.lastMessage = result
|
||||||
data class Error(val synchronizerError: SynchronizerError) : HomeMessageData
|
|
||||||
data class Restoring(val progress: Float) : HomeMessageData
|
Twig.debug {
|
||||||
data class Syncing(val progress: Float) : HomeMessageData
|
when {
|
||||||
data object Updating : HomeMessageData
|
message == null -> "Home message: no message to show"
|
||||||
|
result == null -> "Home message: ${message::class.simpleName} was filtered out"
|
||||||
|
else -> {
|
||||||
|
"Home message: ${result::class.simpleName} shown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
package co.electriccoin.zcash.ui.common.usecase
|
package co.electriccoin.zcash.ui.common.usecase
|
||||||
|
|
||||||
import co.electriccoin.zcash.ui.common.repository.AddressBookRepository
|
import co.electriccoin.zcash.ui.common.repository.AddressBookRepository
|
||||||
|
import co.electriccoin.zcash.ui.common.repository.HomeMessageCacheRepository
|
||||||
|
|
||||||
class ResetInMemoryDataUseCase(
|
class ResetInMemoryDataUseCase(
|
||||||
private val addressBookRepository: AddressBookRepository,
|
private val addressBookRepository: AddressBookRepository,
|
||||||
|
private val homeMessageCacheRepository: HomeMessageCacheRepository
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke() {
|
suspend operator fun invoke() {
|
||||||
addressBookRepository.resetAddressBook()
|
addressBookRepository.resetAddressBook()
|
||||||
|
homeMessageCacheRepository.reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
package co.electriccoin.zcash.ui.fixture
|
|
||||||
|
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
|
||||||
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
|
|
||||||
import cash.z.ecc.android.sdk.fixture.WalletBalanceFixture
|
|
||||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
|
||||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
|
||||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
|
||||||
import cash.z.ecc.sdk.fixture.ZatoshiFixture
|
|
||||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
|
||||||
import co.electriccoin.zcash.ui.common.viewmodel.SynchronizerError
|
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
|
||||||
object WalletSnapshotFixture {
|
|
||||||
val STATUS = Synchronizer.Status.SYNCED
|
|
||||||
val PROGRESS = PercentDecimal.ZERO_PERCENT
|
|
||||||
val TRANSPARENT_BALANCE: Zatoshi = ZatoshiFixture.new(8)
|
|
||||||
val ORCHARD_BALANCE: WalletBalance = WalletBalanceFixture.newLong(8, 2, 1)
|
|
||||||
val SAPLING_BALANCE: WalletBalance = WalletBalanceFixture.newLong(5, 2, 1)
|
|
||||||
|
|
||||||
// Should fill in with non-empty values for better example values in tests and UI previews
|
|
||||||
@Suppress("LongParameterList")
|
|
||||||
fun new(
|
|
||||||
status: Synchronizer.Status = STATUS,
|
|
||||||
processorInfo: CompactBlockProcessor.ProcessorInfo =
|
|
||||||
CompactBlockProcessor.ProcessorInfo(
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
),
|
|
||||||
orchardBalance: WalletBalance = ORCHARD_BALANCE,
|
|
||||||
saplingBalance: WalletBalance = SAPLING_BALANCE,
|
|
||||||
transparentBalance: Zatoshi = TRANSPARENT_BALANCE,
|
|
||||||
progress: PercentDecimal = PROGRESS,
|
|
||||||
synchronizerError: SynchronizerError? = null
|
|
||||||
) = WalletSnapshot(
|
|
||||||
status = status,
|
|
||||||
processorInfo = processorInfo,
|
|
||||||
orchardBalance = orchardBalance,
|
|
||||||
saplingBalance = saplingBalance,
|
|
||||||
transparentBalance = transparentBalance,
|
|
||||||
progress = progress,
|
|
||||||
synchronizerError = synchronizerError,
|
|
||||||
isZashi = false,
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -12,7 +12,7 @@ import co.electriccoin.zcash.ui.common.model.WalletAccount
|
||||||
import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider
|
import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider
|
||||||
import co.electriccoin.zcash.ui.common.usecase.GetHomeMessageUseCase
|
import co.electriccoin.zcash.ui.common.usecase.GetHomeMessageUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.GetSelectedWalletAccountUseCase
|
import co.electriccoin.zcash.ui.common.usecase.GetSelectedWalletAccountUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.HomeMessageData
|
import co.electriccoin.zcash.ui.common.repository.HomeMessageData
|
||||||
import co.electriccoin.zcash.ui.common.usecase.IsRestoreSuccessDialogVisibleUseCase
|
import co.electriccoin.zcash.ui.common.usecase.IsRestoreSuccessDialogVisibleUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.NavigateToCoinbaseUseCase
|
import co.electriccoin.zcash.ui.common.usecase.NavigateToCoinbaseUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ShieldFundsUseCase
|
import co.electriccoin.zcash.ui.common.usecase.ShieldFundsUseCase
|
||||||
|
@ -246,6 +246,6 @@ class HomeViewModel(
|
||||||
// ),
|
// ),
|
||||||
// fullStackTrace = walletSnapshot.synchronizerError.getStackTrace(limit = null)
|
// fullStackTrace = walletSnapshot.synchronizerError.getStackTrace(limit = null)
|
||||||
// )
|
// )
|
||||||
TODO()
|
// TODO()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import co.electriccoin.zcash.ui.Navigator
|
||||||
import co.electriccoin.zcash.ui.NavigatorImpl
|
import co.electriccoin.zcash.ui.NavigatorImpl
|
||||||
import co.electriccoin.zcash.ui.common.compose.LocalActivity
|
import co.electriccoin.zcash.ui.common.compose.LocalActivity
|
||||||
import co.electriccoin.zcash.ui.common.compose.LocalNavController
|
import co.electriccoin.zcash.ui.common.compose.LocalNavController
|
||||||
|
import co.electriccoin.zcash.ui.common.datasource.MessageAvailabilityDataSource
|
||||||
import co.electriccoin.zcash.ui.common.model.OnboardingState
|
import co.electriccoin.zcash.ui.common.model.OnboardingState
|
||||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||||
|
@ -50,25 +51,29 @@ import org.koin.compose.koinInject
|
||||||
@Composable
|
@Composable
|
||||||
fun MainActivity.OnboardingNavigation() {
|
fun MainActivity.OnboardingNavigation() {
|
||||||
val activity = LocalActivity.current
|
val activity = LocalActivity.current
|
||||||
val navigationRouter = koinInject<NavigationRouter>()
|
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val keyboardManager = LocalKeyboardManager.current
|
val keyboardManager = LocalKeyboardManager.current
|
||||||
val flexaViewModel = koinViewModel<FlexaViewModel>()
|
|
||||||
val sheetStateManager = LocalSheetStateManager.current
|
val sheetStateManager = LocalSheetStateManager.current
|
||||||
|
val navigationRouter = koinInject<NavigationRouter>()
|
||||||
|
val flexaViewModel = koinViewModel<FlexaViewModel>()
|
||||||
|
val messageAvailabilityDataSource = koinInject<MessageAvailabilityDataSource>()
|
||||||
|
|
||||||
|
|
||||||
val navigator: Navigator =
|
val navigator: Navigator =
|
||||||
remember(
|
remember(
|
||||||
navController,
|
navController,
|
||||||
flexaViewModel,
|
flexaViewModel,
|
||||||
keyboardManager,
|
keyboardManager,
|
||||||
sheetStateManager
|
sheetStateManager,
|
||||||
|
messageAvailabilityDataSource
|
||||||
) {
|
) {
|
||||||
NavigatorImpl(
|
NavigatorImpl(
|
||||||
activity = this@OnboardingNavigation,
|
activity = this@OnboardingNavigation,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
flexaViewModel = flexaViewModel,
|
flexaViewModel = flexaViewModel,
|
||||||
keyboardManager = keyboardManager,
|
keyboardManager = keyboardManager,
|
||||||
sheetStateManager = sheetStateManager
|
sheetStateManager = sheetStateManager,
|
||||||
|
messageAvailabilityDataSource = messageAvailabilityDataSource
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,8 +77,7 @@ class TransactionHistoryWidgetViewModel(
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||||
initialValue =
|
initialValue = TransactionHistoryWidgetState.Loading
|
||||||
TransactionHistoryWidgetState.Loading
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun onTransactionClick(transaction: Transaction) {
|
private fun onTransactionClick(transaction: Transaction) {
|
||||||
|
|
Loading…
Reference in New Issue