Updated on 2026-08-14
This commit is contained in:
commit
4c97630a6e
266 changed files with 6858 additions and 2386 deletions
|
|
@ -35,6 +35,8 @@ dependencies {
|
|||
implementation(projects.domain.txhistory)
|
||||
implementation(projects.domain.appCurrency)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.appTheme)
|
||||
implementation(projects.domain.appTheme.models)
|
||||
|
||||
implementation(project(":common"))
|
||||
implementation(project(":core:analytics"))
|
||||
|
|
@ -48,13 +50,15 @@ dependencies {
|
|||
implementation(project(":libs:crypto"))
|
||||
implementation(project(":libs:auth"))
|
||||
|
||||
implementation(project(":data:source:preferences"))
|
||||
implementation(projects.data.appCurrency)
|
||||
implementation(projects.data.appTheme)
|
||||
implementation(projects.data.card)
|
||||
implementation(projects.data.common)
|
||||
implementation(projects.data.settings)
|
||||
implementation(projects.data.source.preferences)
|
||||
implementation(projects.data.tokens)
|
||||
implementation(projects.data.txhistory)
|
||||
implementation(projects.data.appCurrency)
|
||||
implementation(projects.data.wallets)
|
||||
|
||||
/** Features */
|
||||
implementation(project(":features:onboarding"))
|
||||
|
|
@ -86,6 +90,7 @@ dependencies {
|
|||
implementation(deps.lifecycle.runtime.ktx)
|
||||
implementation(deps.lifecycle.common.java8)
|
||||
implementation(deps.lifecycle.viewModel.ktx)
|
||||
implementation(deps.lifecycle.compose)
|
||||
|
||||
/** Compose libraries */
|
||||
implementation(deps.compose.constraintLayout)
|
||||
|
|
|
|||
12
app/src/main/java/com/tangem/tap/GlobalSettingsState.kt
Normal file
12
app/src/main/java/com/tangem/tap/GlobalSettingsState.kt
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.tap
|
||||
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
|
||||
internal sealed class GlobalSettingsState {
|
||||
|
||||
object Loading : GlobalSettingsState()
|
||||
|
||||
data class Content(
|
||||
val appThemeMode: AppThemeMode,
|
||||
) : GlobalSettingsState()
|
||||
}
|
||||
|
|
@ -4,10 +4,10 @@ import android.content.Intent
|
|||
import android.content.pm.ActivityInfo
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
|
@ -26,6 +26,7 @@ import com.tangem.tap.common.ActivityResultCallbackHolder
|
|||
import com.tangem.tap.common.DialogManager
|
||||
import com.tangem.tap.common.OnActivityResultCallback
|
||||
import com.tangem.tap.common.SnackbarHandler
|
||||
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.NotificationsHandler
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
|
|
@ -53,6 +54,8 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import java.lang.ref.WeakReference
|
||||
import javax.inject.Inject
|
||||
|
|
@ -109,6 +112,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
@Inject
|
||||
lateinit var walletConnectInteractor: WalletConnectInteractor
|
||||
|
||||
private val viewModel: MainViewModel by viewModels()
|
||||
private var isInitializing: Boolean = true
|
||||
|
||||
// TODO: fixme: inject through DI
|
||||
private val intentProcessor: IntentProcessor = IntentProcessor()
|
||||
|
||||
|
|
@ -119,11 +125,17 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
val splashScreen = installSplashScreen()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
bootstrapMainStateUpdates()
|
||||
|
||||
splashScreen.setKeepOnScreenCondition { isInitializing }
|
||||
|
||||
setContentView(R.layout.activity_main)
|
||||
systemActions()
|
||||
|
||||
store.dispatch(NavigationAction.ActivityCreated(WeakReference(this)))
|
||||
|
||||
cardSdkLifecycleObserver.onCreate(context = this)
|
||||
|
|
@ -204,13 +216,24 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager))
|
||||
}
|
||||
|
||||
private fun bootstrapMainStateUpdates() {
|
||||
viewModel.state
|
||||
.onEach { state ->
|
||||
isInitializing = state is GlobalSettingsState.Loading
|
||||
|
||||
when (state) {
|
||||
is GlobalSettingsState.Content -> {
|
||||
MutableAppThemeModeHolder.value = state.appThemeMode
|
||||
}
|
||||
is GlobalSettingsState.Loading -> Unit
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private fun systemActions() {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
val windowInsetsController = WindowInsetsControllerCompat(window, binding.root)
|
||||
windowInsetsController.isAppearanceLightStatusBars = true
|
||||
windowInsetsController.isAppearanceLightNavigationBars = true
|
||||
|
||||
supportFragmentManager.registerFragmentLifecycleCallbacks(
|
||||
NavBarInsetsFragmentLifecycleCallback(),
|
||||
true,
|
||||
|
|
|
|||
35
app/src/main/java/com/tangem/tap/MainViewModel.kt
Normal file
35
app/src/main/java/com/tangem/tap/MainViewModel.kt
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.tap
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class MainViewModel @Inject constructor(
|
||||
private val getAppThemeModeUseCase: GetAppThemeModeUseCase,
|
||||
) : ViewModel() {
|
||||
|
||||
val state: StateFlow<GlobalSettingsState> = createMainStateFlow()
|
||||
|
||||
private fun createMainStateFlow(): StateFlow<GlobalSettingsState> {
|
||||
return getAppThemeModeUseCase()
|
||||
.map { maybeMode ->
|
||||
val mode = maybeMode.getOrElse { AppThemeMode.DEFAULT }
|
||||
|
||||
GlobalSettingsState.Content(appThemeMode = mode)
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000),
|
||||
initialValue = GlobalSettingsState.Loading,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ import com.tangem.domain.DomainLayer
|
|||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.legacy.WalletManagersRepository
|
||||
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
|
||||
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
|
||||
|
|
@ -41,6 +42,7 @@ import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
|
|||
import com.tangem.tap.common.feedback.FeedbackManager
|
||||
import com.tangem.tap.common.images.createCoilImageLoader
|
||||
import com.tangem.tap.common.log.TangemLogCollector
|
||||
import com.tangem.tap.common.log.TimberFormatStrategy
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.appReducer
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
|
|
@ -164,6 +166,9 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
@Inject
|
||||
lateinit var appCurrencyRepository: AppCurrencyRepository
|
||||
|
||||
@Inject
|
||||
lateinit var walletManagersFacade: WalletManagersFacade
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
|
|
@ -183,12 +188,13 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
tokenDetailsFeatureToggles = tokenDetailsFeatureToggles,
|
||||
scanCardProcessor = scanCardProcessor,
|
||||
appCurrencyRepository = appCurrencyRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.addLogAdapter(AndroidLogAdapter())
|
||||
Logger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy()))
|
||||
Timber.plant(
|
||||
object : Timber.DebugTree() {
|
||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.tap.common.apptheme
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
|
||||
internal object MutableAppThemeModeHolder : AppThemeModeHolder {
|
||||
|
||||
override val appThemeMode: MutableState<AppThemeMode> = mutableStateOf(AppThemeMode.DEFAULT)
|
||||
|
||||
var value: AppThemeMode
|
||||
set(value) {
|
||||
appThemeMode.value = value
|
||||
}
|
||||
get() = appThemeMode.value
|
||||
}
|
||||
|
|
@ -82,7 +82,7 @@ fun WalletManager?.getAddressData(): WalletDataModel.AddressData? {
|
|||
}
|
||||
|
||||
fun <T> WalletManager.Companion.stub(): T {
|
||||
val wallet = Wallet(Blockchain.Unknown, setOf(), Wallet.PublicKey(byteArrayOf(), null, null), setOf())
|
||||
val wallet = Wallet(Blockchain.Unknown, setOf(), Wallet.PublicKey(byteArrayOf(), null), setOf())
|
||||
return object : WalletManager(wallet) {
|
||||
override val currentHost: String = ""
|
||||
override suspend fun update() {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
package com.tangem.tap.common.log
|
||||
|
||||
import com.orhanobut.logger.FormatStrategy
|
||||
import com.orhanobut.logger.LogStrategy
|
||||
import com.orhanobut.logger.LogcatLogStrategy
|
||||
|
||||
class TimberFormatStrategy : FormatStrategy {
|
||||
|
||||
private val logStrategy: LogStrategy = LogcatLogStrategy()
|
||||
|
||||
override fun log(priority: Int, tag: String?, message: String) {
|
||||
logTopBorder(priority, tag)
|
||||
val bytes = message.toByteArray()
|
||||
val length = bytes.size
|
||||
if (length <= CHUNK_SIZE) {
|
||||
logContent(priority, tag, message)
|
||||
logBottomBorder(priority, tag)
|
||||
return
|
||||
}
|
||||
var i = 0
|
||||
while (i < length) {
|
||||
val count = (length - i).coerceAtMost(CHUNK_SIZE)
|
||||
// create a new String with system's default charset (which is UTF-8 for Android)
|
||||
logContent(priority, tag, String(bytes, i, count))
|
||||
i += CHUNK_SIZE
|
||||
}
|
||||
logBottomBorder(priority, tag)
|
||||
}
|
||||
|
||||
private fun logTopBorder(logType: Int, tag: String?) {
|
||||
logChunk(logType, tag, TOP_BORDER)
|
||||
}
|
||||
|
||||
private fun logBottomBorder(logType: Int, tag: String?) {
|
||||
logChunk(logType, tag, BOTTOM_BORDER)
|
||||
}
|
||||
|
||||
private fun logContent(logType: Int, tag: String?, chunk: String) {
|
||||
chunk.split(System.lineSeparator()).forEach { line ->
|
||||
logChunk(logType, tag, "$HORIZONTAL_LINE $line")
|
||||
}
|
||||
}
|
||||
|
||||
private fun logChunk(priority: Int, tag: String?, chunk: String) {
|
||||
logStrategy.log(priority, tag, chunk)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
/**
|
||||
* Android's max limit for a log entry is ~4076 bytes,
|
||||
* so 4000 bytes is used as chunk size since default charset
|
||||
* is UTF-8
|
||||
*/
|
||||
private const val CHUNK_SIZE = 4000
|
||||
|
||||
const val TOP_LEFT_CORNER = "┌"
|
||||
const val BOTTOM_LEFT_CORNER = "└"
|
||||
const val HORIZONTAL_LINE = "│"
|
||||
const val DOUBLE_DIVIDER = "────────────────────────────────────────────────────────"
|
||||
const val TOP_BORDER = TOP_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER
|
||||
const val BOTTOM_BORDER = BOTTOM_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.di
|
||||
|
||||
import com.tangem.core.navigation.NavigationStateHolder
|
||||
import com.tangem.core.navigation.ReduxNavController
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.wallets.legacy.WalletsStateHolder
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import dagger.Binds
|
||||
|
|
@ -19,5 +20,9 @@ internal interface AppStateHolderModule {
|
|||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindsNavigationStateHolder(appStateHolder: AppStateHolder): NavigationStateHolder
|
||||
fun bindsNavigationStateHolder(appStateHolder: AppStateHolder): ReduxNavController
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindsReduxStateHolder(appStateHolder: AppStateHolder): ReduxStateHolder
|
||||
}
|
||||
18
app/src/main/java/com/tangem/tap/di/ThemeModule.kt
Normal file
18
app/src/main/java/com/tangem/tap/di/ThemeModule.kt
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.tap.di
|
||||
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ActivityComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(ActivityComponent::class)
|
||||
internal object ThemeModule {
|
||||
|
||||
@Provides
|
||||
fun provideAppThemeModeHolder(): AppThemeModeHolder {
|
||||
return MutableAppThemeModeHolder
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.apptheme.ChangeAppThemeModeUseCase
|
||||
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
||||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
internal object AppThemeDomainModule {
|
||||
|
||||
@Provides
|
||||
fun provideGetAppThemeModeUpdatesUseCase(appThemeModeRepository: AppThemeModeRepository): GetAppThemeModeUseCase {
|
||||
return GetAppThemeModeUseCase(appThemeModeRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideChangeAppThemeModeUseCase(appThemeModeRepository: AppThemeModeRepository): ChangeAppThemeModeUseCase {
|
||||
return ChangeAppThemeModeUseCase(appThemeModeRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.settings.CanUseBiometryUseCase
|
||||
import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase
|
||||
import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.tap.domain.TangemSdkManager
|
||||
import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -17,4 +21,20 @@ internal object SettingsDomainModule {
|
|||
fun providesGetWalletsUseCase(settingsRepository: SettingsRepository): IsUserAlreadyRateAppUseCase {
|
||||
return IsUserAlreadyRateAppUseCase(settingsRepository = settingsRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun providesShouldShowSaveWalletScreenUseCase(
|
||||
settingsRepository: SettingsRepository,
|
||||
): ShouldShowSaveWalletScreenUseCase {
|
||||
return ShouldShowSaveWalletScreenUseCase(settingsRepository = settingsRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun providesCanUseBiometryUseCase(tangemSdkManager: TangemSdkManager): CanUseBiometryUseCase {
|
||||
return CanUseBiometryUseCase(
|
||||
legacySettingsRepository = DefaultLegacySettingsRepository(tangemSdkManager = tangemSdkManager),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,16 @@ import dagger.hilt.android.scopes.ViewModelScoped
|
|||
@InstallIn(ViewModelComponent::class)
|
||||
internal object TokensDomainModule {
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideFetchTokenListUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
quotesRepository: QuotesRepository,
|
||||
networksRepository: NetworksRepository,
|
||||
): FetchTokenListUseCase {
|
||||
return FetchTokenListUseCase(currenciesRepository, networksRepository, quotesRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetTokenListUseCase(
|
||||
|
|
@ -26,6 +36,15 @@ internal object TokensDomainModule {
|
|||
return GetTokenListUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideRemoveCurrencyUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): RemoveCurrencyUseCase {
|
||||
return RemoveCurrencyUseCase(currenciesRepository, dispatchers)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetCurrencyUseCase(
|
||||
|
|
@ -33,8 +52,8 @@ internal object TokensDomainModule {
|
|||
quotesRepository: QuotesRepository,
|
||||
networksRepository: NetworksRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): GetCurrencyUseCase {
|
||||
return GetCurrencyUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers)
|
||||
): GetCurrencyStatusUpdatesUseCase {
|
||||
return GetCurrencyStatusUpdatesUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
@ -44,17 +63,31 @@ internal object TokensDomainModule {
|
|||
quotesRepository: QuotesRepository,
|
||||
networksRepository: NetworksRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): GetPrimaryCurrencyUseCase {
|
||||
return GetPrimaryCurrencyUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers)
|
||||
): GetPrimaryCurrencyStatusUpdatesUseCase {
|
||||
return GetPrimaryCurrencyStatusUpdatesUseCase(
|
||||
currenciesRepository,
|
||||
quotesRepository,
|
||||
networksRepository,
|
||||
dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideFetchCurrencyStatusUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
quotesRepository: QuotesRepository,
|
||||
networksRepository: NetworksRepository,
|
||||
): FetchCurrencyStatusUseCase {
|
||||
return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideToggleTokenListGroupingUseCase(
|
||||
networksRepository: NetworksRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): ToggleTokenListGroupingUseCase {
|
||||
return ToggleTokenListGroupingUseCase(networksRepository, dispatchers)
|
||||
return ToggleTokenListGroupingUseCase(dispatchers)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
@ -71,4 +104,12 @@ internal object TokensDomainModule {
|
|||
): ApplyTokenListSortingUseCase {
|
||||
return ApplyTokenListSortingUseCase(currenciesRepository, dispatchers)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetCryptoCurrencyActionsUseCase(
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): GetCryptoCurrencyActionsUseCase {
|
||||
return GetCryptoCurrencyActionsUseCase(dispatchers)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.di.domain
|
|||
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.legacy.WalletsStateHolder
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.domain.wallets.usecase.*
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -48,4 +49,22 @@ internal object WalletsDomainModule {
|
|||
fun providesSelectWalletUseCase(walletsStateHolder: WalletsStateHolder): SelectWalletUseCase {
|
||||
return SelectWalletUseCase(walletsStateHolder = walletsStateHolder)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun providesUpdateWalletUseCase(walletsStateHolder: WalletsStateHolder): UpdateWalletUseCase {
|
||||
return UpdateWalletUseCase(walletsStateHolder = walletsStateHolder)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun providesDeleteWalletUseCase(walletsStateHolder: WalletsStateHolder): DeleteWalletUseCase {
|
||||
return DeleteWalletUseCase(walletsStateHolder = walletsStateHolder)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun providesShouldSaveUserWalletsUseCase(walletsRepository: WalletsRepository): ShouldSaveUserWalletsUseCase {
|
||||
return ShouldSaveUserWalletsUseCase(walletsRepository = walletsRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.tap.domain.settings
|
||||
|
||||
import com.tangem.domain.settings.repositories.LegacySettingsRepository
|
||||
import com.tangem.tap.domain.TangemSdkManager
|
||||
|
||||
internal class DefaultLegacySettingsRepository(
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
) : LegacySettingsRepository {
|
||||
|
||||
override fun canUseBiometry(): Boolean = tangemSdkManager.canUseBiometry
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.domain.tasks.product
|
||||
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.Card
|
||||
|
|
@ -224,7 +225,8 @@ private class ScanWalletProcessor(
|
|||
walletData = session.environment.walletData,
|
||||
primaryCard = primaryCard,
|
||||
)
|
||||
val derivations = collectDerivations(card, config, scanResponse.derivationStyleProvider)
|
||||
val derivations =
|
||||
collectDerivations(card, config, scanResponse.derivationStyleProvider)
|
||||
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
|
||||
callback(CompletionResult.Success(scanResponse))
|
||||
return@launch
|
||||
|
|
@ -252,60 +254,101 @@ private class ScanWalletProcessor(
|
|||
derivationStyleProvider.getDerivationStyle(),
|
||||
)
|
||||
.toMutableList()
|
||||
.ifEmpty {
|
||||
mutableListOf(
|
||||
BlockchainNetwork(
|
||||
blockchain = Blockchain.Bitcoin,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
),
|
||||
BlockchainNetwork(
|
||||
blockchain = Blockchain.Ethereum,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
),
|
||||
)
|
||||
}
|
||||
.ifEmpty { getDefaultBlockchains(derivationStyleProvider) }
|
||||
|
||||
if (card.settings.isHDWalletAllowed) {
|
||||
blockchainsToDerive.addAll(
|
||||
listOf(
|
||||
BlockchainNetwork(
|
||||
blockchain = Blockchain.Ethereum,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
),
|
||||
BlockchainNetwork(
|
||||
blockchain = Blockchain.EthereumTestnet,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
),
|
||||
),
|
||||
)
|
||||
blockchainsToDerive += getEthereumBlockchains(derivationStyleProvider)
|
||||
}
|
||||
if (additionalBlockchainsToDerive != null) {
|
||||
blockchainsToDerive.addAll(
|
||||
additionalBlockchainsToDerive.map {
|
||||
BlockchainNetwork(
|
||||
blockchain = it,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
additionalBlockchainsToDerive?.let {
|
||||
blockchainsToDerive += getAdditionalBlockchainToDerive(derivationStyleProvider, it)
|
||||
}
|
||||
|
||||
// we should generate second key for cardano
|
||||
// because cardano address generation for wallet2 requires keys from 2 derivations
|
||||
// https://developers.cardano.org/docs/get-started/cardano-serialization-lib/generating-keys/
|
||||
val secondCardanoNetwork = blockchainsToDerive
|
||||
.find { it.blockchain == Blockchain.Cardano }
|
||||
?.let { getCardanoSecondNetwork(it) }
|
||||
secondCardanoNetwork?.let { blockchainsToDerive.add(it) }
|
||||
|
||||
// pay attention to this
|
||||
if (!card.useOldStyleDerivation) {
|
||||
blockchainsToDerive.removeAll(
|
||||
listOf(
|
||||
Blockchain.BSC, Blockchain.BSCTestnet,
|
||||
Blockchain.Polygon, Blockchain.PolygonTestnet,
|
||||
Blockchain.RSK,
|
||||
Blockchain.Fantom, Blockchain.FantomTestnet,
|
||||
Blockchain.Avalanche, Blockchain.AvalancheTestnet,
|
||||
).map {
|
||||
BlockchainNetwork(
|
||||
blockchain = it,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
)
|
||||
},
|
||||
removeUnnecessaryBlockchains(blockchainsToDerive, derivationStyleProvider)
|
||||
}
|
||||
|
||||
return blockchainsToDerive.distinct()
|
||||
}
|
||||
|
||||
private fun getDefaultBlockchains(
|
||||
derivationStyleProvider: DerivationStyleProvider,
|
||||
): MutableList<BlockchainNetwork> {
|
||||
return mutableListOf(
|
||||
BlockchainNetwork(
|
||||
blockchain = Blockchain.Bitcoin,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
),
|
||||
BlockchainNetwork(
|
||||
blockchain = Blockchain.Ethereum,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getEthereumBlockchains(derivationStyleProvider: DerivationStyleProvider): List<BlockchainNetwork> {
|
||||
return listOf(
|
||||
BlockchainNetwork(
|
||||
blockchain = Blockchain.Ethereum,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
),
|
||||
BlockchainNetwork(
|
||||
blockchain = Blockchain.EthereumTestnet,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getAdditionalBlockchainToDerive(
|
||||
derivationStyleProvider: DerivationStyleProvider,
|
||||
collection: Collection<Blockchain>,
|
||||
): List<BlockchainNetwork> {
|
||||
return collection.map {
|
||||
BlockchainNetwork(
|
||||
blockchain = it,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
)
|
||||
}
|
||||
return blockchainsToDerive.distinct()
|
||||
}
|
||||
|
||||
private fun getCardanoSecondNetwork(cardanoBlockchainNetwork: BlockchainNetwork): BlockchainNetwork? {
|
||||
val cardanoStandardDerivation = cardanoBlockchainNetwork.derivationPath?.let { DerivationPath(it) }
|
||||
?: return null
|
||||
val cardanoPatchedDerivation = CardanoUtils.extendedDerivationPath(cardanoStandardDerivation)
|
||||
return BlockchainNetwork(
|
||||
blockchain = Blockchain.Cardano,
|
||||
derivationPath = cardanoPatchedDerivation.rawPath,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun removeUnnecessaryBlockchains(
|
||||
blockchainsToDerive: MutableList<BlockchainNetwork>,
|
||||
derivationStyleProvider: DerivationStyleProvider,
|
||||
) {
|
||||
blockchainsToDerive.removeAll(
|
||||
listOf(
|
||||
Blockchain.BSC, Blockchain.BSCTestnet,
|
||||
Blockchain.Polygon, Blockchain.PolygonTestnet,
|
||||
Blockchain.RSK,
|
||||
Blockchain.Fantom, Blockchain.FantomTestnet,
|
||||
Blockchain.Avalanche, Blockchain.AvalancheTestnet,
|
||||
).map {
|
||||
BlockchainNetwork(
|
||||
blockchain = it,
|
||||
derivationStyleProvider = derivationStyleProvider,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun collectDerivations(
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ class WalletConnectRepositoryImpl @Inject constructor(
|
|||
}.groupBy { pair -> pair.first }
|
||||
.mapValues { entry -> entry.value.map { pair -> pair.second }.toSet() }
|
||||
|
||||
val preparedNamespaces = sessionProposal.requiredNamespaces
|
||||
val preparedRequiredNamespaces = sessionProposal.requiredNamespaces
|
||||
.map { requiredNamespace ->
|
||||
val accountsRequired = requiredNamespace.value.chains
|
||||
?.mapNotNull { chain -> userChains[chain] }
|
||||
|
|
@ -272,7 +272,9 @@ class WalletConnectRepositoryImpl @Inject constructor(
|
|||
|
||||
val sessionApproval = Wallet.Params.SessionApprove(
|
||||
proposerPublicKey = sessionProposal.proposerPublicKey,
|
||||
namespaces = preparedNamespaces,
|
||||
namespaces = preparedRequiredNamespaces.ifEmpty {
|
||||
sessionProposal.createPreparedOptionalNamespaces(userChains)
|
||||
},
|
||||
)
|
||||
|
||||
Timber.d("Session approval is prepared for sending: $sessionApproval")
|
||||
|
|
@ -301,6 +303,25 @@ class WalletConnectRepositoryImpl @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun Wallet.Model.SessionProposal.createPreparedOptionalNamespaces(
|
||||
userChains: Map<String, Set<String>>,
|
||||
): Map<String, Wallet.Model.Namespace.Session> {
|
||||
return optionalNamespaces
|
||||
.map { optionalNamespace ->
|
||||
val accountsOptional = optionalNamespace.value.chains
|
||||
?.mapNotNull { chain -> userChains[chain] }
|
||||
?.flatten() ?: emptyList()
|
||||
|
||||
val methods = optionalNamespace.value.methods
|
||||
optionalNamespace.key to Wallet.Model.Namespace.Session(
|
||||
accounts = accountsOptional.distinct(),
|
||||
methods = methods,
|
||||
events = optionalNamespace.value.events,
|
||||
)
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
|
||||
override fun sendRequest(requestData: RequestData, result: String) {
|
||||
val session = currentSessions.find { it.topic == requestData.topic }
|
||||
// Add Ethereum Chain method is processed without user input, skip logging it
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.features.customtoken.impl.domain
|
||||
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
|
|
@ -123,9 +124,15 @@ class DefaultCustomTokenInteractor(
|
|||
it.blockchain.getSupportedCurves().contains(curve)
|
||||
}.mapNotNull { it.derivationPath }.map { DerivationPath(it) }
|
||||
|
||||
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct()
|
||||
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList()
|
||||
if (bothCandidates.isEmpty()) return null
|
||||
|
||||
currencyList.find { it is Currency.Blockchain && it.blockchain == Blockchain.Cardano }?.let { currency ->
|
||||
currency.derivationPath?.let {
|
||||
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
|
||||
}
|
||||
}
|
||||
|
||||
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
|
||||
val alreadyDerivedKeys: ExtendedPublicKeysMap =
|
||||
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.features.demo
|
|||
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.tap.common.extensions.dispatchNotification
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.details.redux.DetailsAction
|
||||
|
|
@ -21,8 +22,8 @@ object DemoHelper {
|
|||
|
||||
private val disabledActionFeatures = listOf(
|
||||
WalletConnectAction.StartWalletConnect::class.java,
|
||||
WalletAction.TradeCryptoAction.Buy::class.java,
|
||||
WalletAction.TradeCryptoAction.Sell::class.java,
|
||||
TradeCryptoAction.Buy::class.java,
|
||||
TradeCryptoAction.Sell::class.java,
|
||||
BackupAction.StartBackup::class.java,
|
||||
WalletAction.ExploreAddress::class.java,
|
||||
DetailsAction.ResetToFactory.Start::class.java,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import android.webkit.WebView
|
|||
import androidx.transition.TransitionInflater
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.ui.fragments.setStatusBarColor
|
||||
import com.tangem.core.ui.extensions.setStatusBarColor
|
||||
import com.tangem.tap.common.extensions.beginDelayedTransition
|
||||
import com.tangem.tap.common.extensions.hide
|
||||
import com.tangem.tap.common.extensions.show
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package com.tangem.tap.features.intentHandler.handlers
|
||||
|
||||
import android.content.Intent
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||
import com.tangem.tap.features.intentHandler.IntentHandler
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.store
|
||||
import timber.log.Timber
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ class SellCurrencyIntentHandler : IntentHandler {
|
|||
|
||||
Timber.d("MoonPay Sell: $amount $currency to $destinationAddress")
|
||||
store.dispatchWithMain(
|
||||
WalletAction.TradeCryptoAction.SendCrypto(
|
||||
TradeCryptoAction.SendCrypto(
|
||||
currencyId = currency,
|
||||
amount = amount,
|
||||
destinationAddress = destinationAddress,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.navigation.ShareElement
|
||||
import com.tangem.core.ui.fragments.setStatusBarColor
|
||||
import com.tangem.core.ui.extensions.setStatusBarColor
|
||||
import com.tangem.datasource.asset.AssetReader
|
||||
import com.tangem.domain.common.TwinCardNumber
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
|
|
|
|||
|
|
@ -6,24 +6,27 @@ 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
|
||||
import com.tangem.core.ui.fragments.ComposeBottomSheetFragment
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.screen.ComposeBottomSheetFragment
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
|
||||
import com.tangem.tap.features.saveWallet.ui.components.EnrollBiometricsDialogContent
|
||||
import com.tangem.tap.features.saveWallet.ui.components.SaveWalletScreenContent
|
||||
import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment() {
|
||||
|
||||
@Inject
|
||||
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment<SaveWalletScreenState>() {
|
||||
override val expandedHeightFraction: Float = .98f
|
||||
|
||||
private val viewModel by viewModels<SaveWalletViewModel>()
|
||||
|
|
@ -34,12 +37,8 @@ internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment<SaveWa
|
|||
}
|
||||
|
||||
@Composable
|
||||
override fun provideState(): State<SaveWalletScreenState> {
|
||||
return viewModel.state.collectAsState()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(state: SaveWalletScreenState, modifier: Modifier) {
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
|
||||
val enrollBiometricsDialog by rememberUpdatedState(newValue = state.enrollBiometricsDialog)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
|||
import com.tangem.domain.common.extensions.minimalAmount
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Basic
|
||||
|
|
@ -38,7 +39,6 @@ import com.tangem.tap.features.send.redux.*
|
|||
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
|
||||
import com.tangem.tap.features.send.redux.states.*
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -254,7 +254,7 @@ private fun sendTransaction(
|
|||
),
|
||||
)
|
||||
Analytics.sendSelectedCurrencyEvent(mainCurrencyType)
|
||||
dispatch(WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId))
|
||||
dispatch(TradeCryptoAction.FinishSelling(externalTransactionData.transactionId))
|
||||
} else {
|
||||
Analytics.send(
|
||||
Basic.TransactionSent(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import com.google.android.material.textfield.TextInputEditText
|
|||
import com.tangem.Message
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.sdk.extensions.hideSoftKeyboard
|
||||
import com.tangem.tap.common.KeyboardObserver
|
||||
import com.tangem.tap.common.analytics.events.Token
|
||||
|
|
@ -39,7 +40,6 @@ 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
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
|
||||
import com.tangem.tap.mainScope
|
||||
import com.tangem.tap.store
|
||||
|
|
@ -337,9 +337,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
|||
if (externalTransactionData == null) {
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
} else {
|
||||
store.dispatch(
|
||||
WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId),
|
||||
)
|
||||
store.dispatch(TradeCryptoAction.FinishSelling(externalTransactionData.transactionId))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,14 +6,24 @@ import androidx.activity.viewModels
|
|||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.ui.fragments.ComposeActivity
|
||||
import com.tangem.core.ui.components.SystemBarsEffect
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.screen.ComposeActivity
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
import com.tangem.tap.common.analytics.events.Chat
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
internal class SprinklrActivity : ComposeActivity() {
|
||||
|
||||
@Inject
|
||||
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
internal class SprinklrActivity : ComposeActivity<SprinklrScreenState>() {
|
||||
private val viewModel by viewModels<SprinklrViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
|
@ -25,12 +35,14 @@ internal class SprinklrActivity : ComposeActivity<SprinklrScreenState>() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
override fun provideState(): State<SprinklrScreenState> {
|
||||
return viewModel.state.collectAsState()
|
||||
}
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
val systemBarsColor = TangemTheme.colors.background.primary
|
||||
SystemBarsEffect {
|
||||
setSystemBarsColor(systemBarsColor)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(state: SprinklrScreenState, modifier: Modifier) {
|
||||
BackHandler(onBack = state.onNavigateBack)
|
||||
SprinklrScreenContent(
|
||||
modifier = modifier
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.features.tokens.impl.domain
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.derivation.DerivationStyle
|
||||
import com.tangem.common.CompletionResult
|
||||
|
|
@ -190,9 +191,15 @@ internal class DefaultTokensListInteractor(
|
|||
.mapNotNull(Currency::derivationPath)
|
||||
.map(::DerivationPath)
|
||||
|
||||
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct()
|
||||
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList()
|
||||
if (bothCandidates.isEmpty()) return null
|
||||
|
||||
currencyList.find { it is Currency.Blockchain && it.blockchain == Blockchain.Cardano }?.let { currency ->
|
||||
currency.derivationPath?.let {
|
||||
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
|
||||
}
|
||||
}
|
||||
|
||||
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
|
||||
val alreadyDerivedKeys = scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
|
||||
|
|
|
|||
|
|
@ -7,17 +7,14 @@ import androidx.compose.animation.fadeOut
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.FabPosition
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
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.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -32,9 +29,7 @@ import androidx.compose.ui.unit.TextUnit
|
|||
import androidx.compose.ui.unit.TextUnitType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.items
|
||||
import androidx.paging.compose.*
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -134,8 +129,11 @@ private fun TokensListContent(
|
|||
item { DifferentAddressesWarning() }
|
||||
}
|
||||
|
||||
items(items = tokens, key = TokenItemState::composedId) {
|
||||
it?.let { TokenItem(model = it) }
|
||||
tokens.itemKey(TokenItemState::composedId)
|
||||
tokens.itemContentType(TokenItemState::composedId)
|
||||
|
||||
items(items = tokens.itemSnapshotList.items, key = TokenItemState::composedId) {
|
||||
TokenItem(model = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.features.tokens.legacy.redux
|
||||
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.derivation.DerivationStyle
|
||||
import com.tangem.common.CompletionResult
|
||||
|
|
@ -179,9 +180,15 @@ object TokensMiddleware {
|
|||
it.blockchain.getSupportedCurves().contains(curve)
|
||||
}.mapNotNull { it.derivationPath }.map { DerivationPath(it) }
|
||||
|
||||
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct()
|
||||
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList()
|
||||
if (bothCandidates.isEmpty()) return null
|
||||
|
||||
currencyList.find { it is Currency.Blockchain && it.blockchain == Blockchain.Cardano }?.let { currency ->
|
||||
currency.derivationPath?.let {
|
||||
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
|
||||
}
|
||||
}
|
||||
|
||||
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
|
||||
val alreadyDerivedKeys: ExtendedPublicKeysMap =
|
||||
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import com.tangem.tap.features.wallet.models.Currency
|
|||
import com.tangem.tap.store
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
class CryptoCurrencyConverter : Converter<Currency, CryptoCurrency> {
|
||||
internal class CryptoCurrencyConverter : Converter<Currency, CryptoCurrency> {
|
||||
|
||||
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() }
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,16 @@ import androidx.core.os.bundleOf
|
|||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.domain.common.extensions.toCoinId
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.feature.swap.presentation.SwapFragment
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Token
|
||||
|
|
@ -27,6 +31,7 @@ import com.tangem.tap.features.wallet.redux.WalletAction
|
|||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -35,19 +40,27 @@ import kotlinx.serialization.json.Json
|
|||
import com.tangem.feature.swap.domain.models.domain.Currency as SwapCurrency
|
||||
|
||||
class TradeCryptoMiddleware {
|
||||
fun handle(state: () -> AppState?, action: WalletAction.TradeCryptoAction) {
|
||||
fun handle(state: () -> AppState?, action: TradeCryptoAction) {
|
||||
if (DemoHelper.tryHandle(state, action)) return
|
||||
|
||||
when (action) {
|
||||
is WalletAction.TradeCryptoAction.Buy -> proceedBuyAction(state, action)
|
||||
is WalletAction.TradeCryptoAction.Sell -> proceedSellAction()
|
||||
is WalletAction.TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen(action)
|
||||
is WalletAction.TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
|
||||
is WalletAction.TradeCryptoAction.Swap -> openSwap()
|
||||
is TradeCryptoAction.Buy -> proceedBuyAction(state, action)
|
||||
is TradeCryptoAction.Sell -> proceedSellAction()
|
||||
is TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen(action)
|
||||
is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
|
||||
is TradeCryptoAction.Swap -> {
|
||||
openSwap(currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency())
|
||||
}
|
||||
is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action)
|
||||
TradeCryptoAction.New.Send -> store.dispatch(WalletAction.Send())
|
||||
is TradeCryptoAction.New.Sell -> proceedNewSellAction(action)
|
||||
is TradeCryptoAction.New.Swap -> {
|
||||
openSwap(currency = action.cryptoCurrency.toSwapCurrency())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedBuyAction(state: () -> AppState?, action: WalletAction.TradeCryptoAction.Buy) {
|
||||
private fun proceedBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) {
|
||||
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
|
||||
val currency = chooseAppropriateCurrency(store.state.walletState) ?: return
|
||||
|
||||
|
|
@ -75,7 +88,7 @@ class TradeCryptoMiddleware {
|
|||
buyErc20TestnetTokens(
|
||||
card = card,
|
||||
walletManager = walletManager,
|
||||
token = currency.token,
|
||||
destinationAddress = currency.token.contractAddress,
|
||||
)
|
||||
}
|
||||
return
|
||||
|
|
@ -93,6 +106,56 @@ class TradeCryptoMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun proceedNewBuyAction(state: () -> AppState?, action: TradeCryptoAction.New.Buy) {
|
||||
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress?.defaultAddress ?: return
|
||||
|
||||
if (action.checkUserLocation && state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE) {
|
||||
store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog())
|
||||
return
|
||||
}
|
||||
|
||||
val status = action.cryptoCurrencyStatus
|
||||
val currency = status.currency
|
||||
val blockchain = Blockchain.fromId(currency.network.id.value)
|
||||
if (currency is CryptoCurrency.Token && currency.network.isTestnet) {
|
||||
scope.launch {
|
||||
val walletManager = store.state.daggerGraphState
|
||||
.get(DaggerGraphState::walletManagersFacade)
|
||||
.getOrCreateWalletManager(
|
||||
userWallet = action.userWallet,
|
||||
blockchain = blockchain,
|
||||
derivationPath = blockchain.derivationPath(
|
||||
style = action.userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(),
|
||||
),
|
||||
)
|
||||
|
||||
if (walletManager !is EthereumWalletManager) {
|
||||
store.dispatchDebugErrorNotification("Testnet tokens available only for the Ethereum")
|
||||
return@launch
|
||||
}
|
||||
|
||||
buyErc20TestnetTokens(
|
||||
card = action.userWallet.scanResponse.card,
|
||||
walletManager = walletManager,
|
||||
destinationAddress = currency.contractAddress,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val exchangeManager = store.state.globalState.exchangeManager
|
||||
exchangeManager.getUrl(
|
||||
action = CurrencyExchangeManager.Action.Buy,
|
||||
blockchain = blockchain,
|
||||
cryptoCurrencyName = currency.symbol,
|
||||
fiatCurrencyName = action.appCurrencyCode,
|
||||
walletAddress = networkAddress,
|
||||
)?.let {
|
||||
store.dispatchOpenUrl(it)
|
||||
Analytics.send(Token.Topup.ScreenOpened())
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedSellAction() {
|
||||
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
|
||||
val currency = chooseAppropriateCurrency(store.state.walletState) ?: return
|
||||
|
|
@ -115,6 +178,22 @@ class TradeCryptoMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun proceedNewSellAction(action: TradeCryptoAction.New.Sell) {
|
||||
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress?.defaultAddress ?: return
|
||||
val currency = action.cryptoCurrencyStatus.currency
|
||||
|
||||
store.state.globalState.exchangeManager.getUrl(
|
||||
action = CurrencyExchangeManager.Action.Sell,
|
||||
blockchain = Blockchain.fromId(currency.network.id.value),
|
||||
cryptoCurrencyName = currency.symbol,
|
||||
fiatCurrencyName = action.appCurrencyCode,
|
||||
walletAddress = networkAddress,
|
||||
)?.let {
|
||||
store.dispatchOpenUrl(it)
|
||||
Analytics.send(Token.Withdraw.ScreenOpened())
|
||||
}
|
||||
}
|
||||
|
||||
private fun chooseAppropriateCurrency(walletState: WalletState): Currency? {
|
||||
return if (walletState.primaryTokenData == null) {
|
||||
walletState.selectedWalletData?.currency
|
||||
|
|
@ -126,7 +205,7 @@ class TradeCryptoMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun preconfigureAndOpenSendScreen(action: WalletAction.TradeCryptoAction.SendCrypto) {
|
||||
private fun preconfigureAndOpenSendScreen(action: TradeCryptoAction.SendCrypto) {
|
||||
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
|
||||
|
||||
Analytics.send(Token.ButtonSend(AnalyticsParam.CurrencyType.Currency(selectedWalletData.currency)))
|
||||
|
|
@ -160,16 +239,44 @@ class TradeCryptoMiddleware {
|
|||
)?.let { store.dispatchOpenUrl(it) }
|
||||
}
|
||||
|
||||
private fun openSwap() {
|
||||
val currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency()
|
||||
val bundle =
|
||||
bundleOf(
|
||||
SwapFragment.CURRENCY_BUNDLE_KEY to Json.encodeToString(currency),
|
||||
SwapFragment.DERIVATION_PATH to store.state.walletState.selectedWalletData?.currency?.derivationPath,
|
||||
)
|
||||
private fun openSwap(currency: SwapCurrency?) {
|
||||
val bundle = bundleOf(
|
||||
SwapFragment.CURRENCY_BUNDLE_KEY to Json.encodeToString(currency),
|
||||
SwapFragment.DERIVATION_PATH to store.state.walletState.selectedWalletData?.currency?.derivationPath,
|
||||
)
|
||||
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle))
|
||||
}
|
||||
|
||||
private fun CryptoCurrency.toSwapCurrency(): SwapCurrency {
|
||||
val blockchain = Blockchain.fromId(network.id.value)
|
||||
|
||||
return when (this) {
|
||||
is CryptoCurrency.Coin -> {
|
||||
SwapCurrency.NativeToken(
|
||||
id = blockchain.toCoinId(),
|
||||
name = name,
|
||||
symbol = symbol,
|
||||
networkId = blockchain.toNetworkId(),
|
||||
// no need to set logoUrl for blockchain cause
|
||||
// error when form url with coinId, coinId of eth and arbitrum the same
|
||||
logoUrl = "",
|
||||
)
|
||||
}
|
||||
is CryptoCurrency.Token -> {
|
||||
SwapCurrency.NonNativeToken(
|
||||
id = id.value,
|
||||
name = name,
|
||||
symbol = symbol,
|
||||
networkId = blockchain.toNetworkId(),
|
||||
logoUrl = getIconUrl(id.value),
|
||||
contractAddress = contractAddress,
|
||||
decimalCount = decimals,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Currency.toSwapCurrency(): SwapCurrency {
|
||||
return when (this) {
|
||||
is Currency.Blockchain -> {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.tangem.core.analytics.Analytics
|
|||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.userwallets.GetCardImageUseCase
|
||||
import com.tangem.domain.wallets.legacy.lockIfLockable
|
||||
import com.tangem.tap.*
|
||||
|
|
@ -88,7 +89,7 @@ class WalletMiddleware {
|
|||
val walletState = store.state.walletState
|
||||
|
||||
when (action) {
|
||||
is WalletAction.TradeCryptoAction -> tradeCryptoMiddleware.handle(state, action)
|
||||
is TradeCryptoAction -> tradeCryptoMiddleware.handle(state, action)
|
||||
is WalletAction.Warnings -> warningsMiddleware.handle(action, globalState)
|
||||
is WalletAction.MultiWallet -> multiWalletMiddleware.handle(action, walletState)
|
||||
is WalletAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action)
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
|
|||
error = null,
|
||||
)
|
||||
}
|
||||
is WalletAction.TradeCryptoAction -> return newState
|
||||
is WalletAction.AppCurrencyAction -> {
|
||||
newState = appCurrencyReducer.reduce(action, newState)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import com.tangem.core.analytics.Analytics
|
|||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.feature.swap.api.SwapFeatureToggleManager
|
||||
import com.tangem.feature.swap.domain.SwapInteractor
|
||||
import com.tangem.sdk.extensions.dpToPx
|
||||
|
|
@ -276,9 +277,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), SafeSt
|
|||
) {
|
||||
val exchangeManager = store.state.globalState.exchangeManager
|
||||
binding.rowButtons.apply {
|
||||
onBuyClick = { store.dispatch(WalletAction.TradeCryptoAction.Buy()) }
|
||||
onSellClick = { store.dispatch(WalletAction.TradeCryptoAction.Sell) }
|
||||
onSwapClick = { store.dispatch(WalletAction.TradeCryptoAction.Swap) }
|
||||
onBuyClick = { store.dispatch(TradeCryptoAction.Buy()) }
|
||||
onSellClick = { store.dispatch(TradeCryptoAction.Sell) }
|
||||
onSwapClick = { store.dispatch(TradeCryptoAction.Swap) }
|
||||
onTradeClick = {
|
||||
store.dispatch(
|
||||
WalletAction.DialogAction.ChooseTradeActionDialog(
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import com.badoo.mvicore.modelWatcher
|
|||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.core.ui.fragments.setStatusBarColor
|
||||
import com.tangem.core.ui.extensions.setStatusBarColor
|
||||
import com.tangem.core.ui.utils.OneTouchClickListener
|
||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.feature.swap.api.SwapFeatureToggleManager
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.features.wallet.redux.models.WalletDialog
|
||||
|
|
@ -39,15 +40,15 @@ class ChooseTradeActionBottomSheetDialog(
|
|||
|
||||
dialogBtnBuy.setOnClickListener {
|
||||
dismiss()
|
||||
store.dispatch(WalletAction.TradeCryptoAction.Buy())
|
||||
store.dispatch(TradeCryptoAction.Buy())
|
||||
}
|
||||
dialogBtnSell.setOnClickListener {
|
||||
dismiss()
|
||||
store.dispatch(WalletAction.TradeCryptoAction.Sell)
|
||||
store.dispatch(TradeCryptoAction.Sell)
|
||||
}
|
||||
dialogBtnSwap.setOnClickListener {
|
||||
dismiss()
|
||||
store.dispatch(WalletAction.TradeCryptoAction.Swap)
|
||||
store.dispatch(TradeCryptoAction.Swap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import android.view.LayoutInflater
|
|||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.tap.common.analytics.events.Token
|
||||
import com.tangem.tap.common.extensions.dispatchDialogHide
|
||||
import com.tangem.tap.common.extensions.dispatchOpenUrl
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.features.wallet.redux.models.WalletDialog
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.databinding.DialogRussiansCardholdersWarningBinding
|
||||
|
|
@ -40,7 +40,7 @@ class RussianCardholdersWarningBottomSheetDialog(
|
|||
if (dialogData != null) {
|
||||
store.dispatchOpenUrl(dialogData.topUpUrl)
|
||||
} else {
|
||||
store.dispatch(WalletAction.TradeCryptoAction.Buy(checkUserLocation = false))
|
||||
store.dispatch(TradeCryptoAction.Buy(checkUserLocation = false))
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.tap.common.analytics.events.Token
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.domain.model.WalletDataModel
|
||||
|
|
@ -151,9 +152,9 @@ class SingleWalletView : WalletView() {
|
|||
|
||||
val exchangeManager = store.state.globalState.exchangeManager
|
||||
binding?.rowButtons?.apply {
|
||||
onBuyClick = { store.dispatch(WalletAction.TradeCryptoAction.Buy()) }
|
||||
onSellClick = { store.dispatch(WalletAction.TradeCryptoAction.Sell) }
|
||||
onSwapClick = { store.dispatch(WalletAction.TradeCryptoAction.Swap) }
|
||||
onBuyClick = { store.dispatch(TradeCryptoAction.Buy()) }
|
||||
onSellClick = { store.dispatch(TradeCryptoAction.Sell) }
|
||||
onSwapClick = { store.dispatch(TradeCryptoAction.Swap) }
|
||||
onTradeClick = {
|
||||
store.dispatch(
|
||||
WalletAction.DialogAction.ChooseTradeActionDialog(
|
||||
|
|
|
|||
|
|
@ -7,35 +7,32 @@ 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.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.ui.fragments.ComposeBottomSheetFragment
|
||||
import com.tangem.core.ui.components.wallets.RenameWalletDialogContent
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.screen.ComposeBottomSheetFragment
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
import com.tangem.tap.common.analytics.events.MyWallets
|
||||
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
|
||||
import com.tangem.tap.features.walletSelector.ui.components.BiometricsDisabledWarningContent
|
||||
import com.tangem.tap.features.walletSelector.ui.components.BiometricsLockoutWarningContent
|
||||
import com.tangem.tap.features.walletSelector.ui.components.KeyInvalidatedWarningContent
|
||||
import com.tangem.tap.features.walletSelector.ui.components.RemoveWalletDialogContent
|
||||
import com.tangem.tap.features.walletSelector.ui.components.RenameWalletDialogContent
|
||||
import com.tangem.tap.features.walletSelector.ui.components.WalletSelectorScreenContent
|
||||
import com.tangem.tap.features.walletSelector.ui.components.*
|
||||
import com.tangem.tap.features.walletSelector.ui.model.DialogModel
|
||||
import com.tangem.tap.features.walletSelector.ui.model.WarningModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<WalletSelectorScreenState>() {
|
||||
internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment() {
|
||||
|
||||
@Inject
|
||||
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
private val viewModel by viewModels<WalletSelectorViewModel>()
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
|
@ -45,10 +42,8 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<Wa
|
|||
}
|
||||
|
||||
@Composable
|
||||
override fun provideState(): State<WalletSelectorScreenState> = viewModel.state.collectAsState()
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(state: WalletSelectorScreenState, modifier: Modifier) {
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
|
||||
val dialog by rememberUpdatedState(newValue = state.dialog)
|
||||
|
|
@ -90,7 +85,13 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<Wa
|
|||
if (dialog == null) return
|
||||
when (dialog) {
|
||||
is DialogModel.RemoveWalletDialog -> RemoveWalletDialogContent(dialog)
|
||||
is DialogModel.RenameWalletDialog -> RenameWalletDialogContent(dialog)
|
||||
is DialogModel.RenameWalletDialog -> {
|
||||
RenameWalletDialogContent(
|
||||
name = dialog.currentName,
|
||||
onConfirm = dialog.onConfirm,
|
||||
onDismiss = dialog.onDismiss,
|
||||
)
|
||||
}
|
||||
is WarningModel.BiometricsLockoutWarning -> BiometricsLockoutWarningContent(dialog)
|
||||
is WarningModel.KeyInvalidatedWarning -> KeyInvalidatedWarningContent(dialog)
|
||||
is WarningModel.BiometricsDisabledWarning -> BiometricsDisabledWarningContent(dialog)
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
package com.tangem.tap.features.walletSelector.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.AdditionalTextInputDialogParams
|
||||
import com.tangem.core.ui.components.DialogButton
|
||||
import com.tangem.core.ui.components.TextInputDialog
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.walletSelector.ui.model.DialogModel
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Composable
|
||||
internal fun RenameWalletDialogContent(dialog: DialogModel.RenameWalletDialog) {
|
||||
var value by remember {
|
||||
mutableStateOf(TextFieldValue(text = dialog.currentName))
|
||||
}
|
||||
|
||||
TextInputDialog(
|
||||
fieldValue = value,
|
||||
confirmButton = DialogButton(
|
||||
title = stringResource(id = R.string.common_ok),
|
||||
enabled = value.text.isNotEmpty() && value.text != dialog.currentName,
|
||||
onClick = { dialog.onConfirm(value.text) },
|
||||
),
|
||||
onDismissDialog = dialog.onDismiss,
|
||||
onValueChange = { newValue ->
|
||||
value = newValue
|
||||
},
|
||||
title = stringResource(R.string.user_wallet_list_rename_popup_title),
|
||||
dismissButton = DialogButton(
|
||||
title = stringResource(id = R.string.common_cancel),
|
||||
onClick = dialog.onDismiss,
|
||||
),
|
||||
textFieldParams = AdditionalTextInputDialogParams(
|
||||
label = stringResource(R.string.user_wallet_list_rename_popup_placeholder),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
private fun RenameWalletDialogContentSample(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
RenameWalletDialogContent(dialog = DialogModel.RenameWalletDialog("", {}, {}))
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun RenameWalletDialogContentPreview_Light() {
|
||||
TangemTheme {
|
||||
RenameWalletDialogContentSample()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun RenameWalletDialogContentPreview_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
RenameWalletDialogContentSample()
|
||||
}
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.features.welcome.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -10,21 +11,27 @@ import androidx.compose.material.SnackbarHostState
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.ui.components.SystemBarsEffect
|
||||
import com.tangem.core.ui.fragments.ComposeFragment
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
import com.tangem.tap.common.analytics.events.SignIn
|
||||
import com.tangem.tap.common.extensions.eraseContext
|
||||
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
|
||||
import com.tangem.tap.features.welcome.ui.components.WarningDialog
|
||||
import com.tangem.tap.features.welcome.ui.components.WelcomeScreenContent
|
||||
import com.tangem.wallet.R
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class WelcomeFragment : ComposeFragment<WelcomeScreenState>() {
|
||||
@AndroidEntryPoint
|
||||
internal class WelcomeFragment : ComposeFragment() {
|
||||
|
||||
@Inject
|
||||
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
private val viewModel by viewModels<WelcomeViewModel>()
|
||||
|
||||
|
|
@ -35,19 +42,15 @@ internal class WelcomeFragment : ComposeFragment<WelcomeScreenState>() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
override fun provideState(): State<WelcomeScreenState> {
|
||||
return viewModel.state.collectAsState()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(state: WelcomeScreenState, modifier: Modifier) {
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
|
||||
val warning by rememberUpdatedState(newValue = state.warning)
|
||||
|
||||
val backgroundColor = colorResource(id = R.color.background_primary)
|
||||
val backgroundColor = TangemTheme.colors.background.primary
|
||||
SystemBarsEffect {
|
||||
setSystemBarsColor(color = backgroundColor)
|
||||
setSystemBarsColor(backgroundColor)
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
|
|
@ -56,7 +59,8 @@ internal class WelcomeFragment : ComposeFragment<WelcomeScreenState>() {
|
|||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.systemBarsPadding(),
|
||||
.systemBarsPadding()
|
||||
.background(backgroundColor),
|
||||
) {
|
||||
WelcomeScreenContent(
|
||||
showUnlockProgress = state.showUnlockWithBiometricsProgress,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
|
|||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
|
|
@ -87,17 +86,12 @@ class CurrencyExchangeManager(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletManager, token: Token) {
|
||||
suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletManager, destinationAddress: String) {
|
||||
walletManager.safeUpdate()
|
||||
|
||||
val amountToSend = Amount(walletManager.wallet.blockchain)
|
||||
val destinationAddress = token.contractAddress
|
||||
|
||||
val feeResult = walletManager.getFee(
|
||||
amountToSend,
|
||||
destinationAddress,
|
||||
) as? Result.Success ?: return
|
||||
|
||||
val feeResult = walletManager.getFee(amountToSend, destinationAddress) as? Result.Success ?: return
|
||||
val fee = when (val feeForTx = feeResult.data) {
|
||||
is TransactionFee.Choosable -> feeForTx.minimum
|
||||
is TransactionFee.Single -> feeForTx.normal
|
||||
|
|
@ -106,8 +100,6 @@ suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletMa
|
|||
val coinValue = walletManager.wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO
|
||||
if (coinValue < fee.amount.value) return
|
||||
|
||||
val transaction = walletManager.createTransaction(amountToSend, fee, destinationAddress)
|
||||
|
||||
val signer = TangemSigner(
|
||||
card = card,
|
||||
tangemSdk = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk,
|
||||
|
|
@ -121,5 +113,13 @@ suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletMa
|
|||
),
|
||||
)
|
||||
}
|
||||
walletManager.send(transaction, signer)
|
||||
|
||||
walletManager.send(
|
||||
transactionData = walletManager.createTransaction(
|
||||
amount = amountToSend,
|
||||
fee = fee,
|
||||
destination = destinationAddress,
|
||||
),
|
||||
signer = signer,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
package com.tangem.tap.proxy
|
||||
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.core.navigation.NavigationStateHolder
|
||||
import com.tangem.core.navigation.ReduxNavController
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.legacy.WalletsStateHolder
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
|
|
@ -14,6 +16,7 @@ import com.tangem.tap.domain.walletStores.WalletStoresManager
|
|||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Store
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -21,7 +24,7 @@ import javax.inject.Inject
|
|||
* Holds objects from old modules, that missing in DI graph.
|
||||
* Object sets manually to use in new modules and [AppStateHolder] proxies its to DI.
|
||||
*/
|
||||
class AppStateHolder @Inject constructor() : WalletsStateHolder, NavigationStateHolder {
|
||||
class AppStateHolder @Inject constructor() : WalletsStateHolder, ReduxNavController, ReduxStateHolder {
|
||||
|
||||
override var userWalletsListManager: UserWalletsListManager? = null
|
||||
set(value) {
|
||||
|
|
@ -50,4 +53,10 @@ class AppStateHolder @Inject constructor() : WalletsStateHolder, NavigationState
|
|||
override fun navigate(action: NavigationAction) {
|
||||
mainStore?.dispatch(action)
|
||||
}
|
||||
|
||||
override fun getBackStack(): List<AppScreen> = mainStore?.state?.navigationState?.backStack.orEmpty()
|
||||
|
||||
override fun dispatch(action: Action) {
|
||||
mainStore?.dispatch(action)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.proxy
|
||||
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.common.CompletionResult
|
||||
|
|
@ -29,6 +30,7 @@ import com.tangem.tap.scope
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import com.tangem.tap.features.wallet.models.Currency as WalletModelCurrency
|
||||
|
||||
class DerivationManagerImpl(
|
||||
private val appStateHolder: AppStateHolder,
|
||||
|
|
@ -50,7 +52,7 @@ class DerivationManagerImpl(
|
|||
val scanResponse = appStateHolder.scanResponse
|
||||
if (scanResponse != null) {
|
||||
val blockchainNetwork = BlockchainNetwork(blockchain, scanResponse.derivationStyleProvider)
|
||||
val appCurrency = com.tangem.tap.features.wallet.models.Currency.fromBlockchainNetwork(
|
||||
val appCurrency = WalletModelCurrency.fromBlockchainNetwork(
|
||||
blockchainNetwork,
|
||||
appToken,
|
||||
)
|
||||
|
|
@ -90,7 +92,7 @@ class DerivationManagerImpl(
|
|||
|
||||
private fun deriveMissingBlockchains(
|
||||
scanResponse: ScanResponse,
|
||||
currencyList: List<com.tangem.tap.features.wallet.models.Currency>,
|
||||
currencyList: List<WalletModelCurrency>,
|
||||
onSuccess: (ScanResponse) -> Unit,
|
||||
onFailure: (Exception) -> Unit,
|
||||
) {
|
||||
|
|
@ -157,7 +159,7 @@ class DerivationManagerImpl(
|
|||
private fun getDerivations(
|
||||
curve: EllipticCurve,
|
||||
scanResponse: ScanResponse,
|
||||
currencyList: List<com.tangem.tap.features.wallet.models.Currency>,
|
||||
currencyList: List<WalletModelCurrency>,
|
||||
): DerivationData? {
|
||||
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
|
||||
|
||||
|
|
@ -171,7 +173,7 @@ class DerivationManagerImpl(
|
|||
it.blockchain.getSupportedCurves().contains(curve)
|
||||
}.mapNotNull { it.derivationPath }.map { DerivationPath(it) }
|
||||
|
||||
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct()
|
||||
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList()
|
||||
if (bothCandidates.isEmpty()) return null
|
||||
|
||||
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
|
||||
|
|
@ -182,6 +184,13 @@ class DerivationManagerImpl(
|
|||
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
|
||||
if (toDerive.isEmpty()) return null
|
||||
|
||||
currencyList.find { it is WalletModelCurrency.Blockchain && it.blockchain == Blockchain.Cardano }
|
||||
?.let { currency ->
|
||||
currency.derivationPath?.let {
|
||||
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
|
||||
}
|
||||
}
|
||||
|
||||
return DerivationData(
|
||||
derivations = mapKeyOfWalletPublicKey to toDerive,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
|||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.card.ScanCardUseCase
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.features.tester.api.TesterRouter
|
||||
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
|
||||
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
|
||||
|
|
@ -33,6 +34,7 @@ data class DaggerGraphState(
|
|||
val scanCardProcessor: ScanCardProcessor? = null,
|
||||
val cardSdkConfigRepository: CardSdkConfigRepository? = null,
|
||||
val appCurrencyRepository: AppCurrencyRepository? = null,
|
||||
val walletManagersFacade: WalletManagersFacade? = null,
|
||||
) : StateType {
|
||||
|
||||
inline fun <reified T> get(getDependency: DaggerGraphState.() -> T?): T {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="AppTheme" parent="BaseAppTheme">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -37,5 +37,8 @@ fun createNetworkLoggingInterceptor(): Interceptor {
|
|||
return LoggingInterceptor.Builder()
|
||||
.setLevel(Level.BODY)
|
||||
.log(Log.VERBOSE)
|
||||
.tag(NETWORK_LOGS_TAG)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
private const val NETWORK_LOGS_TAG = "NetworkLogs"
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
package com.tangem.datasource.local.appcurrency
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
// TODO: Will be implemented in [REDACTED_TASK_KEY] task
|
||||
internal class MockSelectedAppCurrencyStore : SelectedAppCurrencyStore {
|
||||
|
||||
override fun get(): Flow<CurrenciesResponse.Currency> {
|
||||
return flowOf(
|
||||
CurrenciesResponse.Currency(
|
||||
id = "usd",
|
||||
code = "USD",
|
||||
name = "US Dollar",
|
||||
unit = "$",
|
||||
type = "fiat",
|
||||
rateBTC = "",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun store(item: CurrenciesResponse.Currency) {
|
||||
/* no-op */
|
||||
}
|
||||
}
|
||||
|
|
@ -7,5 +7,9 @@ interface SelectedAppCurrencyStore {
|
|||
|
||||
fun get(): Flow<CurrenciesResponse.Currency>
|
||||
|
||||
suspend fun getSyncOrNull(): CurrenciesResponse.Currency?
|
||||
|
||||
suspend fun store(item: CurrenciesResponse.Currency)
|
||||
|
||||
suspend fun isEmpty(): Boolean
|
||||
}
|
||||
|
|
@ -7,4 +7,8 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStore
|
|||
|
||||
internal class DefaultSelectedAppCurrencyStore(
|
||||
dataStore: StringKeyDataStore<CurrenciesResponse.Currency>,
|
||||
) : SelectedAppCurrencyStore, KeylessDataStoreDecorator<CurrenciesResponse.Currency>(dataStore)
|
||||
) : SelectedAppCurrencyStore, KeylessDataStoreDecorator<CurrenciesResponse.Currency>(dataStore) {
|
||||
override suspend fun isEmpty(): Boolean {
|
||||
return getSyncOrNull() == null
|
||||
}
|
||||
}
|
||||
|
|
@ -10,15 +10,15 @@ internal abstract class KeylessDataStoreDecorator<Value : Any>(
|
|||
return STRING_KEY
|
||||
}
|
||||
|
||||
fun get(): Flow<Value> {
|
||||
open fun get(): Flow<Value> {
|
||||
return get(Unit)
|
||||
}
|
||||
|
||||
suspend fun getSyncOrNull(): Value? {
|
||||
open suspend fun getSyncOrNull(): Value? {
|
||||
return getSyncOrNull(Unit)
|
||||
}
|
||||
|
||||
suspend fun store(item: Value) {
|
||||
open suspend fun store(item: Value) {
|
||||
store(Unit, item)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
package com.tangem.core.navigation
|
||||
|
||||
/**
|
||||
* Navigation state holder
|
||||
* Navigation controller that based on redux actions
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface NavigationStateHolder {
|
||||
interface ReduxNavController {
|
||||
|
||||
/** Navigate by [action] */
|
||||
fun navigate(action: NavigationAction)
|
||||
|
||||
fun getBackStack(): List<AppScreen>
|
||||
}
|
||||
|
|
@ -95,6 +95,7 @@
|
|||
<string name="common_receive">Получить</string>
|
||||
<string name="common_reject">Отклонить</string>
|
||||
<string name="common_reload">Перезагрузить</string>
|
||||
<string name="common_rename">Переименовать</string>
|
||||
<string name="common_reset">Сбросить</string>
|
||||
<string name="common_save_changes">Сохранить изменения</string>
|
||||
<string name="common_search">Искать</string>
|
||||
|
|
@ -116,6 +117,7 @@
|
|||
<string name="common_transactions">Транзакции</string>
|
||||
<string name="common_transfer">Перевод</string>
|
||||
<string name="common_understand">Я понял</string>
|
||||
<string name="common_unlock_needed">Необходима разблокировка</string>
|
||||
<string name="common_unreachable">Недоступно</string>
|
||||
<string name="common_yes">Да</string>
|
||||
<string name="contract_address_copied_message">Адрес контракта скопирован!</string>
|
||||
|
|
@ -457,6 +459,7 @@
|
|||
<string name="transaction_history_contract_address">контракт: %s</string>
|
||||
<string name="transaction_history_empty_transactions">У вас еще нет транзакций</string>
|
||||
<string name="transaction_history_error_failed_to_load">Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию.</string>
|
||||
<string name="transaction_history_multiple_addresses">Несколько адресов</string>
|
||||
<string name="transaction_history_not_supported_description">История транзакций в настоящее время не поддерживается для этого блокчейна. Но не волнуйтесь, мы работаем над этим! А пока вы можете проверить ее в обозревателе.</string>
|
||||
<string name="transaction_history_transaction_from_address">от: %s</string>
|
||||
<string name="transaction_history_transaction_to_address">на: %s</string>
|
||||
|
|
@ -472,6 +475,8 @@
|
|||
<string name="twins_recreate_toolbar">Tangem Twin</string>
|
||||
<string name="twins_recreate_warning">Это действие необратимо. У вас не будет доступа к старому кошельку.</string>
|
||||
<string name="twins_scan_twin_with_number">Приложите twin-карту с номером %s и не убирайте до окончания операции</string>
|
||||
<string name="unlock_wallet_description_full">Используйте %s или отсканируйте карту, чтобы получить доступ к своему кошельку</string>
|
||||
<string name="unlock_wallet_description_short">Используйте %s или отсканируйте карту</string>
|
||||
<string name="user_wallet_list_add_button">Добавить новый кошелек</string>
|
||||
<string name="user_wallet_list_delete_prompt">Вы уверены, что хотите удалить этот кошелек?</string>
|
||||
<string name="user_wallet_list_editing_count">%d выбрано</string>
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@
|
|||
<string name="common_ok">OK</string>
|
||||
<string name="common_origin_card">主卡片</string>
|
||||
<string name="common_reject">拒絕</string>
|
||||
<string name="common_rename">重新命名</string>
|
||||
<string name="common_reset">重置</string>
|
||||
<string name="common_save_changes">保存設置</string>
|
||||
<string name="common_search">搜索</string>
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@
|
|||
<string name="common_receive">Receive</string>
|
||||
<string name="common_reject">Reject</string>
|
||||
<string name="common_reload">Reload</string>
|
||||
<string name="common_rename">Rename</string>
|
||||
<string name="common_reset">Reset</string>
|
||||
<string name="common_save_changes">Save changes</string>
|
||||
<string name="common_search">Search</string>
|
||||
|
|
@ -115,6 +116,7 @@
|
|||
<string name="common_transactions">Transactions</string>
|
||||
<string name="common_transfer">Transfer</string>
|
||||
<string name="common_understand">I understand</string>
|
||||
<string name="common_unlock_needed">Unlock needed</string>
|
||||
<string name="common_unreachable">Unreachable</string>
|
||||
<string name="common_yes">Yes</string>
|
||||
<string name="contract_address_copied_message">Contract address copied!</string>
|
||||
|
|
@ -448,6 +450,7 @@
|
|||
<string name="transaction_history_contract_address">contract: %s</string>
|
||||
<string name="transaction_history_empty_transactions">You don\'t have any transactions yet</string>
|
||||
<string name="transaction_history_error_failed_to_load">Failed to load transaction history.\nClick on reload button to update the information.</string>
|
||||
<string name="transaction_history_multiple_addresses">Multiple addresses</string>
|
||||
<string name="transaction_history_not_supported_description">Transaction history is currently not supported for this blockchain. But don\'t worry, we\'re working on it! In the meantime you can check it in the explorer.</string>
|
||||
<string name="transaction_history_transaction_from_address">from: %s</string>
|
||||
<string name="transaction_history_transaction_to_address">to: %s</string>
|
||||
|
|
@ -463,6 +466,8 @@
|
|||
<string name="twins_recreate_toolbar">Tangem Twin</string>
|
||||
<string name="twins_recreate_warning">This action is irreversible. You will not have access to the old wallet.</string>
|
||||
<string name="twins_scan_twin_with_number">Tap the twin card with number %s and do not remove until the end of the operation</string>
|
||||
<string name="unlock_wallet_description_full">Use %s or scan a card to have an access to your wallet</string>
|
||||
<string name="unlock_wallet_description_short">Use %s or scan a card</string>
|
||||
<string name="user_wallet_list_add_button">Add new wallet</string>
|
||||
<string name="user_wallet_list_delete_prompt">Are you sure you want to delete this wallet?</string>
|
||||
<string name="user_wallet_list_editing_count">%d selected</string>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,13 @@ plugins {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
/** Project - Domain */
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.appTheme.models)
|
||||
|
||||
/** Project - Core */
|
||||
implementation(projects.core.res)
|
||||
|
||||
/** AndroidX libraries */
|
||||
implementation(deps.androidx.fragment.ktx)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
|
|
@ -22,6 +29,4 @@ dependencies {
|
|||
implementation(deps.material)
|
||||
implementation(deps.compose.shimmer)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
|
||||
implementation(project(":core:res"))
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package com.tangem.core.ui.components
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=281-248&mode=design&t=bXqehWPHyATKcZEW-4)
|
||||
* */
|
||||
@Composable
|
||||
fun SimpleSettingsRow(
|
||||
title: String,
|
||||
@DrawableRes icon: Int,
|
||||
onItemsClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
subtitle: String? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.height(TangemTheme.dimens.size56)
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
onClick = {
|
||||
if (enabled) {
|
||||
onItemsClick()
|
||||
}
|
||||
},
|
||||
),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val textColor: Color by animateColorAsState(
|
||||
targetValue = if (enabled) {
|
||||
TangemTheme.colors.text.primary1
|
||||
} else {
|
||||
TangemTheme.colors.text.secondary
|
||||
},
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing20),
|
||||
tint = textColor,
|
||||
)
|
||||
Column(modifier = Modifier.padding(end = TangemTheme.dimens.spacing20)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = textColor,
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = !subtitle.isNullOrEmpty(),
|
||||
) {
|
||||
Text(
|
||||
text = subtitle ?: "",
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.valentinilk.shimmer.shimmer
|
||||
|
||||
|
|
@ -14,13 +15,13 @@ import com.valentinilk.shimmer.shimmer
|
|||
* Rectangle shimmer item with rounded shape from DS
|
||||
*/
|
||||
@Composable
|
||||
fun RectangleShimmer(modifier: Modifier = Modifier) {
|
||||
fun RectangleShimmer(modifier: Modifier = Modifier, radius: Dp = TangemTheme.dimens.radius6) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.shimmer()
|
||||
.background(
|
||||
color = TangemTheme.colors.button.secondary,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius6),
|
||||
shape = RoundedCornerShape(size = radius),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -31,16 +32,14 @@ fun RectangleShimmer(modifier: Modifier = Modifier) {
|
|||
*/
|
||||
@Composable
|
||||
fun CircleShimmer(modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier.shimmer()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(
|
||||
color = TangemTheme.colors.button.secondary,
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = modifier
|
||||
.shimmer()
|
||||
.background(
|
||||
color = TangemTheme.colors.button.secondary,
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// region preview
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ fun HorizontalActionChips(
|
|||
) {
|
||||
items(
|
||||
items = buttons,
|
||||
key = { config -> "${config.text.hashCode()} ${config.iconResId}" },
|
||||
key = { config -> config.text.hashCode() },
|
||||
itemContent = { ActionButton(config = it) },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.core.ui.components.buttons.actions
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
|
@ -7,6 +8,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
|
@ -78,43 +80,52 @@ private fun Button(
|
|||
modifier: Modifier = Modifier,
|
||||
color: Color = TangemTheme.colors.button.secondary,
|
||||
) {
|
||||
val backgroundColor by animateColorAsState(
|
||||
targetValue = if (config.enabled) color else TangemTheme.colors.button.disabled,
|
||||
label = "Update background color",
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.heightIn(min = TangemTheme.dimens.size36)
|
||||
.clip(shape)
|
||||
.background(
|
||||
color = if (config.enabled) color else TangemTheme.colors.button.disabled,
|
||||
shape = shape,
|
||||
)
|
||||
.background(color = backgroundColor, shape = shape)
|
||||
.clickable(enabled = config.enabled, onClick = config.onClick)
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing24,
|
||||
)
|
||||
.padding(start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing24)
|
||||
.padding(vertical = TangemTheme.dimens.spacing8),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = config.iconResId),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(size = TangemTheme.dimens.size20),
|
||||
tint = when {
|
||||
val iconTint by animateColorAsState(
|
||||
targetValue = when {
|
||||
!config.enabled -> TangemTheme.colors.icon.informative
|
||||
config.dimContent -> TangemTheme.colors.icon.secondary
|
||||
else -> TangemTheme.colors.icon.primary1
|
||||
},
|
||||
label = "Update tint color",
|
||||
)
|
||||
|
||||
Icon(
|
||||
painter = painterResource(id = config.iconResId),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(size = TangemTheme.dimens.size20),
|
||||
tint = iconTint,
|
||||
)
|
||||
|
||||
SpacerW8()
|
||||
|
||||
Text(
|
||||
text = config.text.resolveReference(),
|
||||
color = when {
|
||||
val textColor by animateColorAsState(
|
||||
targetValue = when {
|
||||
!config.enabled -> TangemTheme.colors.text.disabled
|
||||
config.dimContent -> TangemTheme.colors.text.secondary
|
||||
else -> TangemTheme.colors.text.primary1
|
||||
},
|
||||
label = "Update text color",
|
||||
)
|
||||
|
||||
Text(
|
||||
text = config.text.resolveReference(),
|
||||
color = textColor,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.button,
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ private fun LoadingContent() {
|
|||
RectangleShimmer(
|
||||
modifier = Modifier.size(
|
||||
width = TangemTheme.dimens.size158,
|
||||
height = TangemTheme.dimens.size20,
|
||||
height = TangemTheme.dimens.size18,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.compose.foundation.Image
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -22,6 +23,8 @@ import com.tangem.core.ui.R
|
|||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import java.util.UUID
|
||||
|
||||
|
|
@ -158,6 +161,18 @@ private fun Icon(state: TransactionState, modifier: Modifier = Modifier) {
|
|||
is TransactionState.Loading -> {
|
||||
CircleShimmer(modifier = modifier.size(TangemTheme.dimens.size40))
|
||||
}
|
||||
is TransactionState.Locked -> {
|
||||
Box(modifier = modifier.size(TangemTheme.dimens.size40)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(
|
||||
color = TangemTheme.colors.button.secondary,
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -206,6 +221,11 @@ private fun Title(state: TransactionState, modifier: Modifier = Modifier) {
|
|||
modifier = modifier.size(width = TangemTheme.dimens.size70, height = TangemTheme.dimens.size12),
|
||||
)
|
||||
}
|
||||
is TransactionState.Locked -> {
|
||||
LockedContent(
|
||||
modifier = modifier.size(width = TangemTheme.dimens.size70, height = TangemTheme.dimens.size12),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -219,7 +239,7 @@ private fun Subtitle(state: TransactionState, modifier: Modifier = Modifier) {
|
|||
is TransactionState.Send,
|
||||
-> stringResource(
|
||||
id = R.string.transaction_history_transaction_to_address,
|
||||
state.address,
|
||||
state.address.resolveReference(),
|
||||
)
|
||||
is TransactionState.Receiving,
|
||||
is TransactionState.Receive,
|
||||
|
|
@ -227,13 +247,13 @@ private fun Subtitle(state: TransactionState, modifier: Modifier = Modifier) {
|
|||
is TransactionState.Approved,
|
||||
-> stringResource(
|
||||
id = R.string.transaction_history_transaction_from_address,
|
||||
state.address,
|
||||
state.address.resolveReference(),
|
||||
)
|
||||
is TransactionState.Swapping,
|
||||
is TransactionState.Swapped,
|
||||
-> stringResource(
|
||||
id = R.string.transaction_history_contract_address,
|
||||
state.address,
|
||||
state.address.resolveReference(),
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
|
|
@ -247,6 +267,11 @@ private fun Subtitle(state: TransactionState, modifier: Modifier = Modifier) {
|
|||
modifier = modifier.size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12),
|
||||
)
|
||||
}
|
||||
is TransactionState.Locked -> {
|
||||
LockedContent(
|
||||
modifier = modifier.size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -267,6 +292,11 @@ private fun Amount(state: TransactionState, modifier: Modifier = Modifier) {
|
|||
modifier = modifier.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12),
|
||||
)
|
||||
}
|
||||
is TransactionState.Locked -> {
|
||||
LockedContent(
|
||||
modifier = modifier.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -287,9 +317,24 @@ private fun Timestamp(state: TransactionState, modifier: Modifier = Modifier) {
|
|||
modifier = modifier.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12),
|
||||
)
|
||||
}
|
||||
is TransactionState.Locked -> {
|
||||
LockedContent(
|
||||
modifier = modifier.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LockedContent(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier.background(
|
||||
color = TangemTheme.colors.field.primary,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius6),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_TransactionItem_LightTheme(
|
||||
|
|
@ -314,49 +359,49 @@ private class TransactionItemStateProvider : CollectionPreviewParameterProvider<
|
|||
collection = listOf(
|
||||
TransactionState.Sending(
|
||||
txHash = UUID.randomUUID().toString(),
|
||||
address = "33BddS...ga2B",
|
||||
address = TextReference.Str("33BddS...ga2B"),
|
||||
amount = "-0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
),
|
||||
TransactionState.Receiving(
|
||||
txHash = UUID.randomUUID().toString(),
|
||||
address = "33BddS...ga2B",
|
||||
address = TextReference.Str("33BddS...ga2B"),
|
||||
amount = "+0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
),
|
||||
TransactionState.Approving(
|
||||
txHash = UUID.randomUUID().toString(),
|
||||
address = "33BddS...ga2B",
|
||||
address = TextReference.Str("33BddS...ga2B"),
|
||||
amount = "+0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
),
|
||||
TransactionState.Swapping(
|
||||
txHash = UUID.randomUUID().toString(),
|
||||
address = "33BddS...ga2B",
|
||||
address = TextReference.Str("33BddS...ga2B"),
|
||||
amount = "+0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
),
|
||||
TransactionState.Send(
|
||||
txHash = UUID.randomUUID().toString(),
|
||||
address = "33BddS...ga2B",
|
||||
address = TextReference.Str("33BddS...ga2B"),
|
||||
amount = "-0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
),
|
||||
TransactionState.Receive(
|
||||
txHash = UUID.randomUUID().toString(),
|
||||
address = "33BddS...ga2B",
|
||||
address = TextReference.Str("33BddS...ga2B"),
|
||||
amount = "+0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
),
|
||||
TransactionState.Approved(
|
||||
txHash = UUID.randomUUID().toString(),
|
||||
address = "33BddS...ga2B",
|
||||
address = TextReference.Str("33BddS...ga2B"),
|
||||
amount = "+0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
),
|
||||
TransactionState.Swapped(
|
||||
txHash = UUID.randomUUID().toString(),
|
||||
address = "33BddS...ga2B",
|
||||
address = TextReference.Str("33BddS...ga2B"),
|
||||
amount = "+0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
),
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.itemsIndexed
|
||||
import androidx.paging.compose.itemContentType
|
||||
import androidx.paging.compose.itemKey
|
||||
import com.tangem.core.ui.components.transactions.empty.EmptyTransactionBlock
|
||||
import com.tangem.core.ui.components.transactions.empty.EmptyTransactionsBlockState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
|
|
@ -26,7 +28,7 @@ fun LazyListScope.txHistoryItems(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (state) {
|
||||
is TxHistoryState.ContentState -> {
|
||||
is TxHistoryState.Content -> {
|
||||
contentItems(
|
||||
txHistoryItems = requireNotNull(txHistoryItems),
|
||||
modifier = modifier,
|
||||
|
|
@ -58,8 +60,18 @@ private fun LazyListScope.contentItems(
|
|||
txHistoryItems: LazyPagingItems<TxHistoryState.TxHistoryItemState>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
txHistoryItems.itemKey { item ->
|
||||
when (item) {
|
||||
is TxHistoryState.TxHistoryItemState.GroupTitle -> item.title
|
||||
is TxHistoryState.TxHistoryItemState.Title -> item.onExploreClick.hashCode()
|
||||
is TxHistoryState.TxHistoryItemState.Transaction -> item.state.txHash
|
||||
}
|
||||
}
|
||||
|
||||
txHistoryItems.itemContentType { it::class.java }
|
||||
|
||||
itemsIndexed(
|
||||
items = txHistoryItems,
|
||||
items = txHistoryItems.itemSnapshotList.items,
|
||||
key = { _, item ->
|
||||
when (item) {
|
||||
is TxHistoryState.TxHistoryItemState.GroupTitle -> item.title
|
||||
|
|
@ -68,8 +80,6 @@ private fun LazyListScope.contentItems(
|
|||
}
|
||||
},
|
||||
) { index, item ->
|
||||
if (item == null) return@itemsIndexed
|
||||
|
||||
TxHistoryListItem(
|
||||
state = item,
|
||||
modifier = modifier
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.core.ui.components.transactions.intents
|
||||
|
||||
interface TxHistoryClickIntents {
|
||||
|
||||
fun onBuyClick()
|
||||
|
||||
fun onReloadClick()
|
||||
|
||||
fun onExploreClick()
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.core.ui.components.transactions.state
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
/**
|
||||
* Transaction component state
|
||||
*
|
||||
|
|
@ -20,14 +22,14 @@ sealed interface TransactionState {
|
|||
*/
|
||||
sealed class Content(
|
||||
override val txHash: String,
|
||||
open val address: String,
|
||||
open val address: TextReference,
|
||||
open val amount: String,
|
||||
open val timestamp: String,
|
||||
) : TransactionState {
|
||||
|
||||
fun copySealed(
|
||||
txHash: String = this.txHash,
|
||||
address: String = this.address,
|
||||
address: TextReference = this.address,
|
||||
amount: String = this.amount,
|
||||
timestamp: String = this.timestamp,
|
||||
): Content {
|
||||
|
|
@ -54,7 +56,7 @@ sealed interface TransactionState {
|
|||
*/
|
||||
sealed class ProcessedTransactionContent(
|
||||
override val txHash: String,
|
||||
override val address: String,
|
||||
override val address: TextReference,
|
||||
override val amount: String,
|
||||
override val timestamp: String,
|
||||
) : Content(txHash, address, amount, timestamp)
|
||||
|
|
@ -69,7 +71,7 @@ sealed interface TransactionState {
|
|||
*/
|
||||
sealed class CompletedTransactionContent(
|
||||
override val txHash: String,
|
||||
override val address: String,
|
||||
override val address: TextReference,
|
||||
override val amount: String,
|
||||
override val timestamp: String,
|
||||
) : Content(txHash, address, amount, timestamp)
|
||||
|
|
@ -84,7 +86,7 @@ sealed interface TransactionState {
|
|||
*/
|
||||
data class Sending(
|
||||
override val txHash: String,
|
||||
override val address: String,
|
||||
override val address: TextReference,
|
||||
override val amount: String,
|
||||
override val timestamp: String,
|
||||
) : ProcessedTransactionContent(txHash, address, amount, timestamp)
|
||||
|
|
@ -99,7 +101,7 @@ sealed interface TransactionState {
|
|||
*/
|
||||
data class Receiving(
|
||||
override val txHash: String,
|
||||
override val address: String,
|
||||
override val address: TextReference,
|
||||
override val amount: String,
|
||||
override val timestamp: String,
|
||||
) : ProcessedTransactionContent(txHash, address, amount, timestamp)
|
||||
|
|
@ -114,7 +116,7 @@ sealed interface TransactionState {
|
|||
*/
|
||||
data class Approving(
|
||||
override val txHash: String,
|
||||
override val address: String,
|
||||
override val address: TextReference,
|
||||
override val amount: String,
|
||||
override val timestamp: String,
|
||||
) : ProcessedTransactionContent(txHash, address, amount, timestamp)
|
||||
|
|
@ -129,7 +131,7 @@ sealed interface TransactionState {
|
|||
*/
|
||||
data class Swapping(
|
||||
override val txHash: String,
|
||||
override val address: String,
|
||||
override val address: TextReference,
|
||||
override val amount: String,
|
||||
override val timestamp: String,
|
||||
) : ProcessedTransactionContent(txHash, address, amount, timestamp)
|
||||
|
|
@ -144,7 +146,7 @@ sealed interface TransactionState {
|
|||
*/
|
||||
data class Send(
|
||||
override val txHash: String,
|
||||
override val address: String,
|
||||
override val address: TextReference,
|
||||
override val amount: String,
|
||||
override val timestamp: String,
|
||||
) : CompletedTransactionContent(txHash, address, amount, timestamp)
|
||||
|
|
@ -159,7 +161,7 @@ sealed interface TransactionState {
|
|||
*/
|
||||
data class Receive(
|
||||
override val txHash: String,
|
||||
override val address: String,
|
||||
override val address: TextReference,
|
||||
override val amount: String,
|
||||
override val timestamp: String,
|
||||
) : CompletedTransactionContent(txHash, address, amount, timestamp)
|
||||
|
|
@ -174,7 +176,7 @@ sealed interface TransactionState {
|
|||
*/
|
||||
data class Approved(
|
||||
override val txHash: String,
|
||||
override val address: String,
|
||||
override val address: TextReference,
|
||||
override val amount: String,
|
||||
override val timestamp: String,
|
||||
) : CompletedTransactionContent(txHash, address, amount, timestamp)
|
||||
|
|
@ -189,7 +191,7 @@ sealed interface TransactionState {
|
|||
*/
|
||||
data class Swapped(
|
||||
override val txHash: String,
|
||||
override val address: String,
|
||||
override val address: TextReference,
|
||||
override val amount: String,
|
||||
override val timestamp: String,
|
||||
) : CompletedTransactionContent(txHash, address, amount, timestamp)
|
||||
|
|
@ -200,4 +202,11 @@ sealed interface TransactionState {
|
|||
* @property txHash transaction hash
|
||||
*/
|
||||
data class Loading(override val txHash: String) : TransactionState
|
||||
|
||||
/**
|
||||
* Locked state
|
||||
*
|
||||
* @property txHash transaction hash
|
||||
*/
|
||||
data class Locked(override val txHash: String) : TransactionState
|
||||
}
|
||||
|
|
@ -1,77 +1,17 @@
|
|||
package com.tangem.core.ui.components.transactions.state
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.core.ui.components.wallet.WalletLockedContentState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
/**
|
||||
* Wallet transaction history state
|
||||
*/
|
||||
/** Wallet transaction history state */
|
||||
sealed interface TxHistoryState {
|
||||
|
||||
/**
|
||||
* Wallet transaction history state with content
|
||||
*
|
||||
* @property items content items
|
||||
* @property contentItems content items
|
||||
*/
|
||||
sealed class ContentState(open val items: Flow<PagingData<TxHistoryItemState>>) : TxHistoryState
|
||||
|
||||
/**
|
||||
* Loading state
|
||||
*
|
||||
* @property onExploreClick lambda be invoke when explore button was clicked
|
||||
*/
|
||||
data class Loading(val onExploreClick: () -> Unit) : ContentState(
|
||||
items = flowOf(
|
||||
PagingData.from(
|
||||
listOf(
|
||||
TxHistoryItemState.Title(onExploreClick = onExploreClick),
|
||||
TxHistoryItemState.Transaction(state = TransactionState.Loading(txHash = LOADING_TX_HASH)),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
/**
|
||||
* Wallet transaction history state with loading transactions
|
||||
*
|
||||
* @property itemsCount count of loading transactions
|
||||
*/
|
||||
data class ContentWithLoadingItems(val itemsCount: Int) : ContentState(
|
||||
items = flowOf(
|
||||
value = PagingData.from(
|
||||
data = buildList(capacity = itemsCount) {
|
||||
add(TxHistoryItemState.Transaction(state = TransactionState.Loading(txHash = LOADING_TX_HASH)))
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
/**
|
||||
* Wallet transaction history state with content
|
||||
*
|
||||
* @property items content items
|
||||
*/
|
||||
data class Content(override val items: Flow<PagingData<TxHistoryItemState>>) : ContentState(items)
|
||||
|
||||
/**
|
||||
* Locked state
|
||||
*
|
||||
* @property onExploreClick lambda be invoke when explore button was clicked
|
||||
*/
|
||||
data class Locked(val onExploreClick: () -> Unit) :
|
||||
ContentState(
|
||||
items = flowOf(
|
||||
PagingData.from(
|
||||
listOf(
|
||||
TxHistoryItemState.Title(onExploreClick = onExploreClick),
|
||||
TxHistoryItemState.Transaction(state = TransactionState.Loading(txHash = LOADING_TX_HASH)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
WalletLockedContentState
|
||||
data class Content(val contentItems: MutableStateFlow<PagingData<TxHistoryItemState>>) : TxHistoryState
|
||||
|
||||
/**
|
||||
* Empty state
|
||||
|
|
@ -119,7 +59,18 @@ sealed interface TxHistoryState {
|
|||
data class Transaction(val state: TransactionState) : TxHistoryItemState
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val LOADING_TX_HASH = "LOADING_TX_HASH"
|
||||
companion object {
|
||||
private const val LOADING_TX_HASH = "LOADING_TX_HASH"
|
||||
|
||||
fun getDefaultLoadingTransactions(onExploreClick: () -> Unit): PagingData<TxHistoryItemState> {
|
||||
return PagingData.from(
|
||||
data = listOf(
|
||||
TxHistoryItemState.Title(onExploreClick = onExploreClick),
|
||||
TxHistoryItemState.Transaction(
|
||||
state = TransactionState.Loading(txHash = LOADING_TX_HASH),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package com.tangem.core.ui.components.wallet
|
||||
|
||||
/**
|
||||
* Wallet locked content state.
|
||||
* It allows to divide the locked content of multi-currency and single-currency wallets.
|
||||
*/
|
||||
interface WalletLockedContentState
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.core.ui.components.wallets
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.AdditionalTextInputDialogParams
|
||||
import com.tangem.core.ui.components.DialogButton
|
||||
import com.tangem.core.ui.components.TextInputDialog
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Rename a wallet dialog
|
||||
*
|
||||
* @param name wallet name
|
||||
* @param onConfirm lambda be invoked when Confirm button is clicked
|
||||
* @param onDismiss lambda be invoked when dialog is dismissed
|
||||
*/
|
||||
@Composable
|
||||
fun RenameWalletDialogContent(name: String, onConfirm: (newName: String) -> Unit, onDismiss: () -> Unit) {
|
||||
var value by remember { mutableStateOf(TextFieldValue(text = name)) }
|
||||
|
||||
TextInputDialog(
|
||||
fieldValue = value,
|
||||
confirmButton = DialogButton(
|
||||
title = stringResource(id = R.string.common_ok),
|
||||
enabled = value.text.isNotEmpty() && value.text != name,
|
||||
onClick = { onConfirm(value.text) },
|
||||
),
|
||||
onDismissDialog = onDismiss,
|
||||
onValueChange = { value = it },
|
||||
title = stringResource(R.string.user_wallet_list_rename_popup_title),
|
||||
dismissButton = DialogButton(title = stringResource(id = R.string.common_cancel), onClick = onDismiss),
|
||||
textFieldParams = AdditionalTextInputDialogParams(
|
||||
label = stringResource(R.string.user_wallet_list_rename_popup_placeholder),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun RenameWalletDialogContentPreview_Light() {
|
||||
TangemTheme(isDark = false) {
|
||||
RenameWalletDialogContent(name = "", onConfirm = {}, onDismiss = {})
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun RenameWalletDialogContentPreview_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
RenameWalletDialogContent(name = "", onConfirm = {}, onDismiss = {})
|
||||
}
|
||||
}
|
||||
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.core.ui.event
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.NonRestartableComposable
|
||||
|
||||
/**
|
||||
* A Composable function that reacts to a given [StateEvent], executing the provided action only once when the event
|
||||
* is triggered.
|
||||
*
|
||||
* @param event The [StateEvent] to listen to.
|
||||
* @param onTrigger The action to execute when the event is triggered.
|
||||
*/
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
fun EventEffect(event: StateEvent, onTrigger: suspend () -> Unit) {
|
||||
LaunchedEffect(event) {
|
||||
if (event is StateEvent.Triggered) {
|
||||
onTrigger()
|
||||
event.consume()
|
||||
}
|
||||
}
|
||||
}
|
||||
51
core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt
Normal file
51
core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.core.ui.event
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
/**
|
||||
* Represents compose state event, which can be consumed or triggered.
|
||||
*
|
||||
* This is especially useful for handling one-off UI events like showing snack bars or navigation which should not be
|
||||
* re-triggered on recompositions or state changes.
|
||||
*/
|
||||
@Immutable
|
||||
sealed class StateEvent {
|
||||
|
||||
/** Defines the action to be executed when the event is consumed. */
|
||||
protected abstract val onConsume: () -> Unit
|
||||
|
||||
/**
|
||||
* Represents an already consumed state event.
|
||||
* Events of this type will not trigger any further actions.
|
||||
*/
|
||||
object Consumed : StateEvent() {
|
||||
override val onConsume: () -> Unit = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a state event that has been triggered but not yet consumed.
|
||||
*
|
||||
* @property onConsume The action to be executed when the event is consumed.
|
||||
*/
|
||||
data class Triggered(override val onConsume: () -> Unit) : StateEvent()
|
||||
|
||||
/**
|
||||
* Consumes the event, triggering any associated action.
|
||||
*/
|
||||
fun consume() {
|
||||
onConsume()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [StateEvent.Triggered] instance.
|
||||
*
|
||||
* @param onConsume The action to be executed when the event is consumed.
|
||||
* @return A triggered state event.
|
||||
*/
|
||||
fun triggered(onConsume: () -> Unit): StateEvent.Triggered = StateEvent.Triggered(onConsume)
|
||||
|
||||
/**
|
||||
* Represents a statically defined [StateEvent.Consumed] event.
|
||||
*/
|
||||
val consumed: StateEvent.Consumed = StateEvent.Consumed
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.core.ui.extensions
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
|
||||
/**
|
||||
* Retrieves the resource ID for the network badge of a [CryptoCurrency].
|
||||
*
|
||||
* This property provides a way to fetch the appropriate drawable resource ID
|
||||
* for the network badge of a given cryptocurrency. For coins, this will typically
|
||||
* return null as they do not have network badges, while tokens will fetch the icon
|
||||
* based on their associated network ID.
|
||||
*
|
||||
* @return Drawable resource ID for the network badge or null if the cryptocurrency is a coin.
|
||||
*/
|
||||
@get:DrawableRes
|
||||
val CryptoCurrency.networkBadgeIconResId: Int?
|
||||
get() = when (this) {
|
||||
is CryptoCurrency.Coin -> null
|
||||
is CryptoCurrency.Token -> getActiveIconRes(network.id.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the resource ID for the icon of a [CryptoCurrency].
|
||||
*
|
||||
* This property provides a way to fetch the appropriate drawable resource ID
|
||||
* for the icon of a given cryptocurrency.
|
||||
*
|
||||
* @return Drawable resource ID for the cryptocurrency icon.
|
||||
*/
|
||||
@get:DrawableRes
|
||||
val CryptoCurrency.iconResId: Int
|
||||
get() = when (this) {
|
||||
is CryptoCurrency.Coin -> {
|
||||
val rawCoinId = id.rawCurrencyId
|
||||
|
||||
if (rawCoinId != null) {
|
||||
getActiveIconResByCoinId(rawCoinId, network.id.value)
|
||||
} else {
|
||||
R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
is CryptoCurrency.Token -> R.drawable.ic_alert_24
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.core.ui.fragments
|
||||
package com.tangem.core.ui.extensions
|
||||
|
||||
import android.view.WindowManager
|
||||
import androidx.annotation.ColorRes
|
||||
|
|
@ -20,12 +20,19 @@ sealed interface TextReference {
|
|||
* Text resource id
|
||||
*
|
||||
* @property id resource id
|
||||
* @property formatArgs arguments
|
||||
*
|
||||
* Impossible to use [kotlinx.collections.immutable.ImmutableList] because [Any] is unstable.
|
||||
* @property formatArgs arguments. Impossible to use [kotlinx.collections.immutable.ImmutableList] because [Any] is
|
||||
* unstable.
|
||||
*/
|
||||
data class Res(@StringRes val id: Int, val formatArgs: WrappedList<Any> = WrappedList(emptyList())) : TextReference
|
||||
|
||||
/**
|
||||
* Plural resource id
|
||||
*
|
||||
* @property id resource id
|
||||
* @property count count
|
||||
* @property formatArgs arguments. Impossible to use [kotlinx.collections.immutable.ImmutableList] because [Any] is
|
||||
* unstable.
|
||||
*/
|
||||
data class PluralRes(@PluralsRes val id: Int, val count: Int, val formatArgs: WrappedList<Any>) : TextReference
|
||||
|
||||
/**
|
||||
|
|
@ -34,9 +41,16 @@ sealed interface TextReference {
|
|||
* @property value value
|
||||
*/
|
||||
data class Str(val value: String) : TextReference
|
||||
|
||||
/**
|
||||
* Combined reference. It concatenates all [refs].
|
||||
*
|
||||
* @see [TextReference.plus] method
|
||||
*/
|
||||
data class Combined(val refs: WrappedList<TextReference>) : TextReference
|
||||
}
|
||||
|
||||
/** Get text */
|
||||
/** Resolve [TextReference] as [String] */
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun TextReference.resolveReference(): String {
|
||||
|
|
@ -44,5 +58,23 @@ fun TextReference.resolveReference(): String {
|
|||
is TextReference.Res -> stringResource(id, *formatArgs.toTypedArray())
|
||||
is TextReference.PluralRes -> pluralStringResource(id, count, *formatArgs.toTypedArray())
|
||||
is TextReference.Str -> value
|
||||
is TextReference.Combined -> {
|
||||
buildString {
|
||||
refs.forEach {
|
||||
append(it.resolveReference())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Concatenate [this] reference with [ref] */
|
||||
operator fun TextReference.plus(ref: TextReference): TextReference {
|
||||
return when (this) {
|
||||
is TextReference.Combined -> copy(refs = (refs.data + ref).toWrappedList())
|
||||
is TextReference.PluralRes,
|
||||
is TextReference.Res,
|
||||
is TextReference.Str,
|
||||
-> TextReference.Combined(refs = wrappedList(this, ref))
|
||||
}
|
||||
}
|
||||
|
|
@ -7,4 +7,8 @@ import androidx.compose.runtime.Immutable
|
|||
*/
|
||||
@JvmInline
|
||||
@Immutable
|
||||
value class WrappedList<T>(val data: List<T>) : List<T> by data
|
||||
value class WrappedList<T>(val data: List<T>) : List<T> by data
|
||||
|
||||
fun <T> List<T>.toWrappedList(): WrappedList<T> = WrappedList(data = this)
|
||||
|
||||
fun <T> wrappedList(vararg elements: T): WrappedList<T> = WrappedList(data = listOf(*elements))
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package com.tangem.core.ui.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
abstract class ComposeActivity<ScreenState> : AppCompatActivity(), ComposeScreen<ScreenState> {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(createComposeView(context = this))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
package com.tangem.core.ui.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
abstract class ComposeFragment<ScreenState> : Fragment(), ComposeScreen<ScreenState> {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return createComposeView(inflater.context)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
package com.tangem.core.ui.fragments
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import com.tangem.core.ui.components.SystemBarsEffect
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
internal interface ComposeScreen<ScreenState> {
|
||||
fun createComposeView(context: Context): View {
|
||||
return ComposeView(context).apply {
|
||||
setContent {
|
||||
TangemTheme {
|
||||
val backgroundColor = TangemTheme.colors.background.primary
|
||||
SystemBarsEffect {
|
||||
setSystemBarsColor(
|
||||
color = backgroundColor,
|
||||
)
|
||||
}
|
||||
|
||||
ScreenContent(
|
||||
state = provideState().value,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = backgroundColor),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("TopLevelComposableFunctions")
|
||||
@Composable
|
||||
fun provideState(): State<ScreenState>
|
||||
|
||||
@Suppress("TopLevelComposableFunctions")
|
||||
@Composable
|
||||
fun ScreenContent(state: ScreenState, modifier: Modifier)
|
||||
}
|
||||
|
|
@ -46,6 +46,7 @@ data class TangemDimens internal constructor(
|
|||
val size10: Dp = 10.dp,
|
||||
val size11: Dp = 11.dp,
|
||||
val size12: Dp = 12.dp,
|
||||
val size14: Dp = 14.dp,
|
||||
val size16: Dp = 16.dp,
|
||||
val size18: Dp = 18.dp,
|
||||
val size20: Dp = 20.dp,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.core.ui.screen
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
|
||||
/**
|
||||
* An abstract base class for activities that use Compose for UI rendering.
|
||||
* Extends [ComponentActivity] and implements [ComposeScreen] interface.
|
||||
*/
|
||||
abstract class ComposeActivity : ComponentActivity(), ComposeScreen {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(createComposeView(context = this))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.core.ui.fragments
|
||||
package com.tangem.core.ui.screen
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
|
@ -10,20 +9,46 @@ import androidx.annotation.FloatRange
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
abstract class ComposeBottomSheetFragment<ScreenState> : BottomSheetDialogFragment(), ComposeScreen<ScreenState> {
|
||||
/**
|
||||
* An abstract base class for bottom sheet dialogs that use Compose for UI rendering.
|
||||
* Extends [BottomSheetDialogFragment] and implements [ComposeScreen] interface.
|
||||
*/
|
||||
abstract class ComposeBottomSheetFragment : BottomSheetDialogFragment(), ComposeScreen {
|
||||
|
||||
/**
|
||||
* The initial state of the bottom sheet. Default is [BottomSheetBehavior.STATE_EXPANDED].
|
||||
*/
|
||||
open val initialBottomSheetState = BottomSheetBehavior.STATE_EXPANDED
|
||||
|
||||
/**
|
||||
* The fraction of the screen height that the bottom sheet should take when expanded.
|
||||
* Default is `null`, indicating that the height will be determined by the content.
|
||||
*/
|
||||
@FloatRange(from = 0.0, to = 1.0)
|
||||
open val expandedHeightFraction: Float? = null
|
||||
|
||||
override val screenModifier: Modifier
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
get() = Modifier
|
||||
.fillMaxWidth()
|
||||
.let {
|
||||
if (expandedHeightFraction != null) it.fillMaxHeight(expandedHeightFraction!!) else it
|
||||
}
|
||||
.background(
|
||||
color = TangemTheme.colors.background.plain,
|
||||
shape = TangemTheme.shapes.bottomSheet,
|
||||
)
|
||||
|
||||
override fun getTheme(): Int = R.style.AppTheme_TransparentBottomSheetDialog
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
|
|
@ -40,25 +65,4 @@ abstract class ComposeBottomSheetFragment<ScreenState> : BottomSheetDialogFragme
|
|||
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun createComposeView(context: Context): View {
|
||||
return ComposeView(context).apply {
|
||||
setContent {
|
||||
TangemTheme {
|
||||
ScreenContent(
|
||||
state = provideState().value,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.let {
|
||||
if (expandedHeightFraction != null) it.fillMaxHeight(expandedHeightFraction!!) else it
|
||||
}
|
||||
.background(
|
||||
color = TangemTheme.colors.background.plain,
|
||||
shape = TangemTheme.shapes.bottomSheet,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.core.ui.screen
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.transition.TransitionInflater
|
||||
import com.tangem.core.ui.R
|
||||
|
||||
/**
|
||||
* An abstract base class for fragments that use Compose for UI rendering.
|
||||
* Extends [Fragment] and implements [ComposeScreen] interface.
|
||||
*/
|
||||
abstract class ComposeFragment : Fragment(), ComposeScreen {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val isTransitionsInflated = TransitionInflater.from(requireContext()).inflateTransitions()
|
||||
|
||||
return createComposeView(inflater.context).also {
|
||||
it.isTransitionGroup = isTransitionsInflated
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates transitions for the fragment. Override this method to customize
|
||||
* enter and exit transitions for the fragment.
|
||||
*
|
||||
* @return `true` if transitions were inflated; `false` otherwise.
|
||||
*/
|
||||
protected open fun TransitionInflater.inflateTransitions(): Boolean {
|
||||
enterTransition = inflateTransition(R.transition.slide_right)
|
||||
exitTransition = inflateTransition(R.transition.fade)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.core.ui.screen
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
|
||||
/**
|
||||
* Interface representing a Compose screen with common theming and content composition properties.
|
||||
*
|
||||
* This interface defines properties and functions that allow a Compose screen to manage its theme,
|
||||
* background color, and screen modifier. It also provides a composable function to define the content
|
||||
* of the screen.
|
||||
*/
|
||||
internal interface ComposeScreen {
|
||||
|
||||
/**
|
||||
* The holder for managing the current application theme mode.
|
||||
*/
|
||||
val appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
/**
|
||||
* The screen modifier.
|
||||
*/
|
||||
val screenModifier: Modifier
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
get() = Modifier.fillMaxSize()
|
||||
|
||||
/**
|
||||
* Composable function to define the content of the screen.
|
||||
*
|
||||
* @param modifier The modifier to apply to the screen content.
|
||||
*/
|
||||
@Suppress("TopLevelComposableFunctions")
|
||||
@Composable
|
||||
fun ScreenContent(modifier: Modifier)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [ComposeView] with the defined content for the Compose screen.
|
||||
*
|
||||
* @param context The context.
|
||||
* @return A [ComposeView] instance with the defined screen content.
|
||||
*/
|
||||
internal fun ComposeScreen.createComposeView(context: Context): ComposeView {
|
||||
return ComposeView(context).apply {
|
||||
setContent {
|
||||
val appThemeMode by appThemeModeHolder.appThemeMode
|
||||
|
||||
TangemTheme(isDark = shouldUseDarkTheme(appThemeMode)) {
|
||||
ScreenContent(modifier = screenModifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the dark theme should be used based on the given [AppThemeMode].
|
||||
*
|
||||
* @param appThemeMode The application theme mode.
|
||||
* @return `true` if the dark theme should be used, `false` otherwise.
|
||||
*/
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun shouldUseDarkTheme(appThemeMode: AppThemeMode): Boolean {
|
||||
return when (appThemeMode) {
|
||||
AppThemeMode.FORCE_DARK -> true
|
||||
AppThemeMode.FORCE_LIGHT -> false
|
||||
AppThemeMode.FOLLOW_SYSTEM -> isSystemInDarkTheme()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.core.ui.theme
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
|
||||
/**
|
||||
* Representing a holder for the application theme mode.
|
||||
*/
|
||||
@Stable
|
||||
interface AppThemeModeHolder {
|
||||
|
||||
/**
|
||||
* A [State] representing the current application theme mode.
|
||||
*/
|
||||
val appThemeMode: State<AppThemeMode>
|
||||
}
|
||||
|
|
@ -12,22 +12,14 @@ object BigDecimalFormatter {
|
|||
|
||||
private const val TEMP_CURRENCY_CODE = "USD"
|
||||
|
||||
fun formatCryptoAmount(
|
||||
cryptoAmount: BigDecimal,
|
||||
cryptoCurrency: String,
|
||||
decimals: Int,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
val formatterCurrency = getCurrency(cryptoCurrency)
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = formatterCurrency
|
||||
fun formatCryptoAmount(cryptoAmount: BigDecimal, cryptoCurrency: String, decimals: Int): String {
|
||||
val formatter = NumberFormat.getNumberInstance().apply {
|
||||
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8)
|
||||
minimumFractionDigits = 2
|
||||
roundingMode = RoundingMode.DOWN
|
||||
}
|
||||
|
||||
return formatter.format(cryptoAmount)
|
||||
.replace(formatterCurrency.getSymbol(locale), cryptoCurrency)
|
||||
return formatter.format(cryptoAmount) + "\u2009$cryptoCurrency"
|
||||
}
|
||||
|
||||
fun formatFiatAmount(
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
|
@ -10,9 +10,9 @@ import com.tangem.domain.appcurrency.model.AppCurrency
|
|||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEmpty
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.joda.time.Duration
|
||||
import timber.log.Timber
|
||||
|
|
@ -27,11 +27,18 @@ internal class DefaultAppCurrencyRepository(
|
|||
|
||||
private val appCurrencyConverter = AppCurrencyConverter()
|
||||
|
||||
override fun getSelectedAppCurrency(): Flow<AppCurrency> {
|
||||
return selectedAppCurrencyStore.get()
|
||||
.onEmpty { fetchDefaultAppCurrency() }
|
||||
.map(appCurrencyConverter::convert)
|
||||
.flowOn(dispatchers.io)
|
||||
override fun getSelectedAppCurrency(): Flow<AppCurrency> = channelFlow {
|
||||
launch(dispatchers.io) {
|
||||
selectedAppCurrencyStore.get()
|
||||
.map(appCurrencyConverter::convert)
|
||||
.collect(::send)
|
||||
}
|
||||
|
||||
launch(dispatchers.io) {
|
||||
if (selectedAppCurrencyStore.isEmpty()) {
|
||||
fetchDefaultAppCurrency()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAvailableAppCurrencies(): List<AppCurrency> {
|
||||
|
|
|
|||
1
data/app-theme/.gitignore
vendored
Normal file
1
data/app-theme/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
34
data/app-theme/build.gradle.kts
Normal file
34
data/app-theme/build.gradle.kts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.data.apptheme"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Project - Domain */
|
||||
implementation(projects.domain.core)
|
||||
implementation(projects.domain.appTheme)
|
||||
implementation(projects.domain.appTheme.models)
|
||||
|
||||
/** Project - Data */
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.data.common)
|
||||
|
||||
/** Project - Utils */
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.core)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Other */
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.jodatime)
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.data.apptheme
|
||||
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
internal class MockAppThemeModeRepository : AppThemeModeRepository {
|
||||
|
||||
private val appThemeModeFlow = MutableStateFlow(AppThemeMode.DEFAULT)
|
||||
|
||||
override fun getAppThemeMode(): Flow<AppThemeMode> {
|
||||
return appThemeModeFlow
|
||||
}
|
||||
|
||||
override suspend fun changeAppThemeMode(mode: AppThemeMode) {
|
||||
appThemeModeFlow.value = mode
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.data.apptheme.di
|
||||
|
||||
import com.tangem.data.apptheme.MockAppThemeModeRepository
|
||||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object AppThemeModeDataModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppThemeModeRepository(): AppThemeModeRepository {
|
||||
return MockAppThemeModeRepository()
|
||||
}
|
||||
}
|
||||
|
|
@ -15,4 +15,8 @@ internal class DefaultSettingsRepository(
|
|||
preferencesDataSource.appRatingLaunchObserver.isReadyToShow()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun shouldShowSaveUserWalletScreen(): Boolean {
|
||||
return withContext(dispatchers.io) { preferencesDataSource.shouldShowSaveUserWalletScreen }
|
||||
}
|
||||
}
|
||||
|
|
@ -8,8 +8,8 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
|
|||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.local.token.UserTokensStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.core.error.DataError
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.core.error.DataError
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
|
|
@ -53,6 +53,22 @@ internal class DefaultCurrenciesRepository(
|
|||
storeAndPushTokens(userWalletId, response)
|
||||
}
|
||||
|
||||
override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) =
|
||||
withContext(dispatchers.io) {
|
||||
val savedCurrencies = requireNotNull(
|
||||
value = userTokensStore.getSyncOrNull(userWalletId),
|
||||
lazyMessage = { "Saved tokens empty. Can not perform remove currency action" },
|
||||
)
|
||||
|
||||
val token = userTokensResponseFactory.createResponseToken(currency)
|
||||
storeAndPushTokens(
|
||||
userWalletId = userWalletId,
|
||||
response = savedCurrencies.copy(
|
||||
tokens = savedCurrencies.tokens.filter { it != token },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency {
|
||||
return withContext(dispatchers.io) {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
|
|
@ -62,20 +78,38 @@ internal class DefaultCurrenciesRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getMultiCurrencyWalletCurrencies(
|
||||
override fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
|
||||
return channelFlow {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
|
||||
|
||||
launch(dispatchers.io) {
|
||||
getMultiCurrencyWalletCurrencies(userWallet).collect(::send)
|
||||
}
|
||||
|
||||
launch(dispatchers.io) {
|
||||
fetchTokensIfCacheExpired(userWallet, refresh = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getMultiCurrencyWalletCurrenciesSync(
|
||||
userWalletId: UserWalletId,
|
||||
refresh: Boolean,
|
||||
): Flow<List<CryptoCurrency>> = channelFlow {
|
||||
): List<CryptoCurrency> {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
|
||||
|
||||
launch(dispatchers.io) {
|
||||
getMultiCurrencyWalletCurrencies(userWallet).collect(::send)
|
||||
fetchTokensIfCacheExpired(userWallet, refresh)
|
||||
|
||||
val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) {
|
||||
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
|
||||
}
|
||||
|
||||
launch(dispatchers.io) {
|
||||
fetchTokensIfCacheExpired(userWallet, refresh)
|
||||
}
|
||||
return responseCurrenciesFactory.createCurrencies(
|
||||
response = storedTokens,
|
||||
card = userWallet.scanResponse.card,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getMultiCurrencyWalletCurrency(
|
||||
|
|
@ -164,7 +198,7 @@ internal class DefaultCurrenciesRepository(
|
|||
|
||||
tangemTechApi.saveUserTokens(userWallet.walletId.stringValue, response)
|
||||
} else {
|
||||
throw error
|
||||
Timber.e(error, "Unable to fetch currencies for: ${userWallet.walletId}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,14 +16,8 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
|
|||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
internal class DefaultNetworksRepository(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
|
|
@ -45,10 +39,9 @@ internal class DefaultNetworksRepository(
|
|||
return networkConverter.convertSet(networksIds)
|
||||
}
|
||||
|
||||
override fun getNetworkStatuses(
|
||||
override fun getNetworkStatusesUpdates(
|
||||
userWalletId: UserWalletId,
|
||||
networks: Set<Network.ID>,
|
||||
refresh: Boolean,
|
||||
): Flow<Set<NetworkStatus>> = channelFlow {
|
||||
launch(dispatchers.io) {
|
||||
networksStatuses.collect {
|
||||
|
|
@ -57,10 +50,19 @@ internal class DefaultNetworksRepository(
|
|||
}
|
||||
|
||||
launch(dispatchers.io) {
|
||||
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh)
|
||||
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh = false)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getNetworkStatusesSync(
|
||||
userWalletId: UserWalletId,
|
||||
networks: Set<Network.ID>,
|
||||
refresh: Boolean,
|
||||
): Set<NetworkStatus> = withContext(dispatchers.io) {
|
||||
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh)
|
||||
networksStatuses.first().toSet()
|
||||
}
|
||||
|
||||
private suspend fun fetchNetworksStatusesIfCacheExpired(
|
||||
userWalletId: UserWalletId,
|
||||
networks: Set<Network.ID>,
|
||||
|
|
@ -88,7 +90,7 @@ internal class DefaultNetworksRepository(
|
|||
private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, networkId: Network.ID) {
|
||||
val currencies = getCurrencies(userWalletId)
|
||||
.asSequence()
|
||||
.filter { it.networkId == networkId }
|
||||
.filter { it.network.id == networkId }
|
||||
|
||||
val result = walletManagersFacade.update(
|
||||
userWalletId = userWalletId,
|
||||
|
|
|
|||
|
|
@ -9,11 +9,9 @@ import com.tangem.domain.tokens.models.CryptoCurrency
|
|||
import com.tangem.domain.tokens.models.Quote
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultQuotesRepository(
|
||||
|
|
@ -28,7 +26,7 @@ internal class DefaultQuotesRepository(
|
|||
|
||||
private var quotesFetchedForAppCurrency: String? = null
|
||||
|
||||
override fun getQuotes(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Flow<Set<Quote>> {
|
||||
override fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<Quote>> {
|
||||
return channelFlow {
|
||||
launch(dispatchers.io) {
|
||||
quotesStore.get(currenciesIds)
|
||||
|
|
@ -38,12 +36,26 @@ internal class DefaultQuotesRepository(
|
|||
|
||||
launch(dispatchers.io) {
|
||||
selectedAppCurrencyStore.get().collectLatest { appCurrency ->
|
||||
fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh)
|
||||
fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getQuotesSync(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Set<Quote> {
|
||||
return withContext(dispatchers.io) {
|
||||
val selectedAppCurrency = requireNotNull(selectedAppCurrencyStore.getSyncOrNull()) {
|
||||
"Unable to get selected application currency to update quotes"
|
||||
}
|
||||
|
||||
fetchExpiredQuotes(currenciesIds, selectedAppCurrency.id, refresh)
|
||||
|
||||
val quotes = quotesStore.get(currenciesIds).first()
|
||||
|
||||
quotesConverter.convertSet(quotes)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchExpiredQuotes(
|
||||
currenciesIds: Set<CryptoCurrency.ID>,
|
||||
appCurrencyId: String,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
package com.tangem.data.tokens.utils
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token as SdkToken
|
||||
import com.tangem.domain.common.DerivationStyleProvider
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import timber.log.Timber
|
||||
import com.tangem.blockchain.common.Token as SdkToken
|
||||
|
||||
// FIXME: Make internal
|
||||
class CryptoCurrencyFactory {
|
||||
|
||||
fun createToken(
|
||||
|
|
@ -19,9 +20,10 @@ class CryptoCurrencyFactory {
|
|||
}
|
||||
|
||||
val id = getTokenId(blockchain, sdkToken)
|
||||
|
||||
return CryptoCurrency.Token(
|
||||
id = id,
|
||||
networkId = getNetworkId(blockchain),
|
||||
network = getNetwork(blockchain) ?: return null,
|
||||
name = sdkToken.name,
|
||||
symbol = sdkToken.symbol,
|
||||
iconUrl = getTokenIconUrl(blockchain, sdkToken),
|
||||
|
|
@ -29,8 +31,6 @@ class CryptoCurrencyFactory {
|
|||
isCustom = isCustomToken(id),
|
||||
contractAddress = sdkToken.contractAddress,
|
||||
derivationPath = getDerivationPath(blockchain, derivationStyleProvider),
|
||||
blockchainName = blockchain.fullName,
|
||||
standardType = getTokenStandardType(blockchain, sdkToken),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ class CryptoCurrencyFactory {
|
|||
|
||||
return CryptoCurrency.Coin(
|
||||
id = getCoinId(blockchain),
|
||||
networkId = getNetworkId(blockchain),
|
||||
network = getNetwork(blockchain) ?: return null,
|
||||
name = blockchain.fullName,
|
||||
symbol = blockchain.currency,
|
||||
iconUrl = getCoinIconUrl(blockchain),
|
||||
|
|
|
|||
|
|
@ -3,22 +3,13 @@ package com.tangem.data.tokens.utils
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.tokens.models.Network
|
||||
import com.tangem.utils.converter.Converter
|
||||
import timber.log.Timber
|
||||
|
||||
internal class NetworkConverter : Converter<Network.ID, Network?> {
|
||||
|
||||
override fun convert(value: Network.ID): Network? {
|
||||
val blockchain = Blockchain.fromId(value.value)
|
||||
|
||||
if (blockchain == Blockchain.Unknown) {
|
||||
Timber.e("Unable to convert Unknown blockchain to the domain network model")
|
||||
return null
|
||||
}
|
||||
|
||||
return Network(
|
||||
id = value,
|
||||
name = blockchain.fullName,
|
||||
)
|
||||
return getNetwork(blockchain)
|
||||
}
|
||||
|
||||
override fun convertList(input: Collection<Network.ID>): List<Network> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.data.tokens.utils
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.tokens.models.Network
|
||||
import timber.log.Timber
|
||||
|
||||
internal fun getNetwork(blockchain: Blockchain): Network? {
|
||||
if (blockchain == Blockchain.Unknown) {
|
||||
Timber.e("Unable to convert Unknown blockchain to the domain network model")
|
||||
return null
|
||||
}
|
||||
|
||||
return Network(
|
||||
id = Network.ID(blockchain.id),
|
||||
name = blockchain.fullName,
|
||||
isTestnet = blockchain.isTestnet(),
|
||||
standardType = getNetworkStandardType(blockchain),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType {
|
||||
return when (blockchain) {
|
||||
Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.StandardType.ERC20
|
||||
Blockchain.BSC, Blockchain.BSCTestnet -> Network.StandardType.BEP20
|
||||
Blockchain.Binance, Blockchain.BinanceTestnet -> Network.StandardType.BEP2
|
||||
Blockchain.Tron, Blockchain.TronTestnet -> Network.StandardType.TRC20
|
||||
else -> Network.StandardType.Unspecified(blockchain.name)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,14 @@
|
|||
package com.tangem.data.tokens.utils
|
||||
|
||||
import com.tangem.domain.tokens.model.NetworkAddress
|
||||
import com.tangem.domain.tokens.model.NetworkStatus
|
||||
import com.tangem.domain.tokens.model.PendingTransaction
|
||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||
import com.tangem.domain.tokens.models.Network
|
||||
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
|
||||
import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction
|
||||
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class NetworkStatusFactory {
|
||||
|
|
@ -19,10 +23,18 @@ internal class NetworkStatusFactory {
|
|||
value = when (result) {
|
||||
is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation
|
||||
is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable
|
||||
is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount(result.amountToCreateAccount)
|
||||
is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount(
|
||||
address = getNetworkAddress(result.defaultAddress, result.addresses),
|
||||
amountToCreateAccount = result.amountToCreateAccount,
|
||||
)
|
||||
is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified(
|
||||
amounts = formatAmounts(result.tokensAmounts, currencies),
|
||||
hasTransactionsInProgress = result.hasTransactionsInProgress,
|
||||
address = getNetworkAddress(result.defaultAddress, result.addresses),
|
||||
amounts = formatAmounts(result.currenciesAmounts, currencies),
|
||||
pendingTransactions = formatTransactions(
|
||||
networksAddresses = result.addresses,
|
||||
transactions = result.currentTransactions,
|
||||
currencies = currencies,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -39,13 +51,91 @@ internal class NetworkStatusFactory {
|
|||
is CryptoCurrencyAmount.Coin -> currencies.singleOrNull { it is CryptoCurrency.Coin }
|
||||
is CryptoCurrencyAmount.Token -> currencies.firstOrNull {
|
||||
it is CryptoCurrency.Token &&
|
||||
it.id.rawCurrencyId == amount.id &&
|
||||
it.id.rawCurrencyId == amount.tokenId &&
|
||||
it.contractAddress == amount.tokenContractAddress
|
||||
}
|
||||
}
|
||||
|
||||
currency?.id?.let { it to amount.value }
|
||||
if (currency == null) {
|
||||
Timber.e("Unable to find cryptocurrency for amount: $amount")
|
||||
null
|
||||
} else {
|
||||
currency.id to amount.value
|
||||
}
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
|
||||
private fun formatTransactions(
|
||||
networksAddresses: Set<String>,
|
||||
transactions: Set<CryptoCurrencyTransaction>,
|
||||
currencies: Set<CryptoCurrency>,
|
||||
): Map<CryptoCurrency.ID, Set<PendingTransaction>> {
|
||||
if (transactions.isEmpty()) return emptyMap()
|
||||
|
||||
return currencies
|
||||
.asSequence()
|
||||
.map { currency ->
|
||||
val currencyTransactions = when (currency) {
|
||||
is CryptoCurrency.Coin -> transactions.filterTo(hashSetOf()) { transaction ->
|
||||
transaction is CryptoCurrencyTransaction.Coin
|
||||
}
|
||||
is CryptoCurrency.Token -> transactions.filterTo(hashSetOf()) { transaction ->
|
||||
transaction is CryptoCurrencyTransaction.Token &&
|
||||
transaction.tokenId == currency.id.rawCurrencyId &&
|
||||
transaction.tokenContractAddress == currency.contractAddress
|
||||
}
|
||||
}
|
||||
|
||||
currency.id to createCurrentTransactions(networksAddresses, currencyTransactions)
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
|
||||
private fun createCurrentTransactions(
|
||||
networksAddresses: Set<String>,
|
||||
transactions: Set<CryptoCurrencyTransaction>,
|
||||
): Set<PendingTransaction> {
|
||||
return transactions.mapNotNullTo(hashSetOf()) { createCurrentTransaction(networksAddresses, it) }
|
||||
}
|
||||
|
||||
private fun createCurrentTransaction(
|
||||
networksAddresses: Set<String>,
|
||||
transaction: CryptoCurrencyTransaction,
|
||||
): PendingTransaction? {
|
||||
val direction = when {
|
||||
transaction.toAddress in networksAddresses -> PendingTransaction.Direction.Incoming(
|
||||
fromAddress = transaction.fromAddress,
|
||||
)
|
||||
transaction.fromAddress in networksAddresses -> PendingTransaction.Direction.Outgoing(
|
||||
toAddress = transaction.toAddress,
|
||||
)
|
||||
else -> {
|
||||
Timber.e(
|
||||
"""
|
||||
Unable to find transaction direction
|
||||
|- To address: ${transaction.toAddress}
|
||||
|- From address: ${transaction.fromAddress}
|
||||
|- Network addresses: $networksAddresses
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return PendingTransaction(
|
||||
amount = transaction.amount,
|
||||
direction = direction,
|
||||
sentAt = transaction.sentAt,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getNetworkAddress(defaultAddress: String, availableAddresses: Set<String>): NetworkAddress {
|
||||
return if (availableAddresses.size != 1) {
|
||||
NetworkAddress.Selectable(defaultAddress, availableAddresses)
|
||||
} else {
|
||||
NetworkAddress.Single(defaultAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
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