Updated on 2026-08-14
This commit is contained in:
parent
a0f1a6591c
commit
e993dc0647
119 changed files with 4449 additions and 720 deletions
3
.idea/codeStyles/Project.xml
generated
3
.idea/codeStyles/Project.xml
generated
|
|
@ -12,6 +12,7 @@
|
|||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||
<option name="ALLOW_TRAILING_COMMA" value="true" />
|
||||
<option name="BLANK_LINES_BEFORE_DECLARATION_WITH_COMMENT_OR_ANNOTATION_ON_SEPARATE_LINE" value="0" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<Properties>
|
||||
|
|
@ -199,4 +200,4 @@
|
|||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
</component>
|
||||
|
|
|
|||
2
.idea/codeStyles/codeStyleConfig.xml
generated
2
.idea/codeStyles/codeStyleConfig.xml
generated
|
|
@ -2,4 +2,4 @@
|
|||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
</component>
|
||||
|
|
|
|||
4
.idea/inspectionProfiles/Project_Default.xml
generated
4
.idea/inspectionProfiles/Project_Default.xml
generated
|
|
@ -61,8 +61,8 @@
|
|||
<inspection_tool class="RedundantSemicolon" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantSetter" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantSuspendModifier" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantUnitExpression" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantUnitReturnType" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantUnitExpression" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="RedundantUnitReturnType" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="RedundantVisibilityModifier" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantWith" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RemoveCurlyBracesFromTemplate" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ android {
|
|||
initWith(getByName("release"))
|
||||
versionNameSuffix = "-beta"
|
||||
applicationIdSuffix = ".debug"
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,11 +34,10 @@
|
|||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="true"
|
||||
android:allowBackup="false"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
tools:ignore="GoogleAppIndexingWarning"
|
||||
tools:replace="android:fullBackupContent">
|
||||
tools:replace="android:allowBackup">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.wallet.api.enabled"
|
||||
|
|
@ -142,5 +141,4 @@
|
|||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit a1658496e777b611fc990ef2bc1a1a1fd48bc1e6
|
||||
Subproject commit bfc2bf8157089bce6b44779bdae66df2c920de70
|
||||
115
app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt
Normal file
115
app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
package com.tangem.tap
|
||||
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration
|
||||
|
||||
internal class LockUserWalletsTimer(
|
||||
owner: LifecycleOwner,
|
||||
private val duration: Duration = with(Duration) { 5.minutes },
|
||||
) : LifecycleOwner by owner,
|
||||
DefaultLifecycleObserver {
|
||||
|
||||
private var delayJob: Job? = null
|
||||
set(value) {
|
||||
field?.cancel()
|
||||
field = value
|
||||
}
|
||||
private var isStopped = false
|
||||
private var openWelcomeScreenWhenResumed = false
|
||||
|
||||
init {
|
||||
lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
Timber.d(
|
||||
"""
|
||||
Owner resumed
|
||||
|- Was stopped: $isStopped
|
||||
|- Need to open welcome screen: $openWelcomeScreenWhenResumed
|
||||
""".trimIndent(),
|
||||
)
|
||||
isStopped = false
|
||||
start()
|
||||
if (openWelcomeScreenWhenResumed) {
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
|
||||
openWelcomeScreenWhenResumed = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
Timber.d("Owner stopped")
|
||||
isStopped = true
|
||||
}
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
Timber.d("Owner destroyed")
|
||||
stop()
|
||||
}
|
||||
|
||||
fun restart() {
|
||||
if (delayJob == null) return
|
||||
Timber.d(
|
||||
"""
|
||||
Timer restart
|
||||
|- Duration millis: ${duration.inWholeMilliseconds}
|
||||
""".trimIndent(),
|
||||
)
|
||||
start(log = false)
|
||||
}
|
||||
|
||||
private fun start(log: Boolean = true) {
|
||||
if (log) {
|
||||
Timber.d(
|
||||
"""
|
||||
Timer start
|
||||
|- Duration millis: ${duration.inWholeMilliseconds}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
delayJob = createDelayJob()
|
||||
}
|
||||
|
||||
private fun stop(log: Boolean = true) {
|
||||
if (log) {
|
||||
Timber.d(
|
||||
"""
|
||||
Timer stop
|
||||
|- Was started: ${delayJob?.isActive ?: false}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
delayJob = null
|
||||
}
|
||||
|
||||
private fun createDelayJob(): Job = lifecycleScope.launch(Dispatchers.Default) {
|
||||
val startTime = System.currentTimeMillis()
|
||||
delay(duration)
|
||||
if (isActive) {
|
||||
val userWalletsListManager = userWalletsListManagerSafe ?: return@launch
|
||||
if (userWalletsListManager.hasSavedUserWallets) {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
Timber.d(
|
||||
"""
|
||||
Finished
|
||||
|- App is stopped: $isStopped
|
||||
|- Millis passed: ${currentTime - startTime}
|
||||
""".trimIndent(),
|
||||
)
|
||||
userWalletsListManager.lock()
|
||||
if (isStopped) {
|
||||
openWelcomeScreenWhenResumed = true
|
||||
} else {
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ import com.tangem.tap.common.shop.GooglePayService.Companion.LOAD_PAYMENT_DATA_R
|
|||
import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient
|
||||
import com.tangem.tap.domain.TangemSdkManager
|
||||
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
|
||||
import com.tangem.tap.domain.userWalletList.di.provideDummyImplementation
|
||||
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
|
||||
import com.tangem.tap.features.shop.redux.ShopAction
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -44,6 +44,10 @@ lateinit var tangemSdk: TangemSdk
|
|||
lateinit var tangemSdkManager: TangemSdkManager
|
||||
lateinit var backupService: BackupService
|
||||
lateinit var userWalletsListManager: UserWalletsListManager
|
||||
internal var lockUserWalletsTimer: LockUserWalletsTimer? = null
|
||||
private set
|
||||
var userWalletsListManagerSafe: UserWalletsListManager? = null
|
||||
private set
|
||||
var notificationsHandler: NotificationsHandler? = null
|
||||
|
||||
private val coroutineContext: CoroutineContext
|
||||
|
|
@ -77,7 +81,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
tangemSdkManager = TangemSdkManager(tangemSdk, this)
|
||||
appStateHolder.tangemSdkManager = tangemSdkManager
|
||||
backupService = BackupService.init(tangemSdk, this)
|
||||
userWalletsListManager = UserWalletsListManager.provideDummyImplementation()
|
||||
userWalletsListManager = UserWalletsListManager.provideBiometricImplementation(
|
||||
context = applicationContext,
|
||||
tangemSdkManager = tangemSdkManager,
|
||||
)
|
||||
userWalletsListManagerSafe = userWalletsListManager
|
||||
lockUserWalletsTimer = LockUserWalletsTimer(owner = this)
|
||||
|
||||
store.dispatch(
|
||||
ShopAction.CheckIfGooglePayAvailable(
|
||||
|
|
@ -176,4 +185,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
override fun removeOnActivityResultCallback(callback: OnActivityResultCallback) {
|
||||
onActivityResultCallbacks.remove(callback)
|
||||
}
|
||||
|
||||
override fun onUserInteraction() {
|
||||
super.onUserInteraction()
|
||||
|
||||
lockUserWalletsTimer?.restart()
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ import coil.ImageLoader
|
|||
import coil.ImageLoaderFactory
|
||||
import com.tangem.Log
|
||||
import com.tangem.LogFormat
|
||||
import com.tangem.blockchain.common.BlockchainSdkConfig
|
||||
import com.tangem.blockchain.common.WalletManagerFactory
|
||||
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.domain.DomainLayer
|
||||
|
|
@ -39,8 +41,14 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
|||
import com.tangem.tap.domain.tokens.UserTokensRepository
|
||||
import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator
|
||||
import com.tangem.tap.domain.totalBalance.di.provideDefaultImplementation
|
||||
import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager
|
||||
import com.tangem.tap.domain.walletCurrencies.di.provideDefaultImplementation
|
||||
import com.tangem.tap.domain.walletStores.WalletStoresManager
|
||||
import com.tangem.tap.domain.walletStores.di.provideDummyImplementation
|
||||
import com.tangem.tap.domain.walletStores.di.provideDefaultImplementation
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation
|
||||
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
|
||||
import com.tangem.tap.network.NetworkConnectivity
|
||||
import com.tangem.tap.persistence.CardBalanceStateAdapter
|
||||
|
|
@ -64,10 +72,40 @@ lateinit var shopService: TangemShopService
|
|||
lateinit var assetReader: AssetReader
|
||||
lateinit var userTokensRepository: UserTokensRepository
|
||||
|
||||
val walletStoresManager by lazy {
|
||||
WalletStoresManager.provideDummyImplementation()
|
||||
private val walletStoresRepository by lazy { WalletStoresRepository.provideDefaultImplementation() }
|
||||
private val walletManagersRepository by lazy {
|
||||
WalletManagersRepository.provideDefaultImplementation(
|
||||
walletManagerFactory = WalletManagerFactory(
|
||||
blockchainSdkConfig = store.state.globalState.configManager
|
||||
?.config
|
||||
?.blockchainSdkConfig
|
||||
?: BlockchainSdkConfig(),
|
||||
),
|
||||
)
|
||||
}
|
||||
private val walletAmountsRepository by lazy {
|
||||
WalletAmountsRepository.provideDefaultImplementation(
|
||||
tangemTechService = store.state.domainNetworks.tangemTechService,
|
||||
)
|
||||
}
|
||||
val walletStoresManager by lazy {
|
||||
WalletStoresManager.provideDefaultImplementation(
|
||||
userTokensRepository = userTokensRepository,
|
||||
walletStoresRepository = walletStoresRepository,
|
||||
walletManagersRepository = walletManagersRepository,
|
||||
walletAmountsRepository = walletAmountsRepository,
|
||||
appCurrencyProvider = { store.state.globalState.appCurrency },
|
||||
)
|
||||
}
|
||||
val walletCurrenciesManager by lazy {
|
||||
WalletCurrenciesManager.provideDefaultImplementation(
|
||||
userTokensRepository = userTokensRepository,
|
||||
walletStoresRepository = walletStoresRepository,
|
||||
walletManagersRepository = walletManagersRepository,
|
||||
walletAmountsRepository = walletAmountsRepository,
|
||||
appCurrencyProvider = { store.state.globalState.appCurrency },
|
||||
)
|
||||
}
|
||||
|
||||
val totalFiatBalanceCalculator by lazy {
|
||||
TotalFiatBalanceCalculator.provideDefaultImplementation()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ fun Store<*>.dispatchNotification(resId: Int) {
|
|||
}
|
||||
|
||||
@Suppress("unused") // receiver type
|
||||
fun Store<*>.onUserWalletSelected(userWallet: UserWallet, refresh: Boolean = false) {
|
||||
// TODO: Load tokens for selected user wallet. Will be created in further MRs
|
||||
suspend fun Store<*>.onUserWalletSelected(userWallet: UserWallet, refresh: Boolean = false) {
|
||||
store.state.globalState.tapWalletManager.onWalletSelected(userWallet, refresh)
|
||||
}
|
||||
|
||||
fun Store<*>.dispatchToastNotification(resId: Int) {
|
||||
|
|
@ -77,7 +77,6 @@ suspend fun Store<*>.onCardScanned(scanResponse: ScanResponse) {
|
|||
fun Store<*>.dispatchOpenUrl(url: String) {
|
||||
store.dispatch(NavigationAction.OpenUrl(url))
|
||||
}
|
||||
|
||||
fun Store<*>.dispatchShare(url: String) {
|
||||
store.dispatch(NavigationAction.Share(url))
|
||||
}
|
||||
|
|
@ -71,7 +71,9 @@ data class AppState(
|
|||
companion object {
|
||||
fun getMiddleware(): List<Middleware<AppState>> {
|
||||
return listOf(
|
||||
logMiddleware, navigationMiddleware, notificationsMiddleware,
|
||||
logMiddleware,
|
||||
navigationMiddleware,
|
||||
notificationsMiddleware,
|
||||
GlobalMiddleware.handler,
|
||||
HomeMiddleware.handler,
|
||||
OnboardingNoteMiddleware.handler,
|
||||
|
|
@ -90,6 +92,7 @@ data class AppState(
|
|||
WelcomeMiddleware().middleware,
|
||||
SaveWalletMiddleware().middleware,
|
||||
WalletSelectorMiddleware().middleware,
|
||||
LockUserWalletsTimerMiddleware().middleware,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.tap.common.redux
|
||||
|
||||
import com.tangem.tap.lockUserWalletsTimer
|
||||
import org.rekotlin.Middleware
|
||||
|
||||
class LockUserWalletsTimerMiddleware {
|
||||
val middleware: Middleware<AppState> = { _, _ ->
|
||||
{ nextDispatch ->
|
||||
{ action ->
|
||||
lockUserWalletsTimer?.restart()
|
||||
nextDispatch(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -167,9 +167,9 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
|
|||
scope.launch {
|
||||
tangemSdkManager.changeDisplayedCardIdNumbersCount(null)
|
||||
val result = tangemSdkManager.scanProduct(
|
||||
userTokensRepository,
|
||||
action.additionalBlockchainsToDerive,
|
||||
action.messageResId,
|
||||
userTokensRepository = userTokensRepository,
|
||||
additionalBlockchainsToDerive = action.additionalBlockchainsToDerive,
|
||||
messageRes = action.messageResId,
|
||||
)
|
||||
withMainContext {
|
||||
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
|
||||
|
|
|
|||
|
|
@ -5,11 +5,21 @@ import androidx.annotation.StringRes
|
|||
import com.tangem.Message
|
||||
import com.tangem.TangemSdk
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.*
|
||||
import com.tangem.common.CardFilter
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.SuccessResponse
|
||||
import com.tangem.common.UserCode
|
||||
import com.tangem.common.UserCodeType
|
||||
import com.tangem.common.biometric.BiometricManager
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.common.core.*
|
||||
import com.tangem.common.core.CardIdDisplayFormat
|
||||
import com.tangem.common.core.CardSessionRunnable
|
||||
import com.tangem.common.core.Config
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.core.UserCodeRequestPolicy
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.hdWallet.DerivationPath
|
||||
import com.tangem.common.map
|
||||
import com.tangem.common.usersCode.UserCodeRepository
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
|
|
@ -28,6 +38,7 @@ import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
|
|||
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
|
||||
import com.tangem.tap.domain.tasks.product.ScanProductTask
|
||||
import com.tangem.tap.domain.tokens.UserTokensRepository
|
||||
import com.tangem.tap.domain.userWalletList.di.USER_WALLETS_BIOMETRIC_KEY_NAME
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.isActive
|
||||
|
|
@ -42,8 +53,12 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
val canEnrollBiometrics: Boolean
|
||||
get() = tangemSdk.biometricManager.canEnrollBiometrics
|
||||
|
||||
val biometricManager: BiometricManager
|
||||
get() = tangemSdk.biometricManager
|
||||
|
||||
suspend fun scanProduct(
|
||||
userTokensRepository: UserTokensRepository,
|
||||
cardId: String? = null,
|
||||
additionalBlockchainsToDerive: Collection<Blockchain>? = null,
|
||||
messageRes: Int? = null,
|
||||
useBiometricsForAccessCode: Boolean = false,
|
||||
|
|
@ -52,8 +67,13 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
|
||||
val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header))
|
||||
return runTaskAsyncReturnOnMain(
|
||||
runnable = ScanProductTask(null, userTokensRepository, additionalBlockchainsToDerive),
|
||||
cardId = null, initialMessage = message,
|
||||
runnable = ScanProductTask(
|
||||
card = null,
|
||||
userTokensRepository = userTokensRepository,
|
||||
additionalBlockchainsToDerive = additionalBlockchainsToDerive,
|
||||
),
|
||||
cardId = cardId,
|
||||
initialMessage = message,
|
||||
).also { sendScanResultsToAnalytics(it) }
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +109,9 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
suspend fun derivePublicKeys(
|
||||
cardId: String,
|
||||
derivations: Map<ByteArrayKey, List<DerivationPath>>,
|
||||
useBiometricsForAccessCode: Boolean = false,
|
||||
): CompletionResult<DerivationTaskResponse> {
|
||||
setAccessCodeRequestPolicy(useBiometricsForAccessCode)
|
||||
return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId)
|
||||
}
|
||||
|
||||
|
|
@ -102,6 +124,16 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
.map { CardDTO(it) }
|
||||
}
|
||||
|
||||
suspend fun unlockBiometricKeys(): CompletionResult<Unit> {
|
||||
return biometricManager.authenticate(
|
||||
mode = BiometricManager.AuthenticationMode.Keys(
|
||||
USER_WALLETS_BIOMETRIC_KEY_NAME,
|
||||
tangemSdk.config.userCodesBiometricKeyName,
|
||||
),
|
||||
)
|
||||
.map { /* no-op */ }
|
||||
}
|
||||
|
||||
suspend fun saveAccessCode(accessCode: String, cardsIds: Set<String>): CompletionResult<Unit> {
|
||||
return createUserCodeRepository().save(
|
||||
cardIds = cardsIds,
|
||||
|
|
@ -110,6 +142,20 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
stringValue = accessCode,
|
||||
),
|
||||
)
|
||||
.map {
|
||||
biometricManager.unauthenticate(
|
||||
keyName = tangemSdk.config.userCodesBiometricKeyName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearSavedUserCodes(): CompletionResult<Unit> {
|
||||
return createUserCodeRepository().clear()
|
||||
.map {
|
||||
biometricManager.unauthenticate(
|
||||
keyName = tangemSdk.config.userCodesBiometricKeyName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setPasscode(cardId: String?): CompletionResult<SuccessResponse> {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
package com.tangem.tap.domain
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.BlockchainSdkConfig
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.common.WalletManagerFactory
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
|
|
@ -13,6 +10,7 @@ import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
|||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.common.ThrottlerWithValues
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.operations.attestation.Attestation
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.safeUpdate
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
|
|
@ -20,16 +18,21 @@ import com.tangem.tap.domain.configurable.config.ConfigManager
|
|||
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
|
||||
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
|
||||
import com.tangem.tap.domain.extensions.makeWalletManagersForApp
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.domain.walletStores.WalletStoresError
|
||||
import com.tangem.tap.features.demo.isDemoCard
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
|
||||
import com.tangem.tap.features.wallet.models.toBlockchainNetworks
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.network.NetworkConnectivity
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.userTokensRepository
|
||||
import com.tangem.tap.walletStoresManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
class TapWalletManager {
|
||||
val walletManagerFactory: WalletManagerFactory
|
||||
|
|
@ -81,6 +84,60 @@ class TapWalletManager {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun onWalletSelected(userWallet: UserWallet, refresh: Boolean) {
|
||||
val scanResponse = userWallet.scanResponse
|
||||
val card = scanResponse.card
|
||||
val attestationFailed = card.attestation.status == Attestation.Status.Failed
|
||||
|
||||
store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(scanResponse)
|
||||
updateConfigManager(scanResponse)
|
||||
|
||||
withMainContext {
|
||||
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
|
||||
store.dispatch(WalletConnectAction.ResetState)
|
||||
store.dispatch(GlobalAction.SaveScanNoteResponse(scanResponse))
|
||||
store.dispatch(WalletConnectAction.RestoreSessions(scanResponse))
|
||||
store.dispatch(WalletAction.UserWalletChanged(userWallet))
|
||||
store.dispatch(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed))
|
||||
store.dispatch(WalletAction.Warnings.CheckIfNeeded)
|
||||
|
||||
if (refresh) {
|
||||
loadData(userWallet, refresh = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadData(userWallet: UserWallet, refresh: Boolean = false) {
|
||||
walletStoresManager.fetch(userWallet, refresh)
|
||||
.doOnSuccess {
|
||||
Timber.d("Wallet stores fetched for ${userWallet.walletId}")
|
||||
store.dispatchOnMain(WalletAction.LoadData.Success)
|
||||
}
|
||||
.doOnFailure { error ->
|
||||
val errorAction = when (error) {
|
||||
is WalletStoresError -> when (error) {
|
||||
is WalletStoresError.FetchFiatRatesError,
|
||||
is WalletStoresError.UpdateWalletManagerError,
|
||||
-> WalletAction.LoadData.Failure(error = null)
|
||||
is WalletStoresError.WalletManagerNotCreated -> WalletAction.LoadData.Failure(
|
||||
error = TapError.WalletManager.CreationError,
|
||||
)
|
||||
is WalletStoresError.UnknownBlockchain -> WalletAction.LoadData.Failure(
|
||||
error = TapError.UnknownBlockchain,
|
||||
)
|
||||
is WalletStoresError.NoInternetConnection -> WalletAction.LoadData.Failure(
|
||||
error = TapError.NoInternetConnection,
|
||||
)
|
||||
}
|
||||
else -> WalletAction.LoadData.Failure(error = null)
|
||||
}
|
||||
|
||||
Timber.e(error, "Wallet stores fetching failed for ${userWallet.walletId}")
|
||||
|
||||
store.dispatchOnMain(errorAction)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun onCardScanned(data: ScanResponse) {
|
||||
walletManagersThrottler.clear()
|
||||
store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(data)
|
||||
|
|
@ -143,7 +200,7 @@ class TapWalletManager {
|
|||
private fun checkIfDerivationsAreMissing(blockchainNetworks: List<BlockchainNetwork>, scanResponse: ScanResponse) {
|
||||
blockchainNetworks.map {
|
||||
if (it.tokens.isNotEmpty()) {
|
||||
WalletAction.MultiWallet.AddTokens(it.tokens, it, false)
|
||||
WalletAction.MultiWallet.AddTokens(it.tokens, it)
|
||||
}
|
||||
}
|
||||
val missingDerivations = blockchainNetworks
|
||||
|
|
@ -171,7 +228,6 @@ class TapWalletManager {
|
|||
WalletAction.MultiWallet.AddBlockchains(
|
||||
blockchains = listOf(BlockchainNetwork.fromWalletManager(primaryWalletManager)),
|
||||
walletManagers = listOf(primaryWalletManager),
|
||||
save = false,
|
||||
),
|
||||
WalletAction.LoadFiatRate(),
|
||||
)
|
||||
|
|
@ -187,7 +243,6 @@ class TapWalletManager {
|
|||
WalletAction.MultiWallet.AddBlockchains(
|
||||
blockchains = blockchainNetworks,
|
||||
walletManagers = walletManagers,
|
||||
save = false,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -197,7 +252,6 @@ class TapWalletManager {
|
|||
WalletAction.MultiWallet.AddTokens(
|
||||
tokens = it.tokens,
|
||||
blockchain = it,
|
||||
save = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ class ConfigManager {
|
|||
blockchairApiKey = values.blockchairApiKey,
|
||||
blockchairAuthorizationToken = values.blockchairAuthorizationToken,
|
||||
blockcypherTokens = values.blockcypherTokens,
|
||||
infuraProjectId = values.infuraProjectId
|
||||
infuraProjectId = values.infuraProjectId,
|
||||
),
|
||||
appsFlyerDevKey = values.appsFlyerDevKey,
|
||||
amplitudeApiKey = values.amplitudeApiKey,
|
||||
|
|
|
|||
|
|
@ -22,4 +22,6 @@ data class UserWallet(
|
|||
) {
|
||||
val cardId: String
|
||||
get() = scanResponse.card.cardId
|
||||
|
||||
internal var isSaved: Boolean = true
|
||||
}
|
||||
|
|
@ -35,6 +35,12 @@ data class WalletDataModel(
|
|||
open val pendingTransactions: List<PendingTransaction> = emptyList()
|
||||
open val errorMessage: String? = null
|
||||
open val isErrorStatus: Boolean = false
|
||||
|
||||
fun asRefreshing() = Refreshing(
|
||||
amount = amount,
|
||||
pendingTransactions = pendingTransactions,
|
||||
errorMessage = errorMessage,
|
||||
)
|
||||
}
|
||||
|
||||
object Loading : Status()
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import java.math.BigDecimal
|
|||
* Contains info about the blockchain and its currencies
|
||||
* @param userWalletId ID of the [UserWallet] which uses that store
|
||||
* @param blockchainNetwork Store's [BlockchainNetwork]
|
||||
* @param walletManager Store's [WalletManager], may be null if it fails to create this manager
|
||||
* @param walletManager Store's [WalletManager], may be null if it fails to create this manager. TODO: Remove after
|
||||
* WalletMiddleware refactoring
|
||||
* @param walletsData List of [WalletDataModel] which represents store's blockchain currency and tokens currencies
|
||||
* @param walletRent Store's [WalletRent], null if store has no rent or currency balance is greater then
|
||||
* [WalletRent.exemptionAmount]
|
||||
|
|
@ -18,6 +19,7 @@ import java.math.BigDecimal
|
|||
data class WalletStoreModel(
|
||||
val userWalletId: UserWalletId,
|
||||
val blockchainNetwork: BlockchainNetwork,
|
||||
@Deprecated("Don't use it, will be removed")
|
||||
val walletManager: WalletManager?,
|
||||
val walletsData: List<WalletDataModel>,
|
||||
val walletRent: WalletRent?,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
package com.tangem.tap.domain.model.builders
|
||||
|
||||
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.model.WalletDataModel
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.redux.reducers.createAddressesData
|
||||
import java.math.BigDecimal
|
||||
|
||||
class WalletStoreBuilder(
|
||||
private val userWallet: UserWallet,
|
||||
) {
|
||||
private var walletManager: WalletManager? = null
|
||||
private var blockchainNetwork: BlockchainNetwork? = null
|
||||
|
||||
fun setWalletManager(walletManager: WalletManager?) = this.apply {
|
||||
this.walletManager = walletManager
|
||||
}
|
||||
|
||||
fun setBlockchainNetwork(blockchainNetwork: BlockchainNetwork?) = this.apply {
|
||||
this.blockchainNetwork = blockchainNetwork
|
||||
}
|
||||
|
||||
fun build(): WalletStoreModel {
|
||||
val blockchainNetwork = this.blockchainNetwork
|
||||
?: walletManager?.let(BlockchainNetwork::fromWalletManager)
|
||||
?: error("Blockchain network and wallet manager must not be null")
|
||||
|
||||
val blockchainWalletData = blockchainNetwork.getBlockchainWalletData(walletManager)
|
||||
val tokensWalletsData = blockchainNetwork.getTokensWalletsData(walletManager)
|
||||
|
||||
return WalletStoreModel(
|
||||
userWalletId = userWallet.walletId,
|
||||
blockchainNetwork = blockchainNetwork,
|
||||
walletManager = walletManager,
|
||||
walletsData = (listOf(blockchainWalletData) + tokensWalletsData),
|
||||
walletRent = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BlockchainNetwork.getBlockchainWalletData(walletManager: WalletManager?): WalletDataModel {
|
||||
return WalletDataModel(
|
||||
currency = Currency.Blockchain(
|
||||
blockchain = blockchain,
|
||||
derivationPath = derivationPath,
|
||||
),
|
||||
status = WalletDataModel.Loading,
|
||||
walletAddresses = walletManager?.wallet?.createAddressesData().orEmpty(),
|
||||
existentialDeposit = getExistentialDeposit(walletManager),
|
||||
fiatRate = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun BlockchainNetwork.getTokensWalletsData(walletManager: WalletManager?): List<WalletDataModel> {
|
||||
return this.tokens
|
||||
.map { token ->
|
||||
WalletDataModel(
|
||||
currency = Currency.Token(
|
||||
token = token,
|
||||
blockchain = blockchain,
|
||||
derivationPath = derivationPath,
|
||||
),
|
||||
status = WalletDataModel.Loading,
|
||||
walletAddresses = walletManager?.wallet?.createAddressesData().orEmpty(),
|
||||
existentialDeposit = getExistentialDeposit(walletManager),
|
||||
fiatRate = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getExistentialDeposit(walletManager: WalletManager?): BigDecimal? {
|
||||
return (walletManager as? ExistentialDepositProvider)?.getExistentialDeposit()
|
||||
}
|
||||
|
|
@ -46,6 +46,7 @@ object ScanCardProcessor {
|
|||
suspend fun scan(
|
||||
useBiometricsForAccessCode: Boolean = false,
|
||||
additionalBlockchainsToDerive: Collection<Blockchain>? = null,
|
||||
cardId: String? = null,
|
||||
onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {},
|
||||
onScanStateChange: suspend (scanInProgress: Boolean) -> Unit = {},
|
||||
onWalletNotCreated: suspend (() -> Unit) = {},
|
||||
|
|
@ -56,6 +57,7 @@ object ScanCardProcessor {
|
|||
onScanStateChange(true)
|
||||
tangemSdkManager.scanProduct(
|
||||
userTokensRepository = userTokensRepository,
|
||||
cardId = cardId,
|
||||
additionalBlockchainsToDerive = additionalBlockchainsToDerive,
|
||||
useBiometricsForAccessCode = useBiometricsForAccessCode,
|
||||
)
|
||||
|
|
@ -202,7 +204,7 @@ object ScanCardProcessor {
|
|||
} else {
|
||||
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
|
||||
onWalletNotCreated()
|
||||
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly))
|
||||
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
|
||||
navigateTo(AppScreen.OnboardingTwins) { onProgressStateChange(it) }
|
||||
} else {
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ class CreateProductWalletTask(
|
|||
private val type: ProductType,
|
||||
) : CardSessionRunnable<CreateProductWalletTaskResponse> {
|
||||
|
||||
override val allowsAccessCodeFromRepository: Boolean = false
|
||||
|
||||
override fun run(
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ class ScanProductTask(
|
|||
private val additionalBlockchainsToDerive: Collection<Blockchain>? = null,
|
||||
) : CardSessionRunnable<ScanResponse> {
|
||||
|
||||
override val allowsAccessCodeFromRepository: Boolean
|
||||
get() = !additionalBlockchainsToDerive.isNullOrEmpty()
|
||||
|
||||
override fun run(
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> Unit,
|
||||
|
|
|
|||
|
|
@ -19,16 +19,21 @@ class TwinCardsManager(
|
|||
card: CardDTO,
|
||||
assetReader: AssetReader,
|
||||
) {
|
||||
|
||||
private val currentCardId: String = card.cardId
|
||||
private val firstCardId: String = card.cardId
|
||||
private var secondCardId: String? = null
|
||||
|
||||
private var currentCardPublicKey: String? = null
|
||||
private var secondCardPublicKey: String? = null
|
||||
var secondCardPublicKey: String? = null
|
||||
private set
|
||||
|
||||
private val issuerKeyPair: KeyPair = getIssuerKeys(assetReader, card.issuer.publicKey.toHexString())
|
||||
|
||||
suspend fun createFirstWallet(message: Message): CompletionResult<CreateWalletResponse> {
|
||||
val response = tangemSdkManager.runTaskAsync(CreateFirstTwinWalletTask(), currentCardId, message)
|
||||
val response = tangemSdkManager.runTaskAsync(
|
||||
runnable = CreateFirstTwinWalletTask(),
|
||||
cardId = firstCardId,
|
||||
initialMessage = message,
|
||||
)
|
||||
when (response) {
|
||||
is CompletionResult.Success -> currentCardPublicKey = response.data.wallet.publicKey.toHexString()
|
||||
is CompletionResult.Failure -> {}
|
||||
|
|
@ -43,6 +48,7 @@ class TwinCardsManager(
|
|||
): CompletionResult<CreateWalletResponse> {
|
||||
val task = CreateSecondTwinWalletTask(
|
||||
firstPublicKey = currentCardPublicKey!!,
|
||||
firstCardId = firstCardId,
|
||||
issuerKeys = issuerKeyPair,
|
||||
preparingMessage = preparingMessage,
|
||||
creatingWalletMessage = creatingWalletMessage,
|
||||
|
|
@ -51,6 +57,7 @@ class TwinCardsManager(
|
|||
when (response) {
|
||||
is CompletionResult.Success -> {
|
||||
secondCardPublicKey = response.data.wallet.publicKey.toHexString()
|
||||
secondCardId = response.data.cardId
|
||||
}
|
||||
is CompletionResult.Failure -> {}
|
||||
}
|
||||
|
|
@ -59,8 +66,9 @@ class TwinCardsManager(
|
|||
|
||||
suspend fun complete(message: Message): Result<ScanResponse> {
|
||||
val response = tangemSdkManager.runTaskAsync(
|
||||
FinalizeTwinTask(secondCardPublicKey!!.hexToBytes(), issuerKeyPair),
|
||||
currentCardId, message,
|
||||
runnable = FinalizeTwinTask(secondCardPublicKey!!.hexToBytes(), issuerKeyPair),
|
||||
cardId = firstCardId,
|
||||
initialMessage = message,
|
||||
)
|
||||
return when (response) {
|
||||
is CompletionResult.Success -> Result.Success(response.data)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.tap.domain.userWalletList
|
||||
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.wallet.R
|
||||
|
||||
sealed class UserWalletListError(code: Int) : TangemError(code) {
|
||||
override val silent: Boolean
|
||||
get() = (cause as? TangemError)?.silent == true
|
||||
|
||||
override val messageResId: Int? = null
|
||||
|
||||
object WalletAlreadySaved : UserWalletListError(code = 60001) {
|
||||
override var customMessage: String = "This wallet has already been saved, you can add another one"
|
||||
override val messageResId: Int = R.string.user_wallet_list_error_wallet_already_saved
|
||||
}
|
||||
|
||||
class SaveEncryptionKeysError(
|
||||
override val cause: Throwable,
|
||||
) : UserWalletListError(code = 60001) {
|
||||
override var customMessage: String = "Encryption keys could not be saved: ${cause.localizedMessage}"
|
||||
}
|
||||
|
||||
class ReceiveEncryptionKeysError(
|
||||
override val cause: Throwable,
|
||||
) : UserWalletListError(code = 60002) {
|
||||
override var customMessage: String = "Encryption keys could not be received: ${cause.localizedMessage}"
|
||||
}
|
||||
|
||||
class SaveSensitiveInformationError(
|
||||
override val cause: Throwable,
|
||||
) : UserWalletListError(code = 60003) {
|
||||
override var customMessage: String = "Sensitive information could not be saved: ${cause.localizedMessage}"
|
||||
}
|
||||
|
||||
class ReceiveSensitiveInformationError(
|
||||
override val cause: Throwable,
|
||||
) : UserWalletListError(code = 60004) {
|
||||
override var customMessage: String = "Sensitive information could not be received: ${cause.localizedMessage}"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,70 @@
|
|||
package com.tangem.tap.domain.userWalletList.di
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import com.tangem.common.json.TangemSdkAdapter
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.tangem_sdk_new.storage.AndroidSecureStorage
|
||||
import com.tangem.tangem_sdk_new.storage.createEncryptedSharedPreferences
|
||||
import com.tangem.tap.domain.TangemSdkManager
|
||||
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
|
||||
import com.tangem.tap.domain.userWalletList.implementation.DummyUserWalletsListManager
|
||||
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
|
||||
import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository
|
||||
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository
|
||||
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository
|
||||
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository
|
||||
import com.tangem.tap.domain.userWalletList.utils.json.*
|
||||
|
||||
fun UserWalletsListManager.Companion.provideDummyImplementation(): UserWalletsListManager {
|
||||
return DummyUserWalletsListManager()
|
||||
const val USER_WALLETS_STORAGE_NAME = "user_wallets_storage"
|
||||
const val USER_WALLETS_BIOMETRIC_KEY_NAME = "user_wallets"
|
||||
|
||||
fun UserWalletsListManager.Companion.provideBiometricImplementation(
|
||||
context: Context,
|
||||
tangemSdkManager: TangemSdkManager,
|
||||
): UserWalletsListManager {
|
||||
val moshi = Moshi.Builder()
|
||||
.add(WalletDerivedKeysMapAdapter())
|
||||
.add(ScanResponseDerivedKeysMapAdapter())
|
||||
.add(ByteArrayKeyAdapter())
|
||||
.add(ExtendedPublicKeysMapAdapter())
|
||||
.add(CardBackupStatusAdapter())
|
||||
.add(TangemSdkAdapter.DateAdapter())
|
||||
.add(TangemSdkAdapter.DerivationPathAdapter())
|
||||
.add(TangemSdkAdapter.DerivationNodeAdapter())
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
|
||||
val secureStorage = AndroidSecureStorage(
|
||||
preferences = SecureStorage.createEncryptedSharedPreferences(
|
||||
context = context,
|
||||
storageName = USER_WALLETS_STORAGE_NAME,
|
||||
),
|
||||
)
|
||||
|
||||
val keysRepository = BiometricUserWalletsKeysRepository(
|
||||
biometricKeyName = USER_WALLETS_BIOMETRIC_KEY_NAME,
|
||||
moshi = moshi,
|
||||
secureStorage = secureStorage,
|
||||
biometricManager = tangemSdkManager.biometricManager,
|
||||
)
|
||||
val publicInformationRepository = DefaultUserWalletsPublicInformationRepository(
|
||||
moshi = moshi,
|
||||
secureStorage = secureStorage,
|
||||
)
|
||||
val sensitiveInformationRepository = DefaultUserWalletsSensitiveInformationRepository(
|
||||
moshi = moshi,
|
||||
secureStorage = secureStorage,
|
||||
)
|
||||
val selectedUserWalletRepository = DefaultSelectedUserWalletRepository(
|
||||
secureStorage = secureStorage,
|
||||
)
|
||||
|
||||
return BiometricUserWalletsListManager(
|
||||
tangemSdkManager = tangemSdkManager,
|
||||
keysRepository = keysRepository,
|
||||
publicInformationRepository = publicInformationRepository,
|
||||
sensitiveInformationRepository = sensitiveInformationRepository,
|
||||
selectedUserWalletRepository = selectedUserWalletRepository,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,303 @@
|
|||
package com.tangem.tap.domain.userWalletList.implementation
|
||||
|
||||
import com.tangem.common.*
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.domain.common.util.encryptionKey
|
||||
import com.tangem.tap.domain.TangemSdkManager
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.userWalletList.UserWalletListError
|
||||
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
||||
import com.tangem.tap.domain.userWalletList.repository.SelectedUserWalletRepository
|
||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository
|
||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsPublicInformationRepository
|
||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsSensitiveInformationRepository
|
||||
import com.tangem.tap.domain.userWalletList.utils.toUserWallets
|
||||
import com.tangem.tap.domain.userWalletList.utils.updateWith
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class BiometricUserWalletsListManager(
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val keysRepository: UserWalletsKeysRepository,
|
||||
private val publicInformationRepository: UserWalletsPublicInformationRepository,
|
||||
private val sensitiveInformationRepository: UserWalletsSensitiveInformationRepository,
|
||||
private val selectedUserWalletRepository: SelectedUserWalletRepository,
|
||||
) : UserWalletsListManager {
|
||||
private val state = MutableStateFlow(State())
|
||||
|
||||
override val userWallets: Flow<List<UserWallet>>
|
||||
get() = state
|
||||
.mapLatest { it.wallets }
|
||||
.distinctUntilChanged()
|
||||
|
||||
override val selectedUserWallet: Flow<UserWallet>
|
||||
get() = state
|
||||
.mapLatest { state ->
|
||||
state.wallets.find {
|
||||
it.walletId == state.selectedWalletId
|
||||
}
|
||||
}
|
||||
.filterNotNull()
|
||||
.distinctUntilChanged()
|
||||
|
||||
override val selectedUserWalletSync: UserWallet?
|
||||
get() = findSelectedWallet()
|
||||
|
||||
override val isLocked: Flow<Boolean>
|
||||
get() = state
|
||||
.mapLatest { it.isLocked }
|
||||
.distinctUntilChanged()
|
||||
|
||||
override val isLockedSync: Boolean
|
||||
get() = state.value.isLocked
|
||||
|
||||
override val hasSavedUserWallets: Boolean
|
||||
get() = selectedUserWalletRepository.get() != null
|
||||
|
||||
override suspend fun unlockWithBiometry(): CompletionResult<UserWallet?> {
|
||||
return unlockWithBiometryInternal()
|
||||
.map { selectedUserWalletSync }
|
||||
}
|
||||
|
||||
override suspend fun unlockWithCard(userWallet: UserWallet): CompletionResult<Unit> {
|
||||
state.update { prevState ->
|
||||
userWallet.isSaved = false
|
||||
prevState.copy(
|
||||
encryptionKeys = listOf(UserWalletEncryptionKey(userWallet)),
|
||||
wallets = listOf(userWallet),
|
||||
)
|
||||
}
|
||||
|
||||
return loadModels()
|
||||
.map {
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
selectedWalletId = userWallet.walletId,
|
||||
isLocked = prevState.wallets.size != 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun lock() {
|
||||
tangemSdkManager.biometricManager.unauthenticate()
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
encryptionKeys = emptyList(),
|
||||
isLocked = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun selectWallet(walletId: UserWalletId): CompletionResult<UserWallet> = catching {
|
||||
if (state.value.selectedWalletId == walletId) {
|
||||
return@catching findSelectedWallet()!!
|
||||
}
|
||||
|
||||
if (!state.value.isLocked) {
|
||||
selectedUserWalletRepository.set(walletId)
|
||||
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
selectedWalletId = walletId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
findSelectedWallet()!!
|
||||
}
|
||||
|
||||
override suspend fun save(userWallet: UserWallet): CompletionResult<Unit> {
|
||||
return saveInternal(userWallet, override = false)
|
||||
}
|
||||
|
||||
override suspend fun update(userWallet: UserWallet): CompletionResult<Unit> {
|
||||
return saveInternal(userWallet, override = true)
|
||||
}
|
||||
|
||||
override suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit> {
|
||||
if (state.value.isLocked) {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
val walletIdsToRemove = state.value.wallets
|
||||
.map { it.walletId }
|
||||
.filter { it in walletIds }
|
||||
|
||||
changeSelectedWalletIfNeeded(walletIdsToRemove)
|
||||
|
||||
return sensitiveInformationRepository.delete(walletIdsToRemove)
|
||||
.flatMap { publicInformationRepository.delete(walletIdsToRemove) }
|
||||
.flatMap { keysRepository.delete(walletIdsToRemove) }
|
||||
.map { keys ->
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
encryptionKeys = keys,
|
||||
wallets = prevState.wallets.filter { it.walletId !in walletIdsToRemove },
|
||||
)
|
||||
}
|
||||
}
|
||||
.flatMap { loadModels() }
|
||||
}
|
||||
|
||||
override suspend fun clear(): CompletionResult<Unit> {
|
||||
return sensitiveInformationRepository.delete(
|
||||
walletIds = state.value.wallets.map { it.walletId },
|
||||
)
|
||||
.flatMap { publicInformationRepository.clear() }
|
||||
.flatMap { keysRepository.clear() }
|
||||
.map {
|
||||
selectedUserWalletRepository.set(null)
|
||||
tangemSdkManager.biometricManager.unauthenticate()
|
||||
state.update { State() }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun get(walletId: UserWalletId): CompletionResult<UserWallet> = withUnlock {
|
||||
return catching {
|
||||
state.value.wallets.first { it.walletId == walletId }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveInternal(
|
||||
userWallet: UserWallet,
|
||||
override: Boolean,
|
||||
): CompletionResult<Unit> = withUnlock {
|
||||
val isWalletSaved = state.value.wallets
|
||||
.filter { it.isSaved }
|
||||
.flatMap(UserWallet::cardsInWallet)
|
||||
.contains(userWallet.cardId)
|
||||
|
||||
if (isWalletSaved && !override) {
|
||||
CompletionResult.Failure(UserWalletListError.WalletAlreadySaved)
|
||||
} else {
|
||||
keysRepository.save(
|
||||
walletId = userWallet.walletId,
|
||||
encryptionKey = userWallet.scanResponse.card.encryptionKey,
|
||||
)
|
||||
.doOnSuccess { keys ->
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
encryptionKeys = (keys + prevState.encryptionKeys).distinctBy { it.walletId },
|
||||
)
|
||||
}
|
||||
}
|
||||
.flatMap { publicInformationRepository.save(userWallet) }
|
||||
.flatMap { sensitiveInformationRepository.save(userWallet) }
|
||||
.flatMap { loadModels() }
|
||||
.doOnSuccess {
|
||||
userWallet.isSaved = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun <reified T> withUnlock(
|
||||
block: () -> CompletionResult<T>,
|
||||
): CompletionResult<T> {
|
||||
return (if (state.value.isLocked) unlockWithBiometryInternal() else CompletionResult.Success(Unit))
|
||||
.flatMap { block() }
|
||||
}
|
||||
|
||||
private suspend fun unlockWithBiometryInternal(): CompletionResult<Unit> {
|
||||
return keysRepository.getAll()
|
||||
.map { keys ->
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
encryptionKeys = (keys + prevState.encryptionKeys).distinctBy { it.walletId },
|
||||
)
|
||||
}
|
||||
}
|
||||
.flatMap { loadModels() }
|
||||
.map {
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
isLocked = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadModels(): CompletionResult<Unit> {
|
||||
return getSavedUserWallets()
|
||||
.map { userWallets ->
|
||||
if (userWallets.isNotEmpty()) state.update { prevState ->
|
||||
val wallets = (userWallets + prevState.wallets).distinctBy { it.walletId }
|
||||
|
||||
prevState.copy(
|
||||
wallets = wallets,
|
||||
selectedWalletId = findOrSetSelectedWallet(prevState.selectedWalletId, wallets),
|
||||
)
|
||||
}
|
||||
}
|
||||
.doOnFailure { error ->
|
||||
Timber.e(error, "Unable to load user wallets")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getSavedUserWallets(): CompletionResult<List<UserWallet>> {
|
||||
return publicInformationRepository.getAll()
|
||||
.map { it.toUserWallets() }
|
||||
.flatMap { userWallets ->
|
||||
sensitiveInformationRepository.getAll(state.value.encryptionKeys)
|
||||
.map { walletIdToSensitiveInformation ->
|
||||
userWallets.updateWith(walletIdToSensitiveInformation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun findOrSetSelectedWallet(
|
||||
prevSelectedWalletId: UserWalletId?,
|
||||
userWallets: List<UserWallet>,
|
||||
): UserWalletId? {
|
||||
return prevSelectedWalletId
|
||||
?: (selectedUserWalletRepository.get()
|
||||
?: (userWallets.firstOrNull()?.walletId
|
||||
?.also { selectedUserWalletRepository.set(it) }))
|
||||
}
|
||||
|
||||
private fun changeSelectedWalletIfNeeded(
|
||||
walletsIdsToRemove: List<UserWalletId>,
|
||||
) {
|
||||
val remainingWallets = state.value.wallets.filter {
|
||||
it.walletId !in walletsIdsToRemove
|
||||
}
|
||||
val selectedWallet = findSelectedWallet()
|
||||
when {
|
||||
remainingWallets.isEmpty() -> {
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
selectedWalletId = null,
|
||||
)
|
||||
}
|
||||
selectedUserWalletRepository.set(null)
|
||||
}
|
||||
!remainingWallets.contains(selectedWallet) -> {
|
||||
val newSelectedWallet = remainingWallets.first()
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
selectedWalletId = newSelectedWallet.walletId,
|
||||
)
|
||||
}
|
||||
selectedUserWalletRepository.set(newSelectedWallet.walletId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun findSelectedWallet(): UserWallet? {
|
||||
return with(state.value) {
|
||||
wallets.find {
|
||||
it.walletId == selectedWalletId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class State(
|
||||
val encryptionKeys: List<UserWalletEncryptionKey> = emptyList(),
|
||||
val wallets: List<UserWallet> = emptyList(),
|
||||
val selectedWalletId: UserWalletId? = null,
|
||||
val isLocked: Boolean = true,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.tap.domain.userWalletList.model
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.domain.common.util.encryptionKey
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class UserWalletEncryptionKey(
|
||||
val walletId: UserWalletId,
|
||||
val encryptionKey: ByteArray,
|
||||
) {
|
||||
constructor(userWallet: UserWallet) : this(
|
||||
walletId = userWallet.walletId,
|
||||
encryptionKey = userWallet.scanResponse.card.encryptionKey,
|
||||
)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is UserWalletEncryptionKey) return false
|
||||
|
||||
if (walletId != other.walletId) return false
|
||||
if (!encryptionKey.contentEquals(other.encryptionKey)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = walletId.hashCode()
|
||||
result = 31 * result + encryptionKey.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.tap.domain.userWalletList.model
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class UserWalletSensitiveInformation(
|
||||
val wallets: List<CardDTO.Wallet>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class UserWalletPublicInformation(
|
||||
val name: String,
|
||||
val walletId: UserWalletId,
|
||||
val artworkUrl: String,
|
||||
val cardsInWallet: Set<String>,
|
||||
val scanResponse: ScanResponse,
|
||||
)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.tap.domain.userWalletList.repository
|
||||
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
|
||||
internal interface SelectedUserWalletRepository {
|
||||
fun get(): UserWalletId?
|
||||
fun set(walletId: UserWalletId?)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.tap.domain.userWalletList.repository
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
||||
|
||||
internal interface UserWalletsKeysRepository {
|
||||
suspend fun getAll(): CompletionResult<List<UserWalletEncryptionKey>>
|
||||
suspend fun save(walletId: UserWalletId, encryptionKey: ByteArray): CompletionResult<List<UserWalletEncryptionKey>>
|
||||
suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<List<UserWalletEncryptionKey>>
|
||||
suspend fun clear(): CompletionResult<Unit>
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.tap.domain.userWalletList.repository
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation
|
||||
|
||||
internal interface UserWalletsPublicInformationRepository {
|
||||
suspend fun save(userWallet: UserWallet): CompletionResult<Unit>
|
||||
|
||||
suspend fun getAll(): CompletionResult<List<UserWalletPublicInformation>>
|
||||
|
||||
suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit>
|
||||
suspend fun clear(): CompletionResult<Unit>
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.tap.domain.userWalletList.repository
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
|
||||
|
||||
internal interface UserWalletsSensitiveInformationRepository {
|
||||
suspend fun save(userWallet: UserWallet): CompletionResult<Unit>
|
||||
suspend fun getAll(
|
||||
encryptionKeys: List<UserWalletEncryptionKey>,
|
||||
): CompletionResult<Map<UserWalletId, UserWalletSensitiveInformation>>
|
||||
|
||||
suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit>
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
package com.tangem.tap.domain.userWalletList.repository.implementation
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.biometric.BiometricManager
|
||||
import com.tangem.common.biometric.BiometricStorage
|
||||
import com.tangem.common.flatMap
|
||||
import com.tangem.common.map
|
||||
import com.tangem.common.mapFailure
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.common.extensions.replaceByOrAdd
|
||||
import com.tangem.tap.domain.userWalletList.UserWalletListError
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository
|
||||
|
||||
internal class BiometricUserWalletsKeysRepository(
|
||||
biometricKeyName: String,
|
||||
moshi: Moshi,
|
||||
secureStorage: SecureStorage,
|
||||
biometricManager: BiometricManager,
|
||||
) : UserWalletsKeysRepository {
|
||||
private val biometricStorage = BiometricStorage(
|
||||
biometricKeyName = biometricKeyName,
|
||||
biometricManager = biometricManager,
|
||||
secureStorage = secureStorage,
|
||||
)
|
||||
private val walletsKeysAdapter: JsonAdapter<List<UserWalletEncryptionKey>> = moshi.adapter(
|
||||
Types.newParameterizedType(List::class.java, UserWalletEncryptionKey::class.java),
|
||||
)
|
||||
|
||||
override suspend fun getAll(): CompletionResult<List<UserWalletEncryptionKey>> {
|
||||
return biometricStorage.get(key = StorageKey.WalletEncryptionKeys.name)
|
||||
.map { encryptionKeys ->
|
||||
encryptionKeys.decodeToKeys()
|
||||
}
|
||||
.mapFailure { error ->
|
||||
UserWalletListError.ReceiveEncryptionKeysError(error.cause ?: error)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun save(
|
||||
walletId: UserWalletId,
|
||||
encryptionKey: ByteArray,
|
||||
): CompletionResult<List<UserWalletEncryptionKey>> {
|
||||
return getAll()
|
||||
.flatMap { keys ->
|
||||
if (keys.any { it.walletId == walletId }) {
|
||||
return@flatMap CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
val encodedKeys = keys.toMutableList()
|
||||
.apply {
|
||||
replaceByOrAdd(UserWalletEncryptionKey(walletId, encryptionKey)) {
|
||||
it.walletId == walletId
|
||||
}
|
||||
}
|
||||
.encode()
|
||||
|
||||
biometricStorage.store(
|
||||
key = StorageKey.WalletEncryptionKeys.name,
|
||||
data = encodedKeys,
|
||||
)
|
||||
}
|
||||
.flatMap { getAll() }
|
||||
.mapFailure { error ->
|
||||
UserWalletListError.SaveEncryptionKeysError(error.cause ?: error)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<List<UserWalletEncryptionKey>> {
|
||||
return getAll()
|
||||
.map { keys ->
|
||||
val keysToRemove = keys.filter { it.walletId in walletIds }.toSet()
|
||||
(keys - keysToRemove).encode()
|
||||
}
|
||||
.flatMap { encodedKeys ->
|
||||
biometricStorage.store(
|
||||
key = StorageKey.WalletEncryptionKeys.name,
|
||||
data = encodedKeys,
|
||||
)
|
||||
}
|
||||
.flatMap { getAll() }
|
||||
}
|
||||
|
||||
override suspend fun clear(): CompletionResult<Unit> {
|
||||
return biometricStorage.delete(key = StorageKey.WalletEncryptionKeys.name)
|
||||
}
|
||||
|
||||
private fun List<UserWalletEncryptionKey>.encode(): ByteArray {
|
||||
return this.let(walletsKeysAdapter::toJson)
|
||||
.encodeToByteArray(throwOnInvalidSequence = true)
|
||||
}
|
||||
|
||||
private fun ByteArray?.decodeToKeys(): List<UserWalletEncryptionKey> {
|
||||
return this?.decodeToString(throwOnInvalidSequence = true)
|
||||
?.let(walletsKeysAdapter::fromJson)
|
||||
.orEmpty()
|
||||
}
|
||||
|
||||
private enum class StorageKey {
|
||||
WalletEncryptionKeys
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.tap.domain.userWalletList.repository.implementation
|
||||
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.userWalletList.repository.SelectedUserWalletRepository
|
||||
|
||||
internal class DefaultSelectedUserWalletRepository(
|
||||
private val secureStorage: SecureStorage,
|
||||
) : SelectedUserWalletRepository {
|
||||
override fun get(): UserWalletId? {
|
||||
return secureStorage.get(StorageKey.SelectedWalletId.name)
|
||||
?.decodeToString(throwOnInvalidSequence = true)
|
||||
?.let { UserWalletId(it) }
|
||||
}
|
||||
|
||||
override fun set(walletId: UserWalletId?) {
|
||||
if (walletId == null) {
|
||||
secureStorage.delete(StorageKey.SelectedWalletId.name)
|
||||
} else {
|
||||
secureStorage.store(
|
||||
data = walletId.stringValue.encodeToByteArray(throwOnInvalidSequence = true),
|
||||
account = StorageKey.SelectedWalletId.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private enum class StorageKey {
|
||||
SelectedWalletId
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.tangem.tap.domain.userWalletList.repository.implementation
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.catching
|
||||
import com.tangem.common.flatMap
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.common.extensions.replaceByOrAdd
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation
|
||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsPublicInformationRepository
|
||||
import com.tangem.tap.domain.userWalletList.utils.publicInformation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultUserWalletsPublicInformationRepository(
|
||||
moshi: Moshi,
|
||||
private val secureStorage: SecureStorage,
|
||||
) : UserWalletsPublicInformationRepository {
|
||||
private val publicInformationAdapter: JsonAdapter<List<UserWalletPublicInformation>> = moshi.adapter(
|
||||
Types.newParameterizedType(List::class.java, UserWalletPublicInformation::class.java),
|
||||
)
|
||||
|
||||
override suspend fun save(userWallet: UserWallet): CompletionResult<Unit> {
|
||||
return getAll()
|
||||
.flatMap { savedInformation ->
|
||||
val infoToSave = withContext(Dispatchers.Default) {
|
||||
savedInformation.toMutableList()
|
||||
.apply {
|
||||
replaceByOrAdd(userWallet.publicInformation) {
|
||||
userWallet.walletId == it.walletId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
save(infoToSave)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAll(): CompletionResult<List<UserWalletPublicInformation>> = catching {
|
||||
withContext(Dispatchers.IO) {
|
||||
secureStorage.get(StorageKey.UserWalletPublicInformation.name)
|
||||
?.decodeToString()
|
||||
?.let(publicInformationAdapter::fromJson)
|
||||
.orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit> {
|
||||
return getAll()
|
||||
.flatMap { publicInformation ->
|
||||
val infoToRemove = publicInformation
|
||||
.filter { it.walletId in walletIds }
|
||||
.toSet()
|
||||
|
||||
save(
|
||||
publicInformation = publicInformation - infoToRemove,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clear(): CompletionResult<Unit> = catching {
|
||||
secureStorage.delete(StorageKey.UserWalletPublicInformation.name)
|
||||
}
|
||||
|
||||
@JvmName("saveWithPublicInformation")
|
||||
private suspend fun save(
|
||||
publicInformation: List<UserWalletPublicInformation>,
|
||||
): CompletionResult<Unit> = catching {
|
||||
withContext(Dispatchers.IO) {
|
||||
publicInformation
|
||||
.let(publicInformationAdapter::toJson)
|
||||
.encodeToByteArray(throwOnInvalidSequence = true)
|
||||
.also { secureStorage.store(it, StorageKey.UserWalletPublicInformation.name) }
|
||||
}
|
||||
}
|
||||
|
||||
private enum class StorageKey {
|
||||
UserWalletPublicInformation
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package com.tangem.tap.domain.userWalletList.repository.implementation
|
||||
|
||||
import android.security.keystore.KeyProperties
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.catching
|
||||
import com.tangem.common.mapFailure
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.domain.common.util.encryptionKey
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.userWalletList.UserWalletListError
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
|
||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsSensitiveInformationRepository
|
||||
import com.tangem.tap.domain.userWalletList.utils.sensitiveInformation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
internal class DefaultUserWalletsSensitiveInformationRepository(
|
||||
moshi: Moshi,
|
||||
private val secureStorage: SecureStorage,
|
||||
) : UserWalletsSensitiveInformationRepository {
|
||||
private val sensitiveInformationAdapter: JsonAdapter<UserWalletSensitiveInformation> = moshi.adapter(
|
||||
UserWalletSensitiveInformation::class.java,
|
||||
)
|
||||
private val cipher: Cipher by lazy {
|
||||
Cipher.getInstance("$algorithm/$blockMode/$encryptionPadding")
|
||||
}
|
||||
|
||||
override suspend fun save(userWallet: UserWallet): CompletionResult<Unit> {
|
||||
return catching {
|
||||
withContext(Dispatchers.Default) {
|
||||
userWallet.sensitiveInformation
|
||||
.let(sensitiveInformationAdapter::toJson)
|
||||
.encodeToByteArray(throwOnInvalidSequence = true)
|
||||
.encryptAndStoreIv(
|
||||
walletId = userWallet.walletId,
|
||||
encryptionKey = userWallet.scanResponse.card.encryptionKey,
|
||||
)
|
||||
.let { encryptedInformation ->
|
||||
withContext(Dispatchers.IO) {
|
||||
secureStorage.store(
|
||||
data = encryptedInformation,
|
||||
account = StorageKey.SensitiveInformation(userWallet.walletId).name,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.mapFailure { error ->
|
||||
UserWalletListError.SaveSensitiveInformationError(error.cause ?: error)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAll(
|
||||
encryptionKeys: List<UserWalletEncryptionKey>,
|
||||
): CompletionResult<Map<UserWalletId, UserWalletSensitiveInformation>> {
|
||||
return catching {
|
||||
if (encryptionKeys.isEmpty()) {
|
||||
return@catching emptyMap()
|
||||
}
|
||||
|
||||
val keyToEncryptedInformation = withContext(Dispatchers.IO) {
|
||||
encryptionKeys.associateWith { encryptionKey ->
|
||||
secureStorage.get(StorageKey.SensitiveInformation(encryptionKey.walletId).name)
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
val keyToInformation =
|
||||
mutableMapOf<UserWalletId, UserWalletSensitiveInformation>()
|
||||
|
||||
keyToEncryptedInformation.forEach { (key, encryptedInformation) ->
|
||||
val information = encryptedInformation
|
||||
?.getIvAndDecrypt(
|
||||
walletId = key.walletId,
|
||||
encryptionKey = key.encryptionKey,
|
||||
)
|
||||
?.decodeToString(throwOnInvalidSequence = true)
|
||||
?.let(sensitiveInformationAdapter::fromJson)
|
||||
|
||||
if (information != null) {
|
||||
keyToInformation[key.walletId] = information
|
||||
}
|
||||
}
|
||||
|
||||
keyToInformation
|
||||
}
|
||||
}
|
||||
.mapFailure { error ->
|
||||
UserWalletListError.ReceiveSensitiveInformationError(error.cause ?: error)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit> = catching {
|
||||
walletIds
|
||||
.forEach { walletId ->
|
||||
secureStorage.delete(StorageKey.SensitiveInformation(walletId).name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ByteArray.encryptAndStoreIv(walletId: UserWalletId, encryptionKey: ByteArray): ByteArray {
|
||||
val secretKey = SecretKeySpec(encryptionKey, algorithm)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
||||
val encryptedData = cipher.doFinal(this)
|
||||
secureStorage.store(data = cipher.iv, account = StorageKey.SensitiveInformationIv(walletId).name)
|
||||
return encryptedData
|
||||
}
|
||||
|
||||
private fun ByteArray.getIvAndDecrypt(walletId: UserWalletId, encryptionKey: ByteArray): ByteArray? {
|
||||
val iv = secureStorage.get(StorageKey.SensitiveInformationIv(walletId).name)
|
||||
?: error("IV not found")
|
||||
val ivParam = IvParameterSpec(iv)
|
||||
val secretKeySpec = SecretKeySpec(encryptionKey, algorithm)
|
||||
return cipher
|
||||
.also { it.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParam) }
|
||||
.doFinal(this)
|
||||
}
|
||||
|
||||
private sealed interface StorageKey {
|
||||
val name: String
|
||||
|
||||
class SensitiveInformation(walletId: UserWalletId) : StorageKey {
|
||||
override val name: String = "user_wallet_sensitive_information_${walletId.stringValue}"
|
||||
}
|
||||
|
||||
class SensitiveInformationIv(walletId: UserWalletId) : StorageKey {
|
||||
override val name: String = "user_wallet_sensitive_information_iv_${walletId.stringValue}"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val algorithm = KeyProperties.KEY_ALGORITHM_AES
|
||||
private const val blockMode = KeyProperties.BLOCK_MODE_CBC
|
||||
private const val encryptionPadding = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.tap.domain.userWalletList.utils
|
||||
|
||||
internal fun List<ByteArray>.containsBA(element: ByteArray?): Boolean {
|
||||
this.forEach {
|
||||
if (it.contentEquals(element)) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.tangem.tap.domain.userWalletList.utils
|
||||
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
|
||||
|
||||
internal val UserWallet.sensitiveInformation: UserWalletSensitiveInformation
|
||||
get() = UserWalletSensitiveInformation(scanResponse.card.wallets)
|
||||
|
||||
internal val UserWallet.publicInformation: UserWalletPublicInformation
|
||||
get() = UserWalletPublicInformation(
|
||||
name = name,
|
||||
walletId = walletId,
|
||||
artworkUrl = artworkUrl,
|
||||
cardsInWallet = cardsInWallet,
|
||||
scanResponse = scanResponse.copy(
|
||||
card = scanResponse.card.copy(
|
||||
wallets = emptyList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
internal fun UserWalletPublicInformation.toUserWallet(): UserWallet {
|
||||
return UserWallet(
|
||||
name = name,
|
||||
walletId = walletId,
|
||||
artworkUrl = artworkUrl,
|
||||
cardsInWallet = cardsInWallet,
|
||||
scanResponse = scanResponse,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun List<UserWalletPublicInformation>.toUserWallets(): List<UserWallet> {
|
||||
return this.map { it.toUserWallet() }
|
||||
}
|
||||
|
||||
internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInformation): UserWallet {
|
||||
return copy(
|
||||
scanResponse = scanResponse.copy(
|
||||
card = scanResponse.card.copy(
|
||||
wallets = sensitiveInformation.wallets,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun List<UserWallet>.updateWith(
|
||||
walletIdToSensitiveInformation: Map<UserWalletId, UserWalletSensitiveInformation>,
|
||||
): List<UserWallet> {
|
||||
return if (walletIdToSensitiveInformation.isEmpty()) this else {
|
||||
this.map { wallet ->
|
||||
walletIdToSensitiveInformation[wallet.walletId]
|
||||
?.let(wallet::updateWith)
|
||||
?: wallet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.tap.domain.userWalletList.utils.json
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
|
||||
internal class ByteArrayKeyAdapter {
|
||||
@ToJson
|
||||
fun toJson(
|
||||
writer: JsonWriter,
|
||||
src: ByteArrayKey,
|
||||
byteArrayAdapter: JsonAdapter<ByteArray>,
|
||||
) {
|
||||
byteArrayAdapter.toJson(writer, src.bytes)
|
||||
}
|
||||
|
||||
@FromJson
|
||||
fun fromJson(
|
||||
reader: JsonReader,
|
||||
byteArrayAdapter: JsonAdapter<ByteArray>,
|
||||
): ByteArrayKey? {
|
||||
return byteArrayAdapter.fromJson(reader)?.let {
|
||||
ByteArrayKey(bytes = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.tangem.tap.domain.userWalletList.utils.json
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.tangem.domain.common.CardDTO
|
||||
|
||||
internal class CardBackupStatusAdapter {
|
||||
@ToJson
|
||||
fun toJson(
|
||||
writer: JsonWriter,
|
||||
src: CardDTO.BackupStatus?,
|
||||
mapAdapter: JsonAdapter<Map<String, String>>,
|
||||
) {
|
||||
val jsonMap = mutableMapOf<String, String>()
|
||||
|
||||
when (src) {
|
||||
is CardDTO.BackupStatus.Active -> {
|
||||
jsonMap["status"] = "active"
|
||||
jsonMap["cardCount"] = src.cardCount.toString()
|
||||
}
|
||||
is CardDTO.BackupStatus.CardLinked -> {
|
||||
jsonMap["status"] = "card_linked"
|
||||
jsonMap["cardCount"] = src.cardCount.toString()
|
||||
}
|
||||
is CardDTO.BackupStatus.NoBackup -> {
|
||||
jsonMap["status"] = "no_backup"
|
||||
}
|
||||
null -> {
|
||||
jsonMap["status"] = "null"
|
||||
}
|
||||
}
|
||||
|
||||
mapAdapter.toJson(writer, jsonMap)
|
||||
}
|
||||
|
||||
@FromJson
|
||||
fun fromJson(
|
||||
reader: JsonReader,
|
||||
mapAdapter: JsonAdapter<Map<String, String>>,
|
||||
): CardDTO.BackupStatus? {
|
||||
val map = mapAdapter.fromJson(reader) ?: return null
|
||||
|
||||
return when (map["status"]) {
|
||||
"active" -> CardDTO.BackupStatus.Active(
|
||||
cardCount = map["cardCount"]?.toInt() ?: 0,
|
||||
)
|
||||
"card_linked" -> CardDTO.BackupStatus.CardLinked(
|
||||
cardCount = map["cardCount"]?.toInt() ?: 0,
|
||||
)
|
||||
"no_backup" -> CardDTO.BackupStatus.NoBackup
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.tangem.tap.domain.userWalletList.utils.json
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.hdWallet.DerivationPath
|
||||
import com.tangem.common.hdWallet.ExtendedPublicKey
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
|
||||
internal class ExtendedPublicKeysMapAdapter {
|
||||
@ToJson
|
||||
fun toJson(
|
||||
writer: JsonWriter,
|
||||
src: ExtendedPublicKeysMap,
|
||||
mapAdapter: JsonAdapter<Map<String, String>>,
|
||||
derivationPathAdapter: JsonAdapter<DerivationPath>,
|
||||
extendedPublicKeyAdapter: JsonAdapter<ExtendedPublicKey>,
|
||||
) {
|
||||
val jsonMap = mutableMapOf<String, String>()
|
||||
|
||||
src.forEach { (derivationPath, extendedPublicKey) ->
|
||||
val derivationPathJson = derivationPathAdapter.toJson(derivationPath)
|
||||
val derivationPathEncoded = derivationPathJson.encodeToByteArray().toHexString()
|
||||
val extendedPublicKeyJson = extendedPublicKeyAdapter.toJson(extendedPublicKey)
|
||||
|
||||
jsonMap[derivationPathEncoded] = extendedPublicKeyJson
|
||||
}
|
||||
|
||||
mapAdapter.toJson(writer, jsonMap)
|
||||
}
|
||||
|
||||
@FromJson
|
||||
fun fromJson(
|
||||
reader: JsonReader,
|
||||
mapAdapter: JsonAdapter<Map<String, String>>,
|
||||
derivationPathAdapter: JsonAdapter<DerivationPath>,
|
||||
extendedPublicKeyAdapter: JsonAdapter<ExtendedPublicKey>,
|
||||
): ExtendedPublicKeysMap {
|
||||
val map = mutableMapOf<DerivationPath, ExtendedPublicKey>()
|
||||
|
||||
mapAdapter.fromJson(reader)?.forEach { (derivationPathEncoded, extendedPublicKeyJson) ->
|
||||
val derivationPathJson = derivationPathEncoded.hexToBytes().decodeToString()
|
||||
val derivationPath = derivationPathAdapter.fromJson(derivationPathJson)
|
||||
val extendedPublicKey = extendedPublicKeyAdapter.fromJson(extendedPublicKeyJson)
|
||||
|
||||
if (derivationPath != null && extendedPublicKey != null) {
|
||||
map[derivationPath] = extendedPublicKey
|
||||
}
|
||||
}
|
||||
|
||||
return ExtendedPublicKeysMap(map)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package com.tangem.tap.domain.userWalletList.utils.json
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
|
||||
internal class ScanResponseDerivedKeysMapAdapter {
|
||||
@ToJson
|
||||
fun toJson(
|
||||
writer: JsonWriter,
|
||||
src: Map<ByteArrayKey, ExtendedPublicKeysMap>,
|
||||
mapAdapter: JsonAdapter<Map<String, String>>,
|
||||
byteArrayKeyAdapter: JsonAdapter<ByteArrayKey>,
|
||||
extendedPublicKeysMapAdapter: JsonAdapter<ExtendedPublicKeysMap>,
|
||||
) {
|
||||
val jsonMap = mutableMapOf<String, String>()
|
||||
|
||||
src.forEach { (key, extendedPublicKeysMap) ->
|
||||
val keyJson = byteArrayKeyAdapter.toJson(key)
|
||||
val extendedPublicKeysMapJson = extendedPublicKeysMapAdapter.toJson(extendedPublicKeysMap)
|
||||
|
||||
jsonMap[keyJson] = extendedPublicKeysMapJson
|
||||
}
|
||||
|
||||
mapAdapter.toJson(writer, jsonMap)
|
||||
}
|
||||
|
||||
@FromJson
|
||||
fun fromJson(
|
||||
reader: JsonReader,
|
||||
mapAdapter: JsonAdapter<Map<String, String>>,
|
||||
byteArrayKeyAdapter: JsonAdapter<ByteArrayKey>,
|
||||
extendedPublicKeysMapAdapter: JsonAdapter<ExtendedPublicKeysMap>,
|
||||
): Map<ByteArrayKey, ExtendedPublicKeysMap> {
|
||||
val map = mutableMapOf<ByteArrayKey, ExtendedPublicKeysMap>()
|
||||
|
||||
mapAdapter.fromJson(reader)?.forEach { (keyJson, extendedPublicKeysMapJson) ->
|
||||
val key = byteArrayKeyAdapter.fromJson(keyJson)
|
||||
val extendedPublicKeysMap = extendedPublicKeysMapAdapter.fromJson(extendedPublicKeysMapJson)
|
||||
|
||||
if (key != null && extendedPublicKeysMap != null) {
|
||||
map[key] = extendedPublicKeysMap
|
||||
}
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package com.tangem.tap.domain.userWalletList.utils.json
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.hdWallet.DerivationPath
|
||||
import com.tangem.common.hdWallet.ExtendedPublicKey
|
||||
|
||||
internal class WalletDerivedKeysMapAdapter {
|
||||
@ToJson
|
||||
fun toJson(
|
||||
writer: JsonWriter,
|
||||
src: Map<DerivationPath, ExtendedPublicKey>,
|
||||
mapAdapter: JsonAdapter<Map<String, String>>,
|
||||
derivationPathAdapter: JsonAdapter<DerivationPath>,
|
||||
extendedPublicKeyAdapter: JsonAdapter<ExtendedPublicKey>,
|
||||
) {
|
||||
val jsonMap = mutableMapOf<String, String>()
|
||||
|
||||
src.forEach { (derivationPath, extendedPublicKey) ->
|
||||
val derivationPathJson = derivationPathAdapter.toJson(derivationPath)
|
||||
val derivationPathEncoded = derivationPathJson.encodeToByteArray().toHexString()
|
||||
val extendedPublicKeyJson = extendedPublicKeyAdapter.toJson(extendedPublicKey)
|
||||
|
||||
jsonMap[derivationPathEncoded] = extendedPublicKeyJson
|
||||
}
|
||||
|
||||
mapAdapter.toJson(writer, jsonMap)
|
||||
}
|
||||
|
||||
@FromJson
|
||||
fun fromJson(
|
||||
reader: JsonReader,
|
||||
mapAdapter: JsonAdapter<Map<String, String>>,
|
||||
derivationPathAdapter: JsonAdapter<DerivationPath>,
|
||||
extendedPublicKeyAdapter: JsonAdapter<ExtendedPublicKey>,
|
||||
): Map<DerivationPath, ExtendedPublicKey> {
|
||||
val map = mutableMapOf<DerivationPath, ExtendedPublicKey>()
|
||||
|
||||
mapAdapter.fromJson(reader)?.forEach { (derivationPathEncoded, extendedPublicKeyJson) ->
|
||||
val derivationPathJson = derivationPathEncoded.hexToBytes().decodeToString()
|
||||
val derivationPath = derivationPathAdapter.fromJson(derivationPathJson)
|
||||
val extendedPublicKey = extendedPublicKeyAdapter.fromJson(extendedPublicKeyJson)
|
||||
|
||||
if (derivationPath != null && extendedPublicKey != null) {
|
||||
map[derivationPath] = extendedPublicKey
|
||||
}
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.tap.domain.walletCurrencies
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
|
||||
interface WalletCurrenciesManager {
|
||||
suspend fun update(
|
||||
userWallet: UserWallet,
|
||||
blockchainNetwork: BlockchainNetwork,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
suspend fun addCurrencies(
|
||||
userWallet: UserWallet,
|
||||
currenciesToAdd: List<Currency>,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
suspend fun removeCurrency(
|
||||
userWallet: UserWallet,
|
||||
currencyToRemove: Currency,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
suspend fun removeCurrencies(
|
||||
userWallet: UserWallet,
|
||||
currenciesToRemove: List<Currency>,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
companion object
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.tap.domain.walletCurrencies.di
|
||||
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.domain.tokens.UserTokensRepository
|
||||
import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager
|
||||
import com.tangem.tap.domain.walletCurrencies.implementation.DefaultWalletCurrenciesManager
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
|
||||
|
||||
fun WalletCurrenciesManager.Companion.provideDefaultImplementation(
|
||||
userTokensRepository: UserTokensRepository,
|
||||
walletStoresRepository: WalletStoresRepository,
|
||||
walletAmountsRepository: WalletAmountsRepository,
|
||||
walletManagersRepository: WalletManagersRepository,
|
||||
appCurrencyProvider: () -> FiatCurrency,
|
||||
): WalletCurrenciesManager {
|
||||
return DefaultWalletCurrenciesManager(
|
||||
userTokensRepository = userTokensRepository,
|
||||
walletStoresRepository = walletStoresRepository,
|
||||
walletAmountsRepository = walletAmountsRepository,
|
||||
walletManagersRepository = walletManagersRepository,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
package com.tangem.tap.domain.walletCurrencies.implementation
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.catching
|
||||
import com.tangem.common.flatMap
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.model.builders.WalletStoreBuilder
|
||||
import com.tangem.tap.domain.tokens.UserTokensRepository
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager
|
||||
import com.tangem.tap.domain.walletStores.implementation.utils.fold
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.models.getTokens
|
||||
import com.tangem.tap.features.wallet.models.toCurrencies
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultWalletCurrenciesManager(
|
||||
private val userTokensRepository: UserTokensRepository,
|
||||
private val walletStoresRepository: WalletStoresRepository,
|
||||
private val walletAmountsRepository: WalletAmountsRepository,
|
||||
private val walletManagersRepository: WalletManagersRepository,
|
||||
private val appCurrencyProvider: () -> FiatCurrency,
|
||||
) : WalletCurrenciesManager {
|
||||
override suspend fun update(
|
||||
userWallet: UserWallet,
|
||||
blockchainNetwork: BlockchainNetwork,
|
||||
): CompletionResult<Unit> {
|
||||
val walletStore = walletStoresRepository.get(userWallet.walletId).first()
|
||||
.find {
|
||||
it.blockchainNetwork.blockchain == blockchainNetwork.blockchain
|
||||
&& it.blockchainNetwork.derivationPath == blockchainNetwork.derivationPath
|
||||
}
|
||||
|
||||
return if (walletStore != null) {
|
||||
walletAmountsRepository.update(
|
||||
userWallet = userWallet,
|
||||
walletStore = walletStore,
|
||||
fiatCurrency = appCurrencyProvider(),
|
||||
)
|
||||
} else CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun addCurrencies(
|
||||
userWallet: UserWallet,
|
||||
currenciesToAdd: List<Currency>,
|
||||
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
|
||||
var newBlockchainNetworks = listOf<BlockchainNetwork>()
|
||||
catching {
|
||||
val card = userWallet.scanResponse.card
|
||||
val savedCurrencies = withContext(Dispatchers.IO) {
|
||||
userTokensRepository.getUserTokens(card)
|
||||
}
|
||||
newBlockchainNetworks = (savedCurrencies + currenciesToAdd)
|
||||
.toBlockchainNetworks(userWallet.scanResponse.card)
|
||||
val newCurrencies = newBlockchainNetworks.toCurrencies()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
userTokensRepository.saveUserTokens(
|
||||
card = card,
|
||||
tokens = newCurrencies,
|
||||
)
|
||||
}
|
||||
}
|
||||
.flatMap {
|
||||
newBlockchainNetworks.updateWalletStores(userWallet)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun removeCurrency(
|
||||
userWallet: UserWallet,
|
||||
currencyToRemove: Currency,
|
||||
): CompletionResult<Unit> {
|
||||
return removeCurrencies(userWallet, listOf(currencyToRemove))
|
||||
}
|
||||
|
||||
override suspend fun removeCurrencies(
|
||||
userWallet: UserWallet,
|
||||
currenciesToRemove: List<Currency>,
|
||||
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
|
||||
var remainingBlockchainsNetworks = emptyList<BlockchainNetwork>()
|
||||
catching {
|
||||
val card = userWallet.scanResponse.card
|
||||
val savedCurrencies = withContext(Dispatchers.IO) {
|
||||
userTokensRepository.getUserTokens(card)
|
||||
}
|
||||
|
||||
val remainingCurrencies = arrayListOf<Currency>()
|
||||
savedCurrencies.forEach { savedCurrency ->
|
||||
if (savedCurrency !in currenciesToRemove) {
|
||||
remainingCurrencies.add(savedCurrency)
|
||||
}
|
||||
}
|
||||
|
||||
remainingBlockchainsNetworks = remainingCurrencies.toBlockchainNetworks(userWallet.scanResponse.card)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
userTokensRepository.saveUserTokens(
|
||||
card = card,
|
||||
tokens = remainingCurrencies,
|
||||
)
|
||||
}
|
||||
}
|
||||
.flatMap {
|
||||
remainingBlockchainsNetworks.updateWalletStores(userWallet)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Currency>.toBlockchainNetworks(card: CardDTO): List<BlockchainNetwork> {
|
||||
val blockchainNetworks = arrayListOf<BlockchainNetwork>()
|
||||
val findDerivationPath: (currency: Currency) -> String? = { currency ->
|
||||
currency.derivationPath
|
||||
?: currency.blockchain.derivationPath(card.derivationStyle)
|
||||
?.rawPath
|
||||
}
|
||||
|
||||
for (currency in this.sortedByDescending { it.isBlockchain() }) {
|
||||
when (currency) {
|
||||
is Currency.Blockchain -> {
|
||||
val blockchainNetwork = BlockchainNetwork(
|
||||
blockchain = currency.blockchain,
|
||||
derivationPath = findDerivationPath(currency),
|
||||
tokens = getTokens(currency),
|
||||
)
|
||||
|
||||
blockchainNetworks.add(blockchainNetwork)
|
||||
}
|
||||
is Currency.Token -> {
|
||||
val tokenBlockchainNetworkIndex = blockchainNetworks
|
||||
.indexOfFirst {
|
||||
it.blockchain == currency.blockchain &&
|
||||
it.derivationPath == currency.derivationPath
|
||||
}
|
||||
|
||||
if (tokenBlockchainNetworkIndex == -1) {
|
||||
blockchainNetworks.add(
|
||||
BlockchainNetwork(
|
||||
blockchain = currency.blockchain,
|
||||
derivationPath = findDerivationPath(currency),
|
||||
tokens = listOf(currency.token),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
val tokenBlockchainNetwork = blockchainNetworks[tokenBlockchainNetworkIndex]
|
||||
if (currency.token in tokenBlockchainNetwork.tokens) {
|
||||
continue
|
||||
} else {
|
||||
blockchainNetworks.add(
|
||||
tokenBlockchainNetworkIndex,
|
||||
tokenBlockchainNetwork.copy(
|
||||
tokens = tokenBlockchainNetwork.tokens + currency.token,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return blockchainNetworks
|
||||
}
|
||||
|
||||
private suspend fun List<BlockchainNetwork>.updateWalletStores(
|
||||
userWallet: UserWallet,
|
||||
): CompletionResult<Unit> {
|
||||
val userWalletId = userWallet.walletId
|
||||
return this
|
||||
.also { blockchainNetworks ->
|
||||
walletStoresRepository.deleteDifference(
|
||||
userWalletId = userWalletId,
|
||||
currentBlockchains = blockchainNetworks.map { it.blockchain },
|
||||
)
|
||||
}
|
||||
.map { blockchainNetwork ->
|
||||
walletManagersRepository.findOrMake(
|
||||
userWallet = userWallet,
|
||||
blockchainNetwork = blockchainNetwork,
|
||||
refresh = true,
|
||||
)
|
||||
.flatMap { walletManager ->
|
||||
walletStoresRepository.storeOrUpdate(
|
||||
userWalletId = userWalletId,
|
||||
walletStore = WalletStoreBuilder(userWallet)
|
||||
.setWalletManager(walletManager)
|
||||
.setBlockchainNetwork(blockchainNetwork)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
.fold()
|
||||
.flatMap {
|
||||
walletAmountsRepository.update(
|
||||
userWallet = userWallet,
|
||||
fiatCurrency = appCurrencyProvider(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.tap.domain.walletStores
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.core.TangemError
|
||||
|
||||
sealed class WalletStoresError(code: Int) : TangemError(code) {
|
||||
override val silent: Boolean
|
||||
get() = (cause as? TangemError)?.silent == true
|
||||
|
||||
override val messageResId: Int? = null
|
||||
override val message: String?
|
||||
get() = customMessage
|
||||
|
||||
class FetchFiatRatesError(
|
||||
currencies: List<String>,
|
||||
override val cause: Throwable?,
|
||||
) : WalletStoresError(60011) {
|
||||
override var customMessage: String = "Failed to fetch fiat rates for currencies $currencies"
|
||||
}
|
||||
|
||||
class UnknownBlockchain : WalletStoresError(60012) {
|
||||
override var customMessage: String = "Unknown blockchain"
|
||||
}
|
||||
|
||||
object NoInternetConnection : WalletStoresError(60013) {
|
||||
override var customMessage: String = "No internet connection"
|
||||
}
|
||||
|
||||
class WalletManagerNotCreated(blockchain: Blockchain) : WalletStoresError(60014) {
|
||||
override var customMessage: String = "Wallet manager can not be created for $blockchain"
|
||||
}
|
||||
|
||||
class UpdateWalletManagerError(
|
||||
blockchain: Blockchain,
|
||||
override val cause: Throwable,
|
||||
) : WalletStoresError(600015) {
|
||||
override var customMessage: String = "Unable to update wallet manager for currency $blockchain: $cause"
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,29 @@ package com.tangem.tap.domain.walletStores.di
|
|||
|
||||
import com.tangem.tap.domain.walletStores.WalletStoresManager
|
||||
import com.tangem.tap.domain.walletStores.implementation.DummyWalletStoresManager
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.domain.tokens.UserTokensRepository
|
||||
import com.tangem.tap.domain.walletStores.implementation.DefaultWalletStoresManager
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
|
||||
|
||||
fun WalletStoresManager.Companion.provideDummyImplementation(): WalletStoresManager {
|
||||
return DummyWalletStoresManager()
|
||||
}
|
||||
|
||||
fun WalletStoresManager.Companion.provideDefaultImplementation(
|
||||
userTokensRepository: UserTokensRepository,
|
||||
walletStoresRepository: WalletStoresRepository,
|
||||
walletAmountsRepository: WalletAmountsRepository,
|
||||
walletManagersRepository: WalletManagersRepository,
|
||||
appCurrencyProvider: () -> FiatCurrency,
|
||||
): WalletStoresManager {
|
||||
return DefaultWalletStoresManager(
|
||||
userTokensRepository = userTokensRepository,
|
||||
walletStoresRepository = walletStoresRepository,
|
||||
walletAmountsRepository = walletAmountsRepository,
|
||||
walletManagersRepository = walletManagersRepository,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
package com.tangem.tap.domain.walletStores.implementation
|
||||
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.flatMap
|
||||
import com.tangem.common.flatMapOnFailure
|
||||
import com.tangem.common.map
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
import com.tangem.tap.domain.model.builders.WalletStoreBuilder
|
||||
import com.tangem.tap.domain.tokens.UserTokensRepository
|
||||
import com.tangem.tap.domain.walletStores.WalletStoresError
|
||||
import com.tangem.tap.domain.walletStores.WalletStoresManager
|
||||
import com.tangem.tap.domain.walletStores.implementation.utils.fold
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
|
||||
import com.tangem.tap.features.wallet.models.toBlockchainNetworks
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultWalletStoresManager(
|
||||
private val userTokensRepository: UserTokensRepository,
|
||||
private val walletStoresRepository: WalletStoresRepository,
|
||||
private val walletAmountsRepository: WalletAmountsRepository,
|
||||
private val walletManagersRepository: WalletManagersRepository,
|
||||
private val appCurrencyProvider: () -> FiatCurrency,
|
||||
) : WalletStoresManager {
|
||||
private val state = MutableStateFlow(State())
|
||||
|
||||
override fun getAll(): Flow<Map<UserWalletId, List<WalletStoreModel>>> {
|
||||
return walletStoresRepository.getAll()
|
||||
}
|
||||
|
||||
override fun get(userWalletId: UserWalletId): Flow<List<WalletStoreModel>> {
|
||||
return walletStoresRepository.get(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun delete(userWalletsIds: List<String>): CompletionResult<Unit> {
|
||||
val walletIds = userWalletsIds.map { UserWalletId(it) }
|
||||
return walletStoresRepository.delete(walletIds)
|
||||
.flatMap { walletManagersRepository.delete(walletIds) }
|
||||
}
|
||||
|
||||
override suspend fun clear(): CompletionResult<Unit> {
|
||||
return walletStoresRepository.clear()
|
||||
}
|
||||
|
||||
override suspend fun fetch(
|
||||
userWallets: List<UserWallet>,
|
||||
refresh: Boolean,
|
||||
): CompletionResult<Unit> {
|
||||
val fiatCurrency = appCurrencyProvider.invoke()
|
||||
val isFiatCurrencyChanged = state.value.fiatCurrency != fiatCurrency
|
||||
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
fiatCurrency = fiatCurrency,
|
||||
)
|
||||
}
|
||||
|
||||
return userWallets
|
||||
.mapNotNull { userWallet ->
|
||||
val hasNotWalletStoresForUserWallet = !walletStoresRepository.contains(userWallet.walletId)
|
||||
if (refresh || hasNotWalletStoresForUserWallet || isFiatCurrencyChanged) {
|
||||
fetchWalletsIfNeeded(userWallet, refresh)
|
||||
} else null
|
||||
}
|
||||
.fold(initial = arrayListOf<UserWallet>()) { acc, data ->
|
||||
acc.apply { add(data) }
|
||||
}
|
||||
.flatMap {
|
||||
walletAmountsRepository.update(it, fiatCurrency)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun fetch(
|
||||
userWallet: UserWallet,
|
||||
refresh: Boolean,
|
||||
): CompletionResult<Unit> {
|
||||
return fetch(listOf(userWallet), refresh)
|
||||
}
|
||||
|
||||
private suspend fun fetchWalletsIfNeeded(
|
||||
userWallet: UserWallet,
|
||||
refresh: Boolean,
|
||||
): CompletionResult<UserWallet> {
|
||||
return if (userWallet.scanResponse.card.isMultiwalletAllowed) {
|
||||
fetchMultiWallets(userWallet, refresh)
|
||||
} else {
|
||||
fetchSingleWallet(userWallet, refresh)
|
||||
}
|
||||
.map { userWallet }
|
||||
}
|
||||
|
||||
private suspend fun fetchMultiWallets(
|
||||
userWallet: UserWallet,
|
||||
refresh: Boolean,
|
||||
): CompletionResult<Unit> {
|
||||
val scanResponse = userWallet.scanResponse
|
||||
val userTokens = withContext(Dispatchers.IO) {
|
||||
userTokensRepository.getUserTokens(scanResponse.card)
|
||||
}
|
||||
val userWalletId = userWallet.walletId
|
||||
|
||||
return withContext(Dispatchers.Default) {
|
||||
userTokens.toBlockchainNetworks()
|
||||
.also { blockchainNetworks ->
|
||||
walletStoresRepository.deleteDifference(
|
||||
userWalletId = userWalletId,
|
||||
currentBlockchains = blockchainNetworks.map { it.blockchain },
|
||||
)
|
||||
}
|
||||
.map { blockchainNetwork ->
|
||||
val storeWalletStore: suspend (WalletManager?) -> CompletionResult<Unit> =
|
||||
{ walletManager ->
|
||||
walletStoresRepository.storeOrUpdate(
|
||||
userWalletId = userWalletId,
|
||||
walletStore = WalletStoreBuilder(userWallet)
|
||||
.setWalletManager(walletManager)
|
||||
.setBlockchainNetwork(blockchainNetwork)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
walletManagersRepository.findOrMake(
|
||||
userWallet = userWallet,
|
||||
blockchainNetwork = blockchainNetwork,
|
||||
refresh = refresh,
|
||||
)
|
||||
.flatMap { walletManager ->
|
||||
storeWalletStore(walletManager)
|
||||
}
|
||||
.flatMapOnFailure { error ->
|
||||
when (error) {
|
||||
is WalletStoresError.WalletManagerNotCreated,
|
||||
is WalletStoresError.UpdateWalletManagerError,
|
||||
-> storeWalletStore(null)
|
||||
else -> CompletionResult.Failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
.fold()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchSingleWallet(
|
||||
userWallet: UserWallet,
|
||||
refresh: Boolean,
|
||||
): CompletionResult<Unit> {
|
||||
return walletManagersRepository.findOrMake(
|
||||
userWallet = userWallet,
|
||||
refresh = refresh,
|
||||
)
|
||||
.flatMap { walletManager ->
|
||||
walletStoresRepository.storeOrUpdate(
|
||||
userWalletId = userWallet.walletId,
|
||||
walletStore = WalletStoreBuilder(userWallet)
|
||||
.setWalletManager(walletManager)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal data class State(
|
||||
val fiatCurrency: FiatCurrency? = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.tap.domain.walletStores.implementation.utils
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
|
||||
internal fun List<CompletionResult<Unit>>.fold(): CompletionResult<Unit> {
|
||||
return fold(Unit) { _, _ -> Unit }
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal inline fun <reified D, reified R> List<CompletionResult<D>>.fold(
|
||||
initial: R,
|
||||
operation: (acc: R, data: D) -> R,
|
||||
): CompletionResult<R> {
|
||||
var resultData = initial
|
||||
for (result in this) {
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
resultData = operation(resultData, result.data)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
return result as CompletionResult.Failure<R>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return CompletionResult.Success(resultData)
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.tap.domain.walletStores.repository
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
|
||||
interface WalletAmountsRepository {
|
||||
suspend fun update(
|
||||
userWallets: List<UserWallet>,
|
||||
fiatCurrency: FiatCurrency,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
suspend fun update(
|
||||
userWallet: UserWallet,
|
||||
fiatCurrency: FiatCurrency,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
suspend fun update(
|
||||
userWallet: UserWallet,
|
||||
walletStore: WalletStoreModel,
|
||||
fiatCurrency: FiatCurrency,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
companion object
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.tap.domain.walletStores.repository
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
|
||||
interface WalletManagersRepository {
|
||||
suspend fun findOrMake(
|
||||
userWallet: UserWallet,
|
||||
blockchainNetwork: BlockchainNetwork? = null,
|
||||
refresh: Boolean = false,
|
||||
): CompletionResult<WalletManager>
|
||||
|
||||
suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit>
|
||||
|
||||
suspend fun delete(
|
||||
userWalletId: UserWalletId,
|
||||
blockchain: Blockchain,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
companion object
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.tap.domain.walletStores.repository
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface WalletStoresRepository {
|
||||
fun getAll(): Flow<Map<UserWalletId, List<WalletStoreModel>>>
|
||||
fun get(userWalletId: UserWalletId): Flow<List<WalletStoreModel>>
|
||||
|
||||
suspend fun contains(userWalletId: UserWalletId): Boolean
|
||||
|
||||
suspend fun delete(userWalletsIds: List<UserWalletId>): CompletionResult<Unit>
|
||||
|
||||
suspend fun deleteDifference(
|
||||
userWalletId: UserWalletId,
|
||||
currentBlockchains: List<Blockchain>,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
suspend fun clear(): CompletionResult<Unit>
|
||||
|
||||
suspend fun storeOrUpdate(
|
||||
userWalletId: UserWalletId,
|
||||
walletStore: WalletStoreModel,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
companion object
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.tap.domain.walletStores.repository.di
|
||||
|
||||
import com.tangem.blockchain.common.WalletManagerFactory
|
||||
import com.tangem.network.api.tangemTech.TangemTechService
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.DefaultWalletAmountsRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.DefaultWalletManagersRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.DefaultWalletStoresRepository
|
||||
|
||||
fun WalletStoresRepository.Companion.provideDefaultImplementation(): WalletStoresRepository {
|
||||
return DefaultWalletStoresRepository()
|
||||
}
|
||||
|
||||
fun WalletManagersRepository.Companion.provideDefaultImplementation(
|
||||
walletManagerFactory: WalletManagerFactory,
|
||||
): WalletManagersRepository {
|
||||
return DefaultWalletManagersRepository(walletManagerFactory)
|
||||
}
|
||||
|
||||
fun WalletAmountsRepository.Companion.provideDefaultImplementation(
|
||||
tangemTechService: TangemTechService,
|
||||
): WalletAmountsRepository {
|
||||
return DefaultWalletAmountsRepository(tangemTechService)
|
||||
}
|
||||
|
|
@ -0,0 +1,427 @@
|
|||
package com.tangem.tap.domain.walletStores.repository.implementation
|
||||
|
||||
import com.tangem.blockchain.blockchains.solana.RentProvider
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.extensions.Result.Failure
|
||||
import com.tangem.blockchain.extensions.Result.Success
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.catching
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.flatMap
|
||||
import com.tangem.common.flatMapOnFailure
|
||||
import com.tangem.common.map
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.network.api.tangemTech.TangemTechService
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.common.extensions.replaceByOrAdd
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
import com.tangem.tap.domain.walletStores.WalletStoresError
|
||||
import com.tangem.tap.domain.walletStores.implementation.utils.fold
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.replaceWalletStore
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithAmounts
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithError
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithFiatRates
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithMissedDerivation
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithRent
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithUnreachable
|
||||
import com.tangem.tap.domain.walletStores.storage.WalletManagerStorage
|
||||
import com.tangem.tap.domain.walletStores.storage.WalletStoresStorage
|
||||
import com.tangem.tap.features.wallet.models.PendingTransactionType
|
||||
import com.tangem.tap.features.wallet.models.filterByCoin
|
||||
import com.tangem.tap.features.wallet.models.getPendingTransactions
|
||||
import com.tangem.tap.network.NetworkConnectivity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class DefaultWalletAmountsRepository(
|
||||
private val tangemTechService: TangemTechService,
|
||||
) : WalletAmountsRepository {
|
||||
private val walletStoresStorage = WalletStoresStorage
|
||||
private val walletManagersStorage = WalletManagerStorage
|
||||
|
||||
override suspend fun update(
|
||||
userWallets: List<UserWallet>,
|
||||
fiatCurrency: FiatCurrency,
|
||||
): CompletionResult<Unit> {
|
||||
return if (userWallets.isEmpty()) CompletionResult.Success(Unit)
|
||||
else withContext(Dispatchers.Default) {
|
||||
awaitAll(
|
||||
async { fetchAmounts(userWallets) },
|
||||
async { fetchFiatRates(userWallets, fiatCurrency) },
|
||||
)
|
||||
.fold()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun update(
|
||||
userWallet: UserWallet,
|
||||
fiatCurrency: FiatCurrency,
|
||||
): CompletionResult<Unit> {
|
||||
return update(listOf(userWallet), fiatCurrency)
|
||||
}
|
||||
|
||||
override suspend fun update(
|
||||
userWallet: UserWallet,
|
||||
walletStore: WalletStoreModel,
|
||||
fiatCurrency: FiatCurrency,
|
||||
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
|
||||
val walletId = userWallet.walletId
|
||||
val scanResponse = userWallet.scanResponse
|
||||
|
||||
awaitAll(
|
||||
async {
|
||||
// TODO: Find wallet manager via [com.tangem.tap.domain.walletStores.repository.WalletManagersRepository]
|
||||
val walletManager = walletStore.walletManager
|
||||
fetchAmounts(walletId, scanResponse, walletStore, walletManager)
|
||||
.flatMap { fetchRentIfNeeded(walletStore, walletManager) }
|
||||
},
|
||||
async { fetchFiatRates(listOf(userWallet), fiatCurrency) },
|
||||
)
|
||||
.fold()
|
||||
}
|
||||
|
||||
private suspend fun fetchAmounts(
|
||||
userWallets: List<UserWallet>,
|
||||
): CompletionResult<Unit> = coroutineScope {
|
||||
userWallets.map { userWallet ->
|
||||
val walletId = userWallet.walletId
|
||||
val scanResponse = userWallet.scanResponse
|
||||
val walletStores = walletStoresStorage.getSync(walletId)
|
||||
|
||||
walletStores.map { walletStore ->
|
||||
async {
|
||||
// TODO: Find wallet manager via [com.tangem.tap.domain.walletStores.repository.WalletManagersRepository]
|
||||
val walletManager = walletStore.walletManager
|
||||
fetchAmounts(walletId, scanResponse, walletStore, walletManager)
|
||||
.flatMap { fetchRentIfNeeded(walletStore, walletManager) }
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
.fold()
|
||||
}
|
||||
.fold()
|
||||
}
|
||||
|
||||
private suspend fun fetchFiatRates(
|
||||
userWallets: List<UserWallet>,
|
||||
fiatCurrency: FiatCurrency,
|
||||
): CompletionResult<Unit> {
|
||||
val walletsIds = userWallets.map { it.walletId }
|
||||
val walletStores = walletsIds
|
||||
.flatMap { walletStoresStorage.getSync(it) }
|
||||
|
||||
val currencies = walletStores
|
||||
.asSequence()
|
||||
.flatMap { it.walletsData }
|
||||
.map { it.currency }
|
||||
|
||||
val coinsIds = currencies.mapNotNull { it.coinId }.distinct().toList()
|
||||
|
||||
val fiatRatesResult = withContext(Dispatchers.IO) {
|
||||
tangemTechService.rates(
|
||||
currency = fiatCurrency.code,
|
||||
ids = coinsIds,
|
||||
)
|
||||
}
|
||||
|
||||
return when (fiatRatesResult) {
|
||||
is Result.Success -> {
|
||||
Timber.d(
|
||||
"""
|
||||
Fetched fiat rates
|
||||
|- User wallets ids: $walletsIds
|
||||
|- Coins ids: $coinsIds
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
walletStores.forEach { walletStore ->
|
||||
updateWithFiatRates(
|
||||
walletStore = walletStore,
|
||||
fiatRates = fiatRatesResult.data.rates,
|
||||
)
|
||||
}
|
||||
|
||||
CompletionResult.Success(Unit)
|
||||
}
|
||||
is Result.Failure -> {
|
||||
val error = WalletStoresError.FetchFiatRatesError(
|
||||
currencies = currencies.map { it.currencySymbol }.toList(),
|
||||
cause = fiatRatesResult.error,
|
||||
)
|
||||
|
||||
Timber.e(
|
||||
error,
|
||||
"""
|
||||
Unable to fetch fiat rates
|
||||
|- User wallets ids: $walletsIds
|
||||
|- Coins ids: $coinsIds
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
CompletionResult.Failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchAmounts(
|
||||
walletId: UserWalletId,
|
||||
scanResponse: ScanResponse,
|
||||
walletStore: WalletStoreModel,
|
||||
walletManager: WalletManager?,
|
||||
): CompletionResult<Unit> {
|
||||
val hasMissedDerivations = with(walletStore.blockchainNetwork) {
|
||||
derivationPath != null && !scanResponse.hasDerivation(blockchain, derivationPath)
|
||||
}
|
||||
val blockchain = walletStore.blockchainNetwork.blockchain
|
||||
val tokens = walletStore.blockchainNetwork.tokens.map { it.name }
|
||||
|
||||
return when {
|
||||
hasMissedDerivations -> {
|
||||
Timber.e(
|
||||
"""
|
||||
Missed derivation
|
||||
|- User wallet id: $walletId
|
||||
|- Blockchain: $blockchain
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
updateWithMissedDerivation(
|
||||
walletStore = walletStore,
|
||||
)
|
||||
|
||||
CompletionResult.Success(Unit)
|
||||
}
|
||||
walletManager == null -> {
|
||||
Timber.e(
|
||||
"""
|
||||
Wallet manager is null
|
||||
|- User wallet id: $walletId
|
||||
|- Blockchain: $blockchain
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
updateWithUnreachable(
|
||||
walletStore = walletStore,
|
||||
)
|
||||
|
||||
CompletionResult.Success(Unit)
|
||||
}
|
||||
else -> {
|
||||
withInternetConnection { walletManager.update() }
|
||||
.map { updateWalletManagerWithAmounts(walletId, walletManager) }
|
||||
.doOnSuccess {
|
||||
Timber.d(
|
||||
"""
|
||||
Fetched amounts
|
||||
|- User wallet id: $walletId
|
||||
|- Blockchain: $blockchain
|
||||
|- Tokens: $tokens
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
updateWithAmounts(
|
||||
walletStore = walletStore,
|
||||
wallet = walletManager.wallet,
|
||||
)
|
||||
}
|
||||
.flatMapOnFailure { error ->
|
||||
Timber.e(
|
||||
error,
|
||||
"""
|
||||
Unable to fetch amounts
|
||||
|- User wallet id: $walletId
|
||||
|- Blockchain: $blockchain
|
||||
|- Tokens: $tokens
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
if (error is BlockchainSdkError) {
|
||||
updateWithError(
|
||||
walletStore = walletStore,
|
||||
wallet = walletManager.wallet,
|
||||
error = error,
|
||||
)
|
||||
CompletionResult.Success(Unit)
|
||||
} else {
|
||||
CompletionResult.Failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchRentIfNeeded(
|
||||
walletStore: WalletStoreModel,
|
||||
walletManager: WalletManager?,
|
||||
): CompletionResult<Unit> {
|
||||
val rentProvider = walletManager as? RentProvider
|
||||
|
||||
if (walletManager == null || rentProvider == null) {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
when (val result = rentProvider.minimalBalanceForRentExemption()) {
|
||||
is Success -> {
|
||||
val balance = walletManager.wallet.fundsAvailable(AmountType.Coin)
|
||||
val outgoingTxs = walletManager.wallet.getPendingTransactions(
|
||||
PendingTransactionType.Outgoing,
|
||||
).filterByCoin()
|
||||
|
||||
val rentExempt = result.data
|
||||
val setRent = if (outgoingTxs.isEmpty()) {
|
||||
balance < rentExempt
|
||||
} else {
|
||||
val outgoingAmount = outgoingTxs.sumOf { it.amountValue ?: BigDecimal.ZERO }
|
||||
val rest = balance.minus(outgoingAmount)
|
||||
balance < rest
|
||||
}
|
||||
|
||||
updateWithRent(
|
||||
walletStore = walletStore,
|
||||
rent = if (setRent) {
|
||||
WalletStoreModel.WalletRent(
|
||||
rent = rentProvider.rentAmount(),
|
||||
exemptionAmount = rentExempt,
|
||||
)
|
||||
} else null,
|
||||
)
|
||||
}
|
||||
is Failure -> Unit
|
||||
}
|
||||
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
private suspend inline fun withInternetConnection(crossinline block: suspend () -> Unit): CompletionResult<Unit> {
|
||||
return if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
|
||||
val error = WalletStoresError.NoInternetConnection
|
||||
Timber.e(error)
|
||||
CompletionResult.Failure(error)
|
||||
} else withContext(Dispatchers.IO) {
|
||||
catching { block() }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWalletManagerWithAmounts(
|
||||
walletId: UserWalletId,
|
||||
walletManager: WalletManager,
|
||||
) = withContext(Dispatchers.Default) {
|
||||
walletManagersStorage.update { prevManagers ->
|
||||
val newManagersForUserWallet = prevManagers[walletId].orEmpty()
|
||||
.toMutableList()
|
||||
.apply {
|
||||
replaceByOrAdd(walletManager) {
|
||||
it.wallet.blockchain == it.wallet.blockchain
|
||||
}
|
||||
}
|
||||
|
||||
prevManagers.apply {
|
||||
set(walletId, newManagersForUserWallet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWithError(
|
||||
walletStore: WalletStoreModel,
|
||||
wallet: Wallet,
|
||||
error: BlockchainSdkError,
|
||||
) = withContext(Dispatchers.Default) {
|
||||
walletStoresStorage.update { prevState ->
|
||||
prevState.replaceWalletStore(
|
||||
walletId = walletStore.userWalletId,
|
||||
walletStore = walletStore,
|
||||
update = {
|
||||
it.updateWithError(
|
||||
wallet = wallet,
|
||||
error = error,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWithAmounts(
|
||||
walletStore: WalletStoreModel,
|
||||
wallet: Wallet,
|
||||
) = withContext(Dispatchers.Default) {
|
||||
walletStoresStorage.update { prevState ->
|
||||
prevState.replaceWalletStore(
|
||||
walletId = walletStore.userWalletId,
|
||||
walletStore = walletStore,
|
||||
update = {
|
||||
it.updateWithAmounts(wallet = wallet)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWithMissedDerivation(
|
||||
walletStore: WalletStoreModel,
|
||||
) = withContext(Dispatchers.Default) {
|
||||
walletStoresStorage.update { prevState ->
|
||||
prevState.replaceWalletStore(
|
||||
walletId = walletStore.userWalletId,
|
||||
walletStore = walletStore,
|
||||
update = {
|
||||
it.updateWithMissedDerivation()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWithUnreachable(
|
||||
walletStore: WalletStoreModel,
|
||||
) = withContext(Dispatchers.Default) {
|
||||
walletStoresStorage.update { prevState ->
|
||||
prevState.replaceWalletStore(
|
||||
walletId = walletStore.userWalletId,
|
||||
walletStore = walletStore,
|
||||
update = {
|
||||
it.updateWithUnreachable()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWithFiatRates(
|
||||
walletStore: WalletStoreModel,
|
||||
fiatRates: Map<String, Double>,
|
||||
) = withContext(Dispatchers.Default) {
|
||||
walletStoresStorage.update { prevState ->
|
||||
prevState.replaceWalletStore(
|
||||
walletId = walletStore.userWalletId,
|
||||
walletStore = walletStore,
|
||||
update = {
|
||||
it.updateWithFiatRates(rates = fiatRates)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWithRent(
|
||||
walletStore: WalletStoreModel,
|
||||
rent: WalletStoreModel.WalletRent?,
|
||||
) = withContext(Dispatchers.Default) {
|
||||
walletStoresStorage.update { prevState ->
|
||||
prevState.replaceWalletStore(
|
||||
walletId = walletStore.userWalletId,
|
||||
walletStore = walletStore,
|
||||
update = {
|
||||
it.updateWithRent(rent)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
package com.tangem.tap.domain.walletStores.repository.implementation
|
||||
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.catching
|
||||
import com.tangem.common.hdWallet.DerivationPath
|
||||
import com.tangem.common.map
|
||||
import com.tangem.common.mapFailure
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.domain.walletStores.WalletStoresError
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
|
||||
import com.tangem.tap.domain.walletStores.storage.WalletManagerStorage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultWalletManagersRepository(
|
||||
private val walletManagerFactory: WalletManagerFactory,
|
||||
) : WalletManagersRepository {
|
||||
private val walletManagersStorage = WalletManagerStorage
|
||||
|
||||
override suspend fun findOrMake(
|
||||
userWallet: UserWallet,
|
||||
blockchainNetwork: BlockchainNetwork?,
|
||||
refresh: Boolean,
|
||||
): CompletionResult<WalletManager> = withContext(Dispatchers.Default) {
|
||||
if (refresh) {
|
||||
deleteInternal(userWallet.walletId, blockchainNetwork?.blockchain)
|
||||
makeAndStore(userWallet, blockchainNetwork)
|
||||
} else {
|
||||
val foundWalletManager = findWalletManager(
|
||||
userWalletId = userWallet.walletId,
|
||||
blockchain = blockchainNetwork?.blockchain,
|
||||
)
|
||||
|
||||
foundWalletManager?.updateTokens(
|
||||
scanResponse = userWallet.scanResponse,
|
||||
blockchainNetwork = blockchainNetwork,
|
||||
)
|
||||
?: makeAndStore(userWallet, blockchainNetwork)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun makeAndStore(
|
||||
userWallet: UserWallet,
|
||||
blockchainNetwork: BlockchainNetwork?,
|
||||
): CompletionResult<WalletManager> {
|
||||
val scanResponse = userWallet.scanResponse
|
||||
val blockchain = blockchainNetwork?.blockchain
|
||||
?: scanResponse.getBlockchain().let { blockchain ->
|
||||
if (scanResponse.card.isTestCard) blockchain.getTestnetVersion() else blockchain
|
||||
}
|
||||
val derivationParams = getDerivationParams(
|
||||
derivationPath = blockchainNetwork?.derivationPath,
|
||||
card = scanResponse.card,
|
||||
)
|
||||
|
||||
val walletManager = blockchain?.let {
|
||||
walletManagerFactory.makeWalletManagerForApp(
|
||||
scanResponse = userWallet.scanResponse,
|
||||
blockchain = blockchain,
|
||||
derivationParams = derivationParams,
|
||||
)
|
||||
}
|
||||
|
||||
return when {
|
||||
blockchain == Blockchain.Unknown || blockchain == null -> {
|
||||
val error = WalletStoresError.UnknownBlockchain()
|
||||
Timber.e(error)
|
||||
CompletionResult.Failure(error)
|
||||
}
|
||||
walletManager != null -> {
|
||||
walletManager.updateTokens(
|
||||
scanResponse = scanResponse,
|
||||
blockchainNetwork = blockchainNetwork,
|
||||
)
|
||||
.map { updatedWalletManager ->
|
||||
store(userWallet.walletId, updatedWalletManager)
|
||||
updatedWalletManager
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val error = WalletStoresError.WalletManagerNotCreated(blockchain)
|
||||
Timber.e(error)
|
||||
CompletionResult.Failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> = catching {
|
||||
walletManagersStorage.update { prevManagers ->
|
||||
prevManagers.apply {
|
||||
userWalletIds.forEach { userWalletId ->
|
||||
remove(userWalletId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(
|
||||
userWalletId: UserWalletId,
|
||||
blockchain: Blockchain,
|
||||
): CompletionResult<Unit> = catching {
|
||||
deleteInternal(userWalletId, blockchain)
|
||||
}
|
||||
|
||||
private suspend fun store(userWalletId: UserWalletId, walletManager: WalletManager) {
|
||||
walletManagersStorage.update { prevManagers ->
|
||||
prevManagers.apply {
|
||||
set(
|
||||
key = userWalletId,
|
||||
value = this[userWalletId].orEmpty() + walletManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun deleteInternal(userWalletId: UserWalletId, blockchain: Blockchain?) {
|
||||
walletManagersStorage.update { prevManagers ->
|
||||
prevManagers.apply {
|
||||
if (blockchain == null) {
|
||||
set(
|
||||
key = userWalletId,
|
||||
value = emptyList(),
|
||||
)
|
||||
} else {
|
||||
set(
|
||||
key = userWalletId,
|
||||
value = this[userWalletId]
|
||||
?.filter { it.wallet.blockchain == blockchain }
|
||||
.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletManager.updateTokens(
|
||||
scanResponse: ScanResponse,
|
||||
blockchainNetwork: BlockchainNetwork?,
|
||||
): CompletionResult<WalletManager> {
|
||||
val walletManager = this
|
||||
return catching {
|
||||
val tokens = blockchainNetwork?.tokens ?: listOfNotNull(scanResponse.getPrimaryToken())
|
||||
|
||||
if (tokens.isNotEmpty()) {
|
||||
walletManager.addTokens(tokens)
|
||||
}
|
||||
|
||||
walletManager
|
||||
}
|
||||
.mapFailure {
|
||||
val error = WalletStoresError.UpdateWalletManagerError(
|
||||
blockchain = walletManager.wallet.blockchain,
|
||||
cause = it,
|
||||
)
|
||||
Timber.e(error)
|
||||
error
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun findWalletManager(
|
||||
userWalletId: UserWalletId,
|
||||
blockchain: Blockchain?,
|
||||
): WalletManager? {
|
||||
return walletManagersStorage.getAllSync()[userWalletId]?.let { userWalletManagers ->
|
||||
if (blockchain == null) userWalletManagers.firstOrNull()
|
||||
else userWalletManagers.find { it.wallet.blockchain == blockchain }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDerivationParams(derivationPath: String?, card: CardDTO): DerivationParams? {
|
||||
return derivationPath?.let {
|
||||
DerivationParams.Custom(
|
||||
path = DerivationPath(it),
|
||||
)
|
||||
} ?: if (!card.settings.isHDWalletAllowed) {
|
||||
null
|
||||
} else if (card.useOldStyleDerivation) {
|
||||
DerivationParams.Default(DerivationStyle.LEGACY)
|
||||
} else {
|
||||
DerivationParams.Default(DerivationStyle.NEW)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package com.tangem.tap.domain.walletStores.repository.implementation
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.catching
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.isSameWalletStore
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.replaceWalletStore
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithSelf
|
||||
import com.tangem.tap.domain.walletStores.storage.WalletStoresStorage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultWalletStoresRepository : WalletStoresRepository {
|
||||
private val walletStoresStorage = WalletStoresStorage
|
||||
|
||||
override fun getAll(): Flow<Map<UserWalletId, List<WalletStoreModel>>> {
|
||||
return walletStoresStorage.getAll()
|
||||
}
|
||||
|
||||
override fun get(userWalletId: UserWalletId): Flow<List<WalletStoreModel>> {
|
||||
return walletStoresStorage.get(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun contains(userWalletId: UserWalletId): Boolean {
|
||||
return walletStoresStorage.getSync(userWalletId).isNotEmpty()
|
||||
}
|
||||
|
||||
override suspend fun delete(userWalletsIds: List<UserWalletId>): CompletionResult<Unit> = catching {
|
||||
walletStoresStorage.update { prevStores ->
|
||||
prevStores.filterKeys { it !in userWalletsIds } as HashMap<UserWalletId, List<WalletStoreModel>>
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteDifference(
|
||||
userWalletId: UserWalletId,
|
||||
currentBlockchains: List<Blockchain>,
|
||||
): CompletionResult<Unit> = catching {
|
||||
walletStoresStorage.update { prevStores ->
|
||||
prevStores.apply {
|
||||
this[userWalletId] = this[userWalletId]
|
||||
?.filter { it.blockchainNetwork.blockchain in currentBlockchains }
|
||||
.orEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clear(): CompletionResult<Unit> = catching {
|
||||
walletStoresStorage.update { hashMapOf() }
|
||||
}
|
||||
|
||||
override suspend fun storeOrUpdate(
|
||||
userWalletId: UserWalletId,
|
||||
walletStore: WalletStoreModel,
|
||||
): CompletionResult<Unit> = catching {
|
||||
walletStoresStorage.update { prevStores ->
|
||||
prevStores.addOrUpdate(userWalletId, walletStore)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun HashMap<UserWalletId, List<WalletStoreModel>>.addOrUpdate(
|
||||
userWalletId: UserWalletId,
|
||||
walletStore: WalletStoreModel,
|
||||
): HashMap<UserWalletId, List<WalletStoreModel>> = withContext(Dispatchers.Default) {
|
||||
val prevStores = this@addOrUpdate
|
||||
val walletStores = prevStores[userWalletId]
|
||||
|
||||
if (walletStores.isNullOrEmpty()) {
|
||||
prevStores.apply {
|
||||
set(userWalletId, listOf(walletStore))
|
||||
}
|
||||
} else {
|
||||
val oldWalletStore = walletStores.find(walletStore::isSameWalletStore)
|
||||
|
||||
if (oldWalletStore == null) {
|
||||
prevStores.apply {
|
||||
set(userWalletId, walletStores + walletStore)
|
||||
}
|
||||
} else {
|
||||
prevStores.replaceWalletStore(
|
||||
walletId = userWalletId,
|
||||
walletStore = oldWalletStore,
|
||||
update = { it.updateWithSelf(walletStore) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
package com.tangem.tap.domain.walletStores.repository.implementation.utils
|
||||
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.tap.domain.extensions.amountToCreateAccount
|
||||
import com.tangem.tap.domain.getFirstToken
|
||||
import com.tangem.tap.domain.model.WalletDataModel
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.models.getPendingTransactions
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal fun WalletDataModel.updateWithFiatRate(
|
||||
fiatRate: BigDecimal?,
|
||||
): WalletDataModel {
|
||||
return this.copy(
|
||||
fiatRate = fiatRate,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun List<WalletDataModel>.updateWithFiatRates(
|
||||
fiatRates: Map<String, Double>,
|
||||
): List<WalletDataModel> {
|
||||
return this.map { walletData ->
|
||||
val rate = fiatRates[walletData.currency.coinId]?.toBigDecimal()
|
||||
walletData.updateWithFiatRate(rate)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun WalletDataModel.updateWithAmount(wallet: Wallet): WalletDataModel {
|
||||
val pendingTransactions = wallet.getPendingTransactions()
|
||||
return this.copy(
|
||||
status = when (val currency = this.currency) {
|
||||
is Currency.Blockchain -> {
|
||||
val amount = wallet.fundsAvailable(AmountType.Coin)
|
||||
if (pendingTransactions.isEmpty()) {
|
||||
WalletDataModel.VerifiedOnline(
|
||||
amount = amount,
|
||||
)
|
||||
} else {
|
||||
WalletDataModel.TransactionInProgress(
|
||||
amount = amount,
|
||||
pendingTransactions = pendingTransactions,
|
||||
)
|
||||
}
|
||||
}
|
||||
is Currency.Token -> {
|
||||
val token = currency.token
|
||||
val amount = wallet.fundsAvailable(AmountType.Token(token))
|
||||
val hasTokenPendingTransactions = pendingTransactions
|
||||
.any { it.transactionData.amount.currencySymbol == token.symbol }
|
||||
when {
|
||||
hasTokenPendingTransactions -> {
|
||||
WalletDataModel.TransactionInProgress(
|
||||
amount = amount,
|
||||
pendingTransactions = pendingTransactions,
|
||||
)
|
||||
}
|
||||
pendingTransactions.isNotEmpty() -> {
|
||||
WalletDataModel.SameCurrencyTransactionInProgress(
|
||||
amount = amount,
|
||||
pendingTransactions = pendingTransactions,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
WalletDataModel.VerifiedOnline(
|
||||
amount = amount,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal fun List<WalletDataModel>.updateWithAmounts(wallet: Wallet): List<WalletDataModel> {
|
||||
return this.map { walletData ->
|
||||
walletData.updateWithAmount(wallet)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun WalletDataModel.updateWithError(
|
||||
wallet: Wallet,
|
||||
error: TangemError,
|
||||
): WalletDataModel {
|
||||
return this.copy(
|
||||
status = when (error) {
|
||||
is BlockchainSdkError.AccountNotFound -> {
|
||||
val amountToCreateAccount = wallet.blockchain
|
||||
.amountToCreateAccount(wallet.getFirstToken())
|
||||
|
||||
if (amountToCreateAccount != null) {
|
||||
WalletDataModel.NoAccount(
|
||||
amountToCreateAccount = amountToCreateAccount,
|
||||
)
|
||||
} else {
|
||||
WalletDataModel.Unreachable(
|
||||
errorMessage = error.customMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> WalletDataModel.Unreachable(
|
||||
errorMessage = error.customMessage,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal fun List<WalletDataModel>.updateWithError(
|
||||
wallet: Wallet,
|
||||
error: TangemError,
|
||||
): List<WalletDataModel> {
|
||||
return this.map { walletData ->
|
||||
walletData.updateWithError(wallet, error)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun WalletDataModel.updateWithSelf(
|
||||
newWalletData: WalletDataModel,
|
||||
): WalletDataModel {
|
||||
val oldWalletData = this
|
||||
val oldStatus = oldWalletData.status
|
||||
return oldWalletData.copy(
|
||||
status = when (val newStatus = newWalletData.status) {
|
||||
is WalletDataModel.Loading -> when (oldStatus) {
|
||||
is WalletDataModel.MissedDerivation -> WalletDataModel.Loading
|
||||
else -> oldStatus.asRefreshing()
|
||||
}
|
||||
is WalletDataModel.MissedDerivation,
|
||||
is WalletDataModel.Refreshing,
|
||||
is WalletDataModel.NoAccount,
|
||||
is WalletDataModel.Unreachable,
|
||||
is WalletDataModel.SameCurrencyTransactionInProgress,
|
||||
is WalletDataModel.TransactionInProgress,
|
||||
is WalletDataModel.VerifiedOnline,
|
||||
-> newStatus
|
||||
},
|
||||
walletAddresses = newWalletData.walletAddresses,
|
||||
existentialDeposit = newWalletData.existentialDeposit,
|
||||
fiatRate = newWalletData.fiatRate ?: oldWalletData.fiatRate,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun List<WalletDataModel>.updateWithMissedDerivation(): List<WalletDataModel> {
|
||||
return this.map { walletData ->
|
||||
walletData.copy(
|
||||
status = WalletDataModel.MissedDerivation,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun List<WalletDataModel>.updateWithUnreachable(): List<WalletDataModel> {
|
||||
return this.map { walletData ->
|
||||
walletData.copy(
|
||||
status = WalletDataModel.Unreachable(errorMessage = null),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun List<WalletDataModel>.updateWithSelf(
|
||||
walletsData: List<WalletDataModel>,
|
||||
): List<WalletDataModel> {
|
||||
val oldWalletsData = this
|
||||
val updatedWalletsData = arrayListOf<WalletDataModel>()
|
||||
|
||||
walletsData.forEach { newWalletData ->
|
||||
val walletDataToUpdate = oldWalletsData.find(newWalletData::isSameWalletData)
|
||||
if (walletDataToUpdate != null) {
|
||||
updatedWalletsData.add(walletDataToUpdate.updateWithSelf(newWalletData))
|
||||
} else {
|
||||
updatedWalletsData.add(newWalletData)
|
||||
}
|
||||
}
|
||||
|
||||
return updatedWalletsData
|
||||
}
|
||||
|
||||
internal fun WalletDataModel.isSameWalletData(other: WalletDataModel): Boolean {
|
||||
return currency == other.currency
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package com.tangem.tap.domain.walletStores.repository.implementation.utils
|
||||
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
|
||||
internal inline fun HashMap<UserWalletId, List<WalletStoreModel>>.replaceWalletStore(
|
||||
walletId: UserWalletId,
|
||||
walletStore: WalletStoreModel,
|
||||
update: (walletStore: WalletStoreModel) -> WalletStoreModel,
|
||||
): HashMap<UserWalletId, List<WalletStoreModel>> {
|
||||
return this.apply {
|
||||
this[walletId] = this[walletId]
|
||||
?.replaceWalletStore(walletStore, update)
|
||||
.orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun WalletStoreModel.updateWithError(
|
||||
wallet: Wallet,
|
||||
error: TangemError,
|
||||
): WalletStoreModel {
|
||||
return this.copy(
|
||||
walletsData = walletsData.updateWithError(
|
||||
wallet = wallet,
|
||||
error = error,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun WalletStoreModel.updateWithAmounts(
|
||||
wallet: Wallet,
|
||||
): WalletStoreModel {
|
||||
return this.copy(
|
||||
walletsData = walletsData.updateWithAmounts(wallet = wallet),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun WalletStoreModel.updateWithFiatRates(
|
||||
rates: Map<String, Double>,
|
||||
): WalletStoreModel {
|
||||
return this.copy(
|
||||
walletsData = walletsData.updateWithFiatRates(rates),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun WalletStoreModel.updateWithSelf(
|
||||
newWalletStore: WalletStoreModel,
|
||||
): WalletStoreModel {
|
||||
val oldStore = this
|
||||
return oldStore.copy(
|
||||
walletManager = newWalletStore.walletManager,
|
||||
walletRent = newWalletStore.walletRent,
|
||||
walletsData = oldStore.walletsData.updateWithSelf(newWalletStore.walletsData),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun WalletStoreModel.updateWithMissedDerivation(): WalletStoreModel {
|
||||
return this.copy(
|
||||
walletsData = walletsData.updateWithMissedDerivation(),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun WalletStoreModel.updateWithUnreachable(): WalletStoreModel {
|
||||
return this.copy(
|
||||
walletsData = walletsData.updateWithUnreachable(),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun WalletStoreModel.updateWithRent(rent: WalletStoreModel.WalletRent?): WalletStoreModel {
|
||||
return this.copy(
|
||||
walletRent = rent,
|
||||
)
|
||||
}
|
||||
|
||||
internal inline fun List<WalletStoreModel>.replaceWalletStore(
|
||||
newWalletStore: WalletStoreModel,
|
||||
update: (walletStore: WalletStoreModel) -> WalletStoreModel,
|
||||
): List<WalletStoreModel> {
|
||||
val mutableStores = ArrayList(this)
|
||||
for ((index, walletStore) in this.withIndex()) {
|
||||
if (walletStore.isSameWalletStore(newWalletStore)) {
|
||||
mutableStores[index] = update(walletStore)
|
||||
break
|
||||
}
|
||||
}
|
||||
return mutableStores
|
||||
}
|
||||
|
||||
internal fun WalletStoreModel.isSameWalletStore(other: WalletStoreModel): Boolean {
|
||||
return blockchainNetwork == other.blockchainNetwork
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.tap.domain.walletStores.storage
|
||||
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
internal object WalletManagerStorage {
|
||||
private val managers =
|
||||
MutableSharedFlow<HashMap<UserWalletId, List<WalletManager>>>(replay = 1)
|
||||
|
||||
init {
|
||||
managers.tryEmit(hashMapOf())
|
||||
}
|
||||
|
||||
suspend fun getAllSync(): Map<UserWalletId, List<WalletManager>> {
|
||||
return managers.first()
|
||||
}
|
||||
|
||||
private val mutex = Mutex()
|
||||
suspend fun update(
|
||||
f: suspend (HashMap<UserWalletId, List<WalletManager>>) -> HashMap<UserWalletId, List<WalletManager>>,
|
||||
) {
|
||||
while (mutex.isLocked) {
|
||||
delay(timeMillis = 60)
|
||||
}
|
||||
|
||||
mutex.withLock {
|
||||
val prevState = managers.first()
|
||||
managers.emit(f(prevState))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.tap.domain.walletStores.storage
|
||||
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
internal object WalletStoresStorage {
|
||||
private val stores =
|
||||
MutableSharedFlow<HashMap<UserWalletId, List<WalletStoreModel>>>(replay = 1)
|
||||
|
||||
init {
|
||||
stores.tryEmit(hashMapOf())
|
||||
}
|
||||
|
||||
fun getAll(): Flow<Map<UserWalletId, List<WalletStoreModel>>> {
|
||||
return stores
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun get(userWalletId: UserWalletId): Flow<List<WalletStoreModel>> {
|
||||
return stores
|
||||
.mapLatest { stores ->
|
||||
stores[userWalletId].orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getSync(userWalletId: UserWalletId): List<WalletStoreModel> {
|
||||
return stores.first().getOrElse(userWalletId) { emptyList() }
|
||||
}
|
||||
|
||||
private val mutex = Mutex()
|
||||
suspend fun update(
|
||||
f: suspend (HashMap<UserWalletId, List<WalletStoreModel>>) -> HashMap<UserWalletId, List<WalletStoreModel>>,
|
||||
) {
|
||||
while (mutex.isLocked) {
|
||||
delay(timeMillis = 60)
|
||||
}
|
||||
|
||||
mutex.withLock {
|
||||
val prevState = stores.first()
|
||||
stores.emit(f(prevState))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -45,8 +45,20 @@ sealed class DetailsAction : Action {
|
|||
}
|
||||
|
||||
sealed class AppSettings : DetailsAction() {
|
||||
data class SwitchPrivacySetting(val enable: Boolean, val setting: PrivacySetting) :
|
||||
AppSettings()
|
||||
data class SwitchPrivacySetting(
|
||||
val enable: Boolean,
|
||||
val setting: PrivacySetting,
|
||||
) : AppSettings() {
|
||||
data class Success(
|
||||
val enable: Boolean,
|
||||
val setting: PrivacySetting,
|
||||
) : AppSettings()
|
||||
}
|
||||
|
||||
object EnrollBiometrics : AppSettings() {
|
||||
object Enroll : AppSettings()
|
||||
object Cancel : AppSettings()
|
||||
}
|
||||
}
|
||||
|
||||
data class ChangeAppCurrency(val fiatCurrency: FiatCurrency) : DetailsAction()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ package com.tangem.tap.features.details.redux
|
|||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.flatMap
|
||||
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
|
||||
import com.tangem.domain.common.util.userWalletId
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
|
|
@ -9,44 +12,53 @@ import com.tangem.tap.common.analytics.events.AnalyticsParam
|
|||
import com.tangem.tap.common.analytics.events.Settings
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.domain.model.builders.UserWalletBuilder
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
import com.tangem.tap.walletStoresManager
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
|
||||
class DetailsMiddleware {
|
||||
private val eraseWalletMiddleware = EraseWalletMiddleware()
|
||||
private val manageSecurityMiddleware = ManageSecurityMiddleware()
|
||||
private val managePrivacyMiddleware = ManagePrivacyMiddleware()
|
||||
val detailsMiddleware: Middleware<AppState> = { _, state ->
|
||||
val detailsMiddleware: Middleware<AppState> = { _, stateProvider ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
handleAction(state, action)
|
||||
if (!DemoHelper.tryHandle(stateProvider, action)) {
|
||||
val detailsState = stateProvider()?.detailsState
|
||||
if (detailsState != null) {
|
||||
handleAction(detailsState, action)
|
||||
}
|
||||
}
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAction(state: () -> AppState?, action: Action) {
|
||||
if (DemoHelper.tryHandle(state, action)) return
|
||||
|
||||
private fun handleAction(state: DetailsState, action: Action) {
|
||||
when (action) {
|
||||
is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action)
|
||||
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action)
|
||||
is DetailsAction.AppSettings -> managePrivacyMiddleware.handle(action)
|
||||
is DetailsAction.AppSettings -> managePrivacyMiddleware.handle(state, action)
|
||||
is DetailsAction.ShowDisclaimer -> {
|
||||
val uri = store.state.detailsState.cardTermsOfUseUrl
|
||||
if (uri != null) {
|
||||
|
|
@ -65,14 +77,17 @@ class DetailsMiddleware {
|
|||
}
|
||||
DetailsAction.ScanCard -> {
|
||||
scope.launch {
|
||||
when (val result = tangemSdkManager.scanCard()) {
|
||||
is CompletionResult.Success -> {
|
||||
val scannedCard = result.data
|
||||
tangemSdkManager.scanCard(
|
||||
cardId = state.scanResponse?.card?.cardId,
|
||||
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
|
||||
state.scanResponse?.card?.isAccessCodeSet == true,
|
||||
)
|
||||
.doOnSuccess { card ->
|
||||
val currentCardId = store.state.globalState.scanResponse?.card
|
||||
?.userWalletId
|
||||
?.stringValue
|
||||
if (scannedCard.userWalletId.stringValue == currentCardId) {
|
||||
store.dispatchOnMain(DetailsAction.PrepareCardSettingsData(scannedCard))
|
||||
if (card.userWalletId.stringValue == currentCardId) {
|
||||
store.dispatchOnMain(DetailsAction.PrepareCardSettingsData(card))
|
||||
} else {
|
||||
store.dispatchDialogShow(
|
||||
AppDialog.SimpleOkDialogRes(
|
||||
|
|
@ -82,8 +97,6 @@ class DetailsMiddleware {
|
|||
)
|
||||
}
|
||||
}
|
||||
is CompletionResult.Failure -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -104,17 +117,22 @@ class DetailsMiddleware {
|
|||
is DetailsAction.ResetToFactory.Proceed -> {
|
||||
val card = store.state.detailsState.cardSettingsState?.card ?: return
|
||||
scope.launch {
|
||||
when (val result = tangemSdkManager.resetToFactorySettings(card.cardId)) {
|
||||
is CompletionResult.Success -> {
|
||||
tangemSdkManager.resetToFactorySettings(card.cardId)
|
||||
.flatMap { userWalletsListManager.delete(listOf(card.userWalletId)) }
|
||||
.doOnSuccess {
|
||||
Analytics.send(Settings.CardSettings.FactoryResetFinished())
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
|
||||
val screen = if (userWalletsListManager.hasSavedUserWallets) {
|
||||
AppScreen.Welcome
|
||||
} else {
|
||||
AppScreen.Home
|
||||
}
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(screen))
|
||||
}
|
||||
|
||||
is CompletionResult.Failure -> {
|
||||
(result.error as? TangemSdkError)?.let { error ->
|
||||
Analytics.send(Settings.CardSettings.FactoryResetFinished(error)) }
|
||||
.doOnFailure { error ->
|
||||
(error as? TangemSdkError)?.let { sdkError ->
|
||||
Analytics.send(Settings.CardSettings.FactoryResetFinished(sdkError))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
|
|
@ -174,12 +192,109 @@ class DetailsMiddleware {
|
|||
}
|
||||
|
||||
class ManagePrivacyMiddleware {
|
||||
fun handle(action: DetailsAction.AppSettings) {
|
||||
fun handle(state: DetailsState, action: DetailsAction.AppSettings) {
|
||||
when (action) {
|
||||
is DetailsAction.AppSettings.SwitchPrivacySetting -> {
|
||||
// TODO()
|
||||
if (tangemSdkManager.canEnrollBiometrics) {
|
||||
store.dispatch(DetailsAction.AppSettings.EnrollBiometrics)
|
||||
}
|
||||
when (action.setting) {
|
||||
PrivacySetting.SaveWallets -> toggleSaveWallets(state, enable = action.enable)
|
||||
PrivacySetting.SaveAccessCode -> toggleSaveAccessCodes(state, enable = action.enable)
|
||||
}
|
||||
}
|
||||
is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> Unit
|
||||
is DetailsAction.AppSettings.EnrollBiometrics -> Unit
|
||||
is DetailsAction.AppSettings.EnrollBiometrics.Enroll -> enrollBiometrics()
|
||||
is DetailsAction.AppSettings.EnrollBiometrics.Cancel -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun enrollBiometrics() {
|
||||
store.dispatchOnMain(NavigationAction.OpenBiometricsSettings)
|
||||
}
|
||||
|
||||
private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch {
|
||||
if (state.saveWallets == enable) return@launch
|
||||
if (enable) {
|
||||
saveCurrentWallet()
|
||||
} else {
|
||||
deleteSavedWallets()
|
||||
if (state.saveAccessCodes) {
|
||||
deleteSavedAccessCodes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSaveAccessCodes(state: DetailsState, enable: Boolean) = scope.launch {
|
||||
if (state.saveAccessCodes == enable) return@launch
|
||||
if (enable) {
|
||||
if (!state.saveWallets) {
|
||||
saveCurrentWallet()
|
||||
}
|
||||
saveAccessCodes()
|
||||
} else {
|
||||
deleteSavedAccessCodes()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveCurrentWallet() {
|
||||
val scanResponse = store.state.detailsState.scanResponse ?: return
|
||||
val userWallet = UserWalletBuilder(scanResponse).build()
|
||||
|
||||
userWalletsListManager.save(userWallet)
|
||||
.doOnFailure { error ->
|
||||
Timber.e(error, "Wallet saving failed")
|
||||
}
|
||||
.doOnSuccess {
|
||||
preferencesStorage.shouldShowSaveWallet = false
|
||||
preferencesStorage.shouldSaveUserWallets = true
|
||||
store.dispatchOnMain(
|
||||
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
|
||||
setting = PrivacySetting.SaveWallets,
|
||||
enable = true,
|
||||
),
|
||||
)
|
||||
store.onUserWalletSelected(userWallet)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun deleteSavedWallets() {
|
||||
userWalletsListManager.clear()
|
||||
.flatMap { walletStoresManager.clear() }
|
||||
.doOnSuccess {
|
||||
preferencesStorage.shouldSaveUserWallets = false
|
||||
store.dispatchOnMain(
|
||||
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
|
||||
setting = PrivacySetting.SaveWallets,
|
||||
enable = false,
|
||||
),
|
||||
)
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAccessCodes() {
|
||||
preferencesStorage.shouldSaveAccessCodes = true
|
||||
store.dispatchOnMain(
|
||||
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
|
||||
setting = PrivacySetting.SaveAccessCode,
|
||||
enable = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun deleteSavedAccessCodes() {
|
||||
tangemSdkManager.clearSavedUserCodes()
|
||||
.doOnSuccess {
|
||||
preferencesStorage.shouldSaveAccessCodes = false
|
||||
store.dispatchOnMain(
|
||||
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
|
||||
setting = PrivacySetting.SaveAccessCode,
|
||||
enable = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,10 @@ import com.tangem.domain.common.isTangemTwin
|
|||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.domain.extensions.isWalletDataSupported
|
||||
import com.tangem.tap.domain.extensions.signedHashesCount
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
import org.rekotlin.Action
|
||||
import java.util.*
|
||||
|
||||
|
|
@ -53,6 +56,9 @@ private fun handlePrepareScreen(
|
|||
cardTermsOfUseUrl = action.cardTou.getUrl(action.scanResponse.card),
|
||||
createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup,
|
||||
appCurrency = store.state.globalState.appCurrency,
|
||||
isBiometricsAvailable = tangemSdkManager.canUseBiometry,
|
||||
saveWallets = userWalletsListManager.hasSavedUserWallets,
|
||||
saveAccessCodes = preferencesStorage.shouldSaveAccessCodes,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -151,12 +157,15 @@ private fun handlePrivacyAction(
|
|||
state: DetailsState,
|
||||
): DetailsState {
|
||||
return when (action) {
|
||||
is DetailsAction.AppSettings.SwitchPrivacySetting -> {
|
||||
when (action.setting) {
|
||||
PrivacySetting.SaveWallets -> state.copy(saveWallets = action.enable)
|
||||
PrivacySetting.SaveAccessCode -> state.copy(saveAccessCodes = action.enable)
|
||||
}
|
||||
is DetailsAction.AppSettings.SwitchPrivacySetting -> state
|
||||
is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> when (action.setting) {
|
||||
PrivacySetting.SaveWallets -> state.copy(saveWallets = action.enable)
|
||||
PrivacySetting.SaveAccessCode -> state.copy(saveAccessCodes = action.enable)
|
||||
}
|
||||
is DetailsAction.AppSettings.EnrollBiometrics -> state.copy(needEnrollBiometrics = true)
|
||||
is DetailsAction.AppSettings.EnrollBiometrics.Enroll,
|
||||
is DetailsAction.AppSettings.EnrollBiometrics.Cancel,
|
||||
-> state.copy(needEnrollBiometrics = false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ data class DetailsState(
|
|||
val appCurrency: FiatCurrency = FiatCurrency.Default,
|
||||
val saveWallets: Boolean = false,
|
||||
val saveAccessCodes: Boolean = false,
|
||||
val isBiometricsAvailable: Boolean = false,
|
||||
val needEnrollBiometrics: Boolean = false,
|
||||
) : StateType {
|
||||
|
||||
// if you do not delegate - the application crashes on startup,
|
||||
|
|
|
|||
|
|
@ -1,23 +1,12 @@
|
|||
package com.tangem.tap.features.details.ui.appsettings
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
|
|
@ -26,6 +15,8 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.core.ui.components.dialogs.EnrollBiometricsDialogContent
|
||||
import com.tangem.core.ui.models.EnrollBiometricsDialog
|
||||
import com.tangem.tap.common.compose.TangemTypography
|
||||
import com.tangem.tap.features.details.redux.PrivacySetting
|
||||
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
|
||||
|
|
@ -64,6 +55,8 @@ private fun AppSettings(
|
|||
)
|
||||
}
|
||||
|
||||
EnrollBiometricsDialog(dialog = state.enrollBiometricsDialog)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize(),
|
||||
|
|
@ -84,6 +77,15 @@ private fun AppSettings(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EnrollBiometricsDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
dialog: EnrollBiometricsDialog?,
|
||||
) {
|
||||
if (dialog == null) return
|
||||
EnrollBiometricsDialogContent(modifier, dialog)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppSettingsElement(
|
||||
state: AppSettingsScreenState,
|
||||
|
|
@ -224,6 +226,7 @@ fun AppSettingsScreenPreview() {
|
|||
PrivacySetting.SaveAccessCode to false,
|
||||
),
|
||||
onSettingToggled = { _, _ -> },
|
||||
enrollBiometricsDialog = null,
|
||||
),
|
||||
onBackPressed = { },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package com.tangem.tap.features.details.ui.appsettings
|
||||
|
||||
import com.tangem.core.ui.models.EnrollBiometricsDialog
|
||||
import com.tangem.tap.features.details.redux.PrivacySetting
|
||||
|
||||
data class AppSettingsScreenState(
|
||||
val settings: Map<PrivacySetting, Boolean>,
|
||||
val enrollBiometricsDialog: EnrollBiometricsDialog?,
|
||||
val onSettingToggled: (PrivacySetting, Boolean) -> Unit,
|
||||
)
|
||||
|
||||
|
||||
)
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.tap.features.details.ui.appsettings
|
||||
|
||||
import com.tangem.core.ui.models.EnrollBiometricsDialog
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.details.redux.DetailsAction
|
||||
import com.tangem.tap.features.details.redux.DetailsState
|
||||
|
|
@ -11,12 +13,14 @@ import org.rekotlin.Store
|
|||
class AppSettingsViewModel(private val store: Store<AppState>) {
|
||||
private val _uiState = MutableStateFlow(updateState(store.state.detailsState))
|
||||
val uiState: StateFlow<AppSettingsScreenState> = _uiState
|
||||
|
||||
fun updateState(state: DetailsState): AppSettingsScreenState {
|
||||
return AppSettingsScreenState(
|
||||
settings = mapOf(
|
||||
PrivacySetting.SaveWallets to state.saveWallets,
|
||||
PrivacySetting.SaveAccessCode to state.saveAccessCodes,
|
||||
),
|
||||
enrollBiometricsDialog = if (state.needEnrollBiometrics) createEnrollBiometricsDialog() else null,
|
||||
onSettingToggled = { privacySetting, enabled -> onSettingsToggled(privacySetting, enabled) },
|
||||
)
|
||||
}
|
||||
|
|
@ -24,4 +28,13 @@ class AppSettingsViewModel(private val store: Store<AppState>) {
|
|||
private fun onSettingsToggled(setting: PrivacySetting, enable: Boolean) {
|
||||
store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting))
|
||||
}
|
||||
|
||||
private fun createEnrollBiometricsDialog() = EnrollBiometricsDialog(
|
||||
onCancel = {
|
||||
store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics.Cancel)
|
||||
},
|
||||
onEnroll = {
|
||||
store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics.Enroll)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
package com.tangem.tap.features.details.ui.details
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Immutable
|
||||
data class DetailsScreenState(
|
||||
val elements: List<SettingsElement>,
|
||||
val tangemLinks: List<SocialNetworkLink>,
|
||||
|
|
@ -13,6 +15,7 @@ data class DetailsScreenState(
|
|||
val appNameRes: Int = R.string.app_name
|
||||
}
|
||||
|
||||
@Immutable
|
||||
enum class SettingsElement(
|
||||
val iconRes: Int,
|
||||
val titleRes: Int,
|
||||
|
|
@ -30,6 +33,7 @@ enum class SettingsElement(
|
|||
PrivacyPolicy(R.drawable.ic_lock, R.string.details_row_privacy_policy);
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class SocialNetworkLink(
|
||||
val network: SocialNetwork,
|
||||
val url: String,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class DetailsViewModel(private val store: Store<AppState>) {
|
|||
SettingsElement.PrivacyPolicy -> {
|
||||
if (state.privacyPolicyUrl != null) it else null
|
||||
}
|
||||
SettingsElement.AppSettings -> null // TODO: until we implement settings from this screen
|
||||
SettingsElement.AppSettings -> if (state.isBiometricsAvailable) it else null
|
||||
SettingsElement.AppCurrency -> if (state.scanResponse?.card?.isMultiwalletAllowed != true) it else null
|
||||
SettingsElement.TermsOfUse -> if (state.scanResponse?.card?.isStart2Coin == true) it else null
|
||||
else -> it
|
||||
|
|
@ -44,7 +44,7 @@ class DetailsViewModel(private val store: Store<AppState>) {
|
|||
}
|
||||
|
||||
return DetailsScreenState(
|
||||
settings,
|
||||
elements = settings,
|
||||
tangemLinks = getSocialLinks(),
|
||||
tangemVersion = getTangemAppVersion(),
|
||||
appCurrency = state.appCurrency.name,
|
||||
|
|
@ -80,7 +80,7 @@ class DetailsViewModel(private val store: Store<AppState>) {
|
|||
}
|
||||
SettingsElement.AppSettings -> {
|
||||
Analytics.send(Settings.ButtonAppSettings())
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings)) //TODO: To be available later
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings))
|
||||
}
|
||||
SettingsElement.LinkMoreCards -> {
|
||||
Analytics.send(Settings.ButtonCreateBackup())
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import org.rekotlin.StoreSubscriber
|
|||
class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
|
||||
|
||||
private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
|
||||
private var composeView: ComposeView? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
@ -38,20 +37,16 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
|
|||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
val context = container?.context ?: return null
|
||||
|
||||
): View {
|
||||
store.dispatch(BackupAction.CheckForUnfinishedBackup)
|
||||
|
||||
composeView = ComposeView(context).apply {
|
||||
return ComposeView(inflater.context).apply {
|
||||
setContent {
|
||||
AppCompatTheme {
|
||||
ScreenContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return composeView
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
|
@ -71,7 +66,6 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
|
|||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
rollbackStatusBarIconsColor()
|
||||
composeView = null
|
||||
}
|
||||
|
||||
override fun newState(state: HomeState) {
|
||||
|
|
|
|||
|
|
@ -1,51 +1,33 @@
|
|||
package com.tangem.tap.features.home.redux
|
||||
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.extensions.withIOContext
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.operations.backup.BackupService
|
||||
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
|
||||
import com.tangem.tap.backupService
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.events.IntroductionProcess
|
||||
import com.tangem.tap.common.analytics.paramsInterceptor.BatchIdParamsInterceptor
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnResult
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.tap.common.entities.IndeterminateProgressButton
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.dispatchOpenUrl
|
||||
import com.tangem.tap.common.extensions.onCardScanned
|
||||
import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.common.postUiDelayBg
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
|
||||
import com.tangem.tap.features.disclaimer.redux.DisclaimerType
|
||||
import com.tangem.tap.features.disclaimer.redux.isAccepted
|
||||
import com.tangem.tap.domain.model.builders.UserWalletBuilder
|
||||
import com.tangem.tap.domain.scanCard.ScanCardProcessor
|
||||
import com.tangem.tap.features.home.BELARUS_COUNTRY_CODE
|
||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.features.home.redux.HomeMiddleware.Companion.BUY_WALLET_URL
|
||||
import com.tangem.tap.features.onboarding.OnboardingHelper
|
||||
import com.tangem.tap.features.onboarding.OnboardingSaltPayHelper
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
|
||||
import com.tangem.tap.features.send.redux.states.ButtonState
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
|
||||
class HomeMiddleware {
|
||||
companion object {
|
||||
|
|
@ -71,34 +53,13 @@ private fun handleHomeAction(action: Action) {
|
|||
store.dispatch(GlobalAction.ExchangeManager.Init)
|
||||
store.dispatch(GlobalAction.FetchUserCountry)
|
||||
}
|
||||
|
||||
is HomeAction.ShouldScanCardOnResume -> {
|
||||
if (action.shouldScanCard) {
|
||||
store.dispatch(HomeAction.ShouldScanCardOnResume(false))
|
||||
postUiDelayBg(700) { store.dispatch(HomeAction.ReadCard) }
|
||||
}
|
||||
}
|
||||
is HomeAction.ReadCard -> {
|
||||
changeButtonState(ButtonState.PROGRESS)
|
||||
val scanCardAction = GlobalAction.ScanCard(
|
||||
onSuccess = { scanResponse ->
|
||||
store.dispatch(HomeAction.ScanInProgress(false))
|
||||
checkForUnfinishedBackupForSaltPay(
|
||||
backupService = backupService,
|
||||
scanResponse = scanResponse,
|
||||
nextHandler = {
|
||||
showDisclaimerIfNeed(
|
||||
scanResponse = scanResponse,
|
||||
nextHandler = ::onScanSuccess,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
onFailure = ::onScanFailure,
|
||||
)
|
||||
store.dispatch(HomeAction.ScanInProgress(true))
|
||||
postUiDelayBg(300) { store.dispatch(scanCardAction) }
|
||||
}
|
||||
is HomeAction.ReadCard -> readCard()
|
||||
is HomeAction.GoToShop -> {
|
||||
when (action.userCountryCode) {
|
||||
RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE -> store.dispatchOpenUrl(BUY_WALLET_URL)
|
||||
|
|
@ -108,134 +69,44 @@ private fun handleHomeAction(action: Action) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It checks only the SaltPay cards. To check for unfinished backups for the standard Wallet cards
|
||||
* see BackupAction.CheckForUnfinishedBackup
|
||||
* If user touches card other than Visa SaltPay - show dialog and block next processing
|
||||
*/
|
||||
private fun checkForUnfinishedBackupForSaltPay(
|
||||
backupService: BackupService,
|
||||
scanResponse: ScanResponse,
|
||||
nextHandler: (ScanResponse) -> Unit,
|
||||
) {
|
||||
if (!backupService.hasIncompletedBackup || !backupService.primaryCardIsSaltPayVisa()) {
|
||||
nextHandler(scanResponse)
|
||||
return
|
||||
}
|
||||
|
||||
fun isTheSamePrimaryCard(card: CardDTO): Boolean {
|
||||
return backupService.primaryCardId?.let { it == card.cardId } ?: false
|
||||
}
|
||||
|
||||
if (scanResponse.isSaltPayWallet() || !isTheSamePrimaryCard(scanResponse.card)) {
|
||||
changeButtonState(ButtonState.ENABLED)
|
||||
showSaltPayTapVisaLogoCardDialog()
|
||||
} else {
|
||||
nextHandler(scanResponse)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDisclaimerIfNeed(scanResponse: ScanResponse, nextHandler: (ScanResponse) -> Unit) {
|
||||
val disclaimerType = DisclaimerType.get(scanResponse)
|
||||
store.dispatch(DisclaimerAction.SetDisclaimerType(disclaimerType))
|
||||
|
||||
if (disclaimerType.isAccepted()) {
|
||||
nextHandler((scanResponse))
|
||||
} else {
|
||||
changeButtonState(ButtonState.ENABLED)
|
||||
store.dispatch(
|
||||
DisclaimerAction.Show {
|
||||
private fun readCard() = scope.launch {
|
||||
delay(timeMillis = 200)
|
||||
ScanCardProcessor.scan(
|
||||
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes,
|
||||
onProgressStateChange = { showProgress ->
|
||||
if (showProgress) {
|
||||
changeButtonState(ButtonState.PROGRESS)
|
||||
nextHandler(scanResponse)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onScanSuccess(scanResponse: ScanResponse) {
|
||||
Analytics.send(IntroductionProcess.CardWasScanned())
|
||||
val globalState = store.state.globalState
|
||||
val tapWalletManager = globalState.tapWalletManager
|
||||
tapWalletManager.updateConfigManager(scanResponse)
|
||||
|
||||
Analytics.addParamsInterceptor(BatchIdParamsInterceptor(scanResponse.card.batchId))
|
||||
|
||||
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
|
||||
|
||||
if (scanResponse.isSaltPay()) {
|
||||
if (scanResponse.isSaltPayVisa()) {
|
||||
} else {
|
||||
changeButtonState(ButtonState.ENABLED)
|
||||
}
|
||||
},
|
||||
onScanStateChange = { scanInProgress ->
|
||||
store.dispatch(HomeAction.ScanInProgress(scanInProgress))
|
||||
},
|
||||
onSuccess = { scanResponse ->
|
||||
scope.launch {
|
||||
val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
|
||||
val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse, manager)
|
||||
delay(500)
|
||||
withMainContext {
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
val isOnboardingCase = result.data
|
||||
if (isOnboardingCase) {
|
||||
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
|
||||
store.dispatch(OnboardingSaltPayAction.SetDependencies(manager, config))
|
||||
store.dispatch(OnboardingSaltPayAction.Update)
|
||||
navigateTo(AppScreen.OnboardingWallet)
|
||||
} else {
|
||||
navigateTo(AppScreen.Wallet)
|
||||
withIOContext { store.onCardScanned(scanResponse) }
|
||||
}
|
||||
if (preferencesStorage.shouldSaveUserWallets) {
|
||||
val userWallet = UserWalletBuilder(scanResponse).build()
|
||||
userWalletsListManager.save(userWallet)
|
||||
.doOnFailure { error ->
|
||||
Timber.e(error, "Unable to save user wallet")
|
||||
store.onCardScanned(scanResponse)
|
||||
}
|
||||
is Result.Failure -> {
|
||||
changeButtonState(ButtonState.ENABLED)
|
||||
SaltPayExceptionHandler.handle(result.error)
|
||||
.doOnSuccess {
|
||||
store.onUserWalletSelected(userWallet)
|
||||
}
|
||||
}
|
||||
.doOnResult {
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
|
||||
}
|
||||
} else {
|
||||
store.onCardScanned(scanResponse)
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (scanResponse.card.backupStatus?.isActive == false) {
|
||||
changeButtonState(ButtonState.ENABLED)
|
||||
showSaltPayTapVisaLogoCardDialog()
|
||||
} else {
|
||||
navigateTo(AppScreen.Wallet, null)
|
||||
scope.launch { store.onCardScanned(scanResponse) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
|
||||
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = true))
|
||||
val appScreen = OnboardingHelper.whereToNavigate(scanResponse)
|
||||
navigateTo(appScreen)
|
||||
} else {
|
||||
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
|
||||
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly))
|
||||
navigateTo(AppScreen.OnboardingTwins)
|
||||
} else {
|
||||
navigateTo(AppScreen.Wallet, null)
|
||||
}
|
||||
scope.launch { store.onCardScanned(scanResponse) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSaltPayTapVisaLogoCardDialog() {
|
||||
store.dispatchDialogShow(
|
||||
AppDialog.SimpleOkDialogRes(
|
||||
headerId = R.string.saltpay_error_empty_backup_title,
|
||||
messageId = R.string.saltpay_error_empty_backup_message,
|
||||
),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun onScanFailure(error: TangemError) {
|
||||
store.dispatch(HomeAction.ScanInProgress(false))
|
||||
changeButtonState(ButtonState.ENABLED)
|
||||
}
|
||||
|
||||
private fun changeButtonState(state: ButtonState) {
|
||||
store.dispatch(HomeAction.ChangeScanCardButtonState(IndeterminateProgressButton(state)))
|
||||
}
|
||||
|
||||
private fun navigateTo(screen: AppScreen, transition: FragmentShareTransition? = null) {
|
||||
postUiDelayBg(DELAY_SDK_DIALOG_CLOSE) {
|
||||
changeButtonState(ButtonState.ENABLED)
|
||||
store.dispatch(NavigationAction.NavigateTo(screen, transition))
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,18 @@ package com.tangem.tap.features.onboarding
|
|||
|
||||
import com.tangem.domain.common.ProductType
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.onCardScanned
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -39,5 +49,42 @@ class OnboardingHelper {
|
|||
ProductType.SaltPay -> AppScreen.OnboardingWallet
|
||||
}
|
||||
}
|
||||
|
||||
fun trySaveWalletAndNavigateToWalletScreen(
|
||||
scanResponse: ScanResponse,
|
||||
accessCode: String? = null,
|
||||
backupCardsIds: List<String>? = null,
|
||||
) {
|
||||
when {
|
||||
userWalletsListManager.hasSavedUserWallets -> scope.launch {
|
||||
delay(timeMillis = 1_200)
|
||||
store.dispatchOnMain(
|
||||
SaveWalletAction.ProvideBackupInfo(
|
||||
scanResponse = scanResponse,
|
||||
accessCode = accessCode,
|
||||
backupCardsIds = backupCardsIds?.toSet(),
|
||||
),
|
||||
)
|
||||
store.dispatchOnMain(SaveWalletAction.Save)
|
||||
}
|
||||
tangemSdkManager.canUseBiometry &&
|
||||
preferencesStorage.shouldShowSaveWallet -> scope.launch {
|
||||
delay(timeMillis = 1_200)
|
||||
store.dispatchOnMain(
|
||||
SaveWalletAction.ProvideBackupInfo(
|
||||
scanResponse = scanResponse,
|
||||
accessCode = accessCode,
|
||||
backupCardsIds = backupCardsIds?.toSet(),
|
||||
),
|
||||
)
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.SaveWallet))
|
||||
}
|
||||
else -> scope.launch {
|
||||
store.onCardScanned(scanResponse)
|
||||
}
|
||||
}
|
||||
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,17 +14,15 @@ import com.tangem.tap.common.extensions.dispatchOnMain
|
|||
import com.tangem.tap.common.extensions.dispatchOpenUrl
|
||||
import com.tangem.tap.common.extensions.getAddressData
|
||||
import com.tangem.tap.common.extensions.getTopUpUrl
|
||||
import com.tangem.tap.common.extensions.onCardScanned
|
||||
import com.tangem.tap.common.postUi
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.features.onboarding.OnboardingHelper
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.redux.ProgressState
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
|
|
@ -190,10 +188,7 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
|
|||
}
|
||||
OnboardingNoteAction.Done -> {
|
||||
store.dispatch(GlobalAction.Onboarding.Stop)
|
||||
scope.launch {
|
||||
store.onCardScanned(scanResponse)
|
||||
withMainContext { store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet)) }
|
||||
}
|
||||
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,13 +6,11 @@ import com.tangem.domain.common.extensions.withMainContext
|
|||
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.events.Onboarding
|
||||
import com.tangem.tap.common.extensions.onCardScanned
|
||||
import com.tangem.tap.common.postUi
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.onboarding.OnboardingHelper
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
|
|
@ -130,10 +128,7 @@ private fun handleOtherCardsAction(action: Action) {
|
|||
}
|
||||
OnboardingOtherCardsAction.Done -> {
|
||||
store.dispatch(GlobalAction.Onboarding.Stop)
|
||||
scope.launch {
|
||||
store.onCardScanned(onboardingManager.scanResponse)
|
||||
withMainContext { store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet)) }
|
||||
}
|
||||
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(onboardingManager.scanResponse)
|
||||
}
|
||||
is OnboardingOtherCardsAction.Confetti.Hide,
|
||||
is OnboardingOtherCardsAction.SetArtworkUrl,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ sealed class TwinCardsAction : Action {
|
|||
data class LaunchThirdStep(val message: Message) : TwinCardsAction()
|
||||
}
|
||||
|
||||
|
||||
// for the onboarding
|
||||
data class SetPairCardId(val cardId: String) : TwinCardsAction()
|
||||
object TopUp : TwinCardsAction()
|
||||
|
|
@ -53,6 +52,10 @@ sealed class TwinCardsAction : Action {
|
|||
data class SetWalletManager(val walletManager: WalletManager) : TwinCardsAction()
|
||||
object Done : TwinCardsAction()
|
||||
|
||||
data class SaveScannedTwinCardAndNavigateToWallet(
|
||||
val scanResponse: ScanResponse,
|
||||
) : TwinCardsAction()
|
||||
|
||||
sealed class Balance {
|
||||
object Update : TwinCardsAction()
|
||||
data class Set(val balance: OnboardingWalletBalance) : TwinCardsAction()
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ import com.tangem.tap.common.analytics.events.AnalyticsParam
|
|||
import com.tangem.tap.common.analytics.events.Onboarding
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.dispatchOpenUrl
|
||||
import com.tangem.tap.common.extensions.getAddressData
|
||||
import com.tangem.tap.common.extensions.getTopUpUrl
|
||||
import com.tangem.tap.common.extensions.onCardScanned
|
||||
import com.tangem.tap.common.postUi
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
|
|
@ -25,6 +25,7 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
|
|||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
|
||||
import com.tangem.tap.domain.twins.TwinCardsManager
|
||||
import com.tangem.tap.features.onboarding.OnboardingHelper
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.redux.ProgressState
|
||||
import com.tangem.tap.preferencesStorage
|
||||
|
|
@ -88,7 +89,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
|
|||
|
||||
when (action) {
|
||||
is TwinCardsAction.Init -> {
|
||||
if (twinCardsState.currentStep == TwinCardsStep.WelcomeOnly) return
|
||||
if (twinCardsState.currentStep is TwinCardsStep.WelcomeOnly) return
|
||||
|
||||
val scanResponse = getScanResponse()
|
||||
onboardingManager?.apply {
|
||||
|
|
@ -141,7 +142,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
|
|||
}
|
||||
is TwinCardsAction.SetStepOfScreen -> {
|
||||
when (action.step) {
|
||||
TwinCardsStep.WelcomeOnly, TwinCardsStep.Welcome -> {
|
||||
is TwinCardsStep.WelcomeOnly, TwinCardsStep.Welcome -> {
|
||||
Analytics.send(Onboarding.Twins.ScreenOpened())
|
||||
preferencesStorage.saveTwinsOnboardingShown()
|
||||
}
|
||||
|
|
@ -289,21 +290,24 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
|
|||
store.dispatchOpenUrl(topUpUrl)
|
||||
}
|
||||
TwinCardsAction.Done -> {
|
||||
scope.launch {
|
||||
val scanResponse = getScanResponse()
|
||||
store.onCardScanned(scanResponse)
|
||||
withMainContext {
|
||||
when (twinCardsState.mode) {
|
||||
CreateTwinWalletMode.CreateWallet -> {
|
||||
store.dispatch(GlobalAction.Onboarding.Stop)
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
|
||||
}
|
||||
CreateTwinWalletMode.RecreateWallet -> {
|
||||
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
|
||||
}
|
||||
}
|
||||
val scanResponse = getScanResponse()
|
||||
when (twinCardsState.mode) {
|
||||
CreateTwinWalletMode.CreateWallet -> {
|
||||
store.dispatchOnMain(GlobalAction.Onboarding.Stop)
|
||||
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(
|
||||
scanResponse = scanResponse,
|
||||
backupCardsIds = listOfNotNull(twinCardsState.twinCardsManager?.secondCardPublicKey),
|
||||
)
|
||||
}
|
||||
CreateTwinWalletMode.RecreateWallet -> {
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
|
||||
}
|
||||
}
|
||||
}
|
||||
is TwinCardsAction.SaveScannedTwinCardAndNavigateToWallet -> {
|
||||
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(
|
||||
scanResponse = action.scanResponse,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.features.onboarding.products.twins.redux
|
||||
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TwinCardNumber
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.twins.TwinCardsManager
|
||||
|
|
@ -62,9 +63,19 @@ data class TwinCardsState(
|
|||
|
||||
enum class CreateTwinWalletMode { CreateWallet, RecreateWallet }
|
||||
|
||||
enum class TwinCardsStep {
|
||||
None, WelcomeOnly, Welcome, Warning, CreateFirstWallet, CreateSecondWallet, CreateThirdWallet,
|
||||
sealed class TwinCardsStep {
|
||||
object None : TwinCardsStep()
|
||||
data class WelcomeOnly(
|
||||
val scanResponse: ScanResponse,
|
||||
) : TwinCardsStep()
|
||||
|
||||
object Welcome : TwinCardsStep()
|
||||
object Warning : TwinCardsStep()
|
||||
object CreateFirstWallet : TwinCardsStep()
|
||||
object CreateSecondWallet : TwinCardsStep()
|
||||
object CreateThirdWallet : TwinCardsStep()
|
||||
|
||||
// for the onboarding
|
||||
TopUpWallet, Done
|
||||
object TopUpWallet : TwinCardsStep()
|
||||
object Done : TwinCardsStep()
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import coil.load
|
|||
import com.tangem.Message
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TwinCardNumber
|
||||
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
|
||||
import com.tangem.tap.common.AndroidAssetReader
|
||||
|
|
@ -23,8 +24,6 @@ import com.tangem.tap.common.extensions.getDrawableCompat
|
|||
import com.tangem.tap.common.extensions.hide
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.common.redux.navigation.ShareElement
|
||||
import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
|
||||
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
|
||||
|
|
@ -43,7 +42,7 @@ import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding
|
|||
class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
|
||||
|
||||
private val mainBinding by lazy { binding.vMain }
|
||||
private var previousStep = TwinCardsStep.None
|
||||
private var previousStep: TwinCardsStep = TwinCardsStep.None
|
||||
|
||||
private lateinit var twinsWidget: TwinsCardWidget
|
||||
private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget
|
||||
|
|
@ -127,7 +126,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
|
|||
pbBinding.pbState.progress = state.progress
|
||||
|
||||
when (state.currentStep) {
|
||||
TwinCardsStep.WelcomeOnly -> setupWelcomeOnlyState(state)
|
||||
is TwinCardsStep.WelcomeOnly -> setupWelcomeOnlyState(state, state.currentStep.scanResponse)
|
||||
TwinCardsStep.Welcome -> setupWelcomeState(state)
|
||||
TwinCardsStep.Warning -> setupWarningState(state)
|
||||
TwinCardsStep.CreateFirstWallet -> setupCreateFirstWalletState(state)
|
||||
|
|
@ -157,9 +156,9 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun setupWelcomeOnlyState(state: TwinCardsState) {
|
||||
private fun setupWelcomeOnlyState(state: TwinCardsState, scanResponse: ScanResponse) {
|
||||
setupWelcomeState(state) {
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
|
||||
store.dispatch(TwinCardsAction.SaveScannedTwinCardAndNavigateToWallet(scanResponse))
|
||||
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.None))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
|
|||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.home.redux.HomeAction
|
||||
import com.tangem.tap.features.onboarding.OnboardingHelper
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
|
||||
|
|
@ -69,17 +70,14 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
|
|||
Analytics.send(Onboarding.Started())
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
card == null -> {
|
||||
// it's possible when found unfinished backup for standard Wallet cards
|
||||
store.dispatch(OnboardingWalletAction.ResumeBackup)
|
||||
}
|
||||
|
||||
card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup -> {
|
||||
store.dispatch(OnboardingWalletAction.ResumeBackup)
|
||||
}
|
||||
|
||||
card.wallets.isNotEmpty() && card.backupStatus?.isActive == true -> {
|
||||
when {
|
||||
// check for unfinished backup for saltPay cards. See more
|
||||
|
|
@ -162,8 +160,11 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
|
|||
} else {
|
||||
val backupState = store.state.onboardingWalletState.backupState
|
||||
val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState)
|
||||
scope.launch { globalState.tapWalletManager.onCardScanned(updatedScanResponse) }
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
|
||||
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(
|
||||
scanResponse = updatedScanResponse,
|
||||
accessCode = backupState.accessCode,
|
||||
backupCardsIds = backupState.backupCardIds,
|
||||
)
|
||||
}
|
||||
}
|
||||
is OnboardingWalletAction.ResumeBackup -> {
|
||||
|
|
@ -233,6 +234,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
|
||||
when (action) {
|
||||
is BackupAction.StartBackup -> {
|
||||
tangemSdkManager.setAccessCodeRequestPolicy(useBiometricsForAccessCode = false)
|
||||
Analytics.send(Onboarding.Backup.Started())
|
||||
backupService.discardSavedBackup()
|
||||
val primaryCard = scanResponse?.primaryCard
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import com.tangem.domain.common.ScanResponse
|
|||
import org.rekotlin.Action
|
||||
|
||||
internal sealed interface SaveWalletAction : Action {
|
||||
data class ProvideAdditionalInfo(
|
||||
data class ProvideBackupInfo(
|
||||
val scanResponse: ScanResponse,
|
||||
val accessCode: String?,
|
||||
val backupCardsIds: Set<String>?,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.tangem.common.flatMap
|
|||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.model.builders.UserWalletBuilder
|
||||
|
|
@ -34,10 +35,10 @@ internal class SaveWalletMiddleware {
|
|||
private fun handleAction(action: SaveWalletAction, state: SaveWalletState) {
|
||||
when (action) {
|
||||
is SaveWalletAction.Save -> saveWalletIfBiometricsEnrolled(state)
|
||||
is SaveWalletAction.Save.Success -> popBack()
|
||||
is SaveWalletAction.EnrollBiometrics.Enroll -> enrollBiometrics()
|
||||
is SaveWalletAction.SaveWalletWasShown -> saveWalletWasShown()
|
||||
is SaveWalletAction.ProvideAdditionalInfo,
|
||||
is SaveWalletAction.Save.Success,
|
||||
is SaveWalletAction.ProvideBackupInfo,
|
||||
is SaveWalletAction.Dismiss,
|
||||
is SaveWalletAction.CloseError,
|
||||
is SaveWalletAction.Save.Error,
|
||||
|
|
@ -60,52 +61,56 @@ internal class SaveWalletMiddleware {
|
|||
}
|
||||
|
||||
private fun saveWallet(state: SaveWalletState) {
|
||||
val scanResponse = state.additionalInfo?.scanResponse
|
||||
val scanResponse = state.backupInfo?.scanResponse
|
||||
?: store.state.globalState.scanResponse
|
||||
?: return
|
||||
|
||||
scope.launch {
|
||||
val userWallet = UserWalletBuilder(scanResponse)
|
||||
.setBackupCardsIds(backupCardsIds = state.additionalInfo?.backupCardsIds)
|
||||
.setBackupCardsIds(backupCardsIds = state.backupInfo?.backupCardsIds)
|
||||
.build()
|
||||
|
||||
userWalletsListManager.save(userWallet)
|
||||
.flatMap {
|
||||
trySaveAccessCode(
|
||||
userWallet = userWallet,
|
||||
additionalInfo = state.additionalInfo,
|
||||
)
|
||||
}
|
||||
saveAccessCodeIfNeeded(state.backupInfo?.accessCode, userWallet.cardsInWallet)
|
||||
.flatMap { userWalletsListManager.save(userWallet) }
|
||||
.doOnFailure { error ->
|
||||
store.dispatchOnMain(SaveWalletAction.Save.Error(error))
|
||||
}
|
||||
.doOnSuccess {
|
||||
preferencesStorage.shouldSaveUserWallets = true
|
||||
preferencesStorage.shouldSaveAccessCodes = true
|
||||
|
||||
val isSavedWalletSelected =
|
||||
userWalletsListManager.selectedUserWalletSync?.walletId == userWallet.walletId
|
||||
|
||||
if (isSavedWalletSelected) {
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
} else {
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.WalletSelector))
|
||||
}
|
||||
|
||||
store.dispatchOnMain(SaveWalletAction.Save.Success)
|
||||
store.onUserWalletSelected(userWallet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun popBack() {
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
}
|
||||
|
||||
private fun saveWalletWasShown() {
|
||||
preferencesStorage.shouldShowSaveWallet = false
|
||||
}
|
||||
|
||||
private suspend fun trySaveAccessCode(
|
||||
userWallet: UserWallet,
|
||||
additionalInfo: SaveWalletState.WalletAdditionalInfo?,
|
||||
private suspend fun saveAccessCodeIfNeeded(
|
||||
accessCode: String?,
|
||||
cardsInWallet: Set<String>,
|
||||
): CompletionResult<Unit> {
|
||||
return when {
|
||||
additionalInfo?.accessCode != null -> {
|
||||
tangemSdkManager.saveAccessCode(
|
||||
accessCode = additionalInfo.accessCode,
|
||||
cardsIds = userWallet.cardsInWallet,
|
||||
)
|
||||
accessCode != null -> {
|
||||
tangemSdkManager.unlockBiometricKeys()
|
||||
.flatMap {
|
||||
tangemSdkManager.saveAccessCode(
|
||||
accessCode = accessCode,
|
||||
cardsIds = cardsInWallet,
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
CompletionResult.Success(Unit)
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ internal object SaveWalletReducer {
|
|||
|
||||
private fun internalReduce(action: SaveWalletAction, state: SaveWalletState): SaveWalletState {
|
||||
return when (action) {
|
||||
is SaveWalletAction.ProvideAdditionalInfo -> state.copy(
|
||||
additionalInfo = SaveWalletState.WalletAdditionalInfo(
|
||||
is SaveWalletAction.ProvideBackupInfo -> state.copy(
|
||||
backupInfo = SaveWalletState.WalletBackupInfo(
|
||||
scanResponse = action.scanResponse,
|
||||
accessCode = action.accessCode,
|
||||
backupCardsIds = action.backupCardsIds,
|
||||
|
|
@ -27,11 +27,11 @@ internal object SaveWalletReducer {
|
|||
isSaveInProgress = false,
|
||||
)
|
||||
is SaveWalletAction.Save.Success -> state.copy(
|
||||
additionalInfo = null,
|
||||
backupInfo = null,
|
||||
isSaveInProgress = false,
|
||||
)
|
||||
is SaveWalletAction.Dismiss -> state.copy(
|
||||
additionalInfo = null,
|
||||
backupInfo = null,
|
||||
)
|
||||
is SaveWalletAction.CloseError -> state.copy(
|
||||
error = null,
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ import com.tangem.domain.common.ScanResponse
|
|||
import org.rekotlin.StateType
|
||||
|
||||
data class SaveWalletState(
|
||||
val additionalInfo: WalletAdditionalInfo? = null,
|
||||
val backupInfo: WalletBackupInfo? = null,
|
||||
val isSaveInProgress: Boolean = false,
|
||||
val needEnrollBiometrics: Boolean = false,
|
||||
val error: TangemError? = null,
|
||||
) : StateType {
|
||||
data class WalletAdditionalInfo(
|
||||
data class WalletBackupInfo(
|
||||
val scanResponse: ScanResponse,
|
||||
val accessCode: String?,
|
||||
val backupCardsIds: Set<String>?,
|
||||
|
|
|
|||
|
|
@ -6,13 +6,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.SnackbarHost
|
||||
import androidx.compose.material.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.fragment.app.viewModels
|
||||
|
|
|
|||
|
|
@ -5,26 +5,18 @@ import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras
|
|||
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
|
||||
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.common.TransactionError
|
||||
import com.tangem.blockchain.common.TransactionSender
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Token
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.safeUpdate
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
|
|
@ -36,27 +28,17 @@ import com.tangem.tap.domain.extensions.minimalAmount
|
|||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.demo.DemoTransactionSender
|
||||
import com.tangem.tap.features.demo.isDemoCard
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
|
||||
import com.tangem.tap.features.send.redux.AmountAction
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi
|
||||
import com.tangem.tap.features.send.redux.*
|
||||
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
|
||||
import com.tangem.tap.features.send.redux.PrepareSendScreen
|
||||
import com.tangem.tap.features.send.redux.SendAction
|
||||
import com.tangem.tap.features.send.redux.SendActionUi
|
||||
import com.tangem.tap.features.send.redux.states.ButtonState
|
||||
import com.tangem.tap.features.send.redux.states.ExternalTransactionData
|
||||
import com.tangem.tap.features.send.redux.states.MainCurrencyType
|
||||
import com.tangem.tap.features.send.redux.states.TransactionExtrasState
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdk
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Middleware
|
||||
import java.util.*
|
||||
|
|
@ -231,13 +213,9 @@ private fun sendTransaction(
|
|||
dispatch(NavigationAction.PopBackTo())
|
||||
}
|
||||
scope.launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.Main) {
|
||||
dispatch(WalletAction.LoadWallet(BlockchainNetwork.fromWalletManager(walletManager)))
|
||||
}
|
||||
updateWallet(walletManager)
|
||||
delay(11000) // more than 10000 to avoid throttling
|
||||
withContext(Dispatchers.Main) {
|
||||
dispatch(WalletAction.LoadWallet(BlockchainNetwork.fromWalletManager(walletManager)))
|
||||
}
|
||||
updateWallet(walletManager)
|
||||
}
|
||||
}
|
||||
is SimpleResult.Failure -> {
|
||||
|
|
@ -327,4 +305,17 @@ private fun updateWarnings(dispatch: (Action) -> Unit) {
|
|||
|
||||
val warnings = warningsManager.getWarnings(WarningMessage.Location.SendScreen, listOf(blockchain))
|
||||
dispatch(SendAction.Warnings.Set(warnings))
|
||||
}
|
||||
|
||||
private suspend fun updateWallet(walletManager: WalletManager) {
|
||||
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
|
||||
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
|
||||
if (selectedUserWallet != null) {
|
||||
walletCurrenciesManager.update(
|
||||
userWallet = selectedUserWallet,
|
||||
blockchainNetwork = blockchainNetwork,
|
||||
)
|
||||
} else {
|
||||
store.dispatchOnMain(WalletAction.LoadWallet(BlockchainNetwork.fromWalletManager(walletManager)))
|
||||
}
|
||||
}
|
||||
|
|
@ -30,25 +30,10 @@ import com.tangem.tap.common.toggleWidget.IndeterminateProgressButtonWidget
|
|||
import com.tangem.tap.common.toggleWidget.ViewStateWidget
|
||||
import com.tangem.tap.features.BaseStoreFragment
|
||||
import com.tangem.tap.features.addBackPressHandler
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.CheckClipboard
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.PasteAddressPayId
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.SetTruncateHandler
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.TruncateOrRestore
|
||||
import com.tangem.tap.features.send.redux.AmountAction
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi.CheckAmountToSend
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi.SetMainCurrency
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi.SetMaxAmount
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi.ToggleMainCurrency
|
||||
import com.tangem.tap.features.send.redux.FeeActionUi.ChangeIncludeFee
|
||||
import com.tangem.tap.features.send.redux.FeeActionUi.ChangeSelectedFee
|
||||
import com.tangem.tap.features.send.redux.FeeActionUi.ToggleControlsVisibility
|
||||
import com.tangem.tap.features.send.redux.ReceiptAction
|
||||
import com.tangem.tap.features.send.redux.ReleaseSendState
|
||||
import com.tangem.tap.features.send.redux.SendAction
|
||||
import com.tangem.tap.features.send.redux.SendActionUi
|
||||
import com.tangem.tap.features.send.redux.TransactionExtrasAction
|
||||
import com.tangem.tap.features.send.redux.*
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.*
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi.*
|
||||
import com.tangem.tap.features.send.redux.FeeActionUi.*
|
||||
import com.tangem.tap.features.send.redux.states.FeeType
|
||||
import com.tangem.tap.features.send.redux.states.MainCurrencyType
|
||||
import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber
|
||||
|
|
@ -60,12 +45,7 @@ import com.tangem.wallet.R
|
|||
import com.tangem.wallet.databinding.FragmentSendBinding
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.text.DecimalFormatSymbols
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.tangem.common.CompletionResult
|
|||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.common.flatMap
|
||||
import com.tangem.common.hdWallet.DerivationPath
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.DomainWrapped
|
||||
|
|
@ -18,8 +19,7 @@ import com.tangem.domain.features.addCustomToken.CustomCurrency
|
|||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
|
||||
import com.tangem.tap.assetReader
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.events.ManageTokens
|
||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||
|
|
@ -34,9 +34,6 @@ import com.tangem.tap.domain.tokens.LoadAvailableCoinsService
|
|||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Middleware
|
||||
|
|
@ -49,14 +46,12 @@ class TokensMiddleware {
|
|||
when (action) {
|
||||
is TokensAction.LoadCurrencies -> handleLoadCurrencies(action.scanResponse)
|
||||
is TokensAction.SaveChanges -> handleSaveChanges(action)
|
||||
is TokensAction.PrepareAndNavigateToAddCustomToken -> {
|
||||
handleAddingCustomToken(action)
|
||||
}
|
||||
is TokensAction.PrepareAndNavigateToAddCustomToken -> handleAddingCustomToken()
|
||||
is TokensAction.SetSearchInput -> {
|
||||
Analytics.send(ManageTokens.TokenSearched())
|
||||
handleLoadCurrencies(
|
||||
scanResponse = store.state.globalState.scanResponse,
|
||||
newSearchInput = action.searchInput
|
||||
newSearchInput = action.searchInput,
|
||||
)
|
||||
}
|
||||
is TokensAction.LoadMore -> {
|
||||
|
|
@ -106,8 +101,8 @@ class TokensMiddleware {
|
|||
.filter(supportedBlockchains.toSet())
|
||||
store.dispatchOnMain(
|
||||
TokensAction.LoadCurrencies.Success(
|
||||
currencies, loadCoinsResult.data.moreAvailable
|
||||
)
|
||||
currencies, loadCoinsResult.data.moreAvailable,
|
||||
),
|
||||
)
|
||||
}
|
||||
is Result.Failure -> store.dispatchOnMain(TokensAction.LoadCurrencies.Failure)
|
||||
|
|
@ -116,14 +111,14 @@ class TokensMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleSaveChanges(action: TokensAction.SaveChanges) {
|
||||
val scanResponse = store.state.globalState.scanResponse ?: return
|
||||
private fun handleSaveChanges(action: TokensAction.SaveChanges) = scope.launch {
|
||||
val scanResponse = store.state.globalState.scanResponse ?: return@launch
|
||||
|
||||
val currentTokens = store.state.tokensState.addedWallets.toNonCustomTokensWithBlockchains(
|
||||
scanResponse.card.derivationStyle
|
||||
scanResponse.card.derivationStyle,
|
||||
)
|
||||
val currentBlockchains = store.state.tokensState.addedWallets.toNonCustomBlockchains(
|
||||
scanResponse.card.derivationStyle
|
||||
scanResponse.card.derivationStyle,
|
||||
)
|
||||
|
||||
val blockchainsToAdd = action.addedBlockchains.filter { !currentBlockchains.contains(it) }
|
||||
|
|
@ -149,7 +144,7 @@ class TokensMiddleware {
|
|||
) {
|
||||
store.dispatchDebugErrorNotification("Nothing to save")
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
return
|
||||
return@launch
|
||||
}
|
||||
|
||||
val currencyList = convertToCurrencies(
|
||||
|
|
@ -201,8 +196,9 @@ class TokensMiddleware {
|
|||
|
||||
scope.launch {
|
||||
val result = tangemSdkManager.derivePublicKeys(
|
||||
scanResponse.card.cardId,
|
||||
derivations
|
||||
cardId = scanResponse.card.cardId,
|
||||
derivations = derivations,
|
||||
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes,
|
||||
)
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
|
|
@ -221,6 +217,7 @@ class TokensMiddleware {
|
|||
)
|
||||
store.dispatchOnMain(GlobalAction.SaveScanNoteResponse(updatedScanResponse))
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
|
||||
onSuccess(updatedScanResponse)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
|
|
@ -275,50 +272,78 @@ class TokensMiddleware {
|
|||
scanResponse: ScanResponse,
|
||||
currencyList: List<Currency>,
|
||||
) {
|
||||
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
|
||||
val derivationStyle = scanResponse.card.derivationStyle
|
||||
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
|
||||
if (selectedUserWallet != null) {
|
||||
val updatedUserWallet = selectedUserWallet.copy(
|
||||
scanResponse = scanResponse,
|
||||
)
|
||||
|
||||
val addActions = currencyList.mapIndexedNotNull { index, currency ->
|
||||
when (currency) {
|
||||
is Currency.Blockchain -> {
|
||||
val derivationPath = currency.derivationPath?.let { DerivationPath(it) }
|
||||
|
||||
val derivationParams = derivationStyle?.let {
|
||||
when (derivationPath) {
|
||||
null -> DerivationParams.Default(derivationStyle)
|
||||
else -> DerivationParams.Custom(derivationPath)
|
||||
}
|
||||
scope.launch {
|
||||
userWalletsListManager.update(updatedUserWallet)
|
||||
.flatMap {
|
||||
walletCurrenciesManager.addCurrencies(
|
||||
userWallet = updatedUserWallet,
|
||||
currenciesToAdd = currencyList,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
|
||||
val derivationStyle = scanResponse.card.derivationStyle
|
||||
|
||||
val addActions = currencyList.mapIndexedNotNull { index, currency ->
|
||||
when (currency) {
|
||||
is Currency.Blockchain -> {
|
||||
val derivationPath = currency.derivationPath?.let { DerivationPath(it) }
|
||||
|
||||
val derivationParams = derivationStyle?.let {
|
||||
when (derivationPath) {
|
||||
null -> DerivationParams.Default(derivationStyle)
|
||||
else -> DerivationParams.Custom(derivationPath)
|
||||
}
|
||||
}
|
||||
val walletManager = factory.makeWalletManagerForApp(
|
||||
scanResponse = scanResponse,
|
||||
blockchain = currency.blockchain,
|
||||
derivationParams = derivationParams,
|
||||
) ?: return@mapIndexedNotNull null
|
||||
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
|
||||
WalletAction.MultiWallet.AddBlockchain(
|
||||
blockchain = blockchainNetwork,
|
||||
walletManager = walletManager,
|
||||
save = index == currencyList.lastIndex,
|
||||
)
|
||||
}
|
||||
is Currency.Token -> {
|
||||
val rawDerivationPath = currency.derivationPath
|
||||
?: currency.blockchain.derivationPath(derivationStyle)?.rawPath
|
||||
val blockchainNetwork =
|
||||
BlockchainNetwork(currency.blockchain, rawDerivationPath, listOf(currency.token))
|
||||
WalletAction.MultiWallet.AddToken(
|
||||
token = currency.token,
|
||||
blockchain = blockchainNetwork,
|
||||
save = index == currencyList.lastIndex,
|
||||
)
|
||||
}
|
||||
val walletManager = factory.makeWalletManagerForApp(
|
||||
scanResponse = scanResponse,
|
||||
blockchain = currency.blockchain,
|
||||
derivationParams = derivationParams,
|
||||
) ?: return@mapIndexedNotNull null
|
||||
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
|
||||
WalletAction.MultiWallet.AddBlockchain(
|
||||
blockchain = blockchainNetwork,
|
||||
walletManager = walletManager,
|
||||
save = index == currencyList.lastIndex,
|
||||
)
|
||||
}
|
||||
is Currency.Token -> {
|
||||
val rawDerivationPath = currency.derivationPath
|
||||
?: currency.blockchain.derivationPath(derivationStyle)?.rawPath
|
||||
val blockchainNetwork =
|
||||
BlockchainNetwork(currency.blockchain, rawDerivationPath, listOf(currency.token))
|
||||
WalletAction.MultiWallet.AddToken(
|
||||
token = currency.token,
|
||||
blockchain = blockchainNetwork,
|
||||
save = index == currencyList.lastIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
addActions.forEach { store.dispatchOnMain(it) }
|
||||
}
|
||||
addActions.forEach { store.dispatchOnMain(it) }
|
||||
}
|
||||
|
||||
private fun removeCurrenciesIfNeeded(currencies: List<Currency>) {
|
||||
if (currencies.isNotEmpty()) store.dispatch(WalletAction.MultiWallet.RemoveWallets(currencies))
|
||||
private suspend fun removeCurrenciesIfNeeded(currencies: List<Currency>) {
|
||||
when {
|
||||
currencies.isEmpty() -> Unit
|
||||
userWalletsListManager.hasSavedUserWallets -> {
|
||||
walletCurrenciesManager.removeCurrencies(
|
||||
userWallet = userWalletsListManager.selectedUserWalletSync!!,
|
||||
currenciesToRemove = currencies,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
store.dispatch(WalletAction.MultiWallet.RemoveWallets(currencies))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isNeedToDerive(scanResponse: ScanResponse, currency: Currency): Boolean {
|
||||
|
|
@ -327,7 +352,7 @@ class TokensMiddleware {
|
|||
} ?: false
|
||||
}
|
||||
|
||||
private fun handleAddingCustomToken(action: TokensAction.PrepareAndNavigateToAddCustomToken) {
|
||||
private fun handleAddingCustomToken() = scope.launch {
|
||||
val onAddCustomToken = fun(customCurrency: CustomCurrency) {
|
||||
val scanResponse = store.state.globalState.scanResponse ?: return
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
package com.tangem.tap.features.wallet.redux
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.common.redux.NotificationAction
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||
import com.tangem.tap.domain.model.TotalFiatBalance
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.redux.models.WalletDialog
|
||||
|
|
@ -27,7 +26,8 @@ sealed class WalletAction : Action {
|
|||
|
||||
object LoadData : WalletAction() {
|
||||
object Refresh : WalletAction()
|
||||
data class Failure(val error: TapError) : WalletAction()
|
||||
object Success : WalletAction()
|
||||
data class Failure(val error: TapError?) : WalletAction()
|
||||
}
|
||||
|
||||
data class LoadWallet(
|
||||
|
|
@ -54,20 +54,28 @@ sealed class WalletAction : Action {
|
|||
sealed class MultiWallet : WalletAction() {
|
||||
data class SetIsMultiwalletAllowed(val isMultiwalletAllowed: Boolean) : MultiWallet()
|
||||
|
||||
data class AddBlockchains(
|
||||
val blockchains: List<BlockchainNetwork>,
|
||||
val walletManagers: List<WalletManager>,
|
||||
) : MultiWallet()
|
||||
|
||||
data class AddTokens(
|
||||
val tokens: List<Token>,
|
||||
val blockchain: BlockchainNetwork,
|
||||
) : MultiWallet()
|
||||
|
||||
data class AddBlockchain(
|
||||
val blockchain: BlockchainNetwork,
|
||||
val walletManager: WalletManager?,
|
||||
val save: Boolean,
|
||||
) : MultiWallet()
|
||||
|
||||
data class AddBlockchains(
|
||||
val blockchains: List<BlockchainNetwork>, val walletManagers: List<WalletManager>, val save: Boolean,
|
||||
data class AddToken(
|
||||
val token: Token,
|
||||
val blockchain: BlockchainNetwork,
|
||||
val save: Boolean,
|
||||
) : MultiWallet()
|
||||
|
||||
data class AddTokens(val tokens: List<Token>, val blockchain: BlockchainNetwork, val save: Boolean) :
|
||||
MultiWallet()
|
||||
|
||||
data class AddToken(val token: Token, val blockchain: BlockchainNetwork, val save: Boolean) : MultiWallet()
|
||||
data class SaveCurrencies(
|
||||
val blockchainNetworks: List<BlockchainNetwork>, val card: CardDTO? = null,
|
||||
) : MultiWallet()
|
||||
|
|
@ -161,6 +169,8 @@ sealed class WalletAction : Action {
|
|||
|
||||
object CreateWallet : WalletAction()
|
||||
object EmptyWallet : WalletAction()
|
||||
object ChangeWallet : WalletAction()
|
||||
object ShowSaveWalletIfNeeded : WalletAction()
|
||||
|
||||
sealed class TradeCryptoAction : WalletAction() {
|
||||
object Sell : TradeCryptoAction()
|
||||
|
|
@ -190,4 +200,8 @@ sealed class WalletAction : Action {
|
|||
object ChooseAppCurrency : AppCurrencyAction()
|
||||
data class SelectAppCurrency(val fiatCurrency: FiatCurrency) : AppCurrencyAction()
|
||||
}
|
||||
|
||||
data class UserWalletChanged(val userWallet: UserWallet) : WalletAction()
|
||||
data class WalletStoresChanged(val walletStores: List<WalletStoreModel>) : WalletAction()
|
||||
data class TotalFiatBalanceChanged(val balance: TotalFiatBalance) : WalletAction()
|
||||
}
|
||||
|
|
@ -14,16 +14,13 @@ import com.tangem.tap.common.toggleWidget.WidgetState
|
|||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.models.PendingTransaction
|
||||
import com.tangem.tap.features.wallet.models.TotalBalance
|
||||
import com.tangem.tap.features.wallet.models.WalletRent
|
||||
import com.tangem.tap.features.wallet.models.WalletWarning
|
||||
import com.tangem.tap.features.wallet.models.*
|
||||
import com.tangem.tap.features.wallet.redux.reducers.calculateTotalFiatAmount
|
||||
import com.tangem.tap.features.wallet.redux.reducers.findProgressState
|
||||
import com.tangem.tap.features.wallet.ui.BalanceStatus
|
||||
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
import org.rekotlin.StateType
|
||||
import java.math.BigDecimal
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
|
|
@ -80,6 +77,9 @@ data class WalletState(
|
|||
primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard &&
|
||||
primaryWallet?.currencyData?.status != BalanceStatus.UnknownBlockchain
|
||||
|
||||
val hasSavedWallets: Boolean
|
||||
get() = userWalletsListManager.hasSavedUserWallets
|
||||
|
||||
fun getWalletManager(currency: Currency?): WalletManager? {
|
||||
if (currency?.blockchain == null) return null
|
||||
return getWalletStore(currency)?.walletManager
|
||||
|
|
@ -242,11 +242,11 @@ data class WalletState(
|
|||
totalBalance = TotalBalance(
|
||||
state = walletsData.findProgressState(),
|
||||
fiatAmount = walletsData.calculateTotalFiatAmount(),
|
||||
fiatCurrency = store.state.globalState.appCurrency
|
||||
)
|
||||
fiatCurrency = store.state.globalState.appCurrency,
|
||||
),
|
||||
)
|
||||
} else this.copy(
|
||||
totalBalance = null
|
||||
totalBalance = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ import com.tangem.tap.domain.TapWalletManager
|
|||
import com.tangem.tap.features.details.redux.DetailsAction
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.features.wallet.redux.models.WalletDialog
|
||||
import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction
|
||||
import com.tangem.tap.persistence.FiatCurrenciesPrefStorage
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AppCurrencyMiddleware(
|
||||
|
|
@ -65,11 +67,19 @@ class AppCurrencyMiddleware(
|
|||
|
||||
private fun selectCurrency(action: WalletAction.AppCurrencyAction.SelectAppCurrency) {
|
||||
Analytics.send(MainScreen.MainCurrencyChanged(AnalyticsParam.CurrencyType.FiatCurrency(action.fiatCurrency)))
|
||||
tapWalletManager.rates.clear()
|
||||
fiatCurrenciesPrefStorage.saveAppCurrency(action.fiatCurrency)
|
||||
store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrency))
|
||||
store.dispatch(DetailsAction.ChangeAppCurrency(action.fiatCurrency))
|
||||
store.dispatch(WalletAction.LoadFiatRate())
|
||||
store.dispatch(WalletSelectorAction.ChangeAppCurrency(action.fiatCurrency))
|
||||
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
|
||||
if (selectedUserWallet != null) {
|
||||
scope.launch {
|
||||
tapWalletManager.loadData(selectedUserWallet, refresh = true)
|
||||
}
|
||||
} else {
|
||||
tapWalletManager.rates.clear()
|
||||
store.dispatch(WalletAction.LoadFiatRate())
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<CurrenciesResponse.Currency>.mapToUiModel(): List<FiatCurrency> {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ package com.tangem.tap.features.wallet.redux.middlewares
|
|||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Token.ButtonRemoveToken
|
||||
|
|
@ -17,6 +19,8 @@ import com.tangem.tap.common.redux.navigation.AppScreen
|
|||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.scanCard.ScanCardProcessor
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.demo.isDemoCard
|
||||
|
|
@ -26,9 +30,6 @@ import com.tangem.tap.features.wallet.redux.WalletAction
|
|||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
import com.tangem.tap.features.wallet.redux.models.WalletDialog
|
||||
import com.tangem.tap.features.wallet.redux.reducers.toWallet
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.userTokensRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -53,7 +54,7 @@ class MultiWalletMiddleware {
|
|||
addTokens(listOf(action.token), action.blockchain, walletState, globalState, action.save)
|
||||
}
|
||||
is WalletAction.MultiWallet.AddTokens -> {
|
||||
addTokens(action.tokens, action.blockchain, walletState, globalState, action.save)
|
||||
addTokens(action.tokens, action.blockchain, walletState, globalState, save = false)
|
||||
}
|
||||
is WalletAction.MultiWallet.AddBlockchain -> {
|
||||
action.walletManager?.let {
|
||||
|
|
@ -121,19 +122,27 @@ class MultiWalletMiddleware {
|
|||
}
|
||||
}
|
||||
is WalletAction.MultiWallet.RemoveWallet -> {
|
||||
val currency = action.currency
|
||||
val card = globalState.scanResponse?.card.guard {
|
||||
store.dispatchErrorNotification(TapError.UnsupportedState("card is NULL"))
|
||||
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
|
||||
return
|
||||
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
|
||||
if (selectedUserWallet != null) scope.launch {
|
||||
walletCurrenciesManager.removeCurrency(
|
||||
userWallet = selectedUserWallet,
|
||||
currencyToRemove = action.currency,
|
||||
)
|
||||
} else {
|
||||
val currency = action.currency
|
||||
val card = globalState.scanResponse?.card.guard {
|
||||
store.dispatchErrorNotification(TapError.UnsupportedState("card is NULL"))
|
||||
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
|
||||
return
|
||||
}
|
||||
var currencies = walletState?.currencies ?: emptyList()
|
||||
currencies = currencies.filterNot { it == currency }
|
||||
if (currency.isBlockchain()) {
|
||||
currencies
|
||||
.filter { it.blockchain == currency.blockchain && it.derivationPath == currency.derivationPath }
|
||||
}
|
||||
scope.launch { userTokensRepository.saveUserTokens(card, currencies) }
|
||||
}
|
||||
var currencies = walletState?.currencies ?: emptyList()
|
||||
currencies = currencies.filterNot { it == currency }
|
||||
if (currency.isBlockchain()) {
|
||||
currencies
|
||||
.filter { it.blockchain == currency.blockchain && it.derivationPath == currency.derivationPath }
|
||||
}
|
||||
scope.launch { userTokensRepository.saveUserTokens(card, currencies) }
|
||||
}
|
||||
is WalletAction.MultiWallet.RemoveWallets -> {
|
||||
val card = globalState.scanResponse?.card.guard {
|
||||
|
|
@ -154,11 +163,36 @@ class MultiWalletMiddleware {
|
|||
}
|
||||
}
|
||||
is WalletAction.MultiWallet.ScanToGetDerivations -> {
|
||||
store.dispatch(WalletAction.Scan)
|
||||
val selectedWallet = userWalletsListManager.selectedUserWalletSync
|
||||
if (selectedWallet != null) {
|
||||
scanAndUpdateCard(selectedWallet, walletState)
|
||||
} else {
|
||||
store.dispatch(WalletAction.Scan)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun scanAndUpdateCard(
|
||||
selectedWallet: UserWallet,
|
||||
state: WalletState?,
|
||||
) = scope.launch {
|
||||
ScanCardProcessor.scan(
|
||||
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes,
|
||||
cardId = selectedWallet.cardId,
|
||||
additionalBlockchainsToDerive = state?.missingDerivations?.map { it.blockchain },
|
||||
) { scanResponse ->
|
||||
val userWallet = selectedWallet.copy(
|
||||
scanResponse = scanResponse,
|
||||
)
|
||||
|
||||
userWalletsListManager.update(userWallet)
|
||||
.doOnSuccess {
|
||||
store.state.globalState.tapWalletManager.loadData(userWallet, refresh = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addDummyBalances(walletManagers: List<WalletManager>) {
|
||||
walletManagers.forEach {
|
||||
if (it.wallet.fundsAvailable(AmountType.Coin) == BigDecimal.ZERO) {
|
||||
|
|
|
|||
|
|
@ -11,17 +11,10 @@ import com.tangem.common.services.Result
|
|||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.operations.attestation.Attestation
|
||||
import com.tangem.operations.attestation.OnlineCardVerifier
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.events.Token
|
||||
import com.tangem.tap.common.extensions.copyToClipboard
|
||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.dispatchOpenUrl
|
||||
import com.tangem.tap.common.extensions.dispatchToastNotification
|
||||
import com.tangem.tap.common.extensions.onCardScanned
|
||||
import com.tangem.tap.common.extensions.shareText
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
|
|
@ -29,25 +22,20 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
|
|||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.failedRates
|
||||
import com.tangem.tap.domain.loadedRates
|
||||
import com.tangem.tap.domain.model.WalletDataModel
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.home.redux.HomeAction
|
||||
import com.tangem.tap.features.send.redux.PrepareSendScreen
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.models.PendingTransactionType
|
||||
import com.tangem.tap.features.wallet.models.filterByCoin
|
||||
import com.tangem.tap.features.wallet.models.getPendingTransactions
|
||||
import com.tangem.tap.features.wallet.models.getSendableAmounts
|
||||
import com.tangem.tap.features.wallet.models.*
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.features.wallet.redux.WalletData
|
||||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
import com.tangem.tap.features.wallet.redux.WalletStore
|
||||
import com.tangem.tap.network.NetworkConnectivity
|
||||
import com.tangem.tap.network.NetworkStateChanged
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -219,20 +207,34 @@ class WalletMiddleware {
|
|||
}
|
||||
}
|
||||
is WalletAction.LoadData,
|
||||
is WalletAction.LoadData.Refresh -> {
|
||||
is WalletAction.LoadData.Refresh,
|
||||
-> {
|
||||
val selectedWallet = userWalletsListManager.selectedUserWalletSync
|
||||
scope.launch {
|
||||
val scanNoteResponse = globalState.scanResponse ?: return@launch
|
||||
if (walletState.walletsData.isNotEmpty()) {
|
||||
globalState.tapWalletManager.reloadData(scanNoteResponse)
|
||||
if (selectedWallet != null) {
|
||||
globalState.tapWalletManager.loadData(
|
||||
userWallet = selectedWallet,
|
||||
refresh = action is WalletAction.LoadData.Refresh,
|
||||
)
|
||||
} else {
|
||||
globalState.tapWalletManager.loadData(scanNoteResponse)
|
||||
val scanNoteResponse = globalState.scanResponse ?: return@launch
|
||||
if (walletState.walletsData.isNotEmpty()) {
|
||||
globalState.tapWalletManager.reloadData(scanNoteResponse)
|
||||
} else {
|
||||
globalState.tapWalletManager.loadData(scanNoteResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is NetworkStateChanged -> {
|
||||
globalState.scanResponse?.let { scanNoteResponse ->
|
||||
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
|
||||
scope.launch { globalState.tapWalletManager.loadData(scanNoteResponse) }
|
||||
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
|
||||
val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync
|
||||
if (selectedUserWallet != null) scope.launch {
|
||||
globalState.tapWalletManager.loadData(selectedUserWallet)
|
||||
} else {
|
||||
globalState.scanResponse?.let { scanNoteResponse ->
|
||||
scope.launch { globalState.tapWalletManager.loadData(scanNoteResponse) }
|
||||
}
|
||||
}
|
||||
}
|
||||
is WalletAction.CopyAddress -> {
|
||||
|
|
@ -255,7 +257,7 @@ class WalletMiddleware {
|
|||
if (newAction is PrepareSendScreen && newAction.walletManager == null) {
|
||||
store.dispatch(NavigationAction.PopBackTo(screen = AppScreen.Home))
|
||||
FirebaseCrashlytics.getInstance().recordException(
|
||||
IllegalStateException("PrepareSendScreen: walletManager is null")
|
||||
IllegalStateException("PrepareSendScreen: walletManager is null"),
|
||||
)
|
||||
store.dispatchToastNotification(R.string.internal_error_wallet_manager_not_found)
|
||||
} else {
|
||||
|
|
@ -265,6 +267,75 @@ class WalletMiddleware {
|
|||
}
|
||||
}
|
||||
}
|
||||
is WalletAction.ShowSaveWalletIfNeeded -> {
|
||||
showSaveWalletIfNeeded()
|
||||
}
|
||||
is WalletAction.ChangeWallet -> {
|
||||
changeWallet()
|
||||
}
|
||||
is WalletAction.UserWalletChanged -> {
|
||||
scope.launch {
|
||||
globalState.tapWalletManager.loadData(action.userWallet)
|
||||
}
|
||||
}
|
||||
is WalletAction.WalletStoresChanged -> {
|
||||
scope.launch(Dispatchers.Default) {
|
||||
fetchTotalFiatBalance(action.walletStores, walletState)
|
||||
findMissedDerivations(action.walletStores)
|
||||
tryToShowAppRatingWarning(action.walletStores)
|
||||
}
|
||||
}
|
||||
is WalletAction.TotalFiatBalanceChanged -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchTotalFiatBalance(walletStores: List<WalletStoreModel>, state: WalletState) {
|
||||
scope.launch {
|
||||
val totalFiatBalance = totalFiatBalanceCalculator.calculate(
|
||||
prevAmount = state.totalBalance?.fiatAmount ?: BigDecimal.ZERO,
|
||||
walletStores = walletStores,
|
||||
)
|
||||
store.dispatchOnMain(WalletAction.TotalFiatBalanceChanged(totalFiatBalance))
|
||||
}
|
||||
}
|
||||
|
||||
private fun findMissedDerivations(wallStores: List<WalletStoreModel>) {
|
||||
scope.launch {
|
||||
val missedDerivations = wallStores
|
||||
.filter { store ->
|
||||
store.walletsData.any { it.status is WalletDataModel.MissedDerivation }
|
||||
}
|
||||
.map { it.blockchainNetwork }
|
||||
|
||||
store.dispatchOnMain(WalletAction.MultiWallet.AddMissingDerivations(missedDerivations))
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryToShowAppRatingWarning(walletStores: List<WalletStoreModel>) {
|
||||
warningsMiddleware.tryToShowAppRatingWarning(
|
||||
hasNonZeroWallets = walletStores
|
||||
.flatMap { it.walletsData }
|
||||
.any { it.status.amount.isGreaterThan(BigDecimal.ZERO) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun showSaveWalletIfNeeded() {
|
||||
if (preferencesStorage.shouldShowSaveWallet
|
||||
&& tangemSdkManager.canUseBiometry
|
||||
&& store.state.navigationState.backStack.lastOrNull() == AppScreen.Wallet
|
||||
) {
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.SaveWallet))
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeWallet() {
|
||||
when {
|
||||
userWalletsListManager.hasSavedUserWallets -> {
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.WalletSelector))
|
||||
}
|
||||
else -> {
|
||||
store.dispatch(WalletAction.Scan)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,11 +67,8 @@ class WarningsMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
fun tryToShowAppRatingWarning(wallet: Wallet) {
|
||||
val nonZeroWalletsCount = wallet.amounts.filter {
|
||||
it.value.value?.isGreaterThan(BigDecimal.ZERO) ?: false
|
||||
}.size
|
||||
if (nonZeroWalletsCount > 0) {
|
||||
fun tryToShowAppRatingWarning(hasNonZeroWallets: Boolean) {
|
||||
if (hasNonZeroWallets) {
|
||||
preferencesStorage.appRatingLaunchObserver.foundWalletWithFunds()
|
||||
}
|
||||
if (preferencesStorage.appRatingLaunchObserver.isReadyToShow()) {
|
||||
|
|
@ -79,6 +76,13 @@ class WarningsMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
fun tryToShowAppRatingWarning(wallet: Wallet) {
|
||||
val nonZeroWalletsCount = wallet.amounts.filter {
|
||||
it.value.value?.isGreaterThan(BigDecimal.ZERO) ?: false
|
||||
}.size
|
||||
tryToShowAppRatingWarning(hasNonZeroWallets = nonZeroWalletsCount > 0)
|
||||
}
|
||||
|
||||
private fun showCardWarningsIfNeeded(globalState: GlobalState?) {
|
||||
globalState?.scanResponse?.let { scanResponse ->
|
||||
val card = scanResponse.card
|
||||
|
|
|
|||
|
|
@ -9,17 +9,9 @@ import com.tangem.tap.common.extensions.toFiatString
|
|||
import com.tangem.tap.common.extensions.toFormattedCurrencyString
|
||||
import com.tangem.tap.domain.getFirstToken
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.models.WalletRent
|
||||
import com.tangem.tap.features.wallet.models.filterByToken
|
||||
import com.tangem.tap.features.wallet.models.getPendingTransactions
|
||||
import com.tangem.tap.features.wallet.models.removeUnknownTransactions
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.features.wallet.redux.WalletData
|
||||
import com.tangem.tap.features.wallet.redux.WalletMainButton
|
||||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
import com.tangem.tap.features.wallet.models.*
|
||||
import com.tangem.tap.features.wallet.redux.*
|
||||
import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
|
||||
import com.tangem.tap.features.wallet.redux.WalletStore
|
||||
import com.tangem.tap.features.wallet.ui.BalanceStatus
|
||||
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
|
||||
import com.tangem.tap.features.wallet.ui.TokenData
|
||||
|
|
@ -174,9 +166,13 @@ class MultiWalletReducer {
|
|||
is WalletAction.MultiWallet.ShowWalletBackupWarning -> state.copy(
|
||||
showBackupWarning = action.show,
|
||||
)
|
||||
is WalletAction.MultiWallet.AddMissingDerivations -> state.copy(missingDerivations = action.blockchains)
|
||||
is WalletAction.MultiWallet.AddMissingDerivations -> state.copy(
|
||||
missingDerivations = action.blockchains,
|
||||
)
|
||||
is WalletAction.MultiWallet.BackupWallet -> state
|
||||
is WalletAction.MultiWallet.ScanToGetDerivations -> state
|
||||
is WalletAction.MultiWallet.ScanToGetDerivations -> state.copy(
|
||||
state = ProgressState.Loading,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,34 +3,31 @@ package com.tangem.tap.features.wallet.redux.reducers
|
|||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.common.extensions.mapNotNullValues
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.common.TwinCardNumber
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.common.extensions.toFiatRateString
|
||||
import com.tangem.tap.common.extensions.toFiatString
|
||||
import com.tangem.tap.common.extensions.toFiatValue
|
||||
import com.tangem.tap.common.extensions.toFormattedCurrencyString
|
||||
import com.tangem.tap.common.extensions.toFormattedFiatValue
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.extensions.getArtworkUrl
|
||||
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
|
||||
import com.tangem.tap.domain.getFirstToken
|
||||
import com.tangem.tap.domain.model.TotalFiatBalance
|
||||
import com.tangem.tap.domain.model.WalletDataModel
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.models.TotalBalance
|
||||
import com.tangem.tap.features.wallet.models.WalletRent
|
||||
import com.tangem.tap.features.wallet.redux.AddressData
|
||||
import com.tangem.tap.features.wallet.redux.Artwork
|
||||
import com.tangem.tap.features.wallet.redux.ErrorType
|
||||
import com.tangem.tap.features.wallet.redux.ProgressState
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.features.wallet.redux.WalletAddresses
|
||||
import com.tangem.tap.features.wallet.redux.WalletData
|
||||
import com.tangem.tap.features.wallet.redux.WalletMainButton
|
||||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
import com.tangem.tap.features.wallet.redux.WalletStore
|
||||
import com.tangem.tap.features.wallet.redux.*
|
||||
import com.tangem.tap.features.wallet.ui.BalanceStatus
|
||||
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.tap.features.wallet.ui.TokenData
|
||||
import com.tangem.tap.store
|
||||
import org.rekotlin.Action
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -124,7 +121,10 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
|
|||
),
|
||||
)
|
||||
}
|
||||
else -> { /* no-op */
|
||||
else -> {
|
||||
newState = newState.copy(
|
||||
state = ProgressState.Error,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -297,8 +297,8 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
|
|||
newState = newState.updateWalletData(
|
||||
selectedWalletData?.copy(
|
||||
walletAddresses = WalletAddresses(
|
||||
address,
|
||||
walletAddresses.list,
|
||||
selectedAddress = address,
|
||||
list = walletAddresses.list,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -319,13 +319,164 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
|
|||
}
|
||||
is WalletAction.UserTokens.Loading -> newState = newState.copy(loadingUserTokens = true)
|
||||
is WalletAction.UserTokens.Loaded -> newState = newState.copy(loadingUserTokens = false)
|
||||
else -> { /* no-op */
|
||||
is WalletAction.UserWalletChanged -> with(action.userWallet) {
|
||||
val card = scanResponse.card
|
||||
newState = WalletState(
|
||||
cardId = card.cardId,
|
||||
isMultiwalletAllowed = card.isMultiwalletAllowed,
|
||||
cardImage = Artwork(
|
||||
artworkId = artworkUrl,
|
||||
),
|
||||
isTestnet = card.isTestCard,
|
||||
state = ProgressState.Loading,
|
||||
wallets = newState.wallets,
|
||||
showBackupWarning = card.isMultiwalletAllowed &&
|
||||
card.settings.isBackupAllowed &&
|
||||
card.backupStatus == CardDTO.BackupStatus.NoBackup,
|
||||
)
|
||||
}
|
||||
is WalletAction.WalletStoresChanged -> {
|
||||
newState = newState.copy(
|
||||
wallets = action.walletStores.mapToReduxModel(newState.isMultiwalletAllowed),
|
||||
)
|
||||
}
|
||||
is WalletAction.TotalFiatBalanceChanged -> {
|
||||
newState = newState.copy(
|
||||
totalBalance = action.balance.mapToReduxModel(),
|
||||
)
|
||||
}
|
||||
is WalletAction.LoadData.Success -> {
|
||||
val selectedCurrency = if (!newState.isMultiwalletAllowed) {
|
||||
newState.wallets.firstOrNull()
|
||||
?.walletsData
|
||||
?.firstOrNull()
|
||||
?.currency
|
||||
} else {
|
||||
newState.selectedCurrency
|
||||
}
|
||||
|
||||
newState = newState.copy(
|
||||
state = ProgressState.Done,
|
||||
selectedCurrency = selectedCurrency,
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
appStateHolder.walletState = newState
|
||||
return newState
|
||||
}
|
||||
|
||||
@JvmName("walletStoreModelToReduxModel")
|
||||
private fun List<WalletStoreModel>.mapToReduxModel(
|
||||
isMultiWalletAllowed: Boolean,
|
||||
): List<WalletStore> {
|
||||
return this.map { walletStoreModel ->
|
||||
with(walletStoreModel) {
|
||||
WalletStore(
|
||||
walletManager = walletManager,
|
||||
blockchainNetwork = blockchainNetwork,
|
||||
walletsData = walletsData.mapToReduxModel(isMultiWalletAllowed, walletStoreModel.walletRent),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmName("walletDataModelToReduxModel")
|
||||
private fun List<WalletDataModel>.mapToReduxModel(
|
||||
isMultiWalletAllowed: Boolean,
|
||||
walletRent: WalletStoreModel.WalletRent?,
|
||||
): List<WalletData> {
|
||||
return this.map { walletDataModel ->
|
||||
with(walletDataModel) {
|
||||
val amount = status.amount
|
||||
val amountFormatted = amount.toFormattedCurrencyString(
|
||||
decimals = currency.decimals,
|
||||
currency = currency.currencySymbol,
|
||||
)
|
||||
val appCurrency = store.state.globalState.appCurrency
|
||||
val fiatAmount = fiatRate?.let { status.amount.toFiatValue(it) }
|
||||
val fiatAmountFormatted = fiatAmount
|
||||
?.takeIf { !status.isErrorStatus }
|
||||
?.toFormattedFiatValue(appCurrency.symbol)
|
||||
val fiatRateFormatted = fiatRate?.toFiatRateString(appCurrency.symbol)
|
||||
|
||||
WalletData(
|
||||
currency = currency,
|
||||
walletAddresses = walletAddresses.getOrNull(0)?.let { selectedAddress ->
|
||||
WalletAddresses(
|
||||
selectedAddress = selectedAddress,
|
||||
list = walletAddresses,
|
||||
)
|
||||
},
|
||||
existentialDepositString = existentialDeposit?.toPlainString(),
|
||||
fiatRate = fiatRate,
|
||||
fiatRateString = fiatRateFormatted,
|
||||
pendingTransactions = status.pendingTransactions,
|
||||
mainButton = WalletMainButton.SendButton(
|
||||
enabled = !status.amount.isZero() && status.pendingTransactions.isEmpty(),
|
||||
),
|
||||
walletRent = walletRent?.let {
|
||||
WalletRent(
|
||||
minRentValue = "${it.rent.stripZeroPlainString()} ${currency.blockchain.currency}",
|
||||
rentExemptValue = "${it.exemptionAmount.stripZeroPlainString()} ${currency.blockchain.currency}",
|
||||
)
|
||||
},
|
||||
currencyData = BalanceWidgetData(
|
||||
status = when (status) {
|
||||
is WalletDataModel.Loading -> BalanceStatus.Loading
|
||||
is WalletDataModel.NoAccount -> BalanceStatus.NoAccount
|
||||
is WalletDataModel.Refreshing -> BalanceStatus.Refreshing
|
||||
is WalletDataModel.SameCurrencyTransactionInProgress -> BalanceStatus.SameCurrencyTransactionInProgress
|
||||
is WalletDataModel.TransactionInProgress -> BalanceStatus.TransactionInProgress
|
||||
is WalletDataModel.Unreachable,
|
||||
is WalletDataModel.MissedDerivation,
|
||||
-> BalanceStatus.Unreachable
|
||||
is WalletDataModel.VerifiedOnline -> BalanceStatus.VerifiedOnline
|
||||
},
|
||||
currency = currency.currencyName,
|
||||
currencySymbol = currency.currencySymbol,
|
||||
blockchainAmount = status.amount,
|
||||
amount = amount,
|
||||
amountFormatted = amountFormatted,
|
||||
fiatAmount = fiatAmount,
|
||||
fiatAmountFormatted = fiatAmountFormatted,
|
||||
token = when {
|
||||
!isMultiWalletAllowed && currency is Currency.Token -> {
|
||||
TokenData(
|
||||
amount = amount,
|
||||
amountFormatted = amountFormatted,
|
||||
fiatAmount = fiatAmount,
|
||||
fiatAmountFormatted = fiatAmountFormatted,
|
||||
tokenSymbol = currency.currencySymbol,
|
||||
fiatRate = fiatRate,
|
||||
fiatRateString = fiatRateFormatted,
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
},
|
||||
amountToCreateAccount = (status as? WalletDataModel.NoAccount)
|
||||
?.amountToCreateAccount
|
||||
?.toString(),
|
||||
errorMessage = status.errorMessage,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun TotalFiatBalance.mapToReduxModel(): TotalBalance {
|
||||
return TotalBalance(
|
||||
state = when (this) {
|
||||
is TotalFiatBalance.Loading -> ProgressState.Loading
|
||||
is TotalFiatBalance.Refreshing -> ProgressState.Refreshing
|
||||
is TotalFiatBalance.Error -> ProgressState.Error
|
||||
is TotalFiatBalance.Loaded -> ProgressState.Done
|
||||
},
|
||||
fiatAmount = amount,
|
||||
fiatCurrency = store.state.globalState.appCurrency,
|
||||
)
|
||||
}
|
||||
|
||||
fun createAddressList(wallet: Wallet?, walletAddresses: WalletAddresses? = null): WalletAddresses? {
|
||||
if (wallet == null) return null
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ data class BalanceWidgetData(
|
|||
)
|
||||
|
||||
data class TokenData(
|
||||
val amountFormatted: String,
|
||||
val amountFormatted: String?,
|
||||
val amount: BigDecimal? = null,
|
||||
val tokenSymbol: String,
|
||||
val fiatAmountFormatted: String? = null,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
package com.tangem.tap.features.wallet.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.*
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.annotation.ColorRes
|
||||
|
|
@ -22,33 +18,26 @@ import com.tangem.tap.common.TestActions
|
|||
import com.tangem.tap.common.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.events.DetailsScreen
|
||||
import com.tangem.tap.common.analytics.events.Token
|
||||
import com.tangem.tap.common.extensions.appendIfNotNull
|
||||
import com.tangem.tap.common.extensions.beginDelayedTransition
|
||||
import com.tangem.tap.common.extensions.fitChipsByGroupWidth
|
||||
import com.tangem.tap.common.extensions.getColor
|
||||
import com.tangem.tap.common.extensions.getString
|
||||
import com.tangem.tap.common.extensions.hide
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.common.extensions.toQrCode
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.onboarding.getQRReceiveMessage
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.models.PendingTransaction
|
||||
import com.tangem.tap.features.wallet.redux.ErrorType
|
||||
import com.tangem.tap.features.wallet.redux.ProgressState
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.features.wallet.redux.WalletData
|
||||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
import com.tangem.tap.features.wallet.redux.*
|
||||
import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
|
||||
import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter
|
||||
import com.tangem.tap.features.wallet.ui.adapters.WalletDetailWarningMessagesAdapter
|
||||
import com.tangem.tap.features.wallet.ui.images.load
|
||||
import com.tangem.tap.features.wallet.ui.test.TestWallet
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.userWalletsListManagerSafe
|
||||
import com.tangem.tap.walletCurrenciesManager
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.FragmentWalletDetailsBinding
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
|
||||
|
|
@ -158,15 +147,19 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
|
|||
binding.srlWalletDetails.setOnRefreshListener {
|
||||
if (selectedWallet.currencyData.status != BalanceStatus.Loading) {
|
||||
Analytics.send(Token.Refreshed())
|
||||
store.dispatch(
|
||||
WalletAction.LoadWallet(
|
||||
blockchain = BlockchainNetwork(
|
||||
selectedWallet.currency.blockchain,
|
||||
selectedWallet.currency.derivationPath,
|
||||
emptyList(),
|
||||
),
|
||||
),
|
||||
val blockchainNetwork = BlockchainNetwork(
|
||||
blockchain = selectedWallet.currency.blockchain,
|
||||
derivationPath = selectedWallet.currency.derivationPath,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync
|
||||
if (selectedUserWallet != null) scope.launch {
|
||||
walletCurrenciesManager.update(selectedUserWallet, blockchainNetwork)
|
||||
} else {
|
||||
store.dispatch(
|
||||
WalletAction.LoadWallet(blockchainNetwork),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import android.view.View
|
|||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.transition.TransitionInflater
|
||||
|
|
@ -41,6 +42,7 @@ import com.tangem.tap.features.wallet.ui.wallet.SaltPaySingleWalletView
|
|||
import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView
|
||||
import com.tangem.tap.features.wallet.ui.wallet.WalletView
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.FragmentWalletBinding
|
||||
|
|
@ -54,6 +56,8 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
|
||||
private var walletView: WalletView = SingleWalletView()
|
||||
|
||||
private val viewModel by viewModels<WalletViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
|
|
@ -63,7 +67,13 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
this,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
|
||||
val popBackTo = if (userWalletsListManager.hasSavedUserWallets) {
|
||||
userWalletsListManager.lock()
|
||||
AppScreen.Welcome
|
||||
} else {
|
||||
AppScreen.Home
|
||||
}
|
||||
store.dispatch(NavigationAction.PopBackTo(popBackTo))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -78,6 +88,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
state.select { it.walletState }
|
||||
}
|
||||
walletView.setFragment(this, binding)
|
||||
viewModel.launch()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
|
|
@ -91,7 +102,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
|
||||
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.Send))
|
||||
store.dispatch(WalletAction.ChangeWallet)
|
||||
}
|
||||
setupWarningsRecyclerView()
|
||||
walletView.changeWalletView(this, binding)
|
||||
|
|
@ -162,6 +173,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
store.dispatch(WalletAction.LoadData.Refresh)
|
||||
}
|
||||
}
|
||||
|
||||
val navigationIconRes = if (state.hasSavedWallets) R.drawable.ic_wallet_24 else R.drawable.ic_tap_card_24
|
||||
binding.toolbar.setNavigationIcon(navigationIconRes)
|
||||
}
|
||||
|
||||
private fun showWarningsIfPresent(warnings: List<WarningMessage>) {
|
||||
|
|
@ -211,9 +225,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
store.state.globalState.scanResponse?.let { scanNoteResponse ->
|
||||
store.dispatch(
|
||||
DetailsAction.PrepareScreen(
|
||||
scanNoteResponse,
|
||||
store.state.walletState.walletManagers.map { it.wallet },
|
||||
CardTou(),
|
||||
scanResponse = scanNoteResponse,
|
||||
wallets = store.state.walletState.walletManagers.map { it.wallet },
|
||||
cardTou = CardTou(),
|
||||
),
|
||||
)
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.Details))
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue