Updated on 2026-08-14

This commit is contained in:
Tangem 2023-10-20 12:21:24 +03:00
commit 2da48c037a
961 changed files with 24212 additions and 13399 deletions

View file

@ -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")
}
}

View file

@ -4,6 +4,9 @@ import android.content.Context
import android.net.Uri
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK
import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.getColorCompat
import com.tangem.wallet.R
@ -15,6 +18,9 @@ class CustomTabsManager {
.setNavigationBarColor(context.getColorCompat(R.color.toolbarColor))
.build(),
)
.setColorScheme(
if (MutableAppThemeModeHolder.isDarkThemeActive) COLOR_SCHEME_DARK else COLOR_SCHEME_LIGHT,
)
.build()
customTabsIntent.launchUrl(context, Uri.parse(url))
}

View file

@ -0,0 +1,13 @@
package com.tangem.tap.common.analytics
import com.tangem.core.analytics.Analytics
import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.setContext
internal class DefaultChangeCardAnalyticsContextUseCase : ChangeCardAnalyticsContextUseCase {
override fun invoke(scanResponse: ScanResponse) {
Analytics.setContext(scanResponse)
}
}

View file

@ -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)
}
@ -136,9 +133,9 @@ sealed class AnalyticsParam {
}
sealed class WalletCreationType(val value: String) {
object PrivateKey : WalletCreationType("Private key")
object NewSeed : WalletCreationType("New seed")
object SeedImport : WalletCreationType("Seed import")
object PrivateKey : WalletCreationType(value = "Private Key")
object NewSeed : WalletCreationType(value = "New Seed")
object SeedImport : WalletCreationType(value = "Seed Import")
}
companion object Key {
@ -155,7 +152,8 @@ sealed class AnalyticsParam {
const val ERROR_DESCRIPTION = "Error Description"
const val ERROR_CODE = "Error Code"
const val ERROR_KEY = "Error Key"
const val CREATION_TYPE = "Creation type"
const val CREATION_TYPE = "Creation Type"
const val SEED_PHRASE_LENGTH = "Seed Phrase Length"
const val DAPP_NAME = "DApp Name"
const val DAPP_URL = "DApp Url"
const val METHOD_NAME = "Method Name"

View file

@ -32,14 +32,18 @@ sealed class Basic(
batch: String,
signInType: SignInType,
walletsCount: String,
hasBackup: Boolean?,
) : Basic(
event = "Signed in",
params = mapOf(
AnalyticsParam.CURRENCY to currency.value,
AnalyticsParam.BATCH to batch,
"Sign in type" to signInType.name,
"Wallets Count" to walletsCount,
),
params = buildMap {
put(AnalyticsParam.CURRENCY, currency.value)
put(AnalyticsParam.BATCH, batch)
put("Sign in type", signInType.name)
put("Wallets Count", walletsCount)
if (hasBackup != null) {
put("Backuped", if (hasBackup) "Yes" else "No")
}
},
) {
enum class SignInType {
Card, Biometric

View file

@ -23,9 +23,14 @@ sealed class Onboarding(
class ButtonCreateWallet : CreateWallet("Button - Create Wallet")
class WalletCreatedSuccessfully(
creationType: AnalyticsParam.WalletCreationType = AnalyticsParam.WalletCreationType.PrivateKey,
seedPhraseLength: Int? = null,
) : CreateWallet(
event = "Wallet Created Successfully",
params = mapOf(AnalyticsParam.CREATION_TYPE to creationType.value),
params = buildMap {
put(AnalyticsParam.CREATION_TYPE, creationType.value)
if (seedPhraseLength != null) put(AnalyticsParam.SEED_PHRASE_LENGTH, seedPhraseLength.toString())
},
)
}

View file

@ -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")
}

View file

@ -22,6 +22,7 @@ sealed class Settings(
class ButtonAppSettings : Settings(event = "Button - App Settings")
class ButtonCreateBackup : Settings(event = "Button - Create Backup")
class ButtonWalletConnect : Settings(event = "Button - Wallet Connect")
object ScanNewCard : Settings(event = "Button - Scan New Card")
class ButtonSocialNetwork(network: SocialNetwork) : Settings(
event = "Button - Social Network",
@ -78,5 +79,10 @@ sealed class Settings(
)
object ButtonEnableBiometricAuthentication : AppSettings(event = "Button - Enable Biometric Authentication")
class MainCurrencyChanged(currencyType: String) : MainScreen(
event = "Main Currency Changed",
params = mapOf("Currency Type" to currencyType),
)
}
}

View file

@ -14,4 +14,6 @@ internal object MutableAppThemeModeHolder : AppThemeModeHolder {
appThemeMode.value = value
}
get() = appThemeMode.value
var isDarkThemeActive: Boolean = false
}

View file

@ -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,
) {}
}
}

