Updated on 2026-08-14

This commit is contained in:
Tangem 2024-01-19 16:59:31 +03:00
commit e7f19c6936
886 changed files with 11772 additions and 23078 deletions

View file

@ -76,11 +76,10 @@ dependencies {
implementation(projects.data.txhistory)
implementation(projects.data.wallets)
implementation(projects.data.analytics)
implementation(projects.data.transaction)
/** Features */
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)
@ -99,6 +98,8 @@ dependencies {
implementation(projects.features.manageTokens.api)
implementation(projects.features.manageTokens.impl)
implementation(projects.features.send.impl)
implementation(projects.features.qrScanning.api)
implementation(projects.features.qrScanning.impl)
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)
@ -146,6 +147,7 @@ dependencies {
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/** Other libraries */
@ -158,7 +160,6 @@ dependencies {
implementation(deps.timber)
implementation(deps.reKotlin)
implementation(deps.zxing.qrCore)
implementation(deps.otaliastudiosCameraView)
implementation(deps.coil)
implementation(deps.appsflyer)
implementation(deps.amplitude)
@ -186,7 +187,9 @@ dependencies {
}
/** Testing libraries */
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
androidTestImplementation(deps.test.junit.android)
androidTestImplementation(deps.test.espresso)
@ -208,7 +211,7 @@ dependencies {
/** Excluded dependencies */
implementation("com.google.guava:guava:30.0-android") {
// excludes version 9999.0-empty-to-avoid-conflict-with-guava
exclude(group="com.google.guava", module = "listenablefuture")
exclude(group = "com.google.guava", module = "listenablefuture")
}
}

View file

@ -137,14 +137,6 @@
</activity>
<activity
android:name="com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity"
android:theme="@style/AppTheme" />
<activity
android:name="com.tangem.feature.learn2earn.presentation.webView.Learn2earnWebViewActivity"
android:theme="@style/AppTheme" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"

View file

@ -536,6 +536,26 @@
"networkId": "decimal/test"
}
]
},
{
"id": "xdce-crowd-sale",
"name": "XDC",
"symbol": "XDC",
"networks": [
{
"networkId": "xdc-network/test"
}
]
},
{
"id": "aptos",
"name": "Aptos",
"symbol": "APT",
"networks": [
{
"networkId": "aptos/test"
}
]
}
]
}

View file

@ -23,6 +23,7 @@ import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import arrow.core.getOrElse
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.feature.qrscanning.QrScanningRouter
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import com.tangem.core.navigation.AppScreen
@ -134,6 +135,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var sendRouter: SendRouter
@Inject
lateinit var qrScanningRouter: QrScanningRouter
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode?>
@ -206,6 +210,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
manageTokensRouter = manageTokensRouter,
cardSdkConfigRepository = cardSdkConfigRepository,
sendRouter = sendRouter,
qrScanningRouter = qrScanningRouter,
),
)
}
@ -247,20 +252,15 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
private fun createAppThemeModeFlow(): SharedFlow<AppThemeMode?> {
val tapApplication = application as TapApplication
val featureToggle = tapApplication.darkThemeFeatureToggle
return if (featureToggle.isDarkThemeEnabled) {
tapApplication.getAppThemeModeUseCase()
.map { maybeMode ->
maybeMode.getOrElse { AppThemeMode.DEFAULT }
}
.shareIn(
scope = lifecycleScope + Dispatchers.IO,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000),
)
} else {
MutableStateFlow(AppThemeMode.FORCE_LIGHT)
}
return tapApplication.getAppThemeModeUseCase()
.map { maybeMode ->
maybeMode.getOrElse { AppThemeMode.DEFAULT }
}
.shareIn(
scope = lifecycleScope + Dispatchers.IO,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000),
)
}
override fun onStart() {
@ -454,7 +454,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
private fun checkForNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
BuildConfig.DEBUG &&
BuildConfig.LOG_ENABLED &&
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED
) {

View file

@ -11,6 +11,7 @@ import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks
class NavBarInsetsFragmentLifecycleCallback : FragmentLifecycleCallbacks() {
override fun onFragmentViewCreated(fm: FragmentManager, f: Fragment, v: View, savedInstanceState: Bundle?) {
if (v is ComposeView) return

View file

@ -10,8 +10,6 @@ import com.orhanobut.logger.AndroidLogAdapter
import com.orhanobut.logger.Logger
import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.filter.OneTimeEventFilter
@ -30,23 +28,22 @@ import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.common.LogConfig
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.WalletManagersRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.features.tester.api.TesterFeatureToggles
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
import com.tangem.tap.common.analytics.topup.TopUpController
import com.tangem.tap.common.chat.ChatManager
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
import com.tangem.tap.common.feedback.FeedbackManager
@ -59,24 +56,11 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.shop.TangemShopService
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tasks.product.DerivationsFinder
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.tokens.UserTokensStorageService
import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator
import com.tangem.tap.domain.totalBalance.di.provideDefaultImplementation
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation
import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager
import com.tangem.tap.domain.walletCurrencies.di.provideDefaultImplementation
import com.tangem.tap.domain.walletStores.WalletStoresManager
import com.tangem.tap.domain.walletStores.di.provideDefaultImplementation
import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.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.utils.coroutines.AppCoroutineDispatcherProvider
@ -95,47 +79,8 @@ lateinit var activityResultCaller: ActivityResultCaller
lateinit var preferencesStorage: PreferencesDataSource
lateinit var walletConnectRepository: WalletConnectRepository
lateinit var shopService: TangemShopService
internal lateinit var userTokensRepository: UserTokensRepository
internal lateinit var derivationsFinder: DerivationsFinder
private val walletStoresRepository by lazy { WalletStoresRepository.provideDefaultImplementation() }
private val walletManagersRepository by lazy {
WalletManagersRepository.provideDefaultImplementation(
walletManagerFactory = WalletManagerFactory(
config = store.state.globalState.configManager
?.config
?.blockchainSdkConfig
?: BlockchainSdkConfig(),
),
)
}
private val walletAmountsRepository by lazy {
WalletAmountsRepository.provideDefaultImplementation(
tangemTechService = store.state.domainNetworks.tangemTechService,
)
}
val walletStoresManager by lazy {
WalletStoresManager.provideDefaultImplementation(
userTokensRepository = userTokensRepository,
walletStoresRepository = walletStoresRepository,
walletManagersRepository = walletManagersRepository,
walletAmountsRepository = walletAmountsRepository,
appCurrencyProvider = { store.state.globalState.appCurrency },
)
}
val walletCurrenciesManager by lazy {
WalletCurrenciesManager.provideDefaultImplementation(
userTokensRepository = userTokensRepository,
walletStoresRepository = walletStoresRepository,
walletManagersRepository = walletManagersRepository,
walletAmountsRepository = walletAmountsRepository,
appCurrencyProvider = { store.state.globalState.appCurrency },
)
}
val totalFiatBalanceCalculator by lazy {
TotalFiatBalanceCalculator.provideDefaultImplementation()
}
@HiltAndroidApp
internal class TapApplication : Application(), ImageLoaderFactory {
@ -161,18 +106,12 @@ internal class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var preferencesDataSource: PreferencesDataSource
@Inject
lateinit var walletFeatureToggles: WalletFeatureToggles
@Inject
lateinit var walletConnect2Repository: WalletConnect2Repository
@Inject
lateinit var walletConnectSessionsRepository: WalletConnectSessionsRepository
// @Inject
// lateinit var learn2earnInteractor: Learn2earnInteractor
@Inject
lateinit var manageTokensFeatureToggles: ManageTokensFeatureToggles
@ -200,12 +139,6 @@ internal class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var balanceHidingRepository: BalanceHidingRepository
@Inject
lateinit var detailsFeatureToggles: DetailsFeatureToggles
@Inject
lateinit var darkThemeFeatureToggle: DarkThemeFeatureToggle
@Inject
lateinit var userTokensStore: UserTokensStore
@ -220,6 +153,12 @@ internal class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var oneTimeEventFilter: OneTimeEventFilter
@Inject
lateinit var derivationsRepository: DerivationsRepository
@Inject
lateinit var testerFeatureToggles: TesterFeatureToggles
// endregion Injected
override fun onCreate() {
@ -227,7 +166,7 @@ internal class TapApplication : Application(), ImageLoaderFactory {
store = createReduxStore()
if (BuildConfig.DEBUG) {
if (BuildConfig.LOG_ENABLED) {
Logger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy()))
Timber.plant(
object : Timber.DebugTree() {
@ -250,7 +189,6 @@ internal class TapApplication : Application(), ImageLoaderFactory {
runBlocking {
initUserWalletsListManager()
featureTogglesManager.init()
// learn2earnInteractor.init()
}
val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT)
@ -266,23 +204,12 @@ internal class TapApplication : Application(), ImageLoaderFactory {
)
}
val userTokensStorageService = UserTokensStorageService.init(context = this)
userTokensRepository = UserTokensRepository.init(
tangemTechService = store.state.domainNetworks.tangemTechService,
networkConnectionManager = networkConnectionManager,
storageService = userTokensStorageService,
)
derivationsFinder = DerivationsFinder(
legacyTokensStore = userTokensStorageService,
newTokensStore = userTokensStore,
walletFeatureToggles = walletFeatureToggles,
dispatchers = AppCoroutineDispatcherProvider(),
)
appStateHolder.mainStore = store
appStateHolder.userTokensRepository = userTokensRepository
appStateHolder.walletStoresManager = walletStoresManager
initTopUpController()
walletConnect2Repository.init(projectId = configManager.config.walletConnectProjectId)
}
@ -292,10 +219,8 @@ internal class TapApplication : Application(), ImageLoaderFactory {
middleware = AppState.getMiddleware(),
state = AppState(
daggerGraphState = DaggerGraphState(
assetReader = assetReader,
networkConnectionManager = networkConnectionManager,
customTokenFeatureToggles = customTokenFeatureToggles,
walletFeatureToggles = walletFeatureToggles,
walletConnectRepository = walletConnect2Repository,
walletConnectSessionsRepository = walletConnectSessionsRepository,
manageTokensFeatureToggles = manageTokensFeatureToggles,
@ -307,26 +232,15 @@ internal class TapApplication : Application(), ImageLoaderFactory {
currenciesRepository = currenciesRepository,
appThemeModeRepository = appThemeModeRepository,
balanceHidingRepository = balanceHidingRepository,
detailsFeatureToggles = detailsFeatureToggles,
walletsRepository = walletsRepository,
sendFeatureToggles = sendFeatureToggles,
derivationsRepository = derivationsRepository,
testerFeatureToggles = testerFeatureToggles,
),
),
)
}
private fun initTopUpController() {
val topUpController = TopUpController(
scanResponseProvider = {
store.state.globalState.scanResponse
?: store.state.globalState.onboardingState.onboardingManager?.scanResponse
},
walletStoresManagerProvider = { walletStoresManager },
topupWalletStorage = preferencesStorage.toppedUpWalletStorage,
)
store.dispatch(GlobalAction.SetTopUpController(topUpController))
}
override fun newImageLoader(): ImageLoader {
return createCoilImageLoader(
context = this,

View file

@ -5,18 +5,13 @@ import android.content.Context
import com.tangem.core.navigation.StateDialog
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.ui.SimpleAlertDialog
import com.tangem.tap.common.ui.SimpleCancelableAlertDialog
import com.tangem.tap.common.ui.*
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.*
import com.tangem.tap.features.onboarding.AddressInfoBottomSheetDialog
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.products.twins.ui.dialog.TwinningProcessNotCompletedDialog
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.*
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.features.wallet.ui.dialogs.*
import com.tangem.tap.features.wallet.ui.wallet.CurrencySelectionDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
@ -50,13 +45,14 @@ class DialogManager : StoreSubscriber<GlobalState> {
if (dialog != null) return
dialog = when (state.dialog) {
is AppDialog.SimpleOkDialog -> SimpleOkDialog.create(state.dialog, context)
is AppDialog.SimpleOkDialogRes -> SimpleOkDialog.create(state.dialog, context)
is AppDialog.SimpleOkErrorDialog -> SimpleOkDialog.create(state.dialog, context)
is AppDialog.SimpleOkWarningDialog -> SimpleOkDialog.create(state.dialog, context)
is StateDialog.ScanFailsDialog -> ScanFailsDialog.create(context)
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context)
is AppDialog.RussianCardholdersWarningDialog -> RussianCardholdersWarningBottomSheetDialog(
context,
state.dialog.data,
)
is OnboardingDialog.TwinningProcessNotCompleted -> TwinningProcessNotCompletedDialog.create(context)
is OnboardingDialog.InterruptOnboarding -> InterruptOnboardingDialog.create(context, state.dialog)
is WalletConnectDialog.UnsupportedCard ->
@ -124,6 +120,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
onReject = state.dialog.onReject,
)
}
is BackupDialog.AttestationFailed -> AttestationFailedDialog.create(context)
is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context)
is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context)
is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(context)
@ -132,11 +129,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
context = context,
cardId = state.dialog.cardId,
)
is WalletDialog.CurrencySelectionDialog -> CurrencySelectionDialog.create(state.dialog, context)
is WalletDialog.ChooseTradeActionDialog -> ChooseTradeActionBottomSheetDialog(context, state.dialog)
is WalletDialog.SelectAmountToSendDialog -> AmountToSendBottomSheetDialog(context, state.dialog)
is WalletDialog.SignedHashesMultiWalletDialog -> SignedHashesWarningDialog.create(context)
is WalletDialog.TokensAreLinkedDialog -> SimpleAlertDialog.create(
is AppDialog.TokensAreLinkedDialog -> SimpleAlertDialog.create(
title = context.getString(state.dialog.titleRes, state.dialog.currencySymbol),
message = context.getString(
state.dialog.messageRes,
@ -145,15 +138,13 @@ class DialogManager : StoreSubscriber<GlobalState> {
),
context = context,
)
is WalletDialog.RemoveWalletDialog -> SimpleCancelableAlertDialog.create(
is AppDialog.RemoveWalletDialog -> SimpleCancelableAlertDialog.create(
title = context.getString(state.dialog.titleRes, state.dialog.currencyTitle),
messageRes = state.dialog.messageRes,
context = context,
primaryButtonRes = state.dialog.primaryButtonRes,
primaryButtonAction = state.dialog.onOk,
)
is WalletDialog.RussianCardholdersWarningDialog ->
RussianCardholdersWarningBottomSheetDialog(context, state.dialog.data)
else -> null
}
dialog?.show()

View file

@ -20,7 +20,7 @@ class GlobalLayoutStateHandler<T : View>(
if (attachImmediately) attach()
}
fun attach() {
private fun attach() {
if (isAttached) {
Timber.d("Already attached")
return

View file

@ -11,20 +11,6 @@ fun postUi(ms: Long = 0, func: Runnable) {
if (ms == 0L) uiHandler.post { func.run() } else uiHandler.postDelayed(func, ms)
}
fun postBackground(ms: Long = 0, func: Runnable) {
if (ms == 0L) backgroundHandler.post { func.run() } else backgroundHandler.postDelayed(func, ms)
}
fun postUiDelayBg(ms: Long, func: Runnable) {
backgroundHandler.postDelayed({ uiHandler.post(func) }, ms)
}
fun post(ms: Long = 0, func: Runnable) {
if (ms == 0L) {
func.run()
} else {
val currentLooper = Looper.myLooper() ?: return
Handler(currentLooper).postDelayed(func, ms)
}
}

View file

@ -1,34 +0,0 @@
package com.tangem.tap.common
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
/**
[REDACTED_AUTHOR]
*/
open class ShimmerRecyclerAdapter(
private val viewHolderViewFactory: (ViewGroup) -> ViewGroup,
) : ListAdapter<ShimmerData, ShimmerVH>(DiffUtilCallback) {
override fun getItemId(position: Int): Long {
return if (currentList.isEmpty()) 0 else currentList[position].hashCode().toLong()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ShimmerVH {
return ShimmerVH(viewHolderViewFactory.invoke(parent))
}
override fun onBindViewHolder(holder: ShimmerVH, position: Int) {}
object DiffUtilCallback : DiffUtil.ItemCallback<ShimmerData>() {
override fun areContentsTheSame(oldItem: ShimmerData, newItem: ShimmerData) = oldItem == newItem
override fun areItemsTheSame(oldItem: ShimmerData, newItem: ShimmerData) = oldItem == newItem
}
}
class ShimmerVH(viewGroup: ViewGroup) : RecyclerView.ViewHolder(viewGroup)
@Suppress("UnusedPrivateMember")
data class ShimmerData(private val any: String = "")

View file

@ -8,11 +8,8 @@ import androidx.appcompat.widget.LinearLayoutCompat
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.store
import com.tangem.wallet.BuildConfig
/**
[REDACTED_AUTHOR]
@ -22,18 +19,6 @@ object TestActions {
// It used only for the test actions in debug or debug_beta builds
var testAmountInjectionForWalletManagerEnabled = false
/**
* @param isTestView - true must be used if you want to show or hide your view depends on BuildConfig
*/
fun initFor(view: View, actions: List<TestAction>, isTestView: Boolean = false) {
if (!BuildConfig.TEST_ACTION_ENABLED) return
if (isTestView) view.show(BuildConfig.TEST_ACTION_ENABLED)
view.setOnClickListener {
store.dispatchDialogShow(AppDialog.TestActionsDialog(actions))
}
}
}
typealias TestAction = Pair<String, () -> Unit>

View file

@ -22,10 +22,6 @@ class AnalyticsFactory {
filters.add(filter)
}
fun addParamsInterceptor(interceptor: ParamsInterceptor) {
interceptors.add(interceptor)
}
fun build(analytics: Analytics, data: AnalyticsHandlerBuilder.Data) {
builders.mapNotNull { it.build(data) }.forEach { analytics.addHandler(it.id(), it) }
filters.forEach { analytics.addFilter(it) }

View file

@ -1,10 +1,10 @@
package com.tangem.tap.common.analytics.converters
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.common.Converter
import com.tangem.common.core.TangemSdkError
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.features.demo.DemoTransactionSender
import com.tangem.utils.converter.Converter
/**
[REDACTED_AUTHOR]

View file

@ -1,17 +1,18 @@
package com.tangem.tap.common.analytics.converters
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.Converter
import com.tangem.domain.common.CardTypesResolver
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.utils.converter.Converter
import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam
/**
[REDACTED_AUTHOR]
*/
class ParamCardCurrencyConverter : Converter<CardTypesResolver, AnalyticsParam.CardCurrency?> {
class ParamCardCurrencyConverter : Converter<CardTypesResolver, CoreAnalyticsParam.WalletType?> {
override fun convert(value: CardTypesResolver): AnalyticsParam.CardCurrency? {
if (value.isMultiwalletAllowed()) return AnalyticsParam.CardCurrency.MultiCurrency
override fun convert(value: CardTypesResolver): CoreAnalyticsParam.WalletType? {
if (value.isMultiwalletAllowed()) return CoreAnalyticsParam.WalletType.MultiCurrency
val type = when {
value.isTangemNote() -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain())
@ -21,6 +22,6 @@ class ParamCardCurrencyConverter : Converter<CardTypesResolver, AnalyticsParam.C
else -> null
} ?: return null
return AnalyticsParam.CardCurrency.SingleCurrency(type)
return CoreAnalyticsParam.WalletType.SingleCurrency(type.value)
}
}

View file

@ -1,9 +1,9 @@
package com.tangem.tap.common.analytics.converters
import com.shopify.buy3.Storefront
import com.tangem.common.Converter
import com.tangem.tap.common.analytics.events.Shop
import com.tangem.tap.features.shop.domain.models.ProductType
import com.tangem.utils.converter.Converter
/**
[REDACTED_AUTHOR]

View file

@ -1,19 +0,0 @@
package com.tangem.tap.common.analytics.converters
import com.tangem.common.Converter
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.common.analytics.events.Basic
/**
[REDACTED_AUTHOR]
*/
class TopUpEventConverter : Converter<Pair<UserWalletId, CardTypesResolver>, Basic.ToppedUp?> {
override fun convert(value: Pair<UserWalletId, CardTypesResolver>): Basic.ToppedUp? {
val (userWalletId, resolver) = value
val paramCardCurrency = ParamCardCurrencyConverter().convert(resolver) ?: return null
return Basic.ToppedUp(userWalletId, paramCardCurrency)
}
}

View file

@ -6,42 +6,36 @@ import com.tangem.tap.features.details.redux.SecurityOption
sealed class AnalyticsParam {
sealed class CurrencyType(val value: String) {
class Currency(currency: com.tangem.tap.features.wallet.models.Currency) : CurrencyType(currency.currencySymbol)
class Currency(currency: com.tangem.tap.domain.model.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 Amount(amount: com.tangem.blockchain.common.Amount) : CurrencyType(amount.currencySymbol)
}
// MultiCurrency or CurrencyType
sealed class CardCurrency(val value: String) {
object MultiCurrency : CardCurrency(value = "Multicurrency")
class SingleCurrency(type: CurrencyType) : CardCurrency(type.value)
}
sealed class CardBalanceState(val value: String) {
object Empty : CardBalanceState("Empty")
object Full : CardBalanceState("Full")
object CustomToken : CardBalanceState("Custom Token")
object BlockchainError : CardBalanceState("Blockchain Error")
object NoRate : CardBalanceState("No Rate")
companion object
}
sealed class RateApp(val value: String) {
object Liked : RateApp("Liked")
object Disliked : RateApp("Disliked")
object Closed : RateApp("Close")
}
sealed class OnOffState(val value: String) {
object On : OnOffState("On")
object Off : OnOffState("Off")
companion object {
operator fun invoke(value: Boolean): OnOffState = if (value) On else Off
}
}
sealed class UserCode(val value: String) {
object AccessCode : UserCode("Access Code")
object Passcode : UserCode("Passcode")
}
sealed class SecurityMode(val value: String) {
@ -78,62 +72,6 @@ sealed class AnalyticsParam {
object BlockchainSdk : Error("Blockchain Sdk Error")
}
sealed class ScannedFrom(val value: String) {
object Introduction : ScannedFrom("Introduction")
object Main : ScannedFrom("Main")
object SignIn : ScannedFrom("Sign In")
object MyWallets : ScannedFrom("My Wallets")
}
sealed class TxSentFrom(val value: String) {
data class Send(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
) : TxSentFrom("Send"), TxData
data class Swap(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
) : TxSentFrom("Swap"), TxData
data class Approve(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
val permissionType: String,
) : TxSentFrom("Approve"), TxData
object WalletConnect : TxSentFrom("WalletConnect")
object Sell : TxSentFrom("Sell")
}
sealed interface TxData {
val blockchain: String
val token: String
val feeType: FeeType
}
sealed class FeeType(val value: String) {
object Fixed : FeeType("Fixed")
object Min : FeeType("Min")
object Normal : FeeType("Normal")
object Max : FeeType("Max")
companion object {
fun fromString(feeType: String): FeeType {
return when (feeType) {
Min.value -> Min
Normal.value -> Normal
Max.value -> Max
Fixed.value -> Fixed
else -> Fixed
}
}
}
}
sealed class WalletCreationType(val value: String) {
object PrivateKey : WalletCreationType(value = "Private Key")
object NewSeed : WalletCreationType(value = "New Seed")

View file

@ -12,23 +12,14 @@ sealed class MainScreen(
class ScreenOpened : MainScreen("Screen opened")
class ButtonScanCard : MainScreen("Button - Scan Card")
class ButtonMyWallets : MainScreen("Button - My Wallets")
class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreen(
event = "Enable Biometric",
params = mapOf("State" to state.value),
)
class MainCurrencyChanged(currencyType: AnalyticsParam.CurrencyType) : MainScreen(
event = "Main Currency Changed",
params = mapOf("Currency Type" to currencyType.value),
)
class NoticeRateAppButton(result: AnalyticsParam.RateApp) : MainScreen(
event = "Notice - Rate The App Button Tapped",
params = mapOf("Result" to result.value),
)
class NoticeBackupYourWalletTapped : MainScreen("Notice - Backup Your Wallet Tapped")
class NoticeScanYourCardTapped : MainScreen("Notice - Scan Your Card Tapped")
}

View file

@ -1,19 +0,0 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent
sealed class MyWallets(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("My Wallets", event, params) {
class MyWalletsScreenOpened : MyWallets(event = "My Wallets Screen Opened")
sealed class Button {
class ScanNewCard : MyWallets(event = "Button - Scan New Card")
class UnlockWithBiometrics : MyWallets(event = "Button - Unlock all with Face ID")
class EditWalletTapped : MyWallets(event = "Button - Edit Wallet Tapped")
class DeleteWalletTapped : MyWallets(event = "Button - Delete Wallet Tapped")
class WalletUnlockTapped : MyWallets(event = "Button - Wallet Unlock Tapped")
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent
/**
[REDACTED_AUTHOR]
*/
sealed class Portfolio(
event: String,
params: Map<String, String> = mapOf(),
) : 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

@ -89,5 +89,12 @@ sealed class Settings(
event = "App Theme Switched",
params = mapOf("State" to theme.value),
)
object EnableBiometrics : AppSettings(event = "Notice - Enable Biometric")
class HideBalanceChanged(state: AnalyticsParam.OnOffState) : AppSettings(
event = "Hide Balance Changed",
params = mapOf("State" to state.value),
)
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.common.analytics.events.AnalyticsParam.CurrencyType
/**
[REDACTED_AUTHOR]
@ -13,50 +12,6 @@ sealed class Token(
error: Throwable? = null,
) : AnalyticsEvent(category, event, params, error) {
class Refreshed : Token("Token", "Refreshed")
class ButtonExplore : Token("Token", "Button - Explore")
class ButtonRemoveToken(type: CurrencyType) : Token(
"Token",
"Button - Remove Token",
params = mapOf("Token" to type.value),
)
class ButtonBuy(type: CurrencyType) : Token(
category = "Token",
event = "Button - Buy",
params = mapOf("Token" to type.value),
)
class ButtonSell(type: CurrencyType) : Token(
category = "Token",
event = "Button - Sell",
params = mapOf("Token" to type.value),
)
class ButtonExchange(type: CurrencyType) : Token(
category = "Token",
event = "Button - Exchange",
params = mapOf("Token" to type.value),
)
class ButtonSend(type: CurrencyType) : Token(
category = "Token",
event = "Button - Send",
params = mapOf("Token" to type.value),
)
class Bought(type: CurrencyType) : Token(
category = "Token",
event = "Token Bought",
params = mapOf("Token" to type.value),
)
object ShowWalletAddress : Token(
category = "Token",
event = "Button - Show the Wallet Address",
)
sealed class Receive(
event: String,
params: Map<String, String> = mapOf(),

View file

@ -3,8 +3,8 @@ package com.tangem.tap.common.analytics.handlers.amplitude
import android.app.Application
import com.amplitude.api.Amplitude
import com.amplitude.api.AmplitudeClient
import com.tangem.common.Converter
import com.tangem.core.analytics.api.EventLogger
import com.tangem.utils.converter.Converter
import org.json.JSONObject
/**

View file

@ -2,7 +2,6 @@ package com.tangem.tap.common.analytics.handlers.firebase
import android.os.Bundle
import androidx.core.os.bundleOf
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
@ -29,8 +28,4 @@ internal class FirebaseClient : FirebaseAnalyticsClient {
}
private fun Map<String, String>.toBundle(): Bundle = bundleOf(*this.toList().toTypedArray())
companion object {
const val ORDER_EVENT = FirebaseAnalytics.Event.PURCHASE
}
}

View file

@ -48,6 +48,7 @@ class CardContextInterceptor(
ProductType.Wallet2 -> "Wallet 2.0"
ProductType.Ring -> "Ring"
ProductType.Start2Coin -> "Start2Coin"
ProductType.Visa -> "VISA"
else -> if (DemoHelper.isDemoCard(scanResponse)) {
if (DemoHelper.isTestDemoCard(scanResponse)) {
"Demo Test"

View file

@ -1,207 +0,0 @@
package com.tangem.tap.common.analytics.topup
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.isZero
import com.tangem.core.analytics.Analytics
import com.tangem.data.source.preferences.model.DataSourceTopupInfo
import com.tangem.data.source.preferences.storage.ToppedUpWalletStorage
import com.tangem.domain.common.BlockchainNetwork
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.common.analytics.converters.TopUpEventConverter
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager
import com.tangem.tap.domain.walletStores.WalletStoresManager
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.scope
import com.tangem.utils.extensions.copy
import kotlinx.coroutines.launch
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
class TopUpController(
var scanResponseProvider: (() -> ScanResponse?)? = null,
var walletStoresManagerProvider: (() -> WalletStoresManager)? = null,
private val topupWalletStorage: ToppedUpWalletStorage,
) : WalletCurrenciesManager.Listener {
private var hadMissedDerivations: Boolean = false
private val addedCurrencies = mutableListOf<Currency>()
override fun didUpdate(userWallet: UserWallet, currency: Currency) {
tryToSend()
}
override fun willCurrenciesAdd(userWallet: UserWallet, currenciesToAdd: List<Currency>) {
addedCurrencies.addAll(currenciesToAdd.distinct())
}
fun walletStoresChanged(walletStores: List<WalletStoreModel>) {
val missedDerivations = walletStores
.flatMap { it.walletsData }
.map { it.status }
.filterIsInstance<WalletDataModel.MissedDerivation>()
hadMissedDerivations = missedDerivations.isNotEmpty()
}
fun scanToGetDerivations() {
hadMissedDerivations = true
}
fun addMissingDerivations(blockchains: List<BlockchainNetwork>) {
hadMissedDerivations = blockchains.isNotEmpty()
}
fun totalBalanceStateChanged(totalFiatBalance: TotalFiatBalance) {
if (totalFiatBalance is TotalFiatBalance.Loaded) tryToSend()
}
fun loadDataSuccess() {
tryToSend()
}
private fun tryToSend() {
if (hadMissedDerivations) return
val scanResponse = scanResponseProvider?.invoke() ?: return
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build() ?: return
val walletStoresManager = walletStoresManagerProvider?.invoke() ?: return
scope.launch {
val walletDataModels = walletStoresManager.getSync(userWalletId).flatMap { it.walletsData }
if (walletDataModels.isEmpty()) return@launch
val isCorrectStatus = walletDataModels.any {
it.status is WalletDataModel.Loading ||
it.status is WalletDataModel.NoAccount ||
it.status is WalletDataModel.Unreachable ||
it.status is WalletDataModel.MissedDerivation ||
it.status.isErrorStatus
}
if (isCorrectStatus) return@launch
val isToppedUpInPast = findToppedUpCurrenciesInPast(walletDataModels).isNotEmpty()
if (isToppedUpInPast) {
val newWalletInfo = DataSourceTopupInfo(
walletId = userWalletId.stringValue,
cardBalanceState = DataSourceTopupInfo.CardBalanceState.Full,
)
topupWalletStorage.save(newWalletInfo)
return@launch
}
val cardBalanceState = BalanceCalculator(walletDataModels).calculate().toCardBalanceState()
send(userWalletId, cardBalanceState, scanResponse.cardTypesResolver)
}
}
/**
* A UserWalletId registration should be after creating wallets
*/
fun registerEmptyWallet(scanResponse: ScanResponse) {
UserWalletIdBuilder.scanResponse(scanResponse).build()?.let {
topupWalletStorage.save(
DataSourceTopupInfo(
walletId = it.stringValue,
cardBalanceState = DataSourceTopupInfo.CardBalanceState.Empty,
),
)
}
}
fun send(scanResponse: ScanResponse, cardBalanceState: AnalyticsParam.CardBalanceState) {
UserWalletIdBuilder.scanResponse(scanResponse).build()?.let {
send(it, cardBalanceState, scanResponse.cardTypesResolver)
}
}
fun send(
userWalletId: UserWalletId,
cardBalanceState: AnalyticsParam.CardBalanceState,
cardTypesResolver: CardTypesResolver,
) {
val topupInfo = topupWalletStorage.restore(userWalletId.stringValue).guard {
val topupInfo = DataSourceTopupInfo(
walletId = userWalletId.stringValue,
cardBalanceState = when (cardBalanceState) {
AnalyticsParam.CardBalanceState.BlockchainError ->
DataSourceTopupInfo.CardBalanceState.BlockchainError
AnalyticsParam.CardBalanceState.CustomToken ->
DataSourceTopupInfo.CardBalanceState.CustomToken
AnalyticsParam.CardBalanceState.Empty ->
DataSourceTopupInfo.CardBalanceState.Empty
AnalyticsParam.CardBalanceState.Full ->
DataSourceTopupInfo.CardBalanceState.Full
AnalyticsParam.CardBalanceState.NoRate ->
DataSourceTopupInfo.CardBalanceState.NoRate
},
)
topupWalletStorage.save(topupInfo)
return
}
val isToppedUp = topupInfo.cardBalanceState == DataSourceTopupInfo.CardBalanceState.Full
if (isToppedUp) return
if (cardBalanceState.isToppedUp()) {
topupWalletStorage.save(topupInfo.copy(cardBalanceState = DataSourceTopupInfo.CardBalanceState.Full))
TopUpEventConverter().convert(value = userWalletId to cardTypesResolver)?.let {
Analytics.send(it)
}
}
}
private fun findToppedUpCurrenciesInPast(walletDataModels: List<WalletDataModel>): List<WalletDataModel> {
val currenciesToppedUpInPast = addedCurrencies.copy()
.mapNotNull { currency ->
val foundCurrencyModel = walletDataModels
.find { it.currency == currency }
?: return@mapNotNull null
if (foundCurrencyModel.status.amount.isZero()) null else foundCurrencyModel
}
addedCurrencies.clear()
return currenciesToppedUpInPast
}
private fun BigDecimal.toCardBalanceState(): AnalyticsParam.CardBalanceState = when {
isZero() -> AnalyticsParam.CardBalanceState.Empty
else -> AnalyticsParam.CardBalanceState.Full
}
private fun AnalyticsParam.CardBalanceState.isToppedUp(): Boolean = this == AnalyticsParam.CardBalanceState.Full
}
private interface IBalanceCalculator {
fun calculate(): BigDecimal
}
private class BalanceCalculator(
private val walletDataModels: List<WalletDataModel>,
) : IBalanceCalculator {
override fun calculate(): BigDecimal {
val singleToken = walletDataModels
.filter { it.currency.isToken() }
.firstOrNull { it.isCardSingleToken }
val totalAmount = singleToken?.status?.amount
?: walletDataModels.calculateTotalCryptoAmount()
return totalAmount
}
private fun List<WalletDataModel>.calculateTotalCryptoAmount(): BigDecimal = this
.map { it.status.amount }
.reduce(BigDecimal::plus)
}

View file

@ -1,67 +0,0 @@
package com.tangem.tap.common.compose
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
@Suppress("MagicNumber")
@Composable
fun TextAutoSize(
text: String,
fontSizeRange: FontSizeRange,
modifier: Modifier = Modifier,
textStyle: TextStyle = LocalTextStyle.current,
) {
val fontSizeValue = remember { mutableStateOf(fontSizeRange.max.value) }
val readyToDraw = remember { mutableStateOf(false) }
val textState = remember { mutableStateOf(text) }
if (textState.value != text) {
readyToDraw.value = false
fontSizeValue.value = fontSizeRange.max.value
textState.value = text
}
Text(
modifier = modifier.drawWithContent { if (readyToDraw.value) drawContent() },
text = text,
softWrap = false,
style = textStyle,
fontSize = fontSizeValue.value.sp,
onTextLayout = {
if (it.hasVisualOverflow) {
val nextFontSizeValue = fontSizeValue.value - fontSizeRange.step.value
if (nextFontSizeValue <= fontSizeRange.min.value) {
fontSizeValue.value = fontSizeRange.min.value
readyToDraw.value = true
} else {
fontSizeValue.value = nextFontSizeValue * 0.8f
}
} else {
readyToDraw.value = true
}
},
)
}
data class FontSizeRange(
val min: TextUnit,
val max: TextUnit,
val step: TextUnit = DEFAULT_TEXT_STEP,
) {
init {
require(min < max) { "min should be less than max, $this" }
require(step.value > 0) { "step should be greater than 0, $this" }
}
companion object {
private val DEFAULT_TEXT_STEP = 1.sp
}
}

View file

@ -1,14 +0,0 @@
package com.tangem.tap.common.compose
import androidx.compose.material.ripple.LocalRippleTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
/**
* Used for disable ripple if button is enable = false
*/
@Composable
fun ToggledRippleTheme(isEnabled: Boolean, content: @Composable () -> Unit) {
val theme = LocalRippleTheme provides if (isEnabled) LocalRippleTheme.current else NoRippleTheme()
CompositionLocalProvider(theme) { content() }
}

View file

@ -1,47 +0,0 @@
package com.tangem.tap.common.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.tangem.domain.common.util.ValueDebouncer
/**
[REDACTED_AUTHOR]
* This is an empty compose view. It just remember the ValueDebouncer inside of itself.
*/
@Composable
fun <T> valueDebouncerAsState(
initialValue: T,
onValueChange: (T) -> Unit,
debounce: Long = 600,
onEmitValueReceive: (T) -> Unit = {},
): ValueDebouncer<T> {
return remember {
ValueDebouncer(
initialValue = initialValue,
debounceDuration = debounce,
onEmitValueReceived = { emitValue ->
emitValue?.let { onEmitValueReceive(it) }
},
onValueChanged = { changedValue ->
changedValue?.let { onValueChange(it) }
},
)
}
}
@Composable
fun <T> valueDebouncerNullableAsState(
initialValue: T?,
onValueChange: (T?) -> Unit,
debounce: Long = 400,
onEmitValueReceive: (T?) -> Unit = {},
): ValueDebouncer<T?> {
return remember {
ValueDebouncer(
initialValue = initialValue,
debounceDuration = debounce,
onEmitValueReceived = onEmitValueReceive,
onValueChanged = onValueChange,
)
}
}

View file

@ -1,33 +0,0 @@
package com.tangem.tap.common.compose
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
/**
[REDACTED_AUTHOR]
*/
@Composable
fun ErrorView(text: String, modifier: Modifier = Modifier, style: TextStyle = LocalTextStyle.current) {
Text(
text,
color = MaterialTheme.colors.error,
modifier = modifier,
style = style,
)
}
@Preview
@Composable
private fun ErrorViewTest() {
Box(Modifier.padding(16.dp)) {
ErrorView(text = "Some error description")
}
}

View file

@ -1,17 +0,0 @@
package com.tangem.tap.common.compose
import androidx.compose.material.ripple.RippleAlpha
import androidx.compose.material.ripple.RippleTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
/**
[REDACTED_AUTHOR]
*/
class NoRippleTheme : RippleTheme {
@Composable
override fun defaultColor() = Color.Unspecified
@Composable
override fun rippleAlpha(): RippleAlpha = RippleAlpha(0.0f, 0.0f, 0.0f, 0.0f)
}

View file

@ -1,165 +0,0 @@
package com.tangem.tap.common.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.text.isDigitsOnly
/**
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun PinCodeWidget(
config: PinViewConfig = tangemPinConfig,
onPinChange: (String, Boolean) -> Unit = { pin, isLastSymbolEntered -> },
) {
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
val rTextFieldValue = remember { mutableStateOf(TextFieldValue("")) }
val indexedSymbols: List<String?> = createPinSymbolsList(config.pinsCount, rTextFieldValue.value.text)
fun isLastSymbolEntered(): Boolean = rTextFieldValue.value.text.length == config.pinsCount
fun handleOnTextFieldValueChanged(value: TextFieldValue) {
if (!value.text.isDigitsOnly()) return
if (value.text.length <= config.pinsCount) {
rTextFieldValue.value = value
onPinChange(value.text, isLastSymbolEntered())
}
}
Box(
modifier = config.modifier
.pointerInput(Unit) {
detectTapGestures {
focusRequester.requestFocus()
keyboardController?.show()
}
},
) {
Row {
for (index in 0 until config.pinsCount) {
PinElement(
config = config,
pinSymbol = indexedSymbols[index] ?: "",
)
}
}
TextField(
modifier = Modifier
.alpha(0f)
.size(1.dp)
.align(Alignment.Center)
.focusRequester(focusRequester),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Decimal,
imeAction = if (isLastSymbolEntered()) ImeAction.Done else ImeAction.Next,
),
keyboardActions = KeyboardActions(
onDone = { keyboardController?.hide() },
),
value = rTextFieldValue.value,
onValueChange = ::handleOnTextFieldValueChanged,
)
}
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
@Composable
private fun PinElement(config: PinViewConfig, pinSymbol: String) {
Box(Modifier.padding(config.pinBoxPadding)) {
Box(config.pinBoxModifier) {
Text(
text = pinSymbol,
modifier = config.pinTextModifier.align(Alignment.Center),
style = config.pinsTextStyle ?: LocalTextStyle.current,
)
}
}
}
private fun createPinSymbolsList(size: Int, text: String): List<String?> = List(size) {
try {
text[it].toString()
} catch (ex: IndexOutOfBoundsException) {
null
}
}
data class PinViewConfig(
val modifier: Modifier = Modifier,
val pinBoxModifier: Modifier = Modifier,
val pinBoxPadding: Dp = 0.dp,
val pinTextModifier: Modifier = Modifier,
val pinsCount: Int = 4,
val pinsTextStyle: TextStyle? = null,
)
private val tangemPinConfig = PinViewConfig(
modifier = Modifier
.wrapContentSize(),
pinBoxModifier = Modifier
.width(42.dp)
.height(56.dp)
.clip(RoundedCornerShape(8.dp))
.background(Color(0xFFF0F0F0)),
pinBoxPadding = 6.dp,
pinTextModifier = Modifier,
pinsCount = 4,
pinsTextStyle = TextStyle(
fontWeight = FontWeight(500),
fontSize = 24.sp,
),
)
@Preview
@Composable
private fun PinCodeWidgetPreview() {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
PinCodeWidget(tangemPinConfig)
}
}

View file

@ -1,49 +0,0 @@
package com.tangem.tap.common.compose
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.common.module.ModuleMessage
import com.tangem.core.ui.components.SpacerH8
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
@Composable
fun AddCustomTokenWarning(warning: ModuleMessage, converter: ModuleMessageConverter, modifier: Modifier = Modifier) {
Surface(
modifier = modifier,
shape = MaterialTheme.shapes.small,
color = colorResource(id = R.color.warning_warning),
elevation = 4.dp,
) {
Column(
modifier = Modifier.padding(16.dp),
) {
Text(
text = stringResource(id = R.string.common_warning),
color = colorResource(id = R.color.white),
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
)
SpacerH8()
Text(
text = converter.convert(warning).message,
color = colorResource(id = R.color.white),
fontSize = 13.sp,
lineHeight = 18.sp,
)
}
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.getFromClipboard
/**
[REDACTED_AUTHOR]
*/
@Suppress("ComposableFunctionName")
@Composable
fun copyToClipboard(value: Any, label: String = "") {
LocalContext.current.copyToClipboard(value, label)
}
@Suppress("ComposableFunctionName")
@Composable
fun getFromClipboard(default: CharSequence? = null): CharSequence? {
return LocalContext.current.getFromClipboard(default)
}

View file

@ -14,6 +14,4 @@ fun Dp.toPx(): Float {
return with(LocalDensity.current) { currentDp.toPx() }
}
fun DpSize.halfWidth(): Dp = this.width / 2
fun DpSize.halfHeight(): Dp = this.height / 2

View file

@ -1,33 +0,0 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalView
import com.tangem.tap.common.extensions.hideKeyboard
@Composable
fun LazyListState.OnBottomReached(loadMoreThreshold: Int, onLoadMore: () -> Unit) {
require(loadMoreThreshold >= 0)
val shouldLoadMore by remember {
derivedStateOf {
val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()
?: return@derivedStateOf false
lastVisibleItem.index >= layoutInfo.totalItemsCount - 1 - loadMoreThreshold
}
}
LaunchedEffect(shouldLoadMore) {
if (shouldLoadMore) onLoadMore()
}
}
@Composable
fun LazyListState.HideKeyboardOnScroll() {
if (isScrollInProgress) {
LocalView.current.hideKeyboard()
}
}

View file

@ -1,14 +0,0 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.runtime.MutableState
/**
[REDACTED_AUTHOR]
*/
fun <T> MutableState<List<T>>.addAndNotify(value: T) {
this.value = this.value.toMutableList().apply { add(value) }
}
fun <T> MutableState<List<T>>.removeAndNotify(value: T) {
this.value = this.value.toMutableList().apply { remove(value) }
}

View file

@ -5,7 +5,6 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.tangem.sdk.extensions.dpToPx
import com.tangem.sdk.extensions.pxToDp
/**
@ -17,8 +16,5 @@ fun Painter.dpSize(): DpSize = DpSize(
intrinsicSize.height.pxToDp().dp,
)
@Composable
private fun Float.dpToPx(): Float = LocalContext.current.dpToPx(this)
@Composable
private fun Float.pxToDp(): Float = LocalContext.current.pxToDp(this)

View file

@ -1,7 +1,6 @@
package com.tangem.tap.common.entities
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.features.wallet.redux.ProgressState
open class Button(val enabled: Boolean)

View file

@ -1,18 +0,0 @@
package com.tangem.tap.common.entities
data class FiatCurrency(
val code: String,
val name: String,
val symbol: String,
) {
val displayName: String
get() = "${this.name} (${this.code}) - ${this.symbol}"
companion object {
val Default = FiatCurrency(
symbol = "$",
code = "USD",
name = "US Dollar",
)
}
}

View file

@ -0,0 +1,5 @@
package com.tangem.tap.common.entities
import com.tangem.tap.common.toggleWidget.WidgetState
enum class ProgressState : WidgetState { Loading, Done, Error }

View file

@ -1,6 +0,0 @@
package com.tangem.tap.common.extensions
import android.content.res.AssetManager
fun AssetManager.readJsonFileToString(fileName: String): String =
this.open("$fileName.json").bufferedReader().readText()

View file

@ -1,7 +1,6 @@
package com.tangem.tap.common.extensions
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import java.io.ByteArrayOutputStream
@Suppress("MagicNumber")
@ -9,9 +8,4 @@ fun Bitmap.toByteArray(): ByteArray {
val stream = ByteArrayOutputStream()
this.compress(Bitmap.CompressFormat.JPEG, 20, stream)
return stream.toByteArray()
}
@Suppress("MagicNumber")
fun ByteArray.toBitmap(): Bitmap {
return BitmapFactory.decodeByteArray(this, 0, this.size)
}

View file

@ -7,10 +7,6 @@ import android.net.*
import androidx.annotation.*
import androidx.core.content.*
fun Context.isPermissionGranted(permission: String): Boolean {
return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
}
/**
* Get uri to any resource type via given Resource Instance
* @param resId - resource id

View file

@ -1,10 +0,0 @@
package com.tangem.tap.common.extensions
import coil.request.ImageRequest
/**
[REDACTED_AUTHOR]
*/
fun ImageRequest.Builder.cardImageData(any: Any?): ImageRequest.Builder = apply {
data(any)
}

View file

@ -1,11 +0,0 @@
package com.tangem.tap.common.extensions
import android.widget.ImageView
import androidx.annotation.DrawableRes
/**
[REDACTED_AUTHOR]
*/
fun ImageView.setDrawable(@DrawableRes resId: Int) {
setImageDrawable(context.getDrawableCompat(resId))
}

View file

@ -14,7 +14,6 @@ import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRe
import com.tangem.tap.features.details.ui.details.DetailsFragment
import com.tangem.tap.features.details.ui.resetcard.ResetCardFragment
import com.tangem.tap.features.details.ui.securitymode.SecurityModeFragment
import com.tangem.tap.features.details.ui.walletconnect.QrScanFragment
import com.tangem.tap.features.details.ui.walletconnect.WalletConnectFragment
import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment
import com.tangem.tap.features.home.HomeFragment
@ -27,9 +26,6 @@ import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.shop.ui.ShopFragment
import com.tangem.tap.features.tokens.impl.presentation.TokensListFragment
import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.walletSelector.ui.WalletSelectorBottomSheetFragment
import com.tangem.tap.features.welcome.ui.WelcomeFragment
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
@ -132,16 +128,9 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.OnboardingTwins -> TwinsCardsFragment()
AppScreen.OnboardingOther -> OnboardingOtherCardsFragment()
AppScreen.Wallet -> {
val featureToggles = store.state.daggerGraphState.get(
getDependency = DaggerGraphState::walletFeatureToggles,
)
if (featureToggles.isRedesignedScreenEnabled) {
store.state.daggerGraphState
.get(getDependency = DaggerGraphState::walletRouter)
.getEntryFragment()
} else {
WalletFragment()
}
store.state.daggerGraphState
.get(getDependency = DaggerGraphState::walletRouter)
.getEntryFragment()
}
AppScreen.Send -> {
val featureToggles = store.state.daggerGraphState.get(
@ -176,24 +165,20 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
}
AppScreen.AddCustomToken -> AddCustomTokenFragment()
AppScreen.WalletDetails -> {
val featureToggles = store.state.daggerGraphState.get(
getDependency = DaggerGraphState::walletFeatureToggles,
)
if (featureToggles.isRedesignedScreenEnabled) {
store.state.daggerGraphState
.get(getDependency = DaggerGraphState::tokenDetailsRouter)
.getEntryFragment()
} else {
WalletDetailsFragment()
}
store.state.daggerGraphState
.get(getDependency = DaggerGraphState::tokenDetailsRouter)
.getEntryFragment()
}
AppScreen.WalletConnectSessions -> WalletConnectFragment()
AppScreen.QrScan -> QrScanFragment()
AppScreen.QrScanning -> {
store.state.daggerGraphState
.get(getDependency = DaggerGraphState::qrScanningRouter)
.getEntryFragment()
}
AppScreen.ReferralProgram -> ReferralFragment()
AppScreen.Swap -> SwapFragment()
AppScreen.Welcome -> WelcomeFragment()
AppScreen.SaveWallet -> SaveWalletBottomSheetFragment()
AppScreen.WalletSelector -> WalletSelectorBottomSheetFragment()
AppScreen.AppCurrencySelector -> AppCurrencySelectorFragment()
AppScreen.ModalNotification -> ModalNotificationBottomSheetFragment()
}

View file

@ -1,17 +1,11 @@
package com.tangem.tap.common.extensions
import android.text.Spanned
import android.text.SpannedString
import android.text.style.RelativeSizeSpan
import androidx.core.text.buildSpannedString
import com.tangem.common.extensions.isZero
import timber.log.Timber
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.text.NumberFormat
import java.util.*
import java.util.Locale
// TODO: move extensions to utils
fun BigDecimal.toFormattedString(
@ -29,105 +23,6 @@ fun BigDecimal.toFormattedString(
return df.format(this)
}
/**
* To formatted crypto currency string
* Specific method because there is no crypto currency codes in Locale
*/
@Suppress("MagicNumber")
fun BigDecimal.toFormattedCryptoCurrencyString(
decimals: Int,
currency: String,
roundingMode: RoundingMode = RoundingMode.DOWN,
limitNumberOfDecimals: Boolean = true,
): String {
val decimalsForRounding = if (limitNumberOfDecimals) {
if (decimals > 8) 8 else decimals
} else {
decimals
}
try {
val locale = Locale.getDefault()
val formatter = NumberFormat.getCurrencyInstance(locale)
// first create currency instance for "USD" with Locale default to replace currency to crypto later
Currency.getInstance("USD")?.let { currencyTmp ->
formatter.currency = currencyTmp
formatter.maximumFractionDigits = decimalsForRounding
formatter.minimumFractionDigits = 0
formatter.isGroupingUsed = true
formatter.roundingMode = roundingMode
// cause formatter created for USD, replace Currency with Crypto symbol on right by Locale place
return formatter.format(this).replace(currencyTmp.getSymbol(locale), "$currency")
}
} catch (e: IllegalArgumentException) {
Timber.e(e, "can't parse currency")
}
// if something went wrong - use old way to format
val formattedAmount = this.toFormattedString(
decimals = decimalsForRounding,
roundingMode = roundingMode,
locale = Locale.getDefault(),
)
return "$formattedAmount $currency"
}
fun BigDecimal.toFiatRateString(fiatCurrencyName: String, fiatCode: String): String {
try {
val formatter = NumberFormat.getCurrencyInstance(Locale.getDefault())
Currency.getInstance(fiatCode)?.let { currency ->
formatter.currency = currency
formatter.maximumFractionDigits = 2
formatter.roundingMode = RoundingMode.HALF_UP
return formatter.format(this).replace(currency.symbol, "$fiatCurrencyName")
}
} catch (e: IllegalArgumentException) {
Timber.e(e, "can't parse currency")
}
val value = this
.setScale(2, RoundingMode.HALF_UP)
.formatWithSpaces()
return "$value $fiatCurrencyName"
}
fun BigDecimal.toFiatString(
rateValue: BigDecimal,
fiatCurrencyName: String,
fiatCode: String,
formatWithSpaces: Boolean = false,
): String {
val fiatValue = rateValue.multiply(this)
return fiatValue.toFormattedFiatValue(
fiatCurrencyName = fiatCurrencyName,
fiatCode = fiatCode,
formatWithSpaces = formatWithSpaces,
)
}
fun BigDecimal.toFiatValue(rateValue: BigDecimal): BigDecimal {
val fiatValue = rateValue.multiply(this)
return fiatValue.setScale(2, RoundingMode.HALF_UP)
}
fun BigDecimal.toFormattedFiatValue(
fiatCurrencyName: String,
fiatCode: String,
formatWithSpaces: Boolean = false,
): String {
try {
val formatter = NumberFormat.getCurrencyInstance(Locale.getDefault())
Currency.getInstance(fiatCode)?.let { currency ->
formatter.currency = currency
formatter.maximumFractionDigits = 2
formatter.roundingMode = RoundingMode.HALF_UP
return formatter.format(this).replace(currency.symbol, "$fiatCurrencyName")
}
} catch (e: IllegalArgumentException) {
Timber.e(e, "can't parse currency")
}
val fiatValue = this.setScale(2, RoundingMode.HALF_UP)
.let { if (formatWithSpaces) it.formatWithSpaces() else it }
return "$fiatValue$fiatCurrencyName"
}
fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString()
// 0.00 -> 0.00
@ -146,70 +41,4 @@ fun BigDecimal.setPrecision(precision: Int, roundingMode: RoundingMode = Roundin
return this.setScale(scale() - precision() + precision, roundingMode)
}
fun BigDecimal.isPositive(): Boolean = this.compareTo(BigDecimal.ZERO) == 1
fun BigDecimal.isNegative(): Boolean = this.compareTo(BigDecimal.ZERO) == -1
fun BigDecimal.isGreaterThan(value: BigDecimal): Boolean = this.compareTo(value) == 1
fun BigDecimal.isLessThan(value: BigDecimal): Boolean = this.compareTo(value) == -1
fun BigDecimal.isGreaterThanOrEqual(value: BigDecimal): Boolean {
val compareResult = this.compareTo(value)
return compareResult == 1 || compareResult == 0
}
fun BigDecimal.isLessThanOrEqual(value: BigDecimal): Boolean {
val compareResult = this.compareTo(value)
return compareResult == -1 || compareResult == 0
}
fun BigDecimal.formatAmountAsSpannedString(
currencySymbol: String,
reminderPartSizeProportion: Float = 0.7f,
): SpannedString {
val amount = this
.setScale(2, RoundingMode.HALF_UP)
.formatWithSpaces()
val integer = amount.substringBefore('.')
val reminder = amount.substringAfter('.')
// test formatter log Log.e("TEST ", BigDecimal("1234567890987654321.1234567890987654321").formatWithSpaces())
return buildSpannedString {
append(integer)
append('.')
append(
"$reminder $currencySymbol",
RelativeSizeSpan(reminderPartSizeProportion),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
)
}
}
@Suppress("MagicNumber")
fun BigDecimal.formatWithSpaces(): String {
val str = this.toString()
var integerStr = str.substringBefore('.')
val reminderStr = str.substringAfter('.')
val packets = arrayListOf<String>()
var index: Int = integerStr.length
while (0 < index) {
if (index <= 3) {
packets.add(integerStr)
break
}
index -= 3
packets.add(integerStr.substring(startIndex = index))
integerStr = integerStr.substring(startIndex = 0, endIndex = index)
}
return buildString {
packets.reversed().forEachIndexed { index, packet ->
append(packet)
if (index != packets.lastIndex) append(' ')
}
if (reminderStr.isNotBlank()) {
append('.')
append(reminderStr)
}
}
}
fun BigDecimal.isPositive(): Boolean = this.signum() == 1

View file

@ -42,16 +42,8 @@ fun Store<*>.dispatchNotification(resId: Int) {
dispatchOnMain(GlobalAction.ShowNotification(resId))
}
suspend fun Store<AppState>.onUserWalletSelected(
userWallet: UserWallet,
refresh: Boolean = false,
sendAnalyticsEvent: Boolean = false,
) {
state.globalState.tapWalletManager.onWalletSelected(userWallet, refresh, sendAnalyticsEvent)
}
fun Store<*>.dispatchToastNotification(resId: Int) {
dispatchOnMain(GlobalAction.ShowToastNotification(resId))
suspend fun Store<AppState>.onUserWalletSelected(userWallet: UserWallet, sendAnalyticsEvent: Boolean = false) {
state.globalState.tapWalletManager.onWalletSelected(userWallet, sendAnalyticsEvent)
}
fun Store<*>.dispatchErrorNotification(error: TapError) {

View file

@ -1,41 +1,12 @@
package com.tangem.tap.common.extensions
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.text.Spannable
import android.text.style.ForegroundColorSpan
import androidx.core.content.ContextCompat
import androidx.core.text.toSpannable
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import java.util.*
fun String?.ellipsizeBeforeSpace(allowedSize: Int): String {
if (this.isNullOrBlank()) return ""
val size = this.length
val sizeDifference = size - allowedSize
val endIndex = this.indexOf(" ")
val startIndex = endIndex - sizeDifference
val newString = this.removeRange(startIndex, endIndex)
return newString.substring(0 until startIndex) + "..." +
newString.substring(startIndex until newString.length)
}
fun String.colorSegment(context: Context, color: Int, startIndex: Int = 0, endIndex: Int = this.length): Spannable {
return this.toSpannable()
.also { spannable ->
spannable.setSpan(
ForegroundColorSpan(ContextCompat.getColor(context, color)),
startIndex,
endIndex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
)
}
}
import java.util.Hashtable
@Suppress("MagicNumber")
fun String.toQrCode(): Bitmap {
@ -58,8 +29,6 @@ fun String.toQrCode(): Bitmap {
return bmp
}
fun String.urlEncode(): String = Uri.encode(this)
fun String.removePrefixOrNull(prefix: String): String? = when {
startsWith(prefix) -> substring(prefix.length)
else -> null

View file

@ -1,24 +1,3 @@
package com.tangem.tap.common.extensions
/**
[REDACTED_AUTHOR]
*/
fun StringBuilder.appendIf(value: String, predicate: (String) -> Boolean): StringBuilder {
if (predicate(value)) this.append(value)
return this
}
fun StringBuilder.appendIfNotNull(value: String?, prefix: String? = null, postfix: String? = null): StringBuilder {
if (value == null) return this
prefix?.let { append(it) }
append(value)
postfix?.let { append(it) }
return this
}
fun String.appendIfNotNull(value: String?, prefix: String? = null, postfix: String? = null): String {
return StringBuilder(this).apply { appendIfNotNull(value, prefix, postfix) }.toString()
}
fun StringBuilder.breakLine(count: Int = 1): StringBuilder = append("\n".repeat(count))

View file

@ -1,31 +0,0 @@
package com.tangem.tap.common.extensions
import android.content.Context
import android.view.View
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
fun Context.toast(message: String, length: Int = Toast.LENGTH_LONG) {
Toast.makeText(this, message, length).show()
}
fun Context.toast(@StringRes messageRes: Int, length: Int = Toast.LENGTH_LONG) {
Toast.makeText(this, this.getString(messageRes), length).show()
}
fun View.toast(message: String, length: Int = Toast.LENGTH_LONG) {
context.toast(message, length)
}
fun View.toast(@StringRes messageRes: Int, length: Int = Toast.LENGTH_LONG) {
context.toast(context.getString(messageRes), length)
}
fun Fragment.toast(message: String, length: Int = Toast.LENGTH_LONG) {
context?.toast(message, length)
}
fun Fragment.toast(@StringRes messageRes: Int, length: Int = Toast.LENGTH_LONG) {
context?.let { it.toast(it.getString(messageRes), length) }
}

View file

@ -1,31 +0,0 @@
package com.tangem.tap.common.extensions
import android.graphics.Color
import androidx.annotation.ColorInt
import androidx.core.graphics.luminance
import androidx.core.graphics.toColorInt
import com.tangem.blockchain.common.Token
@Suppress("MagicNumber")
@ColorInt
fun Token.getColor(isTestnet: Boolean = false): Int {
val defaultColor = "#C7C7CC".toColorInt() // equivalent to R.color.lightGray4
return if (isTestnet) {
defaultColor
} else {
try {
("#" + this.contractAddress.subSequence(2..7).toString()).toColorInt()
} catch (exception: Exception) {
defaultColor
}
}
}
@Suppress("MagicNumber")
@ColorInt
fun Token.getTextColor(isTestnet: Boolean = false): Int = when {
isTestnet -> Color.WHITE
this.getColor().luminance > 0.5 -> Color.BLACK
else -> Color.WHITE
}

View file

@ -1,24 +0,0 @@
package com.tangem.tap.common.extensions
import androidx.transition.Transition
/**
[REDACTED_AUTHOR]
*/
inline fun Transition.addListener(
crossinline onStart: (animator: Transition) -> Unit = {},
crossinline onEnd: (animator: Transition) -> Unit = {},
crossinline onCancel: (animator: Transition) -> Unit = {},
crossinline onPause: (animator: Transition) -> Unit = {},
crossinline onRepeat: (animator: Transition) -> Unit = {},
): Transition.TransitionListener {
val listener = object : Transition.TransitionListener {
override fun onTransitionStart(transition: Transition) = onStart(transition)
override fun onTransitionEnd(transition: Transition) = onEnd(transition)
override fun onTransitionCancel(transition: Transition) = onCancel(transition)
override fun onTransitionPause(transition: Transition) = onPause(transition)
override fun onTransitionResume(transition: Transition) = onRepeat(transition)
}
addListener(listener)
return listener
}

View file

@ -3,25 +3,12 @@
package com.tangem.tap.common.extensions
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.*
import android.graphics.drawable.Drawable
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import androidx.annotation.*
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import com.google.android.material.card.MaterialCardView
fun Context.getDrawableCompat(@DrawableRes drawableResId: Int): Drawable? {
return ContextCompat.getDrawable(this, drawableResId)
@ -72,48 +59,9 @@ fun View.hide(invokeBeforeStateChanged: (() -> Unit)? = null) {
this.visibility = View.GONE
}
fun View.invisible(invisible: Boolean = true, invokeBeforeStateChanged: (() -> Unit)? = null) {
if (invisible) {
if (this.visibility == View.INVISIBLE) return
invokeBeforeStateChanged?.invoke()
this.visibility = View.INVISIBLE
} else {
this.show(invokeBeforeStateChanged)
}
}
fun Context.dpToPixels(dp: Int): Int = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp.toFloat(),
this.resources.displayMetrics,
).toInt()
tailrec fun Context?.getActivity(): Activity? = this as? Activity
?: (this as? ContextWrapper)?.baseContext?.getActivity()
fun MaterialCardView.setMargins(
marginLeftDp: Int = 16,
marginTopDp: Int = 8,
marginRightDp: Int = 16,
marginBottomDp: Int = 8,
) {
val params = this.layoutParams
(params as ViewGroup.MarginLayoutParams).setMargins(
context.dpToPixels(marginLeftDp),
context.dpToPixels(marginTopDp),
context.dpToPixels(marginRightDp),
context.dpToPixels(marginBottomDp),
)
this.layoutParams = params
}
fun View.hideKeyboard() {
val inputMethodManager =
context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
inputMethodManager?.hideSoftInputFromWindow(this.windowToken, 0)
}
fun Context.copyToClipboard(value: Any, label: String = "") {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager ?: return
@ -140,37 +88,6 @@ fun Context.shareText(text: String) {
startActivity(shareIntent)
}
fun Fragment.shareText(text: String) {
requireContext().shareText(text)
}
fun View.getString(resId: Int, vararg formatArgs: Any?): String {
return context.getString(resId, *formatArgs)
}
fun View.animateVisibility(
show: Boolean,
durationMillis: Long = SHORT_ANIMATION_DURATION,
hiddenVisibility: Int = View.GONE,
) {
if (show) {
if (this.visibility == View.VISIBLE) return
this.animate()
.alpha(1f)
.setDuration(durationMillis)
.withStartAction {
this.alpha = 0f
this.isVisible = true
}
} else {
if (this.visibility == hiddenVisibility) return
this.animate()
.alpha(0f)
.setDuration(durationMillis)
.withStartAction {
this.visibility = hiddenVisibility
}
}
}
private const val SHORT_ANIMATION_DURATION = 80L
}

View file

@ -3,15 +3,9 @@ package com.tangem.tap.common.extensions
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewParent
import androidx.core.view.forEach
import androidx.transition.AutoTransition
import androidx.transition.Transition
import androidx.transition.TransitionManager
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.tangem.tap.common.GlobalLayoutStateHandler
import timber.log.Timber
/**
[REDACTED_AUTHOR]
@ -20,30 +14,6 @@ fun ViewGroup.inflate(viewToInflate: Int, attachToRoot: Boolean = false): View {
return LayoutInflater.from(context).inflate(viewToInflate, this, attachToRoot)
}
fun ViewParent?.beginDelayedTransition(transition: Transition = AutoTransition()) {
if (this == null) Timber.e("Can't invoke beginDelayedTransition, because parent is NULL")
(this as? ViewGroup)?.beginDelayedTransition(transition)
}
fun ViewGroup.beginDelayedTransition(transition: Transition = AutoTransition()) {
TransitionManager.beginDelayedTransition(this, transition)
}
fun View.beginDelayedTransition(transition: Transition = AutoTransition()) {
(this as? ViewGroup)?.beginDelayedTransition(transition)
}
fun ChipGroup.fitChipsByGroupWidth() {
val layoutStateHandler = GlobalLayoutStateHandler(this)
layoutStateHandler.onStateChanged = stateHandler@{
if (it.childCount < 2) {
layoutStateHandler.detach()
return@stateHandler
}
val spacingBetweenViews = it.chipSpacingHorizontal * (it.childCount - 1)
val width = (it.width - spacingBetweenViews) / it.childCount
it.forEach { chip -> (chip as? Chip)?.width = width }
layoutStateHandler.detach()
}
}

View file

@ -3,15 +3,14 @@ package com.tangem.tap.common.extensions
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.AddressType
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
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.redux.reducers.createAddressesData
import com.tangem.tap.domain.model.WalletAddressData
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
@ -59,7 +58,7 @@ suspend fun WalletManager.safeUpdate(isDemoCard: Boolean): Result<Wallet> = try
}
}
fun WalletManager.getTopUpUrl(): String? {
internal fun WalletManager.getTopUpUrl(): String? {
val globalState = store.state.globalState
val defaultAddress = wallet.address
@ -73,9 +72,28 @@ fun WalletManager.getTopUpUrl(): String? {
)
}
fun WalletManager?.getAddressData(): WalletDataModel.AddressData? {
internal fun WalletManager?.getAddressData(): WalletAddressData? {
val wallet = this?.wallet ?: return null
val addressDataList = wallet.createAddressesData()
return if (addressDataList.isEmpty()) null else addressDataList[0]
}
private fun Wallet.createAddressesData(): List<WalletAddressData> {
val listOfAddressData = mutableListOf<WalletAddressData>()
// put a defaultAddress at the first place
addresses.forEach {
val addressData = WalletAddressData(
it.value,
it.type,
getShareUri(it.value),
getExploreUrl(it.value),
)
if (it.type == AddressType.Default) {
listOfAddressData.add(0, addressData)
} else {
listOfAddressData.add(addressData)
}
}
return listOfAddressData
}

View file

@ -2,17 +2,6 @@ package com.tangem.tap.common.extensions
import android.webkit.WebView
/**
[REDACTED_AUTHOR]
*/
fun WebView.configureSettings() {
resumeTimers()
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
}
}
fun WebView.stop() {
stopLoading()
pauseTimers()

View file

@ -3,6 +3,7 @@ package com.tangem.tap.common.feedback
import android.os.Build
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.address.Address
import com.tangem.crypto.NetworkType
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletIdBuilder
@ -35,6 +36,7 @@ class AdditionalFeedbackInfo {
var cardIssuer: String = ""
var cardBlockchain: String = ""
var userWalletId: String = ""
var extendedPublicKey: String = ""
// wallets
val walletsInfo = CopyOnWriteArrayList<EmailWalletInfo>()
@ -70,6 +72,9 @@ class AdditionalFeedbackInfo {
cardIssuer = data.card.issuer.name
signedHashesCount = formatSignedHashes(data.card.wallets)
userWalletId = UserWalletIdBuilder.scanResponse(data).build()?.stringValue ?: ""
extendedPublicKey = data.card.wallets.firstOrNull { it.extendedPublicKey != null }
?.extendedPublicKey
?.serialize(networkType = NetworkType.Mainnet).orEmpty()
}
@Deprecated("Don't use it directly")

View file

@ -1,5 +1,6 @@
package com.tangem.tap.common.feedback
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.extensions.breakLine
class FeedbackDataBuilder(
@ -27,26 +28,32 @@ class FeedbackDataBuilder(
}
fun appendWalletsInfo(): FeedbackDataBuilder {
infoHolder.walletsInfo.forEach {
infoHolder.walletsInfo.forEach { walletInfo ->
builder.appendDelimiter()
builder.appendKeyValue("Blockchain", it.blockchain.fullName)
builder.appendKeyValue("Derivation path", it.derivationPath)
builder.appendKeyValue("Outputs count", it.outputsCount)
builder.appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
builder.appendKeyValue("Derivation path", walletInfo.derivationPath)
if (it.tokens.isNotEmpty()) {
if (walletInfo.blockchain == Blockchain.Bitcoin) {
builder.appendKeyValue("XPUB", infoHolder.extendedPublicKey)
}
builder.appendKeyValue("Outputs count", walletInfo.outputsCount)
if (walletInfo.tokens.isNotEmpty()) {
builder.append("Tokens:")
breakLine()
it.tokens.forEach { token ->
walletInfo.tokens.forEach { token ->
builder.appendKeyValue("ID", token.id ?: "[custom token]")
builder.appendKeyValue("Name", token.name)
builder.appendKeyValue("Contract address", token.contractAddress)
}
}
builder.appendKeyValue("Host", it.host)
builder.appendKeyValue("Wallet address", it.addresses)
builder.appendKeyValue("Explorer link", it.explorerLink)
builder.appendKeyValue("Host", walletInfo.host)
builder.appendKeyValue("Wallet address", walletInfo.addresses)
builder.appendKeyValue("Explorer link", walletInfo.explorerLink)
}
return this
}

View file

@ -1,86 +0,0 @@
package com.tangem.tap.common.qrCodeScan
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.common.util.concurrent.ListenableFuture
import com.otaliastudios.cameraview.CameraView.PERMISSION_REQUEST_CODE
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PreviewBinder
import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutQrScanningBinding
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
/**
[REDACTED_AUTHOR]
*/
class ScanQrCodeActivity : AppCompatActivity() {
private val binding: LayoutQrScanningBinding by viewBinding(LayoutQrScanningBinding::bind)
private var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>? = null
private var cameraExecutor: ExecutorService? = null
private val binder = PreviewBinder()
override fun onCreate(state: Bundle?) {
super.onCreate(state)
if (!permissionIsGranted()) requestPermission()
setContentView(R.layout.layout_qr_scanning)
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraExecutor = Executors.newSingleThreadExecutor()
cameraProviderFuture?.addListener(
{
val cameraProvider = cameraProviderFuture?.get()
binder.bindPreview(
context = this,
binding = binding,
lifecycleOwner = this,
cameraProvider = requireNotNull(cameraProvider),
cameraExecutor = requireNotNull(cameraExecutor),
onScanned = { result ->
setResult(SCAN_QR_REQUEST_CODE, Intent().apply { putExtra(SCAN_RESULT, result) })
finish()
},
)
},
ContextCompat.getMainExecutor(this),
)
binding.overlay.post {
binding.overlay.setViewFinder()
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (requestCode != PERMISSION_REQUEST_CODE) return
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
finish()
}
}
private fun permissionIsGranted(): Boolean {
val cameraPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
return cameraPermission == PackageManager.PERMISSION_GRANTED
}
private fun requestPermission() {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), PERMISSION_REQUEST_CODE)
}
companion object {
const val SCAN_QR_REQUEST_CODE = 1001
const val SCAN_RESULT = "scanResult"
}
}

View file

@ -1,42 +0,0 @@
package com.tangem.tap.common.qrCodeScan
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import androidx.core.content.ContextCompat
import com.tangem.wallet.R
class ViewFinderOverlay(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val boxPaint: Paint = Paint().apply {
color = ContextCompat.getColor(context, R.color.white)
style = Paint.Style.STROKE
strokeWidth = context.resources.getDimensionPixelOffset(R.dimen.qr_border_stroke_width).toFloat()
}
private val boxWidthRatio = 0.8F
private val boxCornerRadius: Float =
context.resources.getDimensionPixelOffset(R.dimen.qr_border_corner_radius).toFloat()
private var boxRect: RectF? = null
@Suppress("MagicNumber")
fun setViewFinder() {
val overlayWidth = width.toFloat()
val overlayHeight = height.toFloat()
val boxSize = overlayWidth * boxWidthRatio
val cx = overlayWidth / 2
val cy = overlayHeight / 2
boxRect = RectF(cx - boxSize / 2, cy - boxSize / 2, cx + boxSize / 2, cy + boxSize / 2)
invalidate()
}
override fun draw(canvas: Canvas) {
super.draw(canvas)
boxRect?.let {
canvas.drawRoundRect(it, boxCornerRadius, boxCornerRadius, boxPaint)
}
}
}

View file

@ -3,16 +3,14 @@ package com.tangem.tap.common.redux
import com.tangem.common.extensions.VoidCallback
import com.tangem.core.navigation.StateDialog
import com.tangem.tap.common.TestAction
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.domain.model.WalletAddressData
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
sealed class AppDialog : StateDialog {
data class SimpleOkDialog(val header: String, val message: String, val onOk: VoidCallback? = null) : AppDialog()
data class SimpleOkErrorDialog(val message: String, val onOk: VoidCallback? = null) : AppDialog()
data class SimpleOkWarningDialog(val message: String, val onOk: VoidCallback? = null) : AppDialog()
data class SimpleOkDialogRes(
val headerId: Int,
val messageId: Int,
@ -20,24 +18,33 @@ sealed class AppDialog : StateDialog {
val onOk: VoidCallback? = null,
) : AppDialog()
data class OkCancelDialogRes(
val headerId: Int,
val messageId: Int,
val okButton: DialogButton,
val cancelButton: DialogButton,
) : AppDialog()
data class DialogButton(
val title: Int,
val action: VoidCallback? = null,
)
data class AddressInfoDialog(
internal data class AddressInfoDialog(
val currency: Currency,
val addressData: WalletDataModel.AddressData,
val addressData: WalletAddressData,
) : AppDialog()
data class TestActionsDialog(
val actionsList: List<TestAction>,
) : AppDialog()
data class RussianCardholdersWarningDialog(val data: Data?) : AppDialog() {
data class Data(val topUpUrl: String)
}
data class RemoveWalletDialog(
val currencyTitle: String,
val onOk: () -> Unit,
) : AppDialog() {
val messageRes: Int = R.string.token_details_hide_alert_message
val titleRes: Int = R.string.token_details_hide_alert_title
val primaryButtonRes: Int = R.string.token_details_hide_alert_hide
}
data class TokensAreLinkedDialog(
val currencyTitle: String,
val currencySymbol: String,
) : AppDialog() {
val messageRes: Int = R.string.token_details_unable_hide_alert_message
val titleRes: Int = R.string.token_details_unable_hide_alert_title
}
}

View file

@ -15,8 +15,6 @@ import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.shop.redux.ShopReducer
import com.tangem.tap.features.signin.redux.SignInReducer
import com.tangem.tap.features.tokens.legacy.redux.TokensReducer
import com.tangem.tap.features.wallet.redux.reducers.WalletReducer
import com.tangem.tap.features.walletSelector.redux.WalletSelectorReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphReducer
@ -33,7 +31,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder)
onboardingNoteState = OnboardingNoteReducer.reduce(action, state),
onboardingWalletState = OnboardingWalletReducer.reduce(action, state),
onboardingOtherCardsState = OnboardingOtherCardsReducer.reduce(action, state),
walletState = WalletReducer.reduce(action, state, appStateHolder),
twinCardsState = TwinCardsReducer.reduce(action, state),
sendState = SendScreenReducer.reduce(action, state.sendState),
detailsState = DetailsReducer.reduce(action, state),
@ -43,7 +40,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder)
shopState = ShopReducer.reduce(action, state.shopState),
welcomeState = WelcomeReducer.reduce(action, state),
saveWalletState = SaveWalletReducer.reduce(action, state),
walletSelectorState = WalletSelectorReducer.reduce(action, state),
signInState = SignInReducer.reduce(action, state),
daggerGraphState = DaggerGraphReducer.reduce(action, state),
)

View file

@ -35,10 +35,7 @@ import com.tangem.tap.features.signin.redux.SignInMiddleware
import com.tangem.tap.features.signin.redux.SignInState
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
import com.tangem.tap.features.tokens.legacy.redux.TokensState
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.middlewares.WalletMiddleware
import com.tangem.tap.features.walletSelector.redux.WalletSelectorMiddleware
import com.tangem.tap.features.walletSelector.redux.WalletSelectorState
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeState
import com.tangem.tap.proxy.redux.DaggerGraphMiddleware
@ -54,7 +51,6 @@ data class AppState(
val onboardingNoteState: OnboardingNoteState = OnboardingNoteState(),
val onboardingWalletState: OnboardingWalletState = OnboardingWalletState(),
val onboardingOtherCardsState: OnboardingOtherCardsState = OnboardingOtherCardsState(),
val walletState: WalletState = WalletState(),
val twinCardsState: TwinCardsState = TwinCardsState(),
val sendState: SendState = SendState(),
val detailsState: DetailsState = DetailsState(),
@ -64,7 +60,6 @@ data class AppState(
val shopState: ShopState = ShopState(),
val welcomeState: WelcomeState = WelcomeState(),
val saveWalletState: SaveWalletState = SaveWalletState(),
val walletSelectorState: WalletSelectorState = WalletSelectorState(),
val signInState: SignInState = SignInState(),
val daggerGraphState: DaggerGraphState = DaggerGraphState(),
) : StateType {
@ -72,7 +67,7 @@ data class AppState(
private val domainState: DomainState
get() = domainStore.state
val domainNetworks: NetworkServices
private val domainNetworks: NetworkServices
get() = domainState.globalState.networkServices
val featureRepositoryProvider: FeatureRepositoryProvider
@ -92,7 +87,6 @@ data class AppState(
OnboardingNoteMiddleware.handler,
OnboardingWalletMiddleware.handler,
OnboardingOtherCardsMiddleware.handler,
WalletMiddleware().walletMiddleware,
TwinCardsMiddleware.handler,
SendMiddleware().sendMiddleware,
DetailsMiddleware().detailsMiddleware,
@ -103,12 +97,12 @@ data class AppState(
ShopMiddleware().shopMiddleware,
WelcomeMiddleware().middleware,
SaveWalletMiddleware().middleware,
WalletSelectorMiddleware().middleware,
LockUserWalletsTimerMiddleware().middleware,
AccessCodeRequestPolicyMiddleware().middleware,
SignInMiddleware.middleware,
DaggerGraphMiddleware.daggerGraphMiddleware,
LegacyMiddleware.legacyMiddleware,
TradeCryptoMiddleware.middleware,
)
}
}

View file

@ -3,13 +3,9 @@ package com.tangem.tap.common.redux
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.tap.features.home.data.HomeRepositoryImpl
import com.tangem.tap.features.home.domain.HomeRepository
import com.tangem.tap.features.wallet.data.WalletRepositoryImpl
import com.tangem.tap.features.wallet.domain.WalletRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
class FeatureRepositoryProvider(tangemTechApi: TangemTechApi, dispatchers: CoroutineDispatcherProvider) {
val walletRepository: WalletRepository = WalletRepositoryImpl(tangemTechApi, dispatchers)
val homeRepository: HomeRepository = HomeRepositoryImpl(tangemTechApi, dispatchers)
}

View file

@ -20,22 +20,14 @@ class NotificationsHandler(coordinatorLayout: CoordinatorLayout) {
private val basicCoordinatorLayout = WeakReference(coordinatorLayout)
private var baseLayout = basicCoordinatorLayout
fun replaceBaseLayout(coordinatorLayout: CoordinatorLayout) {
baseLayout = WeakReference(coordinatorLayout)
}
fun returnBaseLayout() {
baseLayout = basicCoordinatorLayout
}
fun showNotification(message: String) {
private fun showNotification(message: String) {
baseLayout.get()?.let { layout ->
Snackbar.make(layout, message, Snackbar.LENGTH_LONG)
.also { snackbar -> snackbar.show() }
}
}
fun showDebugNotification(message: String) {
private fun showDebugNotification(message: String) {
baseLayout.get()?.let { layout ->
Snackbar.make(layout, message, Snackbar.LENGTH_LONG)
.also { snackbar ->

View file

@ -1,7 +0,0 @@
package com.tangem.tap.common.redux
import org.rekotlin.Action
abstract class Request : Action {
abstract suspend fun execute()
}

View file

@ -5,17 +5,15 @@ import com.tangem.common.CompletionResult
import com.tangem.core.navigation.StateDialog
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.models.ChatConfig
import com.tangem.domain.appcurrency.model.AppCurrency
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
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackData
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.redux.DebugErrorAction
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.ToastNotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
@ -26,7 +24,6 @@ sealed class GlobalAction : Action {
// notifications
data class ShowNotification(override val messageResource: Int) : GlobalAction(), NotificationAction
data class ShowToastNotification(override val messageResource: Int) : GlobalAction(), ToastNotificationAction
data class ShowErrorNotification(override val error: TapError) : GlobalAction(), ErrorAction
data class DebugShowErrorNotification(override val error: TapError) : GlobalAction(), DebugErrorAction
@ -61,9 +58,9 @@ sealed class GlobalAction : Action {
data class SetIfCardVerifiedOnline(val verified: Boolean) : GlobalAction()
data class ChangeAppCurrency(val appCurrency: FiatCurrency) : GlobalAction()
data class ChangeAppCurrency(val appCurrency: AppCurrency) : GlobalAction()
object RestoreAppCurrency : GlobalAction() {
data class Success(val appCurrency: FiatCurrency) : GlobalAction()
data class Success(val appCurrency: AppCurrency) : GlobalAction()
}
data class UpdateWalletSignedHashes(
@ -78,7 +75,6 @@ sealed class GlobalAction : Action {
data class SetConfigManager(val configManager: ConfigManager) : GlobalAction()
data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction()
data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction()
data class SetTopUpController(val topUpController: TopUpController) : GlobalAction()
data class SendEmail(val feedbackData: FeedbackData) : GlobalAction()
data class OpenChat(val feedbackData: FeedbackData, val chatConfig: ChatConfig? = null) : GlobalAction()

View file

@ -6,19 +6,18 @@ import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.StateDialog
import com.tangem.datasource.config.models.Config
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.LogConfig
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.core.analytics.models.Basic
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchDialogShow
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.send.redux.SendAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.mainScope
import com.tangem.tap.network.exchangeServices.BuyExchangeService
import com.tangem.tap.network.exchangeServices.CardExchangeRules
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
@ -27,6 +26,9 @@ 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.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -67,17 +69,7 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
}
}
is GlobalAction.RestoreAppCurrency -> {
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()
}
restoreAppCurrency()
}
is GlobalAction.HideWarningMessage -> {
store.state.globalState.warningManager?.let {
@ -87,7 +79,6 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
// store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
// }
store.dispatch(WalletAction.Warnings.Update)
store.dispatch(SendAction.Warnings.Update)
}
}
@ -115,9 +106,6 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
}
feedbackManager.openChat(chatConfig, action.feedbackData)
}
is GlobalAction.UpdateWalletSignedHashes -> {
store.dispatch(WalletAction.Warnings.CheckRemainingSignatures(action.remainingSignatures))
}
is GlobalAction.UpdateFeedbackInfo -> {
store.state.globalState.feedbackManager?.infoHolder
?.setWalletsInfo(action.walletManagers)
@ -169,9 +157,6 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
}
}
}
is GlobalAction.SetTopUpController -> {
walletCurrenciesManager.addListener(action.topUpController)
}
is GlobalAction.UpdateUserWalletsListManager -> {
val walletManagersFacade = store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade)
@ -201,23 +186,12 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
}
}
private fun restoreAppCurrencyLegacy() {
store.dispatch(
GlobalAction.RestoreAppCurrency.Success(
preferencesStorage.fiatCurrenciesPrefStorage.getAppCurrency()
?.run { FiatCurrency(code, name, symbol) }
?: FiatCurrency.Default,
),
)
}
private fun restoreAppCurrencyNew() {
private fun restoreAppCurrency() {
scope.launch {
val currency = store.state.daggerGraphState.get(DaggerGraphState::appCurrencyRepository)
.getSelectedAppCurrency()
.firstOrNull()
?.run { FiatCurrency(code, name, symbol) }
?: FiatCurrency.Default
?: AppCurrency.Default
store.dispatchWithMain(GlobalAction.RestoreAppCurrency.Success(currency))
}

View file

@ -52,7 +52,6 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
globalState.copy(configManager = action.configManager)
}
is GlobalAction.SetWarningManager -> globalState.copy(warningManager = action.warningManager)
is GlobalAction.SetTopUpController -> globalState.copy(topUpController = action.topUpController)
is GlobalAction.UpdateWalletSignedHashes -> {
val card = globalState.scanResponse?.card ?: return globalState
val wallet = card.wallets

View file

@ -2,11 +2,10 @@ package com.tangem.tap.common.redux.global
import com.tangem.core.navigation.StateDialog
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.appcurrency.model.AppCurrency
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
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
@ -23,13 +22,12 @@ data class GlobalState(
val configManager: ConfigManager? = null,
val warningManager: WarningMessagesManager? = null,
val feedbackManager: FeedbackManager? = null,
val appCurrency: FiatCurrency = FiatCurrency.Default,
val appCurrency: AppCurrency = AppCurrency.Default,
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,
val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(),
val userCountryCode: String? = null,
val userWalletsListManager: UserWalletsListManager? = null,
val topUpController: TopUpController? = null,
val appThemeMode: AppThemeMode = AppThemeMode.DEFAULT,
) : StateType

View file

@ -169,7 +169,7 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
return Result.success(products)
}
suspend fun applyPromoCode(promoCode: String, productType: ProductType): Result<TangemProduct> {
private suspend fun applyPromoCode(promoCode: String, productType: ProductType): Result<TangemProduct> {
val checkout = checkouts[productType] ?: return Result.failure(Exception("No checkout"))
val result = if (promoCode.isBlank()) {

View file

@ -19,26 +19,7 @@ import kotlin.coroutines.suspendCoroutine
@Suppress("LargeClass")
class ShopifyService(private val application: Application, val shop: ShopifyShop) {
val client: GraphClient by lazy { initClient() }
suspend fun getShopName(): Result<String> {
val query = query { rootQuery: QueryRootQuery ->
rootQuery
.shop { shopQuery: ShopQuery ->
shopQuery
.name()
}
}
return when (val result = queryAsync(query)) {
is GraphCallResult.Success -> {
val name = result.response.data!!.shop.name
Result.success(name)
}
is GraphCallResult.Failure -> {
Result.failure(result.error)
}
}
}
private val client: GraphClient by lazy { initClient() }
@Suppress("MagicNumber")
suspend fun getProducts(collectionTitleFilter: String? = null): Result<List<Product>> {

View file

@ -8,9 +8,13 @@ import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.ContentViewCallback
import com.google.android.material.snackbar.Snackbar
import com.tangem.sdk.extensions.dpToPx
import com.tangem.wallet.R
/**
@ -27,15 +31,29 @@ class MaxAmountSnackbar(
val parent = view.findSuitableParent() ?: throw IllegalArgumentException(
"No suitable parent found from the given view. Please provide a valid view.",
)
val inflater = LayoutInflater.from(view.context)
val inflater = LayoutInflater.from(parent.context)
val customView = inflater.inflate(R.layout.view_snackbar_max_amount, parent, false) as MaxAmountSnackbarView
customView.setOnClickListener { onClick() }
return MaxAmountSnackbar(parent, customView).apply {
updateBottomMargin()
duration = Snackbar.LENGTH_INDEFINITE
}
}
private fun MaxAmountSnackbar.updateBottomMargin() {
ViewCompat.setOnApplyWindowInsetsListener(this.view) { _, insets ->
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
val bottomInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
this.view.updateLayoutParams<CoordinatorLayout.LayoutParams> {
bottomMargin = imeInsets - bottomInsets + context.dpToPx(dp = 8f).toInt()
}
insets
}
}
private fun View?.findSuitableParent(): ViewGroup? {
var view = this
var fallback: ViewGroup? = null

View file

@ -28,12 +28,12 @@ abstract class BaseTruncate : Truncate {
protected var hasBeenTruncated = false
override fun apply(tv: TextView, text: String, with: String): String {
val roughLength = getRoughFitLength(tv, text, with)
val roughLength = getRoughFitLength(tv, text)
val fittedText = preciseFitting(tv, roughTruncate(text, roughLength), with)
return if (hasBeenTruncated) attachWith(fittedText, with) else fittedText
}
protected fun getRoughFitLength(tv: TextView, text: String, with: String): Int {
private fun getRoughFitLength(tv: TextView, text: String): Int {
val existingSpace = tv.measuredWidth - (tv.paddingStart + tv.paddingEnd)
val textWillTakeSpace = tv.paint.measureText(text)
val overSizeRatio: Float = textWillTakeSpace / existingSpace
@ -44,7 +44,7 @@ abstract class BaseTruncate : Truncate {
return maxLengthOfText.toInt()
}
protected fun preciseFitting(tv: TextView, text: String, with: String): String {
private fun preciseFitting(tv: TextView, text: String, with: String): String {
if (!hasBeenTruncated) return text
val spaceForText = tv.measuredWidth - (tv.paddingStart + tv.paddingEnd)
@ -126,11 +126,5 @@ fun TextView.truncateWith(text: String, type: TruncateType, with: String = "..."
return truncate.apply(this, text, with)
}
fun TextView.truncateStartWith(text: String, with: String = "..."): String =
this.truncateWith(text, TruncateType.START, with)
fun TextView.truncateMiddleWith(text: String, with: String = "..."): String =
this.truncateWith(text, TruncateType.MIDDLE, with)
fun TextView.truncateEndWith(text: String, with: String = "..."): String =
this.truncateWith(text, TruncateType.END, with)
this.truncateWith(text, TruncateType.MIDDLE, with)

View file

@ -3,9 +3,9 @@ package com.tangem.tap.common.toggleWidget
import android.graphics.drawable.Drawable
import android.view.View
import com.google.android.material.button.MaterialButton
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.wallet.redux.ProgressState
/**
[REDACTED_AUTHOR]

View file

@ -2,25 +2,16 @@ package com.tangem.tap.common.toggleWidget
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateInterpolator
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.animation.AnimationSet
import android.view.animation.AnticipateOvershootInterpolator
import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation
import android.view.animation.ScaleAnimation
import android.view.animation.*
import android.widget.ViewSwitcher
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.common.entities.ProgressState
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
@Suppress("MagicNumber")
class RefreshBalanceWidget(
private val root: ViewGroup,
) : ViewStateWidget {
class RefreshBalanceWidget(root: ViewGroup) : ViewStateWidget {
var isShowing: Boolean? = null
private set

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.onboarding
package com.tangem.tap.common.ui
import android.content.Context
import android.os.Bundle
@ -7,14 +7,9 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchShare
import com.tangem.tap.common.extensions.dispatchToastNotification
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.extensions.toQrCode
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.model.WalletAddressData
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.DialogOnboardingAddressInfoBinding
@ -22,7 +17,7 @@ import com.tangem.wallet.databinding.DialogOnboardingAddressInfoBinding
/**
[REDACTED_AUTHOR]
*/
class AddressInfoBottomSheetDialog(
internal class AddressInfoBottomSheetDialog(
private val stateDialog: AppDialog.AddressInfoDialog,
context: Context,
) : BottomSheetDialog(context) {
@ -47,7 +42,7 @@ class AddressInfoBottomSheetDialog(
showData(data = stateDialog.addressData)
}
private fun showData(data: WalletDataModel.AddressData) = with(binding!!) {
private fun showData(data: WalletAddressData) = with(binding!!) {
pseudoToolbar.imvClose.setOnClickListener {
dismissWithAnimation = true
cancel()
@ -57,7 +52,6 @@ class AddressInfoBottomSheetDialog(
btnFlCopyAddress.setOnClickListener {
Analytics.send(Token.Receive.ButtonCopyAddress())
context.copyToClipboard(data.address)
store.dispatchToastNotification(R.string.copy_toast_msg)
}
btnFlShare.setOnClickListener {
Analytics.send(Token.Receive.ButtonShareAddress())

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.wallet.ui.dialogs
package com.tangem.tap.common.ui
import android.content.Context
import android.os.Bundle
@ -6,17 +6,16 @@ import android.view.LayoutInflater
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.store
import com.tangem.wallet.databinding.DialogRussiansCardholdersWarningBinding
class RussianCardholdersWarningBottomSheetDialog(
context: Context,
private val dialogData: WalletDialog.RussianCardholdersWarningDialog.Data?,
private val dialogData: AppDialog.RussianCardholdersWarningDialog.Data?,
) : BottomSheetDialog(context) {
private var binding: DialogRussiansCardholdersWarningBinding? = null
@ -40,9 +39,8 @@ class RussianCardholdersWarningBottomSheetDialog(
if (dialogData != null) {
store.dispatchOpenUrl(dialogData.topUpUrl)
Analytics.send(Token.Topup.ScreenOpened())
} else {
store.dispatch(TradeCryptoAction.Buy(checkUserLocation = false))
}
dismiss()
}
binding?.btnNo?.setOnClickListener {

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.wallet.ui.dialogs
package com.tangem.tap.common.ui
import android.content.Context
import androidx.appcompat.app.AlertDialog

View file

@ -0,0 +1,31 @@
package com.tangem.tap.common.ui
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
object SimpleOkDialog {
fun create(dialog: AppDialog.SimpleOkDialogRes, context: Context): AlertDialog {
val message = if (dialog.args.isEmpty()) {
context.getString(dialog.messageId)
} else {
context.getString(dialog.messageId, *dialog.args.toTypedArray())
}
return AlertDialog.Builder(context).apply {
setTitle(context.getString(dialog.headerId))
setMessage(message)
setPositiveButton(R.string.common_ok) { _, _ -> }
setOnDismissListener {
store.dispatchDialogHide()
dialog.onOk?.invoke()
}
}.create()
}
}

View file

@ -1,36 +0,0 @@
package com.tangem.tap.common.utils
import android.os.Handler
import android.os.Looper
import org.rekotlin.StoreSubscriber
private val mainLooper by lazy { Looper.getMainLooper() }
private val mainHandler by lazy { Handler(mainLooper) }
/**
* A subscriber interface for safely subscribing to state changes in a Store.
*
* @param State the type of the state in the Store
*/
interface SafeStoreSubscriber<State> : StoreSubscriber<State> {
/**
* A function that will be called when the state in the Store changes.
*
* @param state the new state in the Store
*/
override fun newState(state: State) {
if (Thread.currentThread() != mainLooper.thread) {
mainHandler.post { newStateOnMain(state) }
} else {
newStateOnMain(state)
}
}
/**
* A function that will be called on the main thread when the state in the Store changes.
*
* @param state the new state in the Store
*/
fun newStateOnMain(state: State)
}

View file

@ -21,4 +21,8 @@ internal class RuntimeUserWalletsStore(
?.firstOrNull()
?.singleOrNull { it.walletId == key }
}
override suspend fun update(userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet) {
walletsStateHolder.userWalletsListManager?.update(userWalletId, update)
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.tap.di.data
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.card.DefaultDerivationsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object CardDataModule {
@Singleton
@Provides
fun providesDerivationsRepository(
tangemSdkManager: TangemSdkManager,
userWalletsStore: UserWalletsStore,
dispatchers: CoroutineDispatcherProvider,
): DerivationsRepository {
return DefaultDerivationsRepository(tangemSdkManager, userWalletsStore, dispatchers)
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.tap.di.domain
import com.tangem.domain.card.*
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.wallets.legacy.WalletsStateHolder
@ -61,8 +62,14 @@ internal object CardDomainModule {
@Provides
@ViewModelScoped
fun provideDerivePublicKeysUseCase(tangemSdkManager: TangemSdkManager): DerivePublicKeysUseCase {
return DefaultDerivePublicKeysUseCase(tangemSdkManager = tangemSdkManager)
fun provideDerivePublicKeysUseCase(
tangemSdkManager: TangemSdkManager,
derivationsRepository: DerivationsRepository,
): DerivePublicKeysUseCase {
return DefaultDerivePublicKeysUseCase(
tangemSdkManager = tangemSdkManager,
derivationsRepository = derivationsRepository,
)
}
@Provides

View file

@ -0,0 +1,20 @@
package com.tangem.tap.di.domain
import com.tangem.feature.qrscanning.repo.QrScanningEventsRepository
import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object QrScanningDomainModule {
@Provides
@Singleton
fun provideListenToQrScanUseCase(repository: QrScanningEventsRepository): ListenToQrScanningUseCase {
return ListenToQrScanningUseCase(repository)
}
}

View file

@ -18,6 +18,18 @@ import dagger.hilt.android.scopes.ViewModelScoped
@Suppress("TooManyFunctions")
internal object TokensDomainModule {
@Provides
@ViewModelScoped
fun provideAddCryptoCurrenciesUseCase(
currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
): AddCryptoCurrenciesUseCase {
return AddCryptoCurrenciesUseCase(
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
)
}
@Provides
@ViewModelScoped
fun provideFetchTokenListUseCase(
@ -242,4 +254,44 @@ internal object TokensDomainModule {
fun provideGetGlobalTokenListUseCase(tokensListRepository: TokensListRepository): GetGlobalTokenListUseCase {
return GetGlobalTokenListUseCase(repository = tokensListRepository)
}
@Provides
@ViewModelScoped
fun provideCheckTokenCompatibilityUseCase(
repository: NetworksCompatibilityRepository,
): CheckCurrencyCompatibilityUseCase {
return CheckCurrencyCompatibilityUseCase(repository)
}
@Provides
@ViewModelScoped
fun provideFindTokenByContractAddressUseCase(
tokensListRepository: TokensListRepository,
): FindTokenByContractAddressUseCase {
return FindTokenByContractAddressUseCase(repository = tokensListRepository)
}
@Provides
@ViewModelScoped
fun provideValidateContractAddressUseCase(
tokensListRepository: TokensListRepository,
): ValidateContractAddressUseCase {
return ValidateContractAddressUseCase(tokensListRepository = tokensListRepository)
}
@Provides
@ViewModelScoped
fun provideAreTokensSupportedByNetworkUseCase(
repository: NetworksCompatibilityRepository,
): AreTokensSupportedByNetworkUseCase {
return AreTokensSupportedByNetworkUseCase(repository = repository)
}
@Provides
@ViewModelScoped
fun provideGetNetworksSupportedByWallet(
repository: NetworksCompatibilityRepository,
): GetNetworksSupportedByWallet {
return GetNetworksSupportedByWallet(repository = repository)
}
}

View file

@ -2,6 +2,8 @@ package com.tangem.tap.di.domain
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -38,4 +40,10 @@ internal object TransactionDomainModule {
walletManagersFacade = walletManagersFacade,
)
}
@Provides
@ViewModelScoped
fun provideCreateTransactionUseCase(transactionRepository: TransactionRepository): CreateTransactionUseCase {
return CreateTransactionUseCase(transactionRepository)
}
}

View file

@ -3,8 +3,10 @@ package com.tangem.tap.di.domain
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.repository.WalletAddressServiceRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -91,4 +93,36 @@ internal object WalletsDomainModule {
fun providesShouldSaveUserWalletsUseCase(walletsRepository: WalletsRepository): ShouldSaveUserWalletsUseCase {
return ShouldSaveUserWalletsUseCase(walletsRepository = walletsRepository)
}
@Provides
@ViewModelScoped
fun providesValidateWalletAddressUseCase(
walletAddressServiceRepository: WalletAddressServiceRepository,
dispatchers: CoroutineDispatcherProvider,
): ValidateWalletAddressUseCase {
return ValidateWalletAddressUseCase(
walletAddressServiceRepository = walletAddressServiceRepository,
dispatchers = dispatchers,
)
}
@Provides
@ViewModelScoped
fun providesValidateWalletMemoUseCase(
walletAddressServiceRepository: WalletAddressServiceRepository,
): ValidateWalletMemoUseCase {
return ValidateWalletMemoUseCase(walletAddressServiceRepository = walletAddressServiceRepository)
}
@Provides
@ViewModelScoped
fun providesParseSharedAddressUseCase(
walletAddressServiceRepository: WalletAddressServiceRepository,
dispatchers: CoroutineDispatcherProvider,
): ParseSharedAddressUseCase {
return ParseSharedAddressUseCase(
walletAddressServiceRepository = walletAddressServiceRepository,
dispatchers = dispatchers,
)
}
}

View file

@ -25,9 +25,8 @@ import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.pins.SetUserCodeCommand
import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.core.analytics.models.Basic
import com.tangem.tap.derivationsFinder
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
@ -126,15 +125,6 @@ class TangemSdkManager(
}
}
suspend fun createWallet(cardId: String?): CompletionResult<CardDTO> {
return runTaskAsyncReturnOnMain(
CreateWalletAndRescanTask(),
cardId,
initialMessage = Message(resources.getString(R.string.initial_message_create_wallet_body)),
)
.map { CardDTO(it) }
}
suspend fun derivePublicKeys(
cardId: String?,
derivations: Map<ByteArrayKey, List<DerivationPath>>,
@ -267,6 +257,9 @@ class TangemSdkManager(
filter = CardFilter(
allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(),
maxFirmwareVersion = FirmwareVersion(major = 6, minor = 33),
batchIdFilter = CardFilter.Companion.ItemFilter.Deny(
items = setOf("0027", "0030", "0031", "0035", "DA88"),
),
),
)
}

View file

@ -26,7 +26,6 @@ sealed class TapError(
val stateError: String,
) : TapError(R.string.common_custom_string, listOf("Unsupported state: $stateError"))
object UnknownBlockchain : TapError(R.string.wallet_error_unsupported_blockchain_subtitle)
object NoInternetConnection : TapError(R.string.wallet_notification_no_internet)
object AmountExceedsBalance : TapError(R.string.send_validation_amount_exceeds_balance)
data class AmountLowerExistentialDeposit(
@ -41,7 +40,6 @@ sealed class TapError(
object DustChange : TapError(R.string.send_error_dust_change)
sealed class WalletManager {
object CreationError : CustomError(customMessage = "Can't create wallet manager")
class NoAccountError(amountToCreateAccount: String) : CustomError(amountToCreateAccount)
class InternalError(message: String) : CustomError(message)
object BlockchainIsUnreachableTryLater : TapError(R.string.wallet_balance_blockchain_unreachable_try_later)

View file

@ -4,39 +4,27 @@ import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.core.analytics.Analytics
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.operations.attestation.Attestation
import com.tangem.tap.*
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.core.analytics.models.Basic
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
import com.tangem.tap.domain.walletconnect2.domain.models.Account
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
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.tap.store
import com.tangem.tap.tangemSdkManager
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(
private val dispatchers: CoroutineDispatcherProvider = AppCoroutineDispatcherProvider(),
@ -55,15 +43,15 @@ class TapWalletManager(
val walletManagerFactory: WalletManagerFactory
by lazy { WalletManagerFactory(blockchainSdkConfig) }
suspend fun onWalletSelected(userWallet: UserWallet, refresh: Boolean, sendAnalyticsEvent: Boolean) {
suspend fun onWalletSelected(userWallet: UserWallet, sendAnalyticsEvent: Boolean) {
// If a previous job was running, it gets cancelled before the new one starts,
// ensuring that only one job is active at any given time.
loadUserWalletDataJob = CoroutineScope(dispatchers.io)
.launch { loadUserWalletData(userWallet, refresh, sendAnalyticsEvent) }
.launch { loadUserWalletData(userWallet, sendAnalyticsEvent) }
.also { it.join() }
}
private suspend fun loadUserWalletData(userWallet: UserWallet, refresh: Boolean, sendAnalyticsEvent: Boolean) {
private suspend fun loadUserWalletData(userWallet: UserWallet, sendAnalyticsEvent: Boolean) {
Analytics.setContext(userWallet.scanResponse)
if (sendAnalyticsEvent) {
Analytics.send(Basic.WalletOpened())
@ -78,114 +66,11 @@ class TapWalletManager(
withMainContext {
// Order is important
store.dispatch(DisclaimerAction.SetDisclaimer(card.createDisclaimer()))
store.dispatchWalletAction(action = WalletAction.UserWalletChanged(userWallet))
store.dispatchWalletAction(
action = WalletAction.UpdateCanSaveUserWallets(
canSaveUserWallets = store.state.daggerGraphState.get(DaggerGraphState::walletsRepository)
.shouldSaveUserWalletsSync(),
),
)
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.dispatchWalletAction(action = WalletAction.Warnings.CheckIfNeeded)
}
val walletFeatureToggles = store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles)
if (!walletFeatureToggles.isRedesignedScreenEnabled) {
setupWalletConnectV2(userWallet)
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) {
val cardId = if (userWallet.scanResponse.card.backupStatus?.isActive != true) {
userWallet.cardId
} else { // if wallet has backup, any card from wallet can be used to sign
null
}
scope.launch {
val wcInteractor = store.state.daggerGraphState.walletConnectInteractor ?: return@launch
wcInteractor.startListening(
userWalletId = userWallet.walletId.stringValue,
cardId = cardId,
)
}
}
suspend fun loadData(userWallet: UserWallet, refresh: Boolean = false) {
walletStoresManager.fetch(userWallet, refresh)
.doOnSuccess {
Timber.d("Wallet stores fetched for ${userWallet.walletId}")
store.dispatchOnMain(WalletAction.LoadData.Success)
store.state.globalState.topUpController?.loadDataSuccess()
store.dispatchWithMain(WalletAction.Warnings.CheckHashesCount.VerifyOnlineIfNeeded)
val wcInteractor = store.state.daggerGraphState.walletConnectInteractor
wcInteractor?.setUserChains(getAccountsForWc(wcInteractor))
}
.doOnFailure { error ->
val errorAction = when (error) {
is WalletStoresError -> when (error) {
is WalletStoresError.FetchFiatRatesError -> WalletAction.LoadData.Failure(error = null)
is WalletStoresError.UpdateWalletManagerTokensError -> WalletAction.LoadData.Failure(
error = TapError.WalletManager.InternalError(
message = error.cause.localizedMessage ?: error.customMessage,
),
)
is WalletStoresError.WalletManagerNotCreated -> WalletAction.LoadData.Failure(
error = TapError.WalletManager.CreationError,
)
is WalletStoresError.UnknownBlockchain -> WalletAction.LoadData.Failure(
error = TapError.UnknownBlockchain,
)
is WalletStoresError.NoInternetConnection -> WalletAction.LoadData.Failure(
error = TapError.NoInternetConnection,
)
}
else -> WalletAction.LoadData.Failure(error = null)
}
Timber.e(error, "Wallet stores fetching failed for ${userWallet.walletId}")
store.dispatchOnMain(errorAction)
}
}
private suspend fun getAccountsForWc(wcInteractor: WalletConnectInteractor): List<Account> {
val walletManagerToggles = store.state.daggerGraphState
.get(DaggerGraphState::walletFeatureToggles)
val walletManagers = if (walletManagerToggles.isRedesignedScreenEnabled) {
val walletManagerFacade = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
val userWallet = userWalletsListManager.selectedUserWalletSync ?: return emptyList()
walletManagerFacade.getStoredWalletManagers(userWallet.walletId)
} else {
store.state.walletState.walletManagers
}
return walletManagers.mapNotNull {
val wallet = it.wallet
val chainId = wcInteractor.blockchainHelper.networkIdToChainIdOrNull(
wallet.blockchain.toNetworkId(),
)
chainId?.let {
Account(
chainId,
wallet.address,
wallet.publicKey.derivationPath?.rawPath,
)
}
}
}

View file

@ -0,0 +1,80 @@
package com.tangem.tap.domain.card
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import timber.log.Timber
internal typealias Derivations = Map<ByteArrayKey, List<DerivationPath>>
private typealias DerivedKeys = Map<ByteArrayKey, ExtendedPublicKeysMap>
internal class DefaultDerivationsRepository(
private val tangemSdkManager: TangemSdkManager,
private val userWalletsStore: UserWalletsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : DerivationsRepository {
override suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) {
Timber.d("Nothing to derive")
return
}
val derivations = MissedDerivationsFinder(scanResponse = userWallet.scanResponse)
.find(currencies)
.ifEmpty {
Timber.d("Nothing to derive")
return
}
tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)
.doOnSuccess { response ->
updatePublicKeys(userWalletId = userWalletId, keys = response.entries).fold(
onSuccess = { return },
onFailure = { throw it },
)
}
.doOnFailure { throw it }
error("This code should never be reached")
}
private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): Result<Unit> {
return runCatching(dispatchers.io) {
userWalletsStore.update(
userWalletId = userWalletId,
update = { userWallet -> userWallet.updateDerivedKeys(keys) },
)
}
}
private fun UserWallet.updateDerivedKeys(keys: DerivedKeys): UserWallet {
return copy(
scanResponse = scanResponse.copy(
derivedKeys = getUpdatedDerivedKeys(oldKeys = scanResponse.derivedKeys, newKeys = keys),
),
)
}
private fun getUpdatedDerivedKeys(oldKeys: DerivedKeys, newKeys: DerivedKeys): DerivedKeys {
return (oldKeys.keys + newKeys.keys).toSet()
.associateWith { walletKey ->
val oldDerivations = ExtendedPublicKeysMap(oldKeys[walletKey] ?: emptyMap())
val newDerivations = newKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
ExtendedPublicKeysMap(oldDerivations + newDerivations)
}
}
}

View file

@ -8,12 +8,15 @@ import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.tap.domain.TangemSdkManager
// TODO: [REDACTED_JIRA]
internal class DefaultDerivePublicKeysUseCase(
private val tangemSdkManager: TangemSdkManager,
private val derivationsRepository: DerivationsRepository,
) : DerivePublicKeysUseCase {
override suspend fun invoke(
@ -26,4 +29,13 @@ internal class DefaultDerivePublicKeysUseCase(
return Unit.left()
}
override suspend fun invoke(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
): Either<Throwable, Unit> {
return Either.catch {
derivationsRepository.derivePublicKeys(userWalletId, currencies)
}
}
}

View file

@ -0,0 +1,118 @@
package com.tangem.tap.domain.card
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.KeyWalletPublicKey
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.operations.derivation.ExtendedPublicKeysMap
private typealias DerivationData = Pair<ByteArrayKey, List<DerivationPath>>
/**
* Finder of missed derivations
*
* @property scanResponse scanning response
*
[REDACTED_AUTHOR]
*/
internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) {
/** Find missed derivations for given currencies [currencies] */
fun find(currencies: List<CryptoCurrency>): Derivations {
return buildMap<ByteArrayKey, MutableList<DerivationPath>> {
currencies
.mapToNewDerivations()
.forEach { data ->
val current = this[data.first]
if (current != null) {
current.addAll(data.second)
current.distinct()
} else {
this[data.first] = data.second.toMutableList()
}
}
}
}
private fun List<CryptoCurrency>.mapToNewDerivations(): List<DerivationData> {
val config = CardConfig.createConfig(scanResponse.card)
return mapNotNull { currency ->
val blockchain = Blockchain.fromId(id = currency.network.id.value)
val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null
findNewDerivations(curve = curve, scanResponse = scanResponse, currency = currency)
}
}
private fun findNewDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
currency: CryptoCurrency,
): DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val publicKey = wallet.publicKey.toMapKey()
val derivationCandidates = currency
.getDerivationCandidates(curve)
.ifEmpty { return null }
.filterAlreadyDerivedKeys(publicKey)
.ifEmpty { return null }
return publicKey to derivationCandidates
}
private fun CryptoCurrency.getDerivationCandidates(curve: EllipticCurve): List<DerivationPath> {
val blockchain = Blockchain.fromId(id = network.id.value)
return buildList {
add(blockchain.getDerivationPath(curve = curve))
add(blockchain.getCustomDerivationPath(curve = curve, currency = this@getDerivationCandidates))
add(blockchain.getCardanoDerivationPathIfNeeded(currency = this@getDerivationCandidates))
}
.filterNotNull()
.distinct()
}
private fun Blockchain.getDerivationPath(curve: EllipticCurve): DerivationPath? {
return if (getSupportedCurves().contains(curve)) {
derivationPath(style = scanResponse.derivationStyleProvider.getDerivationStyle())
} else {
null
}
}
private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, currency: CryptoCurrency): DerivationPath? {
return if (getSupportedCurves().contains(curve)) {
currency.network.derivationPath.value?.let(::DerivationPath)
} else {
null
}
}
private fun Blockchain.getCardanoDerivationPathIfNeeded(currency: CryptoCurrency): DerivationPath? {
return if (currency is CryptoCurrency.Coin && this == Blockchain.Cardano) {
currency.network.derivationPath.value?.let {
CardanoUtils.extendedDerivationPath(derivationPath = DerivationPath(it))
}
} else {
null
}
}
private fun List<DerivationPath>.filterAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List<DerivationPath> {
val alreadyDerivedPaths = getAlreadyDerivedKeys(publicKey)
return filterNot(alreadyDerivedPaths::contains)
}
private fun getAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List<DerivationPath> {
val extendedPublicKeysMap = scanResponse.derivedKeys[publicKey] ?: ExtendedPublicKeysMap(emptyMap())
return extendedPublicKeysMap.keys.toList()
}
}

View file

@ -51,14 +51,12 @@ data class WarningMessage(
}
enum class Location {
@Json(name = "main")
MainScreen,
@Json(name = "send")
SendScreen,
}
enum class Origin {
Local, Remote
Remote,
}
}

View file

@ -1,26 +1,17 @@
package com.tangem.tap.domain.configurable.warningMessage
import com.tangem.blockchain.common.Blockchain
import com.tangem.utils.extensions.removeBy
import com.tangem.wallet.R
import java.util.concurrent.CopyOnWriteArrayList
/**
[REDACTED_AUTHOR]
*/
// TODO: Delete with WalletFeatureToggles
@Deprecated(message = "Used only in old wallet screen")
// TODO: Delete with SendFeatureToggles
@Deprecated(message = "Used only in old send screen")
class WarningMessagesManager {
private val warningsList = CopyOnWriteArrayList<WarningMessage>()
fun addWarning(warning: WarningMessage) {
if (findWarning(warning) == null) {
warningsList.add(warning)
sortByPriority()
}
}
fun getWarnings(location: WarningMessage.Location, blockchains: List<Blockchain>): List<WarningMessage> {
return warningsList.filter { message ->
val messageBlockchains = message.blockchainList
@ -45,131 +36,7 @@ class WarningMessagesManager {
}
}
fun removeWarnings(origin: WarningMessage.Origin) {
warningsList.removeBy { it.origin == origin }
sortByPriority()
}
fun removeWarnings(messageRes: Int) {
warningsList.removeBy { it.messageResId == messageRes }
}
fun containsWarning(warning: WarningMessage) = warning in warningsList
private fun sortByPriority() {
warningsList.sortBy { it.priority.ordinal }
}
private fun findWarning(warning: WarningMessage): WarningMessage? {
return warningsList.firstOrNull { it == warning }
}
companion object {
const val REMAINING_SIGNATURES_WARNING = 10
val devCardWarning = WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
location = listOf(WarningMessage.Location.MainScreen),
blockchains = null,
titleResId = R.string.common_warning,
// messageResId = R.string.alert_developer_card,
origin = WarningMessage.Origin.Local,
)
val alreadySignedHashesWarning = WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.Temporary,
priority = WarningMessage.Priority.Info,
location = listOf(WarningMessage.Location.MainScreen),
blockchains = null,
titleResId = R.string.common_warning,
// messageResId = R.string.alert_card_signed_transactions,
origin = WarningMessage.Origin.Local,
)
val signedHashesMultiWalletWarning = WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.Temporary,
priority = WarningMessage.Priority.Info,
location = listOf(WarningMessage.Location.MainScreen),
blockchains = null,
// titleResId = R.string.warning_important_security_info,
// messageResId = R.string.warning_signed_tx_previously,
origin = WarningMessage.Origin.Local,
buttonTextId = R.string.warning_button_learn_more,
titleFormatArg = "\u26A0",
)
val appRatingWarning = WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.AppRating,
priority = WarningMessage.Priority.Info,
location = listOf(WarningMessage.Location.MainScreen),
blockchains = null,
titleResId = R.string.warning_rate_app_title,
messageResId = R.string.warning_rate_app_message,
origin = WarningMessage.Origin.Local,
)
val onlineVerificationFailed = WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
location = listOf(WarningMessage.Location.MainScreen),
blockchains = null,
titleResId = R.string.warning_failed_to_verify_card_title,
messageResId = R.string.warning_failed_to_verify_card_message,
origin = WarningMessage.Origin.Local,
)
val testCardWarning = WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.TestCard,
priority = WarningMessage.Priority.Critical,
location = listOf(WarningMessage.Location.MainScreen, WarningMessage.Location.SendScreen),
blockchains = null,
titleResId = R.string.common_warning,
messageResId = R.string.warning_testnet_card_message,
origin = WarningMessage.Origin.Local,
)
val demoCardWarning = WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
location = listOf(WarningMessage.Location.MainScreen),
blockchains = null,
titleResId = R.string.common_warning,
// messageResId = R.string.alert_demo_message,
origin = WarningMessage.Origin.Local,
)
fun remainingSignaturesNotEnough(remainingSignatures: Int): WarningMessage {
return WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
location = listOf(WarningMessage.Location.MainScreen),
blockchains = null,
titleResId = R.string.common_warning,
// messageResId = R.string.warning_low_signatures_format,
origin = WarningMessage.Origin.Local,
messageFormatArg = remainingSignatures.toString(),
)
}
// fun isAlreadySignedHashesWarning(warning: WarningMessage): Boolean {
// return warning.messageResId == R.string.alert_card_signed_transactions
// }
}
}

View file

@ -9,17 +9,6 @@ import com.tangem.domain.userwallets.Artwork
import com.tangem.operations.attestation.CardVerifyAndGetInfo
import com.tangem.operations.attestation.OnlineCardVerifier
val CardDTO.remainingSignatures: Int?
get() = this.wallets.firstOrNull()?.remainingSignatures
val CardDTO.isHdWalletAllowedByApp: Boolean
get() = settings.isHDWalletAllowed
@Suppress("UnnecessaryParentheses")
fun CardDTO.hasSignedHashes(): Boolean {
return wallets.any { (it.totalSignedHashes ?: 0) > 0 }
}
fun CardDTO.signedHashesCount(): Int {
return wallets.sumOf { it.totalSignedHashes ?: 0 }
}

View file

@ -1,17 +0,0 @@
package com.tangem.tap.domain.extensions
import com.tangem.blockchain.common.Token
/**
[REDACTED_AUTHOR]
*/
private val tokenCustomIconUrls = mutableMapOf<String, String>()
fun Token.getCustomIconUrl(): String? {
return tokenCustomIconUrls[this.contractAddress]
}
fun Token.setCustomIconUrl(url: String) {
tokenCustomIconUrls[this.contractAddress] = url
}

Some files were not shown because too many files have changed in this diff Show more