Updated on 2026-08-14

This commit is contained in:
Tangem 2023-06-29 13:33:17 +03:00
commit 2822193ff8
156 changed files with 4469 additions and 3234 deletions

View file

@ -39,6 +39,8 @@ dependencies {
/** Features */
implementation(project(":features:onboarding"))
implementation(project(":features:learn2earn:api"))
implementation(project(":features:learn2earn:impl"))
implementation(project(":features:referral:presentation"))
implementation(project(":features:referral:domain"))
implementation(project(":features:referral:data"))

View file

@ -138,6 +138,10 @@
android:name="com.tangem.tap.features.sprinklr.ui.SprinklrActivity"
android:theme="@style/AppTheme" />
<activity
android:name="com.tangem.feature.learn2earn.presentation.webView.Learn2earnWebViewActivity"
android:theme="@style/Theme.MaterialComponents.Light.NoActionBar" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"

View file

@ -1,6 +1,4 @@
{
"isWalletPayIdEnabled": true,
"isSendingToPayIdEnabled": true,
"isTopUpEnabled": true,
"isCreatingTwinCardsAllowed": true
}

View file

@ -1,6 +1,4 @@
{
"isWalletPayIdEnabled": false,
"isSendingToPayIdEnabled": true,
"isTopUpEnabled": true,
"isCreatingTwinCardsAllowed": true
}

View file

@ -33,6 +33,11 @@ import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.features.intentHandler.IntentProcessor
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
import com.tangem.tap.features.intentHandler.handlers.BuyCurrencyIntentHandler
import com.tangem.tap.features.intentHandler.handlers.SellCurrencyIntentHandler
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.features.welcome.redux.WelcomeAction
@ -45,6 +50,7 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.lang.ref.WeakReference
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
@ -95,6 +101,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var walletConnectInteractor: WalletConnectInteractor
// TODO: fixme: inject through DI
private val intentProcessor: IntentProcessor = IntentProcessor()
private var snackbar: Snackbar? = null
private val dialogManager = DialogManager()
private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind)
@ -117,6 +126,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
lockUserWalletsTimer = LockUserWalletsTimer(owner = this)
initUserWalletsListManager()
initIntentHandlers()
store.dispatch(
ShopAction.CheckIfGooglePayAvailable(
@ -133,6 +143,14 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
)
}
private fun initIntentHandlers() {
val hasSavedWalletsProvider = { store.state.globalState.userWalletsListManager?.hasUserWallets == true }
intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider))
intentProcessor.addHandler(WalletConnectLinkIntentHandler())
intentProcessor.addHandler(BuyCurrencyIntentHandler())
intentProcessor.addHandler(SellCurrencyIntentHandler())
}
private fun initUserWalletsListManager() {
val manager = if (preferencesStorage.shouldSaveUserWallets) {
UserWalletsListManager.provideBiometricImplementation(
@ -164,12 +182,14 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
super.onResume()
notificationsHandler = NotificationsHandler(binding.fragmentContainer)
navigateToInitialScreenIfNeeded(intent)
navigateToInitialScreenIfNeededOnResume(intent)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intentHandler.handleIntent(intent, userWalletsListManager.hasUserWallets)
scope.launch {
intentProcessor.handleIntent(intent)
}
}
override fun onStart() {
@ -185,6 +205,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
override fun onDestroy() {
store.dispatch(NavigationAction.ActivityDestroyed(WeakReference(this)))
intentProcessor.removeAll()
super.onDestroy()
}
@ -235,29 +256,39 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
lockUserWalletsTimer?.restart()
}
private fun navigateToInitialScreenIfNeeded(intent: Intent?) {
private fun navigateToInitialScreenIfNeededOnResume(intentWhichStartedActivity: Intent?) {
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
val isNotScannedBefore = store.state.globalState.scanResponse == null
val isOnboardingServiceNotActive = store.state.globalState.onboardingState.onboardingStarted
val isShopNotOpened = store.state.shopState.total != null
when {
!backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive && isShopNotOpened -> {
navigateToInitialScreen(intent)
navigateToInitialScreenOnResume(intentWhichStartedActivity)
}
backStackIsEmpty -> {
navigateToInitialScreen(intent)
navigateToInitialScreenOnResume(intentWhichStartedActivity)
}
else -> Unit
}
}
private fun navigateToInitialScreen(intent: Intent?) {
private fun navigateToInitialScreenOnResume(intentWhichStartedActivity: Intent?) {
if (store.state.globalState.userWalletsListManager?.hasUserWallets == true) {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Welcome))
store.dispatchOnMain(WelcomeAction.HandleIntentIfNeeded(intent))
store.dispatchOnMain(WelcomeAction.SetInitialIntent(intentWhichStartedActivity))
scope.launch {
val handler = BackgroundScanIntentHandler(hasSavedUserWalletsProvider = { true })
val isBackgroundScanNotHandled = handler.handleIntent(intentWhichStartedActivity)
val hasNotIncompletedBackup = !backupService.hasIncompletedBackup
if (isBackgroundScanNotHandled && hasNotIncompletedBackup) {
store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics)
}
}
} else {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Home))
intentHandler.handleIntent(intent, hasSavedUserWallets = false)
scope.launch {
intentProcessor.handleIntent(intentWhichStartedActivity)
}
}
store.dispatch(BackupAction.CheckForUnfinishedBackup)
}

View file