View file

@ -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,
)
}
}

View file

@ -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
}
}

View file

@ -47,6 +47,7 @@ fun Blockchain.getGreyedOutIconRes(): Int {
Blockchain.AlephZero, Blockchain.AlephZeroTestnet -> R.drawable.ic_azero_no_color
Blockchain.OctaSpace, Blockchain.OctaSpaceTestnet -> R.drawable.ic_octaspace_no_color
Blockchain.Chia, Blockchain.ChiaTestnet -> R.drawable.ic_chia_no_color
Blockchain.Near, Blockchain.NearTestnet -> R.drawable.ic_near_no_color
else -> R.drawable.ic_tangem_logo
}
}

View file

@ -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,21 +155,10 @@ 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,
getDependency = DaggerGraphState::walletFeatureToggles,
)
if (featureToggles.isRedesignedScreenEnabled) {
store.state.daggerGraphState
@ -186,5 +175,6 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.Welcome -> WelcomeFragment()
AppScreen.SaveWallet -> SaveWalletBottomSheetFragment()
AppScreen.WalletSelector -> WalletSelectorBottomSheetFragment()
AppScreen.AppCurrencySelector -> AppCurrencySelectorFragment()
}
}

View file

@ -1,6 +0,0 @@
package com.tangem.tap.common.extensions
/**
[REDACTED_AUTHOR]
*/
typealias ValueCallback<T> = (T) -> Unit

View file

@ -1,12 +1,12 @@
package com.tangem.tap.common.extensions
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.amountToCreateAccount
import com.tangem.tap.common.TestActions
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.model.WalletDataModel
@ -71,6 +71,7 @@ fun WalletManager.getTopUpUrl(): String? {
cryptoCurrencyName = wallet.blockchain.currency,
fiatCurrencyName = globalState.appCurrency.code,
walletAddress = defaultAddress,
isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive,
)
}
@ -79,12 +80,4 @@ fun WalletManager?.getAddressData(): WalletDataModel.AddressData? {
val addressDataList = wallet.createAddressesData()
return if (addressDataList.isEmpty()) null else addressDataList[0]
}
fun <T> WalletManager.Companion.stub(): T {
val wallet = Wallet(Blockchain.Unknown, setOf(), Wallet.PublicKey(byteArrayOf(), null), setOf())
return object : WalletManager(wallet) {
override val currentHost: String = ""
override suspend fun update() {}
} as T
}

View file

