Updated on 2026-08-14
This commit is contained in:
commit
f0a85e8b5b
508 changed files with 12810 additions and 8013 deletions
|
|
@ -22,13 +22,13 @@ configurations {
|
|||
|
||||
dependencies {
|
||||
implementation(files("libs/walletconnect-1.5.6.aar"))
|
||||
implementation(project(":domain:legacy"))
|
||||
implementation(project(":domain:models"))
|
||||
implementation(project(":domain:core"))
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.core)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.demo)
|
||||
implementation(project(":domain:wallets"))
|
||||
implementation(project(":domain:wallets:models"))
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
|
|
@ -37,21 +37,24 @@ dependencies {
|
|||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.appTheme)
|
||||
implementation(projects.domain.appTheme.models)
|
||||
implementation(projects.domain.balanceHiding)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
|
||||
implementation(project(":common"))
|
||||
implementation(project(":core:analytics"))
|
||||
implementation(projects.common)
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(project(":core:navigation"))
|
||||
implementation(project(":core:featuretoggles"))
|
||||
implementation(project(":core:res"))
|
||||
implementation(project(":core:ui"))
|
||||
implementation(project(":core:datasource"))
|
||||
implementation(project(":core:utils"))
|
||||
implementation(project(":libs:crypto"))
|
||||
implementation(project(":libs:auth"))
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.featuretoggles)
|
||||
implementation(projects.core.res)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.libs.crypto)
|
||||
implementation(projects.libs.auth)
|
||||
|
||||
implementation(projects.data.appCurrency)
|
||||
implementation(projects.data.appTheme)
|
||||
implementation(projects.data.balanceHiding)
|
||||
implementation(projects.data.card)
|
||||
implementation(projects.data.common)
|
||||
implementation(projects.data.settings)
|
||||
|
|
@ -61,20 +64,20 @@ dependencies {
|
|||
implementation(projects.data.wallets)
|
||||
|
||||
/** Features */
|
||||
implementation(project(":features:onboarding"))
|
||||
implementation(project(":features:learn2earn:api"))
|
||||
implementation(project(":features:learn2earn:impl"))
|
||||
implementation(project(":features:referral:presentation"))
|
||||
implementation(project(":features:referral:domain"))
|
||||
implementation(project(":features:referral:data"))
|
||||
implementation(project(":features:swap:api"))
|
||||
implementation(project(":features:swap:presentation"))
|
||||
implementation(project(":features:swap:domain"))
|
||||
implementation(project(":features:swap:data"))
|
||||
implementation(project(":features:tester:api"))
|
||||
implementation(project(":features:tester:impl"))
|
||||
implementation(project(":features:wallet:api"))
|
||||
implementation(project(":features:wallet:impl"))
|
||||
implementation(projects.features.onboarding)
|
||||
implementation(projects.features.learn2earn.api)
|
||||
implementation(projects.features.learn2earn.impl)
|
||||
implementation(projects.features.referral.presentation)
|
||||
implementation(projects.features.referral.domain)
|
||||
implementation(projects.features.referral.data)
|
||||
implementation(projects.features.swap.api)
|
||||
implementation(projects.features.swap.presentation)
|
||||
implementation(projects.features.swap.domain)
|
||||
implementation(projects.features.swap.data)
|
||||
implementation(projects.features.tester.api)
|
||||
implementation(projects.features.tester.impl)
|
||||
implementation(projects.features.wallet.api)
|
||||
implementation(projects.features.wallet.impl)
|
||||
implementation(projects.features.tokendetails.api)
|
||||
implementation(projects.features.tokendetails.impl)
|
||||
|
||||
|
|
@ -95,6 +98,7 @@ dependencies {
|
|||
/** Compose libraries */
|
||||
implementation(deps.compose.constraintLayout)
|
||||
implementation(deps.compose.material)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.animation)
|
||||
implementation(deps.compose.coil)
|
||||
implementation(deps.compose.constraintLayout)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
package com.tangem.tap
|
||||
|
||||
import android.app.Application
|
||||
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.appcompat.app.AppCompatDelegate
|
||||
import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
|
@ -14,6 +17,7 @@ import com.google.android.material.snackbar.Snackbar
|
|||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.data.card.sdk.CardSdkLifecycleObserver
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.card.ScanCardUseCase
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
|
|
@ -127,11 +131,15 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val splashScreen = installSplashScreen()
|
||||
|
||||
if (!isDarkThemeFeatureEnabled(application)) {
|
||||
setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
cardSdkLifecycleObserver.onCreate(context = this)
|
||||
|
||||
bootstrapMainStateUpdates()
|
||||
bootstrapMainStateUpdates(application)
|
||||
|
||||
splashScreen.setKeepOnScreenCondition { isInitializing }
|
||||
|
||||
|
|
@ -165,6 +173,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
)
|
||||
}
|
||||
|
||||
private fun isDarkThemeFeatureEnabled(application: Application): Boolean {
|
||||
val featureToggle = (application as TapApplication).darkThemeFeatureToggle
|
||||
return featureToggle.isDarkThemeEnabled
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
dialogManager.onStart(this)
|
||||
|
|
@ -216,14 +229,25 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager))
|
||||
}
|
||||
|
||||
private fun bootstrapMainStateUpdates() {
|
||||
private fun bootstrapMainStateUpdates(application: Application) {
|
||||
viewModel.state
|
||||
.onEach { state ->
|
||||
isInitializing = state is GlobalSettingsState.Loading
|
||||
|
||||
when (state) {
|
||||
is GlobalSettingsState.Content -> {
|
||||
MutableAppThemeModeHolder.value = state.appThemeMode
|
||||
if (isDarkThemeFeatureEnabled(application)) {
|
||||
MutableAppThemeModeHolder.value = state.appThemeMode
|
||||
|
||||
val mode = when (state.appThemeMode) {
|
||||
AppThemeMode.FORCE_DARK -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
AppThemeMode.FORCE_LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
AppThemeMode.FOLLOW_SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
setDefaultNightMode(mode)
|
||||
} else {
|
||||
MutableAppThemeModeHolder.value = AppThemeMode.FORCE_LIGHT
|
||||
}
|
||||
}
|
||||
is GlobalSettingsState.Loading -> Unit
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,12 @@ import com.tangem.datasource.config.ConfigManager
|
|||
import com.tangem.datasource.config.FeaturesLocalLoader
|
||||
import com.tangem.datasource.config.models.Config
|
||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.domain.DomainLayer
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.legacy.WalletManagersRepository
|
||||
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
|
||||
|
|
@ -61,6 +63,8 @@ import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementa
|
|||
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
|
||||
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
|
||||
import com.tangem.tap.features.details.DarkThemeFeatureToggle
|
||||
import com.tangem.tap.features.details.featuretoggles.DetailsFeatureToggles
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.wallet.BuildConfig
|
||||
|
|
@ -169,6 +173,21 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
@Inject
|
||||
lateinit var walletManagersFacade: WalletManagersFacade
|
||||
|
||||
@Inject
|
||||
lateinit var currenciesRepository: CurrenciesRepository
|
||||
|
||||
@Inject
|
||||
lateinit var appThemeModeRepository: AppThemeModeRepository
|
||||
|
||||
@Inject
|
||||
lateinit var balanceHidingRepository: BalanceHidingRepository
|
||||
|
||||
@Inject
|
||||
lateinit var detailsFeatureToggles: DetailsFeatureToggles
|
||||
|
||||
@Inject
|
||||
lateinit var darkThemeFeatureToggle: DarkThemeFeatureToggle
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
|
|
@ -189,6 +208,11 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
scanCardProcessor = scanCardProcessor,
|
||||
appCurrencyRepository = appCurrencyRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
appStateHolder = appStateHolder,
|
||||
currenciesRepository = currenciesRepository,
|
||||
appThemeModeRepository = appThemeModeRepository,
|
||||
balanceHidingRepository = balanceHidingRepository,
|
||||
detailsFeatureToggles = detailsFeatureToggles,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -208,7 +232,6 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
activityResultCaller = foregroundActivityObserver
|
||||
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
|
||||
|
||||
DomainLayer.init()
|
||||
preferencesStorage = preferencesDataSource
|
||||
walletConnectRepository = WalletConnectRepository(this)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
package com.tangem.tap.common
|
||||
|
||||
import timber.log.Timber
|
||||
|
||||
class CompositionCounter(
|
||||
val id: String,
|
||||
count: Int = 0,
|
||||
) {
|
||||
var count: Int = count
|
||||
private set
|
||||
|
||||
fun increase(id: String): CompositionCounter {
|
||||
if (this.id != id) return this
|
||||
|
||||
count += 1
|
||||
return CompositionCounter(id, count)
|
||||
}
|
||||
}
|
||||
|
||||
class CompositionLogger(
|
||||
private val recomposeViewId: String,
|
||||
private val tag: String = recomposeViewId,
|
||||
private var turnOnForIds: List<String> = listOf(recomposeViewId),
|
||||
) {
|
||||
val count: Int
|
||||
get() = compositionCounter.count
|
||||
|
||||
private var compositionCounter: CompositionCounter = CompositionCounter(recomposeViewId)
|
||||
|
||||
fun nextComposition() {
|
||||
compositionCounter = compositionCounter.increase(recomposeViewId)
|
||||
log("")
|
||||
}
|
||||
|
||||
fun log(message: String) {
|
||||
if (!turnOnForIds.contains(recomposeViewId)) return
|
||||
|
||||
Timber.d("$tag[$recomposeViewId]:[${compositionCounter.count}]: $message")
|
||||
}
|
||||
}
|
||||
|
|
@ -8,10 +8,7 @@ sealed class AnalyticsParam {
|
|||
class Currency(currency: com.tangem.tap.features.wallet.models.Currency) : CurrencyType(currency.currencySymbol)
|
||||
class Blockchain(blockchain: com.tangem.blockchain.common.Blockchain) : CurrencyType(blockchain.currency)
|
||||
class Token(token: com.tangem.blockchain.common.Token) : CurrencyType(token.symbol)
|
||||
class FiatCurrency(
|
||||
fiatCurrency: com.tangem.tap.common.entities.FiatCurrency,
|
||||
) : CurrencyType(fiatCurrency.code)
|
||||
|
||||
class FiatCurrency(fiatCurrency: com.tangem.tap.common.entities.FiatCurrency) : CurrencyType(fiatCurrency.code)
|
||||
class Amount(amount: com.tangem.blockchain.common.Amount) : CurrencyType(amount.currencySymbol)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ sealed class Portfolio(
|
|||
) : AnalyticsEvent("Portfolio", event, params) {
|
||||
|
||||
class Refreshed : Portfolio("Refreshed")
|
||||
|
||||
class ButtonManageTokens : Portfolio("Button - Manage Tokens")
|
||||
|
||||
class TokenTapped : Portfolio("Token is Tapped")
|
||||
|
||||
class OrganizeTokens : Portfolio("Button - Organize Tokens")
|
||||
}
|
||||
|
|
@ -1,277 +0,0 @@
|
|||
package com.tangem.tap.common.compose
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextFieldColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.common.module.ModuleError
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.tap.common.CompositionLogger
|
||||
import com.tangem.tap.common.compose.extensions.stringResourceDefault
|
||||
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun OutlinedTextFieldWidget(
|
||||
fieldData: Field.Data<String>,
|
||||
labelId: Int? = null,
|
||||
label: String = "",
|
||||
placeholderId: Int? = null,
|
||||
placeholder: String = "",
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
isEnabled: Boolean = true,
|
||||
isVisible: Boolean = true,
|
||||
isLoading: Boolean = false,
|
||||
error: ModuleError? = null,
|
||||
errorConverter: ModuleMessageConverter? = null,
|
||||
debounceTextChanges: Long = 400,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
onTextChange: (String) -> Unit,
|
||||
) {
|
||||
if (!isVisible) return
|
||||
|
||||
Column(modifier = Modifier.animateContentSize()) {
|
||||
OutlinedProgressTextField(
|
||||
fieldData = fieldData,
|
||||
label = stringResourceDefault(labelId, label),
|
||||
placeholder = stringResourceDefault(placeholderId, placeholder),
|
||||
trailingIcon = trailingIcon,
|
||||
isEnabled = isEnabled,
|
||||
isLoading = isLoading,
|
||||
error = error,
|
||||
debounce = debounceTextChanges,
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
onTextChange = onTextChange,
|
||||
)
|
||||
errorConverter?.let { AnimatedErrorView(errorConverter = it, error = error) }
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "NestedBlockDepth", "MagicNumber", "MaxLineLength")
|
||||
@Composable
|
||||
private fun OutlinedProgressTextField(
|
||||
fieldData: Field.Data<String>,
|
||||
label: String = "",
|
||||
placeholder: String = "",
|
||||
isEnabled: Boolean = true,
|
||||
isLoading: Boolean = false,
|
||||
error: ModuleError? = null,
|
||||
debounce: Long = 400,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
colors: TextFieldColors = TangemTextFieldsDefault.defaultTextFieldColors,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
onTextChange: (String) -> Unit,
|
||||
) {
|
||||
val logger = remember {
|
||||
CompositionLogger(label, "OutlinedProgressTextField", listOf("Символ токена"))
|
||||
}
|
||||
logger.nextComposition()
|
||||
|
||||
val textValueState = remember { mutableStateOf(fieldData.value) }
|
||||
val textDebouncer = valueDebouncerAsState(
|
||||
initialValue = fieldData.value,
|
||||
debounce = debounce,
|
||||
onEmitValueReceive = {
|
||||
logger.log("DEBOUNCER: onEmitValueReceived: [$it]")
|
||||
logger.log("DEBOUNCER: start RECOMPOSE by new value for textValueState.value = [$it]")
|
||||
textValueState.value = it
|
||||
},
|
||||
onValueChange = {
|
||||
logger.log("DEBOUNCER: onValueChanged: >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> dispatch.toStore([$it])")
|
||||
onTextChange(it)
|
||||
},
|
||||
)
|
||||
|
||||
logger.log("RECOMPOSE ---------------------------------------------------------------START [${logger.count}]")
|
||||
logger.log("RECOMPOSE --data: fieldData.value: [$fieldData]")
|
||||
logger.log("RECOMPOSE --data: textValueState.value: [${textValueState.value}]")
|
||||
logger.log("RECOMPOSE --data: textDebouncer.emittedValue = [${textDebouncer.emittedValue}]")
|
||||
logger.log("RECOMPOSE --data: textDebouncer.debounced = [${textDebouncer.debounced}]")
|
||||
|
||||
if (!fieldData.isUserInput) {
|
||||
// initial value is not from an user
|
||||
val isNotUserInput = "-- IS NOT USER INPUT"
|
||||
logger.log("recompose $isNotUserInput")
|
||||
if (textValueState.value == fieldData.value) {
|
||||
logger.log("$isNotUserInput: внешние данные ОДИНАКОВЫ с данными в поле")
|
||||
} else {
|
||||
logger.log("$isNotUserInput: внешние данные РАЗЛИЧАЮТСЯ с данными в поле")
|
||||
if (textDebouncer.emittedValue != textDebouncer.debounced || textDebouncer.emitsCountBeforeDebounce > 0) {
|
||||
logger.log("$isNotUserInput: пользователь ВВОДИТ данные -> внешние данные игнорируем, ждем RECOMPOSE")
|
||||
} else {
|
||||
logger.log("$isNotUserInput: пользователь НЕ вводит данные -> пытаемся обработать внешние данные")
|
||||
if (textValueState.value != textDebouncer.emittedValue ||
|
||||
textValueState.value != textDebouncer.debounced
|
||||
) {
|
||||
logger.log("$isNotUserInput: даннные в поле не соответствуют данным из textDebouncer")
|
||||
if (textDebouncer.emittedValue.isEmpty() && textDebouncer.debounced.isEmpty()) {
|
||||
logger.log(
|
||||
"$isNotUserInput: даннные в textDebouncer ПУСТЫ -> start RECOMPOSE новые данные для " +
|
||||
"textValueState.value = [${fieldData.value}]",
|
||||
)
|
||||
textValueState.value = fieldData.value
|
||||
} else {
|
||||
logger.log(
|
||||
"$isNotUserInput: даннные в textDebouncer НЕ ПУСТЫ -> start RECOMPOSE новые данные для " +
|
||||
"textValueState.value = [${fieldData.value}]",
|
||||
)
|
||||
textValueState.value = fieldData.value
|
||||
}
|
||||
} else {
|
||||
logger.log(
|
||||
"$isNotUserInput: в пустое поле вставляются данные -> start RECOMPOSE новые данные для " +
|
||||
"textValueState.value = [${fieldData.value}]",
|
||||
)
|
||||
textValueState.value = fieldData.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.log("recompose --------------------------------------------------------------FINISH [${logger.count}]")
|
||||
|
||||
Box {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
value = textValueState.value,
|
||||
onValueChange = {
|
||||
logger.log("WIDGET: textDebouncer.emmit([$it])")
|
||||
textDebouncer.emmit(it)
|
||||
},
|
||||
keyboardOptions = keyboardOptions,
|
||||
label = {
|
||||
Text(
|
||||
text = label,
|
||||
style = TangemTheme.typography.caption,
|
||||
color = colors.labelColor(
|
||||
enabled = isEnabled,
|
||||
error = error != null,
|
||||
interactionSource = interactionSource,
|
||||
).value,
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = placeholder,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = colors.placeholderColor(enabled = isEnabled).value,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
trailingIcon = trailingIcon,
|
||||
singleLine = true,
|
||||
enabled = isEnabled,
|
||||
isError = error != null,
|
||||
visualTransformation = visualTransformation,
|
||||
colors = colors,
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(start = 6.dp, top = 0.dp, end = 6.dp, bottom = 6.dp),
|
||||
visible = isLoading,
|
||||
) {
|
||||
LinearProgressIndicator(
|
||||
color = TangemTheme.colors.icon.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimatedErrorView(errorConverter: ModuleMessageConverter, error: ModuleError? = null) {
|
||||
AnimatedVisibility(
|
||||
visible = error != null,
|
||||
enter = fadeIn() + slideInVertically(),
|
||||
exit = slideOutVertically() + fadeOut(),
|
||||
) {
|
||||
error?.let {
|
||||
ErrorView(
|
||||
text = errorConverter.convert(it).message,
|
||||
style = TextStyle(fontSize = 14.sp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun OutlinedTextFieldWithErrorTest() {
|
||||
val context = LocalContext.current
|
||||
val converter = remember { ModuleMessageConverter(context) }
|
||||
|
||||
class SimpleError(
|
||||
override val code: Int = 1,
|
||||
override val message: String = "Error message",
|
||||
override val data: Any? = null,
|
||||
) : ModuleError()
|
||||
|
||||
val modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
Column {
|
||||
OutlinedTextFieldWidget(
|
||||
fieldData = Field.Data("", false),
|
||||
label = "First label",
|
||||
placeholder = "1 placeholder",
|
||||
error = null,
|
||||
errorConverter = converter,
|
||||
) {}
|
||||
OutlinedTextFieldWidget(
|
||||
fieldData = Field.Data("First", false),
|
||||
label = "First label",
|
||||
placeholder = "1 placeholder",
|
||||
error = null,
|
||||
errorConverter = converter,
|
||||
) {}
|
||||
OutlinedTextFieldWidget(
|
||||
fieldData = Field.Data("First", false),
|
||||
label = "First label",
|
||||
placeholder = "1 placeholder",
|
||||
isLoading = true,
|
||||
error = null,
|
||||
errorConverter = converter,
|
||||
) {}
|
||||
OutlinedTextFieldWidget(
|
||||
fieldData = Field.Data("First", false),
|
||||
label = "First label",
|
||||
placeholder = "1 placeholder",
|
||||
error = SimpleError(),
|
||||
errorConverter = converter,
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
package com.tangem.tap.common.compose
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
* Compose views are not typically used as a main or base view.
|
||||
*/
|
||||
|
||||
@Composable
|
||||
fun TitleSubtitle(title: String, subtitle: String) {
|
||||
Column {
|
||||
Text(text = title)
|
||||
Text(
|
||||
text = subtitle,
|
||||
fontSize = 12.sp,
|
||||
color = Color.Gray,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package com.tangem.tap.common.compose.extensions
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun stringResourceDefault(@StringRes id: Int?, default: String = ""): String {
|
||||
val resources = LocalContext.current.resources
|
||||
return try {
|
||||
resources.getString(requireNotNull(id))
|
||||
} catch (ex: Resources.NotFoundException) {
|
||||
default
|
||||
} catch (ex: IllegalArgumentException) {
|
||||
default
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,8 @@ import com.tangem.core.navigation.AppScreen
|
|||
import com.tangem.core.navigation.FragmentShareTransition
|
||||
import com.tangem.feature.referral.ReferralFragment
|
||||
import com.tangem.feature.swap.presentation.SwapFragment
|
||||
import com.tangem.tap.features.customtoken.legacy.AddCustomTokenFragment
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment
|
||||
import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorFragment
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment
|
||||
import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment
|
||||
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment
|
||||
|
|
@ -33,7 +34,6 @@ import com.tangem.tap.proxy.redux.DaggerGraphState
|
|||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import timber.log.Timber
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment as RedesignedAddCustomTokenFragment
|
||||
|
||||
fun FragmentActivity.openFragment(
|
||||
screen: AppScreen,
|
||||
|
|
@ -155,18 +155,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
|
|||
AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment()
|
||||
AppScreen.Disclaimer -> DisclaimerFragment()
|
||||
AppScreen.AddTokens -> TokensListFragment()
|
||||
|
||||
AppScreen.AddCustomToken -> {
|
||||
val featureToggles = store.state.daggerGraphState.get(
|
||||
getDependency = DaggerGraphState::customTokenFeatureToggles,
|
||||
)
|
||||
if (featureToggles.isRedesignedScreenEnabled) {
|
||||
RedesignedAddCustomTokenFragment()
|
||||
} else {
|
||||
AddCustomTokenFragment()
|
||||
}
|
||||
}
|
||||
|
||||
AppScreen.AddCustomToken -> AddCustomTokenFragment()
|
||||
AppScreen.WalletDetails -> {
|
||||
val featureToggles = store.state.daggerGraphState.get(
|
||||
getDependency = DaggerGraphState::tokenDetailsFeatureToggles,
|
||||
|
|
@ -186,5 +175,6 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
|
|||
AppScreen.Welcome -> WelcomeFragment()
|
||||
AppScreen.SaveWallet -> SaveWalletBottomSheetFragment()
|
||||
AppScreen.WalletSelector -> WalletSelectorBottomSheetFragment()
|
||||
AppScreen.AppCurrencySelector -> AppCurrencySelectorFragment()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
typealias ValueCallback<T> = (T) -> Unit
|
||||
|
|
@ -6,6 +6,7 @@ import com.tangem.common.CompletionResult
|
|||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
import com.tangem.datasource.config.models.ChatConfig
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.tap.common.analytics.topup.TopUpController
|
||||
|
|
@ -103,4 +104,5 @@ sealed class GlobalAction : Action {
|
|||
}
|
||||
|
||||
data class UpdateUserWalletsListManager(val manager: UserWalletsListManager) : GlobalAction()
|
||||
data class ChangeAppThemeMode(val appThemeMode: AppThemeMode) : GlobalAction()
|
||||
}
|
||||
|
|
@ -66,7 +66,13 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
|
|||
}
|
||||
}
|
||||
is GlobalAction.RestoreAppCurrency -> {
|
||||
if (store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles).isRedesignedScreenEnabled) {
|
||||
val daggerGraphState = store.state.daggerGraphState
|
||||
val walletFeatureToggles = daggerGraphState.get(DaggerGraphState::walletFeatureToggles)
|
||||
val detailsFeatureToggles = daggerGraphState.get(DaggerGraphState::detailsFeatureToggles)
|
||||
|
||||
if (walletFeatureToggles.isRedesignedScreenEnabled ||
|
||||
detailsFeatureToggles.isRedesignedAppCurrencySelectorEnabled
|
||||
) {
|
||||
restoreAppCurrencyNew()
|
||||
} else {
|
||||
restoreAppCurrencyLegacy()
|
||||
|
|
@ -131,6 +137,8 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
|
|||
sellService = makeSellExchangeService(config),
|
||||
primaryRules = CardExchangeRules(cardProvider),
|
||||
)
|
||||
// TODO: for refactoring (after remove old design refactor CurrencyExchangeManager and use 1 instance)
|
||||
store.state.daggerGraphState.get(DaggerGraphState::appStateHolder).exchangeService = exchangeManager
|
||||
store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager))
|
||||
store.dispatchOnMain(GlobalAction.ExchangeManager.Update)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,6 +97,9 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
|
|||
userWalletsListManager = action.manager,
|
||||
)
|
||||
}
|
||||
is GlobalAction.ChangeAppThemeMode -> globalState.copy(
|
||||
appThemeMode = action.appThemeMode,
|
||||
)
|
||||
else -> globalState
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.common.redux.global
|
||||
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.tap.common.analytics.topup.TopUpController
|
||||
|
|
@ -29,6 +30,7 @@ data class GlobalState(
|
|||
val userCountryCode: String? = null,
|
||||
val userWalletsListManager: UserWalletsListManager? = null,
|
||||
val topUpController: TopUpController? = null,
|
||||
val appThemeMode: AppThemeMode = AppThemeMode.DEFAULT,
|
||||
) : StateType
|
||||
|
||||
typealias CryptoCurrencyName = String
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@ package com.tangem.tap.di
|
|||
import android.content.Context
|
||||
import com.tangem.domain.card.ScanCardUseCase
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.tap.domain.TangemSdkManager
|
||||
import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository
|
||||
import com.tangem.tap.network.exchangeServices.DefaultRampManager
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.tap.userTokensRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -40,4 +43,10 @@ internal object ActivityModule {
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDefaultRampManager(appStateHolder: AppStateHolder): RampStateManager {
|
||||
return DefaultRampManager(appStateHolder.exchangeService)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,34 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.appcurrency.GetAvailableCurrenciesUseCase
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.SelectAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
internal object AppCurrencyDomainModule {
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetSelectedAppCurrencyUseCase(
|
||||
appCurrencyRepository: AppCurrencyRepository,
|
||||
): GetSelectedAppCurrencyUseCase {
|
||||
return GetSelectedAppCurrencyUseCase(appCurrencyRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideSelectAppCurrencyUseCase(appCurrencyRepository: AppCurrencyRepository): SelectAppCurrencyUseCase {
|
||||
return SelectAppCurrencyUseCase(appCurrencyRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideGetAvailableCurrenciesUseCase(
|
||||
appCurrencyRepository: AppCurrencyRepository,
|
||||
): GetAvailableCurrenciesUseCase {
|
||||
return GetAvailableCurrenciesUseCase(appCurrencyRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
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.balancehiding.DeviceFlipDetector
|
||||
import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase
|
||||
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
|
||||
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
|
||||
import com.tangem.domain.settings.*
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.tap.domain.TangemSdkManager
|
||||
import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository
|
||||
|
|
@ -37,4 +39,24 @@ internal object SettingsDomainModule {
|
|||
legacySettingsRepository = DefaultLegacySettingsRepository(tangemSdkManager = tangemSdkManager),
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun providesIsBalanceHiddenUseCase(balanceHidingRepository: BalanceHidingRepository): IsBalanceHiddenUseCase {
|
||||
return IsBalanceHiddenUseCase(
|
||||
balanceHidingRepository = balanceHidingRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun providesListenUseCase(
|
||||
flipDetector: DeviceFlipDetector,
|
||||
balanceHidingRepository: BalanceHidingRepository,
|
||||
): ListenToFlipsUseCase {
|
||||
return ListenToFlipsUseCase(
|
||||
flipDetector = flipDetector,
|
||||
balanceHidingRepository = balanceHidingRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -56,6 +59,24 @@ internal object TokensDomainModule {
|
|||
return GetCurrencyStatusUpdatesUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetCurrencyWarningsUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
quotesRepository: QuotesRepository,
|
||||
networksRepository: NetworksRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): GetCurrencyWarningsUseCase {
|
||||
return GetCurrencyWarningsUseCase(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
currenciesRepository = currenciesRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
networksRepository = networksRepository,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetPrimaryCurrencyUseCase(
|
||||
|
|
@ -108,8 +129,32 @@ internal object TokensDomainModule {
|
|||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetCryptoCurrencyActionsUseCase(
|
||||
rampStateManager: RampStateManager,
|
||||
marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): GetCryptoCurrencyActionsUseCase {
|
||||
return GetCryptoCurrencyActionsUseCase(dispatchers)
|
||||
return GetCryptoCurrencyActionsUseCase(rampStateManager, marketCryptoCurrencyRepository, dispatchers)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetCurrencyStatusByNetworkUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
quotesRepository: QuotesRepository,
|
||||
networksRepository: NetworksRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): GetNetworkCoinStatusUseCase {
|
||||
return GetNetworkCoinStatusUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
networksRepository = networksRepository,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase {
|
||||
return GetCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import com.tangem.tap.common.analytics.events.Basic
|
|||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||
import com.tangem.tap.common.extensions.setContext
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.domain.walletStores.WalletStoresError
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
|
||||
|
|
@ -28,11 +29,13 @@ import com.tangem.tap.features.disclaimer.createDisclaimer
|
|||
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Store
|
||||
import timber.log.Timber
|
||||
|
||||
class TapWalletManager(
|
||||
|
|
@ -75,17 +78,33 @@ class TapWalletManager(
|
|||
withMainContext {
|
||||
// Order is important
|
||||
store.dispatch(DisclaimerAction.SetDisclaimer(card.createDisclaimer()))
|
||||
store.dispatch(WalletAction.UserWalletChanged(userWallet))
|
||||
store.dispatch(WalletAction.UpdateCanSaveUserWallets(preferencesStorage.shouldSaveUserWallets))
|
||||
store.dispatchWalletAction(action = WalletAction.UserWalletChanged(userWallet))
|
||||
store.dispatchWalletAction(
|
||||
action = WalletAction.UpdateCanSaveUserWallets(
|
||||
canSaveUserWallets = preferencesStorage.shouldSaveUserWallets,
|
||||
),
|
||||
)
|
||||
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
|
||||
store.dispatch(WalletConnectAction.ResetState)
|
||||
store.dispatch(GlobalAction.SaveScanResponse(scanResponse))
|
||||
store.dispatch(WalletConnectAction.RestoreSessions(scanResponse))
|
||||
store.dispatch(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed))
|
||||
store.dispatch(WalletAction.Warnings.CheckIfNeeded)
|
||||
store.dispatchWalletAction(action = WalletAction.Warnings.CheckIfNeeded)
|
||||
}
|
||||
setupWalletConnectV2(userWallet)
|
||||
loadData(userWallet, refresh)
|
||||
|
||||
val walletFeatureToggles = store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles)
|
||||
if (!walletFeatureToggles.isRedesignedScreenEnabled) {
|
||||
loadData(userWallet = userWallet, refresh = refresh)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Store<AppState>.dispatchWalletAction(action: WalletAction) {
|
||||
val walletFeatureToggles = state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles)
|
||||
|
||||
if (!walletFeatureToggles.isRedesignedScreenEnabled) {
|
||||
dispatch(action = action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupWalletConnectV2(userWallet: UserWallet) {
|
||||
|
|
|
|||
|
|
@ -1,105 +0,0 @@
|
|||
package com.tangem.tap.domain.tokens
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.datasource.api.common.MoshiConverter
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.datasource.asset.AssetReader
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class LoadAvailableCoinsService(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val assetReader: AssetReader,
|
||||
) {
|
||||
private val currenciesAdapter: JsonAdapter<CurrenciesFromJson> =
|
||||
MoshiConverter.networkMoshi.adapter(CurrenciesFromJson::class.java)
|
||||
|
||||
suspend fun getSupportedTokens(
|
||||
isTestNet: Boolean,
|
||||
supportedBlockchains: List<Blockchain>,
|
||||
page: Int,
|
||||
searchInput: String?,
|
||||
): Result<LoadedCoins> {
|
||||
if (isTestNet) {
|
||||
return Result.Success(
|
||||
LoadedCoins(
|
||||
currencies = getTestnetCoins().filter(searchInput),
|
||||
moreAvailable = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val offset = page * LOAD_PER_PAGE
|
||||
return when (val result = loadCoins(supportedBlockchains, offset, searchInput)) {
|
||||
is Result.Success -> {
|
||||
val data = result.data
|
||||
|
||||
Result.Success(
|
||||
LoadedCoins(
|
||||
currencies = data.coins.map {
|
||||
Currency.fromCoinResponse(currency = it, imageHost = data.imageHost)
|
||||
},
|
||||
moreAvailable = data.total > offset + LOAD_PER_PAGE,
|
||||
),
|
||||
)
|
||||
}
|
||||
is Result.Failure -> {
|
||||
Result.Failure(result.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadCoins(
|
||||
supportedBlockchains: List<Blockchain>,
|
||||
offset: Int,
|
||||
searchInput: String?,
|
||||
): Result<CoinsResponse> {
|
||||
return withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
tangemTechApi.getCoins(
|
||||
networkIds = supportedBlockchains.joinToString(
|
||||
separator = ",",
|
||||
transform = Blockchain::toNetworkId,
|
||||
),
|
||||
active = true,
|
||||
searchText = searchInput,
|
||||
offset = offset,
|
||||
limit = LOAD_PER_PAGE,
|
||||
)
|
||||
}.fold(
|
||||
onSuccess = { Result.Success(it) },
|
||||
onFailure = { Result.Failure(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTestnetCoins(): List<Currency> {
|
||||
val json = assetReader.readJson(FILE_NAME_TESTNET_COINS)
|
||||
return currenciesAdapter.fromJson(json)!!.coins
|
||||
.map { Currency.fromJsonObject(it) }
|
||||
}
|
||||
|
||||
private fun List<Currency>.filter(searchInput: String?): List<Currency> {
|
||||
if (searchInput.isNullOrBlank()) return this
|
||||
|
||||
return filter { currency ->
|
||||
currency.symbol.contains(searchInput, ignoreCase = true) ||
|
||||
currency.name.contains(searchInput, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val LOAD_PER_PAGE = 100
|
||||
const val FILE_NAME_TESTNET_COINS = "testnet_tokens"
|
||||
}
|
||||
}
|
||||
|
||||
data class LoadedCoins(
|
||||
val currencies: List<Currency>,
|
||||
val moreAvailable: Boolean,
|
||||
)
|
||||
|
|
@ -7,8 +7,5 @@ package com.tangem.tap.features.customtoken.api.featuretoggles
|
|||
*/
|
||||
interface CustomTokenFeatureToggles {
|
||||
|
||||
/** Availability of redesigned screen (internal feature) */
|
||||
val isRedesignedScreenEnabled: Boolean
|
||||
|
||||
val isNewCardScanningEnabled: Boolean
|
||||
}
|
||||
|
|
@ -14,9 +14,6 @@ internal class DefaultCustomTokenFeatureToggles(
|
|||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : CustomTokenFeatureToggles {
|
||||
|
||||
override val isRedesignedScreenEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED")
|
||||
|
||||
override val isNewCardScanningEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "NEW_CARD_SCANNING_ENABLED")
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.legacy
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.tap.common.analytics.events.ManageTokens
|
||||
import com.tangem.tap.features.BaseStoreFragment
|
||||
import com.tangem.tap.features.FragmentOnBackPressedHandler
|
||||
import com.tangem.tap.features.addBackPressHandler
|
||||
import com.tangem.tap.features.customtoken.legacy.compose.AddCustomTokenScreen
|
||||
import com.tangem.tap.features.customtoken.legacy.compose.ClosePopupTrigger
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class AddCustomTokenFragment : BaseStoreFragment(R.layout.view_compose_fragment), StoreSubscriber<AddCustomTokenState> {
|
||||
|
||||
private var state: MutableState<AddCustomTokenState> = mutableStateOf(domainStore.state.addCustomTokensState)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Analytics.send(ManageTokens.CustomToken.ScreenOpened)
|
||||
}
|
||||
|
||||
override fun subscribeToStore() {
|
||||
domainStore.subscribe(this) { state ->
|
||||
state.skipRepeats { oldState, newState ->
|
||||
oldState.addCustomTokensState == newState.addCustomTokensState
|
||||
}.select { it.addCustomTokensState }
|
||||
}
|
||||
}
|
||||
|
||||
override fun newState(state: AddCustomTokenState) {
|
||||
if (activity == null || view == null) return
|
||||
|
||||
this.state.value = state
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
view.findViewById<Toolbar>(R.id.toolbar)?.setTitle(R.string.add_custom_token_title)
|
||||
|
||||
val closePopupTrigger = initClosingPopupTriggerEvent()
|
||||
view.findViewById<ComposeView>(R.id.view_compose)?.setContent {
|
||||
TangemTheme {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
AddCustomTokenScreen(state, closePopupTrigger)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initClosingPopupTriggerEvent(): ClosePopupTrigger = ClosePopupTrigger().apply {
|
||||
onCloseComplete = ::handleOnBackPressed
|
||||
addBackPressHandler(
|
||||
object : FragmentOnBackPressedHandler {
|
||||
override fun handleOnBackPressed() = close()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,208 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.legacy.compose
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconStart
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import com.tangem.domain.common.form.DataField
|
||||
import com.tangem.domain.common.form.FieldId
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
import com.tangem.domain.features.addCustomToken.redux.ScreenState
|
||||
import com.tangem.domain.features.addCustomToken.redux.ViewStates
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.tap.common.compose.AddCustomTokenWarning
|
||||
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
|
||||
import com.tangem.tap.features.customtoken.legacy.compose.test.TestCase
|
||||
import com.tangem.tap.features.customtoken.legacy.compose.test.TestCasesList
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun AddCustomTokenScreen(state: MutableState<AddCustomTokenState>, closePopupTrigger: ClosePopupTrigger) {
|
||||
val selectedTestCase = remember { mutableStateOf(TestCase.ContractAddress) }
|
||||
|
||||
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed),
|
||||
)
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val toggleBottomSheet = { coroutineScope.launch { bottomSheetScaffoldState.toggle() } }
|
||||
|
||||
BottomSheetScaffold(
|
||||
scaffoldState = bottomSheetScaffoldState,
|
||||
sheetContent = {
|
||||
Surface(color = colorResource(id = R.color.lightGray5)) {
|
||||
selectedTestCase.value.content(toggleBottomSheet)
|
||||
}
|
||||
},
|
||||
sheetPeekHeight = 0.dp,
|
||||
) {
|
||||
Column {
|
||||
TestCasesList(
|
||||
onItemClick = {
|
||||
selectedTestCase.value = it
|
||||
toggleBottomSheet()
|
||||
},
|
||||
)
|
||||
ScreenContent(state, closePopupTrigger)
|
||||
}
|
||||
}
|
||||
|
||||
ComposeDialogManager()
|
||||
LaunchedEffect(key1 = Unit, block = { domainStore.dispatch(AddCustomTokenAction.OnCreate) })
|
||||
DisposableEffect(key1 = Unit, effect = { onDispose { domainStore.dispatch(AddCustomTokenAction.OnDestroy) } })
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
private suspend fun BottomSheetScaffoldState.toggle() {
|
||||
if (bottomSheetState.isCollapsed) {
|
||||
bottomSheetState.expand()
|
||||
} else {
|
||||
bottomSheetState.collapse()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScreenContent(state: MutableState<AddCustomTokenState>, closePopupTrigger: ClosePopupTrigger) {
|
||||
val scaffoldState = rememberScaffoldState()
|
||||
|
||||
Scaffold(
|
||||
scaffoldState = scaffoldState,
|
||||
backgroundColor = colorResource(id = R.color.backgroundLightGray),
|
||||
floatingActionButton = {
|
||||
HangingOverKeyboardView(keyboardState = keyboardAsState()) {
|
||||
AddButton(state)
|
||||
}
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
) { paddings ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(paddings)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(bottom = 90.dp),
|
||||
) {
|
||||
item {
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
elevation = 4.dp,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
) {
|
||||
FormFields(state, closePopupTrigger)
|
||||
}
|
||||
}
|
||||
}
|
||||
item { Warnings(state.value.warnings.toList()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FormFields(state: MutableState<AddCustomTokenState>, closePopupTrigger: ClosePopupTrigger) {
|
||||
val context = LocalContext.current
|
||||
val errorConverter = remember { ModuleMessageConverter(context) }
|
||||
|
||||
val stateValue = state.value
|
||||
stateValue.form.fieldList.forEach { field ->
|
||||
val data = ScreenFieldData.fromState(field, stateValue, errorConverter)
|
||||
when (field.id) {
|
||||
ContractAddress -> TokenContractAddressView(data)
|
||||
Network -> TokenNetworkView(data, stateValue, closePopupTrigger)
|
||||
Name -> TokenNameView(data)
|
||||
Symbol -> TokenSymbolView(data)
|
||||
Decimals -> TokenDecimalsView(data)
|
||||
DerivationPath -> TokenDerivationPathView(data, stateValue, closePopupTrigger)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Warnings(warnings: List<AddCustomTokenError.Warning>) {
|
||||
if (warnings.isEmpty()) return
|
||||
|
||||
val context = LocalContext.current
|
||||
val warningConverter = remember { ModuleMessageConverter(context) }
|
||||
|
||||
Column {
|
||||
warnings.forEachIndexed { index, item ->
|
||||
val modifier = when (index) {
|
||||
0 -> Modifier.padding(vertical = 0.dp)
|
||||
warnings.lastIndex -> Modifier.padding(top = 8.dp, bottom = 16.dp)
|
||||
else -> Modifier.padding(top = 8.dp, bottom = 0.dp)
|
||||
}
|
||||
AddCustomTokenWarning(
|
||||
modifier = modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
warning = item,
|
||||
converter = warningConverter,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddButton(state: MutableState<AddCustomTokenState>) {
|
||||
PrimaryButtonIconStart(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
text = stringResource(id = R.string.common_add),
|
||||
iconResId = R.drawable.ic_plus_24,
|
||||
enabled = state.value.screenState.addButton.isEnabled,
|
||||
onClick = { domainStore.dispatch(AddCustomTokenAction.OnAddCustomTokenClicked) },
|
||||
)
|
||||
}
|
||||
|
||||
data class ScreenFieldData(
|
||||
val field: DataField<*>,
|
||||
val error: AddCustomTokenError?,
|
||||
val errorConverter: ModuleMessageConverter,
|
||||
val viewState: ViewStates.TokenField,
|
||||
) {
|
||||
companion object {
|
||||
fun fromState(
|
||||
field: DataField<*>,
|
||||
state: AddCustomTokenState,
|
||||
errorConverter: ModuleMessageConverter,
|
||||
): ScreenFieldData {
|
||||
return ScreenFieldData(
|
||||
field = field,
|
||||
error = state.getError(field.id),
|
||||
errorConverter = errorConverter,
|
||||
viewState = selectField(field.id, state.screenState),
|
||||
)
|
||||
}
|
||||
|
||||
private fun selectField(id: FieldId, screenState: ScreenState): ViewStates.TokenField {
|
||||
return when (id) {
|
||||
ContractAddress -> screenState.contractAddressField
|
||||
Network -> screenState.network
|
||||
Name -> screenState.name
|
||||
Symbol -> screenState.symbol
|
||||
Decimals -> screenState.decimals
|
||||
DerivationPath -> screenState.derivationPath
|
||||
else -> throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.legacy.compose
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.common.form.Field
|
||||
|
||||
@Composable
|
||||
fun BlockchainSpinner(
|
||||
@StringRes title: Int,
|
||||
itemList: List<Blockchain>,
|
||||
selectedItem: Field.Data<Blockchain>,
|
||||
isEnabled: Boolean = true,
|
||||
textFieldConverter: (Blockchain) -> String,
|
||||
dropdownItemView: @Composable ((Blockchain) -> Unit)? = null,
|
||||
closePopupTrigger: ClosePopupTrigger = ClosePopupTrigger(),
|
||||
onItemSelect: (Blockchain) -> Unit,
|
||||
) {
|
||||
OutlinedSpinner(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = stringResource(id = title),
|
||||
itemList = itemList,
|
||||
selectedItem = selectedItem,
|
||||
textFieldConverter = textFieldConverter,
|
||||
dropdownItemView = dropdownItemView,
|
||||
isEnabled = isEnabled,
|
||||
onItemSelected = onItemSelect,
|
||||
closePopupTrigger = closePopupTrigger,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.legacy.compose
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.domain.redux.global.DomainGlobalAction
|
||||
import com.tangem.domain.redux.global.DomainGlobalState
|
||||
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
@Composable
|
||||
internal fun ComposeDialogManager() {
|
||||
val dialogSate = remember { mutableStateOf<DomainDialog?>(null) }
|
||||
val subscriber = remember {
|
||||
object : StoreSubscriber<DomainGlobalState> {
|
||||
override fun newState(state: DomainGlobalState) {
|
||||
dialogSate.value = state.dialog
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ShowTheDialog(dialogSate)
|
||||
|
||||
LaunchedEffect(
|
||||
key1 = Unit,
|
||||
block = {
|
||||
domainStore.subscribe(subscriber) { state ->
|
||||
state.skipRepeats { oldState, newState ->
|
||||
oldState.globalState == newState.globalState
|
||||
}.select { it.globalState }
|
||||
}
|
||||
},
|
||||
)
|
||||
DisposableEffect(
|
||||
key1 = Unit,
|
||||
effect = {
|
||||
onDispose { domainStore.unsubscribe(subscriber) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowTheDialog(dialogState: MutableState<DomainDialog?>) {
|
||||
if (dialogState.value == null) return
|
||||
|
||||
val context = LocalContext.current
|
||||
val errorConverter = remember { ModuleMessageConverter(context) }
|
||||
val onDismissRequest = { domainStore.dispatch(DomainGlobalAction.ShowDialog(null)) }
|
||||
|
||||
when (val dialog = dialogState.value) {
|
||||
is DomainDialog.DialogError -> ErrorDialog(
|
||||
title = stringResource(id = R.string.common_error),
|
||||
body = errorConverter.convert(dialog.error).message,
|
||||
onDismissRequest,
|
||||
)
|
||||
is DomainDialog.SelectTokenDialog -> SelectTokenNetworkDialog(dialog, onDismissRequest)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog with single item selection
|
||||
*/
|
||||
@Composable
|
||||
fun SimpleDialog(
|
||||
title: String,
|
||||
items: List<CoinsResponse.Coin.Network>,
|
||||
onSelect: (CoinsResponse.Coin.Network) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
itemContent: @Composable (CoinsResponse.Coin.Network) -> Unit,
|
||||
) {
|
||||
Dialog(
|
||||
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false),
|
||||
onDismissRequest = { },
|
||||
) {
|
||||
Surface(modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium) {
|
||||
Column(modifier = Modifier.padding(TangemTheme.dimens.spacing22)) {
|
||||
DialogTitle(title = title)
|
||||
LazyColumn {
|
||||
items(items = items, key = CoinsResponse.Coin.Network::networkId) { item ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp)
|
||||
.clickable {
|
||||
onSelect(item)
|
||||
onDismissRequest()
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) { itemContent(item) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DialogTitle(title: String) {
|
||||
Text(
|
||||
text = title,
|
||||
style = LocalTextStyle.provides(
|
||||
TextStyle(
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp,
|
||||
),
|
||||
).value,
|
||||
)
|
||||
SpacerH16()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ErrorDialog(title: String, body: String, onDismissRequest: () -> Unit) {
|
||||
AlertDialog(
|
||||
title = { DialogTitle(title) },
|
||||
text = { Text(body) },
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
Button(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(id = R.string.common_ok))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.legacy.compose
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.core.ui.components.SpacerH8
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.domain.features.addCustomToken.TokenBlockchainField
|
||||
import com.tangem.domain.features.addCustomToken.TokenDerivationPathField
|
||||
import com.tangem.domain.features.addCustomToken.TokenField
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenContractAddressChanged
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenDecimalsChanged
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenDerivationPathChanged
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenNameChanged
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenNetworkChanged
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenSymbolChanged
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.tap.common.compose.OutlinedTextFieldWidget
|
||||
import com.tangem.tap.common.compose.TitleSubtitle
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun TokenContractAddressView(screenFieldData: ScreenFieldData) {
|
||||
if (!screenFieldData.viewState.isVisible) return
|
||||
|
||||
val tokenField = screenFieldData.field as TokenField
|
||||
|
||||
OutlinedTextFieldWidget(
|
||||
fieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_contract_address_input_title,
|
||||
placeholder = "0x0000000000000000000000000000000000000000",
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
isLoading = screenFieldData.viewState.isLoading,
|
||||
error = screenFieldData.error,
|
||||
errorConverter = screenFieldData.errorConverter,
|
||||
// trailingIcon = { PasteClearButton(showFirst = tokenField.data.value.isEmpty()) }
|
||||
) {
|
||||
domainStore.dispatch(OnTokenContractAddressChanged(Field.Data(it, true)))
|
||||
}
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TokenNameView(screenFieldData: ScreenFieldData) {
|
||||
if (!screenFieldData.viewState.isVisible) return
|
||||
|
||||
val tokenField = screenFieldData.field as TokenField
|
||||
|
||||
OutlinedTextFieldWidget(
|
||||
fieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_name_input_title,
|
||||
placeholderId = R.string.custom_token_name_input_placeholder,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
error = screenFieldData.error,
|
||||
errorConverter = screenFieldData.errorConverter,
|
||||
) {
|
||||
domainStore.dispatch(OnTokenNameChanged(Field.Data(it, true)))
|
||||
}
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TokenNetworkView(
|
||||
screenFieldData: ScreenFieldData,
|
||||
state: AddCustomTokenState,
|
||||
closePopupTrigger: ClosePopupTrigger,
|
||||
) {
|
||||
if (!screenFieldData.viewState.isVisible) return
|
||||
|
||||
val notSelected = stringResource(id = R.string.custom_token_network_input_not_selected)
|
||||
val networkField = screenFieldData.field as TokenBlockchainField
|
||||
|
||||
BlockchainSpinner(
|
||||
title = R.string.custom_token_network_input_title,
|
||||
itemList = networkField.itemList,
|
||||
selectedItem = networkField.data,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
textFieldConverter = { state.blockchainToName(it) ?: notSelected },
|
||||
closePopupTrigger = closePopupTrigger,
|
||||
) { domainStore.dispatch(OnTokenNetworkChanged(Field.Data(it, true))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TokenSymbolView(screenFieldData: ScreenFieldData) {
|
||||
if (!screenFieldData.viewState.isVisible) return
|
||||
|
||||
val tokenField = screenFieldData.field as TokenField
|
||||
|
||||
OutlinedTextFieldWidget(
|
||||
fieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_token_symbol_input_title,
|
||||
placeholderId = R.string.custom_token_token_symbol_input_placeholder,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
error = screenFieldData.error,
|
||||
errorConverter = screenFieldData.errorConverter,
|
||||
) { domainStore.dispatch(OnTokenSymbolChanged(Field.Data(it, true))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TokenDecimalsView(screenFieldData: ScreenFieldData) {
|
||||
if (!screenFieldData.viewState.isVisible) return
|
||||
|
||||
val tokenField = screenFieldData.field as TokenField
|
||||
|
||||
OutlinedTextFieldWidget(
|
||||
fieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_decimals_input_title,
|
||||
placeholder = "8",
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
error = screenFieldData.error,
|
||||
errorConverter = screenFieldData.errorConverter,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
) { domainStore.dispatch(OnTokenDecimalsChanged(Field.Data(it, true))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TokenDerivationPathView(
|
||||
screenFieldData: ScreenFieldData,
|
||||
state: AddCustomTokenState,
|
||||
closePopupTrigger: ClosePopupTrigger,
|
||||
) {
|
||||
if (!screenFieldData.viewState.isVisible) return
|
||||
|
||||
val notSelected = stringResource(id = R.string.custom_token_derivation_path_default)
|
||||
val networkField = screenFieldData.field as TokenDerivationPathField
|
||||
|
||||
BlockchainSpinner(
|
||||
title = R.string.custom_token_derivation_path_input_title,
|
||||
itemList = networkField.itemList,
|
||||
selectedItem = networkField.data,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
textFieldConverter = { state.blockchainToName(it) ?: notSelected },
|
||||
dropdownItemView = { blockchain ->
|
||||
val derivationPathName = state.blockchainToName(blockchain, true) ?: notSelected
|
||||
val blockchainName = state.blockchainToName(blockchain) ?: notSelected
|
||||
TitleSubtitle(derivationPathName, blockchainName)
|
||||
},
|
||||
closePopupTrigger = closePopupTrigger,
|
||||
) { domainStore.dispatch(OnTokenDerivationPathChanged(Field.Data(it, true))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.legacy.compose
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.Keyboard
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun HangingOverKeyboardView(
|
||||
modifier: Modifier = Modifier,
|
||||
keyboardState: State<Keyboard>,
|
||||
spaceBetweenKeyboard: Dp = 0.dp,
|
||||
content: @Composable (BoxScope.() -> Unit),
|
||||
) {
|
||||
val padding = remember(keyboardState) {
|
||||
when (val state = keyboardState.value) {
|
||||
is Keyboard.Closed -> 0.dp
|
||||
is Keyboard.Opened -> state.height + spaceBetweenKeyboard
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier.padding(bottom = padding)) { content() }
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.legacy.compose
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.core.os.postDelayed
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.tap.common.compose.TangemTextFieldsDefault
|
||||
import com.tangem.tap.common.extensions.ValueCallback
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
internal fun <T> OutlinedSpinner(
|
||||
label: String,
|
||||
itemList: List<T>,
|
||||
selectedItem: Field.Data<T>,
|
||||
onItemSelected: ValueCallback<T>,
|
||||
modifier: Modifier = Modifier,
|
||||
textFieldConverter: (T) -> String = { it.toString() },
|
||||
dropdownItemView: @Composable ((T) -> Unit)? = null,
|
||||
isEnabled: Boolean = true,
|
||||
onClose: VoidCallback = {},
|
||||
closePopupTrigger: ClosePopupTrigger = ClosePopupTrigger(),
|
||||
) {
|
||||
val rIsExpanded = remember { mutableStateOf(false) }
|
||||
val stateSelectedItem = remember { mutableStateOf(selectedItem.value) }
|
||||
if (!selectedItem.isUserInput) {
|
||||
stateSelectedItem.value = selectedItem.value
|
||||
}
|
||||
|
||||
val onDropDownItemSelectedInternal: (T) -> Unit = {
|
||||
stateSelectedItem.value = it
|
||||
rIsExpanded.value = false
|
||||
onItemSelected(it)
|
||||
}
|
||||
val onDismissRequest = {
|
||||
rIsExpanded.value = false
|
||||
onClose()
|
||||
}
|
||||
|
||||
closePopupTrigger.close = {
|
||||
onDismissRequest()
|
||||
Handler(Looper.getMainLooper()).postDelayed(100) {
|
||||
closePopupTrigger.onCloseComplete()
|
||||
}
|
||||
}
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = rIsExpanded.value,
|
||||
onExpandedChange = { rIsExpanded.value = !rIsExpanded.value },
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = modifier,
|
||||
readOnly = true,
|
||||
enabled = isEnabled,
|
||||
value = textFieldConverter(stateSelectedItem.value),
|
||||
onValueChange = {},
|
||||
label = { Text(label) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) },
|
||||
colors = TangemTextFieldsDefault.defaultTextFieldColors,
|
||||
)
|
||||
|
||||
if (isEnabled) {
|
||||
ExposedDropdownMenu(expanded = rIsExpanded.value, onDismissRequest = onDismissRequest) {
|
||||
itemList.forEach { item ->
|
||||
key(item) {
|
||||
DropdownMenuItem(onClick = { onDropDownItemSelectedInternal(item) }) {
|
||||
if (dropdownItemView == null) Text(textFieldConverter(item)) else dropdownItemView(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ClosePopupTrigger {
|
||||
var close: () -> Unit = {}
|
||||
var onCloseComplete: () -> Unit = {}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TestSpinnerPreview() {
|
||||
OutlinedSpinner(
|
||||
label = "Blockchain name",
|
||||
itemList = listOf(Blockchain.values()),
|
||||
selectedItem = Field.Data(Blockchain.Avalanche, false),
|
||||
onItemSelected = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.legacy.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.tap.common.compose.TitleSubtitle
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun SelectTokenNetworkDialog(dialog: DomainDialog.SelectTokenDialog, onDismissRequest: () -> Unit) {
|
||||
SimpleDialog(
|
||||
title = stringResource(id = R.string.custom_token_network_input_title),
|
||||
items = dialog.items,
|
||||
onSelect = dialog.onSelect,
|
||||
onDismissRequest = onDismissRequest,
|
||||
) { network -> TitleSubtitle(dialog.networkIdConverter(network.networkId), network.contractAddress ?: "") }
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.legacy.compose.test
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
||||
import com.tangem.domain.redux.domainStore
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun ContractAddressTests(onItemClick: VoidCallback) {
|
||||
val casesInfo = listOf(
|
||||
"USDC on ETH" to "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
|
||||
"BUSD on ETH" to "0x4fabb145d64652a948d72533023f6e7a623c7c53",
|
||||
"ETH on AVALANCHE" to "0xf20d962a6c8f70c731bd838a3a388d7d48fa6e15",
|
||||
"USDC on ETH (invalid - cut address)" to "0xa0b86991c6218b36c1d1",
|
||||
"Custom EVM" to "0x1111111111111111112111111111111111111113",
|
||||
"Supported by several networks" to "0xa1faa113cbe53436df28ff0aee54275c13b40975",
|
||||
"Invalid" to "!@#_ _-%%^&&*((){P P2iOWsdfFQLA",
|
||||
)
|
||||
CasesListContent(casesInfo, onItemClick)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SolanaAddressTests(onItemClick: VoidCallback) {
|
||||
val casesInfo = listOf(
|
||||
"USDT (full)" to "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
|
||||
"USDT (valid - 2/3 of address)" to "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8Ben",
|
||||
"USDT (invalid - 1/3 of address)" to "Es9vMFrzaCERmJ",
|
||||
"ETH (full)" to "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk",
|
||||
)
|
||||
CasesListContent(casesInfo, onItemClick)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CasesListContent(casesList: List<Pair<String, String>>, onItemClick: VoidCallback) {
|
||||
LazyColumn(
|
||||
content = {
|
||||
item {
|
||||
Row {
|
||||
ResetContractAddressButton(onItemClick)
|
||||
Text("", modifier = Modifier.weight(1f))
|
||||
ResetAllFieldsButton(onItemClick)
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
items(casesList.size) {
|
||||
val (info, address) = casesList[it]
|
||||
ContractAddressButton(info, address, onItemClick)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ResetAllFieldsButton(onItemClick: VoidCallback) {
|
||||
ActionButton(
|
||||
name = "Reset",
|
||||
onClick = {
|
||||
onItemClick()
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data("", false)))
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(Blockchain.Unknown, false)))
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data("", false)))
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data("", false)))
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data("", false)))
|
||||
domainStore.dispatch(
|
||||
AddCustomTokenAction.OnTokenDerivationPathChanged(
|
||||
Field.Data(
|
||||
Blockchain.Unknown,
|
||||
false,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ResetContractAddressButton(onItemClick: VoidCallback) {
|
||||
ActionButton(
|
||||
name = "Set empty address",
|
||||
onClick = {
|
||||
onItemClick()
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data("", false)))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContractAddressButton(name: String, address: String, onItemClick: VoidCallback) {
|
||||
ActionButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
name = name,
|
||||
onClick = {
|
||||
onItemClick()
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(address, false)))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActionButton(name: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Button(
|
||||
modifier = modifier.padding(horizontal = 8.dp),
|
||||
onClick = onClick,
|
||||
) { Text(name, fontSize = 12.sp) }
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.legacy.compose.test
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun TestCasesList(onItemClick: (TestCase) -> Unit) {
|
||||
if (!BuildConfig.TEST_ACTION_ENABLED) return
|
||||
|
||||
Surface(color = colorResource(id = R.color.lightGray5)) {
|
||||
Column(Modifier.padding(horizontal = 16.dp)) {
|
||||
listOf(TestCase.ContractAddress, TestCase.SolanaTokens)
|
||||
.map { case -> TestCaseListItem(testCase = case, onItemClick = { onItemClick(case) }) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TestCaseListItem(testCase: TestCase, onItemClick: () -> Unit) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = testCase.description,
|
||||
)
|
||||
Button(
|
||||
onClick = onItemClick,
|
||||
) { Text("Start") }
|
||||
}
|
||||
}
|
||||
|
||||
enum class TestCase(val description: String, val content: @Composable (VoidCallback) -> Unit) {
|
||||
ContractAddress("Test contract address field", { ContractAddressTests(it) }),
|
||||
Auto("Test contract address field", { ContractAddressTests(it) }),
|
||||
SolanaTokens("Test Solana contract addresses", { SolanaAddressTests(it) }),
|
||||
;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.tap.features.details
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
|
||||
class DarkThemeFeatureToggle(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) {
|
||||
val isDarkThemeEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "DARK_THEME_ENABLED")
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.tap.features.details.featuretoggles
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
|
||||
internal class DefaultDetailsFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : DetailsFeatureToggles {
|
||||
|
||||
override val isRedesignedAppCurrencySelectorEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_APP_CURRENCY_SELECTOR_ENABLED")
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.tap.features.details.featuretoggles
|
||||
|
||||
interface DetailsFeatureToggles {
|
||||
|
||||
val isRedesignedAppCurrencySelectorEnabled: Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.tap.features.details.featuretoggles
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object DetailsFeatureTogglesModule {
|
||||
|
||||
@Provides
|
||||
fun provideDetailsFeatureToggles(featureTogglesManager: FeatureTogglesManager): DetailsFeatureToggles {
|
||||
return DefaultDetailsFeatureToggles(featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
package com.tangem.tap.features.details.redux
|
||||
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
|
|
@ -12,7 +13,7 @@ sealed class DetailsAction : Action {
|
|||
|
||||
data class PrepareScreen(
|
||||
val scanResponse: ScanResponse,
|
||||
val wallets: List<Wallet>,
|
||||
val darkThemeSwitchEnabled: Boolean,
|
||||
) : DetailsAction()
|
||||
|
||||
object ReCreateTwinsWallet : DetailsAction()
|
||||
|
|
@ -29,6 +30,14 @@ sealed class DetailsAction : Action {
|
|||
|
||||
data class PrepareCardSettingsData(val card: CardDTO, val cardTypesResolver: CardTypesResolver) : DetailsAction()
|
||||
object ResetCardSettingsData : DetailsAction()
|
||||
object ScanAndSaveUserWallet : DetailsAction() {
|
||||
|
||||
object Success : DetailsAction()
|
||||
|
||||
data class Error(val error: TextReference?) : DetailsAction()
|
||||
}
|
||||
|
||||
object DismissError : DetailsAction()
|
||||
|
||||
sealed class AccessCodeRecovery : DetailsAction() {
|
||||
object Open : AccessCodeRecovery()
|
||||
|
|
@ -72,6 +81,18 @@ sealed class DetailsAction : Action {
|
|||
data class BiometricsStatusChanged(
|
||||
val needEnrollBiometrics: Boolean,
|
||||
) : AppSettings()
|
||||
|
||||
data class ChangeAppThemeMode(
|
||||
val appThemeMode: AppThemeMode,
|
||||
) : AppSettings()
|
||||
|
||||
data class ChangeBalanceHiding(
|
||||
val hideBalance: Boolean,
|
||||
) : AppSettings()
|
||||
|
||||
data class ChangeAppCurrency(
|
||||
val fiatCurrency: FiatCurrency,
|
||||
) : AppSettings()
|
||||
}
|
||||
|
||||
data class ChangeAppCurrency(val fiatCurrency: FiatCurrency) : DetailsAction()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.features.details.redux
|
|||
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnSuccess
|
||||
|
|
@ -10,6 +11,11 @@ import com.tangem.common.flatMap
|
|||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.balancehiding.BalanceHidingSettings
|
||||
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
|
|
@ -19,6 +25,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|||
import com.tangem.domain.wallets.legacy.isLockedSync
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Basic
|
||||
import com.tangem.tap.common.analytics.events.Settings
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
|
|
@ -33,6 +40,7 @@ import com.tangem.tap.features.demo.DemoHelper
|
|||
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -47,7 +55,7 @@ import timber.log.Timber
|
|||
class DetailsMiddleware {
|
||||
private val eraseWalletMiddleware = EraseWalletMiddleware()
|
||||
private val manageSecurityMiddleware = ManageSecurityMiddleware()
|
||||
private val managePrivacyMiddleware = ManagePrivacyMiddleware()
|
||||
private val appSettingsMiddleware = AppSettingsMiddleware()
|
||||
private val accessCodeRecoveryMiddleware = AccessCodeRecoveryMiddleware()
|
||||
val detailsMiddleware: Middleware<AppState> = { _, stateProvider ->
|
||||
{ next ->
|
||||
|
|
@ -67,43 +75,14 @@ class DetailsMiddleware {
|
|||
when (action) {
|
||||
is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action)
|
||||
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action, state)
|
||||
is DetailsAction.AppSettings -> managePrivacyMiddleware.handle(state, action)
|
||||
is DetailsAction.AppSettings -> appSettingsMiddleware.handle(state, action)
|
||||
is DetailsAction.ReCreateTwinsWallet -> {
|
||||
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet))
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins))
|
||||
}
|
||||
is DetailsAction.AccessCodeRecovery -> accessCodeRecoveryMiddleware.handle(state, action)
|
||||
DetailsAction.ScanCard -> {
|
||||
scope.launch {
|
||||
store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor)
|
||||
.scan(allowsRequestAccessCodeFromRepository = true)
|
||||
.doOnSuccess { scanResponse ->
|
||||
// if we use biometric, scanResponse in GlobalState is null, and crashes NPE on twin cards
|
||||
store.dispatch(GlobalAction.SaveScanResponse(scanResponse))
|
||||
val currentUserWalletId = state.scanResponse
|
||||
?.let { UserWalletIdBuilder.scanResponse(it).build() }
|
||||
val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse)
|
||||
.build()
|
||||
val isSameWallet = currentUserWalletId == scannedUserWalletId
|
||||
|
||||
if (isSameWallet) {
|
||||
store.dispatchOnMain(
|
||||
DetailsAction.PrepareCardSettingsData(
|
||||
scanResponse.card,
|
||||
scanResponse.cardTypesResolver,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
store.dispatchDialogShow(
|
||||
AppDialog.SimpleOkDialogRes(
|
||||
headerId = R.string.common_warning,
|
||||
messageId = R.string.error_wrong_wallet_tapped,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is DetailsAction.ScanCard -> scanCard(state)
|
||||
is DetailsAction.ScanAndSaveUserWallet -> scanAndSaveUserWallet()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -207,7 +186,7 @@ class DetailsMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
class ManagePrivacyMiddleware {
|
||||
class AppSettingsMiddleware {
|
||||
fun handle(state: DetailsState, action: DetailsAction.AppSettings) {
|
||||
when (action) {
|
||||
is DetailsAction.AppSettings.SwitchPrivacySetting -> {
|
||||
|
|
@ -226,6 +205,17 @@ class DetailsMiddleware {
|
|||
is DetailsAction.AppSettings.EnrollBiometrics -> {
|
||||
enrollBiometrics()
|
||||
}
|
||||
is DetailsAction.AppSettings.ChangeAppThemeMode -> {
|
||||
changeAppThemeMode(action.appThemeMode)
|
||||
}
|
||||
is DetailsAction.AppSettings.ChangeBalanceHiding -> {
|
||||
changeBalanceHiding(action.hideBalance)
|
||||
}
|
||||
is DetailsAction.AppSettings.ChangeAppCurrency -> {
|
||||
store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrency))
|
||||
store.dispatch(DetailsAction.ChangeAppCurrency(action.fiatCurrency))
|
||||
store.dispatch(WalletSelectorAction.ChangeAppCurrency(action.fiatCurrency))
|
||||
}
|
||||
is DetailsAction.AppSettings.SwitchPrivacySetting.Success,
|
||||
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure,
|
||||
is DetailsAction.AppSettings.BiometricsStatusChanged,
|
||||
|
|
@ -261,6 +251,29 @@ class DetailsMiddleware {
|
|||
store.dispatchOnMain(NavigationAction.OpenBiometricsSettings)
|
||||
}
|
||||
|
||||
private fun changeAppThemeMode(appThemeMode: AppThemeMode) {
|
||||
val repository = store.state.daggerGraphState.get(DaggerGraphState::appThemeModeRepository)
|
||||
|
||||
scope.launch {
|
||||
repository.changeAppThemeMode(appThemeMode)
|
||||
|
||||
store.dispatchWithMain(GlobalAction.ChangeAppThemeMode(appThemeMode))
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeBalanceHiding(hideBalance: Boolean) {
|
||||
val repository = store.state.daggerGraphState.get(DaggerGraphState::balanceHidingRepository)
|
||||
|
||||
scope.launch {
|
||||
val newState = BalanceHidingSettings(
|
||||
isHidingEnabledInSettings = hideBalance,
|
||||
isBalanceHidden = false,
|
||||
)
|
||||
|
||||
repository.storeBalanceHidingSettings(newState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch {
|
||||
// Nothing to change
|
||||
if (preferencesStorage.shouldSaveUserWallets == enable) {
|
||||
|
|
@ -455,4 +468,97 @@ class DetailsMiddleware {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun scanCard(state: DetailsState) = scope.launch {
|
||||
store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor)
|
||||
.scan(allowsRequestAccessCodeFromRepository = true)
|
||||
.doOnSuccess { scanResponse ->
|
||||
// if we use biometric, scanResponse in GlobalState is null, and crashes NPE on twin cards
|
||||
store.dispatch(GlobalAction.SaveScanResponse(scanResponse))
|
||||
val currentUserWalletId = state.scanResponse
|
||||
?.let { UserWalletIdBuilder.scanResponse(it).build() }
|
||||
val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse)
|
||||
.build()
|
||||
val isSameWallet = currentUserWalletId == scannedUserWalletId
|
||||
|
||||
if (isSameWallet) {
|
||||
store.dispatchOnMain(
|
||||
DetailsAction.PrepareCardSettingsData(
|
||||
scanResponse.card,
|
||||
scanResponse.cardTypesResolver,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
store.dispatchDialogShow(
|
||||
AppDialog.SimpleOkDialogRes(
|
||||
headerId = R.string.common_warning,
|
||||
messageId = R.string.error_wrong_wallet_tapped,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun scanAndSaveUserWallet() = scope.launch(Dispatchers.IO) {
|
||||
val cardSdkConfigRepository = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository)
|
||||
|
||||
val prevUseBiometricsForAccessCode = cardSdkConfigRepository.isBiometricsRequestPolicy()
|
||||
|
||||
// Update access code policy for access code saving when a card was scanned
|
||||
cardSdkConfigRepository.setAccessCodeRequestPolicy(
|
||||
isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes,
|
||||
)
|
||||
|
||||
store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan(
|
||||
analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.MyWallets),
|
||||
onWalletNotCreated = {
|
||||
// No need to rollback policy, continue with the policy set before the card scan
|
||||
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success)
|
||||
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet))
|
||||
},
|
||||
disclaimerWillShow = {
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
},
|
||||
onSuccess = { scanResponse ->
|
||||
saveUserWalletAndPopBackToWalletScreen(scanResponse)
|
||||
.doOnFailure { error ->
|
||||
// Rollback policy if card saving was failed
|
||||
cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode)
|
||||
Timber.e(error, "Unable to save user wallet")
|
||||
|
||||
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Error(error.toTextReference()))
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
// Rollback policy if card scanning was failed
|
||||
cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode)
|
||||
Timber.e(error, "Unable to scan card")
|
||||
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Error(error.toTextReference()))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun saveUserWalletAndPopBackToWalletScreen(scanResponse: ScanResponse): CompletionResult<Unit> {
|
||||
val userWallet = UserWalletBuilder(scanResponse).build()
|
||||
?: return CompletionResult.Failure(TangemSdkError.WalletIsNotCreated())
|
||||
|
||||
return userWalletsListManager.save(userWallet)
|
||||
.doOnSuccess {
|
||||
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success)
|
||||
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet))
|
||||
|
||||
val walletFeatureToggles = store.state.daggerGraphState
|
||||
.get(DaggerGraphState::walletFeatureToggles)
|
||||
|
||||
if (!walletFeatureToggles.isRedesignedScreenEnabled) {
|
||||
store.onUserWalletSelected(userWallet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun TangemError.toTextReference(): TextReference? {
|
||||
if (silent) return null
|
||||
|
||||
return messageResId?.let(::resourceReference) ?: stringReference(customMessage)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,13 +8,15 @@ import com.tangem.tap.domain.extensions.signedHashesCount
|
|||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.rekotlin.Action
|
||||
import java.util.*
|
||||
import java.util.EnumSet
|
||||
|
||||
object DetailsReducer {
|
||||
fun reduce(action: Action, state: AppState): DetailsState = internalReduce(action, state)
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun internalReduce(action: Action, state: AppState): DetailsState {
|
||||
if (action !is DetailsAction) return state.detailsState
|
||||
val detailsState = state.detailsState
|
||||
|
|
@ -39,9 +41,25 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
|
|||
is DetailsAction.AppSettings -> {
|
||||
handlePrivacyAction(action, detailsState)
|
||||
}
|
||||
is DetailsAction.ChangeAppCurrency ->
|
||||
detailsState.copy(appCurrency = action.fiatCurrency)
|
||||
is DetailsAction.ChangeAppCurrency -> detailsState.copy(
|
||||
appSettingsState = detailsState.appSettingsState.copy(
|
||||
selectedFiatCurrency = action.fiatCurrency,
|
||||
),
|
||||
)
|
||||
is DetailsAction.AccessCodeRecovery -> handleAccessCodeRecoveryAction(action, detailsState)
|
||||
is DetailsAction.ScanAndSaveUserWallet -> detailsState.copy(
|
||||
isScanningInProgress = true,
|
||||
)
|
||||
is DetailsAction.ScanAndSaveUserWallet.Error -> detailsState.copy(
|
||||
isScanningInProgress = false,
|
||||
error = action.error,
|
||||
)
|
||||
is DetailsAction.ScanAndSaveUserWallet.Success -> detailsState.copy(
|
||||
isScanningInProgress = false,
|
||||
)
|
||||
is DetailsAction.DismissError -> detailsState.copy(
|
||||
error = null,
|
||||
)
|
||||
else -> detailsState
|
||||
}
|
||||
}
|
||||
|
|
@ -49,13 +67,18 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
|
|||
private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsState {
|
||||
return DetailsState(
|
||||
scanResponse = action.scanResponse,
|
||||
wallets = action.wallets,
|
||||
createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup,
|
||||
appCurrency = store.state.globalState.appCurrency,
|
||||
appSettingsState = AppSettingsState(
|
||||
isBiometricsAvailable = tangemSdkManager.canUseBiometry,
|
||||
saveWallets = preferencesStorage.shouldSaveUserWallets,
|
||||
saveAccessCodes = preferencesStorage.shouldSaveAccessCodes,
|
||||
selectedFiatCurrency = store.state.globalState.appCurrency,
|
||||
selectedThemeMode = store.state.globalState.appThemeMode,
|
||||
isHidingEnabled = runBlocking {
|
||||
store.state.daggerGraphState
|
||||
.get { balanceHidingRepository }.getBalanceHidingSettings().isHidingEnabledInSettings
|
||||
},
|
||||
darkThemeSwitchEnabled = action.darkThemeSwitchEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -194,6 +217,21 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
|
|||
needEnrollBiometrics = action.needEnrollBiometrics,
|
||||
),
|
||||
)
|
||||
is DetailsAction.AppSettings.ChangeAppThemeMode -> state.copy(
|
||||
appSettingsState = state.appSettingsState.copy(
|
||||
selectedThemeMode = action.appThemeMode,
|
||||
),
|
||||
)
|
||||
is DetailsAction.AppSettings.ChangeAppCurrency -> state.copy(
|
||||
appSettingsState = state.appSettingsState.copy(
|
||||
selectedFiatCurrency = action.fiatCurrency,
|
||||
),
|
||||
)
|
||||
is DetailsAction.AppSettings.ChangeBalanceHiding -> state.copy(
|
||||
appSettingsState = state.appSettingsState.copy(
|
||||
isHidingEnabled = action.hideBalance,
|
||||
),
|
||||
)
|
||||
is DetailsAction.AppSettings.EnrollBiometrics,
|
||||
is DetailsAction.AppSettings.CheckBiometricsStatus,
|
||||
-> state
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
package com.tangem.tap.features.details.redux
|
||||
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.common.entities.Button
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import org.rekotlin.StateType
|
||||
import java.util.*
|
||||
import java.util.EnumSet
|
||||
|
||||
data class DetailsState(
|
||||
val scanResponse: ScanResponse? = null,
|
||||
val wallets: List<Wallet> = emptyList(),
|
||||
val cardSettingsState: CardSettingsState? = null,
|
||||
val privacyPolicyUrl: String? = null,
|
||||
val createBackupAllowed: Boolean = false,
|
||||
val appCurrency: FiatCurrency = FiatCurrency.Default,
|
||||
val isScanningInProgress: Boolean = false,
|
||||
val error: TextReference? = null,
|
||||
val appSettingsState: AppSettingsState = AppSettingsState(),
|
||||
) : StateType
|
||||
|
||||
|
|
@ -56,7 +57,11 @@ data class AppSettingsState(
|
|||
val saveAccessCodes: Boolean = false,
|
||||
val isBiometricsAvailable: Boolean = false,
|
||||
val needEnrollBiometrics: Boolean = false,
|
||||
val isHidingEnabled: Boolean = false,
|
||||
val isInProgress: Boolean = false,
|
||||
val selectedFiatCurrency: FiatCurrency = FiatCurrency.Default,
|
||||
val selectedThemeMode: AppThemeMode = AppThemeMode.DEFAULT,
|
||||
val darkThemeSwitchEnabled: Boolean = false,
|
||||
)
|
||||
|
||||
enum class SecurityOption { LongTap, PassCode, AccessCode }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.tap.features.details.ui.appcurrency
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.components.SystemBarsEffect
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
internal class AppCurrencySelectorFragment : ComposeFragment() {
|
||||
|
||||
@Inject
|
||||
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
private val viewModel: AppCurrencySelectorViewModel by viewModels()
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
val systemBarsColor = TangemTheme.colors.background.secondary
|
||||
SystemBarsEffect {
|
||||
setSystemBarsColor(systemBarsColor)
|
||||
}
|
||||
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
AppCurrencySelectorScreen(
|
||||
modifier = modifier,
|
||||
state = uiState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.tap.features.details.ui.appcurrency
|
||||
|
||||
internal interface AppCurrencySelectorIntents {
|
||||
|
||||
fun onBackClick()
|
||||
|
||||
fun onSearchClick()
|
||||
|
||||
fun onSearchInputChange(input: String)
|
||||
|
||||
fun onCurrencyClick(currency: AppCurrencySelectorState.Currency)
|
||||
|
||||
fun onDismissSearchClick()
|
||||
}
|
||||
|
|
@ -0,0 +1,363 @@
|
|||
package com.tangem.tap.features.details.ui.appcurrency
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorState.Currency
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun AppCurrencySelectorScreen(state: AppCurrencySelectorState, modifier: Modifier = Modifier) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
topBar = {
|
||||
TopBar(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
scrollBehavior = scrollBehavior,
|
||||
state = state,
|
||||
)
|
||||
},
|
||||
content = { paddingValues ->
|
||||
val contentModifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
|
||||
when (state) {
|
||||
is AppCurrencySelectorState.Loading -> LoadingList(
|
||||
modifier = contentModifier,
|
||||
)
|
||||
is AppCurrencySelectorState.Content -> CurrenciesList(
|
||||
modifier = contentModifier,
|
||||
listState = listState,
|
||||
currencies = state.items,
|
||||
selectedId = state.selectedId.orEmpty(),
|
||||
onCurrencyClick = state.onCurrencyClick,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (state is AppCurrencySelectorState.Content) {
|
||||
EventEffect(event = state.scrollToSelected) {
|
||||
listState.scrollToItem(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
state: AppCurrencySelectorState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TopAppBar(
|
||||
modifier = modifier,
|
||||
scrollBehavior = scrollBehavior,
|
||||
colors = TopAppBarColors,
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing8)
|
||||
.size(TangemTheme.dimens.size32),
|
||||
onClick = state.onBackClick,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
painter = painterResource(id = R.drawable.ic_back_24),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
when (state) {
|
||||
is AppCurrencySelectorState.Search -> SearchBar(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onInputChange = state.onSearchInputChange,
|
||||
)
|
||||
is AppCurrencySelectorState.Loading,
|
||||
is AppCurrencySelectorState.Default,
|
||||
-> Text(
|
||||
text = stringResource(id = R.string.details_row_title_currency),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
when (state) {
|
||||
is AppCurrencySelectorState.Content -> {
|
||||
IconButton(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size32),
|
||||
onClick = state.onTopBarActionClick,
|
||||
) {
|
||||
val iconResId = when (state) {
|
||||
is AppCurrencySelectorState.Default -> R.drawable.ic_search_24
|
||||
is AppCurrencySelectorState.Search -> R.drawable.ic_close_24
|
||||
}
|
||||
val iconTint = when (state) {
|
||||
is AppCurrencySelectorState.Default -> TangemTheme.colors.icon.primary1
|
||||
is AppCurrencySelectorState.Search -> TangemTheme.colors.icon.informative
|
||||
}
|
||||
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
painter = painterResource(id = iconResId),
|
||||
tint = iconTint,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
is AppCurrencySelectorState.Loading -> Unit
|
||||
}
|
||||
SpacerW(width = TangemTheme.dimens.spacing8)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchBar(onInputChange: (String) -> Unit, modifier: Modifier = Modifier) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var input by remember { mutableStateOf(value = "") }
|
||||
|
||||
TextField(
|
||||
modifier = modifier
|
||||
.focusRequester(focusRequester),
|
||||
value = input,
|
||||
onValueChange = { input = it },
|
||||
singleLine = true,
|
||||
textStyle = TangemTheme.typography.subtitle2,
|
||||
placeholder = {
|
||||
Text(text = stringResource(id = R.string.common_search))
|
||||
},
|
||||
colors = SearchBarColors,
|
||||
)
|
||||
|
||||
LaunchedEffect(key1 = input) {
|
||||
onInputChange(input)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingList(modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier) {
|
||||
repeat(times = 10) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing24,
|
||||
)
|
||||
.height(TangemTheme.dimens.size56)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
space = TangemTheme.dimens.spacing16,
|
||||
alignment = Alignment.Start,
|
||||
),
|
||||
) {
|
||||
CircleShimmer(modifier = Modifier.size(TangemTheme.dimens.size24))
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.height(TangemTheme.dimens.size24)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CurrenciesList(
|
||||
listState: LazyListState,
|
||||
currencies: ImmutableList<Currency>,
|
||||
selectedId: String,
|
||||
onCurrencyClick: (Currency) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
state = listState,
|
||||
) {
|
||||
items(
|
||||
items = currencies,
|
||||
key = Currency::id,
|
||||
) { currency ->
|
||||
val onClick = remember(key1 = currency) {
|
||||
{ onCurrencyClick(currency) }
|
||||
}
|
||||
|
||||
CurrencyItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
name = currency.name,
|
||||
isSelected = currency.id == selectedId,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CurrencyItem(name: String, isSelected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = LocalIndication.current,
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing24,
|
||||
)
|
||||
.heightIn(min = TangemTheme.dimens.size56),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
space = TangemTheme.dimens.spacing16,
|
||||
alignment = Alignment.Start,
|
||||
),
|
||||
) {
|
||||
RadioButton(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
selected = isSelected,
|
||||
onClick = onClick,
|
||||
interactionSource = interactionSource,
|
||||
colors = RadioButtonColors,
|
||||
)
|
||||
Text(
|
||||
text = name,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
private val TopAppBarColors: TopAppBarColors
|
||||
@Composable
|
||||
get() = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
// Currently (08.09.23) it's not working when scrolling programmatically
|
||||
scrolledContainerColor = TangemTheme.colors.background.secondary,
|
||||
navigationIconContentColor = TangemTheme.colors.icon.primary1,
|
||||
titleContentColor = TangemTheme.colors.text.primary1,
|
||||
actionIconContentColor = TangemTheme.colors.icon.primary1,
|
||||
)
|
||||
|
||||
private val SearchBarColors: TextFieldColors
|
||||
@Composable
|
||||
get() = TextFieldDefaults.colors(
|
||||
unfocusedContainerColor = TangemTheme.colors.background.secondary,
|
||||
focusedContainerColor = TangemTheme.colors.background.secondary,
|
||||
focusedTextColor = TangemTheme.colors.text.primary1,
|
||||
unfocusedTextColor = TangemTheme.colors.text.secondary,
|
||||
focusedPlaceholderColor = TangemTheme.colors.text.disabled,
|
||||
unfocusedPlaceholderColor = TangemTheme.colors.text.disabled,
|
||||
focusedIndicatorColor = TangemTheme.colors.background.secondary,
|
||||
unfocusedIndicatorColor = TangemTheme.colors.background.secondary,
|
||||
cursorColor = TangemTheme.colors.icon.primary1,
|
||||
)
|
||||
|
||||
private val RadioButtonColors: RadioButtonColors
|
||||
@Composable
|
||||
get() = RadioButtonDefaults.colors(
|
||||
selectedColor = TangemTheme.colors.icon.accent,
|
||||
unselectedColor = TangemTheme.colors.icon.secondary,
|
||||
)
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 720)
|
||||
@Composable
|
||||
private fun AppCurrencySelectorScreenPreview_Light(
|
||||
@PreviewParameter(AppCurrencySelectorStateProvider::class) param: AppCurrencySelectorState,
|
||||
) {
|
||||
TangemTheme(isDark = false) {
|
||||
AppCurrencySelectorScreen(param)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 720)
|
||||
@Composable
|
||||
private fun AppCurrencySelectorScreenPreview_Dark(
|
||||
@PreviewParameter(AppCurrencySelectorStateProvider::class) param: AppCurrencySelectorState,
|
||||
) {
|
||||
TangemTheme(isDark = true) {
|
||||
AppCurrencySelectorScreen(param)
|
||||
}
|
||||
}
|
||||
|
||||
private class AppCurrencySelectorStateProvider : CollectionPreviewParameterProvider<AppCurrencySelectorState>(
|
||||
collection = buildList {
|
||||
val items = listOf(
|
||||
"US Dollar (USD) – $",
|
||||
"Inited Arab Emirates Dirham (AED) – DH",
|
||||
"Argentine Peso (ARS) – $",
|
||||
"Australian Dollar (AUD) – A$",
|
||||
"Bangladeshi Taka (BDT) – ৳",
|
||||
"Bahraini Dinar (BHD) – BD",
|
||||
"Bermudian Dollar (BMD) – $",
|
||||
"Brazil Real (BRL) – R$",
|
||||
"Canadian Dollar (CAD) – CA$",
|
||||
"Swiss Franc (CHF) – Fr",
|
||||
"Chilean Peso (CLP) – CLP$",
|
||||
"Chinese Yan (CNY)",
|
||||
)
|
||||
.mapIndexed { index, s -> Currency(index.toString(), s) }
|
||||
.toPersistentList()
|
||||
|
||||
AppCurrencySelectorState.Loading(onBackClick = {}).let(::add)
|
||||
AppCurrencySelectorState.Default(
|
||||
selectedId = "0",
|
||||
items = items,
|
||||
scrollToSelected = consumedEvent(),
|
||||
onCurrencyClick = {},
|
||||
onBackClick = {},
|
||||
onTopBarActionClick = {},
|
||||
).let(::add)
|
||||
AppCurrencySelectorState.Search(
|
||||
selectedId = "0",
|
||||
items = items,
|
||||
scrollToSelected = consumedEvent(),
|
||||
onCurrencyClick = {},
|
||||
onBackClick = {},
|
||||
onSearchInputChange = {},
|
||||
onTopBarActionClick = {},
|
||||
).let(::add)
|
||||
},
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package com.tangem.tap.features.details.ui.appcurrency
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
||||
@Immutable
|
||||
internal sealed class AppCurrencySelectorState {
|
||||
|
||||
abstract val onBackClick: () -> Unit
|
||||
|
||||
data class Loading(
|
||||
override val onBackClick: () -> Unit,
|
||||
) : AppCurrencySelectorState()
|
||||
|
||||
@Immutable
|
||||
sealed class Content : AppCurrencySelectorState() {
|
||||
abstract val selectedId: String
|
||||
abstract val scrollToSelected: StateEvent<Int>
|
||||
abstract val items: PersistentList<Currency>
|
||||
abstract val onCurrencyClick: (Currency) -> Unit
|
||||
abstract val onTopBarActionClick: () -> Unit
|
||||
|
||||
fun copySealed(
|
||||
selectedId: String? = this.selectedId,
|
||||
items: PersistentList<Currency> = this.items,
|
||||
scrollToSelected: StateEvent<Int> = this.scrollToSelected,
|
||||
onCurrencyClick: (Currency) -> Unit = this.onCurrencyClick,
|
||||
onTopBarActionClick: () -> Unit = this.onTopBarActionClick,
|
||||
): AppCurrencySelectorState = when (this) {
|
||||
is Default -> this.copy(
|
||||
selectedId = selectedId.orEmpty(),
|
||||
items = items,
|
||||
scrollToSelected = scrollToSelected,
|
||||
onCurrencyClick = onCurrencyClick,
|
||||
onTopBarActionClick = onTopBarActionClick,
|
||||
)
|
||||
is Search -> this.copy(
|
||||
selectedId = selectedId.orEmpty(),
|
||||
items = items,
|
||||
scrollToSelected = scrollToSelected,
|
||||
onCurrencyClick = onCurrencyClick,
|
||||
onTopBarActionClick = onTopBarActionClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Default(
|
||||
override val selectedId: String,
|
||||
override val items: PersistentList<Currency>,
|
||||
override val onCurrencyClick: (Currency) -> Unit,
|
||||
override val onBackClick: () -> Unit,
|
||||
override val onTopBarActionClick: () -> Unit,
|
||||
override val scrollToSelected: StateEvent<Int>,
|
||||
) : Content()
|
||||
|
||||
data class Search(
|
||||
override val selectedId: String,
|
||||
override val items: PersistentList<Currency>,
|
||||
override val scrollToSelected: StateEvent<Int>,
|
||||
override val onCurrencyClick: (Currency) -> Unit,
|
||||
override val onBackClick: () -> Unit,
|
||||
override val onTopBarActionClick: () -> Unit,
|
||||
val onSearchInputChange: (String) -> Unit,
|
||||
) : Content()
|
||||
|
||||
data class Currency(
|
||||
val id: String,
|
||||
val name: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
package com.tangem.tap.features.details.ui.appcurrency
|
||||
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.tap.features.details.ui.appcurrency.converter.CurrencyConverter
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.mutate
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
internal class AppCurrencySelectorStateHolder(
|
||||
private val intents: AppCurrencySelectorIntents,
|
||||
private val onSubscription: () -> Unit,
|
||||
stateFlowScope: CoroutineScope,
|
||||
) {
|
||||
|
||||
private var availableCurrencies: PersistentList<AppCurrencySelectorState.Currency> = persistentListOf()
|
||||
|
||||
private val stateFlowInternal: MutableStateFlow<AppCurrencySelectorState> = MutableStateFlow(getInitialState())
|
||||
|
||||
private val currencyConverter by lazy(mode = LazyThreadSafetyMode.NONE) { CurrencyConverter() }
|
||||
|
||||
val stateFlow: StateFlow<AppCurrencySelectorState> = stateFlowInternal
|
||||
.onSubscription { onSubscription() }
|
||||
.stateIn(
|
||||
scope = stateFlowScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = getInitialState(),
|
||||
)
|
||||
|
||||
fun updateStateWithAvailableCurrencies(currencies: List<AppCurrency>) {
|
||||
availableCurrencies = currencyConverter.convertList(currencies).toPersistentList()
|
||||
|
||||
updateState {
|
||||
when (this) {
|
||||
is AppCurrencySelectorState.Loading -> getContentState(availableCurrencies)
|
||||
is AppCurrencySelectorState.Content -> copySealed(items = availableCurrencies)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateStateWithSelectedCurrency(selectedCurrency: AppCurrency, selectedCurrencyIndex: Int) {
|
||||
val selectedCurrencyId = selectedCurrency.code
|
||||
|
||||
updateState {
|
||||
when (this) {
|
||||
is AppCurrencySelectorState.Loading -> this
|
||||
is AppCurrencySelectorState.Content -> copySealed(
|
||||
selectedId = selectedCurrencyId,
|
||||
scrollToSelected = triggeredEvent(selectedCurrencyIndex, ::consumeScrollToSelectedEvent),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateStateWithSearch(input: String = "") {
|
||||
val filteredItems = availableCurrencies.mutate { list ->
|
||||
list.removeAll { input.lowercase() !in it.name.lowercase() }
|
||||
}
|
||||
|
||||
updateState {
|
||||
when (this) {
|
||||
is AppCurrencySelectorState.Loading -> this
|
||||
is AppCurrencySelectorState.Search -> copy(items = filteredItems)
|
||||
is AppCurrencySelectorState.Default -> getSearchState(filteredItems, selectedId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateStateWithoutSearch() {
|
||||
updateState {
|
||||
when (this) {
|
||||
is AppCurrencySelectorState.Search -> getContentState(availableCurrencies, selectedId)
|
||||
is AppCurrencySelectorState.Default,
|
||||
is AppCurrencySelectorState.Loading,
|
||||
-> this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialState() = AppCurrencySelectorState.Loading(
|
||||
onBackClick = intents::onBackClick,
|
||||
)
|
||||
|
||||
private fun getContentState(
|
||||
items: PersistentList<AppCurrencySelectorState.Currency>,
|
||||
selectedCurrencyId: String = "",
|
||||
) = AppCurrencySelectorState.Default(
|
||||
selectedId = selectedCurrencyId,
|
||||
items = items,
|
||||
onBackClick = intents::onBackClick,
|
||||
onCurrencyClick = intents::onCurrencyClick,
|
||||
onTopBarActionClick = intents::onSearchClick,
|
||||
scrollToSelected = consumedEvent(),
|
||||
)
|
||||
|
||||
private fun getSearchState(
|
||||
filteredItems: PersistentList<AppCurrencySelectorState.Currency>,
|
||||
selectedCurrencyId: String,
|
||||
) = AppCurrencySelectorState.Search(
|
||||
selectedId = selectedCurrencyId,
|
||||
items = filteredItems,
|
||||
scrollToSelected = consumedEvent(),
|
||||
onBackClick = intents::onBackClick,
|
||||
onCurrencyClick = intents::onCurrencyClick,
|
||||
onSearchInputChange = intents::onSearchInputChange,
|
||||
onTopBarActionClick = intents::onDismissSearchClick,
|
||||
)
|
||||
|
||||
private inline fun updateState(block: AppCurrencySelectorState.() -> AppCurrencySelectorState) {
|
||||
stateFlowInternal.update(block)
|
||||
}
|
||||
|
||||
private fun consumeScrollToSelectedEvent() {
|
||||
updateState {
|
||||
when (this) {
|
||||
is AppCurrencySelectorState.Loading -> this
|
||||
is AppCurrencySelectorState.Content -> copySealed(scrollToSelected = consumedEvent())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package com.tangem.tap.features.details.ui.appcurrency
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tangem.core.navigation.ReduxNavController
|
||||
import com.tangem.domain.appcurrency.GetAvailableCurrenciesUseCase
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.SelectAppCurrencyUseCase
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class AppCurrencySelectorViewModel @Inject constructor(
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getAvailableCurrenciesUseCase: GetAvailableCurrenciesUseCase,
|
||||
private val selectAppCurrencyUseCase: SelectAppCurrencyUseCase,
|
||||
private val reduxNavController: ReduxNavController,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : ViewModel(), AppCurrencySelectorIntents {
|
||||
|
||||
private val stateController = AppCurrencySelectorStateHolder(
|
||||
intents = this,
|
||||
onSubscription = { fetchCurrencies() },
|
||||
stateFlowScope = viewModelScope,
|
||||
)
|
||||
|
||||
val uiState: StateFlow<AppCurrencySelectorState> = stateController.stateFlow
|
||||
|
||||
override fun onBackClick() {
|
||||
reduxNavController.popBackStack()
|
||||
}
|
||||
|
||||
override fun onSearchClick() {
|
||||
stateController.updateStateWithSearch()
|
||||
}
|
||||
|
||||
override fun onSearchInputChange(input: String) {
|
||||
stateController.updateStateWithSearch(input)
|
||||
}
|
||||
|
||||
override fun onCurrencyClick(currency: AppCurrencySelectorState.Currency) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
selectAppCurrencyUseCase(currency.id)
|
||||
.onRight { reduxNavController.popBackStack() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDismissSearchClick() {
|
||||
stateController.updateStateWithoutSearch()
|
||||
}
|
||||
|
||||
private fun fetchCurrencies() {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val availableCurrencies = getAvailableCurrenciesUseCase()
|
||||
.onRight(stateController::updateStateWithAvailableCurrencies)
|
||||
.getOrNull()
|
||||
|
||||
getSelectedAppCurrencyUseCase().collectLatest { maybeSelectedCurrency ->
|
||||
maybeSelectedCurrency
|
||||
.onRight { selectedCurrency ->
|
||||
val selectedCurrencyIndex = availableCurrencies?.indexOfFirst { it == selectedCurrency }
|
||||
|
||||
if (selectedCurrencyIndex != null && selectedCurrencyIndex != -1) {
|
||||
stateController.updateStateWithSelectedCurrency(selectedCurrency, selectedCurrencyIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.tap.features.details.ui.appcurrency.converter
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorState
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class CurrencyConverter : Converter<AppCurrency, AppCurrencySelectorState.Currency> {
|
||||
|
||||
override fun convert(value: AppCurrency): AppCurrencySelectorState.Currency {
|
||||
val fullCurrencyName = with(value) { "$name ($code) - $symbol" }
|
||||
|
||||
return AppCurrencySelectorState.Currency(value.code, fullCurrencyName)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.tangem.tap.features.details.ui.appsettings
|
||||
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal class AppSettingsDialogsFactory {
|
||||
|
||||
fun createDeleteSavedWalletsAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
|
||||
return Dialog.Alert(
|
||||
title = resourceReference(R.string.common_attention),
|
||||
description = resourceReference(R.string.app_settings_off_saved_wallet_alert_message),
|
||||
confirmText = resourceReference(R.string.common_delete),
|
||||
onConfirm = onDelete,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
fun createDeleteSavedAccessCodesAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
|
||||
return Dialog.Alert(
|
||||
title = resourceReference(R.string.common_attention),
|
||||
description = resourceReference(R.string.app_settings_off_saved_access_code_alert_message),
|
||||
confirmText = resourceReference(R.string.common_delete),
|
||||
onConfirm = onDelete,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
fun createThemeModeSelectorDialog(
|
||||
selectedModeIndex: Int,
|
||||
onSelect: (AppThemeMode) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
): Dialog.Selector {
|
||||
val modes = AppThemeMode.available
|
||||
|
||||
return Dialog.Selector(
|
||||
title = resourceReference(R.string.app_settings_theme_selector_title),
|
||||
selectedItemIndex = selectedModeIndex,
|
||||
items = modes.map { mode ->
|
||||
resourceReference(
|
||||
id = when (mode) {
|
||||
AppThemeMode.FORCE_DARK -> R.string.app_settings_theme_mode_dark
|
||||
AppThemeMode.FORCE_LIGHT -> R.string.app_settings_theme_mode_light
|
||||
AppThemeMode.FOLLOW_SYSTEM -> R.string.app_settings_theme_mode_system
|
||||
},
|
||||
)
|
||||
}.toImmutableList(),
|
||||
onSelect = { index ->
|
||||
val mode = AppThemeMode.available[index]
|
||||
|
||||
onSelect(mode)
|
||||
},
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +1,86 @@
|
|||
package com.tangem.tap.features.details.ui.appsettings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.transition.TransitionInflater
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.tap.features.details.featuretoggles.DetailsFeatureToggles
|
||||
import com.tangem.tap.features.details.redux.DetailsAction
|
||||
import com.tangem.tap.features.details.redux.DetailsState
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.rekotlin.StoreSubscriber
|
||||
import javax.inject.Inject
|
||||
|
||||
class AppSettingsFragment : Fragment(), StoreSubscriber<DetailsState> {
|
||||
private val viewModel = AppSettingsViewModel(store)
|
||||
private var screenState: MutableState<AppSettingsScreenState> =
|
||||
mutableStateOf(viewModel.updateState(store.state.detailsState))
|
||||
@AndroidEntryPoint
|
||||
internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val inflater = TransitionInflater.from(requireContext())
|
||||
enterTransition = inflater.inflateTransition(R.transition.fade)
|
||||
exitTransition = inflater.inflateTransition(R.transition.fade)
|
||||
viewModel.checkBiometricsStatus(lifecycleScope)
|
||||
}
|
||||
@Inject
|
||||
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
isTransitionGroup = true
|
||||
TangemTheme {
|
||||
AppSettingsScreen(
|
||||
state = screenState.value,
|
||||
onBackClick = {
|
||||
store.dispatch(DetailsAction.ResetCardSettingsData)
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
},
|
||||
)
|
||||
}
|
||||
@Inject
|
||||
lateinit var detailsFeatureToggles: DetailsFeatureToggles
|
||||
|
||||
@Inject
|
||||
lateinit var appCurrencyRepository: AppCurrencyRepository
|
||||
|
||||
private val viewModel by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||
AppSettingsViewModel(store, detailsFeatureToggles, appCurrencyRepository)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val inflater = TransitionInflater.from(requireContext())
|
||||
enterTransition = inflater.inflateTransition(R.transition.fade)
|
||||
exitTransition = inflater.inflateTransition(R.transition.fade)
|
||||
viewModel.checkBiometricsStatus(lifecycleScope)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
AppSettingsScreen(
|
||||
modifier = modifier,
|
||||
state = viewModel.uiState,
|
||||
onBackClick = {
|
||||
store.dispatch(DetailsAction.ResetCardSettingsData)
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun TransitionInflater.inflateTransitions(): Boolean {
|
||||
enterTransition = inflateTransition(R.transition.fade)
|
||||
exitTransition = inflateTransition(R.transition.fade)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
store.subscribe(this) { state ->
|
||||
state.skipRepeats { oldState, newState ->
|
||||
oldState.detailsState == newState.detailsState
|
||||
}.select { it.detailsState }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
store.subscribe(this) { state ->
|
||||
state.skipRepeats { oldState, newState ->
|
||||
oldState.detailsState == newState.detailsState
|
||||
}.select { it.detailsState }
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.refreshBiometricsStatus(lifecycleScope)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.refreshBiometricsStatus(lifecycleScope)
|
||||
}
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
store.unsubscribe(this)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
store.unsubscribe(this)
|
||||
}
|
||||
|
||||
override fun newState(state: DetailsState) {
|
||||
if (activity == null || view == null) return
|
||||
screenState.value = viewModel.updateState(state)
|
||||
}
|
||||
}
|
||||
override fun newState(state: DetailsState) {
|
||||
if (activity == null || view == null) return
|
||||
viewModel.updateState(state)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package com.tangem.tap.features.details.ui.appsettings
|
||||
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
|
||||
import com.tangem.wallet.R
|
||||
|
||||
internal class AppSettingsItemsFactory {
|
||||
|
||||
fun createEnrollBiometricsCard(onClick: () -> Unit): Item.Card {
|
||||
return Item.Card(
|
||||
id = "enroll_biometrics_card",
|
||||
title = resourceReference(R.string.app_settings_enable_biometrics_title),
|
||||
description = resourceReference(R.string.app_settings_enable_biometrics_description),
|
||||
iconResId = R.drawable.ic_alert_circle_24,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
fun createSaveWalletsSwitch(
|
||||
isChecked: Boolean,
|
||||
isEnabled: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
): Item.Switch {
|
||||
return Item.Switch(
|
||||
id = "save_wallets_switch",
|
||||
title = resourceReference(R.string.app_settings_saved_wallet),
|
||||
description = resourceReference(R.string.app_settings_saved_wallet_footer),
|
||||
isEnabled = isEnabled,
|
||||
isChecked = isChecked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
}
|
||||
|
||||
fun createSaveAccessCodeSwitch(
|
||||
isChecked: Boolean,
|
||||
isEnabled: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
): Item.Switch {
|
||||
return Item.Switch(
|
||||
id = "save_access_codes_switch",
|
||||
title = resourceReference(R.string.app_settings_saved_access_codes),
|
||||
description = resourceReference(R.string.app_settings_saved_access_codes_footer),
|
||||
isEnabled = isEnabled,
|
||||
isChecked = isChecked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
}
|
||||
|
||||
fun createFlipToHideBalanceSwitch(
|
||||
isChecked: Boolean,
|
||||
isEnabled: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
): Item.Switch {
|
||||
return Item.Switch(
|
||||
id = "flip_to_hide_balance_switch",
|
||||
title = resourceReference(R.string.details_row_title_flip_to_hide),
|
||||
description = resourceReference(R.string.details_row_description_flip_to_hide),
|
||||
isEnabled = isEnabled,
|
||||
isChecked = isChecked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
}
|
||||
|
||||
fun createSelectAppCurrencyButton(currentAppCurrencyName: String, onClick: () -> Unit): Item.Button {
|
||||
return Item.Button(
|
||||
id = "select_app_currency_button",
|
||||
title = resourceReference(R.string.details_row_title_currency),
|
||||
description = stringReference(currentAppCurrencyName),
|
||||
isEnabled = true,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
fun createSelectThemeModeButton(currentThemeMode: AppThemeMode, onClick: () -> Unit): Item.Button {
|
||||
return Item.Button(
|
||||
id = "select_theme_mode_button",
|
||||
title = resourceReference(R.string.app_settings_theme_selector_title),
|
||||
description = resourceReference(
|
||||
id = when (currentThemeMode) {
|
||||
AppThemeMode.FORCE_DARK -> R.string.app_settings_theme_mode_dark
|
||||
AppThemeMode.FORCE_LIGHT -> R.string.app_settings_theme_mode_light
|
||||
AppThemeMode.FOLLOW_SYSTEM -> R.string.app_settings_theme_mode_system
|
||||
},
|
||||
),
|
||||
isEnabled = true,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,233 +1,110 @@
|
|||
package com.tangem.tap.features.details.ui.appsettings
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.SpacerH24
|
||||
import com.tangem.core.ui.components.SpacerH32
|
||||
import com.tangem.core.ui.components.SpacerH4
|
||||
import com.tangem.core.ui.components.SpacerW32
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.details.redux.AppSetting
|
||||
import com.tangem.tap.features.details.ui.appsettings.components.EnrollBiometricsCard
|
||||
import com.tangem.tap.features.details.ui.appsettings.components.SettingsAlertDialog
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
|
||||
import com.tangem.tap.features.details.ui.appsettings.components.*
|
||||
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
|
||||
import com.tangem.tap.features.details.ui.common.TangemSwitch
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> Unit) {
|
||||
internal fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
SettingsScreensScaffold(
|
||||
content = { AppSettings(state = state) },
|
||||
modifier = modifier,
|
||||
content = {
|
||||
when (state) {
|
||||
is AppSettingsScreenState.Content -> AppSettings(state = state)
|
||||
is AppSettingsScreenState.Loading -> Unit
|
||||
}
|
||||
},
|
||||
titleRes = R.string.app_settings_title,
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppSettings(state: AppSettingsScreenState) {
|
||||
var dialogType by remember { mutableStateOf<AppSetting?>(null) }
|
||||
val onDialogStateChange: (AppSetting?) -> Unit = { dialogType = it }
|
||||
|
||||
dialogType?.let {
|
||||
SettingsAlertDialog(
|
||||
element = it,
|
||||
onDialogStateChange = onDialogStateChange,
|
||||
onSettingToggle = { state.onSettingToggled(it, false) },
|
||||
)
|
||||
private fun AppSettings(state: AppSettingsScreenState.Content) {
|
||||
val dialog by rememberUpdatedState(newValue = state.dialog)
|
||||
when (val safeDialog = dialog) {
|
||||
is AppSettingsScreenState.Dialog.Alert -> SettingsAlertDialog(dialog = safeDialog)
|
||||
is AppSettingsScreenState.Dialog.Selector -> SettingsSelectorDialog(dialog = safeDialog)
|
||||
null -> Unit
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
if (state.showEnrollBiometricsCard) {
|
||||
EnrollBiometricsCard(onClick = state.onEnrollBiometrics)
|
||||
SpacerH24()
|
||||
}
|
||||
|
||||
AppSettingsElement(
|
||||
state = state,
|
||||
setting = AppSetting.SaveWallets,
|
||||
onDialogStateChange = onDialogStateChange,
|
||||
)
|
||||
SpacerH32()
|
||||
AppSettingsElement(
|
||||
state = state,
|
||||
setting = AppSetting.SaveAccessCode,
|
||||
onDialogStateChange = onDialogStateChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun AppSettingsElement(
|
||||
state: AppSettingsScreenState,
|
||||
setting: AppSetting,
|
||||
onDialogStateChange: (AppSetting?) -> Unit,
|
||||
) {
|
||||
val titleRes = when (setting) {
|
||||
AppSetting.SaveWallets -> R.string.app_settings_saved_wallet
|
||||
AppSetting.SaveAccessCode -> R.string.app_settings_saved_access_codes
|
||||
}
|
||||
val subtitleRes = when (setting) {
|
||||
AppSetting.SaveWallets -> R.string.app_settings_saved_wallet_footer
|
||||
AppSetting.SaveAccessCode -> R.string.app_settings_saved_access_codes_footer
|
||||
}
|
||||
val checked = state.settings[setting] ?: false
|
||||
|
||||
val titleTextColor by rememberUpdatedState(
|
||||
newValue = if (state.isTogglesEnabled) {
|
||||
TangemTheme.colors.text.primary1
|
||||
} else {
|
||||
TangemTheme.colors.text.secondary
|
||||
},
|
||||
)
|
||||
val descriptionTextColor by rememberUpdatedState(
|
||||
newValue = if (state.isTogglesEnabled) {
|
||||
TangemTheme.colors.text.secondary
|
||||
} else {
|
||||
TangemTheme.colors.text.tertiary
|
||||
},
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing20),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(weight = .9f),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = titleRes),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = titleTextColor,
|
||||
)
|
||||
SpacerH4()
|
||||
Text(
|
||||
text = stringResource(id = subtitleRes),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = descriptionTextColor,
|
||||
)
|
||||
}
|
||||
SpacerW32()
|
||||
TangemSwitch(
|
||||
checked = checked,
|
||||
enabled = state.isTogglesEnabled,
|
||||
onCheckedChange = { isChecked ->
|
||||
onCheckedChange(
|
||||
element = setting,
|
||||
enabled = isChecked,
|
||||
onSettingToggled = state.onSettingToggled,
|
||||
onDialogStateChange = onDialogStateChange,
|
||||
LazyColumn {
|
||||
items(
|
||||
items = state.items,
|
||||
key = Item::id,
|
||||
) { item ->
|
||||
when (item) {
|
||||
is Item.Card -> SettingsCardItem(
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
item = item,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCheckedChange(
|
||||
element: AppSetting,
|
||||
enabled: Boolean,
|
||||
onSettingToggled: (AppSetting, Boolean) -> Unit,
|
||||
onDialogStateChange: (AppSetting?) -> Unit,
|
||||
) {
|
||||
// Show warning if user wants to disable the switch
|
||||
if (!enabled) {
|
||||
onDialogStateChange(element)
|
||||
} else {
|
||||
onSettingToggled(element, true)
|
||||
is Item.Button -> SettingsButtonItem(
|
||||
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8),
|
||||
item = item,
|
||||
)
|
||||
is Item.Switch -> SettingsSwitchItem(
|
||||
modifier = Modifier.padding(
|
||||
vertical = TangemTheme.dimens.spacing16,
|
||||
horizontal = TangemTheme.dimens.spacing20,
|
||||
),
|
||||
item = item,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
private fun AppSettingsScreenSample(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary),
|
||||
) {
|
||||
AppSettingsScreen(
|
||||
state = AppSettingsScreenState(
|
||||
settings = mapOf(
|
||||
AppSetting.SaveWallets to true,
|
||||
AppSetting.SaveAccessCode to false,
|
||||
),
|
||||
showEnrollBiometricsCard = false,
|
||||
isTogglesEnabled = true,
|
||||
onSettingToggled = { _, _ -> },
|
||||
onEnrollBiometrics = {},
|
||||
),
|
||||
onBackClick = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun AppSettingsScreenPreview_Light() {
|
||||
private fun AppSettingsScreenPreview_Light(
|
||||
@PreviewParameter(AppSettingsScreenStateProvider::class) state: AppSettingsScreenState,
|
||||
) {
|
||||
TangemTheme {
|
||||
AppSettingsScreenSample()
|
||||
AppSettingsScreen(state = state, onBackClick = {})
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun AppSettingsScreenPreview_Dark() {
|
||||
private fun AppSettingsScreenPreview_Dark(
|
||||
@PreviewParameter(AppSettingsScreenStateProvider::class) state: AppSettingsScreenState,
|
||||
) {
|
||||
TangemTheme(isDark = true) {
|
||||
AppSettingsScreenSample()
|
||||
AppSettingsScreen(state = state, onBackClick = {})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppSettingsScreen_EnrollBiometrics_Sample(modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier.background(TangemTheme.colors.background.primary)) {
|
||||
AppSettingsScreen(
|
||||
state = AppSettingsScreenState(
|
||||
settings = mapOf(
|
||||
AppSetting.SaveWallets to true,
|
||||
AppSetting.SaveAccessCode to false,
|
||||
),
|
||||
showEnrollBiometricsCard = true,
|
||||
isTogglesEnabled = false,
|
||||
onSettingToggled = { _, _ -> },
|
||||
onEnrollBiometrics = {},
|
||||
),
|
||||
onBackClick = { },
|
||||
private class AppSettingsScreenStateProvider : CollectionPreviewParameterProvider<AppSettingsScreenState>(
|
||||
collection = buildList {
|
||||
val itemsFactory = AppSettingsItemsFactory()
|
||||
val items = persistentListOf(
|
||||
itemsFactory.createEnrollBiometricsCard {},
|
||||
itemsFactory.createSelectAppCurrencyButton(currentAppCurrencyName = "US Dollar") {},
|
||||
itemsFactory.createSaveWalletsSwitch(isChecked = true, isEnabled = true, { _ -> }),
|
||||
itemsFactory.createSaveAccessCodeSwitch(isChecked = false, isEnabled = true) { _ -> },
|
||||
itemsFactory.createFlipToHideBalanceSwitch(isChecked = false, isEnabled = true) { _ -> },
|
||||
itemsFactory.createSelectThemeModeButton(AppThemeMode.DEFAULT, {}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun AppSettingsScreen_EnrollBiometrics_Preview_Light() {
|
||||
TangemTheme {
|
||||
AppSettingsScreen_EnrollBiometrics_Sample()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun AppSettingsScreen_EnrollBiometrics_Preview_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
AppSettingsScreen_EnrollBiometrics_Sample()
|
||||
}
|
||||
}
|
||||
AppSettingsScreenState.Content(
|
||||
items = items,
|
||||
dialog = null,
|
||||
).let(::add)
|
||||
},
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -1,11 +1,70 @@
|
|||
package com.tangem.tap.features.details.ui.appsettings
|
||||
|
||||
import com.tangem.tap.features.details.redux.AppSetting
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class AppSettingsScreenState(
|
||||
val settings: Map<AppSetting, Boolean> = emptyMap(),
|
||||
val showEnrollBiometricsCard: Boolean = false,
|
||||
val isTogglesEnabled: Boolean = false,
|
||||
val onSettingToggled: (AppSetting, Boolean) -> Unit = { _, _ -> /* no-op */ },
|
||||
val onEnrollBiometrics: () -> Unit = { /* no-op */ },
|
||||
)
|
||||
@Immutable
|
||||
internal sealed class AppSettingsScreenState {
|
||||
|
||||
object Loading : AppSettingsScreenState()
|
||||
|
||||
data class Content(
|
||||
val items: ImmutableList<Item>,
|
||||
val dialog: Dialog?,
|
||||
) : AppSettingsScreenState()
|
||||
|
||||
@Immutable
|
||||
sealed class Item {
|
||||
|
||||
abstract val id: String
|
||||
|
||||
data class Card(
|
||||
override val id: String,
|
||||
@DrawableRes val iconResId: Int,
|
||||
val title: TextReference,
|
||||
val description: TextReference,
|
||||
val onClick: () -> Unit,
|
||||
) : Item()
|
||||
|
||||
data class Switch(
|
||||
override val id: String,
|
||||
val title: TextReference,
|
||||
val description: TextReference,
|
||||
val isEnabled: Boolean,
|
||||
val isChecked: Boolean,
|
||||
val onCheckedChange: (Boolean) -> Unit,
|
||||
) : Item()
|
||||
|
||||
data class Button(
|
||||
override val id: String,
|
||||
val title: TextReference,
|
||||
val description: TextReference,
|
||||
val isEnabled: Boolean,
|
||||
val onClick: () -> Unit,
|
||||
) : Item()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed class Dialog {
|
||||
|
||||
abstract val onDismiss: () -> Unit
|
||||
|
||||
data class Alert(
|
||||
val title: TextReference,
|
||||
val description: TextReference,
|
||||
val confirmText: TextReference,
|
||||
val onConfirm: () -> Unit,
|
||||
override val onDismiss: () -> Unit,
|
||||
) : Dialog()
|
||||
|
||||
data class Selector(
|
||||
val title: TextReference,
|
||||
val selectedItemIndex: Int,
|
||||
val items: ImmutableList<TextReference>,
|
||||
val onSelect: (Int) -> Unit,
|
||||
override val onDismiss: () -> Unit,
|
||||
) : Dialog()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +1,57 @@
|
|||
package com.tangem.tap.features.details.ui.appsettings
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.details.featuretoggles.DetailsFeatureToggles
|
||||
import com.tangem.tap.features.details.redux.AppSetting
|
||||
import com.tangem.tap.features.details.redux.AppSettingsState
|
||||
import com.tangem.tap.features.details.redux.DetailsAction
|
||||
import com.tangem.tap.features.details.redux.DetailsState
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.rekotlin.Store
|
||||
|
||||
class AppSettingsViewModel(private val store: Store<AppState>) {
|
||||
internal class AppSettingsViewModel(
|
||||
private val store: Store<AppState>,
|
||||
private val detailsFeatureToggles: DetailsFeatureToggles,
|
||||
private val appCurrencyRepository: AppCurrencyRepository,
|
||||
) {
|
||||
|
||||
fun updateState(state: DetailsState): AppSettingsScreenState {
|
||||
return with(state.appSettingsState) {
|
||||
AppSettingsScreenState(
|
||||
settings = mapOf(
|
||||
AppSetting.SaveWallets to saveWallets,
|
||||
AppSetting.SaveAccessCode to saveAccessCodes,
|
||||
),
|
||||
showEnrollBiometricsCard = needEnrollBiometrics,
|
||||
isTogglesEnabled = !needEnrollBiometrics && !isInProgress,
|
||||
onSettingToggled = { privacySetting, enabled ->
|
||||
onSettingsToggled(privacySetting, enabled)
|
||||
},
|
||||
onEnrollBiometrics = {
|
||||
store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics)
|
||||
},
|
||||
)
|
||||
private val itemsFactory = AppSettingsItemsFactory()
|
||||
private val dialogsFactory = AppSettingsDialogsFactory()
|
||||
|
||||
private val appCurrencyUpdatesJobHolder = JobHolder()
|
||||
|
||||
var uiState: AppSettingsScreenState by mutableStateOf(AppSettingsScreenState.Loading)
|
||||
private set
|
||||
|
||||
init {
|
||||
if (detailsFeatureToggles.isRedesignedAppCurrencySelectorEnabled) {
|
||||
bootstrapAppCurrencyUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSettingsToggled(setting: AppSetting, enable: Boolean) {
|
||||
store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting))
|
||||
fun updateState(state: DetailsState) {
|
||||
uiState = AppSettingsScreenState.Content(
|
||||
items = buildItems(state.appSettingsState),
|
||||
dialog = (uiState as? AppSettingsScreenState.Content)?.dialog,
|
||||
)
|
||||
}
|
||||
|
||||
fun checkBiometricsStatus(lifecycleScope: LifecycleCoroutineScope) {
|
||||
|
|
@ -50,4 +71,146 @@ class AppSettingsViewModel(private val store: Store<AppState>) {
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildItems(state: AppSettingsState): ImmutableList<AppSettingsScreenState.Item> {
|
||||
val items = buildList {
|
||||
if (state.needEnrollBiometrics) {
|
||||
itemsFactory.createEnrollBiometricsCard(onClick = ::enrollBiometrics).let(::add)
|
||||
}
|
||||
|
||||
itemsFactory.createSelectAppCurrencyButton(
|
||||
currentAppCurrencyName = state.selectedFiatCurrency.name,
|
||||
onClick = ::showAppCurrencySelector,
|
||||
).let(::add)
|
||||
|
||||
if (state.isBiometricsAvailable) {
|
||||
val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress
|
||||
|
||||
itemsFactory.createSaveWalletsSwitch(
|
||||
isChecked = state.saveWallets,
|
||||
isEnabled = canUseBiometrics,
|
||||
onCheckedChange = ::onSaveWalletsToggled,
|
||||
).let(::add)
|
||||
|
||||
itemsFactory.createSaveAccessCodeSwitch(
|
||||
isChecked = state.saveAccessCodes,
|
||||
isEnabled = canUseBiometrics,
|
||||
onCheckedChange = ::onSaveAccessCodesToggled,
|
||||
).let(::add)
|
||||
}
|
||||
|
||||
itemsFactory.createFlipToHideBalanceSwitch(
|
||||
isChecked = state.isHidingEnabled,
|
||||
isEnabled = true,
|
||||
onCheckedChange = ::onFlipToHideBalanceToggled,
|
||||
).let(::add)
|
||||
|
||||
itemsFactory.createSelectThemeModeButton(state.selectedThemeMode) {
|
||||
showThemeModeSelector(state.selectedThemeMode)
|
||||
}.let(::add)
|
||||
|
||||
if (state.darkThemeSwitchEnabled) {
|
||||
itemsFactory.createSelectThemeModeButton(state.selectedThemeMode) {
|
||||
showThemeModeSelector(state.selectedThemeMode)
|
||||
}.let(::add)
|
||||
}
|
||||
}
|
||||
|
||||
return items.toImmutableList()
|
||||
}
|
||||
|
||||
private fun enrollBiometrics() {
|
||||
store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics)
|
||||
}
|
||||
|
||||
private fun showAppCurrencySelector() {
|
||||
val action = if (detailsFeatureToggles.isRedesignedAppCurrencySelectorEnabled) {
|
||||
NavigationAction.NavigateTo(AppScreen.AppCurrencySelector)
|
||||
} else {
|
||||
WalletAction.AppCurrencyAction.ChooseAppCurrency
|
||||
}
|
||||
|
||||
store.dispatchOnMain(action)
|
||||
}
|
||||
|
||||
private fun showThemeModeSelector(selectedMode: AppThemeMode) {
|
||||
updateContentState {
|
||||
copy(
|
||||
dialog = dialogsFactory.createThemeModeSelectorDialog(
|
||||
selectedModeIndex = selectedMode.ordinal,
|
||||
onSelect = { mode ->
|
||||
store.dispatchOnMain(DetailsAction.AppSettings.ChangeAppThemeMode(mode))
|
||||
dismissDialog()
|
||||
},
|
||||
onDismiss = ::dismissDialog,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSaveWalletsToggled(isChecked: Boolean) {
|
||||
if (isChecked) {
|
||||
onSettingsToggled(AppSetting.SaveWallets, enable = true)
|
||||
} else {
|
||||
updateContentState {
|
||||
copy(
|
||||
dialog = dialogsFactory.createDeleteSavedWalletsAlert(
|
||||
onDelete = {
|
||||
onSettingsToggled(AppSetting.SaveWallets, enable = false)
|
||||
dismissDialog()
|
||||
},
|
||||
onDismiss = ::dismissDialog,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSaveAccessCodesToggled(isChecked: Boolean) {
|
||||
if (isChecked) {
|
||||
onSettingsToggled(AppSetting.SaveAccessCode, enable = true)
|
||||
} else {
|
||||
updateContentState {
|
||||
copy(
|
||||
dialog = dialogsFactory.createDeleteSavedAccessCodesAlert(
|
||||
onDelete = {
|
||||
onSettingsToggled(AppSetting.SaveAccessCode, enable = false)
|
||||
dismissDialog()
|
||||
},
|
||||
onDismiss = ::dismissDialog,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSettingsToggled(setting: AppSetting, enable: Boolean) {
|
||||
store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting))
|
||||
}
|
||||
|
||||
private fun onFlipToHideBalanceToggled(enable: Boolean) {
|
||||
store.dispatch(DetailsAction.AppSettings.ChangeBalanceHiding(hideBalance = enable))
|
||||
}
|
||||
|
||||
private fun dismissDialog() {
|
||||
updateContentState { copy(dialog = null) }
|
||||
}
|
||||
|
||||
private fun bootstrapAppCurrencyUpdates() {
|
||||
appCurrencyRepository
|
||||
.getSelectedAppCurrency()
|
||||
.onEach {
|
||||
val fiatCurrency = with(it) { FiatCurrency(code, name, symbol) }
|
||||
store.dispatchWithMain(DetailsAction.AppSettings.ChangeAppCurrency(fiatCurrency))
|
||||
}
|
||||
.launchIn(scope)
|
||||
.saveIn(appCurrencyUpdatesJobHolder)
|
||||
}
|
||||
|
||||
private fun updateContentState(block: AppSettingsScreenState.Content.() -> AppSettingsScreenState.Content) {
|
||||
uiState = when (val state = uiState) {
|
||||
is AppSettingsScreenState.Content -> block(state)
|
||||
is AppSettingsScreenState.Loading -> state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,97 +1,60 @@
|
|||
package com.tangem.tap.features.details.ui.appsettings.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.TextButton
|
||||
import com.tangem.core.ui.components.WarningTextButton
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.BasicDialog
|
||||
import com.tangem.core.ui.components.DialogButton
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.details.redux.AppSetting
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Composable
|
||||
internal fun SettingsAlertDialog(
|
||||
element: AppSetting,
|
||||
onDialogStateChange: (AppSetting?) -> Unit,
|
||||
onSettingToggle: () -> Unit,
|
||||
) {
|
||||
val text = when (element) {
|
||||
AppSetting.SaveWallets -> R.string.app_settings_off_saved_wallet_alert_message
|
||||
AppSetting.SaveAccessCode -> R.string.app_settings_off_saved_access_code_alert_message
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { onDialogStateChange(null) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
text = stringResource(id = R.string.common_cancel),
|
||||
onClick = {
|
||||
onDialogStateChange(null)
|
||||
},
|
||||
)
|
||||
},
|
||||
dismissButton = {
|
||||
WarningTextButton(
|
||||
text = stringResource(id = R.string.common_delete),
|
||||
onClick = {
|
||||
onDialogStateChange(null)
|
||||
onSettingToggle()
|
||||
},
|
||||
)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.common_attention),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.h2,
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(id = text),
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
},
|
||||
shape = TangemTheme.shapes.roundedCornersLarge,
|
||||
internal fun SettingsAlertDialog(dialog: Dialog.Alert) {
|
||||
BasicDialog(
|
||||
title = dialog.title.resolveReference(),
|
||||
message = dialog.description.resolveReference(),
|
||||
isDismissable = false,
|
||||
confirmButton = DialogButton(
|
||||
title = dialog.confirmText.resolveReference(),
|
||||
warning = true,
|
||||
onClick = dialog.onConfirm,
|
||||
),
|
||||
dismissButton = DialogButton(
|
||||
title = stringResource(id = R.string.common_cancel),
|
||||
onClick = dialog.onDismiss,
|
||||
),
|
||||
onDismissDialog = dialog.onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
private fun SettingsAlertDialogSample(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary),
|
||||
) {
|
||||
SettingsAlertDialog(
|
||||
element = AppSetting.SaveAccessCode,
|
||||
onDialogStateChange = {},
|
||||
onSettingToggle = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun SettingsAlertDialogPreview_Light() {
|
||||
private fun AlertDialogPreview_Light(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) {
|
||||
TangemTheme {
|
||||
SettingsAlertDialogSample()
|
||||
SettingsAlertDialog(dialog = dialog)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun SettingsAlertDialogPreview_Dark() {
|
||||
private fun AlertDialogPreview_Dark(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) {
|
||||
TangemTheme(isDark = true) {
|
||||
SettingsAlertDialogSample()
|
||||
SettingsAlertDialog(dialog = dialog)
|
||||
}
|
||||
}
|
||||
|
||||
private class AlertDialogProvider : CollectionPreviewParameterProvider<Dialog.Alert>(
|
||||
collection = buildList {
|
||||
val dialogsFactory = AppSettingsDialogsFactory()
|
||||
|
||||
dialogsFactory.createDeleteSavedAccessCodesAlert({}, {}).let(::add)
|
||||
dialogsFactory.createDeleteSavedWalletsAlert({}, {}).let(::add)
|
||||
},
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package com.tangem.tap.features.details.ui.appsettings.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
internal fun SettingsButtonItem(item: Item.Button, modifier: Modifier = Modifier) {
|
||||
Surface(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
color = TangemTheme.colors.background.secondary,
|
||||
onClick = item.onClick,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
horizontal = TangemTheme.dimens.spacing20,
|
||||
vertical = TangemTheme.dimens.spacing8,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = item.title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = item.description.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun ButtonItemPreview_Light(@PreviewParameter(ButtonItemProvider::class) item: Item.Button) {
|
||||
TangemTheme {
|
||||
SettingsButtonItem(item = item)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun ButtonItemPreview_Dark(@PreviewParameter(ButtonItemProvider::class) item: Item.Button) {
|
||||
TangemTheme(isDark = true) {
|
||||
SettingsButtonItem(item = item)
|
||||
}
|
||||
}
|
||||
|
||||
private class ButtonItemProvider : CollectionPreviewParameterProvider<Item.Button>(
|
||||
collection = buildList {
|
||||
val itemsFactory = AppSettingsItemsFactory()
|
||||
|
||||
itemsFactory.createSelectAppCurrencyButton(
|
||||
currentAppCurrencyName = "US Dollar",
|
||||
onClick = { /* no-op */ },
|
||||
).let(::add)
|
||||
},
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -1,11 +1,6 @@
|
|||
package com.tangem.tap.features.details.ui.appsettings.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Surface
|
||||
|
|
@ -14,24 +9,25 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.SpacerH4
|
||||
import com.tangem.core.ui.components.SpacerW16
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
internal fun EnrollBiometricsCard(onClick: () -> Unit) {
|
||||
internal fun SettingsCardItem(item: Item.Card, modifier: Modifier = Modifier) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing8)
|
||||
.fillMaxWidth(),
|
||||
color = TangemTheme.colors.background.primary,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
color = TangemTheme.colors.button.disabled,
|
||||
shape = TangemTheme.shapes.roundedCornersLarge,
|
||||
onClick = onClick,
|
||||
onClick = item.onClick,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(all = 16.dp),
|
||||
|
|
@ -39,20 +35,20 @@ internal fun EnrollBiometricsCard(onClick: () -> Unit) {
|
|||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_alert_circle_24),
|
||||
painter = painterResource(id = item.iconResId),
|
||||
tint = TangemTheme.colors.icon.attention,
|
||||
contentDescription = null,
|
||||
)
|
||||
SpacerW16()
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(id = R.string.app_settings_enable_biometrics_title),
|
||||
text = item.title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
SpacerH4()
|
||||
Text(
|
||||
text = stringResource(id = R.string.app_settings_enable_biometrics_description),
|
||||
text = item.description.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
|
|
@ -62,28 +58,29 @@ internal fun EnrollBiometricsCard(onClick: () -> Unit) {
|
|||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
private fun EnrollBiometricsCardSample(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.background(TangemTheme.colors.background.secondary),
|
||||
) {
|
||||
EnrollBiometricsCard(onClick = {})
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun EnrollBiometricsCardPreview_Light() {
|
||||
private fun CardItemPreview_Light(@PreviewParameter(CardItemProvider::class) item: Item.Card) {
|
||||
TangemTheme {
|
||||
EnrollBiometricsCardSample()
|
||||
SettingsCardItem(item = item)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun EnrollBiometricsCardPreview_Dark() {
|
||||
private fun CardItemPreview_Dark(@PreviewParameter(CardItemProvider::class) item: Item.Card) {
|
||||
TangemTheme(isDark = true) {
|
||||
EnrollBiometricsCardSample()
|
||||
SettingsCardItem(item = item)
|
||||
}
|
||||
}
|
||||
|
||||
private class CardItemProvider : CollectionPreviewParameterProvider<Item.Card>(
|
||||
collection = buildList {
|
||||
val itemsFactory = AppSettingsItemsFactory()
|
||||
|
||||
itemsFactory.createEnrollBiometricsCard(
|
||||
onClick = { /* no-op */ },
|
||||
).let(::add)
|
||||
},
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.tangem.tap.features.details.ui.appsettings.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.DialogButton
|
||||
import com.tangem.core.ui.components.SelectorDialog
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun SettingsSelectorDialog(dialog: Dialog.Selector) {
|
||||
SelectorDialog(
|
||||
title = dialog.title.resolveReference(),
|
||||
selectedItemIndex = dialog.selectedItemIndex,
|
||||
items = dialog.items.map { it.resolveReference() }.toImmutableList(),
|
||||
confirmButton = DialogButton(
|
||||
title = stringResource(R.string.common_cancel),
|
||||
onClick = dialog.onDismiss,
|
||||
),
|
||||
onSelect = dialog.onSelect,
|
||||
onDismissDialog = dialog.onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun SettingsSelectorDialogPreview_Light(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) {
|
||||
TangemTheme(isDark = false) {
|
||||
SettingsSelectorDialog(param)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun SettingsSelectorDialogPreview_Dark(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) {
|
||||
TangemTheme(isDark = true) {
|
||||
SettingsSelectorDialog(param)
|
||||
}
|
||||
}
|
||||
|
||||
private class DialogProvider : CollectionPreviewParameterProvider<Dialog.Selector>(
|
||||
collection = listOf(
|
||||
AppSettingsDialogsFactory().createThemeModeSelectorDialog(
|
||||
selectedModeIndex = 0,
|
||||
onSelect = {},
|
||||
onDismiss = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package com.tangem.tap.features.details.ui.appsettings.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.SpacerH4
|
||||
import com.tangem.core.ui.components.SpacerW32
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
|
||||
import com.tangem.tap.features.details.ui.common.TangemSwitch
|
||||
|
||||
@Composable
|
||||
internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier) {
|
||||
val titleTextColor by rememberUpdatedState(
|
||||
newValue = if (item.isEnabled) {
|
||||
TangemTheme.colors.text.primary1
|
||||
} else {
|
||||
TangemTheme.colors.text.secondary
|
||||
},
|
||||
)
|
||||
val descriptionTextColor by rememberUpdatedState(
|
||||
newValue = if (item.isEnabled) {
|
||||
TangemTheme.colors.text.secondary
|
||||
} else {
|
||||
TangemTheme.colors.text.tertiary
|
||||
},
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(weight = .9f),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
text = item.title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = titleTextColor,
|
||||
)
|
||||
SpacerH4()
|
||||
Text(
|
||||
text = item.description.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = descriptionTextColor,
|
||||
)
|
||||
}
|
||||
SpacerW32()
|
||||
TangemSwitch(
|
||||
checked = item.isChecked,
|
||||
enabled = item.isEnabled,
|
||||
onCheckedChange = item.onCheckedChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun SwitchItemPreview_Light(@PreviewParameter(SwitchItemProvider::class) item: Item.Switch) {
|
||||
TangemTheme {
|
||||
SettingsSwitchItem(item = item)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun SwitchItemPreview_Dark(@PreviewParameter(SwitchItemProvider::class) item: Item.Switch) {
|
||||
TangemTheme(isDark = true) {
|
||||
SettingsSwitchItem(item = item)
|
||||
}
|
||||
}
|
||||
|
||||
private class SwitchItemProvider : CollectionPreviewParameterProvider<Item.Switch>(
|
||||
collection = buildList {
|
||||
val itemsFactory = AppSettingsItemsFactory()
|
||||
|
||||
itemsFactory.createSaveAccessCodeSwitch(
|
||||
isChecked = true,
|
||||
isEnabled = true,
|
||||
onCheckedChange = { /* no-op */ },
|
||||
).let(::add)
|
||||
itemsFactory.createSaveAccessCodeSwitch(
|
||||
isChecked = false,
|
||||
isEnabled = true,
|
||||
onCheckedChange = { /* no-op */ },
|
||||
).let(::add)
|
||||
itemsFactory.createSaveAccessCodeSwitch(
|
||||
isChecked = true,
|
||||
isEnabled = false,
|
||||
onCheckedChange = { /* no-op */ },
|
||||
).let(::add)
|
||||
itemsFactory.createSaveAccessCodeSwitch(
|
||||
isChecked = false,
|
||||
isEnabled = false,
|
||||
onCheckedChange = { /* no-op */ },
|
||||
).let(::add)
|
||||
},
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -1,51 +1,49 @@
|
|||
package com.tangem.tap.features.details.ui.cardsettings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.transition.TransitionInflater
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
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.features.details.redux.DetailsAction
|
||||
import com.tangem.tap.features.details.redux.DetailsState
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.rekotlin.StoreSubscriber
|
||||
import javax.inject.Inject
|
||||
|
||||
class CardSettingsFragment : Fragment(), StoreSubscriber<DetailsState> {
|
||||
@AndroidEntryPoint
|
||||
internal class CardSettingsFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
|
||||
|
||||
@Inject
|
||||
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
private val viewModel = CardSettingsViewModel(store)
|
||||
|
||||
private var screenState: MutableState<CardSettingsScreenState> =
|
||||
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState))
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val inflater = TransitionInflater.from(requireContext())
|
||||
enterTransition = inflater.inflateTransition(android.R.transition.fade)
|
||||
exitTransition = inflater.inflateTransition(android.R.transition.fade)
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
CardSettingsScreen(
|
||||
modifier = modifier,
|
||||
state = screenState.value,
|
||||
onBackClick = {
|
||||
store.dispatch(DetailsAction.ResetCardSettingsData)
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
isTransitionGroup = true
|
||||
TangemTheme {
|
||||
CardSettingsScreen(
|
||||
state = screenState.value,
|
||||
onBackClick = {
|
||||
store.dispatch(DetailsAction.ResetCardSettingsData)
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun TransitionInflater.inflateTransitions(): Boolean {
|
||||
enterTransition = inflateTransition(R.transition.fade)
|
||||
exitTransition = inflateTransition(R.transition.fade)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
|
|
|||
|
|
@ -2,13 +2,7 @@ package com.tangem.tap.features.details.ui.cardsettings
|
|||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
|
|
@ -18,7 +12,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -29,10 +22,15 @@ import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
|
|||
import com.tangem.wallet.R
|
||||
|
||||
@Composable
|
||||
fun CardSettingsScreen(state: CardSettingsScreenState, onBackClick: () -> Unit) {
|
||||
internal fun CardSettingsScreen(
|
||||
state: CardSettingsScreenState,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val needReadCard = state.cardDetails == null
|
||||
|
||||
SettingsScreensScaffold(
|
||||
modifier = modifier,
|
||||
content = {
|
||||
if (needReadCard) {
|
||||
CardSettingsReadCard(state.onScanCardClick)
|
||||
|
|
@ -41,14 +39,13 @@ fun CardSettingsScreen(state: CardSettingsScreenState, onBackClick: () -> Unit)
|
|||
}
|
||||
},
|
||||
titleRes = R.string.card_settings_title,
|
||||
backgroundColor = TangemTheme.colors.background.secondary,
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
|
||||
private fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
|
|
@ -84,13 +81,13 @@ fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
|
|||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.scan_card_settings_title),
|
||||
color = colorResource(id = R.color.text_primary_1),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.h3,
|
||||
)
|
||||
Spacer(modifier = Modifier.size(20.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.scan_card_settings_message),
|
||||
color = colorResource(id = R.color.text_secondary),
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
style = TangemTheme.typography.body1,
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
|
|
@ -107,7 +104,7 @@ fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
|
|||
|
||||
@Suppress("ComplexMethod")
|
||||
@Composable
|
||||
fun CardSettings(state: CardSettingsScreenState) {
|
||||
private fun CardSettings(state: CardSettingsScreenState) {
|
||||
if (state.cardDetails == null) return
|
||||
|
||||
LazyColumn(
|
||||
|
|
@ -166,8 +163,25 @@ fun CardSettings(state: CardSettingsScreenState) {
|
|||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview
|
||||
private fun CardSettingsPreview() {
|
||||
private fun CardSettingsScreenStateSample() {
|
||||
CardSettingsScreen(state = CardSettingsScreenState(onScanCardClick = {}) {}, {})
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun CardSettingsScreenStatePreview_Light() {
|
||||
TangemTheme {
|
||||
CardSettingsScreenStateSample()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun CardSettingsScreenStatePreview_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
CardSettingsScreenStateSample()
|
||||
}
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -11,14 +11,14 @@ import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText
|
|||
import com.tangem.wallet.R
|
||||
import com.tangem.tap.features.details.redux.CardInfo as ReduxCardInfo
|
||||
|
||||
data class CardSettingsScreenState(
|
||||
internal data class CardSettingsScreenState(
|
||||
val cardDetails: List<CardInfo>? = null,
|
||||
val accessCodeRecoveryState: AccessCodeRecoveryState? = null,
|
||||
val onScanCardClick: () -> Unit,
|
||||
val onElementClick: (CardInfo) -> Unit,
|
||||
)
|
||||
|
||||
sealed class CardInfo(
|
||||
internal sealed class CardInfo(
|
||||
val titleRes: TextReference,
|
||||
val subtitle: TextReference,
|
||||
val clickable: Boolean = false,
|
||||
|
|
@ -68,7 +68,7 @@ sealed class CardInfo(
|
|||
}
|
||||
|
||||
// TODO("Remove and use the same from coreUI")
|
||||
sealed interface TextReference {
|
||||
internal sealed interface TextReference {
|
||||
class Res(@StringRes val id: Int, val formatArgs: List<Any> = emptyList()) : TextReference {
|
||||
constructor(@StringRes id: Int, vararg formatArgs: Any) : this(id, formatArgs.toList())
|
||||
}
|
||||
|
|
@ -78,7 +78,7 @@ sealed interface TextReference {
|
|||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun TextReference.resolveReference(): String {
|
||||
internal fun TextReference.resolveReference(): String {
|
||||
return when (this) {
|
||||
is TextReference.Res -> stringResource(this.id, *this.formatArgs.toTypedArray())
|
||||
is TextReference.Str -> this.value
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import com.tangem.tap.features.details.redux.CardSettingsState
|
|||
import com.tangem.tap.features.details.redux.DetailsAction
|
||||
import org.rekotlin.Store
|
||||
|
||||
class CardSettingsViewModel(private val store: Store<AppState>) {
|
||||
internal class CardSettingsViewModel(private val store: Store<AppState>) {
|
||||
|
||||
fun updateState(state: CardSettingsState?): CardSettingsScreenState {
|
||||
return if (state?.manageSecurityState == null) {
|
||||
|
|
|
|||
|
|
@ -1,33 +1,41 @@
|
|||
package com.tangem.tap.features.details.ui.common
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
||||
import com.tangem.core.ui.components.SystemBarsEffect
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Composable
|
||||
fun SettingsScreensScaffold(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit,
|
||||
background: @Composable (() -> Unit)? = null,
|
||||
fab: @Composable (() -> Unit)? = null,
|
||||
backgroundColor: Color = TangemTheme.colors.background.secondary,
|
||||
titleRes: Int? = null,
|
||||
internal fun SettingsScreensScaffold(
|
||||
onBackClick: () -> Unit,
|
||||
content: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@StringRes titleRes: Int? = null,
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
fab: @Composable () -> Unit = {},
|
||||
) {
|
||||
BackHandler(true, onBackClick)
|
||||
val state = rememberScaffoldState(snackbarHostState = snackbarHostState)
|
||||
val backgroundColor = TangemTheme.colors.background.secondary
|
||||
|
||||
BackHandler(onBack = onBackClick)
|
||||
SystemBarsEffect {
|
||||
setSystemBarsColor(backgroundColor)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
scaffoldState = state,
|
||||
topBar = {
|
||||
EmptyTopBarWithNavigation(
|
||||
onBackClick = onBackClick,
|
||||
|
|
@ -36,34 +44,32 @@ fun SettingsScreensScaffold(
|
|||
},
|
||||
modifier = modifier.systemBarsPadding(),
|
||||
backgroundColor = backgroundColor,
|
||||
floatingActionButton = { fab?.invoke() },
|
||||
) {
|
||||
if (titleRes != null) {
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
background?.invoke()
|
||||
|
||||
Column(modifier = modifier.fillMaxWidth()) {
|
||||
floatingActionButton = fab,
|
||||
content = { paddings ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddings)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
if (titleRes != null) {
|
||||
Text(
|
||||
text = stringResource(id = titleRes),
|
||||
modifier = modifier.padding(
|
||||
start = TangemTheme.dimens.spacing20,
|
||||
end = TangemTheme.dimens.spacing20,
|
||||
bottom = TangemTheme.dimens.spacing54,
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing20)
|
||||
.padding(bottom = TangemTheme.dimens.spacing36),
|
||||
style = TangemTheme.typography.h1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
content()
|
||||
}
|
||||
|
||||
content()
|
||||
}
|
||||
} else {
|
||||
content()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) {
|
||||
internal fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = stringResource(id = titleRes),
|
||||
modifier = modifier.padding(start = 20.dp, end = 20.dp),
|
||||
|
|
@ -73,7 +79,7 @@ fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun EmptyTopBarWithNavigation(
|
||||
internal fun EmptyTopBarWithNavigation(
|
||||
onBackClick: () -> Unit,
|
||||
backgroundColor: Color = TangemTheme.colors.background.primary,
|
||||
) {
|
||||
|
|
@ -95,7 +101,12 @@ fun EmptyTopBarWithNavigation(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun DetailsMainButton(title: String, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true) {
|
||||
internal fun DetailsMainButton(
|
||||
title: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
PrimaryButtonIconEnd(
|
||||
text = title,
|
||||
enabled = enabled,
|
||||
|
|
@ -107,7 +118,7 @@ fun DetailsMainButton(title: String, onClick: () -> Unit, modifier: Modifier = M
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean, onClick: () -> Unit) {
|
||||
internal fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean, onClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -122,8 +133,8 @@ fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean
|
|||
onClick = null,
|
||||
modifier = Modifier.padding(end = 20.dp),
|
||||
colors = RadioButtonDefaults.colors(
|
||||
unselectedColor = colorResource(id = R.color.icon_secondary),
|
||||
selectedColor = colorResource(id = R.color.icon_accent),
|
||||
unselectedColor = TangemTheme.colors.icon.secondary,
|
||||
selectedColor = TangemTheme.colors.icon.accent,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -131,13 +142,13 @@ fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean
|
|||
Text(
|
||||
text = title,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = colorResource(id = R.color.text_primary_1),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = colorResource(id = R.color.text_secondary),
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,12 @@
|
|||
package com.tangem.tap.features.details.ui.common
|
||||
|
||||
import androidx.compose.animation.animateColor
|
||||
import androidx.compose.animation.core.FastOutLinearInEasing
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.animateDp
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.indication
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -25,17 +15,16 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
fun TangemSwitch(
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
checkedColor: Color = colorResource(id = R.color.control_checked),
|
||||
uncheckedColor: Color = colorResource(id = R.color.icon_informative),
|
||||
checkedColor: Color = TangemTheme.colors.icon.accent,
|
||||
uncheckedColor: Color = TangemTheme.colors.icon.informative,
|
||||
size: Dp = 48.dp,
|
||||
checked: Boolean = false,
|
||||
enabled: Boolean = true,
|
||||
|
|
|
|||
|
|
@ -1,45 +1,53 @@
|
|||
package com.tangem.tap.features.details.ui.details
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.transition.TransitionInflater
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
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.Settings
|
||||
import com.tangem.tap.features.details.DarkThemeFeatureToggle
|
||||
import com.tangem.tap.features.details.redux.DetailsState
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.rekotlin.StoreSubscriber
|
||||
import javax.inject.Inject
|
||||
|
||||
class DetailsFragment : Fragment(), StoreSubscriber<DetailsState> {
|
||||
@AndroidEntryPoint
|
||||
internal class DetailsFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
|
||||
|
||||
private val detailsViewModel = DetailsViewModel(store)
|
||||
@Inject
|
||||
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
@Inject
|
||||
lateinit var darkThemeFeatureToggle: DarkThemeFeatureToggle
|
||||
|
||||
private lateinit var detailsViewModel: DetailsViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
detailsViewModel = DetailsViewModel(store, darkThemeFeatureToggle)
|
||||
Analytics.send(Settings.ScreenOpened())
|
||||
val inflater = TransitionInflater.from(requireContext())
|
||||
enterTransition = inflater.inflateTransition(R.transition.fade)
|
||||
exitTransition = inflater.inflateTransition(R.transition.fade)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
isTransitionGroup = true
|
||||
TangemTheme {
|
||||
DetailsScreen(
|
||||
state = detailsViewModel.detailsScreenState.value,
|
||||
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
DetailsScreen(
|
||||
modifier = modifier,
|
||||
state = detailsViewModel.detailsScreenState.value,
|
||||
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
|
||||
)
|
||||
}
|
||||
|
||||
override fun TransitionInflater.inflateTransitions(): Boolean {
|
||||
enterTransition = inflateTransition(R.transition.fade)
|
||||
exitTransition = inflateTransition(R.transition.fade)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
|
|
|||
|
|
@ -1,156 +1,133 @@
|
|||
package com.tangem.tap.features.details.ui.details
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.SnackbarHost
|
||||
import androidx.compose.material.SnackbarHostState
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.SystemBarsEffect
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerHMax
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.details.ui.common.ScreenTitle
|
||||
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit) {
|
||||
SystemBarsEffect {
|
||||
setSystemBarsColor(color = TangemColorPalette.Light1)
|
||||
}
|
||||
internal fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
SettingsScreensScaffold(
|
||||
modifier = modifier,
|
||||
snackbarHostState = snackbarHostState,
|
||||
content = { Content(state = state) },
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
|
||||
ShowSnackbarIfNeeded(
|
||||
snackbarHostState = snackbarHostState,
|
||||
messageEvent = state.showSnackbar,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Content(state: DetailsScreenState) {
|
||||
Box {
|
||||
private fun Content(state: DetailsScreenState, modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
ScreenTitle(titleRes = R.string.details_title, Modifier.padding(bottom = 52.dp))
|
||||
state.elements.map { element ->
|
||||
if (element == SettingsElement.WalletConnect) {
|
||||
WalletConnectDetailsItem(onItemsClick = state.onItemsClick)
|
||||
} else {
|
||||
DetailsItem(
|
||||
item = element,
|
||||
appCurrency = state.appCurrency,
|
||||
onItemsClick = { state.onItemsClick(element) },
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
TangemSocialAccounts(state.tangemLinks, state.onSocialNetworkClick)
|
||||
Spacer(modifier = Modifier.size(12.dp))
|
||||
Text(
|
||||
text = "${stringResource(id = state.appNameRes)} ${state.tangemVersion}",
|
||||
style = TangemTheme.typography.caption,
|
||||
color = colorResource(id = R.color.text_tertiary),
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 40.dp),
|
||||
ScreenTitle(titleRes = R.string.details_title)
|
||||
SpacerH(height = TangemTheme.dimens.spacing36)
|
||||
SettingsItems(
|
||||
items = state.elements,
|
||||
)
|
||||
SpacerHMax()
|
||||
TangemSocialAccounts(
|
||||
links = state.tangemLinks,
|
||||
onSocialNetworkClick = state.onSocialNetworkClick,
|
||||
)
|
||||
SpacerH(height = TangemTheme.dimens.spacing12)
|
||||
TangemAppVersion(
|
||||
appNameRes = state.appNameRes,
|
||||
version = state.tangemVersion,
|
||||
)
|
||||
SpacerH(height = TangemTheme.dimens.spacing16)
|
||||
}
|
||||
ShowSnackbarIfNeeded(state.showErrorSnackbar.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WalletConnectDetailsItem(onItemsClick: (SettingsElement) -> Unit) {
|
||||
private fun SettingsItems(items: List<SettingsItem>) {
|
||||
items.forEach { item ->
|
||||
if (item.isLarge) {
|
||||
LargeDetailsItem(item)
|
||||
} else {
|
||||
DetailsItem(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LargeDetailsItem(item: SettingsItem) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minHeight = 84.dp)
|
||||
.fillMaxWidth()
|
||||
.clickable { onItemsClick(SettingsElement.WalletConnect) },
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
.clickable(onClick = item.onClick)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing20)
|
||||
.heightIn(min = TangemTheme.dimens.size84)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing20),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_walletconnect),
|
||||
contentDescription = stringResource(id = R.string.wallet_connect_title),
|
||||
modifier = Modifier.padding(start = 20.dp, end = 20.dp),
|
||||
tint = colorResource(id = R.color.all_colors_azure),
|
||||
)
|
||||
if (item.showProgress) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
color = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
painter = painterResource(id = item.iconResId),
|
||||
contentDescription = item.title.resolveReference(),
|
||||
tint = TangemColorPalette.Azure,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.defaultMinSize(minHeight = 56.dp),
|
||||
modifier = Modifier.heightIn(min = TangemTheme.dimens.size56),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
space = TangemTheme.dimens.spacing4,
|
||||
alignment = Alignment.CenterVertically,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.wallet_connect_title),
|
||||
modifier = Modifier.padding(end = 20.dp, bottom = 4.dp),
|
||||
text = item.title.resolveReference(),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = colorResource(id = R.color.text_primary_1),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.wallet_connect_subtitle),
|
||||
modifier = Modifier.padding(end = 20.dp, bottom = 4.dp),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = colorResource(id = R.color.text_secondary),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(56.dp)
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onItemsClick),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = item.iconRes),
|
||||
contentDescription = stringResource(id = item.titleRes),
|
||||
modifier = Modifier.padding(start = 20.dp, end = 20.dp),
|
||||
tint = colorResource(id = R.color.icon_secondary),
|
||||
)
|
||||
Column(modifier = Modifier.padding(end = 20.dp)) {
|
||||
Text(
|
||||
text = stringResource(id = item.titleRes),
|
||||
modifier = Modifier,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = colorResource(id = R.color.text_primary_1),
|
||||
)
|
||||
if (item == SettingsElement.AppCurrency) {
|
||||
if (item.subtitle != null) {
|
||||
Text(
|
||||
text = appCurrency,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = colorResource(id = R.color.text_secondary),
|
||||
text = item.subtitle.resolveReference(),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -158,66 +135,159 @@ fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick: () ->
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun TangemSocialAccounts(links: List<SocialNetworkLink>, onSocialNetworkClick: (SocialNetworkLink) -> Unit) {
|
||||
LazyRow(
|
||||
modifier = Modifier.padding(start = 8.dp, end = 8.dp),
|
||||
private fun DetailsItem(item: SettingsItem) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(enabled = !item.showProgress, onClick = item.onClick)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing20)
|
||||
.heightIn(min = TangemTheme.dimens.size56)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing20),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
items(links) {
|
||||
if (item.showProgress) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
color = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(id = it.network.iconRes),
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.clickable { onSocialNetworkClick(it) },
|
||||
tint = colorResource(id = R.color.icon_informative),
|
||||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
painter = painterResource(id = item.iconResId),
|
||||
contentDescription = item.title.resolveReference(),
|
||||
tint = TangemTheme.colors.icon.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BoxScope.ShowSnackbarIfNeeded(snackbarErrorState: EventError) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
SnackbarHost(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(vertical = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
hostState = snackbarHostState,
|
||||
)
|
||||
val errorTitle = when (snackbarErrorState) {
|
||||
is EventError.DemoReferralNotAvailable -> stringResource(id = R.string.alert_demo_feature_disabled)
|
||||
EventError.Empty -> ""
|
||||
}
|
||||
if (snackbarErrorState != EventError.Empty) {
|
||||
SideEffect {
|
||||
coroutineScope.launch {
|
||||
snackbarHostState.showSnackbar(errorTitle)
|
||||
}
|
||||
when (snackbarErrorState) {
|
||||
is EventError.DemoReferralNotAvailable -> snackbarErrorState.onErrorShow.invoke()
|
||||
else -> {
|
||||
/*no-op*/
|
||||
}
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.SpaceAround,
|
||||
) {
|
||||
Text(
|
||||
text = item.title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
if (item.subtitle != null) {
|
||||
Text(
|
||||
text = item.subtitle.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun Preview() {
|
||||
DetailsScreen(
|
||||
state = DetailsScreenState(
|
||||
elements = SettingsElement.values().toList(),
|
||||
private fun TangemSocialAccounts(links: List<SocialNetworkLink>, onSocialNetworkClick: (SocialNetworkLink) -> Unit) {
|
||||
LazyRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
items(links) {
|
||||
val onClick = remember(it) {
|
||||
{ onSocialNetworkClick(it) }
|
||||
}
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing4)
|
||||
.size(TangemTheme.dimens.size32),
|
||||
onClick = onClick,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
painter = painterResource(id = it.network.iconRes),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowSnackbarIfNeeded(snackbarHostState: SnackbarHostState, messageEvent: StateEvent<TextReference>) {
|
||||
var message: TextReference? by remember { mutableStateOf(value = null) }
|
||||
val resolvedMessage by rememberUpdatedState(newValue = message?.resolveReference())
|
||||
|
||||
LaunchedEffect(resolvedMessage) {
|
||||
resolvedMessage?.let {
|
||||
snackbarHostState.showSnackbar(it)
|
||||
}
|
||||
}
|
||||
|
||||
EventEffect(messageEvent) {
|
||||
message = it
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TangemAppVersion(appNameRes: Int, version: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
text = "${stringResource(id = appNameRes)} $version",
|
||||
style = TangemTheme.typography.caption,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 900)
|
||||
@Composable
|
||||
private fun DetailsScreenPreview_Light(
|
||||
@PreviewParameter(DetailsScreenStateProvider::class) param: DetailsScreenState,
|
||||
) {
|
||||
TangemTheme(isDark = false) {
|
||||
DetailsScreen(param, onBackClick = {})
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 900)
|
||||
@Composable
|
||||
private fun DetailsScreenPreview_Dark(@PreviewParameter(DetailsScreenStateProvider::class) param: DetailsScreenState) {
|
||||
TangemTheme(isDark = true) {
|
||||
DetailsScreen(param, onBackClick = {})
|
||||
}
|
||||
}
|
||||
|
||||
private class DetailsScreenStateProvider : CollectionPreviewParameterProvider<DetailsScreenState>(
|
||||
collection = buildList {
|
||||
DetailsScreenState(
|
||||
elements = buildList {
|
||||
SettingsItem.WalletConnect({}).let(::add)
|
||||
SettingsItem.AddWallet(showProgress = false, {}).let(::add)
|
||||
SettingsItem.LinkMoreCards({}).let(::add)
|
||||
SettingsItem.CardSettings({}).let(::add)
|
||||
SettingsItem.AppSettings({}).let(::add)
|
||||
SettingsItem.Chat({}).let(::add)
|
||||
SettingsItem.SendFeedback({}).let(::add)
|
||||
SettingsItem.ReferralProgram({}).let(::add)
|
||||
SettingsItem.TermsOfService({}).let(::add)
|
||||
}.toImmutableList(),
|
||||
tangemLinks = TangemSocialAccounts.accountsEn,
|
||||
tangemVersion = "Tangem 2.14.12 (343)",
|
||||
appCurrency = "Dollar",
|
||||
onItemsClick = {},
|
||||
showSnackbar = consumedEvent(),
|
||||
onSocialNetworkClick = {},
|
||||
),
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
).let(::add)
|
||||
|
||||
DetailsScreenState(
|
||||
elements = buildList {
|
||||
SettingsItem.WalletConnect({}).let(::add)
|
||||
SettingsItem.AddWallet(showProgress = true, {}).let(::add)
|
||||
SettingsItem.CardSettings({}).let(::add)
|
||||
SettingsItem.AppSettings({}).let(::add)
|
||||
SettingsItem.Chat({}).let(::add)
|
||||
SettingsItem.SendFeedback({}).let(::add)
|
||||
SettingsItem.TermsOfService({}).let(::add)
|
||||
}.toImmutableList(),
|
||||
tangemLinks = TangemSocialAccounts.accountsRu,
|
||||
tangemVersion = "Tangem 2.14.12 (343)",
|
||||
showSnackbar = consumedEvent(),
|
||||
onSocialNetworkClick = {},
|
||||
).let(::add)
|
||||
},
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -1,82 +1,164 @@
|
|||
package com.tangem.tap.features.details.ui.details
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Immutable
|
||||
data class DetailsScreenState(
|
||||
val elements: List<SettingsElement>,
|
||||
val tangemLinks: List<SocialNetworkLink>,
|
||||
internal data class DetailsScreenState(
|
||||
val elements: ImmutableList<SettingsItem>,
|
||||
val tangemLinks: ImmutableList<SocialNetworkLink>,
|
||||
val tangemVersion: String,
|
||||
val appCurrency: String,
|
||||
val onItemsClick: (SettingsElement) -> Unit,
|
||||
val showSnackbar: StateEvent<TextReference>,
|
||||
val onSocialNetworkClick: (SocialNetworkLink) -> Unit,
|
||||
val showErrorSnackbar: MutableState<EventError> = mutableStateOf(EventError.Empty),
|
||||
) {
|
||||
val appNameRes: Int = R.string.tangem_app_name
|
||||
}
|
||||
|
||||
@Immutable
|
||||
enum class SettingsElement(
|
||||
val iconRes: Int,
|
||||
val titleRes: Int,
|
||||
internal sealed class SettingsItem(
|
||||
@DrawableRes val iconResId: Int,
|
||||
val title: TextReference,
|
||||
val subtitle: TextReference? = null,
|
||||
val isLarge: Boolean = false,
|
||||
) {
|
||||
WalletConnect(R.drawable.ic_walletconnect, R.string.wallet_connect_title),
|
||||
Chat(R.drawable.ic_chat, R.string.details_chat),
|
||||
SendFeedback(R.drawable.ic_comment, R.string.details_row_title_send_feedback),
|
||||
ReferralProgram(R.drawable.ic_add_friends, R.string.details_referral_title),
|
||||
CardSettings(R.drawable.ic_card_settings, R.string.card_settings_title),
|
||||
AppCurrency(R.drawable.ic_currency, R.string.details_row_title_currency),
|
||||
AppSettings(R.drawable.ic_settings, R.string.app_settings_title),
|
||||
LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup),
|
||||
TermsOfService(R.drawable.ic_text, R.string.disclaimer_title), // General Terms of Service of the App
|
||||
PrivacyPolicy(R.drawable.ic_lock_24, R.string.details_row_privacy_policy),
|
||||
TesterMenu(R.drawable.ic_alert_24, R.string.tester_menu),
|
||||
|
||||
abstract val onClick: () -> Unit
|
||||
|
||||
open val showProgress: Boolean = false
|
||||
|
||||
data class WalletConnect(
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_walletconnect,
|
||||
title = resourceReference(R.string.wallet_connect_title),
|
||||
subtitle = resourceReference(R.string.wallet_connect_subtitle),
|
||||
isLarge = true,
|
||||
)
|
||||
|
||||
data class AddWallet(
|
||||
override val showProgress: Boolean,
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_plus_24,
|
||||
title = stringReference(value = "Add new wallet"),
|
||||
)
|
||||
|
||||
data class ScanWallet(
|
||||
override val showProgress: Boolean,
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_plus_24,
|
||||
title = stringReference(value = "Scan new wallet"),
|
||||
)
|
||||
|
||||
data class LinkMoreCards(
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_more_cards,
|
||||
title = resourceReference(R.string.details_row_title_create_backup),
|
||||
)
|
||||
|
||||
data class CardSettings(
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_card_settings,
|
||||
title = resourceReference(R.string.card_settings_title),
|
||||
)
|
||||
|
||||
data class AppSettings(
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_settings,
|
||||
title = resourceReference(R.string.app_settings_title),
|
||||
)
|
||||
|
||||
data class Chat(
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_chat,
|
||||
title = resourceReference(R.string.details_chat),
|
||||
)
|
||||
|
||||
data class SendFeedback(
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_comment,
|
||||
title = resourceReference(R.string.details_row_title_send_feedback),
|
||||
)
|
||||
|
||||
data class ReferralProgram(
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_add_friends,
|
||||
title = resourceReference(R.string.details_referral_title),
|
||||
)
|
||||
|
||||
data class TermsOfService(
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_text,
|
||||
title = resourceReference(R.string.disclaimer_title),
|
||||
)
|
||||
|
||||
data class TesterMenu(
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_alert_24,
|
||||
title = resourceReference(R.string.tester_menu),
|
||||
)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class SocialNetworkLink(
|
||||
internal data class SocialNetworkLink(
|
||||
val network: SocialNetwork,
|
||||
val url: String,
|
||||
)
|
||||
|
||||
sealed class EventError {
|
||||
internal sealed class EventError {
|
||||
object Empty : EventError()
|
||||
data class DemoReferralNotAvailable(val onErrorShow: () -> Unit) : EventError()
|
||||
}
|
||||
|
||||
sealed class SocialNetwork(val id: String, val iconRes: Int) {
|
||||
object Telegram : SocialNetwork("Telegram", R.drawable.ic_telegram)
|
||||
object Twitter : SocialNetwork("Twitter", R.drawable.ic_twitter)
|
||||
object Facebook : SocialNetwork("Facebook", R.drawable.ic_facebook)
|
||||
object Telegram : SocialNetwork("Telegram", R.drawable.ic_telegram)
|
||||
object Discord : SocialNetwork("Discord", R.drawable.ic_discord)
|
||||
object Reddit : SocialNetwork("Reddit", R.drawable.ic_reddit)
|
||||
object Instagram : SocialNetwork("Instagram", R.drawable.ic_instagram)
|
||||
object GitHub : SocialNetwork("GitHub", R.drawable.ic_github)
|
||||
object YouTube : SocialNetwork("YouTube", R.drawable.ic_youtube)
|
||||
object Facebook : SocialNetwork("Facebook", R.drawable.ic_facebook)
|
||||
object LinkedIn : SocialNetwork("LinkedIn", R.drawable.ic_linkedin)
|
||||
object Discord : SocialNetwork("Discord", R.drawable.ic_discord)
|
||||
object YouTube : SocialNetwork("YouTube", R.drawable.ic_youtube)
|
||||
}
|
||||
|
||||
object TangemSocialAccounts {
|
||||
val accountsEn: List<SocialNetworkLink> = listOf(
|
||||
internal object TangemSocialAccounts {
|
||||
val accountsEn: ImmutableList<SocialNetworkLink> = persistentListOf(
|
||||
SocialNetworkLink(SocialNetwork.Twitter, "https://x.com/tangem"),
|
||||
SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat"),
|
||||
SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"),
|
||||
SocialNetworkLink(SocialNetwork.Facebook, "https://m.facebook.com/TangemCards/"),
|
||||
SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/tangem"),
|
||||
SocialNetworkLink(SocialNetwork.Reddit, "https://www.reddit.com/r/Tangem/"),
|
||||
SocialNetworkLink(SocialNetwork.Instagram, "https://instagram.com/tangemcards"),
|
||||
SocialNetworkLink(SocialNetwork.GitHub, "https://github.com/tangem"),
|
||||
SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"),
|
||||
SocialNetworkLink(SocialNetwork.Facebook, "https://facebook.com/TangemCards/"),
|
||||
SocialNetworkLink(SocialNetwork.LinkedIn, "https://www.linkedin.com/company/tangem"),
|
||||
SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/7AqTVyqdGS"),
|
||||
SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/@tangem3890"),
|
||||
)
|
||||
val accountsRu: List<SocialNetworkLink> = listOf(
|
||||
val accountsRu: ImmutableList<SocialNetworkLink> = persistentListOf(
|
||||
SocialNetworkLink(SocialNetwork.Twitter, "https://x.com/tangem"),
|
||||
SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat_ru"),
|
||||
SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"),
|
||||
SocialNetworkLink(SocialNetwork.Facebook, "https://m.facebook.com/TangemCards/"),
|
||||
SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/tangem"),
|
||||
SocialNetworkLink(SocialNetwork.Reddit, "https://www.reddit.com/r/Tangem/"),
|
||||
SocialNetworkLink(SocialNetwork.Instagram, "https://instagram.com/tangemcards"),
|
||||
SocialNetworkLink(SocialNetwork.GitHub, "https://github.com/tangem"),
|
||||
SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"),
|
||||
SocialNetworkLink(SocialNetwork.Facebook, "https://facebook.com/TangemCards/"),
|
||||
SocialNetworkLink(SocialNetwork.LinkedIn, "https://www.linkedin.com/company/tangem"),
|
||||
SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/7AqTVyqdGS"),
|
||||
SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/@tangem3890"),
|
||||
)
|
||||
}
|
||||
|
|
@ -5,111 +5,112 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.tap.common.analytics.events.Settings
|
||||
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||
import com.tangem.tap.common.feedback.FeedbackEmail
|
||||
import com.tangem.tap.common.feedback.SupportInfo
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.features.details.DarkThemeFeatureToggle
|
||||
import com.tangem.tap.features.details.redux.DetailsAction
|
||||
import com.tangem.tap.features.details.redux.DetailsState
|
||||
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
|
||||
import com.tangem.tap.features.home.LocaleRegionProvider
|
||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.rekotlin.Store
|
||||
|
||||
class DetailsViewModel(private val store: Store<AppState>) {
|
||||
internal class DetailsViewModel(
|
||||
private val store: Store<AppState>,
|
||||
private val darkThemeFeatureToggle: DarkThemeFeatureToggle,
|
||||
) { // TODO: change to Android ViewModel
|
||||
|
||||
var detailsScreenState: MutableState<DetailsScreenState> = mutableStateOf(updateState(store.state.detailsState))
|
||||
private set
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
fun updateState(state: DetailsState): DetailsScreenState {
|
||||
val cardTypesResolver = state.scanResponse?.cardTypesResolver
|
||||
val settings = SettingsElement.values().mapNotNull {
|
||||
when (it) {
|
||||
SettingsElement.WalletConnect -> {
|
||||
if (cardTypesResolver?.isMultiwalletAllowed() == true) it else null
|
||||
}
|
||||
SettingsElement.SendFeedback -> it
|
||||
SettingsElement.LinkMoreCards -> if (state.createBackupAllowed) it else null
|
||||
SettingsElement.PrivacyPolicy -> {
|
||||
if (state.privacyPolicyUrl != null) it else null
|
||||
}
|
||||
SettingsElement.AppSettings -> if (state.appSettingsState.isBiometricsAvailable) it else null
|
||||
SettingsElement.AppCurrency -> if (cardTypesResolver?.isMultiwalletAllowed() != true) it else null
|
||||
SettingsElement.ReferralProgram -> if (cardTypesResolver?.isTangemWallet() == true) it else null
|
||||
SettingsElement.TesterMenu -> if (BuildConfig.TESTER_MENU_ENABLED) it else null
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
init {
|
||||
bootstrapScreenState()
|
||||
}
|
||||
|
||||
fun updateState(state: DetailsState): DetailsScreenState {
|
||||
return DetailsScreenState(
|
||||
elements = settings,
|
||||
elements = createSettingsItems(state),
|
||||
tangemLinks = getSocialLinks(),
|
||||
tangemVersion = getTangemAppVersion(),
|
||||
appCurrency = state.appCurrency.name,
|
||||
onItemsClick = { handleClickingSettingsItem(it) },
|
||||
onSocialNetworkClick = { handleSocialNetworkClick(it) },
|
||||
showSnackbar = triggerErrorSnackbarIfNeeded(state.error),
|
||||
onSocialNetworkClick = ::handleSocialNetworkClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleSocialNetworkClick(link: SocialNetworkLink) {
|
||||
Analytics.send(Settings.ButtonSocialNetwork(link.network))
|
||||
store.dispatch(NavigationAction.OpenUrl(link.url))
|
||||
private fun createSettingsItems(state: DetailsState): ImmutableList<SettingsItem> {
|
||||
val scanResponse = state.scanResponse ?: return persistentListOf()
|
||||
val cardTypesResolver = scanResponse.cardTypesResolver
|
||||
|
||||
return buildList {
|
||||
SettingsItem.WalletConnect(::navigateToWalletConnect)
|
||||
.takeIf { cardTypesResolver.isMultiwalletAllowed() }
|
||||
?.let(::add)
|
||||
|
||||
SettingsItem.AddWallet(showProgress = state.isScanningInProgress, ::scanAndSaveUserWallet)
|
||||
.takeIf { state.appSettingsState.saveWallets }
|
||||
?.let(::add)
|
||||
|
||||
SettingsItem.ScanWallet(showProgress = state.isScanningInProgress, ::scanAndSaveUserWallet)
|
||||
.takeUnless { state.appSettingsState.saveWallets }
|
||||
?.let(::add)
|
||||
|
||||
SettingsItem.LinkMoreCards(::linkMoreCards)
|
||||
.takeIf { state.createBackupAllowed }
|
||||
?.let(::add)
|
||||
|
||||
SettingsItem.CardSettings(::navigateToCardSettings)
|
||||
.let(::add)
|
||||
|
||||
SettingsItem.AppSettings(::navigateToAppSettings)
|
||||
.let(::add)
|
||||
|
||||
SettingsItem.Chat(::navigateToChat)
|
||||
.let(::add)
|
||||
|
||||
SettingsItem.SendFeedback(::sendFeedback)
|
||||
.let(::add)
|
||||
|
||||
SettingsItem.ReferralProgram(::navigateToReferralProgram)
|
||||
.takeIf { cardTypesResolver.isTangemWallet() }
|
||||
?.let(::add)
|
||||
|
||||
SettingsItem.TermsOfService(::navigateToToS)
|
||||
.let(::add)
|
||||
|
||||
SettingsItem.TesterMenu(::navigateToTesterMenu)
|
||||
.takeIf { BuildConfig.TESTER_MENU_ENABLED }
|
||||
?.let(::add)
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
private fun handleClickingSettingsItem(item: SettingsElement) {
|
||||
when (item) {
|
||||
SettingsElement.WalletConnect -> {
|
||||
Analytics.send(Settings.ButtonWalletConnect())
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
|
||||
}
|
||||
SettingsElement.Chat -> {
|
||||
Analytics.send(Settings.ButtonChat())
|
||||
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
|
||||
}
|
||||
SettingsElement.SendFeedback -> {
|
||||
Analytics.send(Settings.ButtonSendFeedback())
|
||||
store.dispatch(GlobalAction.SendEmail(FeedbackEmail()))
|
||||
}
|
||||
SettingsElement.CardSettings -> {
|
||||
Analytics.send(Settings.ButtonCardSettings())
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings))
|
||||
}
|
||||
SettingsElement.AppCurrency -> {
|
||||
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
|
||||
}
|
||||
SettingsElement.AppSettings -> {
|
||||
Analytics.send(Settings.ButtonAppSettings())
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings))
|
||||
}
|
||||
SettingsElement.LinkMoreCards -> {
|
||||
Analytics.send(Settings.ButtonCreateBackup())
|
||||
store.dispatch(WalletAction.MultiWallet.BackupWallet)
|
||||
}
|
||||
SettingsElement.TermsOfService -> {
|
||||
store.dispatch(DisclaimerAction.Show(AppScreen.Details))
|
||||
}
|
||||
SettingsElement.PrivacyPolicy -> {
|
||||
// TODO: To be available later
|
||||
}
|
||||
SettingsElement.ReferralProgram -> {
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.ReferralProgram))
|
||||
}
|
||||
SettingsElement.TesterMenu -> {
|
||||
store.state.daggerGraphState.testerRouter?.startTesterScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSocialLinks(): List<SocialNetworkLink> {
|
||||
val locale = LocaleRegionProvider().getRegion()
|
||||
return if (locale.lowercase() == RUSSIA_COUNTRY_CODE) {
|
||||
TangemSocialAccounts.accountsRu
|
||||
private fun triggerErrorSnackbarIfNeeded(text: TextReference?): StateEvent<TextReference> {
|
||||
return if (text == null) {
|
||||
consumedEvent()
|
||||
} else {
|
||||
TangemSocialAccounts.accountsEn
|
||||
triggeredEvent(text) {
|
||||
store.dispatch(DetailsAction.DismissError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -118,4 +119,79 @@ class DetailsViewModel(private val store: Store<AppState>) {
|
|||
val versionName: String = BuildConfig.VERSION_NAME
|
||||
return "$versionName ($versionCode)"
|
||||
}
|
||||
|
||||
private fun navigateToTesterMenu() {
|
||||
store.state.daggerGraphState.testerRouter?.startTesterScreen()
|
||||
}
|
||||
|
||||
private fun navigateToToS() {
|
||||
store.dispatch(DisclaimerAction.Show(AppScreen.Details))
|
||||
}
|
||||
|
||||
private fun navigateToReferralProgram() {
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.ReferralProgram))
|
||||
}
|
||||
|
||||
private fun sendFeedback() {
|
||||
Analytics.send(Settings.ButtonSendFeedback())
|
||||
store.dispatch(GlobalAction.SendEmail(FeedbackEmail()))
|
||||
}
|
||||
|
||||
private fun navigateToChat() {
|
||||
Analytics.send(Settings.ButtonChat())
|
||||
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
|
||||
}
|
||||
|
||||
private fun navigateToAppSettings() {
|
||||
Analytics.send(Settings.ButtonAppSettings())
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings))
|
||||
}
|
||||
|
||||
private fun navigateToCardSettings() {
|
||||
Analytics.send(Settings.ButtonCardSettings())
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings))
|
||||
}
|
||||
|
||||
private fun linkMoreCards() {
|
||||
Analytics.send(Settings.ButtonCreateBackup())
|
||||
store.dispatch(WalletAction.MultiWallet.BackupWallet)
|
||||
}
|
||||
|
||||
private fun scanAndSaveUserWallet() {
|
||||
store.dispatch(DetailsAction.ScanAndSaveUserWallet)
|
||||
}
|
||||
|
||||
private fun navigateToWalletConnect() {
|
||||
Analytics.send(Settings.ButtonWalletConnect())
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
|
||||
}
|
||||
|
||||
private fun handleSocialNetworkClick(link: SocialNetworkLink) {
|
||||
Analytics.send(Settings.ButtonSocialNetwork(link.network))
|
||||
store.dispatch(NavigationAction.OpenUrl(link.url))
|
||||
}
|
||||
|
||||
private fun getSocialLinks(): ImmutableList<SocialNetworkLink> {
|
||||
val locale = LocaleRegionProvider().getRegion()
|
||||
return if (locale.lowercase() == RUSSIA_COUNTRY_CODE) {
|
||||
TangemSocialAccounts.accountsRu
|
||||
} else {
|
||||
TangemSocialAccounts.accountsEn
|
||||
}
|
||||
}
|
||||
|
||||
private fun bootstrapScreenState() {
|
||||
userWalletsListManager.selectedUserWallet
|
||||
.distinctUntilChanged()
|
||||
.onEach { selectedUserWallet ->
|
||||
store.dispatchWithMain(
|
||||
DetailsAction.PrepareScreen(
|
||||
selectedUserWallet.scanResponse,
|
||||
darkThemeFeatureToggle.isDarkThemeEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.launchIn(scope)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +1,45 @@
|
|||
package com.tangem.tap.features.details.ui.resetcard
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.transition.TransitionInflater
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
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.features.details.redux.DetailsState
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.rekotlin.StoreSubscriber
|
||||
import javax.inject.Inject
|
||||
|
||||
class ResetCardFragment : Fragment(), StoreSubscriber<DetailsState> {
|
||||
@AndroidEntryPoint
|
||||
internal class ResetCardFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
|
||||
|
||||
@Inject
|
||||
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
private val viewModel = ResetCardViewModel(store)
|
||||
|
||||
private var screenState: MutableState<ResetCardScreenState> =
|
||||
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState))
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val inflater = TransitionInflater.from(requireContext())
|
||||
enterTransition = inflater.inflateTransition(android.R.transition.fade)
|
||||
exitTransition = inflater.inflateTransition(android.R.transition.fade)
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
ResetCardScreen(
|
||||
modifier = modifier,
|
||||
state = screenState.value,
|
||||
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
isTransitionGroup = true
|
||||
TangemTheme {
|
||||
ResetCardScreen(
|
||||
state = screenState.value,
|
||||
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun TransitionInflater.inflateTransitions(): Boolean {
|
||||
enterTransition = inflateTransition(R.transition.fade)
|
||||
exitTransition = inflateTransition(R.transition.fade)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
|
|
|||
|
|
@ -1,24 +1,15 @@
|
|||
package com.tangem.tap.features.details.ui.resetcard
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconToggleButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
|
|
@ -34,32 +25,36 @@ import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
|
|||
import com.tangem.wallet.R
|
||||
|
||||
@Composable
|
||||
fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit) {
|
||||
internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
SettingsScreensScaffold(
|
||||
modifier = modifier,
|
||||
content = { ResetCardView(state = state) },
|
||||
onBackClick = onBackClick,
|
||||
backgroundColor = Color.Transparent,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "MagicNumber")
|
||||
@Composable
|
||||
fun ResetCardView(state: ResetCardScreenState) {
|
||||
private fun ResetCardView(state: ResetCardScreenState) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Box {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ill_reset_background),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.offset(y = (-82).dp),
|
||||
ScreenTitle(titleRes = R.string.card_settings_reset_card_to_factory)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 21.dp),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.img_alert),
|
||||
contentDescription = "",
|
||||
tint = Color.Unspecified,
|
||||
)
|
||||
ScreenTitle(titleRes = R.string.card_settings_reset_card_to_factory)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Column(
|
||||
modifier = Modifier.offset(y = (-32).dp),
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.tap.features.details.ui.resetcard
|
|||
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
|
||||
data class ResetCardScreenState(
|
||||
internal data class ResetCardScreenState(
|
||||
val accepted: Boolean = false,
|
||||
val descriptionText: TextReference,
|
||||
val onAcceptWarningToggleClick: (Boolean) -> Unit,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
|||
import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText
|
||||
import org.rekotlin.Store
|
||||
|
||||
class ResetCardViewModel(private val store: Store<AppState>) {
|
||||
internal class ResetCardViewModel(private val store: Store<AppState>) {
|
||||
|
||||
fun updateState(state: CardSettingsState?): ResetCardScreenState {
|
||||
val descriptionText = state?.cardInfo
|
||||
|
|
|
|||
|
|
@ -1,47 +1,45 @@
|
|||
package com.tangem.tap.features.details.ui.securitymode
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.transition.TransitionInflater
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
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.features.details.redux.DetailsState
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.rekotlin.StoreSubscriber
|
||||
import javax.inject.Inject
|
||||
|
||||
class SecurityModeFragment : Fragment(), StoreSubscriber<DetailsState> {
|
||||
@AndroidEntryPoint
|
||||
internal class SecurityModeFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
|
||||
|
||||
@Inject
|
||||
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
private val viewModel = SecurityModeViewModel(store)
|
||||
|
||||
private var screenState: MutableState<SecurityModeScreenState> =
|
||||
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.manageSecurityState))
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val inflater = TransitionInflater.from(requireContext())
|
||||
enterTransition = inflater.inflateTransition(android.R.transition.fade)
|
||||
exitTransition = inflater.inflateTransition(android.R.transition.fade)
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
SecurityModeScreen(
|
||||
modifier = modifier,
|
||||
state = screenState.value,
|
||||
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
isTransitionGroup = true
|
||||
TangemTheme {
|
||||
SecurityModeScreen(
|
||||
state = screenState.value,
|
||||
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun TransitionInflater.inflateTransitions(): Boolean {
|
||||
enterTransition = inflateTransition(R.transition.fade)
|
||||
exitTransition = inflateTransition(R.transition.fade)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
package com.tangem.tap.features.details.ui.securitymode
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -20,8 +16,13 @@ import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
|
|||
import com.tangem.wallet.R
|
||||
|
||||
@Composable
|
||||
fun SecurityModeScreen(state: SecurityModeScreenState, onBackClick: () -> Unit) {
|
||||
internal fun SecurityModeScreen(
|
||||
state: SecurityModeScreenState,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
SettingsScreensScaffold(
|
||||
modifier = modifier,
|
||||
content = { SecurityModeOptions(state = state) },
|
||||
// titleRes = R.string.card_settings_security_mode,
|
||||
onBackClick = onBackClick,
|
||||
|
|
@ -29,7 +30,7 @@ fun SecurityModeScreen(state: SecurityModeScreenState, onBackClick: () -> Unit)
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun SecurityModeOptions(state: SecurityModeScreenState) {
|
||||
private fun SecurityModeOptions(state: SecurityModeScreenState) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -55,7 +56,7 @@ fun SecurityModeOptions(state: SecurityModeScreenState) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun SecurityOption(option: SecurityOption, state: SecurityModeScreenState) {
|
||||
private fun SecurityOption(option: SecurityOption, state: SecurityModeScreenState) {
|
||||
val selected = option == state.selectedSecurityMode
|
||||
|
||||
val title = option.toTitleRes()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.tap.features.details.ui.securitymode
|
|||
import com.tangem.tap.features.details.redux.SecurityOption
|
||||
import com.tangem.wallet.R
|
||||
|
||||
data class SecurityModeScreenState(
|
||||
internal data class SecurityModeScreenState(
|
||||
val availableOptions: List<SecurityOption>,
|
||||
val selectedSecurityMode: SecurityOption,
|
||||
val isSaveChangesEnabled: Boolean,
|
||||
|
|
@ -11,7 +11,7 @@ data class SecurityModeScreenState(
|
|||
val onSaveChangesClicked: () -> Unit,
|
||||
)
|
||||
|
||||
fun SecurityOption.toTitleRes(): Int {
|
||||
internal fun SecurityOption.toTitleRes(): Int {
|
||||
return when (this) {
|
||||
SecurityOption.LongTap -> R.string.details_manage_security_long_tap
|
||||
SecurityOption.PassCode -> R.string.details_manage_security_passcode
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import com.tangem.tap.features.details.redux.ManageSecurityState
|
|||
import com.tangem.tap.features.details.redux.SecurityOption
|
||||
import org.rekotlin.Store
|
||||
|
||||
class SecurityModeViewModel(val store: Store<AppState>) {
|
||||
internal class SecurityModeViewModel(val store: Store<AppState>) {
|
||||
|
||||
fun updateState(state: ManageSecurityState?): SecurityModeScreenState {
|
||||
if (state == null) {
|
||||
|
|
|
|||
|
|
@ -1,24 +1,30 @@
|
|||
package com.tangem.tap.features.details.ui.walletconnect
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.transition.TransitionInflater
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
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.WalletConnect
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.rekotlin.StoreSubscriber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
internal class WalletConnectFragment : ComposeFragment(), StoreSubscriber<WalletConnectState> {
|
||||
|
||||
@Inject
|
||||
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
class WalletConnectFragment : Fragment(), StoreSubscriber<WalletConnectState> {
|
||||
private val viewModel = WalletConnectViewModel(store)
|
||||
private var screenState: MutableState<WalletConnectScreenState> =
|
||||
mutableStateOf(viewModel.updateState(store.state.walletConnectState))
|
||||
|
|
@ -26,32 +32,31 @@ class WalletConnectFragment : Fragment(), StoreSubscriber<WalletConnectState> {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Analytics.send(WalletConnect.ScreenOpened())
|
||||
val inflater = TransitionInflater.from(requireContext())
|
||||
enterTransition = inflater.inflateTransition(android.R.transition.fade)
|
||||
exitTransition = inflater.inflateTransition(android.R.transition.fade)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
isTransitionGroup = true
|
||||
TangemTheme {
|
||||
WalletConnectScreen(
|
||||
state = screenState.value,
|
||||
onBackClick = {
|
||||
if (screenState.value.isLoading) {
|
||||
store.dispatch(
|
||||
WalletConnectAction.FailureEstablishingSession(
|
||||
store.state.walletConnectState.newSessionData?.session?.session,
|
||||
),
|
||||
)
|
||||
}
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
},
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
WalletConnectScreen(
|
||||
modifier = modifier,
|
||||
state = screenState.value,
|
||||
onBackClick = {
|
||||
if (screenState.value.isLoading) {
|
||||
store.dispatch(
|
||||
WalletConnectAction.FailureEstablishingSession(
|
||||
store.state.walletConnectState.newSessionData?.session?.session,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun TransitionInflater.inflateTransitions(): Boolean {
|
||||
enterTransition = inflateTransition(R.transition.fade)
|
||||
exitTransition = inflateTransition(R.transition.fade)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -26,10 +25,15 @@ import com.tangem.wallet.R
|
|||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
fun WalletConnectScreen(state: WalletConnectScreenState, onBackClick: () -> Unit) {
|
||||
internal fun WalletConnectScreen(
|
||||
state: WalletConnectScreenState,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
SettingsScreensScaffold(
|
||||
modifier = modifier,
|
||||
content = {
|
||||
if (state.sessions.isEmpty()) {
|
||||
EmptyScreen(state)
|
||||
|
|
@ -56,8 +60,8 @@ fun WalletConnectScreen(state: WalletConnectScreenState, onBackClick: () -> Unit
|
|||
private fun AddSessionFab(onAddSession: () -> Unit, modifier: Modifier = Modifier) {
|
||||
FloatingActionButton(
|
||||
onClick = onAddSession,
|
||||
backgroundColor = colorResource(id = R.color.button_primary),
|
||||
contentColor = colorResource(id = R.color.icon_primary_2),
|
||||
backgroundColor = TangemTheme.colors.button.primary,
|
||||
contentColor = TangemTheme.colors.icon.primary2,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
modifier = modifier,
|
||||
) {
|
||||
|
|
@ -73,7 +77,7 @@ private fun EmptyScreen(state: WalletConnectScreenState) {
|
|||
if (state.isLoading) {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = colorResource(id = R.color.icon_accent),
|
||||
color = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
|
|
@ -86,7 +90,7 @@ private fun EmptyScreen(state: WalletConnectScreenState) {
|
|||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_walletconnect),
|
||||
contentDescription = "",
|
||||
colorFilter = ColorFilter.tint(colorResource(id = R.color.icon_inactive)),
|
||||
colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier.width(width = 100.dp),
|
||||
)
|
||||
|
|
@ -94,7 +98,7 @@ private fun EmptyScreen(state: WalletConnectScreenState) {
|
|||
Text(
|
||||
text = stringResource(id = R.string.wallet_connect_subtitle),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = colorResource(id = R.color.text_tertiary),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -106,7 +110,7 @@ private fun WalletConnectSessions(state: WalletConnectScreenState) {
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(2.dp),
|
||||
color = colorResource(id = R.color.icon_accent),
|
||||
color = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
|
@ -127,7 +131,7 @@ private fun WalletConnectSessions(state: WalletConnectScreenState) {
|
|||
Text(
|
||||
text = session.description,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = colorResource(id = R.color.text_primary_1),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
IconButton(
|
||||
|
|
@ -139,7 +143,7 @@ private fun WalletConnectSessions(state: WalletConnectScreenState) {
|
|||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_cross_rounded_24),
|
||||
contentDescription = "",
|
||||
tint = colorResource(id = R.color.icon_warning),
|
||||
tint = TangemTheme.colors.icon.warning,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.tap.features.details.ui.walletconnect
|
|||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class WalletConnectScreenState(
|
||||
internal data class WalletConnectScreenState(
|
||||
val sessions: ImmutableList<WcSessionForScreen>,
|
||||
val isLoading: Boolean = false,
|
||||
val onRemoveSession: (String) -> Unit = {},
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
import org.rekotlin.Store
|
||||
import timber.log.Timber
|
||||
|
||||
class WalletConnectViewModel(private val store: Store<AppState>) {
|
||||
internal class WalletConnectViewModel(private val store: Store<AppState>) {
|
||||
fun updateState(state: WalletConnectState): WalletConnectScreenState {
|
||||
Timber.d("WC2 Sessions: ${state.wc2Sessions}")
|
||||
val sessions = state.sessions.map { wcSession -> WcSessionForScreen.fromSession(wcSession) } + state.wc2Sessions
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs
|
|||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
setStatusBarColor(R.color.backgroundLightGray)
|
||||
setStatusBarColor(R.color.background_secondary)
|
||||
|
||||
webViewClient.onProgressStateChanged = { store.dispatch(DisclaimerAction.OnProgressStateChanged(it)) }
|
||||
store.subscribe(subscriber = this) { state ->
|
||||
|
|
@ -122,6 +122,7 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs
|
|||
webView.loadLocalTermsOfServices()
|
||||
}
|
||||
else -> {
|
||||
webView.setBackgroundColor(resources.getColor(R.color.transparent, null))
|
||||
webView.loadUrl(disclaimer.getUri().toString())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@ import com.tangem.core.analytics.Analytics
|
|||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.tokens.TokensAction
|
||||
import com.tangem.tap.common.analytics.events.IntroductionProcess
|
||||
import com.tangem.tap.features.home.compose.StoriesScreen
|
||||
import com.tangem.tap.features.home.redux.HomeAction
|
||||
import com.tangem.tap.features.home.redux.HomeState
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokensAction
|
||||
import com.tangem.tap.store
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ private fun handleOtherCardsAction(action: Action) {
|
|||
}
|
||||
|
||||
scope.launch {
|
||||
// TODO: Use new repo [REDACTED_JIRA]
|
||||
userTokensRepository.saveUserTokens(
|
||||
card = result.data.card,
|
||||
tokens = blockchainNetworks.toCurrencies(),
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
|
|||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
setStatusBarColor(R.color.backgroundWhite)
|
||||
setStatusBarColor(R.color.background_primary)
|
||||
}
|
||||
|
||||
private fun reconfigureLayoutForTwins(containerBinding: LayoutOnboardingContainerTopBinding) =
|
||||
|
|
@ -375,6 +375,9 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
|
|||
mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
|
||||
requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8),
|
||||
)
|
||||
mainBinding.onboardingTopContainer.imvCardBackground.backgroundTintList =
|
||||
requireContext().resources.getColorStateList(R.color.onboarding_card_background, null)
|
||||
|
||||
updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet_twins)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ private fun handleWalletAction(action: Action) {
|
|||
}
|
||||
|
||||
scope.launch {
|
||||
// TODO: Use new repo [REDACTED_JIRA]
|
||||
userTokensRepository.saveUserTokens(
|
||||
card = result.data.card,
|
||||
tokens = blockchainNetworks.toCurrencies(),
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import com.tangem.common.CardIdFormatter
|
|||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.CardIdDisplayFormat
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.ui.extensions.setStatusBarColor
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
|
||||
|
|
@ -158,6 +159,7 @@ class OnboardingWalletFragment :
|
|||
oldState.onboardingWalletState == newState.onboardingWalletState
|
||||
}.select { it.onboardingWalletState }
|
||||
}
|
||||
setStatusBarColor(R.color.background_primary)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ object ResetBackupCardDialog {
|
|||
setPositiveButton(R.string.common_cancel) { _, _ ->
|
||||
Analytics.send(Onboarding.Backup.ResetCancelEvent)
|
||||
}
|
||||
setNegativeButton(R.string.common_reset) { _, _ ->
|
||||
setNegativeButton(R.string.card_settings_action_sheet_reset) { _, _ ->
|
||||
Analytics.send(Onboarding.Backup.ResetPerformEvent)
|
||||
store.dispatch(BackupAction.ResetBackupCard(cardId))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,26 @@
|
|||
package com.tangem.tap.features.saveWallet.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
|
||||
import com.tangem.tap.features.saveWallet.redux.SaveWalletState
|
||||
import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog
|
||||
import com.tangem.tap.store
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.rekotlin.StoreSubscriber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SaveWalletViewModel : ViewModel(), StoreSubscriber<SaveWalletState> {
|
||||
@HiltViewModel
|
||||
internal class SaveWalletViewModel @Inject constructor(
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : ViewModel(), StoreSubscriber<SaveWalletState> {
|
||||
private val stateInternal = MutableStateFlow(SaveWalletScreenState())
|
||||
val state: StateFlow<SaveWalletScreenState> = stateInternal
|
||||
|
||||
|
|
@ -22,10 +30,12 @@ internal class SaveWalletViewModel : ViewModel(), StoreSubscriber<SaveWalletStat
|
|||
}
|
||||
|
||||
fun saveWallet() {
|
||||
analyticsEventHandler.send(WalletScreenAnalyticsEvent.EnableBiometrics(AnalyticsParam.OnOffState.On))
|
||||
store.dispatch(SaveWalletAction.Save)
|
||||
}
|
||||
|
||||
fun cancelOrClose() {
|
||||
analyticsEventHandler.send(WalletScreenAnalyticsEvent.EnableBiometrics(AnalyticsParam.OnOffState.Off))
|
||||
store.dispatch(SaveWalletAction.Dismiss)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ sealed class AmountAction : SendScreenAction {
|
|||
data class SetAmount(val amountCrypto: BigDecimal, val isUserInput: Boolean) : AmountAction()
|
||||
data class SetAmountError(val error: TapError?) : AmountAction()
|
||||
data class SetDecimalSeparator(val separator: String) : AmountAction()
|
||||
data class HideBalance(val hide: Boolean) : AmountAction()
|
||||
}
|
||||
|
||||
// Fee
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.features.send.redux.reducers
|
||||
|
||||
import com.tangem.common.Strings.STARS
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
import com.tangem.tap.features.send.redux.AmountAction
|
||||
|
|
@ -38,8 +39,8 @@ class AmountReducer : SendInternalReducer {
|
|||
val viewValue = state.restoreDecimalSeparator(fiatToSend.stripZeroPlainString())
|
||||
state.copy(
|
||||
viewAmountValue = InputViewValue(viewValue),
|
||||
viewBalanceValue = rescaledBalance.stripZeroPlainString(),
|
||||
mainCurrency = state.createMainCurrency(currency, currencyCanBeSwitched),
|
||||
viewBalanceValue = if (state.hideBalance) STARS else rescaledBalance.stripZeroPlainString(),
|
||||
mainCurrency = state.createMainCurrency(currency, true),
|
||||
maxLengthOfAmount = sendState.getDecimals(currency),
|
||||
cursorAtTheSamePosition = false,
|
||||
)
|
||||
|
|
@ -80,6 +81,14 @@ class AmountReducer : SendInternalReducer {
|
|||
}
|
||||
is AmountAction.SetAmountError -> state.copy(error = action.error)
|
||||
is AmountAction.SetDecimalSeparator -> state.copy(decimalSeparator = action.separator)
|
||||
is AmountAction.HideBalance -> {
|
||||
val rescaledBalance = sendState.convertExtractCryptoToFiat(state.balanceCrypto, true)
|
||||
|
||||
state.copy(
|
||||
hideBalance = action.hide,
|
||||
viewBalanceValue = if (action.hide) STARS else rescaledBalance.stripZeroPlainString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
return updateLastState(sendState.copy(amountState = result), result)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ data class AmountState(
|
|||
val mainCurrency: MainCurrency = MainCurrency(MainCurrencyType.FIAT, FiatCurrency.Default.code),
|
||||
val amountToSendCrypto: BigDecimal = BigDecimal.ZERO,
|
||||
val balanceCrypto: BigDecimal = BigDecimal.ZERO,
|
||||
val hideBalance: Boolean = false,
|
||||
val cursorAtTheSamePosition: Boolean = true,
|
||||
val maxLengthOfAmount: Int = 2,
|
||||
val decimalSeparator: String = ".",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import android.view.inputmethod.EditorInfo
|
|||
import android.widget.EditText
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
|
|
@ -45,6 +46,7 @@ import com.tangem.tap.mainScope
|
|||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.FragmentSendBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
|
@ -56,8 +58,11 @@ private const val EDIT_TEXT_INPUT_DEBOUNCE = 400L
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@OptIn(FlowPreview::class)
|
||||
@AndroidEntryPoint
|
||||
class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
||||
|
||||
private val viewModel by viewModels<SendViewModel>()
|
||||
|
||||
lateinit var sendBtn: ViewStateWidget
|
||||
|
||||
private lateinit var etAmountToSend: TextInputEditText
|
||||
|
|
@ -70,6 +75,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycle.addObserver(viewModel)
|
||||
Analytics.send(Token.Send.ScreenOpened())
|
||||
}
|
||||
|
||||
|
|
@ -348,6 +354,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
|||
|
||||
override fun onDestroy() {
|
||||
store.dispatch(ReleaseSendState)
|
||||
lifecycle.removeObserver(viewModel)
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.tap.features.send.ui
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase
|
||||
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
|
||||
import com.tangem.tap.features.send.redux.AmountAction
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class SendViewModel @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val appStateHolder: AppStateHolder,
|
||||
private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase,
|
||||
private val listenToFlipsUseCase: ListenToFlipsUseCase,
|
||||
) : ViewModel(), DefaultLifecycleObserver {
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
isBalanceHiddenUseCase()
|
||||
.flowWithLifecycle(owner.lifecycle)
|
||||
.onEach { isBalanceHidden ->
|
||||
withContext(dispatchers.main) {
|
||||
appStateHolder.mainStore?.dispatch(AmountAction.HideBalance(isBalanceHidden))
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
viewModelScope.launch {
|
||||
listenToFlipsUseCase()
|
||||
.flowWithLifecycle(owner.lifecycle)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -260,7 +260,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
|
|||
|
||||
val imageRes = if (state.inputIsEnabled) R.drawable.ic_arrows_up_down else 0
|
||||
tvAmountCurrency.setCompoundDrawablesWithIntrinsicBounds(0, 0, imageRes, 0)
|
||||
val textColor = if (state.inputIsEnabled) R.color.blue else R.color.textGray
|
||||
val textColor = if (state.inputIsEnabled) R.color.accent else R.color.text_secondary
|
||||
tvAmountCurrency.setTextColor(fg.getColor(textColor))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ internal object TokensListInteractorModule {
|
|||
reduxStateHolder = reduxStateHolder,
|
||||
testnetTokensStorage = testnetTokensStorage,
|
||||
),
|
||||
reduxStateHolder = reduxStateHolder,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.tap.features.tokens.impl.di
|
||||
|
||||
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
|
||||
import com.tangem.tap.features.tokens.impl.presentation.router.DefaultTokensListRouter
|
||||
import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter
|
||||
import dagger.Module
|
||||
|
|
@ -18,7 +17,5 @@ internal object TokensListRouterModule {
|
|||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideTokensListRouter(customTokenFeatureToggles: CustomTokenFeatureToggles): TokensListRouter {
|
||||
return DefaultTokensListRouter(customTokenFeatureToggles)
|
||||
}
|
||||
fun provideTokensListRouter(): TokensListRouter = DefaultTokensListRouter()
|
||||
}
|
||||
|
|
@ -1,262 +1,17 @@
|
|||
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
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.common.flatMap
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.common.configs.CardConfig
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.common.util.supportsHdWallet
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.model.WalletDataModel
|
||||
import com.tangem.tap.features.tokens.impl.domain.models.Token
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Default implementation of tokens list interactor
|
||||
* FIXME("Necessary to avoid using redux actions")
|
||||
*
|
||||
* @property repository repository of tokens list feature
|
||||
* @property reduxStateHolder redux state holder
|
||||
* @property repository repository of tokens list feature
|
||||
*/
|
||||
internal class DefaultTokensListInteractor(
|
||||
private val repository: TokensListRepository,
|
||||
private val reduxStateHolder: AppStateHolder,
|
||||
) : TokensListInteractor {
|
||||
internal class DefaultTokensListInteractor(private val repository: TokensListRepository) : TokensListInteractor {
|
||||
|
||||
override fun getTokensList(searchText: String): Flow<PagingData<Token>> {
|
||||
return repository.getAvailableTokens(searchText = searchText.ifBlank(defaultValue = { null }))
|
||||
}
|
||||
|
||||
override suspend fun saveChanges(tokens: List<TokenWithBlockchain>, blockchains: List<Blockchain>) {
|
||||
val scanResponse = requireNotNull(reduxStateHolder.scanResponse)
|
||||
|
||||
val derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle()
|
||||
val currentTokens = store.state.tokensState.addedWallets
|
||||
.toNonCustomTokensWithBlockchains(derivationStyle = derivationStyle)
|
||||
|
||||
val currentBlockchains = store.state.tokensState.addedWallets
|
||||
.toNonCustomBlockchains(derivationStyle = derivationStyle)
|
||||
|
||||
val blockchainsToAdd = blockchains.filterNot(currentBlockchains::contains)
|
||||
val blockchainsToRemove = currentBlockchains.filterNot(blockchains::contains)
|
||||
|
||||
val tokensToAdd = tokens.filterNot(currentTokens::contains)
|
||||
val tokensToRemove = currentTokens.filterNot { token -> tokens.any { it.token == token.token } }
|
||||
|
||||
val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty()
|
||||
val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
|
||||
if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) {
|
||||
store.dispatchDebugErrorNotification(message = "Nothing to save")
|
||||
return
|
||||
}
|
||||
|
||||
remove(
|
||||
tokens = tokensToRemove,
|
||||
blockchains = blockchainsToRemove,
|
||||
derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(),
|
||||
)
|
||||
|
||||
add(tokens = tokensToAdd, blockchains = blockchainsToAdd, scanResponse = scanResponse)
|
||||
}
|
||||
|
||||
private fun List<WalletDataModel>.toNonCustomTokensWithBlockchains(
|
||||
derivationStyle: DerivationStyle?,
|
||||
): List<TokenWithBlockchain> {
|
||||
return this.map(WalletDataModel::currency)
|
||||
.mapNotNull { currency ->
|
||||
if (currency !is Currency.Token || currency.isCustomCurrency(derivationStyle)) return@mapNotNull null
|
||||
TokenWithBlockchain(token = currency.token, blockchain = currency.blockchain)
|
||||
}
|
||||
.distinct()
|
||||
}
|
||||
|
||||
private fun List<WalletDataModel>.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List<Blockchain> {
|
||||
return this.map(WalletDataModel::currency)
|
||||
.mapNotNull { currency ->
|
||||
if (currency.isCustomCurrency(derivationStyle)) return@mapNotNull null
|
||||
(currency as? Currency.Blockchain)?.blockchain
|
||||
}
|
||||
.distinct()
|
||||
}
|
||||
|
||||
private suspend fun remove(
|
||||
tokens: List<TokenWithBlockchain>,
|
||||
blockchains: List<Blockchain>,
|
||||
derivationStyle: DerivationStyle?,
|
||||
) {
|
||||
val currencies = convertToCurrencies(tokens, blockchains, derivationStyle)
|
||||
if (currencies.isEmpty()) return
|
||||
|
||||
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
|
||||
Timber.e("Unable to remove currencies, no user wallet selected")
|
||||
return
|
||||
}
|
||||
|
||||
walletCurrenciesManager.removeCurrencies(userWallet = selectedUserWallet, currenciesToRemove = currencies)
|
||||
}
|
||||
|
||||
private suspend fun add(
|
||||
tokens: List<TokenWithBlockchain>,
|
||||
blockchains: List<Blockchain>,
|
||||
scanResponse: ScanResponse,
|
||||
) {
|
||||
val currenciesToAdd = convertToCurrencies(
|
||||
tokens = tokens,
|
||||
blockchains = blockchains,
|
||||
derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(),
|
||||
)
|
||||
|
||||
// TODO("[REDACTED_TASK_KEY] use DerivationManager")
|
||||
if (scanResponse.supportsHdWallet()) {
|
||||
deriveMissingBlockchains(scanResponse, currenciesToAdd)
|
||||
} else {
|
||||
submitAdd(scanResponse, currenciesToAdd)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun deriveMissingBlockchains(scanResponse: ScanResponse, currencies: List<Currency>) {
|
||||
val config = CardConfig.createConfig(scanResponse.card)
|
||||
val derivationDataList = currencies.mapNotNull { currency ->
|
||||
val curve = config.primaryCurve(currency.blockchain)
|
||||
curve?.let { getDerivations(curve, scanResponse, currency) }
|
||||
}
|
||||
|
||||
val derivations = buildMap<ByteArrayKey, MutableList<DerivationPath>> {
|
||||
derivationDataList.forEach {
|
||||
val current = this[it.derivations.first]
|
||||
if (current != null) {
|
||||
current.addAll(it.derivations.second)
|
||||
current.distinct()
|
||||
} else {
|
||||
this[it.derivations.first] = it.derivations.second.toMutableList()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (derivations.isEmpty()) {
|
||||
submitAdd(scanResponse, currencies)
|
||||
return
|
||||
}
|
||||
|
||||
when (val result = tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)) {
|
||||
is CompletionResult.Success -> {
|
||||
val newDerivedKeys = result.data.entries
|
||||
val oldDerivedKeys = scanResponse.derivedKeys
|
||||
|
||||
val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet()
|
||||
|
||||
val updatedDerivedKeys = walletKeys.associateWith { walletKey ->
|
||||
val oldDerivations = ExtendedPublicKeysMap(map = oldDerivedKeys[walletKey] ?: emptyMap())
|
||||
val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(map = emptyMap())
|
||||
ExtendedPublicKeysMap(map = oldDerivations + newDerivations)
|
||||
}
|
||||
|
||||
val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys)
|
||||
|
||||
store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
|
||||
submitAdd(scanResponse, currencies)
|
||||
return
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
store.dispatchDebugErrorNotification(TapError.CustomError(customMessage = "Error adding tokens"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDerivations(
|
||||
curve: EllipticCurve,
|
||||
scanResponse: ScanResponse,
|
||||
currency: Currency,
|
||||
): TokensMiddleware.DerivationData? {
|
||||
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
|
||||
|
||||
val supportedCurves = currency.blockchain.getSupportedCurves()
|
||||
val path = currency.blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle())
|
||||
.takeIf { supportedCurves.contains(curve) }
|
||||
|
||||
val customPath = currency.derivationPath?.let {
|
||||
DerivationPath(it)
|
||||
}.takeIf { supportedCurves.contains(curve) }
|
||||
|
||||
val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList()
|
||||
if (bothCandidates.isEmpty()) return null
|
||||
|
||||
if (currency is Currency.Blockchain && currency.blockchain == Blockchain.Cardano) {
|
||||
currency.derivationPath?.let {
|
||||
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
|
||||
}
|
||||
}
|
||||
|
||||
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
|
||||
val alreadyDerivedKeys: ExtendedPublicKeysMap =
|
||||
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
|
||||
|
||||
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
|
||||
if (toDerive.isEmpty()) return null
|
||||
|
||||
return TokensMiddleware.DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
|
||||
}
|
||||
|
||||
private suspend fun submitAdd(scanResponse: ScanResponse, currencies: List<Currency>) {
|
||||
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
|
||||
Timber.e("Unable to add currencies, no user wallet selected")
|
||||
return
|
||||
}
|
||||
|
||||
userWalletsListManager
|
||||
.update(
|
||||
userWalletId = selectedUserWallet.walletId,
|
||||
update = { userWallet -> userWallet.copy(scanResponse = scanResponse) },
|
||||
)
|
||||
.flatMap { updatedUserWallet ->
|
||||
walletCurrenciesManager.addCurrencies(
|
||||
userWallet = updatedUserWallet,
|
||||
currenciesToAdd = currencies,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertToCurrencies(
|
||||
tokens: List<TokenWithBlockchain>,
|
||||
blockchains: List<Blockchain>,
|
||||
derivationStyle: DerivationStyle?,
|
||||
): List<Currency> {
|
||||
return tokens.map { tokenWithBlockchain ->
|
||||
Currency.Token(
|
||||
token = tokenWithBlockchain.token,
|
||||
blockchain = tokenWithBlockchain.blockchain,
|
||||
derivationPath = tokenWithBlockchain.blockchain.derivationPath(derivationStyle)?.rawPath,
|
||||
)
|
||||
}.plus(
|
||||
blockchains.map { blockchain ->
|
||||
Currency.Blockchain(
|
||||
blockchain = blockchain,
|
||||
derivationPath = blockchain.derivationPath(derivationStyle)?.rawPath,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
package com.tangem.tap.features.tokens.impl.domain
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.tap.features.tokens.impl.domain.models.Token
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
|
|
@ -15,12 +13,4 @@ internal interface TokensListInteractor {
|
|||
|
||||
/** Get tokens list using filter by text [searchText] */
|
||||
fun getTokensList(searchText: String): Flow<PagingData<Token>>
|
||||
|
||||
/**
|
||||
* Save added tokens
|
||||
*
|
||||
* @param tokens tokens list that need to save
|
||||
* @param blockchains blockchains list that need to save
|
||||
*/
|
||||
suspend fun saveChanges(tokens: List<TokenWithBlockchain>, blockchains: List<Blockchain>)
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.presentation.models
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
|
||||
import com.tangem.tap.store
|
||||
|
||||
/**
|
||||
* Required data for tokens list screen
|
||||
* FIXME("Necessary to avoid using redux state")
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class TokensListArgs {
|
||||
/** Tokens list screen mode */
|
||||
val isManageAccess: Boolean get() = store.state.tokensState.isManageAccess
|
||||
|
||||
/** Tokens list that accessible from the main screen */
|
||||
val mainScreenTokenList: List<TokenWithBlockchain> get() = store.state.tokensState.addedTokens
|
||||
|
||||
/** Blockchains list that accessible from the main screen */
|
||||
val mainScreenBlockchainList: List<Blockchain> get() = store.state.tokensState.addedBlockchains
|
||||
}
|
||||
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