@ -1,59 +1,67 @@
package com.tangem.tap
import android.app.*
import android.content.*
import android.content.pm.*
import coil.*
import com.tangem.*
import com.tangem.blockchain.common.*
import com.tangem.blockchain.network.*
import com.tangem.core.analytics.*
import com.tangem.core.featuretoggle.manager.*
import com.tangem.data.source.preferences.*
import com.tangem.datasource.api.common.*
import com.tangem.datasource.asset.*
import com.tangem.datasource.config.*
import com.tangem.datasource.config.models.*
import com.tangem.datasource.connection.*
import com.tangem.domain.*
import com.tangem.domain.common.*
import com.tangem.features.wallet.featuretoggles.*
import com.tangem.tap.common.*
import com.tangem.tap.common.analytics.*
import com.tangem.tap.common.analytics.api.*
import com.tangem.tap.common.analytics.handlers.amplitude.*
import com.tangem.tap.common.analytics.handlers.appsFlyer.*
import com.tangem.tap.common.analytics.handlers.firebase.*
import com.tangem.tap.common.analytics.topup.*
import com.tangem.tap.common.chat.*
import com.tangem.tap.common.feedback.*
import com.tangem.tap.common.images.*
import com.tangem.tap.common.log.*
import com.tangem.tap.common.redux.*
import com.tangem.tap.common.redux.global.*
import com.tangem.tap.common.shop.*
import com.tangem.tap.domain.configurable.warningMessage.*
import com.tangem.tap.domain.tokens.*
import com.tangem.tap.domain.totalBalance.*
import com.tangem.tap.domain.totalBalance.di.*
import com.tangem.tap.domain.walletCurrencies.*
import com.tangem.tap.domain.walletCurrencies.di.*
import com.tangem.tap.domain.walletStores.*
import com.tangem.tap.domain.walletStores.di.*
import com.tangem.tap.domain.walletStores.repository.*
import com.tangem.tap.domain.walletStores.repository.di.*
import android.app.Application
import android.content.Context
import android.content.pm.PackageManager
import coil.ImageLoader
import coil.ImageLoaderFactory
import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.FeaturesLocalLoader
import com.tangem.datasource.config.models.Config
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.DomainLayer
import com.tangem.domain.common.LogConfig
import com.tangem.feature.learn2earn.domain.api.Learn2earnInteractor
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
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
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
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.tokens.UserTokensRepository
import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator
import com.tangem.tap.domain.totalBalance.di.provideDefaultImplementation
import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager
import com.tangem.tap.domain.walletCurrencies.di.provideDefaultImplementation
import com.tangem.tap.domain.walletStores.WalletStoresManager
import com.tangem.tap.domain.walletStores.di.provideDefaultImplementation
import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.*
import com.tangem.tap.features.customtoken.api.featuretoggles.*
import com.tangem.tap.proxy.*
import com.tangem.tap.proxy.redux.*
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.wallet.BuildConfig
import dagger.hilt.android.*
import kotlinx.coroutines.*
import okhttp3.logging.*
import org.rekotlin.*
import timber.log.*
import javax.inject.*
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.runBlocking
import okhttp3.logging.HttpLoggingInterceptor
import org.rekotlin.Store
import timber.log.Timber
import javax.inject.Inject
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository as WalletConnect2Repository
lateinit var store: Store<AppState>
@ -102,7 +110,6 @@ val walletCurrenciesManager by lazy {
val totalFiatBalanceCalculator by lazy {
TotalFiatBalanceCalculator.provideDefaultImplementation()
}
val intentHandler by lazy { IntentHandler() }
@HiltAndroidApp
class TapApplication : Application(), ImageLoaderFactory {
@ -137,6 +144,9 @@ class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var walletConnectSessionsRepository: WalletConnectSessionsRepository
@Inject
lateinit var learn2earnInteractor: Learn2earnInteractor
override fun onCreate() {
super.onCreate()
@ -190,8 +200,11 @@ class TapApplication : Application(), ImageLoaderFactory {
appStateHolder.userTokensRepository = userTokensRepository
appStateHolder.walletStoresManager = walletStoresManager
scope.launch {
// TODO: Try to performance and user experience.
// [REDACTED_JIRA]
runBlocking {
featureTogglesManager.init()
learn2earnInteractor.init()
}
initTopUpController()

View file

@ -1,127 +0,0 @@
package com.tangem.tap.common
import android.content.Intent
import android.net.Uri
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.os.Build
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.removePrefixOrNull
import com.tangem.tap.domain.walletconnect.WalletConnectManager
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.welcome.redux.WelcomeAction
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
class IntentHandler {
private val nfcActions = arrayOf(
NfcAdapter.ACTION_NDEF_DISCOVERED,
NfcAdapter.ACTION_TECH_DISCOVERED,
NfcAdapter.ACTION_TAG_DISCOVERED,
)
fun handleIntent(intent: Intent?, hasSavedUserWallets: Boolean) {
handleBackgroundScan(intent, hasSavedUserWallets)
handleWalletConnectLink(intent)
handleBuyCurrencyCallback(intent)
handleSellCurrencyCallback(intent)
}
fun handleWalletConnectLink(intent: Intent?) {
val wcUri = when (intent?.scheme) {
WalletConnectManager.WC_SCHEME -> {
intent.data?.toString()
}
TANGEM_SCHEME -> {
intent.data?.toString()?.removePrefixOrNull(TANGEM_WC_PREFIX)
}
else -> {
null
}
}
if (wcUri != null) {
store.dispatch(WalletConnectAction.HandleDeepLink(wcUri))
}
}
fun handleBackgroundScan(intent: Intent?, hasSavedUserWallets: Boolean): Boolean {
if (intent == null || intent.action !in nfcActions) return false
val tag: Tag? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
}
if (tag == null) return false
intent.action = null
if (hasSavedUserWallets) {
// TODO: Remove delay after [REDACTED_JIRA]
scope.launch {
delay(timeMillis = 200)
store.dispatch(WelcomeAction.ProceedWithCard)
}
} else {
store.dispatch(HomeAction.ReadCard())
}
return true
}
private fun handleBuyCurrencyCallback(intent: Intent?) {
val data = intent?.data ?: return
val successUri = Uri.parse(ExchangeUrlBuilder.SUCCESS_URL)
if (data.host == successUri.host && data.authority == successUri.authority) {
val currency = store.state.walletState.selectedCurrency ?: return
val currencyType = AnalyticsParam.CurrencyType.Currency(currency)
Analytics.send(Token.Bought(currencyType))
}
}
fun handleSellCurrencyCallback(intent: Intent?) {
try {
val transactionID =
intent?.data?.getQueryParameter(TRANSACTION_ID_PARAM) ?: return
val currency =
intent.data?.getQueryParameter(CURRENCY_CODE_PARAM) ?: return
val amount =
intent.data?.getQueryParameter(CURRENCY_AMOUNT_PARAM) ?: return
val destinationAddress =
intent.data?.getQueryParameter(DEPOSIT_WALLET_ADDRESS_PARAM)
?: return
Timber.d("MoonPay Sell: $amount $currency to $destinationAddress")
store.dispatch(
WalletAction.TradeCryptoAction.SendCrypto(
currencyId = currency,
amount = amount,
destinationAddress = destinationAddress,
transactionId = transactionID,
),
)
} catch (exception: Exception) {
Timber.d("Not MoonPay URL")
}
}
companion object {
private const val TRANSACTION_ID_PARAM = "transactionId"
private const val CURRENCY_CODE_PARAM = "baseCurrencyCode"
private const val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount"
private const val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress"
private const val TANGEM_SCHEME = "tangem"
private const val TANGEM_WC_PREFIX = "tangem://wc?uri="
}
}

View file

@ -31,12 +31,14 @@ sealed class Basic(
currency: AnalyticsParam.CardCurrency,
batch: String,
signInType: SignInType,
walletsCount: String,
) : Basic(
event = "Signed in",
params = mapOf(
AnalyticsParam.CURRENCY to currency.value,
AnalyticsParam.BATCH to batch,
"Sign in type" to signInType.name,
"Wallets Count" to walletsCount,
),
) {
enum class SignInType {

View file

@ -6,7 +6,6 @@ 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.common.redux.StateDialog
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
@ -20,7 +19,6 @@ data class GlobalState(
val onboardingState: OnboardingState = OnboardingState(),
val cardVerifiedOnline: Boolean = false,
val tapWalletManager: TapWalletManager = TapWalletManager(),
val payIdManager: PayIdManager = PayIdManager(),
val configManager: ConfigManager? = null,
val warningManager: WarningMessagesManager? = null,
val feedbackManager: FeedbackManager? = null,

View file

@ -1,56 +0,0 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.services.Result
import com.tangem.tap.network.payid.PayIdVerifyService
import com.tangem.tap.network.payid.VerifyPayIdResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.*
class PayIdManager {
@Suppress("MagicNumber")
suspend fun verifyPayId(payId: String, blockchain: Blockchain): Result<VerifyPayIdResponse> =
withContext(Dispatchers.IO) {
val splitPayId = payId.split("\$")
val user = splitPayId[0]
val baseUrl = "https://${splitPayId[1]}/"
return@withContext PayIdVerifyService(baseUrl).verifyAddress(user, blockchain.getPayIdNetwork())
}
private fun Blockchain.getPayIdNetwork(): String {
return when (this) {
Blockchain.XRP -> "XRPL"
Blockchain.RSK -> "RSK"
else -> this.currency
}.lowercase(Locale.getDefault())
}
companion object {
private val payIdRegExp = (
"^[a-z0-9!#@%&*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#@%&*+/=?^_`{|}~-]+)*\\\$(?:(?:[a-z0-9]" +
"(?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z-]*[a-z0-9])?|(?:[0-9]{1,3}\\.){3}[0-9]{1,3})\$"
).toRegex()
val payIdSupported: EnumSet<Blockchain> = EnumSet.of(
Blockchain.XRP,
Blockchain.Ethereum,
Blockchain.Bitcoin,
Blockchain.Litecoin,
Blockchain.Stellar,
Blockchain.Cardano,
Blockchain.CardanoShelley,
Blockchain.BitcoinCash,
Blockchain.Binance,
Blockchain.RSK,
Blockchain.Tezos,
)
fun isPayId(value: String?): Boolean = value?.contains(payIdRegExp) ?: false
}
}
fun Blockchain.isPayIdSupported(): Boolean {
return PayIdManager.payIdSupported.contains(this)
}

View file

@ -1,6 +1,9 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.*
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
@ -134,22 +137,13 @@ class TapWalletManager(
fun updateConfigManager(data: ScanResponse) {
val configManager = store.state.globalState.configManager
val blockchain = data.cardTypesResolver.getBlockchain()
if (data.cardTypesResolver.isStart2Coin()) {
configManager?.turnOff(ConfigManager.IS_SENDING_TO_PAY_ID_ENABLED)
configManager?.turnOff(ConfigManager.IS_TOP_UP_ENABLED)
} else if (blockchain == Blockchain.Bitcoin ||
data.walletData?.blockchain == Blockchain.Bitcoin.id
) {
configManager?.resetToDefault(ConfigManager.IS_SENDING_TO_PAY_ID_ENABLED)
configManager?.resetToDefault(ConfigManager.IS_TOP_UP_ENABLED)
} else {
configManager?.resetToDefault(ConfigManager.IS_SENDING_TO_PAY_ID_ENABLED)
configManager?.resetToDefault(ConfigManager.IS_TOP_UP_ENABLED)
}
}
}
fun Wallet.getFirstToken(): Token? {
return getTokens().toList().getOrNull(0)
}
fun Wallet.getFirstToken(): Token? = getTokens().toList().getOrNull(index = 0)

View file

@ -26,6 +26,11 @@ interface UserWalletsListManager {
* */
val hasUserWallets: Boolean
/**
* Count of saved user wallets
*/
val walletsCount: Int
/**
* Set [UserWallet] with provided [UserWalletId] as selected
*

View file

@ -53,6 +53,9 @@ internal class BiometricUserWalletsListManager(
override val hasUserWallets: Boolean
get() = keysRepository.hasSavedEncryptionKeys()
override val walletsCount: Int
get() = state.value.userWallets.size
override suspend fun unlock(): CompletionResult<UserWallet> {
return unlockWithBiometryInternal()
.mapFailure { error ->

View file

@ -7,13 +7,7 @@ import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.userWalletList.UserWalletsListError
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet
import kotlinx.coroutines.flow.*
@OptIn(ExperimentalCoroutinesApi::class)
internal class RuntimeUserWalletsListManager : UserWalletsListManager {
@ -36,6 +30,12 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager {
override val hasUserWallets: Boolean
get() = state.value.userWallet != null
/**
* only 1 wallet stored in runtime implementation
*/
override val walletsCount: Int
get() = 1
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
state.value.userWallet
?.takeIf { it.walletId == userWalletId }

View file

@ -12,36 +12,47 @@ import androidx.compose.ui.platform.ComposeView
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.Fragment
import com.google.accompanist.appcompattheme.AppCompatTheme
import androidx.fragment.app.activityViewModels
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.learn2earn.presentation.Learn2earnViewModel
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.home.compose.StoriesScreen
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.home.redux.Stories
import com.tangem.tap.features.tokens.legacy.redux.TokensAction
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber
@AndroidEntryPoint
class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
private val learn2earnViewModel by activityViewModels<Learn2earnViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
store.dispatch(HomeAction.OnCreate)
store.dispatch(HomeAction.Init)
if (learn2earnViewModel.uiState.storyScreenState.isVisible) {
store.dispatch(HomeAction.InsertStory(position = 0, Stories.OneInchPromo))
// re init homeState after inserting learn2earn story
homeState = mutableStateOf(store.state.homeState)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(inflater.context).apply {
setContent {
BackHandler {
requireActivity().finish()
}
AppCompatTheme {
TangemTheme {
BackHandler {
requireActivity().finish()
}
ScreenContent()
}
}
@ -79,7 +90,8 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
@Composable
private fun ScreenContent() {
StoriesScreen(
homeState,
homeState = homeState,
onLearn2earnClick = learn2earnViewModel.uiState.storyScreenState.onClick,
onScanButtonClick = {
Analytics.send(IntroductionProcess.ButtonScanCard())
store.dispatch(HomeAction.ReadCard())

View file

@ -5,29 +5,11 @@ package com.tangem.tap.features.home.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.union
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@ -43,38 +25,40 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.tap.features.home.compose.content.FirstStoriesContent
import com.tangem.tap.features.home.compose.content.StoriesCurrencies
import com.tangem.tap.features.home.compose.content.StoriesRevolutionaryWallet
import com.tangem.tap.features.home.compose.content.StoriesUltraSecureBackup
import com.tangem.tap.features.home.compose.content.StoriesWalletForEveryone
import com.tangem.tap.features.home.compose.content.StoriesWeb3
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.learn2earn.presentation.ui.Learn2earnStoriesScreen
import com.tangem.tap.features.home.compose.content.*
import com.tangem.tap.features.home.compose.views.HomeButtons
import com.tangem.tap.features.home.compose.views.StoriesProgressBar
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.home.redux.Stories
import com.tangem.wallet.R
import kotlin.math.max
private const val STEPS = 6
@Suppress("LongMethod", "ComplexMethod")
@Composable
fun StoriesScreen(
homeState: MutableState<HomeState>,
onLearn2earnClick: () -> Unit,
onScanButtonClick: () -> Unit,
onShopButtonClick: () -> Unit,
onSearchTokensClick: () -> Unit,
) {
val currentStep = remember { mutableStateOf(1) }
val systemUiController = rememberSystemUiController()
val state = homeState.value
val isDarkBackground = currentStep.value !in 3..5
var currentStory by remember { mutableStateOf(state.firstStory) }
val currentStep = { state.stepOf(currentStory) }
val goToPreviousScreen = {
currentStep.value = max(1, currentStep.value - 1)
currentStory = state.stories[max(0, currentStep() - 1)]
}
val goToNextScreen = {
currentStep.value = if (currentStep.value < STEPS) currentStep.value + 1 else 1
currentStory = if (currentStep() < state.stories.lastIndex) {
state.stories[currentStep() + 1]
} else {
state.firstStory
}
}
val isPressed = remember { mutableStateOf(false) }
@ -82,10 +66,10 @@ fun StoriesScreen(
val hideContent = remember { mutableStateOf(true) }
LaunchedEffect(key1 = isDarkBackground) {
LaunchedEffect(key1 = currentStory.isDarkBackground) {
systemUiController.setSystemBarsColor(
color = Color.Transparent,
darkIcons = !isDarkBackground,
darkIcons = !currentStory.isDarkBackground,
)
}
@ -134,7 +118,7 @@ fun StoriesScreen(
},
)
}
if (!isDarkBackground) {
if (!currentStory.isDarkBackground) {
Image(
modifier = Modifier.fillMaxSize(),
painter = painterResource(id = R.drawable.ic_overlay),
@ -154,9 +138,9 @@ fun StoriesScreen(
verticalArrangement = Arrangement.Center,
) {
StoriesProgressBar(
steps = STEPS,
currentStep = currentStep.value,
stepDuration = currentStep.duration(),
steps = state.stories.lastIndex,
currentStep = currentStep(),
stepDuration = currentStory.duration,
paused = isPaused,
onStepFinish = goToNextScreen,
)
@ -169,15 +153,16 @@ fun StoriesScreen(
.height(17.dp)
.alpha(if (hideContent.value) 0f else 1f)
.align(Alignment.Start),
colorFilter = if (isDarkBackground) null else ColorFilter.tint(Color.Black),
colorFilter = if (currentStory.isDarkBackground) null else ColorFilter.tint(Color.Black),
)
when (currentStep.value) {
1 -> FirstStoriesContent(isPaused, currentStep.duration()) { hideContent.value = it }
2 -> StoriesRevolutionaryWallet(currentStep.duration())
3 -> StoriesUltraSecureBackup(isPaused, currentStep.duration())
4 -> StoriesCurrencies(isPaused, currentStep.duration())
5 -> StoriesWeb3(isPaused, currentStep.duration())
6 -> StoriesWalletForEveryone(currentStep.duration())
when (currentStory) {
Stories.OneInchPromo -> Learn2earnStoriesScreen(onLearn2earnClick)
Stories.TangemIntro -> FirstStoriesContent(isPaused, currentStory.duration) { hideContent.value = it }
Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet(currentStory.duration)
Stories.UltraSecureBackup -> StoriesUltraSecureBackup(isPaused, currentStory.duration)
Stories.Currencies -> StoriesCurrencies(isPaused, currentStory.duration)
Stories.Web3 -> StoriesWeb3(isPaused, currentStory.duration)
Stories.WalletForEveryone -> StoriesWalletForEveryone(currentStory.duration)
}
}
Column(
@ -186,7 +171,7 @@ fun StoriesScreen(
.align(Alignment.BottomCenter)
.fillMaxWidth(),
) {
if (currentStep.value == 4) {
if (currentStory == Stories.Currencies) {
Button(
onClick = onSearchTokensClick,
modifier = Modifier
@ -210,29 +195,31 @@ fun StoriesScreen(
)
}
}
HomeButtons(
modifier = Modifier
.padding(start = 16.dp, top = 0.dp, end = 16.dp, bottom = 37.dp)
.fillMaxWidth(),
isDarkBackground = isDarkBackground,
btnScanStateInProgress = homeState.value.btnScanStateInProgress,
onScanButtonClick = onScanButtonClick,
onShopButtonClick = onShopButtonClick,
)
if (currentStory != Stories.OneInchPromo) {
HomeButtons(
modifier = Modifier
.padding(
start = TangemTheme.dimens.size16,
end = TangemTheme.dimens.size16,
bottom = TangemTheme.dimens.size36,
)
.fillMaxWidth(),
isDarkBackground = currentStory.isDarkBackground,
btnScanStateInProgress = homeState.value.btnScanStateInProgress,
onScanButtonClick = onScanButtonClick,
onShopButtonClick = onShopButtonClick,
)
}
}
}
}
@Suppress("MagicNumber")
private fun MutableState<Int>.duration(): Int = when (this.value) {
1 -> 8000
else -> 6000
}
@Preview
@Composable
private fun StoriesScreenPreview() {
StoriesScreen(
onLearn2earnClick = {},
onScanButtonClick = {},
onShopButtonClick = {},
onSearchTokensClick = {},

View file

@ -4,12 +4,7 @@ import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
@ -52,7 +47,7 @@ fun StoriesProgressBar(
// .height()
.padding(start = 9.dp, end = 9.dp, top = 16.dp),
) {
for (index in 1..steps) {
for (index in 0..steps) {
Row(
modifier = Modifier
.height(2.dp)

View file

@ -11,6 +11,8 @@ sealed class HomeAction : Action {
object OnCreate : HomeAction()
object Init : HomeAction()
data class InsertStory(val position: Int, val story: Stories) : HomeAction()
data class ReadCard(
val analyticsEvent: AnalyticsEvent? = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Introduction),
) : HomeAction()

View file

@ -12,6 +12,13 @@ private fun internalReduce(action: Action, state: AppState): HomeState {
var state = state.homeState
when (action) {
is HomeAction.InsertStory -> {
state = state.copy(
stories = state.stories.toMutableList().apply {
add(action.position, action.story)
},
)
}
is HomeAction.ScanInProgress -> {
state = state.copy(scanInProgress = action.scanInProgress)
}

View file

@ -8,8 +8,38 @@ import org.rekotlin.StateType
data class HomeState(
val scanInProgress: Boolean = false,
val btnScanState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.ENABLED),
val stories: List<Stories> = initDefaultStories(),
) : StateType {
val firstStory: Stories
get() = stories[0]
val btnScanStateInProgress: Boolean
get() = btnScanState.progressState == ProgressState.Loading
fun stepOf(story: Stories): Int = stories.indexOf(story)
companion object {
fun initDefaultStories(): List<Stories> = listOf(
Stories.TangemIntro,
Stories.RevolutionaryWallet,
Stories.UltraSecureBackup,
Stories.Currencies,
Stories.Web3,
Stories.WalletForEveryone,
)
}
}
sealed class Stories(
val isDarkBackground: Boolean,
val duration: Int,
) {
object OneInchPromo : Stories(true, duration = 8000)
object TangemIntro : Stories(true, duration = 8000)
object RevolutionaryWallet : Stories(true, duration = 6000)
object UltraSecureBackup : Stories(false, duration = 6000)
object Currencies : Stories(false, duration = 6000)
object Web3 : Stories(false, duration = 6000)
object WalletForEveryone : Stories(true, duration = 6000)
}

View file

@ -0,0 +1,10 @@
package com.tangem.tap.features.intentHandler
import android.content.Intent
/**
[REDACTED_AUTHOR]
*/
interface IntentHandler {
suspend fun handleIntent(intent: Intent?): Boolean
}

View file

@ -0,0 +1,31 @@
package com.tangem.tap.features.intentHandler
import android.content.Intent
import java.util.concurrent.CopyOnWriteArrayList
/**
[REDACTED_AUTHOR]
*/
// TODO: fixme: close it with the combined interfaces IntentHandler and IntentHandlerHolder
class IntentProcessor {
private val intentHandlers = CopyOnWriteArrayList<IntentHandler>()
fun addHandler(handler: IntentHandler) {
intentHandlers.add(handler)
}
fun removeIntentHandler(handler: IntentHandler) {
intentHandlers.remove(handler)
}
fun removeAll() {
intentHandlers.clear()
}
suspend fun handleIntent(intent: Intent?) {
intentHandlers.forEach {
it.handleIntent(intent)
}
}
}

View file

@ -0,0 +1,52 @@
package com.tangem.tap.features.intentHandler.handlers
import android.content.Intent
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.os.Build
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.features.welcome.redux.WelcomeAction
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
[REDACTED_AUTHOR]
*/
class BackgroundScanIntentHandler(
private val hasSavedUserWalletsProvider: () -> Boolean,
) : IntentHandler {
private val nfcActions = arrayOf(
NfcAdapter.ACTION_NDEF_DISCOVERED,
NfcAdapter.ACTION_TECH_DISCOVERED,
NfcAdapter.ACTION_TAG_DISCOVERED,
)
override suspend fun handleIntent(intent: Intent?): Boolean {
if (intent == null || intent.action !in nfcActions) return false
val tag: Tag? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
}
if (tag == null) return false
intent.action = null
if (hasSavedUserWalletsProvider.invoke()) {
// TODO: Remove delay after [REDACTED_JIRA]
scope.launch {
delay(timeMillis = 200)
store.dispatch(WelcomeAction.ProceedWithCard)
}
} else {
store.dispatch(HomeAction.ReadCard())
}
return true
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.tap.features.intentHandler.handlers
import android.content.Intent
import android.net.Uri
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
import com.tangem.tap.store
/**
[REDACTED_AUTHOR]
*/
class BuyCurrencyIntentHandler : IntentHandler {
override suspend fun handleIntent(intent: Intent?): Boolean {
val data = intent?.data ?: return false
val currency = store.state.walletState.selectedCurrency ?: return false
val successUri = Uri.parse(ExchangeUrlBuilder.SUCCESS_URL)
return if (data.host == successUri.host && data.authority == successUri.authority) {
val currencyType = AnalyticsParam.CurrencyType.Currency(currency)
Analytics.send(Token.Bought(currencyType))
true
} else {
false
}
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.tap.features.intentHandler.handlers
import android.content.Intent
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
class SellCurrencyIntentHandler : IntentHandler {
override suspend fun handleIntent(intent: Intent?): Boolean {
return try {
val intentData = intent?.data ?: return false
val transactionID = intentData.getQueryParameter(TRANSACTION_ID_PARAM) ?: return false
val currency = intentData.getQueryParameter(CURRENCY_CODE_PARAM) ?: return false
val amount = intentData.getQueryParameter(CURRENCY_AMOUNT_PARAM) ?: return false
val destinationAddress = intentData.getQueryParameter(DEPOSIT_WALLET_ADDRESS_PARAM) ?: return false
Timber.d("MoonPay Sell: $amount $currency to $destinationAddress")
store.dispatch(
WalletAction.TradeCryptoAction.SendCrypto(
currencyId = currency,
amount = amount,
destinationAddress = destinationAddress,
transactionId = transactionID,
),
)
true
} catch (exception: Exception) {
Timber.d("Not MoonPay URL")
false
}
}
private companion object {
private const val TRANSACTION_ID_PARAM = "transactionId"
private const val CURRENCY_CODE_PARAM = "baseCurrencyCode"
private const val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount"
private const val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress"
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.tap.features.intentHandler.handlers
import android.content.Intent
import com.tangem.tap.common.extensions.removePrefixOrNull
import com.tangem.tap.domain.walletconnect.WalletConnectManager
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.store
/**
[REDACTED_AUTHOR]
*/
class WalletConnectLinkIntentHandler : IntentHandler {
override suspend fun handleIntent(intent: Intent?): Boolean {
val intentData = intent?.data ?: return false
val scheme = intent.scheme ?: return false
val wcUri = when (scheme) {
WalletConnectManager.WC_SCHEME -> intentData.toString()
TANGEM_SCHEME -> intentData.toString().removePrefixOrNull(TANGEM_WC_PREFIX)
else -> null
}
return if (wcUri == null) {
false
} else {
store.dispatch(WalletConnectAction.HandleDeepLink(wcUri))
true
}
}
private companion object {
private const val TANGEM_SCHEME = "tangem"
private const val TANGEM_WC_PREFIX = "tangem://wc?uri="
}
}

View file

@ -6,7 +6,6 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.core.TangemSdkError
import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.ToastNotificationAction
import com.tangem.tap.domain.TapError
@ -34,15 +33,14 @@ data class PrepareSendScreen(
val tokenRate: BigDecimal? = null,
) : SendScreenAction
// Address or PayId
sealed class AddressPayIdActionUi : SendScreenActionUi {
data class HandleUserInput(val data: String) : AddressPayIdActionUi()
data class PasteAddressPayId(val data: String, val sourceType: AddressEntered.SourceType) : AddressPayIdActionUi()
data class CheckClipboard(val data: String?) : AddressPayIdActionUi()
data class CheckAddressPayId(val sourceType: AddressEntered.SourceType?) : AddressPayIdActionUi()
data class SetTruncateHandler(val handler: (String) -> String) : AddressPayIdActionUi()
data class TruncateOrRestore(val truncate: Boolean) : AddressPayIdActionUi()
data class ChangePayIdState(val sendingToPayIdEnabled: Boolean) : AddressPayIdActionUi()
// Address
sealed class AddressActionUi : SendScreenActionUi {
data class HandleUserInput(val data: String) : AddressActionUi()
data class PasteAddress(val data: String, val sourceType: AddressEntered.SourceType) : AddressActionUi()
data class CheckClipboard(val data: String?) : AddressActionUi()
data class CheckAddress(val sourceType: AddressEntered.SourceType?) : AddressActionUi()
data class SetTruncateHandler(val handler: (String) -> String) : AddressActionUi()
data class TruncateOrRestore(val truncate: Boolean) : AddressActionUi()
}
sealed class TransactionExtrasAction : SendScreenActionUi {
@ -76,27 +74,15 @@ sealed class TransactionExtrasAction : SendScreenActionUi {
}
}
sealed class AddressPayIdVerifyAction : SendScreenAction {
sealed class AddressVerifyAction : SendScreenAction {
enum class Error {
PAY_ID_UNSUPPORTED_BY_BLOCKCHAIN,
PAY_ID_NOT_REGISTERED,
PAY_ID_REQUEST_FAILED,
ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN,
ADDRESS_SAME_AS_WALLET,
}
data class ChangePasteBtnEnableState(val isEnabled: Boolean) : AddressPayIdVerifyAction()
data class ChangePasteBtnEnableState(val isEnabled: Boolean) : AddressVerifyAction()
sealed class PayIdVerification : AddressPayIdVerifyAction() {
data class SetPayIdError(val error: Error?) : PayIdVerification()
data class SetPayIdWalletAddress(
val payId: String,
val payIdWalletAddress: String,
val isUserInput: Boolean,
) : PayIdVerification()
}
sealed class AddressVerification : AddressPayIdVerifyAction() {
sealed class AddressVerification : AddressVerifyAction() {
data class SetAddressError(val error: Error?) : AddressVerification()
data class SetWalletAddress(val address: String, val isUserInput: Boolean) : AddressVerification()
}
@ -155,8 +141,6 @@ sealed class SendAction : SendScreenAction {
override val messageResource: Int = R.string.send_transaction_success
}
data class SendError(override val error: TapError) : SendAction(), ErrorAction
sealed class Dialog : SendAction(), StateDialog {
data class TezosWarningDialog(
val reduceCallback: () -> Unit,

View file

@ -2,49 +2,39 @@ package com.tangem.tap.features.send.redux.middlewares
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.isPayIdSupported
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.AddressVerification.SetAddressError
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.AddressVerification.SetWalletAddress
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.Error
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification.SetPayIdError
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification.SetPayIdWalletAddress
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification.SetAddressError
import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification.SetWalletAddress
import com.tangem.tap.features.send.redux.AddressVerifyAction.Error
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
/**
[REDACTED_AUTHOR]
*/
internal class AddressPayIdMiddleware {
internal class AddressMiddleware {
fun handle(action: AddressPayIdActionUi, appState: AppState?, dispatch: (Action) -> Unit) {
fun handle(action: AddressActionUi, appState: AppState?, dispatch: (Action) -> Unit) {
when (action) {
is AddressPayIdActionUi.HandleUserInput -> handleUserInput(action.data, appState, dispatch)
is AddressPayIdActionUi.PasteAddressPayId -> pasteAddressPayId(action.data, action.sourceType, dispatch)
is AddressPayIdActionUi.CheckClipboard -> verifyClipboard(action.data, appState, dispatch)
is AddressPayIdActionUi.CheckAddressPayId -> verifyAddressPayId(action.sourceType, appState, dispatch)
is AddressActionUi.HandleUserInput -> handleUserInput(action.data, appState, dispatch)
is AddressActionUi.PasteAddress -> pasteAddress(action.data, action.sourceType, dispatch)
is AddressActionUi.CheckClipboard -> verifyClipboard(action.data, appState, dispatch)
is AddressActionUi.CheckAddress -> verifyAddress(action.sourceType, appState, dispatch)
else -> return
}
}
private fun handleUserInput(input: String, appState: AppState?, dispatch: DispatchFunction) {
val sendState = appState?.sendState ?: return
if (input == sendState.addressPayIdState.viewFieldValue.value) return
if (input == sendState.addressState.viewFieldValue.value) return
setAddressAndCheck(data = input, sourceType = null, isUserInput = true, dispatch = dispatch)
}
private fun pasteAddressPayId(data: String, sourceType: AddressEntered.SourceType, dispatch: (Action) -> Unit) {
private fun pasteAddress(data: String, sourceType: AddressEntered.SourceType, dispatch: (Action) -> Unit) {
setAddressAndCheck(data = data, sourceType = sourceType, isUserInput = false, dispatch = dispatch)
}
@ -54,74 +44,27 @@ internal class AddressPayIdMiddleware {
isUserInput: Boolean,
dispatch: (Action) -> Unit,
) {
val potentialPayId = data.lowercase()
if (isPayIdEnabled() && PayIdManager.isPayId(potentialPayId)) {
dispatch(SetPayIdWalletAddress(potentialPayId, "", isUserInput))
} else {
dispatch(SetWalletAddress(data, isUserInput))
}
dispatch(AddressPayIdActionUi.CheckAddressPayId(sourceType))
dispatch(SetWalletAddress(data, isUserInput))
dispatch(AddressActionUi.CheckAddress(sourceType))
}
private fun verifyAddressPayId(
private fun verifyAddress(
sourceType: AddressEntered.SourceType?,
appState: AppState?,
dispatch: (Action) -> Unit,
) {
val sendState = appState?.sendState ?: return
val wallet = sendState.walletManager?.wallet ?: return
val addressPayId = sendState.addressPayIdState.normalFieldValue ?: return
val isUserInput = sendState.addressPayIdState.viewFieldValue.isFromUserInput
val address = sendState.addressState.normalFieldValue ?: return
val isUserInput = sendState.addressState.viewFieldValue.isFromUserInput
if (isPayIdEnabled() && PayIdManager.isPayId(addressPayId)) {
verifyPayId(addressPayId, wallet, isUserInput, dispatch)
} else {
verifyAddress(
address = addressPayId,
wallet = wallet,
isUserInput = isUserInput,
dispatch = dispatch,
sourceType = sourceType,
)
}
}
private fun verifyPayId(payId: String, wallet: Wallet, isUserInput: Boolean, dispatch: DispatchFunction) {
val blockchain = wallet.blockchain
if (!blockchain.isPayIdSupported()) {
dispatch(SetPayIdError(Error.PAY_ID_UNSUPPORTED_BY_BLOCKCHAIN))
return
}
scope.launch {
val result = PayIdManager().verifyPayId(payId, blockchain)
withContext(Dispatchers.Main) {
when (result) {
is Result.Success -> {
val addressDetails = result.data.getAddressDetails()
if (addressDetails == null) {
dispatch(SetPayIdError(Error.PAY_ID_NOT_REGISTERED))
return@withContext
}
val address = addressDetails.address
val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(wallet, address)
if (failReason == null) {
dispatch(SetPayIdWalletAddress(payId, address, isUserInput))
dispatch(TransactionExtrasAction.Prepare(wallet.blockchain, address, addressDetails.tag))
dispatch(FeeAction.RequestFee)
} else {
dispatch(SetAddressError(failReason))
dispatch(TransactionExtrasAction.Release)
}
}
is Result.Failure -> {
dispatch(SetPayIdError(Error.PAY_ID_REQUEST_FAILED))
dispatch(TransactionExtrasAction.Release)
}
}
}
}
verifyAddress(
address = address,
wallet = wallet,
isUserInput = isUserInput,
dispatch = dispatch,
sourceType = sourceType,
)
}
private fun verifyAddress(
@ -219,41 +162,33 @@ internal class AddressPayIdMiddleware {
}
private fun verifyClipboard(input: String?, appState: AppState?, dispatch: DispatchFunction) {
val addressPayId = input ?: return
val address = input ?: return
val wallet = appState?.sendState?.walletManager?.wallet ?: return
val internalDispatcher: (Action) -> Unit = {
when (it) {
is SetWalletAddress, is SetPayIdWalletAddress -> {
dispatch(AddressPayIdVerifyAction.ChangePasteBtnEnableState(true))
is SetWalletAddress -> {
dispatch(AddressVerifyAction.ChangePasteBtnEnableState(true))
}
is SetAddressError, is SetPayIdError -> {
dispatch(AddressPayIdVerifyAction.ChangePasteBtnEnableState(false))
is SetAddressError -> {
dispatch(AddressVerifyAction.ChangePasteBtnEnableState(false))
}
}
}
if (PayIdManager.isPayId(addressPayId) && isPayIdEnabled()) {
verifyPayId(addressPayId, wallet, false, internalDispatcher)
} else {
verifyAddress(
address = addressPayId,
wallet = wallet,
sourceType = null,
isUserInput = false,
dispatch = internalDispatcher,
)
}
}
private fun isPayIdEnabled(): Boolean {
return store.state.globalState.configManager?.config?.isSendingToPayIdEnabled ?: false
verifyAddress(
address = address,
wallet = wallet,
sourceType = null,
isUserInput = false,
dispatch = internalDispatcher,
)
}
}
fun String.splitToMap(firstDelimiter: String, secondDelimiter: String): Map<String, String> {
return this.split(firstDelimiter)
return this
.split(firstDelimiter)
.map { it.split(secondDelimiter) }
.map { it.first() to it.last().toString() }
.toMap()
.associate { it.first() to it.last() }
}

View file

@ -38,7 +38,7 @@ class RequestFeeMiddleware {
}
val typedAmount = sendState.amountState.amountToExtract ?: return
val destinationAddress = sendState.addressPayIdState.destinationWalletAddress!!
val destinationAddress = sendState.addressState.destinationWalletAddress!!
val destinationAmount = Amount(typedAmount, sendState.amountState.amountToSendCrypto)
val txSender = if (scanResponse.isDemoCard()) {
DemoTransactionSender(walletManager)

View file

@ -55,18 +55,17 @@ class SendMiddleware {
{ nextDispatch ->
{ action ->
when (action) {
is AddressPayIdActionUi -> AddressPayIdMiddleware().handle(action, appState(), dispatch)
is AddressActionUi -> AddressMiddleware().handle(action, appState(), dispatch)
is AmountActionUi -> AmountMiddleware().handle(action, appState(), dispatch)
is RequestFee -> RequestFeeMiddleware().handle(appState(), dispatch)
is SendActionUi.SendAmountToRecipient ->
verifyAndSendTransaction(action, appState(), dispatch)
is PrepareSendScreen -> setIfSendingToPayIdEnabled(appState(), dispatch)
is SendAction.Warnings.Update -> updateWarnings(dispatch)
is SendActionUi.CheckIfTransactionDataWasProvided -> {
val transactionData = appState()?.sendState?.externalTransactionData
if (transactionData != null) {
store.dispatchOnMain(
AddressPayIdVerifyAction.AddressVerification.SetWalletAddress(
AddressVerifyAction.AddressVerification.SetWalletAddress(
address = transactionData.destinationAddress,
isUserInput = false,
),
@ -96,7 +95,7 @@ private fun verifyAndSendTransaction(
val sendState = appState?.sendState ?: return
val walletManager = sendState.walletManager ?: return
val card = appState.globalState.scanResponse?.card ?: return
val destinationAddress = sendState.addressPayIdState.destinationWalletAddress ?: return
val destinationAddress = sendState.addressState.destinationWalletAddress ?: return
val typedAmount = sendState.amountState.amountToExtract ?: return
val feeAmount = sendState.feeState.currentFee ?: return
@ -391,12 +390,6 @@ fun createValidateTransactionError(
return TapError.ValidateTransactionErrors(tapErrors) { it.joinToString("\r\n") }
}
private fun setIfSendingToPayIdEnabled(appState: AppState?, dispatch: (Action) -> Unit) {
val isSendingToPayIdEnabled =
appState?.globalState?.configManager?.config?.isSendingToPayIdEnabled ?: false
dispatch(AddressPayIdActionUi.ChangePayIdState(isSendingToPayIdEnabled))
}
private fun updateWarnings(dispatch: (Action) -> Unit) {
val warningsManager = store.state.globalState.warningManager ?: return
val blockchain = store.state.sendState.walletManager?.wallet?.blockchain ?: return

View file

@ -1,72 +0,0 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.AddressVerification
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.AddressPayIdState
import com.tangem.tap.features.send.redux.states.InputViewValue
import com.tangem.tap.features.send.redux.states.SendState
/**
[REDACTED_AUTHOR]
*/
class AddressPayIdReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState = when (action) {
is AddressPayIdActionUi -> handleUiAction(action, sendState, sendState.addressPayIdState)
is AddressPayIdVerifyAction -> handleAction(action, sendState, sendState.addressPayIdState)
else -> sendState
}
private fun handleUiAction(
action: AddressPayIdActionUi,
sendState: SendState,
state: AddressPayIdState,
): SendState {
val result = when (action) {
is AddressPayIdActionUi.HandleUserInput -> state
is AddressPayIdActionUi.SetTruncateHandler -> state.copy(truncateHandler = action.handler)
is AddressPayIdActionUi.TruncateOrRestore -> {
val value = if (action.truncate) state.truncatedFieldValue ?: "" else state.normalFieldValue ?: ""
state.copy(viewFieldValue = state.viewFieldValue.copy(value = value))
}
is AddressPayIdActionUi.PasteAddressPayId -> return sendState
is AddressPayIdActionUi.CheckClipboard -> return sendState
is AddressPayIdActionUi.CheckAddressPayId -> return sendState
is AddressPayIdActionUi.ChangePayIdState -> state.copy(sendingToPayIdEnabled = action.sendingToPayIdEnabled)
}
return updateLastState(sendState.copy(addressPayIdState = result), result)
}
private fun handleAction(
action: AddressPayIdVerifyAction,
sendState: SendState,
state: AddressPayIdState,
): SendState {
val result = when (action) {
is PayIdVerification.SetPayIdWalletAddress -> {
state.copy(
viewFieldValue = InputViewValue(action.payId, action.isUserInput),
normalFieldValue = action.payId,
truncatedFieldValue = state.truncate(action.payId),
destinationWalletAddress = action.payIdWalletAddress,
error = null,
)
}
is AddressVerification.SetWalletAddress -> {
state.copy(
viewFieldValue = InputViewValue(action.address, action.isUserInput),
normalFieldValue = action.address,
truncatedFieldValue = state.truncate(action.address),
destinationWalletAddress = action.address,
error = null,
)
}
is AddressPayIdVerifyAction.ChangePasteBtnEnableState -> state.copy(pasteIsEnabled = action.isEnabled)
is AddressVerification.SetAddressError -> state.copy(error = action.error, destinationWalletAddress = null)
is PayIdVerification.SetPayIdError -> state.copy(error = action.error, destinationWalletAddress = null)
}
return updateLastState(sendState.copy(addressPayIdState = result), result)
}
}

View file

@ -0,0 +1,52 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.tap.features.send.redux.AddressActionUi
import com.tangem.tap.features.send.redux.AddressVerifyAction
import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.AddressState
import com.tangem.tap.features.send.redux.states.InputViewValue
import com.tangem.tap.features.send.redux.states.SendState
/**
[REDACTED_AUTHOR]
*/
class AddressReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState = when (action) {
is AddressActionUi -> handleUiAction(action, sendState, sendState.addressState)
is AddressVerifyAction -> handleAction(action, sendState, sendState.addressState)
else -> sendState
}
private fun handleUiAction(action: AddressActionUi, sendState: SendState, state: AddressState): SendState {
val result = when (action) {
is AddressActionUi.HandleUserInput -> state
is AddressActionUi.SetTruncateHandler -> state.copy(truncateHandler = action.handler)
is AddressActionUi.TruncateOrRestore -> {
val value = if (action.truncate) state.truncatedFieldValue ?: "" else state.normalFieldValue ?: ""
state.copy(viewFieldValue = state.viewFieldValue.copy(value = value))
}
is AddressActionUi.PasteAddress -> return sendState
is AddressActionUi.CheckClipboard -> return sendState
is AddressActionUi.CheckAddress -> return sendState
}
return updateLastState(sendState.copy(addressState = result), result)
}
private fun handleAction(action: AddressVerifyAction, sendState: SendState, state: AddressState): SendState {
val result = when (action) {
is AddressVerification.SetWalletAddress -> {
state.copy(
viewFieldValue = InputViewValue(action.address, action.isUserInput),
normalFieldValue = action.address,
truncatedFieldValue = state.truncate(action.address),
destinationWalletAddress = action.address,
error = null,
)
}
is AddressVerifyAction.ChangePasteBtnEnableState -> state.copy(pasteIsEnabled = action.isEnabled)
is AddressVerification.SetAddressError -> state.copy(error = action.error, destinationWalletAddress = null)
}
return updateLastState(sendState.copy(addressState = result), result)
}
}

View file

@ -25,7 +25,7 @@ object SendScreenReducer {
val reducer: SendInternalReducer = when (action) {
is PrepareSendScreen -> PrepareSendScreenStatesReducer()
is AddressPayIdActionUi, is AddressPayIdVerifyAction -> AddressPayIdReducer()
is AddressActionUi, is AddressVerifyAction -> AddressReducer()
is TransactionExtrasAction -> TransactionExtrasReducer()
is AmountActionUi, is AmountAction -> AmountReducer()
is FeeActionUi, is FeeAction -> FeeReducer()
@ -71,7 +71,7 @@ private class SendReducer : SendInternalReducer {
amountState = state.amountState.copy(
inputIsEnabled = false,
),
addressPayIdState = state.addressPayIdState.copy(
addressState = state.addressState.copy(
inputIsEnabled = false,
),
)

View file

@ -2,17 +2,16 @@ package com.tangem.tap.features.send.redux.states
import androidx.core.text.isDigitsOnly
import com.tangem.blockchain.blockchains.stellar.StellarMemo
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
import com.tangem.tap.features.send.redux.AddressVerifyAction
import java.math.BigInteger
data class AddressPayIdState(
data class AddressState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val normalFieldValue: String? = null,
val truncatedFieldValue: String? = null,
val destinationWalletAddress: String? = null,
val error: AddressPayIdVerifyAction.Error? = null,
val error: AddressVerifyAction.Error? = null,
val truncateHandler: ((String) -> String)? = null,
val sendingToPayIdEnabled: Boolean = false,
val pasteIsEnabled: Boolean = false,
val inputIsEnabled: Boolean = true,
) : SendScreenState {
@ -22,8 +21,6 @@ data class AddressPayIdState(
fun truncate(value: String): String = truncateHandler?.invoke(value) ?: value
fun isReady(): Boolean = error == null && destinationWalletAddress?.isNotEmpty() ?: false
fun isPayIdState(): Boolean = destinationWalletAddress != null && destinationWalletAddress != normalFieldValue
}
data class TransactionExtrasState(
@ -98,11 +95,7 @@ data class BinanceMemoState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val memo: BigInteger? = null,
val error: TransactionExtraError? = null,
) {
companion object {
val MAX_NUMBER: BigInteger = BigInteger("FFFFFFFFFFFFFFFF", 16)
}
}
)
// tag must contains only digits
data class XrpDestinationTagState(

View file

@ -34,7 +34,7 @@ data class SendState(
val coinConverter: CurrencyConverter? = null,
val tokenConverter: CurrencyConverter? = null,
val lastChangedStates: LinkedHashSet<StateId> = linkedSetOf(),
val addressPayIdState: AddressPayIdState = AddressPayIdState(),
val addressState: AddressState = AddressState(),
val transactionExtrasState: TransactionExtrasState = TransactionExtrasState(),
val amountState: AmountState = AmountState(),
val feeState: FeeState = FeeState(),
@ -52,7 +52,7 @@ data class SendState(
MainCurrencyType.CRYPTO -> amountState.amountToExtract?.decimals ?: 0
}
fun convertFiatToCoin(value: BigDecimal): BigDecimal {
private fun convertFiatToCoin(value: BigDecimal): BigDecimal {
return if (!this.coinIsConvertible()) value else coinConverter!!.toCrypto(value)
}
@ -60,14 +60,14 @@ data class SendState(
return if (!this.tokenIsConvertible()) value else tokenConverter!!.toCrypto(value)
}
fun convertCoinToFiat(value: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
private fun convertCoinToFiat(value: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
if (!this.coinIsConvertible()) return value
val converter = coinConverter!!
return if (!scaleWithPrecision) converter.toFiat(value) else converter.toFiatWithPrecision(value)
}
fun convertTokenToFiat(value: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
private fun convertTokenToFiat(value: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
if (!this.tokenIsConvertible()) return value
val converter = tokenConverter!!
@ -107,13 +107,13 @@ data class SendState(
}
companion object {
fun addressPayIdIsReady(): Boolean = store.state.sendState.addressPayIdState.isReady()
private fun addressIsReady(): Boolean = store.state.sendState.addressState.isReady()
fun amountIsReady(): Boolean = store.state.sendState.amountState.isReady()
private fun amountIsReady(): Boolean = store.state.sendState.amountState.isReady()
fun isReadyToRequestFee(): Boolean = addressPayIdIsReady() && amountIsReady()
fun isReadyToRequestFee(): Boolean = addressIsReady() && amountIsReady()
fun isReadyToSend(): Boolean = addressPayIdIsReady() && amountIsReady() &&
fun isReadyToSend(): Boolean = addressIsReady() && amountIsReady() &&
store.state.sendState.feeState.isReady()
}
}

View file

@ -33,7 +33,7 @@ import com.tangem.tap.common.toggleWidget.ViewStateWidget
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.*
import com.tangem.tap.features.send.redux.AddressActionUi.*
import com.tangem.tap.features.send.redux.AmountActionUi.*
import com.tangem.tap.features.send.redux.FeeActionUi.*
import com.tangem.tap.features.send.redux.states.FeeType
@ -80,7 +80,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
etAmountToSend = view.findViewById(R.id.etAmountToSend)
initSendButtonStates()
setupAddressOrPayIdLayout()
setupAddressLayout()
setupTransactionExtrasLayout()
setupAmountLayout()
setupFeeLayout()
@ -99,14 +99,14 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
sendBtn = IndeterminateProgressButtonWidget(btnSend, progress)
}
private fun setupAddressOrPayIdLayout() = with(binding.lSendAddressPayid) {
store.dispatch(SetTruncateHandler { etAddressOrPayId.truncateMiddleWith(it, "...") })
private fun setupAddressLayout() = with(binding.lSendAddress) {
store.dispatch(SetTruncateHandler { etAddress.truncateMiddleWith(it, "...") })
store.dispatch(CheckClipboard(requireContext().getFromClipboard()?.toString()))
etAddressOrPayId.apply {
etAddress.apply {
setOnSystemPasteButtonClickListener {
store.dispatch(
PasteAddressPayId(
PasteAddress(
data = requireContext().getFromClipboard()?.toString() ?: "",
sourceType = Token.Send.AddressEntered.SourceType.PastePopup,
),
@ -119,9 +119,9 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter { store.state.sendState.addressPayIdState.viewFieldValue.value != it }
.filter { store.state.sendState.addressState.viewFieldValue.value != it }
.onEach {
store.dispatch(AddressPayIdActionUi.HandleUserInput(it))
store.dispatch(AddressActionUi.HandleUserInput(it))
}
.launchIn(mainScope)
}
@ -129,12 +129,12 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
imvPaste.setOnClickListener {
Analytics.send(Token.Send.ButtonPaste())
store.dispatch(
PasteAddressPayId(
PasteAddress(
data = requireContext().getFromClipboard()?.toString() ?: "",
sourceType = Token.Send.AddressEntered.SourceType.PasteButton,
),
)
store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused))
store.dispatch(TruncateOrRestore(!etAddress.isFocused))
}
imvQrCode.setOnClickListener {
Analytics.send(Token.Send.ButtonQRCode())
@ -145,7 +145,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
}
private fun setupTransactionExtrasLayout() = with(binding.lSendAddressPayid) {
private fun setupTransactionExtrasLayout() = with(binding.lSendAddress) {
// TODO: [REDACTED_TASK_KEY]
etXlmMemo.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
@ -202,15 +202,15 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
// Delayed launch is needed in order for the UI to be drawn and to process the sent events.
// If do not use the delay, then etAmount error field is not displayed when
// inserting an incorrect amount by shareUri
binding.lSendAddressPayid.imvQrCode.postDelayed(
binding.lSendAddress.imvQrCode.postDelayed(
{
store.dispatch(
PasteAddressPayId(
PasteAddress(
data = scannedCode,
sourceType = Token.Send.AddressEntered.SourceType.QRCode,
),
)
store.dispatch(TruncateOrRestore(!binding.lSendAddressPayid.etAddressOrPayId.isFocused))
store.dispatch(TruncateOrRestore(!binding.lSendAddress.etAddress.isFocused))
},
200,
)

View file

@ -7,30 +7,15 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.text.bold
import com.tangem.common.extensions.remove
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.enableError
import com.tangem.tap.common.extensions.getColor
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.extensions.update
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.getMessageString
import com.tangem.tap.common.text.DecimalDigitsInputFilter
import com.tangem.tap.domain.MultiMessageError
import com.tangem.tap.domain.assembleErrors
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.Error
import com.tangem.tap.features.send.redux.AddressVerifyAction.Error
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.states.AddressPayIdState
import com.tangem.tap.features.send.redux.states.AmountState
import com.tangem.tap.features.send.redux.states.FeeState
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.redux.states.ReceiptLayoutType
import com.tangem.tap.features.send.redux.states.ReceiptState
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.send.redux.states.StateId
import com.tangem.tap.features.send.redux.states.TransactionExtraError
import com.tangem.tap.features.send.redux.states.TransactionExtrasState
import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.features.send.ui.FeeUiHelper
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.send.ui.dialogs.KaspaWarningDialog
@ -61,7 +46,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
lastChangedStates.forEach {
when (it) {
StateId.SEND_SCREEN -> handleSendScreen(fg, state)
StateId.ADDRESS_PAY_ID -> handleAddressPayIdState(fg, state.addressPayIdState)
StateId.ADDRESS_PAY_ID -> handleAddressState(fg, state.addressState)
StateId.TRANSACTION_EXTRAS -> handleTransactionExtrasState(fg, state.transactionExtrasState)
StateId.AMOUNT -> handleAmountState(fg, state.amountState)
StateId.FEE -> handleFeeState(fg, state.feeState)
@ -72,7 +57,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
@Suppress("ComplexMethod")
private fun handleTransactionExtrasState(fg: SendFragment, infoState: TransactionExtrasState) =
with(fg.binding.lSendAddressPayid) {
with(fg.binding.lSendAddress) {
fun showView(view: View, info: Any?) {
view.show(info != null)
}
@ -192,13 +177,10 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
)
}
private fun handleAddressPayIdState(fg: SendFragment, state: AddressPayIdState) =
with(fg.binding.lSendAddressPayid) {
private fun handleAddressState(fg: SendFragment, state: AddressState) {
with(fg.binding.lSendAddress) {
fun parseError(context: Context, error: Error?): String? {
val resId = when (error) {
Error.PAY_ID_UNSUPPORTED_BY_BLOCKCHAIN -> R.string.send_error_payid_unsupported_by_blockchain
Error.PAY_ID_NOT_REGISTERED -> R.string.send_error_payid_not_registered
Error.PAY_ID_REQUEST_FAILED -> R.string.send_error_payid_request_failed
Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN -> R.string.send_validation_invalid_address
Error.ADDRESS_SAME_AS_WALLET -> R.string.send_error_address_same_as_wallet
else -> null
@ -208,8 +190,8 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
imvPaste.isEnabled = state.pasteIsEnabled
val et = etAddressOrPayId
val til = tilAddressOrPayId
val et = etAddress
val til = tilAddress
val parsedError = parseError(til.context, state.error)
til.isEnabled = state.inputIsEnabled
@ -218,19 +200,15 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
flPaste.show(state.inputIsEnabled)
flQrCode.show(state.inputIsEnabled)
val hintResId = if (state.sendingToPayIdEnabled) {
R.string.send_destination_hint_address_payid
} else {
R.string.send_destination_hint_address
}
til.hint = til.getString(hintResId)
til.hint = til.getString(R.string.send_destination_hint_address)
til.error = parsedError
til.isErrorEnabled = parsedError != null
til.helperText = state.destinationWalletAddress
til.isHelperTextEnabled = state.isPayIdState() && parsedError == null
til.isHelperTextEnabled = parsedError == null
if (!state.viewFieldValue.isFromUserInput) et.update(state.viewFieldValue.value)
}
}
private fun handleAmountState(fg: SendFragment, state: AmountState) = with(fg.binding.lSendAmount) {
if (state.error != null) {

View file

@ -8,7 +8,9 @@ import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
@ -22,12 +24,16 @@ import coil.size.Scale
import com.badoo.mvicore.modelWatcher
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.fragments.setStatusBarColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.OneTouchClickListener
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.feature.learn2earn.presentation.Learn2earnViewModel
import com.tangem.feature.learn2earn.presentation.ui.Learn2earnMainPageScreen
import com.tangem.feature.swap.api.SwapFeatureToggleManager
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.tap.MainActivity
import com.tangem.tap.common.analytics.events.Portfolio
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
import com.tangem.tap.common.redux.global.GlobalAction
@ -73,6 +79,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber<W
private var walletView: WalletView = MultiWalletView()
private val learn2earnViewModel by activityViewModels<Learn2earnViewModel>()
private val viewModel by viewModels<WalletViewModel>()
private val totalBalanceWatcher = modelWatcher {
@ -102,6 +109,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber<W
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
learn2earnViewModel.onMainScreenCreated()
}
override fun onStart() {
@ -199,6 +207,27 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber<W
binding.toolbar.setNavigationIcon(
if (state.canSaveUserWallets) R.drawable.ic_wallet_24 else R.drawable.ic_tap_card_24,
)
showLearn2earnView()
}
private fun showLearn2earnView() {
val isShowing = learn2earnViewModel.uiState.mainScreenState.isVisible
if (!isShowing) return
binding.composeLearnToEarnContainer.show(true) { binding.llWarnings.beginDelayedTransition() }
binding.composeLearnToEarnContainer.apply {
setViewCompositionStrategy(
strategy = ViewCompositionStrategy.DisposeOnLifecycleDestroyed(
lifecycle = this@WalletFragment.lifecycle,
),
)
setContent {
TangemTheme {
Learn2earnMainPageScreen(learn2earnViewModel.uiState)
}
}
}
}
private fun setupPullToRefreshLayout(state: WalletState) {

View file

@ -17,16 +17,8 @@ import com.tangem.tap.features.wallet.ui.analytics.WalletAnalyticsEventsMapper
import com.tangem.tap.store
import com.tangem.tap.walletStoresManager
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@ -71,6 +63,7 @@ internal class WalletViewModel @Inject constructor(
currency = currency,
batch = scanResponse.card.batchId,
signInType = signInType,
walletsCount = store.state.globalState.userWalletsListManager?.walletsCount.toString(),
),
)
}

View file

@ -98,7 +98,7 @@ internal fun WalletDataModel.getAvailableActions(
internal fun WalletDataModel.shouldShowMultipleAddress(): Boolean {
val listOfAddresses = walletAddresses?.list.orEmpty()
return listOfAddresses.size > 1 && currency.blockchain != Blockchain.BitcoinCash
return listOfAddresses.size > 1
}
internal fun WalletDataModel.assembleWarnings(

View file

@ -15,7 +15,7 @@ internal sealed interface WelcomeAction : Action {
data class Error(val error: TangemError) : WelcomeAction
}
data class HandleIntentIfNeeded(val intent: Intent?) : WelcomeAction
data class SetInitialIntent(val intent: Intent?) : WelcomeAction
object CloseError : WelcomeAction

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.welcome.redux
import android.content.Intent
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnResult
@ -19,6 +18,7 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.domain.userWalletList.unlockIfLockable
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.features.signin.redux.SignInAction
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
@ -39,24 +39,10 @@ internal class WelcomeMiddleware {
private fun handleAction(action: WelcomeAction, state: WelcomeState) {
when (action) {
is WelcomeAction.ProceedWithBiometrics -> {
proceedWithBiometrics(state)
}
is WelcomeAction.ProceedWithCard -> {
proceedWithCard(state)
}
is WelcomeAction.HandleIntentIfNeeded -> {
handleInitialIntent(action.intent)
}
is WelcomeAction.ClearUserWallets -> {
disableUserWalletsSaving()
}
is WelcomeAction.ProceedWithBiometrics.Error,
is WelcomeAction.ProceedWithCard.Error,
is WelcomeAction.ProceedWithBiometrics.Success,
is WelcomeAction.ProceedWithCard.Success,
is WelcomeAction.CloseError,
-> Unit
is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics(state)
is WelcomeAction.ProceedWithCard -> proceedWithCard(state)
is WelcomeAction.ClearUserWallets -> disableUserWalletsSaving()
else -> Unit
}
}
@ -85,7 +71,9 @@ internal class WelcomeMiddleware {
store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Success)
store.onUserWalletSelected(userWallet = selectedUserWallet)
intentHandler.handleWalletConnectLink(state.intent)
state.intent?.let {
WalletConnectLinkIntentHandler().handleIntent(it)
}
}
}
@ -104,20 +92,13 @@ internal class WelcomeMiddleware {
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success)
store.onUserWalletSelected(userWallet = userWallet)
intentHandler.handleWalletConnectLink(state.intent)
state.intent?.let {
WalletConnectLinkIntentHandler().handleIntent(it)
}
}
}
}
private fun handleInitialIntent(intent: Intent?) {
val isBackgroundScanNotHandled = !intentHandler.handleBackgroundScan(intent, hasSavedUserWallets = true)
val hasNotIncompletedBackup = !backupService.hasIncompletedBackup
if (isBackgroundScanNotHandled && hasNotIncompletedBackup) {
store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics)
}
}
private suspend inline fun scanCardInternal(crossinline onCardScanned: suspend (ScanResponse) -> Unit) {
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes,

View file

@ -14,7 +14,7 @@ internal object WelcomeReducer {
private fun internalReduce(action: WelcomeAction, state: WelcomeState): WelcomeState {
return when (action) {
is WelcomeAction.HandleIntentIfNeeded -> state.copy(intent = action.intent)
is WelcomeAction.SetInitialIntent -> state.copy(intent = action.intent)
is WelcomeAction.ProceedWithBiometrics -> state.copy(isUnlockWithBiometricsInProgress = true)
is WelcomeAction.ProceedWithCard -> state.copy(isUnlockWithCardInProgress = true)
is WelcomeAction.ProceedWithBiometrics.Error -> state.copy(

View file

@ -1,40 +0,0 @@
package com.tangem.tap.network.payid
import com.squareup.moshi.JsonClass
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Path
/**
[REDACTED_AUTHOR]
*/
interface PayIdVerifyApi {
@GET("{user}")
suspend fun verifyAddress(
@Path("user") user: String,
@Header("Accept") acceptNetworkHeader: String,
@Header("PayID-Version") payIdVersion: String = "1.0",
): VerifyPayIdResponse
}
@JsonClass(generateAdapter = true)
data class VerifyPayIdResponse(
val addresses: List<PayIdAddress> = mutableListOf(),
val payId: String? = null,
) {
fun getAddressDetails(): PayIdAddressDetails? = if (addresses.isNotEmpty()) addresses[0].addressDetails else null
}
@JsonClass(generateAdapter = true)
data class PayIdAddress(
var paymentNetwork: String,
var environment: String,
var addressDetailsType: String,
var addressDetails: PayIdAddressDetails,
)
@JsonClass(generateAdapter = true)
data class PayIdAddressDetails(
var address: String,
var tag: String? = null,
)

View file

@ -1,24 +0,0 @@
package com.tangem.tap.network.payid
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.datasource.api.common.createRetrofitInstance
/**
[REDACTED_AUTHOR]
*/
class PayIdVerifyService(
private val baseUrl: String,
) {
private val api = createRetrofitInstance(
baseUrl = baseUrl,
logEnabled = false,
).create(PayIdVerifyApi::class.java)
suspend fun verifyAddress(user: String, network: String): Result<VerifyPayIdResponse> {
return performRequest { api.verifyAddress(user, createNetworkHeader(network)) }
}
private fun createNetworkHeader(network: String): String = "application/$network-mainnet+json"
}

View file

@ -1,6 +1,8 @@
package com.tangem.tap.proxy.di
import androidx.compose.ui.text.intl.Locale
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.feature.learn2earn.domain.api.Learn2earnDependencyProvider
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
@ -48,4 +50,20 @@ class ProxyModule {
appStateHolder = appStateHolder,
)
}
// regions FeatureConsumers
@Provides
@Singleton
fun provideLear2earnDependencies(appStateHolder: AppStateHolder): Learn2earnDependencyProvider {
return object : Learn2earnDependencyProvider {
override fun getUserCountryCodeProvider(): () -> String = {
appStateHolder.mainStore?.state?.globalState?.userCountryCode ?: Locale.current.language
}
override fun getWebViewAuthCredentialsProvider(): () -> String? = {
appStateHolder.mainStore?.state?.globalState?.configManager?.config?.tangemComAuthorization
}
}
}
// endregion FeatureConsumers
}

View file

@ -23,7 +23,7 @@
android:focusable="true"
android:gravity="center_vertical"
android:padding="16dp"
android:text="@string/wallet_button_buy"
android:text="@string/common_buy"
android:textColor="@color/darkGray3"
android:textSize="14sp"
android:textStyle="bold"
@ -38,7 +38,7 @@
android:focusable="true"
android:gravity="center_vertical"
android:padding="16dp"
android:text="@string/wallet_button_sell"
android:text="@string/common_sell"
android:textColor="@color/darkGray3"
android:textSize="14sp"
android:textStyle="bold"

View file

@ -22,7 +22,7 @@
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
app:title="@string/send_title" />
app:title="@string/common_send" />
</com.google.android.material.appbar.AppBarLayout>
@ -39,8 +39,8 @@
android:orientation="vertical">
<include
android:id="@+id/l_send_address_payid"
layout="@layout/layout_send_address_payid"
android:id="@+id/l_send_address"
layout="@layout/layout_send_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp" />
@ -115,7 +115,7 @@
style="@style/TapPrimaryIconButton"
android:layout_width="match_parent"
android:fontFamily="@font/saira_semi_condensed_regular"
android:text="@string/send_title"
android:text="@string/common_send"
app:icon="@drawable/ic_arrow_right" />
<ProgressBar

View file

@ -46,8 +46,8 @@
android:id="@+id/cl_wallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingBottom="92dp">
<ImageView
@ -113,6 +113,12 @@
android:paddingTop="8dp"
app:layout_constraintTop_toBottomOf="@id/rv_warning_messages">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_learn_to_earn_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
<include
android:id="@+id/l_wallet_rescan_warning"
layout="@layout/layout_wallet_rescan_warning"
@ -219,8 +225,8 @@
android:id="@+id/btn_add_token"
style="@style/TapPrimaryButton"
android:layout_width="match_parent"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:text="@string/main_manage_tokens"
android:visibility="gone"

View file

@ -150,63 +150,6 @@
app:layout_constraintTop_toBottomOf="@id/tv_explore"
tools:text="Send only Ethereum (ETH) from Ethereum network to this address. Using other tokens and networks may result in loss of funds." />
<View
android:id="@+id/v_payid_divider"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="@color/lightGray5"
android:layout_marginTop="25dp"
app:layout_constraintTop_toBottomOf="@id/btn_copy" />
<ImageView
android:id="@+id/iv_payid_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/ic_payid"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/v_payid_divider"
android:importantForAccessibility="no" />
<TextView
android:id="@+id/tv_create_payid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="9dp"
android:padding="16dp"
android:text="@string/wallet_address_button_create_payid"
android:textColor="@color/darkGray6"
android:textSize="14sp"
android:textStyle="bold"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/v_payid_divider" />
<TextView
android:id="@+id/tv_payid_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="16dp"
android:textAlignment="textEnd"
android:textColor="@color/darkGray1"
android:textSize="13sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toEndOf="@id/iv_payid_icon"
app:layout_constraintTop_toBottomOf="@id/v_payid_divider"
tools:text="romafdffdfdfn$payid.tangem.com" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_payid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="v_payid_divider, iv_payid_icon, tv_payid_address, tv_create_payid" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -18,10 +18,9 @@
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilAddressOrPayId"
android:id="@+id/tilAddress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/send_destination_hint_address_payid"
app:boxBackgroundColor="@color/backgroundLightGray"
app:errorIconDrawable="@null"
app:layout_constraintEnd_toEndOf="parent"
@ -29,7 +28,7 @@
app:layout_constraintTop_toTopOf="parent">
<com.tangem.tap.features.send.ui.EditTextCustomPaste
android:id="@+id/etAddressOrPayId"
android:id="@+id/etAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/backgroundLightGray"
@ -71,7 +70,7 @@
android:layout_marginTop="10dp"
android:background="@drawable/shape_ellipse"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/tilAddressOrPayId">
app:layout_constraintTop_toTopOf="@+id/tilAddress">
<ImageView
android:id="@+id/imvQrCode"
@ -104,41 +103,6 @@
android:visibility="gone"
tools:visibility="visible">
<!-- <com.google.android.material.chip.ChipGroup-->
<!-- android:id="@+id/groupMemo"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginTop="8dp"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintStart_toStartOf="parent"-->
<!-- app:layout_constraintTop_toTopOf="parent"-->
<!-- app:selectionRequired="true"-->
<!-- app:singleLine="true"-->
<!-- app:singleSelection="true">-->
<!-- <com.google.android.material.chip.Chip-->
<!-- android:id="@+id/chipMemoText"-->
<!-- style="@style/TapChip"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="Text" />-->
<!-- <com.google.android.material.chip.Chip-->
<!-- android:id="@+id/chipMemoId"-->
<!-- style="@style/TapChip"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="ID" />-->
<!-- <com.google.android.material.chip.Chip-->
<!-- android:id="@+id/chipMemoHash"-->
<!-- style="@style/TapChip"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="Hash" />-->
<!-- </com.google.android.material.chip.ChipGroup>-->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilXlmMemo"
android:layout_width="match_parent"

View file

@ -5,7 +5,7 @@
android:id="@+id/amountContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/tilAddressOrPayId">
app:layout_constraintTop_toBottomOf="@+id/tilAddress">
<FrameLayout
android:id="@+id/flAmountToSend"

View file

@ -25,14 +25,14 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_buy"
style="@style/TapPrimaryIconButton"
android:text="@string/wallet_button_buy"
android:text="@string/common_buy"
android:visibility="gone"
app:icon="@drawable/ic_arrow_up" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_sell"
style="@style/TapPrimaryIconButton"
android:text="@string/wallet_button_sell"
android:text="@string/common_sell"
android:visibility="gone"
app:icon="@drawable/ic_arrow_down" />
@ -51,7 +51,7 @@
style="@style/TapPrimaryIconButton"
android:layout_width="0dp"
android:layout_weight="1"
android:text="@string/wallet_button_send"
android:text="@string/common_send"
app:icon="@drawable/ic_send" />
</merge>

View file

@ -1,4 +0,0 @@
package com.tangem;
public class Test2 {
}

View file

@ -1,305 +0,0 @@
package com.tangem.ui
import android.app.Activity
import android.content.SharedPreferences
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.os.Build
import android.os.Bundle
import android.preference.PreferenceManager
import android.text.Editable
import android.text.Html
import android.text.TextWatcher
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.core.os.bundleOf
import com.tangem.Constant
import com.tangem.data.Blockchain
import com.tangem.tangem_card.data.TangemCard
import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD
import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD_UID
import com.tangem.tangem_sdk.data.loadFromBundle
import com.tangem.ui.activity.MainActivity
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.fragment.pin.PinRequestFragment
import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.util.UtilHelper
import com.tangem.wallet.CoinEngine
import com.tangem.wallet.CoinEngineFactory
import com.tangem.wallet.R
import com.tangem.wallet.TangemContext
import kotlinx.android.synthetic.tangemAccess.fragment_confirm_transaction.*
import java.io.IOException
import java.util.*
class ConfirmTransactionFragment : BaseFragment(), NavigationResultListener, NfcAdapter.ReaderCallback {
override val layoutId = R.layout.fragment_confirm_transaction
private lateinit var sp: SharedPreferences
private lateinit var ctx: TangemContext
private lateinit var amount: CoinEngine.Amount
private var isIncludeFee: Boolean = true
private var requestPIN2Count = 0
private var nodeCheck = true
private var dtVerified: Date? = null
private var blockchainCallbacks: CoinEngine.BlockchainRequestsCallbacks? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sp = PreferenceManager.getDefaultSharedPreferences(context)
ctx = TangemContext.loadFromBundle(requireContext(), arguments)
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
navigateUp()
}
}
requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val engine = CoinEngineFactory.create(ctx)
@Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
Html.fromHtml(engine!!.balanceHTML, Html.FROM_HTML_MODE_LEGACY)
else
Html.fromHtml(engine!!.balanceHTML)
tvBalance.text = html
isIncludeFee = arguments?.getBoolean(Constant.EXTRA_FEE_INCLUDED, true) ?: true
if (isIncludeFee)
tvIncFee.setText(R.string.confirm_transaction_including_fee)
else
tvIncFee.setText(R.string.confirm_transaction_not_including_fee)
amount = CoinEngine.Amount(arguments?.getString(Constant.EXTRA_AMOUNT) ?: "0",
arguments?.getString(Constant.EXTRA_AMOUNT_CURRENCY) ?: "")
if (engine.allowSelectFeeInclusion())
tvIncFee.visibility = View.VISIBLE
else
tvIncFee.visibility = View.INVISIBLE
if (ctx.card.blockchainID == Blockchain.Token.id) {
// for Blockchain.Token limit decimals
etAmount.setText(amount.toValueString(ctx.card.tokensDecimal))
} else {
// for others
etAmount.setText(amount.toValueString())
}
tvCurrency.text = engine.balanceCurrency
tvCurrency2.text = engine.feeCurrency
tvCardID.text = ctx.card.cidDescription
etWallet.setText(arguments?.getString(Constant.EXTRA_TARGET_ADDRESS))
btnSend.visibility = View.INVISIBLE
if (!engine.allowSelectFeeLevel()) {
rgFee.visibility = View.INVISIBLE
}
etFee.isEnabled = sp.getBoolean(getString(R.string.pref_manual_editing_fee), false)
// set listeners
rgFee.setOnCheckedChangeListener { _, checkedId -> doSetFee(checkedId) }
etFee.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
try {
val eqFee = engine.evaluateFeeEquivalent(etFee!!.text.toString())
tvFeeEquivalent.text = eqFee
if (!ctx.coinData!!.amountEquivalentDescriptionAvailable) {
tvFeeEquivalent.error = getString(R.string.confirm_transaction_error_service_unavailable)
tvCurrency2.visibility = View.GONE
tvFeeEquivalent.visibility = View.GONE
} else
tvFeeEquivalent.error = null
if (sp.getBoolean(getString(R.string.pref_manual_editing_fee), false))
(activity as MainActivity).toastHelper
.showSingleToast(context, getString(R.string.confirm_transaction_warning_risk_delaying))
} catch (e: Exception) {
e.printStackTrace()
tvFeeEquivalent.text = ""
}
}
override fun afterTextChanged(s: Editable) {
}
})
btnSend.setOnClickListener {
if (UtilHelper.isOnline(requireContext())) {
val calendar = Calendar.getInstance()
calendar.add(Calendar.MINUTE, -1)
if (dtVerified == null || dtVerified!!.before(calendar.time)) {
finishWithError(Activity.RESULT_CANCELED, getString(R.string.confirm_transaction_error_data_is_outdated))
return@setOnClickListener
}
val engineCoin = CoinEngineFactory.create(ctx)
if (engineCoin!!.isNeedCheckNode && !nodeCheck) {
Toast.makeText(context, getString(R.string.confirm_transaction_error_cannot_reach_node), Toast.LENGTH_LONG).show()
return@setOnClickListener
}
val txFee = engineCoin.convertToAmount(etFee.text.toString(), tvCurrency2.text.toString())
val txAmount = engineCoin.convertToAmount(etAmount.text.toString(), tvCurrency.text.toString())
if (!engineCoin.hasBalanceInfo()) {
finishWithError(Activity.RESULT_CANCELED, getString(R.string.confirm_transaction_error_cannot_check_balance))
return@setOnClickListener
} else if (!engineCoin.isBalanceNotZero) {
finishWithError(Activity.RESULT_CANCELED, getString(R.string.general_wallet_empty))
return@setOnClickListener
} else if (!engineCoin.isExtractPossible) {
finishWithError(Activity.RESULT_CANCELED, getString(R.string.confirm_transaction_error_incoming_transaction_unconfirmed))
return@setOnClickListener
}
if (!engineCoin.checkNewTransactionAmountAndFee(txAmount, txFee, isIncludeFee)) {
finishWithError(Activity.RESULT_CANCELED, getString(R.string.prepare_transaction_error_not_enough_funds))
return@setOnClickListener
}
requestPIN2Count = 0
val data = Bundle()
data.putString(Constant.EXTRA_MODE, PinRequestFragment.Mode.RequestPIN2.toString())
ctx.saveToBundle(data)
data.putBoolean(Constant.EXTRA_FEE_INCLUDED, isIncludeFee)
navigateForResult(Constant.REQUEST_CODE_REQUEST_PIN2_, R.id.action_confirmTransactionFragment_to_pinRequestFragment, data)
} else
Toast.makeText(context, getString(R.string.general_error_no_connection), Toast.LENGTH_SHORT).show()
}
progressBar.visibility = View.VISIBLE
if (!navigatedBack) requestFee()
}
private fun requestFee() {
val coinEngine = CoinEngineFactory.create(ctx)
coinEngine!!.requestFee(
object : CoinEngine.BlockchainRequestsCallbacks {
override fun onComplete(success: Boolean) {
if (success) {
progressBar?.visibility = View.INVISIBLE
dtVerified = Date()
doSetFee(rgFee?.checkedRadioButtonId ?: R.id.rbNormalFee)
} else {
finishWithError(Activity.RESULT_CANCELED, ctx.error)
}
}
override fun onProgress() {
}
override fun allowAdvance(): Boolean {
return UtilHelper.isOnline(requireContext())
}
},
etWallet.text.toString(),
amount)
}
override fun onNavigationResult(requestCode: String, resultCode: Int, data: Bundle?) {
Log.d("LIFECYCLE", "NavigationResult assessed ${this::class.java.simpleName}")
if (requestCode == Constant.REQUEST_CODE_SIGN_TRANSACTION) {
if (data != null) {
if (data.containsKey(EXTRA_TANGEM_CARD_UID) && data.containsKey(EXTRA_TANGEM_CARD)) {
val updatedCard = TangemCard(data.getString(EXTRA_TANGEM_CARD_UID))
updatedCard.loadFromBundle(data.getBundle(EXTRA_TANGEM_CARD))
ctx.card = updatedCard
}
}
if (resultCode == Constant.RESULT_INVALID_PIN_ && requestPIN2Count < 2) {
requestPIN2Count++
val bundle = Bundle()
bundle.putString(Constant.EXTRA_MODE, PinRequestFragment.Mode.RequestPIN2.toString())
ctx.saveToBundle(bundle)
bundle.putBoolean(Constant.EXTRA_FEE_INCLUDED, isIncludeFee)
navigateForResult(Constant.REQUEST_CODE_REQUEST_PIN2_,
R.id.action_confirmTransactionFragment_to_pinRequestFragment,
bundle)
return
}
navigateBackWithResult(resultCode, data)
} else if (requestCode == Constant.REQUEST_CODE_REQUEST_PIN2_) {
if (resultCode == Activity.RESULT_OK) {
val bundle = Bundle()
ctx.saveToBundle(bundle)
bundle.putString(Constant.EXTRA_TARGET_ADDRESS, etWallet!!.text.toString())
bundle.putString(Constant.EXTRA_AMOUNT, etAmount.text.toString())
bundle.putString(Constant.EXTRA_AMOUNT_CURRENCY, tvCurrency.text.toString())
bundle.putString(Constant.EXTRA_FEE, etFee.text.toString())
bundle.putString(Constant.EXTRA_FEE_CURRENCY, tvCurrency2.text.toString())
bundle.putBoolean(Constant.EXTRA_FEE_INCLUDED, isIncludeFee)
navigateForResult(Constant.REQUEST_CODE_SIGN_TRANSACTION,
R.id.action_confirmTransactionFragment_to_signTransactionFragment,
bundle)
} else
Toast.makeText(context, R.string.confirm_transaction_error_pin_2_is_required, Toast.LENGTH_LONG).show()
}
}
override fun onTagDiscovered(tag: Tag) {
try {
(activity as MainActivity).nfcManager.ignoreTag(tag)
} catch (e: IOException) {
e.printStackTrace()
}
}
private fun doSetFee(checkedRadioButtonId: Int) {
var txtFee = ""
when (checkedRadioButtonId) {
R.id.rbMinimalFee ->
if (ctx.coinData.minFee != null) {
txtFee = ctx.coinData.minFee!!.toValueString()
btnSend?.visibility = View.VISIBLE
} else
btnSend?.visibility = View.INVISIBLE
R.id.rbNormalFee ->
if (ctx.coinData.normalFee != null) {
txtFee = ctx.coinData.normalFee!!.toValueString()
btnSend?.visibility = View.VISIBLE
} else
btnSend?.visibility = View.INVISIBLE
R.id.rbMaximumFee ->
if (ctx.coinData.maxFee != null) {
txtFee = ctx.coinData.maxFee!!.toValueString()
btnSend?.visibility = View.VISIBLE
} else
btnSend?.visibility = View.INVISIBLE
}
etFee?.setText(txtFee.replace(',', '.'))
}
private fun finishWithError(errorCode: Int, message: String) {
navigateBackWithResult(
errorCode,
bundleOf(Constant.EXTRA_MESSAGE to message),
R.id.loadedWalletFragment)
}
}

View file

@ -1,182 +0,0 @@
package com.tangem.ui
import android.app.Activity
import android.content.Context
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.os.Build
import android.os.Bundle
import android.text.Html
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import com.tangem.Constant
import com.tangem.data.isPayIdSupported
import com.tangem.ui.activity.MainActivity
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.fragment.qr.CameraPermissionManager
import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.util.UtilHelper
import com.tangem.util.extensions.isStart2CoinCard
import com.tangem.wallet.CoinEngineFactory
import com.tangem.wallet.R
import com.tangem.wallet.TangemContext
import kotlinx.android.synthetic.tangemAccess.fragment_prepare_transaction.*
import java.io.IOException
class PrepareTransactionFragment : BaseFragment(), NavigationResultListener, NfcAdapter.ReaderCallback {
companion object {
val TAG: String = PrepareTransactionFragment::class.java.simpleName
}
override val layoutId = R.layout.fragment_prepare_transaction
private val ctx: TangemContext by lazy { TangemContext.loadFromBundle(context, arguments) }
private val cameraPermissionManager: CameraPermissionManager by lazy { CameraPermissionManager(this) }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
tvCardID.text = ctx.card?.cidDescription
val engine = CoinEngineFactory.create(ctx)
@Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
Html.fromHtml(engine!!.balanceHTML, Html.FROM_HTML_MODE_LEGACY)
else
Html.fromHtml(engine!!.balanceHTML)
tvBalance.text = html
if (ctx.blockchain.isPayIdSupported() && !ctx.card.isStart2CoinCard()) {
etWallet.hint = getString(R.string.prepare_transaction_hint_address_or_pay_id)
}
if (!engine.allowSelectFeeInclusion()) {
rgIncFee.visibility = View.INVISIBLE
} else {
rgIncFee.visibility = View.VISIBLE
}
if (ctx.card!!.remainingSignatures < 2) {
etAmount.isEnabled = false
}
if (ctx.card.remainingSignatures == 1) {
androidx.appcompat.app.AlertDialog.Builder(requireContext())
.setTitle(R.string.prepare_transaction_warning_last_signature)
.setMessage(R.string.prepare_transaction_warning_send_full_amount)
.setPositiveButton(R.string.general_ok) { _, _ -> }
.create()
.show()
}
tvCurrency.text = engine.balance.currency
etAmount.setText(engine.balance.toValueString())
// limit number of symbols after comma
etAmount.filters = engine.amountInputFilters
// set listeners
etAmount.setOnEditorActionListener { lv, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
val imm = lv.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(lv.windowToken, 0)
lv.clearFocus()
true
} else {
false
}
}
btnVerify.setOnClickListener {
if (!UtilHelper.isOnline(requireContext())) {
Toast.makeText(context, R.string.general_error_no_connection, Toast.LENGTH_LONG).show()
return@setOnClickListener
}
val engine1 = CoinEngineFactory.create(ctx)
val strAmount: String = etAmount.text.toString().replace(",", ".")
val amount = engine1!!.convertToAmount(etAmount.text.toString(), tvCurrency.text.toString())
try {
if (!engine.checkNewTransactionAmount(amount))
etAmount.error = getString(R.string.prepare_transaction_error_not_enough_funds)
else
etAmount.error = null
} catch (e: Exception) {
etAmount.error = getString(R.string.prepare_transaction_error_unknown_amount_format)
}
// check wallet address
if (!engine1.validateAddress(etWallet.text.toString())) {
etWallet.error = getString(R.string.prepare_transaction_error_incorrect_destination)
return@setOnClickListener
} else
etWallet.error = null
if (etWallet.text.toString() == ctx.coinData!!.wallet) {
etWallet.error = getString(R.string.prepare_transaction_error_same_address)
return@setOnClickListener
}
if (!etAmount.error.isNullOrEmpty() || !etWallet.error.isNullOrEmpty()) {
return@setOnClickListener
}
val data = Bundle()
ctx.saveToBundle(data)
data.putString(Constant.EXTRA_TARGET_ADDRESS, etWallet!!.text.toString())
data.putBoolean(Constant.EXTRA_FEE_INCLUDED, (rgIncFee!!.checkedRadioButtonId == R.id.rbFeeIn))
data.putString(Constant.EXTRA_AMOUNT, strAmount)
data.putString(Constant.EXTRA_AMOUNT_CURRENCY, tvCurrency.text.toString())
navigateForResult(
Constant.REQUEST_CODE_SEND_TRANSACTION__,
R.id.action_prepareTransactionFragment_to_confirmTransactionFragment,
data)
}
ivCamera.setOnClickListener {
if (cameraPermissionManager.isPermissionGranted()) {
navigateForResult(Constant.REQUEST_CODE_SCAN_QR, R.id.action_prepareTransactionFragment_to_qrScanFragment)
} else {
cameraPermissionManager.requirePermission()
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
cameraPermissionManager.handleRequestPermissionResult(requestCode, grantResults) {
navigateForResult(Constant.REQUEST_CODE_SCAN_QR, R.id.action_prepareTransactionFragment_to_qrScanFragment)
}
}
override fun onNavigationResult(requestCode: String, resultCode: Int, data: Bundle?) {
if (requestCode == Constant.REQUEST_CODE_SCAN_QR && resultCode == Activity.RESULT_OK && data != null && data.containsKey("QRCode")) {
val code = data.getString("QRCode")
val schemeSplit = code!!.split(":")
when (schemeSplit.size) {
2 -> {
if (schemeSplit[0] == ctx.blockchain.uriScheme) {
etWallet?.setText(schemeSplit[1])
} else {
etWallet?.setText(code)
}
}
else -> {
etWallet?.setText(code)
}
}
} else if (requestCode == Constant.REQUEST_CODE_SEND_TRANSACTION__) {
navigateBackWithResult(resultCode, data)
}
}
override fun onTagDiscovered(tag: Tag) {
try {
(activity as MainActivity).nfcManager.ignoreTag(tag)
} catch (e: IOException) {
e.printStackTrace()
}
}
}

View file

@ -1,312 +0,0 @@
package com.tangem.ui
import android.app.Activity
import android.content.res.ColorStateList
import android.graphics.Color
import android.media.MediaPlayer
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.App
import com.tangem.Constant
import com.tangem.tangem_card.reader.CardProtocol
import com.tangem.tangem_card.tasks.SignTask
import com.tangem.tangem_card.util.Util
import com.tangem.tangem_sdk.android.nfc.NfcDeviceAntennaLocation
import com.tangem.tangem_sdk.android.reader.NfcReader
import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD
import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD_UID
import com.tangem.tangem_sdk.data.asBundle
import com.tangem.ui.activity.MainActivity
import com.tangem.ui.dialog.NoExtendedLengthSupportDialog
import com.tangem.ui.dialog.WaitSecurityDelayDialog
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.util.Analytics
import com.tangem.util.AnalyticsEvent
import com.tangem.util.LOG
import com.tangem.wallet.CoinEngine
import com.tangem.wallet.CoinEngineFactory
import com.tangem.wallet.R
import com.tangem.wallet.TangemContext
import kotlinx.android.synthetic.main.layout_progress_horizontal.*
import kotlinx.android.synthetic.main.layout_touch_card.*
import kotlinx.android.synthetic.tangemAccess.fragment_sign_transaction.*
class SignTransactionFragment : BaseFragment(), NavigationResultListener,
NfcAdapter.ReaderCallback, CardProtocol.Notifications {
companion object {
val TAG: String = SignTransactionFragment::class.java.simpleName
}
override val layoutId = R.layout.fragment_sign_transaction
private lateinit var ctx: TangemContext
private lateinit var mpFinishSignSound: MediaPlayer
private lateinit var nfcDeviceAntenna: NfcDeviceAntennaLocation
private var signTransactionTask: SignTask? = null
private lateinit var amount: CoinEngine.Amount
private lateinit var fee: CoinEngine.Amount
private var isIncludeFee = true
private var outAddressStr: String? = null
private var lastReadSuccess = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ctx = TangemContext.loadFromBundle(context, arguments)
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
navigateBackWithResult(Activity.RESULT_CANCELED)
}
}
requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mpFinishSignSound = MediaPlayer.create(context, R.raw.scan_card_sound)
// init NFC Antenna
nfcDeviceAntenna = NfcDeviceAntennaLocation(requireContext(), ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
nfcDeviceAntenna.init()
amount = CoinEngine.Amount(arguments?.getString(Constant.EXTRA_AMOUNT), arguments?.getString(Constant.EXTRA_AMOUNT_CURRENCY))
fee = CoinEngine.Amount(arguments?.getString(Constant.EXTRA_FEE), arguments?.getString(Constant.EXTRA_FEE_CURRENCY))
isIncludeFee = arguments?.getBoolean(Constant.EXTRA_FEE_INCLUDED, true) ?: true
outAddressStr = arguments?.getString(Constant.EXTRA_TARGET_ADDRESS)
tvCardID.text = ctx.card!!.cidDescription
progressBar.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar.visibility = View.INVISIBLE
FirebaseAnalytics.getInstance(requireActivity())
.logEvent(AnalyticsEvent.READY_TO_SIGN.event, Analytics.setCardData(ctx))
}
override fun onPause() {
signTransactionTask?.cancel(true)
super.onPause()
}
override fun onStop() {
signTransactionTask?.cancel(true)
super.onStop()
}
override fun onNavigationResult(requestCode: String, resultCode: Int, data: Bundle?) {
if (requestCode == Constant.REQUEST_CODE_SEND_TRANSACTION_) {
navigateBackWithResult(resultCode, data)
}
}
override fun onTagDiscovered(tag: Tag) {
try {
// get IsoDep handle and run cardReader thread
val isoDep = IsoDep.get(tag)
val uid = tag.id
val sUID = Util.byteArrayToHexString(uid)
if (sUID == ctx.card.uid) {
if (lastReadSuccess)
isoDep.timeout = ctx.card.pauseBeforePIN2 + 5000
else
isoDep.timeout = ctx.card.pauseBeforePIN2 + 65000
val coinEngine = CoinEngineFactory.create(ctx)
coinEngine?.setOnNeedSendTransaction { tx ->
if (tx != null) {
val data = Bundle()
ctx.saveToBundle(data)
data.putByteArray(Constant.EXTRA_TX, tx)
navigateForResult(
Constant.REQUEST_CODE_SEND_TRANSACTION_,
R.id.action_signTransactionFragment_to_sendTransactionFragment,
data)
}
}
val transactionToSign = coinEngine?.constructTransaction(amount, fee, isIncludeFee, outAddressStr)
signTransactionTask = SignTask(ctx.card, NfcReader((activity as MainActivity).nfcManager, isoDep),
App.localStorage, App.pinStorage, this, transactionToSign)
signTransactionTask?.start()
} else
(activity as MainActivity).nfcManager.ignoreTag(isoDep.tag)
} catch (e: CardProtocol.TangemException_WrongAmount) {
try {
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_wrong_amount))
data.putString(EXTRA_TANGEM_CARD_UID, ctx.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, ctx.card.asBundle)
navigateBackWithResult(Activity.RESULT_CANCELED, data)
} catch (e: Exception) {
e.printStackTrace()
}
} catch(e: IllegalArgumentException) {
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, e.message)
navigateBackWithResult(Activity.RESULT_CANCELED, data, R.id.loadedWalletFragment)
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onReadStart(cardProtocol: CardProtocol) {
rlProgressBar?.post { rlProgressBar.visibility = View.VISIBLE }
progressBar?.post {
progressBar?.visibility = View.VISIBLE
progressBar?.progress = 5
}
}
override fun onReadProgress(protocol: CardProtocol, progress: Int) {
progressBar?.post { progressBar?.progress = progress }
}
override fun onReadFinish(cardProtocol: CardProtocol?) {
signTransactionTask = null
if (cardProtocol != null) {
if (cardProtocol.error == null) {
FirebaseAnalytics.getInstance(requireActivity())
.logEvent(AnalyticsEvent.SIGNED.event, Analytics.setCardData(ctx))
rlProgressBar?.post { rlProgressBar?.visibility = View.GONE }
progressBar?.post {
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.GREEN)
}
mpFinishSignSound.start()
} else {
lastReadSuccess = false
FirebaseCrashlytics.getInstance().recordException(cardProtocol.error)
if (cardProtocol.error.javaClass == CardProtocol.TangemException_InvalidPIN::class.java) {
progressBar?.post {
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.RED)
}
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_cannot_sign))
data.putString(EXTRA_TANGEM_CARD_UID, cardProtocol.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, cardProtocol.card.asBundle)
navigateBackWithResult(Constant.RESULT_INVALID_PIN_, data)
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
} else {
if (cardProtocol.error is CardProtocol.TangemException_WrongAmount) {
try {
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_wrong_amount))
data.putString(EXTRA_TANGEM_CARD_UID, cardProtocol.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, cardProtocol.card.asBundle)
navigateBackWithResult(Activity.RESULT_CANCELED, data)
} catch (e: Exception) {
e.printStackTrace()
}
}
progressBar?.post {
if (cardProtocol.error is CardProtocol.TangemException_ExtendedLengthNotSupported) {
if (!NoExtendedLengthSupportDialog.allReadyShowed) {
NoExtendedLengthSupportDialog.message = getText(R.string.dialog_the_nfc_adapter_length_apdu).toString() + "\n" + getText(R.string.dialog_the_nfc_adapter_length_apdu_advice).toString()
NoExtendedLengthSupportDialog().show(requireFragmentManager(), NoExtendedLengthSupportDialog.TAG)
}
} else {
(activity as? MainActivity)?.toastHelper?.showSingleToast(
context, getString(R.string.general_notification_scan_again)
)
}
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.RED)
}
}
}
}
rlProgressBar?.postDelayed({
try {
rlProgressBar?.visibility = View.GONE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
}
override fun onReadCancel() {
signTransactionTask = null
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
}
// private val waitSecurityDelayDialogNew = WaitSecurityDelayDialogNew()
override fun onReadBeforeRequest(timeout: Int) {
LOG.i(TAG, "onReadBeforeRequest timeout $timeout")
activity?.let { WaitSecurityDelayDialog.onReadBeforeRequest(it, timeout) }
// if (!waitSecurityDelayDialogNew.isAdded)
// waitSecurityDelayDialogNew.show(supportFragmentManager, WaitSecurityDelayDialogNew.TAG)
// val readBeforeRequest = ReadBeforeRequest()
// readBeforeRequest.timeout = timeout
// EventBus.getDefault().post(readBeforeRequest)
}
override fun onReadAfterRequest() {
LOG.i(TAG, "onReadAfterRequest")
activity?.let { WaitSecurityDelayDialog.onReadAfterRequest(it) }
// val readAfterRequest = ReadAfterRequest()
// EventBus.getDefault().post(readAfterRequest)
}
override fun onReadWait(msec: Int) {
LOG.i(TAG, "onReadWait msec $msec")
activity?.let { WaitSecurityDelayDialog.onReadWait(it, msec) }
// val readWait = ReadWait()
// readWait.msec = msec
// EventBus.getDefault().post(readWait)
}
}

View file

@ -1,343 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/cl"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.tangem.ui.ConfirmTransactionFragment"
tools:ignore="Autofill">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:paddingTop="8dp"
android:paddingBottom="4dp"
android:text="@string/general_send_transaction"
android:textAlignment="center"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_large"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/llTransaction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/btn_light"
android:orientation="vertical"
android:paddingStart="5dp"
android:paddingTop="8dp"
android:paddingEnd="5dp"
android:paddingBottom="8dp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:text="@string/general_from_card"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium" />
<TextView
android:id="@+id/tvCardID"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:fontFamily="@font/maax"
android:textColor="@color/black"
android:textSize="@dimen/text_size_medium"
android:textStyle="bold"
tools:text="BB00 0000 1210 0233" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:fontFamily="@font/maax"
android:text="@string/general_balance"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium" />
<TextView
android:id="@+id/tvBalance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:fontFamily="@font/maax"
android:textColor="@color/black"
android:textSize="@dimen/text_size_medium"
android:textStyle="bold"
tools:text="4.51735 BTC" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/maax"
android:text="@string/general_send_to_wallet"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<EditText
android:id="@+id/etWallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:fontFamily="@font/maax"
android:hint="@string/confirm_transaction_hint_target_address"
android:inputType="textMultiLine"
android:maxLines="2"
android:padding="12dp"
android:singleLine="false"
android:textColor="@color/black"
android:textSize="@dimen/text_size_1_small"
android:textStyle="bold" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="@font/maax"
android:text="@string/general_amount"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvIncFee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginBottom="1dp"
android:fontFamily="@font/maax"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_small"
app:layout_constraintBottom_toBottomOf="@+id/textView3"
app:layout_constraintLeft_toRightOf="@+id/textView3"
tools:text="(including fee)" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/etAmount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:digits="0123456789.,"
android:fontFamily="@font/maax"
android:hint="@string/prepare_transaction_hint_enter_amount"
android:imeOptions="actionDone"
android:inputType="numberDecimal"
android:padding="12dp"
android:textSize="@dimen/text_size_1_large"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/tvCurrency"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_min="150dp"
android:enabled="false"
android:textColor="@color/black"
tools:text="1.34343434343443434343434"/>
<TextView
android:id="@+id/tvCurrency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:fontFamily="@font/maax"
android:text="@string/general_btc"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_1_large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/etAmount"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/llFee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@+id/llTransaction">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="4dp"
android:fontFamily="@font/maax"
android:text="@string/confirm_transaction_fee"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<info.hoang8f.android.segmented.SegmentedGroup
android:id="@+id/rgFee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:checkedButton="@+id/rbNormalFee"
android:gravity="center"
android:orientation="horizontal"
app:sc_border_width="2dp"
app:sc_corner_radius="10dp"
app:sc_tint_color="@color/colorPrimary">
<RadioButton
android:id="@+id/rbMinimalFee"
style="@style/RadioButton"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/confirm_transaction_btn_fee_minimal"
android:textSize="@dimen/text_size_medium" />
<RadioButton
android:id="@+id/rbNormalFee"
style="@style/RadioButton"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/confirm_transaction_btn_fee_normal"
android:textSize="@dimen/text_size_medium" />
<RadioButton
android:id="@+id/rbMaximumFee"
style="@style/RadioButton"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/confirm_transaction_btn_fee_priority"
android:textSize="@dimen/text_size_medium" />
</info.hoang8f.android.segmented.SegmentedGroup>
</LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/etFee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:fontFamily="@font/maax"
android:hint="@string/confirm_transaction_hint_fee_amount"
android:inputType="numberDecimal"
android:padding="12dp"
android:textColor="@color/black"
android:textColorLink="@android:color/holo_blue_dark"
android:textSize="@dimen/text_size_medium"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_min="100dp" />
<TextView
android:id="@+id/tvCurrency2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:fontFamily="@font/maax"
android:text="@string/general_btc"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/etFee"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvFeeEquivalent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:textColor="@android:color/darker_gray"
android:textSize="@dimen/text_size_1_small"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/tvCurrency2"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<Button
android:id="@+id/btnSend"
style="@style/AppTheme.RoundedCornerMaterialButton"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginBottom="16dp"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/confirm_transaction_btn_send"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<ProgressBar
android:id="@+id/progressBar"
style="@android:style/Widget.Holo.Light.ProgressBar"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_marginTop="4dp"
android:elevation="1dp"
android:indeterminate="true"
android:indeterminateTint="@color/colorPrimary"
android:visibility="invisible"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@+id/llFee" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,266 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.tangem.ui.PrepareTransactionFragment"
tools:ignore="contentDescription">
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:text="@string/general_send_transaction"
android:textAlignment="center"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_large"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/llFrom"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/btn_light"
android:orientation="vertical"
android:paddingStart="8dp"
android:paddingTop="24dp"
android:paddingEnd="8dp"
android:paddingBottom="24dp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:layout_marginTop="8dp"
android:fontFamily="@font/maax"
android:text="@string/general_from_card"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium" />
<TextView
android:id="@+id/tvCardID"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:fontFamily="@font/maax"
android:textColor="@color/black"
android:textSize="@dimen/text_size_medium"
android:textStyle="bold"
tools:text="BB00 0000 1210 0233" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:layout_marginTop="12dp"
android:fontFamily="@font/maax"
android:text="@string/general_balance"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tvBalance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="2dp"
android:fontFamily="@font/maax"
android:textColor="@color/black"
android:textSize="@dimen/text_size_medium"
android:textStyle="bold"
tools:text="4.51735 Btc" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/llTo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@color/card_state_loaded_with_zero"
android:orientation="vertical"
android:paddingTop="32dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintLeft_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/llFrom">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:fontFamily="@font/maax"
android:text="@string/general_send_to_wallet"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/ivCamera"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="12dp"
android:src="@drawable/qr_scan"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
<EditText
android:id="@+id/etWallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="@font/maax"
android:hint="@string/prepare_transaction_hint_enter_address"
android:imeOptions="actionNext"
android:inputType="text|textMultiLine|textNoSuggestions"
android:padding="12dp"
android:singleLine="false"
android:textColor="@color/colorPrimaryDark"
android:textColorLink="@android:color/holo_blue_dark"
android:textSize="@dimen/text_size_medium"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/ivCamera"
tools:layout_editor_absoluteY="2dp" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:layout_marginTop="24dp"
android:layout_marginBottom="8dp"
android:fontFamily="@font/maax"
android:text="@string/general_amount"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="8dp">
<EditText
android:id="@+id/etAmount"
tools:text="1.34343434343443434343434"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:digits="0123456789.,"
android:fontFamily="@font/maax"
android:hint="@string/prepare_transaction_hint_enter_amount"
android:imeOptions="actionDone"
android:inputType="numberDecimal"
android:padding="12dp"
android:textColor="@color/colorPrimaryDark"
android:textSize="@dimen/text_size_1_large"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/tvCurrency"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_min="150dp" />
<TextView
android:id="@+id/tvCurrency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:fontFamily="@font/maax"
android:text="@string/general_btc"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_1_large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/etAmount"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<info.hoang8f.android.segmented.SegmentedGroup
android:id="@+id/rgIncFee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:checkedButton="@+id/rbFeeIn"
android:gravity="center"
android:orientation="horizontal"
app:sc_border_width="2dp"
app:sc_corner_radius="10dp"
app:sc_tint_color="@color/colorPrimary"
tools:layout_editor_absoluteX="10dp">
<RadioButton
android:id="@+id/rbFeeIn"
style="@style/RadioButton"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/confirm_transaction_btn_including_fee"
android:textSize="@dimen/text_size_medium" />
<RadioButton
android:id="@+id/rbFeeOut"
style="@style/RadioButton"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/confirm_transaction_btn_not_including_fee"
android:textSize="@dimen/text_size_medium" />
</info.hoang8f.android.segmented.SegmentedGroup>
</LinearLayout>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnVerify"
style="@style/AppTheme.RoundedCornerMaterialButton"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginBottom="16dp"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/prepare_transaction_btn_verify"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
layout="@layout/layout_touch_card"
android:layout_width="400dp"
android:layout_height="250dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.75">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:text="@string/now_touch_the_card_with_id"
android:textAlignment="center"
android:textSize="@dimen/text_size_medium" />
<TextView
android:id="@+id/tvCardID"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:fontFamily="@font/maax"
android:textAlignment="center"
android:textColor="@color/black"
android:textSize="@dimen/text_size_large"
tools:text="CB02 0000 0002 5000" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:text="@string/to_sign_the_transaction"
android:textAlignment="center"
android:textSize="@dimen/text_size_medium" />
</LinearLayout>
<include layout="@layout/layout_progress_horizontal" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="tangem_app_name" translatable="false">Tangem</string>
</resources>

View file

@ -0,0 +1,29 @@
package com.tangem.datasource.api.promotion
import com.tangem.datasource.api.promotion.models.*
import retrofit2.http.*
/**
*
* Promotion API
* @see <a href = "https://www.notion.so/tangem/Promotion-Program-API-0907159c3fdb4975aac761be632f44da">Documentation<a/>
[REDACTED_AUTHOR]
*/
interface PromotionApi {
@Headers("Cache-Control: max-age=3600")
@GET("promotion")
suspend fun getPromotionInfo(@Query("programName") name: String): PromotionInfoResponse
@POST("promotion/code/validate")
suspend fun validateCode(@Body request: CodeValidateRequestBody): CodeValidateResponse
@POST("promotion/code/award")
suspend fun requestAwardByCode(@Body request: CodeAwardRequestBody): CodeAwardResponse
@POST("promotion/validate")
suspend fun validate(@Body request: ValidateRequestBody): ValidateResponse
@POST("promotion/award")
suspend fun requestAward(@Body request: AwardRequestBody): AwardResponse
}

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.api.promotion.models
import com.squareup.moshi.Json
/**
[REDACTED_AUTHOR]
*/
abstract class AbstractPromotionResponse {
abstract val error: Error?
fun isError(): Boolean = error != null
data class Error(
@Json(name = "code") val code: Int,
@Json(name = "message") val message: String,
)
}

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.api.promotion.models
import com.squareup.moshi.Json
/**
[REDACTED_AUTHOR]
*/
data class AwardRequestBody(
@Json(name = "walletId") val walletId: String,
@Json(name = "address") val address: String,
@Json(name = "programName") val programName: String,
)

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.promotion.models
import com.squareup.moshi.Json
/**
[REDACTED_AUTHOR]
*/
data class AwardResponse(
@Json(name = "status") val status: Boolean?,
@Json(name = "error") override val error: Error? = null,
) : AbstractPromotionResponse()

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.api.promotion.models
import com.squareup.moshi.Json
/**
[REDACTED_AUTHOR]
*/
data class CodeAwardRequestBody(
@Json(name = "walletId") val walletId: String,
@Json(name = "address") val address: String,
@Json(name = "code") val code: String?,
)

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.promotion.models
import com.squareup.moshi.Json
/**
[REDACTED_AUTHOR]
*/
data class CodeAwardResponse(
@Json(name = "status") val status: Boolean?,
@Json(name = "error") override val error: Error? = null,
) : AbstractPromotionResponse()

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.promotion.models
import com.squareup.moshi.Json
/**
[REDACTED_AUTHOR]
*/
data class CodeValidateRequestBody(
@Json(name = "walletId") val walletId: String,
@Json(name = "code") val code: String?,
)

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.promotion.models
import com.squareup.moshi.Json
/**
[REDACTED_AUTHOR]
*/
data class CodeValidateResponse(
@Json(name = "valid") val valid: Boolean?,
@Json(name = "error") override val error: Error? = null,
) : AbstractPromotionResponse()

View file

@ -0,0 +1,40 @@
package com.tangem.datasource.api.promotion.models
import com.squareup.moshi.Json
/**
[REDACTED_AUTHOR]
*/
data class PromotionInfoResponse(
@Json(name = "newCard") val newCard: Data?,
@Json(name = "oldCard") val oldCard: Data?,
@Json(name = "awardPaymentToken") val awardPaymentToken: TokenData?,
@Json(name = "error") override val error: Error? = null,
) : AbstractPromotionResponse() {
data class Data(
@Json(name = "status") val status: Status,
@Json(name = "award") val award: Double,
)
enum class Status(val value: String) {
@Json(name = "pending")
PENDING("pending"),
@Json(name = "active")
ACTIVE("active"),
@Json(name = "finished")
FINISHED("finished"),
}
data class TokenData(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
@Json(name = "symbol") val symbol: String,
@Json(name = "active") val active: Boolean,
@Json(name = "networkId") val networkId: String,
@Json(name = "contractAddress") val contractAddress: String,
@Json(name = "decimalCount") val decimalCount: Int,
)
}

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.promotion.models
import com.squareup.moshi.Json
/**
[REDACTED_AUTHOR]
*/
data class ValidateRequestBody(
@Json(name = "walletId") val walletId: String,
@Json(name = "programName") val programName: String,
)

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.promotion.models
import com.squareup.moshi.Json
/**
[REDACTED_AUTHOR]
*/
data class ValidateResponse(
@Json(name = "valid") val valid: Boolean?,
@Json(name = "error") override val error: Error? = null,
) : AbstractPromotionResponse()

View file

@ -17,7 +17,6 @@ interface ConfigManager {
fun resetToDefault(name: String)
companion object {
const val IS_SENDING_TO_PAY_ID_ENABLED = "isSendingToPayIdEnabled"
const val IS_CREATING_TWIN_CARDS_ALLOWED = "isCreatingTwinCardsAllowed"
const val IS_TOP_UP_ENABLED = "isTopUpEnabled"
}

View file

@ -1,13 +1,7 @@
package com.tangem.datasource.config
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.BlockchairCredentials
import com.tangem.blockchain.common.GetBlockCredentials
import com.tangem.blockchain.common.NowNodeCredentials
import com.tangem.blockchain.common.QuickNodeCredentials
import com.tangem.blockchain.common.TonCenterCredentials
import com.tangem.blockchain.common.*
import com.tangem.datasource.config.ConfigManager.Companion.IS_CREATING_TWIN_CARDS_ALLOWED
import com.tangem.datasource.config.ConfigManager.Companion.IS_SENDING_TO_PAY_ID_ENABLED
import com.tangem.datasource.config.ConfigManager.Companion.IS_TOP_UP_ENABLED
import com.tangem.datasource.config.models.Config
import com.tangem.datasource.config.models.ConfigModel
@ -34,7 +28,6 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
override fun turnOff(name: String) {
when (name) {
IS_SENDING_TO_PAY_ID_ENABLED -> config = config.copy(isSendingToPayIdEnabled = false)
IS_TOP_UP_ENABLED -> config = config.copy(isTopUpEnabled = false)
IS_CREATING_TWIN_CARDS_ALLOWED -> config = config.copy(isCreatingTwinCardsAllowed = false)
}
@ -42,13 +35,13 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
override fun resetToDefault(name: String) {
when (name) {
IS_SENDING_TO_PAY_ID_ENABLED ->
config =
config.copy(isSendingToPayIdEnabled = defaultConfig.isSendingToPayIdEnabled)
IS_TOP_UP_ENABLED -> config = config.copy(isTopUpEnabled = defaultConfig.isTopUpEnabled)
IS_CREATING_TWIN_CARDS_ALLOWED ->
config =
config.copy(isCreatingTwinCardsAllowed = defaultConfig.isCreatingTwinCardsAllowed)
IS_TOP_UP_ENABLED -> {
config = config.copy(isTopUpEnabled = defaultConfig.isTopUpEnabled)
}
IS_CREATING_TWIN_CARDS_ALLOWED -> {
config = config.copy(isCreatingTwinCardsAllowed = defaultConfig.isCreatingTwinCardsAllowed)
}
else -> Unit
}
}
@ -57,12 +50,11 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
config = config.copy(
isTopUpEnabled = model.isTopUpEnabled,
isSendingToPayIdEnabled = model.isSendingToPayIdEnabled,
isCreatingTwinCardsAllowed = model.isCreatingTwinCardsAllowed,
)
defaultConfig = defaultConfig.copy(
isTopUpEnabled = model.isTopUpEnabled,
isSendingToPayIdEnabled = model.isSendingToPayIdEnabled,
isCreatingTwinCardsAllowed = model.isCreatingTwinCardsAllowed,
)
}
@ -111,6 +103,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
zendesk = configValues.zendesk,
swapReferrerAccount = configValues.swapReferrerAccount,
walletConnectProjectId = configValues.walletConnectProjectId,
tangemComAuthorization = configValues.tangemComAuthorization,
)
}
}

View file

@ -11,7 +11,6 @@ data class Config(
val appsFlyerDevKey: String = "",
val amplitudeApiKey: String = "",
val blockchainSdkConfig: BlockchainSdkConfig = BlockchainSdkConfig(),
val isSendingToPayIdEnabled: Boolean = true,
val isTopUpEnabled: Boolean = false,
@Deprecated("Not relevant since version 3.23")
val isCreatingTwinCardsAllowed: Boolean = false,
@ -19,4 +18,5 @@ data class Config(
val zendesk: ZendeskConfig? = null,
val swapReferrerAccount: SwapReferrerAccount? = null,
val walletConnectProjectId: String = "",
val tangemComAuthorization: String? = null,
)

View file

@ -8,7 +8,6 @@ import com.squareup.moshi.Json
class FeatureModel(
val isTopUpEnabled: Boolean,
val isSendingToPayIdEnabled: Boolean,
val isCreatingTwinCardsAllowed: Boolean,
)
@ -38,6 +37,7 @@ class ConfigValueModel(
val swapReferrerAccount: SwapReferrerAccount?,
val kaspaSecondaryApiUrl: String,
val walletConnectProjectId: String,
val tangemComAuthorization: String?,
)
data class AppsFlyer(

View file

@ -2,10 +2,13 @@ package com.tangem.datasource.di
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.paymentology.PaymentologyApi
import com.tangem.datasource.api.promotion.PromotionApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.utils.RequestHeader.*
import com.tangem.datasource.utils.addHeaders
import com.tangem.datasource.utils.allowLogging
import com.tangem.lib.auth.AuthProvider
import com.tangem.lib.auth.BuildConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -13,6 +16,7 @@ import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Module
@ -54,10 +58,37 @@ class NetworkModule {
.create(PaymentologyApi::class.java)
}
@Provides
@Singleton
@PromotionOneInch
fun providePromotionOneInchApi(authProvider: AuthProvider, @NetworkMoshi moshi: Moshi): PromotionApi {
val okClient = OkHttpClient.Builder()
.addHeaders(
AuthenticationHeader(authProvider),
)
.allowLogging()
.callTimeout(API_ONE_INCH_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.connectTimeout(API_ONE_INCH_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.readTimeout(API_ONE_INCH_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.writeTimeout(API_ONE_INCH_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.build()
return createBasePromotionRetrofit(okClient, moshi)
}
private fun createBasePromotionRetrofit(okHttpClient: OkHttpClient, moshi: Moshi): PromotionApi {
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(if (BuildConfig.DEBUG) DEV_TANGEM_TECH_BASE_URL else PROD_TANGEM_TECH_BASE_URL)
.client(okHttpClient)
.build()
.create(PromotionApi::class.java)
}
private companion object {
const val PROD_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/"
const val DEV_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/"
private const val PAYMENTOLOGY_BASE_URL: String = "https://paymentologygate.oa.r.appspot.com/"
const val PAYMENTOLOGY_BASE_URL: String = "https://paymentologygate.oa.r.appspot.com/"
const val API_ONE_INCH_TIMEOUT_MS = 5000L
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.datasource.di
import javax.inject.Qualifier
@Qualifier
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class PromotionOneInch

View file

@ -13,7 +13,7 @@ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeade
val request = chain.request().newBuilder().apply {
requestHeaders
.flatMap(RequestHeader::values)
.forEach { addHeader(it.first, it.second) }
.forEach { addHeader(it.first, it.second.invoke()) }
}.build()
chain.proceed(request)

View file

@ -7,15 +7,15 @@ import com.tangem.lib.auth.AuthProvider
*
* @param pairs header name and header value pairs
*/
sealed class RequestHeader(vararg pairs: Pair<String, String>) {
sealed class RequestHeader(vararg pairs: Pair<String, () -> String>) {
/** Header list */
val values: List<Pair<String, String>> = pairs.toList()
val values: List<Pair<String, () -> String>> = pairs.toList()
object CacheControlHeader : RequestHeader("Cache-Control" to "max-age=600")
object CacheControlHeader : RequestHeader("Cache-Control" to { "max-age=600" })
class AuthenticationHeader(authProvider: AuthProvider) : RequestHeader(
"card_public_key" to authProvider.getCardPublicKey(),
"card_id" to authProvider.getCardId(),
"card_id" to { authProvider.getCardId() },
"card_public_key" to { authProvider.getCardPublicKey() },
)
}

View file

@ -12,6 +12,7 @@
<string name="common_done">Erledigt</string>
<string name="common_ok">OK</string>
<string name="common_save_changes">Änderungen speichern</string>
<string name="common_send">Absenden</string>
<string name="common_success">Erfolg</string>
<string name="details_manage_security_access_code">Zugangscode</string>
<string name="details_manage_security_access_code_description">Sie müssen den richtigen Zugangscode eingeben, bevor Sie die Karte scannen.</string>
@ -34,11 +35,7 @@
<string name="main_processing_full_amount">Der Betrag enthält nicht einige Ihrer Mittel</string>
<string name="send_amount_label">Betrag</string>
<string name="send_destination_hint_address">Adresse</string>
<string name="send_destination_hint_address_payid">Adresse oder PayString</string>
<string name="send_error_address_same_as_wallet">Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein</string>
<string name="send_error_payid_not_registered">PayString ist nicht registriert</string>
<string name="send_error_payid_request_failed">PayString-Anfrage ist fehlgeschlagen</string>
<string name="send_error_payid_unsupported_by_blockchain">PayString wird von der Blockchain nicht unterstützt</string>
<string name="send_extras_hint_destination_tag">Tag</string>
<string name="send_extras_hint_memo">Memo</string>
<string name="send_fee_include_description">inkl. Gebühr</string>
@ -48,7 +45,6 @@
<string name="send_fee_picker_priority">Priorität</string>
<string name="send_max_amount_label">Höchstbetrag</string>
<string name="send_network_fee_title">Netzgebühr</string>
<string name="send_title">Absenden</string>
<string name="send_total_label">Gesamt</string>
<string name="send_total_subtitle_asset_format">%1$s und %2$s werden gesendet</string>
<string name="send_total_subtitle_fiat_format">≈ %1$s (inkl. Gebühr: %2$s)</string>
@ -57,11 +53,9 @@
<string name="send_validation_invalid_address">Ungültige Adresse</string>
<string name="shop_one_wallet">Tangem Wallet</string>
<string name="twins_recreate_toolbar">Tangem Twin</string>
<string name="wallet_address_button_create_payid">PayString erstellen</string>
<string name="wallet_balance_loading">Die Bilanz wird aufgeladen…</string>
<string name="wallet_balance_tx_in_progress">Die Transaktion läuft…</string>
<string name="wallet_balance_verified">Verifizierte Bilanz</string>
<string name="wallet_button_send">Absenden</string>
<string name="wallet_connect_title">WalletConnect</string>
<string name="wallet_error_no_account">Das Konto ist nicht erstellt</string>
<string name="wallet_error_unsupported_blockchain">Diese Karte wird nicht unterstützt</string>

View file

@ -12,6 +12,7 @@
<string name="common_done">Exécuté</string>
<string name="common_ok">OK</string>
<string name="common_save_changes">Sauvegarder les modifications</string>
<string name="common_send">Envoyer</string>
<string name="common_success">Avec succès</string>
<string name="details_manage_security_access_code">Code d\'accès</string>
<string name="details_manage_security_access_code_description">Vous devrez entrer le mot de passe correct avant de scanner la carte</string>
@ -34,11 +35,7 @@
<string name="main_processing_full_amount">Le montant n\'inclut pas certains de vos fonds</string>
<string name="send_amount_label">Somme</string>
<string name="send_destination_hint_address">Adresse</string>
<string name="send_destination_hint_address_payid">Adresse ou PayString</string>
<string name="send_error_address_same_as_wallet">L\'adresse est la même que celle de votre portefeuille</string>
<string name="send_error_payid_not_registered">PayString non enregistré</string>
<string name="send_error_payid_request_failed">La demande de PayString a échoué</string>
<string name="send_error_payid_unsupported_by_blockchain">PayString non pris en charge par la blockchain</string>
<string name="send_extras_hint_destination_tag">Tag</string>
<string name="send_extras_hint_memo">Memo</string>
<string name="send_fee_include_description">Inclure les commissions</string>
@ -48,7 +45,6 @@
<string name="send_fee_picker_priority">Priorité</string>
<string name="send_max_amount_label">Somme maximale</string>
<string name="send_network_fee_title">Commissions du réseau</string>
<string name="send_title">Envoyer</string>
<string name="send_total_label">Total</string>
<string name="send_total_subtitle_asset_format">Sera envoyé %1$s et %2$s</string>
<string name="send_total_subtitle_fiat_format">≈ %1$s (incl. les commissions : %2$s)</string>
@ -57,11 +53,9 @@
<string name="send_validation_invalid_address">Adresse incorrecte</string>
<string name="shop_one_wallet">Tangem Wallet</string>
<string name="twins_recreate_toolbar">Tangem Twin</string>
<string name="wallet_address_button_create_payid">Créer PayString</string>
<string name="wallet_balance_loading">Solde est en cours de téléchargement…</string>
<string name="wallet_balance_tx_in_progress">Transaction en cours…</string>
<string name="wallet_balance_verified">Solde confirmé</string>
<string name="wallet_button_send">Envoyer</string>
<string name="wallet_connect_title">WalletConnect</string>
<string name="wallet_error_no_account">Compte n\'est pas créé</string>
<string name="wallet_error_unsupported_blockchain">Cette carte n\'est pas prise en charge</string>

View file

@ -12,6 +12,7 @@
<string name="common_done">Fatto</string>
<string name="common_ok">OK</string>
<string name="common_save_changes">Mantieni le modifiche</string>
<string name="common_send">Invia</string>
<string name="common_success">Con successo</string>
<string name="details_manage_security_access_code">Codice di accesso</string>
<string name="details_manage_security_access_code_description">Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto</string>
@ -34,11 +35,7 @@
<string name="main_processing_full_amount">L\'importo non include alcuni dei tuoi fondi</string>
<string name="send_amount_label">Importo</string>
<string name="send_destination_hint_address">Indirizzo</string>
<string name="send_destination_hint_address_payid">Indirizzo o PayString</string>
<string name="send_error_address_same_as_wallet">L\'indirizzo corrisponde all\'indirizzo del tuo portafoglio</string>
<string name="send_error_payid_not_registered">PayString non registrato</string>
<string name="send_error_payid_request_failed">Richiesta PayString fallita</string>
<string name="send_error_payid_unsupported_by_blockchain">PayString non supportato dalla blockchain</string>
<string name="send_extras_hint_destination_tag">Tag</string>
<string name="send_extras_hint_memo">Memo</string>
<string name="send_fee_include_description">Includi commissione</string>
@ -48,7 +45,6 @@
<string name="send_fee_picker_priority">Prioritario</string>
<string name="send_max_amount_label">Importo totale</string>
<string name="send_network_fee_title">Costi della rete</string>
<string name="send_title">Invia</string>
<string name="send_total_label">Totale</string>
<string name="send_total_subtitle_asset_format">Sarà inviato %1$s e %2$s</string>
<string name="send_total_subtitle_fiat_format">≈ %1$s (inc. commissione: %2$s)</string>
@ -57,11 +53,9 @@
<string name="send_validation_invalid_address">Indirizzo non valido</string>
<string name="shop_one_wallet">Tangem Wallet</string>
<string name="twins_recreate_toolbar">Tangem Twin</string>
<string name="wallet_address_button_create_payid">Crea PayString</string>
<string name="wallet_balance_loading">Il saldo sta per essere caricato…</string>
<string name="wallet_balance_tx_in_progress">Transazione in corso…</string>
<string name="wallet_balance_verified">Saldo verificato</string>
<string name="wallet_button_send">Invia</string>
<string name="wallet_connect_title">WalletConnect</string>
<string name="wallet_error_no_account">Conto non creato</string>
<string name="wallet_error_unsupported_blockchain">Questa carta non è supportata</string>

View file

@ -15,7 +15,7 @@
<string name="alert_signed_hashes_message">Эта карта не является платежным средством. В настоящее время мы не можем сопоставить количество подписей на карте с информацией в блокчейне. Это нормально, но в редких случаях может означать, что предыдущий владелец удерживает подписанную транзакцию от публикации, что является поводом для беспокойства.\nНе принимайте эту карту в качестве физического платежа от кого-то, кому вы не доверяете.\nВо всех остальных отношениях эта карта совершенно безопасна.\nTangem — единственный аппаратный кошелек, предлагающий защиту методом подсчета подписей.</string>
<string name="alert_troubleshooting_scan_card_title">У вас возникли трудности со сканированием карты?</string>
<string name="alert_unsupported_card">Эта карта не предназначена для работы с этим приложением</string>
<string name="app_settings_enable_biometrics_description">Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem App</string>
<string name="app_settings_enable_biometrics_description">Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem</string>
<string name="app_settings_enable_biometrics_title">Включите биометрическую аутентификацию</string>
<string name="app_settings_off_saved_access_code_alert_message">Все сохраненные коды доступа будут удалены. Вам потребуется вводить код доступа при работе с кошельком.</string>
<string name="app_settings_off_saved_wallet_alert_message">При отключении функции сохранения кошелька все ранее сохраненные кошельки будут удалены из приложения.</string>
@ -39,7 +39,7 @@
<string name="card_settings_access_code_recovery_title">Восстановление кода доступа</string>
<string name="card_settings_change_access_code">Смена кода доступа</string>
<string name="card_settings_change_access_code_footer">Код доступа будет изменен только на данной карте</string>
<string name="card_settings_reset_card_to_factory">Сброс к заводским настройкам</string>
<string name="card_settings_reset_card_to_factory">Заводские настройки</string>
<string name="card_settings_security_mode">Тип безопасности</string>
<string name="card_settings_title">Настройки карты</string>
<string name="chat_bot_name">Tangem Bot</string>
@ -52,27 +52,41 @@
<string name="common_balance">Баланс: %s</string>
<string name="common_biometric_authentication">биометрическую аутентификацию</string>
<string name="common_biometrics">биометрией</string>
<string name="common_buy">Купить</string>
<string name="common_camera_denied_alert_message">Вы не предоставили доступ к камере, пожалуйста, измените настройки конфиденциальности.</string>
<string name="common_cancel">Отмена</string>
<string name="common_close">Закрыть</string>
<string name="common_copy">Копировать</string>
<string name="common_copy_address">Скопировать адрес</string>
<string name="common_create">Создать</string>
<string name="common_delete">Удалить</string>
<string name="common_disabled">Отключено</string>
<string name="common_dislike">Не нравится</string>
<string name="common_done">Готово</string>
<string name="common_enable">Включить</string>
<string name="common_enabled">Включено</string>
<string name="common_exchange">Обменять</string>
<string name="common_explore_transaction_history">Посмотреть историю транзакций</string>
<string name="common_explorer">Обозреватель</string>
<string name="common_like">Нравится</string>
<string name="common_main_network">Основная сеть</string>
<string name="common_no">Нет</string>
<string name="common_ok">Ок</string>
<string name="common_no_data">Нет данных</string>
<string name="common_ok">OK</string>
<string name="common_origin_card">Основная карта</string>
<string name="common_receive">Получить</string>
<string name="common_reject">Отклонить</string>
<string name="common_reload">Перезагрузить</string>
<string name="common_save_changes">Сохранить изменения</string>
<string name="common_search">Искать</string>
<string name="common_seed_phrase">Секретная фраза</string>
<string name="common_sell">Продать</string>
<string name="common_send">Отправить</string>
<string name="common_server_unavailable">Сервер недоступен, повторите попытку позднее</string>
<string name="common_share">Поделиться</string>
<string name="common_sign">Подписать</string>
<string name="common_sign_and_send">Подписать и отправить</string>
<string name="common_stake">Стейкинг</string>
<string name="common_start">Начать</string>
<string name="common_submit">Отправить</string>
<string name="common_success">Успешно</string>
@ -81,6 +95,7 @@
<string name="common_transactions">Транзакции</string>
<string name="common_transfer">Перевод</string>
<string name="common_understand">Я понял</string>
<string name="common_unreachable">Недоступно</string>
<string name="common_yes">Да</string>
<string name="contract_address_copied_message">Адрес контракта скопирован!</string>
<string name="currency_subtitle_expanded">Доступные сети</string>
@ -145,11 +160,22 @@
<string name="initial_message_tap_header">Приложите карту</string>
<string name="internal_error_wallet_manager_not_found">Внутренняя ошибка: не удается найти менеджер кошельков</string>
<string name="key_invalidated_warning_description">Вы обновили данные биометрии, отсканируйте свою карту для входа</string>
<string name="main_get_bonus_subtitle">Вы завершили обучение и можете получить свои токены 1inch</string>
<string name="main_get_bonus_title">Получите бонус</string>
<plurals name="main_learn_subtitle">
<item quantity="one">Пройдите обучение и получите %d токен 1inch на свой кошелек</item>
<item quantity="few">Пройдите обучение и получите %d токена 1inch на свой кошелек</item>
<item quantity="many">Пройдите обучение и получите %d токена 1inch на свой кошелек</item>
<item quantity="other">Пройдите обучение и получите %d токенов 1inch на свой кошелек</item>
</plurals>
<string name="main_learn_title">Бонус за обучение</string>
<string name="main_manage_tokens">Управление токенами</string>
<string name="main_no_backup_warning_subtitle">Чтобы защитить свои активы, мы советуем вам выполнить эту процедуру</string>
<string name="main_no_backup_warning_title">Бэкап кошелька не был произведен</string>
<string name="main_page_balance">Баланс</string>
<string name="main_processing_full_amount">В сумме учтены не все монеты</string>
<string name="main_promotion_credited">Токены 1inch будут зачислены на адрес вашего кошелька в течение 2 дней</string>
<string name="main_promotion_no_purchase">По вашему промокоду не было покупки кошелька, а значит вы не можете получить бонус. Купите кошелек Tangem, отсканируйте его в приложении и получите бонус.</string>
<string name="main_scan_card_warning_view_subtitle">Чтобы получить доступ ко всем сетям, вам необходимо отсканировать карту</string>
<string name="main_scan_card_warning_view_title">Отсканируйте карту</string>
<string name="onboarding_access_code_feature_1_description">Вам необходимо установить единый код доступа для защиты всех ваших карт</string>
@ -173,11 +199,8 @@
<string name="onboarding_button_claim">Запросить</string>
<string name="onboarding_button_continue_wallet">Перейти к моему кошельку</string>
<string name="onboarding_button_finalize_backup">Завершение бэкапа</string>
<string name="onboarding_button_kyc_start">Верифицировать (Utorg)</string>
<string name="onboarding_button_kyc_waiting">Обновить</string>
<string name="onboarding_button_pin">Установить Код доступа</string>
<string name="onboarding_button_receive_crypto">Получить криптовалюту</string>
<string name="onboarding_button_register_wallet">Зарегистрироваться</string>
<string name="onboarding_button_scan_origin_card">Сканировать основную карту</string>
<string name="onboarding_button_skip_backup">Пропустить</string>
<string name="onboarding_button_what_does_it_mean">Как это работает?</string>
@ -192,19 +215,9 @@
<string name="onboarding_exit_alert_message">В этом случае вам будет необходимо начать процесс заново.</string>
<string name="onboarding_exit_alert_title">Вы хотите выйти из процесса активации?</string>
<string name="onboarding_getting_started">Подготовка</string>
<string name="onboarding_linking_error_card_with_wallets">Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Хотите сбросить его и использовать карту для бэкапа?</string>
<string name="onboarding_navbar_kyc_progress">Подтвердите свою личность</string>
<string name="onboarding_navbar_kyc_start">Верификация клиента</string>
<string name="onboarding_navbar_pin">Код доступа</string>
<string name="onboarding_navbar_register_wallet">Подключиться</string>
<string name="onboarding_navbar_title_creating_backup">Резервная копия</string>
<string name="onboarding_saltpay_button_backup_origin">Приложите SaltPay карту</string>
<string name="onboarding_saltpay_subtitle_no_backup_cards">Для начала процесса бэкапа вам необходимо добавить Tangem карту</string>
<string name="onboarding_saltpay_subtitle_one_backup_card">Завершите процесс бэкапа создав код доступа</string>
<string name="onboarding_saltpay_title_backup_card">Приложите Tangem карту</string>
<string name="onboarding_saltpay_title_no_backup_card">Бэкап карта не добавлена</string>
<string name="onboarding_saltpay_title_one_backup_card">Бэкап карта создана</string>
<string name="onboarding_saltpay_title_prepare_origin">Приготовьте SaltPay карту</string>
<string name="onboarding_seed_button_read_more">Читать про секретную фразу</string>
<string name="onboarding_seed_generate_message">Запишите эти 12 слов в порядке, указанном ниже, и сохраните их в надежном месте.</string>
<string name="onboarding_seed_generate_title">Ваша секретная фраза</string>
@ -220,13 +233,8 @@
<string name="onboarding_seed_user_validation_title">Итак, проверим</string>
<string name="onboarding_subtitle_claim">Для начала работы просто запросите начисление wxDAI на свой кошелек</string>
<string name="onboarding_subtitle_claim_progress">Это займет несколько секунд</string>
<string name="onboarding_subtitle_kyc_retry">Более подробная информация отправлена на ваш адрес электронной почты.</string>
<string name="onboarding_subtitle_kyc_start">Для начала работы с картой вам необходимо завершить процесс подтверждения личности</string>
<string name="onboarding_subtitle_kyc_waiting">Пожалуйста дождитесь завершения процесса подтверждения личности. Вы будете уведомлены через e-mail. Обычно это занимает не более часа. Вы можете закрыть приложение и вернуться позже.</string>
<string name="onboarding_subtitle_no_backup_cards">Чтобы начать процесс резервного копирования, добавьте одну или две резервные карты.</string>
<string name="onboarding_subtitle_one_backup_card">Вы можете добавить еще одну карту или завершить процесс резервного копирования</string>
<string name="onboarding_subtitle_pin">Установите 4-х значный код.\nОн будет использован для платежей.</string>
<string name="onboarding_subtitle_register_wallet">Подключите вашу карту к децентрализованной платежной системе</string>
<string name="onboarding_subtitle_scan_backup_card_format">Подготовьте резервную карту с номером %s</string>
<string name="onboarding_subtitle_scan_origin_card">Подготовьте основную карту</string>
<string name="onboarding_subtitle_scan_primary_card_format">Подготовьте основную карту с номером %s</string>
@ -237,13 +245,9 @@
<string name="onboarding_title_backup_card_format">Резервная карта #%d</string>
<string name="onboarding_title_claim">Запросить %s</string>
<string name="onboarding_title_claim_progress">Запрашивается</string>
<string name="onboarding_title_kyc_retry">Что-то пошло не так</string>
<string name="onboarding_title_kyc_start">Подтвердите свою личность</string>
<string name="onboarding_title_kyc_waiting">Подтверждение личности в процессе</string>
<string name="onboarding_title_no_backup_cards">Нет резервных карт</string>
<string name="onboarding_title_one_backup_card">Добавлена ​​одна резервная карта</string>
<string name="onboarding_title_pin">Код доступа</string>
<string name="onboarding_title_register_wallet">Подключите свою карту</string>
<string name="onboarding_title_scan_origin_card">Подготовьте свою карту</string>
<string name="onboarding_title_two_backup_cards">Добавлены две резервные карты</string>
<string name="onboarding_top_up_body">Пополните кошелек на любую сумму, чтобы начать пользоваться картой</string>
@ -264,6 +268,9 @@
<string name="organize_tokens_group">Группировка</string>
<string name="organize_tokens_sort_by_balance">По балансу</string>
<string name="organize_tokens_title">Сортировка токенов</string>
<string name="organize_tokens_ungroup">Разгруппировать</string>
<string name="receive_bottom_sheet_title">%1$s %2$s адрес в сети %3$s</string>
<string name="receive_bottom_sheet_warning_message">%1$s (%2$s) в сети %3$s</string>
<string name="referral_button_participate">Участвовать</string>
<string name="referral_error_failed_to_load_info">Не удалось загрузить информацию по реферальной программе. Пожалуйста, попробуйте позже.</string>
<string name="referral_error_failed_to_load_info_with_reason">Не удалось загрузить информацию по реферальной программе. Код ошибки: %s. Пожалуйста, попробуйте позже.</string>
@ -295,12 +302,6 @@
<string name="reset_card_without_backup_to_factory_message">Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты. Вы не сможете восстановить текущий кошелек.</string>
<string name="russian_bank_card_warning_subtitle">У вас есть карта банка другой страны или платежной системы UnionPay?</string>
<string name="russian_bank_card_warning_title">Карты банков РФ в данный момент не принимаются</string>
<string name="saltpay_error_empty_backup_message">Приложите карту с логотипом Visa</string>
<string name="saltpay_error_empty_backup_title">Внимание</string>
<string name="saltpay_error_no_gas_message">Пожалуйста обратитесь в службу поддержки</string>
<string name="saltpay_error_no_gas_title">Недостаточно средств для активации</string>
<string name="saltpay_error_pin_weak_message">Данный Код доступа может быть легко взломан</string>
<string name="saltpay_error_pin_weak_title">Ввод одинаковых цифр является не безопасным</string>
<string name="save_user_wallet_agreement_access_description">Войдите в приложение и следите за своим балансом без сканирования карты</string>
<string name="save_user_wallet_agreement_access_title">Доступ в приложение</string>
<string name="save_user_wallet_agreement_allow_biometrics">Использовать биометрию</string>
@ -317,11 +318,7 @@
<string name="search_tokens_title">Поиск валют</string>
<string name="send_amount_label">Сумма</string>
<string name="send_destination_hint_address">Адрес</string>
<string name="send_destination_hint_address_payid">Адрес или PayString</string>
<string name="send_error_address_same_as_wallet">Адрес совпадает с адресом кошелька</string>
<string name="send_error_payid_not_registered">PayString не зарегистрирован</string>
<string name="send_error_payid_request_failed">Не удалось выполнить запрос PayString</string>
<string name="send_error_payid_unsupported_by_blockchain">PayString не поддерживается блокчейном</string>
<string name="send_extras_error_invalid_destination_tag">Недопустимый Tag. Он не будет добавлен в транзакцию.</string>
<string name="send_extras_error_invalid_memo">Недопустимый Memo. Он не будет добавлен в транзакцию.</string>
<string name="send_extras_hint_destination_tag">Tag</string>
@ -333,7 +330,6 @@
<string name="send_fee_picker_priority">Приоритетная</string>
<string name="send_max_amount_label">Максимальная сумма</string>
<string name="send_network_fee_title">Сетевая комиссия</string>
<string name="send_title">Отправить</string>
<string name="send_title_currency_format">Отправка %s</string>
<string name="send_total_label">Всего</string>
<string name="send_total_subtitle_asset_format">%1$s и %2$s будет отправлено</string>
@ -350,6 +346,7 @@
<string name="solana_rent_warning">Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату.</string>
<string name="story_awe_description">Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте.</string>
<string name="story_awe_title">Революционный аппаратный кошелек</string>
<string name="story_backup_description">До **трех карт** с одним кошельком</string>
<string name="story_backup_description_1">До</string>
<string name="story_backup_description_2_bold">трех карт</string>
<string name="story_backup_description_3">с одним кошельком</string>
@ -358,6 +355,9 @@
<string name="story_currencies_title">Тысячи криптовалют</string>
<string name="story_finish_description">Используйте его на ходу, в любом месте, в любое время. Без проводов и батареек. Как только понадобится крипта, просто приложите карту к телефону.</string>
<string name="story_finish_title">Кошелек для каждого</string>
<string name="story_learn_description">Пройдите обучение и получите возможность купить кошелек Tangem со скидкой и токены 1inch в качестве бонуса</string>
<string name="story_learn_learn">Пройти обучение</string>
<string name="story_learn_title">Получите свой бонус</string>
<string name="story_meet_borrow">Занимайте</string>
<string name="story_meet_buy">Покупайте</string>
<string name="story_meet_exchange">Обменивайте</string>
@ -410,6 +410,7 @@
<string name="token_details_hide_token">Скрыть токен</string>
<string name="token_details_send_blocked_fee_format">%1$s — это токен в сети %2$s. Чтобы отправить транзакцию %3$s, необходимо пополнить баланс %4$s (%5$s) для оплаты комиссии сети.</string>
<string name="token_details_send_blocked_tx_format">Пожалуйста, дождитесь завершения транзакции %s, чтобы иметь возможность отправить средства</string>
<string name="token_details_token_type_subtitle">%1$s токен в сети %%image%% %2$s</string>
<string name="token_details_unable_hide_alert_message">Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети.</string>
<string name="token_details_unable_hide_alert_title">Невозможно скрыть %s</string>
<string name="token_item_no_rate">Нет цены</string>
@ -424,6 +425,7 @@
<string name="transaction_history_error_failed_to_load">Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию.</string>
<string name="transaction_history_not_supported_description">История транзакций в настоящее время не поддерживается для этого блокчейна. Но не волнуйтесь, мы работаем над этим! А пока вы можете проверить ее в обозревателе.</string>
<string name="transaction_history_transaction_from_address">от: %s</string>
<string name="transaction_history_transaction_to_address">на: %s</string>
<string name="transaction_history_tx_in_progress">В процессе…</string>
<string name="twin_error_same_card">Вы отсканировали ту же карту. Для создания twin-кошелька вам необходимо отсканировать карту с номером %d</string>
<string name="twins_onboarding_description_format">Это карта, которую вы держите в руках. У парной карты номер %s.\n\nОбе карты можно использовать для вывода средств из этого кошелька.</string>
@ -445,7 +447,6 @@
<string name="user_wallet_list_single_header">Одновалютные</string>
<string name="user_wallet_list_title">Мои кошельки</string>
<string name="user_wallet_list_unlock_all">Разблокировать все с %s</string>
<string name="wallet_address_button_create_payid">Создать PayString</string>
<string name="wallet_address_button_explore">История транзакций</string>
<string name="wallet_balance_blockchain_unreachable">Сеть недоступна</string>
<string name="wallet_balance_blockchain_unreachable_try_later">Блокчейн недоступен. Попробуй позже.</string>
@ -454,21 +455,26 @@
<string name="wallet_balance_tx_in_progress">Транзакция подтверждается…</string>
<string name="wallet_balance_verified">Подтвержденный баланс</string>
<string name="wallet_button_actions">Действия</string>
<string name="wallet_button_buy">Купить</string>
<string name="wallet_button_sell">Продать</string>
<string name="wallet_button_send">Отправить</string>
<string name="wallet_choose_trade_action">Вы хотите купить или продать криптовалюту?</string>
<string name="wallet_connect_alert_sign_message">Запрос на подпись сообщения.\n\n%s</string>
<string name="wallet_connect_bnb_sign_message">Dapp %1$s, запрос на\nподпись транзакции с BNB.\n\n%2$s</string>
<string name="wallet_connect_bnb_trade_order_message">Торговый ордер на %1$s\nЦена: %2$s\nСумма к получению: %3$s\nСумма к оплате: %4$s</string>
<string name="wallet_connect_bnb_transaction_message">Детали транзакции:\nОт: %1$s\nК: %2$s\nСумма: %3$s</string>
<string name="wallet_connect_bnb_transaction_signed">Транзакция с BNB успешно подписана и отправлена ​​в Dapp.</string>
<string name="wallet_connect_clipboard_alert">Буфер обмена содержит код WalletConnect. Использовать скопированное значение или отсканировать QR-код</string>
<string name="wallet_connect_create_tx_message">Запрос на создание транзакции для %1$s\n%2$s\n\nСумма: %3$s\nКомиссия: %4$s\nВсего: %5$s\nБаланс: %6$s</string>
<string name="wallet_connect_create_tx_not_enough_funds">Невозможно отправить транзакцию. Недостаточно средств.</string>
<string name="wallet_connect_error_failed_to_connect">Не удалось установить сессию WalletConnect. Пожалуйста, повторите попытку позже.</string>
<string name="wallet_connect_error_missing_blockchains">Не все токены добавлены в ваш список. Пожалуйста, добавьте их в начале, а потом попробуйте снова. Недостающие токены: \n</string>
<string name="wallet_connect_error_sing_failed">Не удалось подписать сообщение.\nПожалуйста, попробуйте еще раз</string>
<string name="wallet_connect_error_timeout">Не удалось установить сессию WalletConnect за отведённое время. Пожалуйста, повторите попытку позже.</string>
<string name="wallet_connect_error_unsupported_blockchains">Запрос на подключение через WalletConnect содержит неподдерживаемые блокчеины. Неподдерживаемые блокчеины:\n</string>
<string name="wallet_connect_error_unsupported_dapp">Cоединение с этим Dapp сервисом не может быть установлено из-за его технической реализации.</string>
<string name="wallet_connect_error_with_framework_message">Произошла непредвиденная ошибка. Сообщение ошибки: %s Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки.</string>
<string name="wallet_connect_error_wrong_card_selected">Неверная карта выбрана в приложении Tangem</string>
<string name="wallet_connect_failed_to_build_tx">Не удалось создать транзакцию из данных Dapp. Код: %s</string>
<string name="wallet_connect_generic_error_with_code">Произошла непредвиденная ошибка. Код ошибки: %d Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки.</string>
<string name="wallet_connect_message_signed">Сообщение было успешно подписано и отправлено в Dapp</string>
<string name="wallet_connect_network_not_found_format">Сеть %s не найдена. Пожалуйста, добавьте её и попробуйте заново.</string>
<string name="wallet_connect_paste_from_clipboard">Вставить из буфера обмена</string>
<string name="wallet_connect_request_session_start">Запрос на открытие сессии для\n%1$s\n\nСЕТЬ: %2$s\n\nURL: %3$s</string>
@ -477,8 +483,12 @@
<string name="wallet_connect_scanner_error_not_valid_card">Эту карту нельзя использовать с WalletConnect.</string>
<string name="wallet_connect_scanner_error_unsupported_network">Сеть не поддерживается. Пожалуйста, выберите другую сеть.</string>
<string name="wallet_connect_select_network">Выберите сеть</string>
<string name="wallet_connect_service_no_chain_id">Dapp не предоставил необходимые данные для открытия сессии WalletConnect</string>
<string name="wallet_connect_subtitle">Подключение к Dapps</string>
<string name="wallet_connect_title">WalletConnect</string>
<string name="wallet_connect_transaction_signed">Транзакция успешно подписана и отправлена ​​в Dapp</string>
<string name="wallet_connect_transaction_signed_and_send">Транзакция успешно подписана и отправлена ​​в блокчейн</string>
<string name="wallet_connect_tx_not_found">Не удалось найти хэш транзакции</string>
<string name="wallet_currency_subtitle">Сеть %s</string>
<string name="wallet_error_no_account">Аккаунт не создан</string>
<string name="wallet_error_unsupported_blockchain">Эта карта не поддерживается</string>

View file

@ -46,23 +46,29 @@
<string name="common_balance">餘額: %s</string>
<string name="common_biometric_authentication">生物識別</string>
<string name="common_biometrics">生物</string>
<string name="common_buy">購買</string>
<string name="common_camera_denied_alert_message">您尚未授予相機訪問權限,請更改您的隱私設置</string>
<string name="common_cancel">刪除</string>
<string name="common_close">關閉</string>
<string name="common_copy">複製</string>
<string name="common_copy_address">複製地址</string>
<string name="common_create">創造</string>
<string name="common_delete">刪除</string>
<string name="common_disabled">禁用</string>
<string name="common_dislike">不喜歡</string>
<string name="common_done">完成</string>
<string name="common_enable">允許</string>
<string name="common_enabled">啟用</string>
<string name="common_exchange">交易</string>
<string name="common_like">喜歡</string>
<string name="common_no"></string>
<string name="common_ok">OK</string>
<string name="common_origin_card">主卡片</string>
<string name="common_reject">拒絕</string>
<string name="common_save_changes">保存設置</string>
<string name="common_search">搜索</string>
<string name="common_sell">銷售</string>
<string name="common_send">發送</string>
<string name="common_server_unavailable">伺服器不可用,請稍後在試</string>
<string name="common_share">分享</string>
<string name="common_sign">簽署</string>
@ -74,6 +80,7 @@
<string name="common_terms_and_conditions">條款和條件</string>
<string name="common_transactions">交易</string>
<string name="common_understand">我了解</string>
<string name="common_unreachable">無法觸達</string>
<string name="common_yes"></string>
<string name="contract_address_copied_message">已複製代幣地址</string>
<string name="currency_subtitle_expanded">支持的網路</string>
@ -164,11 +171,8 @@
<string name="onboarding_button_claim">獲取</string>
<string name="onboarding_button_continue_wallet">繼續至我的錢包</string>
<string name="onboarding_button_finalize_backup">完成備份過程</string>
<string name="onboarding_button_kyc_start">通過Utorg驗證</string>
<string name="onboarding_button_kyc_waiting">刷新</string>
<string name="onboarding_button_pin">設置PIN碼</string>
<string name="onboarding_button_receive_crypto">接收貨幣</string>
<string name="onboarding_button_register_wallet">註冊</string>
<string name="onboarding_button_scan_origin_card">掃描主卡</string>
<string name="onboarding_button_skip_backup">暫時略過</string>
<string name="onboarding_button_what_does_it_mean">它是如何運作的?</string>
@ -183,18 +187,9 @@
<string name="onboarding_exit_alert_message">這此情況,您必須要重新開始</string>
<string name="onboarding_exit_alert_title">您想要離開啟用程序嗎?</string>
<string name="onboarding_getting_started">開始</string>
<string name="onboarding_navbar_kyc_progress">驗證身分</string>
<string name="onboarding_navbar_kyc_start">KYC</string>
<string name="onboarding_navbar_pin">PIN 碼</string>
<string name="onboarding_navbar_register_wallet">連接</string>
<string name="onboarding_navbar_title_creating_backup">創建備份</string>
<string name="onboarding_saltpay_button_backup_origin">點擊 SaltPay 卡</string>
<string name="onboarding_saltpay_subtitle_no_backup_cards">要開始備份過程,您必須添加 Tangem 卡片作為備份</string>
<string name="onboarding_saltpay_subtitle_one_backup_card">通過創建訪問密碼完成備份</string>
<string name="onboarding_saltpay_title_backup_card">點擊 Tangem 卡片</string>
<string name="onboarding_saltpay_title_no_backup_card">沒有備份卡片</string>
<string name="onboarding_saltpay_title_one_backup_card">備份卡片已準備完成</string>
<string name="onboarding_saltpay_title_prepare_origin">準備SaltPay卡</string>
<string name="onboarding_seed_button_read_more">閱讀更多關於助記詞的訊息</string>
<string name="onboarding_seed_generate_message">按照下面給出的順序寫下這 12 個單詞,並將它們存放在安全秘密的地方。</string>
<string name="onboarding_seed_generate_title">您的助記詞</string>
@ -210,13 +205,8 @@
<string name="onboarding_seed_user_validation_title">那麼,讓我們檢查一下</string>
<string name="onboarding_subtitle_claim">要開始,只需將 wxDAI 獲取到您的錢包</string>
<string name="onboarding_subtitle_claim_progress">這會花上幾秒</string>
<string name="onboarding_subtitle_kyc_retry">請查看Email已獲得更多指示</string>
<string name="onboarding_subtitle_kyc_start">要開始使用您的卡,您必須通過 KYC驗證</string>
<string name="onboarding_subtitle_kyc_waiting">請等待驗證完成,您將收到電子郵件通知。通常最多需要 1 小時。您可以關閉該應用程序,稍後再回來</string>
<string name="onboarding_subtitle_no_backup_cards">要開始備份過程,最多可添加兩張備份卡。</string>
<string name="onboarding_subtitle_one_backup_card">您可以再添加一張卡或完成備份過程</string>
<string name="onboarding_subtitle_pin">設置一個 4 位密碼。 \n它將用在付款時使用。</string>
<string name="onboarding_subtitle_register_wallet">將您的卡片連接到去中心化支付系統</string>
<string name="onboarding_subtitle_scan_backup_card_format">準備編號為 %s 的備份卡</string>
<string name="onboarding_subtitle_scan_origin_card">準備主卡</string>
<string name="onboarding_subtitle_scan_primary_card_format">準備編號為 %s 的主卡</string>
@ -227,13 +217,9 @@
<string name="onboarding_title_backup_card_format">備份卡 #%d</string>
<string name="onboarding_title_claim">獲取%s</string>
<string name="onboarding_title_claim_progress">獲取中</string>
<string name="onboarding_title_kyc_retry">有東西出錯了</string>
<string name="onboarding_title_kyc_start">確認你的身分</string>
<string name="onboarding_title_kyc_waiting">KYC 認證中</string>
<string name="onboarding_title_no_backup_cards">沒有備用卡</string>
<string name="onboarding_title_one_backup_card">添加了一張備用卡</string>
<string name="onboarding_title_pin">PIN 碼</string>
<string name="onboarding_title_register_wallet">連接你的錢包</string>
<string name="onboarding_title_scan_origin_card">準備好你的卡片</string>
<string name="onboarding_title_two_backup_cards">添加了兩張備用卡</string>
<string name="onboarding_top_up_body">要開始使用,只需為錢包充值任意金額</string>
@ -279,12 +265,6 @@
<string name="reset_card_without_backup_to_factory_message">恢復原廠設置將從所選卡中完全刪除錢包並將其從應用程序中刪除。您將無法恢復當前錢包</string>
<string name="russian_bank_card_warning_subtitle">您有其他國家的銀行卡或銀聯卡嗎?</string>
<string name="russian_bank_card_warning_title">目前不接受俄羅斯銀行卡</string>
<string name="saltpay_error_empty_backup_message">輕觸帶有 visa 標誌的卡片</string>
<string name="saltpay_error_empty_backup_title">注意</string>
<string name="saltpay_error_no_gas_message">請聯繫客服</string>
<string name="saltpay_error_no_gas_title">沒有激活資金</string>
<string name="saltpay_error_pin_weak_message">這樣的 PIN 很容易被暴力破解</string>
<string name="saltpay_error_pin_weak_title">四個相同的數字不安全</string>
<string name="save_user_wallet_agreement_access_description">登錄應用程序並在不掃描卡片的情況下檢查您的資產</string>
<string name="save_user_wallet_agreement_access_title">訪問應用程序</string>
<string name="save_user_wallet_agreement_allow_biometrics">允許使用生物辨識</string>
@ -301,11 +281,7 @@
<string name="search_tokens_title">搜尋代幣</string>
<string name="send_amount_label">數量</string>
<string name="send_destination_hint_address">地址</string>
<string name="send_destination_hint_address_payid">地址或 PayString</string>
<string name="send_error_address_same_as_wallet">地址與錢包地址相同</string>
<string name="send_error_payid_not_registered">PayString 未註冊</string>
<string name="send_error_payid_request_failed">PayString 請求失敗</string>
<string name="send_error_payid_unsupported_by_blockchain">PayString 不被區塊鏈支持</string>
<string name="send_extras_error_invalid_destination_tag">標籤無效。它不會被添加到交易中</string>
<string name="send_extras_error_invalid_memo">Memo無效。 它不會被添加到交易中</string>
<string name="send_extras_hint_destination_tag">Tag</string>
@ -317,7 +293,6 @@
<string name="send_fee_picker_priority">優先</string>
<string name="send_max_amount_label">最大值</string>
<string name="send_network_fee_title">網路費</string>
<string name="send_title">發送</string>
<string name="send_title_currency_format">發送 %s</string>
<string name="send_total_label">總計</string>
<string name="send_total_subtitle_asset_format">%1$s 和 %2$s 將被發送</string>
@ -333,6 +308,7 @@
<string name="solana_rent_warning">Solana 網絡每 2 天收取 %1$s 的費用。無法付此費用的帳戶將從網絡中清除。向您的帳戶存入超過 %2$s 即可免費使用</string>
<string name="story_awe_description">安全地存儲您的加密貨幣,同時將私鑰保存在您的卡中</string>
<string name="story_awe_title">創新式的硬體錢包</string>
<string name="story_backup_description">最多 **3張實體卡片** 到一個錢包</string>
<string name="story_backup_description_1">最多</string>
<string name="story_backup_description_2_bold">3張實體卡片</string>
<string name="story_backup_description_3">到一個錢包</string>
@ -415,7 +391,6 @@
<string name="user_wallet_list_single_header">單一幣種</string>
<string name="user_wallet_list_title">我的錢包</string>
<string name="user_wallet_list_unlock_all">用 %s 解鎖全部</string>
<string name="wallet_address_button_create_payid">創建支付字符串</string>
<string name="wallet_address_button_explore">交易記錄</string>
<string name="wallet_balance_blockchain_unreachable">網路無法使用</string>
<string name="wallet_balance_blockchain_unreachable_try_later">區塊鍊無法使用。稍後再試</string>
@ -424,21 +399,26 @@
<string name="wallet_balance_tx_in_progress">交易進行中</string>
<string name="wallet_balance_verified">檢視餘額</string>
<string name="wallet_button_actions">動作</string>
<string name="wallet_button_buy">購買</string>
<string name="wallet_button_sell">銷售</string>
<string name="wallet_button_send">發送</string>
<string name="wallet_choose_trade_action">您想要購買或賣出交易貨幣?</string>
<string name="wallet_connect_alert_sign_message">請求籤署消息。%s</string>
<string name="wallet_connect_bnb_sign_message">Dapp %1$s請求\n簽署 BNB 交易。\n%2$s</string>
<string name="wallet_connect_bnb_trade_order_message">%1$s 的交易訂單\n價格 %2$s\n接收金額%3$s\n支付數量 %4$s</string>
<string name="wallet_connect_bnb_transaction_message">交易明細:\n從 %1$s\n到 %2$s\n數量 %3$s</string>
<string name="wallet_connect_bnb_transaction_signed">BNB 交易已成功簽署並發送至 Dapp</string>
<string name="wallet_connect_clipboard_alert">剪貼板包含 WalletConnect 代碼。使用複制的值或掃描二維碼</string>
<string name="wallet_connect_create_tx_message">請求為 %1$s 創建交易\n%2$s\n\n數量 %3$s\n費用 %4$s\n全部的 %5$s\n餘額 %6$s</string>
<string name="wallet_connect_create_tx_not_enough_funds">無法交易,無足夠資金</string>
<string name="wallet_connect_error_failed_to_connect">未能建立 WalletConnect 連接。請稍後再試</string>
<string name="wallet_connect_error_missing_blockchains">並非所有代幣都已添加到您的列表中。請先添加它們,然後重試。缺少標記:\n</string>
<string name="wallet_connect_error_sing_failed">無法簽署消息。請重試</string>
<string name="wallet_connect_error_timeout">無法建立 WalletConnect 連接:超時錯誤。請稍後再試</string>
<string name="wallet_connect_error_unsupported_blockchains">會話請求包含不支持 WalletConnect 連接的區塊鏈。不支持的區塊鏈:</string>
<string name="wallet_connect_error_unsupported_blockchains">會話請求包含不支持 WalletConnect 連接的區塊鏈。不支持的區塊鏈:\n</string>
<string name="wallet_connect_error_unsupported_dapp">由於技術問題,無法與此 Dapp 建立連接</string>
<string name="wallet_connect_error_with_framework_message">我們遇到了未知錯誤。錯誤信息:%s。如果問題仍然存在-請隨時聯繫我們的客服人員</string>
<string name="wallet_connect_error_wrong_card_selected">在 Tangem App 中選擇了錯誤的卡</string>
<string name="wallet_connect_failed_to_build_tx">無法從 Dapp 數據創建交易。代碼: %s</string>
<string name="wallet_connect_generic_error_with_code">我們遇到了未知錯誤。錯誤代碼:%d。如果問題仍然存在-請隨時聯繫我們的支持人員</string>
<string name="wallet_connect_message_signed">消息已成功簽名並發送至Dapp</string>
<string name="wallet_connect_network_not_found_format">沒有 %s 網路,請先加入後再試一次</string>
<string name="wallet_connect_paste_from_clipboard">從剪貼板貼上</string>
<string name="wallet_connect_request_session_start">請求開始會話\n%1$s\n\n網絡 %2$s\n\n網址%3$s</string>
@ -447,8 +427,12 @@
<string name="wallet_connect_scanner_error_not_valid_card">此卡不能用於建立 WalletConnect 連接</string>
<string name="wallet_connect_scanner_error_unsupported_network">不支持此網絡。請選擇其他網絡</string>
<string name="wallet_connect_select_network">選擇網路</string>
<string name="wallet_connect_service_no_chain_id">Dapp 沒有提供必要的數據來建立 WalletConnect 連接</string>
<string name="wallet_connect_subtitle">連結到Dapps</string>
<string name="wallet_connect_title">WalletConnect</string>
<string name="wallet_connect_transaction_signed">交易已成功簽署並發送至 Dapp</string>
<string name="wallet_connect_transaction_signed_and_send">交易已成功簽署並發送至區塊鏈</string>
<string name="wallet_connect_tx_not_found">未能找到交易哈希</string>
<string name="wallet_currency_subtitle">%s 網路</string>
<string name="wallet_error_no_account">帳號尚未被創造</string>
<string name="wallet_error_unsupported_blockchain">不支持此卡</string>

View file

@ -50,27 +50,41 @@
<string name="common_balance">Balance: %s</string>
<string name="common_biometric_authentication">biometric authentication</string>
<string name="common_biometrics">biometrics</string>
<string name="common_buy">Buy</string>
<string name="common_camera_denied_alert_message">You have not given access to your camera, please adjust your privacy settings</string>
<string name="common_cancel">Cancel</string>
<string name="common_close">Close</string>
<string name="common_copy">Copy</string>
<string name="common_copy_address">Copy address</string>
<string name="common_create">Create</string>
<string name="common_delete">Delete</string>
<string name="common_disabled">Disabled</string>
<string name="common_dislike">Dislike</string>
<string name="common_done">Done</string>
<string name="common_enable">Enable</string>
<string name="common_enabled">Enabled</string>
<string name="common_exchange">Exchange</string>
<string name="common_explore_transaction_history">Explore transaction history</string>
<string name="common_explorer">Explorer</string>
<string name="common_like">Like</string>
<string name="common_main_network">Main network</string>
<string name="common_no">No</string>
<string name="common_no_data">No data</string>
<string name="common_ok">OK</string>
<string name="common_origin_card">Primary Card</string>
<string name="common_receive">Receive</string>
<string name="common_reject">Reject</string>
<string name="common_reload">Reload</string>
<string name="common_save_changes">Save changes</string>
<string name="common_search">Search</string>
<string name="common_seed_phrase">Seed phrase</string>
<string name="common_sell">Sell</string>
<string name="common_send">Send</string>
<string name="common_server_unavailable">The server is not available, please try again later</string>
<string name="common_share">Share</string>
<string name="common_sign">Sign</string>
<string name="common_sign_and_send">Sign and send</string>
<string name="common_stake">Stake</string>
<string name="common_start">Start</string>
<string name="common_submit">Submit</string>
<string name="common_success">Success</string>
@ -79,6 +93,7 @@
<string name="common_transactions">Transactions</string>
<string name="common_transfer">Transfer</string>
<string name="common_understand">I understand</string>
<string name="common_unreachable">Unreachable</string>
<string name="common_yes">Yes</string>
<string name="contract_address_copied_message">Contract address copied!</string>
<string name="currency_subtitle_expanded">Available networks</string>
@ -143,11 +158,20 @@
<string name="initial_message_tap_header">Tap the card</string>
<string name="internal_error_wallet_manager_not_found">Internal error: wallet manager not found</string>
<string name="key_invalidated_warning_description">You have updated biometrics, scan your card to enter</string>
<string name="main_get_bonus_subtitle">You have completed the training and can get your 1inch tokens</string>
<string name="main_get_bonus_title">Get a bonus</string>
<plurals name="main_learn_subtitle">
<item quantity="one">Complete the training and get %d 1inch token on your wallet</item>
<item quantity="other">Complete the training and get %d 1inch tokens on your wallet</item>
</plurals>
<string name="main_learn_title">Learn &amp; Earn</string>
<string name="main_manage_tokens">Manage tokens</string>
<string name="main_no_backup_warning_subtitle">To protect your assets, we advise you to carry out this procedure</string>
<string name="main_no_backup_warning_title">Your wallet has not been backed up</string>
<string name="main_page_balance">Total balance</string>
<string name="main_processing_full_amount">The amount does not include some of your funds</string>
<string name="main_promotion_credited">1inch tokens will be credited to your wallet address within 2 days</string>
<string name="main_promotion_no_purchase">There was no purchase of a wallet using your promo code, which means you cannot receive a bonus. Buy Tangem wallet, scan it in the app, and get the bonus.</string>
<string name="main_scan_card_warning_view_subtitle">To access all the networks you need to scan the card</string>
<string name="main_scan_card_warning_view_title">Scan your card</string>
<string name="onboarding_access_code_feature_1_description">You have to set up a single access code to protect all your wallets</string>
@ -171,11 +195,8 @@
<string name="onboarding_button_claim">Claim</string>
<string name="onboarding_button_continue_wallet">Continue to my wallet</string>
<string name="onboarding_button_finalize_backup">Finalize the backup</string>
<string name="onboarding_button_kyc_start">Verify via Utorg</string>
<string name="onboarding_button_kyc_waiting">Refresh</string>
<string name="onboarding_button_pin">Set PIN code</string>
<string name="onboarding_button_receive_crypto">Receive crypto</string>
<string name="onboarding_button_register_wallet">Register</string>
<string name="onboarding_button_scan_origin_card">Scan primary card</string>
<string name="onboarding_button_skip_backup">Skip for later</string>
<string name="onboarding_button_what_does_it_mean">How does it work?</string>
@ -191,18 +212,9 @@
<string name="onboarding_exit_alert_title">Do you want to exit the activation process?</string>
<string name="onboarding_getting_started">Getting started</string>
<string name="onboarding_linking_error_card_with_wallets">Another wallet has already been created on the card you\'re trying to add. Do you want to reset it and use the card for a new wallet?</string>
<string name="onboarding_navbar_kyc_progress">Verify your identity</string>
<string name="onboarding_navbar_kyc_start">KYC</string>
<string name="onboarding_navbar_pin">Pin code</string>
<string name="onboarding_navbar_register_wallet">Connect</string>
<string name="onboarding_navbar_title_creating_backup">Creating a backup</string>
<string name="onboarding_saltpay_button_backup_origin">Tap the SaltPay card</string>
<string name="onboarding_saltpay_subtitle_no_backup_cards">To start the backup process you have to add the Tangem card as your backup</string>
<string name="onboarding_saltpay_subtitle_one_backup_card">Finalize the backup process by creating an access code</string>
<string name="onboarding_saltpay_title_backup_card">Tap the Tangem card</string>
<string name="onboarding_saltpay_title_no_backup_card">No backup card</string>
<string name="onboarding_saltpay_title_one_backup_card">Backup card ready</string>
<string name="onboarding_saltpay_title_prepare_origin">Prepare the SaltPay card</string>
<string name="onboarding_seed_button_read_more">Read more about seed phrases</string>
<string name="onboarding_seed_generate_message">Write these 12 words down in the order given below and store them in a safe and secret place.</string>
<string name="onboarding_seed_generate_title">Your secret phrase</string>
@ -218,13 +230,8 @@
<string name="onboarding_seed_user_validation_title">So, lets check</string>
<string name="onboarding_subtitle_claim">To get started, simply claim wxDAI to your wallet</string>
<string name="onboarding_subtitle_claim_progress">It will take a few seconds</string>
<string name="onboarding_subtitle_kyc_retry">Please check your email for further instructions</string>
<string name="onboarding_subtitle_kyc_start">To start using your card you have to pass the KYC process</string>
<string name="onboarding_subtitle_kyc_waiting">Please wait until the verification is completed. You\'ll be notified via email. Usually it takes up to 1 hour. You can close the app and come back later.</string>
<string name="onboarding_subtitle_no_backup_cards">To start the backup process add up to two backup cards.</string>
<string name="onboarding_subtitle_one_backup_card">You can add one more card or finalize the backup process</string>
<string name="onboarding_subtitle_pin">Set up a 4-digit code.\nIt will be used for payments.</string>
<string name="onboarding_subtitle_register_wallet">Connect your card to the decentralized payment system</string>
<string name="onboarding_subtitle_scan_backup_card_format">Prepare the backup card with number %s</string>
<string name="onboarding_subtitle_scan_origin_card">Prepare the primary card</string>
<string name="onboarding_subtitle_scan_primary_card_format">Prepare the primary card with number %s</string>
@ -235,13 +242,9 @@
<string name="onboarding_title_backup_card_format">Backup card #%d</string>
<string name="onboarding_title_claim">Claim %s</string>
<string name="onboarding_title_claim_progress">Claiming</string>
<string name="onboarding_title_kyc_retry">Something went wrong</string>
<string name="onboarding_title_kyc_start">Verify your identity</string>
<string name="onboarding_title_kyc_waiting">KYC is in progress</string>
<string name="onboarding_title_no_backup_cards">No backup cards</string>
<string name="onboarding_title_one_backup_card">One backup card added</string>
<string name="onboarding_title_pin">PIN Code</string>
<string name="onboarding_title_register_wallet">Connect your card</string>
<string name="onboarding_title_scan_origin_card">Prepare your card</string>
<string name="onboarding_title_two_backup_cards">Two backup cards added</string>
<string name="onboarding_top_up_body">To get started, simply top up the wallet with any amount</string>
@ -262,6 +265,10 @@
<string name="organize_tokens_group">Group</string>
<string name="organize_tokens_sort_by_balance">By balance</string>
<string name="organize_tokens_title">Organize tokens</string>
<string name="organize_tokens_ungroup">Ungroup</string>
<string name="receive_bottom_sheet_title">%1$s %2$s address on %3$s network</string>
<string name="receive_bottom_sheet_warning_message">%1$s (%2$s) on %3$s network</string>
<string name="receive_bottom_sheet_warning_message_full">Send only %s to this address. Sending any other currency will result in its irreversible loss.</string>
<string name="referral_button_participate">Participate</string>
<string name="referral_error_failed_to_load_info">Failed to load the information about the referral program. Please try again later.</string>
<string name="referral_error_failed_to_load_info_with_reason">Failed to load the information about the referral program. Error code: %s. Please try again later.</string>
@ -291,12 +298,6 @@
<string name="reset_card_without_backup_to_factory_message">Factory Reset will completely delete the wallet from the selected card and remove it from the app. You will not be able to restore the current wallet.</string>
<string name="russian_bank_card_warning_subtitle">Do you have a bank card of another country or a UnionPay card?</string>
<string name="russian_bank_card_warning_title">Russian bank cards are not accepted at the moment</string>
<string name="saltpay_error_empty_backup_message">Tap the card with the visa logo</string>
<string name="saltpay_error_empty_backup_title">Attention</string>
<string name="saltpay_error_no_gas_message">Please contact support</string>
<string name="saltpay_error_no_gas_title">No funds for activation</string>
<string name="saltpay_error_pin_weak_message">Such a PIN can be brute-forced easily</string>
<string name="saltpay_error_pin_weak_title">Four identical digits isn\'t safe</string>
<string name="save_user_wallet_agreement_access_description">Log into the app and check your balance without scanning the card</string>
<string name="save_user_wallet_agreement_access_title">Access the app</string>
<string name="save_user_wallet_agreement_allow_biometrics">Allow to use biometrics</string>
@ -313,11 +314,7 @@
<string name="search_tokens_title">Search tokens</string>
<string name="send_amount_label">Amount</string>
<string name="send_destination_hint_address">Address</string>
<string name="send_destination_hint_address_payid">Address or PayString</string>
<string name="send_error_address_same_as_wallet">Address is the same as wallet address</string>
<string name="send_error_payid_not_registered">PayString not registered</string>
<string name="send_error_payid_request_failed">PayString request failed</string>
<string name="send_error_payid_unsupported_by_blockchain">PayString unsupported by blockchain</string>
<string name="send_extras_error_invalid_destination_tag">Invalid Tag. It won\'t be added to the transaction.</string>
<string name="send_extras_error_invalid_memo">Invalid Memo. It won\'t be added to the transaction.</string>
<string name="send_extras_hint_destination_tag">Tag</string>
@ -329,7 +326,6 @@
<string name="send_fee_picker_priority">Priority</string>
<string name="send_max_amount_label">Maximum amount</string>
<string name="send_network_fee_title">Network fee</string>
<string name="send_title">Send</string>
<string name="send_title_currency_format">Sending %s</string>
<string name="send_total_label">Total</string>
<string name="send_total_subtitle_asset_format">%1$s and %2$s will be sent</string>
@ -346,6 +342,7 @@
<string name="solana_rent_warning">Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free.</string>
<string name="story_awe_description">Store your crypto assets secure while keeping private keys contained in your card</string>
<string name="story_awe_title">Revolutionary Hardware Wallet</string>
<string name="story_backup_description">Up to **3 physical cards** to one wallet</string>
<string name="story_backup_description_1">Up to</string>
<string name="story_backup_description_2_bold">3 physical cards</string>
<string name="story_backup_description_3">to one wallet</string>
@ -354,6 +351,9 @@
<string name="story_currencies_title">Thousands of Currencies</string>
<string name="story_finish_description">Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto.</string>
<string name="story_finish_title">The Wallet for Everyone</string>
<string name="story_learn_description">Complete the training, get the opportunity to buy Tangem wallet with a discount and receive 1inch tokens on your wallet</string>
<string name="story_learn_learn">Learn</string>
<string name="story_learn_title">Learn and get a bonus</string>
<string name="story_meet_borrow">Borrow</string>
<string name="story_meet_buy">Buy</string>
<string name="story_meet_exchange">Exchange</string>
@ -404,6 +404,7 @@
<string name="token_details_hide_token">Hide token</string>
<string name="token_details_send_blocked_fee_format">%1$s is a token in the %2$s network. To make a %3$s transaction you need to deposit some %4$s (%5$s) to cover the network fee.</string>
<string name="token_details_send_blocked_tx_format">Please wait for %s transaction to complete to be able to send funds</string>
<string name="token_details_token_type_subtitle">%1$s token in %%image%% %2$s network</string>
<string name="token_details_unable_hide_alert_message">The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list.</string>
<string name="token_details_unable_hide_alert_title">Unable to hide %s</string>
<string name="token_item_no_rate">No rate</string>
@ -438,7 +439,6 @@
<string name="user_wallet_list_single_header">Single-currency</string>
<string name="user_wallet_list_title">My Wallets</string>
<string name="user_wallet_list_unlock_all">Unlock all with %s</string>
<string name="wallet_address_button_create_payid">Create PayString</string>
<string name="wallet_address_button_explore">Transaction history</string>
<string name="wallet_balance_blockchain_unreachable">Network is unreachable</string>
<string name="wallet_balance_blockchain_unreachable_try_later">Blockchain is unreachable. Try later</string>
@ -447,21 +447,26 @@
<string name="wallet_balance_tx_in_progress">Transaction is in progress…</string>
<string name="wallet_balance_verified">Verified Balance</string>
<string name="wallet_button_actions">Actions</string>
<string name="wallet_button_buy">Buy</string>
<string name="wallet_button_sell">Sell</string>
<string name="wallet_button_send">Send</string>
<string name="wallet_choose_trade_action">Do you want to buy or sell crypto?</string>
<string name="wallet_connect_alert_sign_message">Requesting to sign a message.\n\n%s</string>
<string name="wallet_connect_bnb_sign_message">Dapp %1$s, requesting to\nsign BNB transaction.\n\n%2$s</string>
<string name="wallet_connect_bnb_trade_order_message">Trade order for %1$s\nPrice: %2$s\nAmount to receive: %3$s\nAmount to pay: %4$s</string>
<string name="wallet_connect_bnb_transaction_message">Transaction details:\nFrom: %1$s\nTo: %2$s\nAmount: %3$s</string>
<string name="wallet_connect_bnb_transaction_signed">The BNB transaction has been successfully signed and sent to the Dapp</string>
<string name="wallet_connect_clipboard_alert">Clipboard contain WalletConnect code. Use copied value or scan QR-code</string>
<string name="wallet_connect_create_tx_message">Request to create transaction for %1$s\n%2$s\n\nAmount: %3$s\nFee: %4$s\nTotal: %5$s\nBalance: %6$s</string>
<string name="wallet_connect_create_tx_not_enough_funds">Can\'t send transaction. Not enough funds.</string>
<string name="wallet_connect_error_failed_to_connect">Failed to establish WalletConnect session. Please, try again later.</string>
<string name="wallet_connect_error_missing_blockchains">Not all tokens were added to your list. Please add them first and try again. Missing tokens:\n</string>
<string name="wallet_connect_error_sing_failed">Failed to sign message.\nPlease, try again</string>
<string name="wallet_connect_error_timeout">Failed to establish WalletConnect session: timeout error. Please, try again later.</string>
<string name="wallet_connect_error_unsupported_blockchains">Session request contains unsupported blockchains for WalletConnect connection. Unsupported blockchains:\n</string>
<string name="wallet_connect_error_unsupported_dapp">Connection with this Dapp cannot be established due to its technical implementation.</string>
<string name="wallet_connect_error_with_framework_message">We\'ve encountered unknown error. Error message: %s. If the problem persists — feel free to contact our support</string>
<string name="wallet_connect_error_wrong_card_selected">Wrong card selected in Tangem App</string>
<string name="wallet_connect_failed_to_build_tx">Failed to create transaction from Dapp data. Code: %s</string>
<string name="wallet_connect_generic_error_with_code">We\'ve encountered unknown error. Error code: %d. If the problem persists — feel free to contact our support</string>
<string name="wallet_connect_message_signed">The message has been successfully signed and sent to the Dapp</string>
<string name="wallet_connect_network_not_found_format">%s network not found. Please, add it first and try again.</string>
<string name="wallet_connect_paste_from_clipboard">Paste from clipboard</string>
<string name="wallet_connect_request_session_start">Request to start a session for\n%1$s\n\nNETWORK: %2$s\n\nURL: %3$s</string>
@ -470,8 +475,12 @@
<string name="wallet_connect_scanner_error_not_valid_card">This card can\'t be used to establish WalletConnect session</string>
<string name="wallet_connect_scanner_error_unsupported_network">This network is not supported. Please select another network.</string>
<string name="wallet_connect_select_network">Select network</string>
<string name="wallet_connect_service_no_chain_id">Dapp didn\'t provide essential data to establish WalletConnect session</string>
<string name="wallet_connect_subtitle">Connect to Dapps</string>
<string name="wallet_connect_title">WalletConnect</string>
<string name="wallet_connect_transaction_signed">The transaction has been successfully signed and sent to the Dapp</string>
<string name="wallet_connect_transaction_signed_and_send">The transaction has been succesfully signed and sent to the blockchain</string>
<string name="wallet_connect_tx_not_found">Failed to find transaction hash</string>
<string name="wallet_currency_subtitle">%s network</string>
<string name="wallet_error_no_account">Account is not created</string>
<string name="wallet_error_unsupported_blockchain">This card is not supported</string>

View file

@ -2,19 +2,17 @@ package com.tangem.core.ui.components
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.res.TangemTheme
// region TextButton
@ -26,7 +24,7 @@ fun TextButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier,
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.None,
icon = TangemButtonIconPosition.None,
onClick = onClick,
enabled = enabled,
showProgress = false,
@ -49,7 +47,7 @@ fun TextButtonIconStart(
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.Start(iconResId),
icon = TangemButtonIconPosition.Start(iconResId),
onClick = onClick,
enabled = enabled,
showProgress = false,
@ -63,7 +61,7 @@ fun WarningTextButton(text: String, onClick: () -> Unit, modifier: Modifier = Mo
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.None,
icon = TangemButtonIconPosition.None,
onClick = onClick,
enabled = enabled,
showProgress = false,
@ -85,7 +83,7 @@ fun PrimaryButton(
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.None,
icon = TangemButtonIconPosition.None,
onClick = onClick,
colors = TangemButtonsDefaults.primaryButtonColors,
enabled = enabled,
@ -108,7 +106,7 @@ fun PrimaryButtonIconEnd(
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.End(iconResId),
icon = TangemButtonIconPosition.End(iconResId),
onClick = onClick,
colors = TangemButtonsDefaults.primaryButtonColors,
enabled = enabled,
@ -131,7 +129,7 @@ fun PrimaryButtonIconStart(
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.Start(iconResId),
icon = TangemButtonIconPosition.Start(iconResId),
onClick = onClick,
colors = TangemButtonsDefaults.primaryButtonColors,
enabled = enabled,
@ -152,7 +150,7 @@ fun SecondaryButton(
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.None,
icon = TangemButtonIconPosition.None,
onClick = onClick,
colors = TangemButtonsDefaults.secondaryButtonColors,
enabled = enabled,
@ -175,7 +173,7 @@ fun SecondaryButtonIconEnd(
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.End(iconResId),
icon = TangemButtonIconPosition.End(iconResId),
onClick = onClick,
colors = TangemButtonsDefaults.secondaryButtonColors,
enabled = enabled,
@ -198,7 +196,7 @@ fun SecondaryButtonIconStart(
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.Start(iconResId),
icon = TangemButtonIconPosition.Start(iconResId),
onClick = onClick,
colors = TangemButtonsDefaults.secondaryButtonColors,
enabled = enabled,
@ -214,7 +212,7 @@ fun SelectorButton(text: String, onClick: () -> Unit, modifier: Modifier = Modif
modifier = modifier,
text = text,
textStyle = TangemTheme.typography.subtitle2,
icon = TangemButtonIcon.End(iconResId = R.drawable.ic_chevron_24),
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
onClick = onClick,
colors = TangemButtonsDefaults.selectorButtonColors,
showProgress = false,
@ -224,362 +222,6 @@ fun SelectorButton(text: String, onClick: () -> Unit, modifier: Modifier = Modif
}
// endregion Other
// region Action
/**
* [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=290-305&t=3z98eFnTeyIx5TH5-4)
* */
@Composable
fun RoundedActionButton(
text: String,
@DrawableRes iconResId: Int,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.Start(iconResId),
onClick = onClick,
enabled = enabled,
showProgress = false,
colors = TangemButtonsDefaults.secondaryButtonColors,
size = TangemButtonSize.RoundedAction,
)
}
/**
* [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=1208-1395&t=3z98eFnTeyIx5TH5-4)
* */
@Composable
fun ActionButton(
text: String,
@DrawableRes iconResId: Int,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.Start(iconResId),
onClick = onClick,
enabled = enabled,
showProgress = false,
colors = TangemButtonsDefaults.secondaryButtonColors,
size = TangemButtonSize.Action,
)
}
/**
* Same as [RoundedActionButton] but colored in primary background color
* */
@Composable
fun BackgroundActionButton(
text: String,
@DrawableRes iconResId: Int,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.Start(iconResId),
onClick = onClick,
enabled = enabled,
showProgress = false,
colors = TangemButtonsDefaults.backgroundButtonColors,
size = TangemButtonSize.RoundedAction,
)
}
// endregion Action
// region Defaults
@Suppress("LongParameterList")
@Composable
private fun TangemButton(
text: String,
icon: TangemButtonIcon,
onClick: () -> Unit,
colors: ButtonColors,
showProgress: Boolean,
enabled: Boolean,
modifier: Modifier = Modifier,
size: TangemButtonSize = TangemButtonSize.Default,
elevation: ButtonElevation = TangemButtonsDefaults.elevation,
textStyle: TextStyle = TangemTheme.typography.button,
) {
Button(
modifier = modifier.heightIn(min = size.toHeightDp()),
onClick = { if (!showProgress) onClick() },
enabled = enabled,
elevation = elevation,
shape = size.toShape(),
colors = colors,
contentPadding = size.toContentPadding(icon = icon),
) {
ButtonContent(
text = text,
textStyle = textStyle,
buttonIcon = icon,
colors = colors,
showProgress = showProgress,
enabled = enabled,
size = size,
)
}
}
@Suppress("LongParameterList")
@Composable
private fun ButtonContent(
text: String,
textStyle: TextStyle,
buttonIcon: TangemButtonIcon,
colors: ButtonColors,
size: TangemButtonSize,
enabled: Boolean,
showProgress: Boolean,
) {
val icon = @Composable { iconResId: Int ->
Icon(
modifier = Modifier.size(TangemTheme.dimens.size20),
painter = painterResource(id = iconResId),
tint = colors.contentColor(enabled = enabled).value,
contentDescription = null,
)
}
if (showProgress) {
Box(modifier = Modifier.wrapContentSize()) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.Center)
.size(TangemTheme.dimens.size24),
color = colors.contentColor(enabled = enabled).value,
strokeWidth = TangemTheme.dimens.size4,
)
}
} else {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(size.toIconPadding()),
) {
if (buttonIcon is TangemButtonIcon.Start) {
icon(buttonIcon.iconResId)
}
Text(
text = text,
style = textStyle,
color = colors.contentColor(enabled = enabled).value,
maxLines = 1,
)
if (buttonIcon is TangemButtonIcon.End) {
icon(buttonIcon.iconResId)
}
}
}
}
@Immutable
private sealed interface TangemButtonIcon {
val iconResId: Int?
data class Start(override val iconResId: Int) : TangemButtonIcon
data class End(override val iconResId: Int) : TangemButtonIcon
object None : TangemButtonIcon {
override val iconResId: Int? = null
}
}
private enum class TangemButtonSize {
Default,
Text,
Selector,
Action,
RoundedAction,
}
@Composable
@ReadOnlyComposable
private fun TangemButtonSize.toHeightDp(): Dp = when (this) {
TangemButtonSize.Default -> TangemTheme.dimens.size48
TangemButtonSize.Text -> TangemTheme.dimens.size40
TangemButtonSize.Selector -> TangemTheme.dimens.size24
TangemButtonSize.Action,
TangemButtonSize.RoundedAction,
-> TangemTheme.dimens.size36
}
@Composable
@ReadOnlyComposable
private fun TangemButtonSize.toShape(): Shape = when (this) {
TangemButtonSize.Default -> TangemTheme.shapes.roundedCornersMedium
TangemButtonSize.Text -> TangemTheme.shapes.roundedCornersSmall
TangemButtonSize.Selector -> TangemTheme.shapes.roundedCornersSmall
TangemButtonSize.Action -> TangemTheme.shapes.roundedCornersMedium
TangemButtonSize.RoundedAction -> TangemTheme.shapes.roundedCornersLarge
}
@Composable
@ReadOnlyComposable
private fun TangemButtonSize.toIconPadding(): Dp = when (this) {
TangemButtonSize.Default -> TangemTheme.dimens.spacing8
TangemButtonSize.Text -> TangemTheme.dimens.spacing8
TangemButtonSize.Selector -> 0.dp
TangemButtonSize.Action,
TangemButtonSize.RoundedAction,
-> TangemTheme.dimens.spacing8
}
@Composable
@ReadOnlyComposable
private fun TangemButtonSize.toContentPadding(icon: TangemButtonIcon): PaddingValues {
val horizontalPadding = this.toHorizontalContentPadding(icon = icon)
return when (this) {
TangemButtonSize.Default -> PaddingValues(
top = TangemTheme.dimens.spacing14,
bottom = TangemTheme.dimens.spacing14,
start = horizontalPadding.first,
end = horizontalPadding.second,
)
TangemButtonSize.Text -> PaddingValues(
top = TangemTheme.dimens.spacing10,
bottom = TangemTheme.dimens.spacing10,
start = horizontalPadding.first,
end = horizontalPadding.second,
)
TangemButtonSize.Selector -> PaddingValues(
top = TangemTheme.dimens.spacing0_5,
bottom = TangemTheme.dimens.spacing0_5,
start = horizontalPadding.first,
end = horizontalPadding.second,
)
TangemButtonSize.Action,
TangemButtonSize.RoundedAction,
-> PaddingValues(
top = TangemTheme.dimens.spacing8,
bottom = TangemTheme.dimens.spacing8,
start = horizontalPadding.first,
end = horizontalPadding.second,
)
}
}
@Composable
@ReadOnlyComposable
private fun TangemButtonSize.toHorizontalContentPadding(icon: TangemButtonIcon): Pair<Dp, Dp> {
return when (this) {
TangemButtonSize.Default -> TangemTheme.dimens.spacing32 to TangemTheme.dimens.spacing32
TangemButtonSize.Text -> when (icon) {
is TangemButtonIcon.None -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing16
is TangemButtonIcon.Start -> TangemTheme.dimens.spacing14 to TangemTheme.dimens.spacing16
is TangemButtonIcon.End -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing14
}
TangemButtonSize.Selector -> TangemTheme.dimens.spacing0_5 to TangemTheme.dimens.spacing0_5
TangemButtonSize.Action,
TangemButtonSize.RoundedAction,
-> when (icon) {
is TangemButtonIcon.None -> TangemTheme.dimens.spacing24 to TangemTheme.dimens.spacing24
is TangemButtonIcon.Start -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing24
is TangemButtonIcon.End -> TangemTheme.dimens.spacing24 to TangemTheme.dimens.spacing16
}
}
}
private object TangemButtonsDefaults {
val elevation: ButtonElevation
@Composable get() = ButtonDefaults
.elevation(
defaultElevation = TangemTheme.dimens.elevation0,
pressedElevation = TangemTheme.dimens.elevation0,
)
val primaryButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = TangemTheme.colors.button.primary,
contentColor = TangemTheme.colors.text.primary2,
disabledBackgroundColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val secondaryButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = TangemTheme.colors.button.secondary,
contentColor = TangemTheme.colors.text.primary1,
disabledBackgroundColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val defaultTextButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = Color.Transparent,
contentColor = TangemTheme.colors.text.secondary,
disabledBackgroundColor = Color.Transparent,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val warningTextButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = Color.Transparent,
contentColor = TangemTheme.colors.text.warning,
disabledBackgroundColor = Color.Transparent,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val selectorButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = Color.Transparent,
contentColor = TangemTheme.colors.text.tertiary,
disabledBackgroundColor = Color.Transparent,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val backgroundButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = TangemTheme.colors.background.primary,
contentColor = TangemTheme.colors.text.primary1,
disabledBackgroundColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
)
}
@Immutable
private open class TangemButtonColors(
private val backgroundColor: Color,
private val contentColor: Color,
private val disabledBackgroundColor: Color,
private val disabledContentColor: Color,
) : ButtonColors {
@Composable
override fun backgroundColor(enabled: Boolean): State<Color> {
return rememberUpdatedState(newValue = if (enabled) backgroundColor else disabledBackgroundColor)
}
@Composable
override fun contentColor(enabled: Boolean): State<Color> {
return rememberUpdatedState(newValue = if (enabled) contentColor else disabledContentColor)
}
}
// endregion Defaults
// region Preview
@Composable
private fun PrimaryButtonSample() {
@ -730,34 +372,4 @@ private fun TextButtonPreview_DarkTheme() {
}
}
@Composable
private fun ActionButtonSample() {
Column(
modifier = Modifier.background(TangemTheme.colors.background.primary),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
RoundedActionButton(text = "Send", iconResId = R.drawable.ic_arrow_up_24, onClick = { })
ActionButton(text = "Send", iconResId = R.drawable.ic_arrow_up_24, onClick = { })
BackgroundActionButton(text = "Send", iconResId = R.drawable.ic_arrow_up_24, onClick = { })
RoundedActionButton(text = "Send", iconResId = R.drawable.ic_arrow_up_24, enabled = false, onClick = { })
ActionButton(text = "Send", iconResId = R.drawable.ic_arrow_up_24, enabled = false, onClick = { })
BackgroundActionButton(text = "Send", iconResId = R.drawable.ic_arrow_up_24, enabled = false, onClick = { })
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun ActionButtonPreview_LightTheme() {
TangemTheme {
ActionButtonSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun ActionButtonPreview_DarkTheme() {
TangemTheme(isDark = true) {
ActionButtonSample()
}
}
// endregion Preview

View file

@ -1,202 +0,0 @@
package com.tangem.core.ui.components
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
/**
* Closable notification with custom icon
* Child of parent component
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=1045-807&t=6CVvYDJe0sB7wBKE-0">Figma component</a>
*
* Use to show banner with custom icon and possibility to close
* i.e. Feedback notification
*
* @param title notification title
* @param icon drawable res on icon
* @param iconColor icon color
* @param onClick callback on click
* @param onCloseClick callback on close icon click
*/
@Composable
fun ClosableNotification(
title: String,
@DrawableRes icon: Int,
iconColor: Color,
onClick: (() -> Unit),
onCloseClick: (() -> Unit),
) {
NotificationCardTemplate(onClick) {
Icon(
modifier = Modifier
.size(TangemTheme.dimens.size20)
.align(Alignment.CenterStart),
painter = painterResource(id = icon),
tint = iconColor,
contentDescription = null,
)
Text(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing28)
.align(Alignment.CenterStart),
text = title,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle2,
)
Icon(
modifier = Modifier
.size(TangemTheme.dimens.size20)
.align(Alignment.CenterEnd)
.clickable(onClick = onCloseClick),
painter = painterResource(id = R.drawable.ic_close_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
)
}
}
/**
* Notification component from Design system
* There are few states for this component, but only one parent, see link below
*
* Use this for Notification with title, subtitle, clickable or not
*
* @param title notification title
* @param subtitle notification subtitle
* @param onClick click on notification, if its null then no chevron icon
*
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=1045-807&t=6CVvYDJe0sB7wBKE-0">Figma component</a>
*/
@Composable
fun WarningNotification(title: String, subtitle: String?, onClick: (() -> Unit)?) {
NotificationCardTemplate(onClick) {
Image(
modifier = Modifier
.size(TangemTheme.dimens.size20)
.align(Alignment.CenterStart),
painter = painterResource(id = R.drawable.img_attention_20),
contentDescription = null,
)
Column(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing28)
.align(Alignment.CenterStart),
) {
Text(
text = title,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle2,
)
if (!subtitle.isNullOrEmpty()) {
SpacerH2()
Text(
text = subtitle,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption,
)
}
}
if (onClick != null) {
Icon(
modifier = Modifier
.size(TangemTheme.dimens.size20)
.align(Alignment.CenterEnd),
painter = painterResource(id = R.drawable.ic_chevron_right_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
)
}
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun NotificationCardTemplate(onClick: (() -> Unit)? = null, content: @Composable BoxScope.() -> Unit) {
Surface(
color = TangemTheme.colors.button.secondary,
shape = RoundedCornerShape(TangemTheme.dimens.radius18),
onClick = onClick ?: {},
enabled = onClick != null,
) {
Box(
Modifier
.padding(
horizontal = TangemTheme.dimens.spacing12,
vertical = TangemTheme.dimens.spacing8,
)
.wrapContentSize(),
) {
content()
}
}
}
// region Preview
@Composable
private fun WarningNotificationPreview() {
Column(modifier = Modifier.fillMaxWidth()) {
WarningNotification(
title = "Your wallet hasnt been backed up",
subtitle = "Lorem ipsum dolor sit amet, consectetur " +
"adipiscing elit, sed do eiusmod tempor incididunt ut labore et...",
onClick = {},
)
SpacerH32()
WarningNotification(
title = "Your wallet hasnt been backed up",
subtitle = null,
onClick = {},
)
SpacerH32()
WarningNotification(
title = "Your wallet hasnt been backed up",
subtitle = "Lorem ipsum dolor sit amet, consectetur " +
"adipiscing elit, sed do eiusmod tempor incididunt ut labore et...",
onClick = null,
)
SpacerH32()
WarningNotification(
title = "Your wallet hasnt been backed up",
subtitle = null,
onClick = null,
)
SpacerH32()
ClosableNotification(
title = "Like tangem app?",
icon = R.drawable.ic_star_24,
iconColor = TangemTheme.colors.icon.attention,
onClick = {},
onCloseClick = {},
)
}
}
@Preview(showBackground = true)
@Composable
private fun Preview_WarningNotification_InLightTheme() {
TangemTheme(isDark = false) {
WarningNotificationPreview()
}
}
@Preview(showBackground = true)
@Composable
private fun Preview_WarningNotification_InDarkTheme() {
TangemTheme(isDark = true) {
WarningNotificationPreview()
}
}
// endregion Preview

View file

@ -0,0 +1,20 @@
package com.tangem.core.ui.components.buttons.actions
import androidx.annotation.DrawableRes
/**
* Action button config
*
* @property text text
* @property iconResId icon resource id
* @property onClick lambda be invoked when action component is clicked
* @property enabled enabled
*
[REDACTED_AUTHOR]
*/
data class ActionConfig(
val text: String,
@DrawableRes val iconResId: Int,
val onClick: () -> Unit,
val enabled: Boolean = true,
)

View file

@ -0,0 +1,96 @@
package com.tangem.core.ui.components.buttons.actions
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.res.TangemTheme
/**
* [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=290-305&t=3z98eFnTeyIx5TH5-4)
*/
@Composable
fun RoundedActionButton(config: ActionConfig, modifier: Modifier = Modifier) {
TangemButton(
modifier = modifier,
text = config.text,
icon = TangemButtonIconPosition.Start(config.iconResId),
onClick = config.onClick,
enabled = config.enabled,
showProgress = false,
colors = TangemButtonsDefaults.secondaryButtonColors,
size = TangemButtonSize.RoundedAction,
)
}
/**
* [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=1208-1395&t=3z98eFnTeyIx5TH5-4)
*/
@Composable
fun ActionButton(config: ActionConfig, modifier: Modifier = Modifier) {
TangemButton(
modifier = modifier,
text = config.text,
icon = TangemButtonIconPosition.Start(config.iconResId),
onClick = config.onClick,
enabled = config.enabled,
showProgress = false,
colors = TangemButtonsDefaults.secondaryButtonColors,
size = TangemButtonSize.Action,
)
}
/**
* Same as [RoundedActionButton] but colored in primary background color
*/
@Composable
fun BackgroundActionButton(config: ActionConfig, modifier: Modifier = Modifier) {
TangemButton(
modifier = modifier,
text = config.text,
icon = TangemButtonIconPosition.Start(config.iconResId),
onClick = config.onClick,
enabled = config.enabled,
showProgress = false,
colors = TangemButtonsDefaults.backgroundButtonColors,
size = TangemButtonSize.RoundedAction,
)
}
@Preview(showBackground = true)
@Composable
private fun Preview_ActionButton_Light(@PreviewParameter(ActionStateProvider::class) state: ActionConfig) {
TangemTheme(isDark = false) {
Column {
RoundedActionButton(state)
ActionButton(state)
BackgroundActionButton(state)
}
}
}
@Preview(showBackground = true)
@Composable
private fun Preview_ActionButton_Dark(@PreviewParameter(ActionStateProvider::class) state: ActionConfig) {
TangemTheme {
Column {
RoundedActionButton(state)
ActionButton(state)
BackgroundActionButton(state)
}
}
}
private class ActionStateProvider : CollectionPreviewParameterProvider<ActionConfig>(
collection = listOf(
ActionConfig(text = "Send", iconResId = R.drawable.ic_arrow_up_24, onClick = {}),
ActionConfig(text = "Receive", iconResId = R.drawable.ic_arrow_down_24, enabled = false, onClick = {}),
),
)

View file

@ -0,0 +1,96 @@
package com.tangem.core.ui.components.buttons.common
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import com.tangem.core.ui.res.TangemTheme
@Suppress("LongParameterList")
@Composable
internal fun TangemButton(
text: String,
icon: TangemButtonIconPosition,
onClick: () -> Unit,
colors: ButtonColors,
showProgress: Boolean,
enabled: Boolean,
modifier: Modifier = Modifier,
size: TangemButtonSize = TangemButtonSize.Default,
elevation: ButtonElevation = TangemButtonsDefaults.elevation,
textStyle: TextStyle = TangemTheme.typography.button,
) {
Button(
modifier = modifier.heightIn(min = size.toHeightDp()),
onClick = { if (!showProgress) onClick() },
enabled = enabled,
elevation = elevation,
shape = size.toShape(),
colors = colors,
contentPadding = size.toContentPadding(icon = icon),
) {
ButtonContent(
text = text,
textStyle = textStyle,
buttonIcon = icon,
colors = colors,
showProgress = showProgress,
enabled = enabled,
size = size,
)
}
}
@Suppress("LongParameterList")
@Composable
private fun ButtonContent(
text: String,
textStyle: TextStyle,
buttonIcon: TangemButtonIconPosition,
colors: ButtonColors,
size: TangemButtonSize,
enabled: Boolean,
showProgress: Boolean,
) {
val icon = @Composable { iconResId: Int ->
Icon(
modifier = Modifier.size(TangemTheme.dimens.size20),
painter = painterResource(id = iconResId),
tint = colors.contentColor(enabled = enabled).value,
contentDescription = null,
)
}
if (showProgress) {
Box(modifier = Modifier.wrapContentSize()) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.Center)
.size(TangemTheme.dimens.size24),
color = colors.contentColor(enabled = enabled).value,
strokeWidth = TangemTheme.dimens.size4,
)
}
} else {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(size.toIconPadding()),
) {
if (buttonIcon is TangemButtonIconPosition.Start) {
icon(buttonIcon.iconResId)
}
Text(
text = text,
style = textStyle,
color = colors.contentColor(enabled = enabled).value,
maxLines = 1,
)
if (buttonIcon is TangemButtonIconPosition.End) {
icon(buttonIcon.iconResId)
}
}
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.core.ui.components.buttons.common
import androidx.compose.material.ButtonColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.graphics.Color
internal class TangemButtonColors(
private val backgroundColor: Color,
private val contentColor: Color,
private val disabledBackgroundColor: Color,
private val disabledContentColor: Color,
) : ButtonColors {
@Composable
override fun backgroundColor(enabled: Boolean): State<Color> {
return rememberUpdatedState(newValue = if (enabled) backgroundColor else disabledBackgroundColor)
}
@Composable
override fun contentColor(enabled: Boolean): State<Color> {
return rememberUpdatedState(newValue = if (enabled) contentColor else disabledContentColor)
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.core.ui.components.buttons.common
import androidx.annotation.DrawableRes
internal sealed interface TangemButtonIconPosition {
val iconResId: Int?
data class Start(@DrawableRes override val iconResId: Int) : TangemButtonIconPosition
data class End(@DrawableRes override val iconResId: Int) : TangemButtonIconPosition
object None : TangemButtonIconPosition {
@DrawableRes
override val iconResId: Int? = null
}
}

View file

@ -0,0 +1,105 @@
package com.tangem.core.ui.components.buttons.common
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
internal enum class TangemButtonSize {
Default,
Text,
Selector,
Action,
RoundedAction,
}
@Composable
@ReadOnlyComposable
internal fun TangemButtonSize.toHeightDp(): Dp = when (this) {
TangemButtonSize.Default -> TangemTheme.dimens.size48
TangemButtonSize.Text -> TangemTheme.dimens.size40
TangemButtonSize.Selector -> TangemTheme.dimens.size24
TangemButtonSize.Action,
TangemButtonSize.RoundedAction,
-> TangemTheme.dimens.size36
}
@Composable
@ReadOnlyComposable
internal fun TangemButtonSize.toShape(): Shape = when (this) {
TangemButtonSize.Default -> TangemTheme.shapes.roundedCornersMedium
TangemButtonSize.Text -> TangemTheme.shapes.roundedCornersSmall
TangemButtonSize.Selector -> TangemTheme.shapes.roundedCornersSmall
TangemButtonSize.Action -> TangemTheme.shapes.roundedCornersMedium
TangemButtonSize.RoundedAction -> TangemTheme.shapes.roundedCornersLarge
}
@Composable
@ReadOnlyComposable
internal fun TangemButtonSize.toIconPadding(): Dp = when (this) {
TangemButtonSize.Default -> TangemTheme.dimens.spacing8
TangemButtonSize.Text -> TangemTheme.dimens.spacing8
TangemButtonSize.Selector -> 0.dp
TangemButtonSize.Action,
TangemButtonSize.RoundedAction,
-> TangemTheme.dimens.spacing8
}
@Composable
@ReadOnlyComposable
internal fun TangemButtonSize.toContentPadding(icon: TangemButtonIconPosition): PaddingValues {
val horizontalPadding = this.toHorizontalContentPadding(icon = icon)
return when (this) {
TangemButtonSize.Default -> PaddingValues(
top = TangemTheme.dimens.spacing14,
bottom = TangemTheme.dimens.spacing14,
start = horizontalPadding.first,
end = horizontalPadding.second,
)
TangemButtonSize.Text -> PaddingValues(
top = TangemTheme.dimens.spacing10,
bottom = TangemTheme.dimens.spacing10,
start = horizontalPadding.first,
end = horizontalPadding.second,
)
TangemButtonSize.Selector -> PaddingValues(
top = TangemTheme.dimens.spacing0_5,
bottom = TangemTheme.dimens.spacing0_5,
start = horizontalPadding.first,
end = horizontalPadding.second,
)
TangemButtonSize.Action,
TangemButtonSize.RoundedAction,
-> PaddingValues(
top = TangemTheme.dimens.spacing8,
bottom = TangemTheme.dimens.spacing8,
start = horizontalPadding.first,
end = horizontalPadding.second,
)
}
}
@Composable
@ReadOnlyComposable
internal fun TangemButtonSize.toHorizontalContentPadding(icon: TangemButtonIconPosition): Pair<Dp, Dp> {
return when (this) {
TangemButtonSize.Default -> TangemTheme.dimens.spacing32 to TangemTheme.dimens.spacing32
TangemButtonSize.Text -> when (icon) {
is TangemButtonIconPosition.None -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing16
is TangemButtonIconPosition.Start -> TangemTheme.dimens.spacing14 to TangemTheme.dimens.spacing16
is TangemButtonIconPosition.End -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing14
}
TangemButtonSize.Selector -> TangemTheme.dimens.spacing0_5 to TangemTheme.dimens.spacing0_5
TangemButtonSize.Action,
TangemButtonSize.RoundedAction,
-> when (icon) {
is TangemButtonIconPosition.None -> TangemTheme.dimens.spacing24 to TangemTheme.dimens.spacing24
is TangemButtonIconPosition.Start -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing24
is TangemButtonIconPosition.End -> TangemTheme.dimens.spacing24 to TangemTheme.dimens.spacing16
}
}
}

View file

@ -0,0 +1,78 @@
package com.tangem.core.ui.components.buttons.common
import androidx.compose.material.ButtonColors
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.ButtonElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.res.TangemTheme
internal object TangemButtonsDefaults {
val elevation: ButtonElevation
@Composable get() = ButtonDefaults.elevation(
defaultElevation = TangemTheme.dimens.elevation0,
pressedElevation = TangemTheme.dimens.elevation0,
)
val primaryButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = TangemTheme.colors.button.primary,
contentColor = TangemTheme.colors.text.primary2,
disabledBackgroundColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val secondaryButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = TangemTheme.colors.button.secondary,
contentColor = TangemTheme.colors.text.primary1,
disabledBackgroundColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val defaultTextButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = Color.Transparent,
contentColor = TangemTheme.colors.text.secondary,
disabledBackgroundColor = Color.Transparent,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val warningTextButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = Color.Transparent,
contentColor = TangemTheme.colors.text.warning,
disabledBackgroundColor = Color.Transparent,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val selectorButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = Color.Transparent,
contentColor = TangemTheme.colors.text.tertiary,
disabledBackgroundColor = Color.Transparent,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val backgroundButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = TangemTheme.colors.background.primary,
contentColor = TangemTheme.colors.text.primary1,
disabledBackgroundColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
)
}

View file

@ -0,0 +1,171 @@
package com.tangem.core.ui.components.notifications
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH2
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
/**
* Notification component from Design system.
* Use this for Notification with title, subtitle, clickable or not.
*
* @param state component state
* @param modifier modifier
*
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=1045-807&t=6CVvYDJe0sB7wBKE-0"
* >Figma component</a>
*/
@Composable
fun Notification(state: NotificationState, modifier: Modifier = Modifier) {
Surface(
onClick = if (state is NotificationState.Action) {
state.onClick
} else {
{}
},
modifier = modifier,
enabled = when (state) {
is NotificationState.Simple -> false
is NotificationState.Action -> true
},
shape = RoundedCornerShape(TangemTheme.dimens.radius18),
color = TangemTheme.colors.button.secondary,
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing12, vertical = TangemTheme.dimens.spacing8),
) {
NotificationIcon(
iconResId = state.iconResId,
iconTint = state.tint,
modifier = Modifier
.size(size = TangemTheme.dimens.size20)
.align(alignment = Alignment.CenterStart),
)
NotificationInfoBlock(
title = state.title,
subtitle = state.subtitle,
modifier = Modifier.align(alignment = Alignment.CenterStart),
)
if (state is NotificationState.Action) {
Icon(
modifier = Modifier
.size(size = TangemTheme.dimens.size20)
.align(alignment = Alignment.CenterEnd),
painter = painterResource(id = R.drawable.ic_chevron_right_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
)
}
}
}
}
@Composable
private fun NotificationIcon(@DrawableRes iconResId: Int, iconTint: Color?, modifier: Modifier = Modifier) {
if (iconTint != null) {
Icon(
painter = painterResource(id = iconResId),
contentDescription = null,
modifier = modifier,
tint = iconTint,
)
} else {
Image(
painter = painterResource(id = iconResId),
contentDescription = null,
modifier = modifier,
)
}
}
@Composable
private fun NotificationInfoBlock(title: String, subtitle: String?, modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing30)) {
Text(
text = title,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.body2,
)
if (!subtitle.isNullOrEmpty()) {
SpacerH2()
Text(
text = subtitle,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption,
)
}
}
}
@Preview
@Composable
private fun Preview_WarningNotification_Light(
@PreviewParameter(NotificationStateProvider::class)
state: NotificationState,
) {
TangemTheme(isDark = false) {
Notification(state)
}
}
@Preview
@Composable
private fun Preview_WarningNotification_Dark(
@PreviewParameter(NotificationStateProvider::class)
state: NotificationState,
) {
TangemTheme(isDark = true) {
Notification(state)
}
}
private class NotificationStateProvider : CollectionPreviewParameterProvider<NotificationState>(
collection = listOf(
NotificationState.Simple(
title = "Your wallet hasnt been backed up",
subtitle = "Lorem ipsum dolor sit amet, consectetur " +
"adipiscing elit, sed do eiusmod tempor incididunt ut labore et...",
iconResId = R.drawable.img_attention_20,
),
NotificationState.Simple(
title = "Your wallet hasnt been backed up",
subtitle = null,
iconResId = R.drawable.ic_alert_circle_24,
tint = TangemColorPalette.Amaranth,
),
NotificationState.Action(
title = "Your wallet hasnt been backed up",
subtitle = "Lorem ipsum dolor sit amet, consectetur " +
"adipiscing elit, sed do eiusmod tempor incididunt ut labore et...",
iconResId = R.drawable.img_attention_20,
onClick = {},
),
NotificationState.Action(
title = "Your wallet hasnt been backed up",
subtitle = null,
iconResId = R.drawable.ic_alert_circle_24,
tint = TangemColorPalette.Amaranth,
onClick = {},
),
),
)

View file

@ -0,0 +1,54 @@
package com.tangem.core.ui.components.notifications
import androidx.annotation.DrawableRes
import androidx.compose.ui.graphics.Color
/**
* Notification component state
*
* @property title title
* @property subtitle subtitle
* @property iconResId icon resource id
* @property tint icon tint
*
[REDACTED_AUTHOR]
*/
sealed class NotificationState(
open val title: String,
open val subtitle: String? = null,
@DrawableRes open val iconResId: Int,
open val tint: Color? = null,
) {
/**
* Simple notification state. Non clickable.
*
* @property title title
* @property subtitle subtitle
* @property iconResId icon resource id
* @property tint icon tint
*/
data class Simple(
override val title: String,
override val subtitle: String? = null,
@DrawableRes override val iconResId: Int,
override val tint: Color? = null,
) : NotificationState(title, subtitle, iconResId, tint)
/**
* Clickable notification state
*
* @property title title
* @property subtitle subtitle
* @property iconResId icon resource id
* @property tint icon tint
* @param onClick lambda be invoked when notification component is clicked
*/
data class Action(
override val title: String,
override val subtitle: String? = null,
@DrawableRes override val iconResId: Int,
override val tint: Color? = null,
val onClick: () -> Unit,
) : NotificationState(title, subtitle, iconResId, tint)
}

View file

@ -1,9 +1,11 @@
package com.tangem.core.ui.extensions
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
/**
@ -24,6 +26,8 @@ sealed interface TextReference {
*/
data class Res(@StringRes val id: Int, val formatArgs: WrappedList<Any> = WrappedList(emptyList())) : TextReference
data class PluralRes(@PluralsRes val id: Int, val count: Int, val formatArgs: WrappedList<Any>) : TextReference
/**
* Text string
*
@ -38,6 +42,7 @@ sealed interface TextReference {
fun TextReference.resolveReference(): String {
return when (this) {
is TextReference.Res -> stringResource(id, *formatArgs.toTypedArray())
is TextReference.PluralRes -> pluralStringResource(id, count, *formatArgs.toTypedArray())
is TextReference.Str -> value
}
}

View file

@ -76,6 +76,7 @@ data class TangemDimens internal constructor(
val size200: Dp = 200.dp,
// endregion Size
// region Spacing
val spacing0: Dp = 0.dp,
val spacing0_5: Dp = 0.5.dp,
val spacing2: Dp = 2.dp,
val spacing4: Dp = 4.dp,
@ -91,6 +92,7 @@ data class TangemDimens internal constructor(
val spacing24: Dp = 24.dp,
val spacing26: Dp = 26.dp,
val spacing28: Dp = 28.dp,
val spacing30: Dp = 30.dp,
val spacing32: Dp = 32.dp,
val spacing34: Dp = 34.dp,
val spacing36: Dp = 34.dp,

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