@ -7,8 +7,10 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletIdBuilder
import com.tangem.tap.common.extensions.stripZeroPlainString
import kotlinx.coroutines.flow.*
class AdditionalFeedbackInfo {
class EmailWalletInfo(
var blockchain: Blockchain = Blockchain.Unknown,
var derivationPath: String = "",
@ -46,6 +48,7 @@ class AdditionalFeedbackInfo {
private val Address.name: String
get() = type.javaClass.simpleName
@Deprecated("Don't use it directly")
fun setCardInfo(data: ScanResponse) {
cardId = data.card.cardId
cardBlockchain = data.walletData?.blockchain ?: ""
@ -55,6 +58,7 @@ class AdditionalFeedbackInfo {
userWalletId = UserWalletIdBuilder.scanResponse(data).build()?.stringValue ?: ""
}
@Deprecated("Don't use it directly")
fun setWalletsInfo(walletManagers: List<WalletManager>) {
walletsInfo.clear()
tokens.clear()

View file

@ -6,6 +6,7 @@ import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.NetworkServices
import com.tangem.tap.common.redux.global.GlobalMiddleware
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.legacy.LegacyMiddleware
import com.tangem.tap.common.redux.navigation.navigationMiddleware
import com.tangem.tap.features.details.redux.DetailsMiddleware
import com.tangem.tap.features.details.redux.DetailsState
@ -68,7 +69,7 @@ data class AppState(
val daggerGraphState: DaggerGraphState = DaggerGraphState(),
) : StateType {
val domainState: DomainState
private val domainState: DomainState
get() = domainStore.state
val domainNetworks: NetworkServices
@ -107,6 +108,7 @@ data class AppState(
AccessCodeRequestPolicyMiddleware().middleware,
SignInMiddleware.middleware,
DaggerGraphMiddleware.daggerGraphMiddleware,
LegacyMiddleware.legacyMiddleware,
)
}
}

View file

@ -1,11 +1,10 @@
package com.tangem.tap.common.redux.global
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
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
@ -48,13 +47,6 @@ sealed class GlobalAction : Action {
object Stop : Onboarding()
}
data class ScanCard(
val additionalBlockchainsToDerive: Collection<Blockchain>? = null,
val onSuccess: ((ScanResponse) -> Unit)? = null,
val onFailure: ((TangemError) -> Unit)? = null,
val messageResId: Int? = null,
) : GlobalAction()
object ScanFailsCounter {
data class ChooseBehavior(val result: CompletionResult<ScanResponse>) : GlobalAction()
object Reset : GlobalAction()
@ -103,4 +95,5 @@ sealed class GlobalAction : Action {
}
data class UpdateUserWalletsListManager(val manager: UserWalletsListManager) : GlobalAction()
data class ChangeAppThemeMode(val appThemeMode: AppThemeMode) : GlobalAction()
}

View file

@ -3,12 +3,12 @@ package com.tangem.tap.common.redux.global
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.datasource.config.models.Config
import com.tangem.domain.common.LogConfig
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchDialogShow
@ -16,7 +16,6 @@ import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.exchangeServices.BuyExchangeService
@ -26,8 +25,13 @@ import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoEnvironment
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService
import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
import com.tangem.tap.preferencesStorage
import com.tangem.tap.proxy.redux.DaggerGraphState
import kotlinx.coroutines.flow.firstOrNull
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.walletCurrenciesManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
@ -66,7 +70,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()
@ -75,10 +85,10 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
is GlobalAction.HideWarningMessage -> {
store.state.globalState.warningManager?.let {
if (it.hideWarning(action.warning)) {
if (WarningMessagesManager.isAlreadySignedHashesWarning(action.warning)) {
// TODO: No appropriate warningMessage identification. Make it better later
store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
}
// if (WarningMessagesManager.isAlreadySignedHashesWarning()) {
// // TODO: No appropriate warningMessage identification. Make it better later
// store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
// }
store.dispatch(WalletAction.Warnings.Update)
store.dispatch(SendAction.Warnings.Update)
@ -131,6 +141,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)
}
@ -143,28 +155,6 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
}
scope.launch { exchangeManager.update() }
}
is GlobalAction.ScanCard -> {
scope.launch {
tangemSdkManager.changeDisplayedCardIdNumbersCount(null)
val result = tangemSdkManager.scanProduct(
userTokensRepository = userTokensRepository,
additionalBlockchainsToDerive = action.additionalBlockchainsToDerive,
messageRes = action.messageResId,
)
withMainContext {
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
when (result) {
is CompletionResult.Success -> {
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data)
action.onSuccess?.invoke(result.data)
}
is CompletionResult.Failure -> {
action.onFailure?.invoke(result.error)
}
}
}
}
}
is GlobalAction.FetchUserCountry -> {
scope.launch {
// TODO("After adding DI") get dependencies by DI
@ -186,6 +176,28 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
is GlobalAction.SetTopUpController -> {
walletCurrenciesManager.addListener(action.topUpController)
}
is GlobalAction.UpdateUserWalletsListManager -> {
/*
* If UserWalletsListManager's implementation is changed,
* then all selectedUserWallet's observers is became irrelevant
*/
action.manager.selectedUserWallet
.distinctUntilChanged()
.onEach { userWallet ->
Analytics.send(event = Basic.WalletOpened())
store.state.globalState.feedbackManager?.infoHolder?.let { infoHolder ->
infoHolder.setCardInfo(data = userWallet.scanResponse)
store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade)
.getAll(userWalletId = userWallet.walletId)
.onEach(infoHolder::setWalletsInfo)
.launchIn(scope)
}
}
.flowOn(Dispatchers.IO)
.launchIn(scope)
}
}
}

View file

@ -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
}
}

View file

@ -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

View file

@ -0,0 +1,22 @@
package com.tangem.tap.common.redux.legacy
import com.tangem.domain.redux.LegacyAction
import com.tangem.tap.common.feedback.RateCanBeBetterEmail
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.store
import org.rekotlin.Middleware
internal object LegacyMiddleware {
val legacyMiddleware: Middleware<AppState> = { _, _ ->
{ next ->
{ action ->
when (action) {
is LegacyAction.SendEmailRateCanBeBetter -> {
store.state.globalState.feedbackManager?.sendEmail(RateCanBeBetterEmail())
}
}
next(action)
}
}
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.common.ui
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store
import com.tangem.wallet.R
@ -39,7 +40,7 @@ object SimpleCancelableAlertDialog {
secondaryButtonAction: () -> Unit = {},
context: Context,
): AlertDialog {
return AlertDialog.Builder(context).apply {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(titleRes?.let { context.getString(it) } ?: title)
setMessage(messageRes?.let { context.getString(it) } ?: message)
setPositiveButton(context.getText(primaryButtonRes)) { _, _ -> primaryButtonAction() }