Updated on 2026-08-14

This commit is contained in:
Tangem 2023-07-04 18:55:19 +03:00
commit 6493949b5f
133 changed files with 4409 additions and 1122 deletions

View file

@ -13,6 +13,9 @@
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY" value="false" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="5" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="3" />
<option name="ALLOW_TRAILING_COMMA" value="true" />
<option name="BLANK_LINES_BEFORE_DECLARATION_WITH_COMMENT_OR_ANNOTATION_ON_SEPARATE_LINE" value="0" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />

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

@ -36,6 +36,7 @@
android:largeHeap="@bool/largeHeap"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher"
android:extractNativeLibs="true"
android:supportsRtl="true"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:allowBackup, android:fullBackupContent">
@ -138,6 +139,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

@ -474,6 +474,17 @@
}
]
},
{
"id": "telos",
"symbol": "TLOS",
"name": "Telos EVM",
"networks":
[
{
"networkId": "telos/test"
}
]
},
{
"id": "ravencoin",
"symbol": "RVN",

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

@ -43,6 +43,7 @@ fun Blockchain.getGreyedOutIconRes(): Int {
Blockchain.TerraV1 -> R.drawable.ic_terra_no_color
Blockchain.TerraV2 -> R.drawable.ic_terra2_no_color
Blockchain.Cronos -> R.drawable.ic_cronos_no_color
Blockchain.Telos, Blockchain.TelosTestnet -> R.drawable.ic_telos_no_color
else -> R.drawable.ic_tangem_logo
}
}

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,7 +12,6 @@ import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
@ -33,7 +32,6 @@ import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
@ -110,19 +108,23 @@ class WalletConnectMiddleware {
)
}
is WalletConnectAction.OpeningSessionTimeout -> {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.SessionTimeout))
Timber.e("OpeningSessionTimeout for topic ${action.session.topic}")
// do not show dialog for now, it shows always to user if cannot establish connection
// store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.SessionTimeout))
}
is WalletConnectAction.FailureEstablishingSession -> {
if (action.error != null) {
store.dispatch(
GlobalAction.ShowDialog(
AppDialog.SimpleOkDialogRes(
headerId = R.string.common_warning,
messageId = action.error.messageResource,
),
),
)
}
Timber.e("FailureEstablishingSession for topic ${action.session?.topic}")
// disable alerts in release to avoid annoying users
// if (action.error != null) {
// store.dispatch(
// GlobalAction.ShowDialog(
// AppDialog.SimpleOkDialogRes(
// headerId = R.string.common_warning,
// messageId = action.error.messageResource,
// ),
// ),
// )
// }
if (action.session != null) {
walletConnectManager.disconnect(action.session)
}
@ -140,11 +142,14 @@ class WalletConnectMiddleware {
}
}
is WalletConnectAction.RefuseOpeningSession -> {
store.dispatch(
GlobalAction.ShowDialog(
WalletConnectDialog.OpeningSessionRejected,
),
)
Timber.e("RefuseOpeningSession")
// do not show for now to avoid anoying users with alert
// store.dispatch(
// GlobalAction.ShowDialog(
// WalletConnectDialog.OpeningSessionRejected,
// ),
// )
}
is WalletConnectAction.ScanCard -> {
val scanResponse = store.state.globalState.scanResponse ?: return
@ -306,16 +311,18 @@ class WalletConnectMiddleware {
)
}
is WalletConnectError.ExternalApprovalError -> {
val message = action.error.message
if (!message.isNullOrEmpty()) {
store.dispatchOnMain(
GlobalAction.ShowDialog(
AppDialog.SimpleOkWarningDialog(
message = message,
),
),
)
}
Timber.e(action.error, "ExternalApprovalError ${action.error.message}")
// do not show dialog on this event
// val message = action.error.message
// if (!message.isNullOrEmpty()) {
// store.dispatchOnMain(
// GlobalAction.ShowDialog(
// AppDialog.SimpleOkWarningDialog(
// message = message,
// ),
// ),
// )
// }
}
else -> Unit
}

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

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

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
android:fillColor="#EBEBEB"/>
<path
android:strokeWidth="1"
android:pathData="M14.862,5.607C14.909,5.486 14.825,5.354 14.695,5.346L12.507,5.2C12.44,5.196 12.375,5.227 12.336,5.283L11.756,6.118C11.72,6.17 11.66,6.201 11.597,6.201L7.745,6.178C7.556,6.177 7.373,6.245 7.231,6.37L6.163,7.308C5.925,7.517 5.839,7.849 5.945,8.147L6.454,9.575C6.457,9.583 6.455,9.591 6.451,9.597C6.165,9.977 5.998,10.434 5.973,10.909L5.964,11.071C5.91,12.08 6.029,13.091 6.316,14.061C6.552,14.859 6.823,15.775 6.989,16.336C7.076,16.632 7.379,16.807 7.679,16.735C8.325,16.58 9.435,16.313 10.309,16.102C11.078,15.917 11.809,15.602 12.472,15.17L12.886,14.901C13.285,14.641 13.597,14.269 13.784,13.831C13.787,13.824 13.793,13.818 13.801,13.817L15.292,13.544C15.603,13.487 15.848,13.246 15.91,12.936L16.188,11.541C16.225,11.356 16.193,11.163 16.097,11L14.288,7.909C14.17,7.706 14.15,7.461 14.234,7.242L14.862,5.607Z"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#B0B0B0"/>
</vector>

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

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

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

@ -103,6 +103,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
zendesk = configValues.zendesk,
swapReferrerAccount = configValues.swapReferrerAccount,
walletConnectProjectId = configValues.walletConnectProjectId,
tangemComAuthorization = configValues.tangemComAuthorization,
)
}
}

View file

@ -18,4 +18,5 @@ data class Config(
val zendesk: ZendeskConfig? = null,
val swapReferrerAccount: SwapReferrerAccount? = null,
val walletConnectProjectId: String = "",
val tangemComAuthorization: String? = null,
)

View file

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

@ -9,7 +9,7 @@
},
{
"name": "NEW_CARD_SCANNING_ENABLED",
"version": "4.8.0"
"version": "4.9.0"
},
{
"name": "REDESIGNED_WALLET_SCREEN_ENABLED",

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>
@ -44,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>
@ -56,7 +56,6 @@
<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>
@ -44,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>
@ -56,7 +56,6 @@
<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>
@ -44,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>
@ -56,7 +56,6 @@
<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>
@ -329,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>
@ -346,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>
@ -354,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>
@ -406,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>
@ -420,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>
@ -449,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>
@ -472,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>
@ -313,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>
@ -329,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>
@ -419,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>
@ -442,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>
@ -325,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>
@ -342,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>
@ -350,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>
@ -400,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>
@ -442,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>
@ -465,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

@ -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 ActionButtonConfig(
val text: String,
@DrawableRes val iconResId: Int,
val onClick: () -> Unit,
val enabled: Boolean = true,
)

View file

@ -0,0 +1,152 @@
package com.tangem.core.ui.components.buttons.actions
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
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.SpacerW8
import com.tangem.core.ui.components.buttons.common.*
import com.tangem.core.ui.res.TangemTheme
/**
* Rounded action button
*
* @param config component config
* @param modifier modifier
* @param color background color
*
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=290-306&mode=design&t=
* cSLQtIbR2J1h5M9U-4">Show in Figma</a>
*/
@Composable
fun RoundedActionButton(
config: ActionButtonConfig,
modifier: Modifier = Modifier,
color: Color = TangemTheme.colors.button.secondary,
) {
Button(
config = config,
shape = RoundedCornerShape(size = TangemTheme.dimens.radius24),
modifier = modifier,
color = color,
)
}
/**
* Action button
*
* @param config component config
* @param modifier modifier
* @param color background color
*
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=290-306&mode=design&t=
* cSLQtIbR2J1h5M9U-4">Show in Figma</a>
*/
@Composable
fun ActionButton(
config: ActionButtonConfig,
modifier: Modifier = Modifier,
color: Color = TangemTheme.colors.button.secondary,
) {
Button(
config = config,
shape = RoundedCornerShape(size = TangemTheme.dimens.radius12),
modifier = modifier,
color = color,
)
}
@Composable
private fun Button(
config: ActionButtonConfig,
shape: RoundedCornerShape,
modifier: Modifier = Modifier,
color: Color = TangemTheme.colors.button.secondary,
) {
Row(
modifier = modifier
.heightIn(min = TangemTheme.dimens.size36)
.clip(shape)
.background(
color = if (config.enabled) color else TangemTheme.colors.button.disabled,
shape = shape,
)
.clickable(enabled = config.enabled, onClick = config.onClick)
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing24,
)
.padding(vertical = TangemTheme.dimens.spacing8),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(id = config.iconResId),
contentDescription = null,
modifier = Modifier.size(size = TangemTheme.dimens.size20),
tint = if (config.enabled) TangemTheme.colors.icon.primary1 else TangemTheme.colors.icon.informative,
)
SpacerW8()
Text(
text = config.text,
color = if (config.enabled) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.disabled,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography.button,
)
}
}
@Preview(group = "RoundedActionButton", showBackground = true)
@Composable
private fun Preview_RoundedActionButton_Light(@PreviewParameter(ActionStateProvider::class) state: ActionButtonConfig) {
TangemTheme(isDark = false) {
RoundedActionButton(state)
}
}
@Preview(group = "RoundedActionButton", showBackground = true)
@Composable
private fun Preview_RoundedActionButton_Dark(@PreviewParameter(ActionStateProvider::class) state: ActionButtonConfig) {
TangemTheme(isDark = true) {
RoundedActionButton(state)
}
}
@Preview(group = "ActionButton", showBackground = true)
@Composable
private fun Preview_ActionButton_Light(@PreviewParameter(ActionStateProvider::class) state: ActionButtonConfig) {
TangemTheme(isDark = false) {
ActionButton(state)
}
}
@Preview(group = "ActionButton", showBackground = true)
@Composable
private fun Preview_ActionButton_Dark(@PreviewParameter(ActionStateProvider::class) state: ActionButtonConfig) {
TangemTheme(isDark = true) {
ActionButton(state)
}
}
private class ActionStateProvider : CollectionPreviewParameterProvider<ActionButtonConfig>(
collection = listOf(
ActionButtonConfig(text = "Enabled", iconResId = R.drawable.ic_arrow_up_24, enabled = true, onClick = {}),
ActionButtonConfig(text = "Disabled", 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

@ -2,14 +2,16 @@ package com.tangem.core.ui.components.notifications
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
@ -32,19 +34,24 @@ import com.tangem.core.ui.res.TangemTheme
*/
@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
.clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius18))
.background(
color = TangemTheme.colors.button.secondary,
shape = RoundedCornerShape(size = TangemTheme.dimens.radius18),
)
.clickable(
enabled = when (state) {
is NotificationState.Simple -> false
is NotificationState.Action -> true
},
onClick = if (state is NotificationState.Action) {
state.onClick
} else {
{}
},
),
) {
Box(
modifier = Modifier

View file

@ -40,6 +40,7 @@ fun getActiveIconRes(blockchainId: String): Int {
"terra", "terra-luna" -> R.drawable.img_terra_22
"terra-2", "terra-luna-2" -> R.drawable.img_terra2_22
"cronos" -> R.drawable.img_cronos_22
"TELOS", "TELOS/test" -> R.drawable.img_telos_22
else -> R.drawable.ic_alert_24
}
}
@ -84,6 +85,7 @@ fun getActiveIconResByCoinId(coinId: String, networkId: String): Int {
"ravencoin" -> R.drawable.img_ravencoin_22
"terra" -> R.drawable.img_terra_22
"terra-2" -> R.drawable.img_terra2_22
"telos" -> R.drawable.img_telos_22
else -> R.drawable.ic_alert_24
}
}

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

@ -20,6 +20,7 @@ data class TangemDimens internal constructor(
val elevation24: Dp = 24.dp,
// endregion Elevation
// region Radius
val radius0: Dp = 0.dp,
val radius2: Dp = 2.dp,
val radius3: Dp = 3.dp,
val radius4: Dp = 4.dp,
@ -29,6 +30,7 @@ data class TangemDimens internal constructor(
val radius12: Dp = 12.dp,
val radius16: Dp = 16.dp,
val radius18: Dp = 18.dp,
val radius24: Dp = 24.dp,
val radius28: Dp = 28.dp,
// endregion Radius
// region Size

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
android:fillColor="#5C40A6"/>
<path
android:strokeWidth="1"
android:pathData="M14.862,5.607C14.909,5.486 14.825,5.354 14.695,5.346L12.507,5.2C12.44,5.196 12.375,5.227 12.336,5.283L11.756,6.118C11.72,6.17 11.66,6.201 11.597,6.201L7.745,6.178C7.556,6.177 7.373,6.245 7.231,6.37L6.163,7.308C5.925,7.517 5.839,7.849 5.945,8.147L6.454,9.575C6.457,9.583 6.455,9.591 6.451,9.597C6.165,9.977 5.998,10.434 5.973,10.909L5.964,11.071C5.91,12.08 6.029,13.091 6.316,14.061C6.552,14.859 6.823,15.775 6.989,16.336C7.076,16.632 7.379,16.807 7.679,16.735C8.325,16.58 9.435,16.313 10.309,16.102C11.078,15.917 11.809,15.602 12.472,15.17L12.886,14.901C13.285,14.641 13.597,14.269 13.784,13.831C13.787,13.824 13.793,13.818 13.801,13.817L15.292,13.544C15.603,13.487 15.848,13.246 15.91,12.936L16.188,11.541C16.225,11.356 16.193,11.163 16.097,11L14.288,7.909C14.17,7.706 14.15,7.461 14.234,7.242L14.862,5.607Z"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,10 @@
package com.tangem.utils.extensions
import java.lang.ref.WeakReference
/**
[REDACTED_AUTHOR]
*/
fun <T> T.toWeakReference(): WeakReference<T> {
return WeakReference(this)
}

View file

@ -68,6 +68,10 @@ class UsedCardsPrefStorage internal constructor(
return cardInfo.isActivationStarted && !cardInfo.isActivationFinished
}
fun hadFinishedActivation(): Boolean {
return restore().any { it.isActivationFinished }
}
private fun findCardInfo(
cardId: String,
list: MutableList<DataSourceUsedCardInfo>? = null,

View file

@ -59,6 +59,10 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
"terra" -> Blockchain.TerraV1
"terra-2" -> Blockchain.TerraV2
"cronos" -> Blockchain.Cronos
"telos" -> Blockchain.Telos
"telos/test" -> Blockchain.TelosTestnet
"azero" -> Blockchain.AlephZero
"azero/test" -> Blockchain.AlephZeroTestnet
else -> null
}
}
@ -122,6 +126,10 @@ fun Blockchain.toNetworkId(): String {
Blockchain.TerraV1 -> "terra"
Blockchain.TerraV2 -> "terra-2"
Blockchain.Cronos -> "cronos"
Blockchain.Telos -> "telos"
Blockchain.TelosTestnet -> "telos/test"
Blockchain.AlephZero -> "azero"
Blockchain.AlephZeroTestnet -> "azero/test"
}
}
@ -162,6 +170,8 @@ fun Blockchain.toCoinId(): String {
Blockchain.TerraV1 -> "terra-luna"
Blockchain.TerraV2 -> "terra-luna-2"
Blockchain.Cronos -> "crypto-com-chain"
Blockchain.Telos, Blockchain.TelosTestnet -> "telos"
Blockchain.AlephZero, Blockchain.AlephZeroTestnet -> "azero"
}
}

View file

@ -36,6 +36,7 @@ private class DomainGlobalReducer : ReStoreReducer<DomainGlobalState> {
return when (action) {
is DomainGlobalAction.SaveScanNoteResponse -> {
val card = action.scanResponse.card
// TODO: AuthHeaders: try to remove it, because now we can use headers with dynamic values
state.networkServices.tangemTechService.addAuthenticationHeader(
RequestHeader.AuthenticationHeader(
object : AuthProvider {

1
features/learn2earn/api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,4 @@
plugins {
alias(deps.plugins.kotlin.jvm)
id("configuration")
}

1
features/learn2earn/impl/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,55 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.kotlin.serialization)
alias(deps.plugins.hilt.android)
id("configuration")
}
dependencies {
/** Core modules */
implementation(project(":common"))
implementation(project(":core:featuretoggles"))
implementation(project(":core:datasource"))
implementation(project(":core:utils"))
implementation(project(":core:ui"))
implementation(project(":core:res"))
implementation(project(":data:source:preferences"))
implementation(project(":libs:auth"))
implementation(project(":libs:crypto"))
implementation(deps.material)
/** AndroidX */
implementation(deps.androidx.core.ktx)
implementation(deps.androidx.appCompat)
implementation(deps.androidx.fragment.ktx)
implementation(deps.androidx.activity.compose)
implementation(deps.androidx.browser)
/** Compose */
implementation(deps.compose.material)
implementation(deps.compose.material3)
implementation(deps.compose.foundation)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
/** Preferences */
implementation(deps.krateSharedPref)
/** Network */
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.retrofit)
implementation(deps.retrofit.moshi)
/** Other libraries */
implementation(deps.kotlin.immutable.collections)
implementation(deps.kotlin.serialization)
implementation(deps.timber)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.feature.learn2earn.impl">
</manifest>

View file

@ -0,0 +1,137 @@
package com.tangem.feature.learn2earn.data
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.promotion.PromotionApi
import com.tangem.datasource.api.promotion.models.*
import com.tangem.feature.learn2earn.data.api.Learn2earnPreferenceStorage
import com.tangem.feature.learn2earn.data.api.Learn2earnRepository
import com.tangem.feature.learn2earn.data.models.PromoUserData
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
internal class DefaultLearn2earnRepository(
private val preferencesStorage: Learn2earnPreferenceStorage,
private val api: PromotionApi,
private val dispatchers: AppCoroutineDispatcherProvider,
moshi: Moshi,
) : Learn2earnRepository {
private val promotionInfoAdapter = moshi.adapter(PromotionInfoResponse::class.java)
private val userDataAdapter = moshi.adapter(PromoUserData::class.java)
private var userData: PromoUserData = restoreUserData()
override fun getUserData(): PromoUserData {
return userData
}
override fun updateUserData(userData: PromoUserData) {
this.userData = userData
saveUserData(userData)
}
override fun getProgramName(): String {
return PROGRAM_NAME
}
override suspend fun getPromotionInfo(): Result<PromotionInfoResponse> {
return withContext(dispatchers.io) {
val promotionInfo = restorePromotionInfo()
val data = promotionInfo?.getData(userData.promoCode)
if (data?.status == PromotionInfoResponse.Status.FINISHED) {
Result.success(promotionInfo)
} else {
runCatching { api.getPromotionInfo(getProgramName()) }
.fold(
onSuccess = { infoResponse ->
savePromotionInfo(infoResponse)
Result.success(infoResponse)
},
onFailure = { exception -> Result.failure(exception) },
)
}
}
}
override suspend fun validate(walletId: String): ValidateResponse {
return withContext(dispatchers.io) {
api.validate(ValidateRequestBody(walletId, getProgramName()))
}
}
override suspend fun requestAward(walletId: String, address: String): AwardResponse {
return withContext(dispatchers.io) {
api.requestAward(AwardRequestBody(walletId, address, getProgramName()))
}
}
override suspend fun validateCode(walletId: String, code: String?): CodeValidateResponse {
return withContext(dispatchers.io) {
api.validateCode(CodeValidateRequestBody(walletId, code))
}
}
override suspend fun requestAwardByCode(walletId: String, address: String, code: String?): CodeAwardResponse {
return withContext(dispatchers.io) {
api.requestAwardByCode(CodeAwardRequestBody(walletId, address, code))
}
}
private fun restorePromotionInfo(): PromotionInfoResponse? {
return try {
preferencesStorage.promotionInfo?.let { promotionInfoAdapter.fromJson(it) }
} catch (ex: Exception) {
Timber.e(ex)
preferencesStorage.promotionInfo = null
null
}
}
private fun savePromotionInfo(infoResponse: PromotionInfoResponse) {
try {
preferencesStorage.promotionInfo = promotionInfoAdapter.toJson(infoResponse)
} catch (ex: Exception) {
Timber.e(ex)
}
}
private fun restoreUserData(): PromoUserData {
return try {
preferencesStorage.userData?.let { userDataAdapter.fromJson(it) }
?: createEmptyUserData().apply { saveUserData(this) }
} catch (ex: Exception) {
Timber.e(ex)
createEmptyUserData().apply { saveUserData(this) }
}
}
private fun saveUserData(userData: PromoUserData) {
try {
preferencesStorage.userData = userDataAdapter.toJson(userData)
} catch (ex: Exception) {
Timber.e(ex)
}
}
private fun createEmptyUserData(): PromoUserData = PromoUserData(
promoCode = null,
isRegisteredInPromotion = false,
isAlreadyReceivedAward = false,
)
private companion object {
const val PROGRAM_NAME: String = "1inch"
}
}
private fun PromotionInfoResponse.getData(promoCode: String?): PromotionInfoResponse.Data? {
return if (promoCode == null) {
newCard
} else {
oldCard
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.feature.learn2earn.data
import android.content.Context
import com.tangem.feature.learn2earn.data.api.Learn2earnPreferenceStorage
import hu.autsoft.krate.SimpleKrate
import hu.autsoft.krate.default.withDefault
import hu.autsoft.krate.stringPref
/**
[REDACTED_AUTHOR]
*/
internal class DefaultPreferenceStorage(
context: Context,
) : SimpleKrate(context = context, name = "Lear2earnPromotion"), Learn2earnPreferenceStorage {
override var promotionInfo: String? by stringPref().withDefault(null)
override var userData: String? by stringPref().withDefault(null)
}

View file

@ -0,0 +1,11 @@
package com.tangem.feature.learn2earn.data.api
/**
[REDACTED_AUTHOR]
*/
interface Learn2earnPreferenceStorage {
var promotionInfo: String?
var userData: String?
}

View file

@ -0,0 +1,26 @@
package com.tangem.feature.learn2earn.data.api
import com.tangem.datasource.api.promotion.models.*
import com.tangem.feature.learn2earn.data.models.PromoUserData
/**
[REDACTED_AUTHOR]
*/
interface Learn2earnRepository {
fun getUserData(): PromoUserData
fun updateUserData(userData: PromoUserData)
fun getProgramName(): String
suspend fun getPromotionInfo(): Result<PromotionInfoResponse>
suspend fun validate(walletId: String): ValidateResponse
suspend fun requestAward(walletId: String, address: String): AwardResponse
suspend fun validateCode(walletId: String, code: String?): CodeValidateResponse
suspend fun requestAwardByCode(walletId: String, address: String, code: String?): CodeAwardResponse
}

View file

@ -0,0 +1,45 @@
package com.tangem.feature.learn2earn.data.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.promotion.PromotionApi
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.di.PromotionOneInch
import com.tangem.feature.learn2earn.data.DefaultLearn2earnRepository
import com.tangem.feature.learn2earn.data.DefaultPreferenceStorage
import com.tangem.feature.learn2earn.data.api.Learn2earnPreferenceStorage
import com.tangem.feature.learn2earn.data.api.Learn2earnRepository
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class Learn2earnDataModule {
@Provides
@Singleton
fun providePreferenceStorage(@ApplicationContext context: Context): Learn2earnPreferenceStorage {
return DefaultPreferenceStorage(context)
}
@Provides
@Singleton
fun provideRepository(
preferenceStorage: Learn2earnPreferenceStorage,
@NetworkMoshi moshi: Moshi,
@PromotionOneInch promotionApi: PromotionApi,
dispatchers: AppCoroutineDispatcherProvider,
): Learn2earnRepository {
return DefaultLearn2earnRepository(
preferencesStorage = preferenceStorage,
api = promotionApi,
dispatchers = dispatchers,
moshi = moshi,
)
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.feature.learn2earn.data.models
/**
[REDACTED_AUTHOR]
*/
data class PromoUserData(
val promoCode: String?,
val isRegisteredInPromotion: Boolean,
val isAlreadyReceivedAward: Boolean,
)

View file

@ -0,0 +1,263 @@
package com.tangem.feature.learn2earn.domain
import android.net.Uri
import com.tangem.datasource.api.promotion.models.AbstractPromotionResponse
import com.tangem.datasource.api.promotion.models.PromotionInfoResponse
import com.tangem.feature.learn2earn.data.api.Learn2earnRepository
import com.tangem.feature.learn2earn.data.models.PromoUserData
import com.tangem.feature.learn2earn.domain.api.*
import com.tangem.feature.learn2earn.domain.models.Promotion
import com.tangem.feature.learn2earn.domain.models.PromotionError
import com.tangem.feature.learn2earn.domain.models.toDomainError
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.lib.crypto.models.Currency
/**
[REDACTED_AUTHOR]
*/
class DefaultLearn2earnInteractor(
private val repository: Learn2earnRepository,
private val userWalletManager: UserWalletManager,
private val derivationManager: DerivationManager,
dependencyProvider: Learn2earnDependencyProvider,
) : Learn2earnInteractor {
override var webViewResultHandler: WebViewResultHandler? = null
private lateinit var promotion: Promotion
private val webViewUriBuilder: WebViewUriBuilder = WebViewUriBuilder(
authCredentialsProvider = dependencyProvider.getWebViewAuthCredentialsProvider(),
userCountryCodeProvider = dependencyProvider.getUserCountryCodeProvider(),
promoCodeProvider = { repository.getUserData().promoCode },
)
override suspend fun init() {
initPromotionInfo()
}
override fun isUserHadPromoCode(): Boolean {
return repository.getUserData().promoCode != null
}
override fun isNeedToShowViewOnStoriesScreen(): Boolean {
return promotionIsActive()
}
override suspend fun isNeedToShowViewOnMainScreen(): Boolean {
if (!promotionIsActive()) return false
val response = repository.validate(userWalletManager.getWalletId())
return response.valid == true
}
override fun isUserRegisteredInPromotion(): Boolean {
return repository.getUserData().isRegisteredInPromotion
}
override fun getAwardAmount(): Int {
val promoCode = repository.getUserData().promoCode
val awardAmount = promotion.getPromotionInfo().getData(promoCode).award.toInt()
return awardAmount
}
@Throws(IllegalArgumentException::class)
override suspend fun requestAward(): Result<Unit> {
val awardCurrency = getCurrencyForAward()
?: return Result.failure(PromotionError.UnknownError("Currency for award is null"))
val walletId = userWalletManager.getWalletId()
val promoCode = repository.getUserData().promoCode
val error = if (promoCode == null) {
requestAward(walletId, awardCurrency)
} else {
requestAwardWithPromoCode(walletId, awardCurrency, promoCode)
}
return if (error == null) {
Result.success(Unit)
} else {
val domainError = error.toDomainError()
handlePromotionError(domainError)
Result.failure(domainError)
}
}
private suspend fun requestAward(walletId: String, awardCurrency: Currency): AbstractPromotionResponse.Error? {
val validateResponse = repository.validate(walletId)
return if (validateResponse.valid == true) {
val address = getWalletAddressForAward(awardCurrency)
val awardResponse = repository.requestAward(walletId, address)
if (awardResponse.status == true) {
updateUserData { it.copy(isAlreadyReceivedAward = true) }
null
} else {
awardResponse.error
}
} else {
validateResponse.error
}
}
private suspend fun requestAwardWithPromoCode(
walletId: String,
awardCurrency: Currency,
promoCode: String,
): AbstractPromotionResponse.Error? {
val codeValidateResponse = repository.validateCode(walletId, promoCode)
return if (codeValidateResponse.valid == true) {
val address = getWalletAddressForAward(awardCurrency)
val awardWithCodeResponse = repository.requestAwardByCode(walletId, address, promoCode)
if (awardWithCodeResponse.status == true) {
updateUserData { it.copy(isAlreadyReceivedAward = true) }
null
} else {
awardWithCodeResponse.error
}
} else {
codeValidateResponse.error
}
}
private suspend fun getWalletAddressForAward(currency: Currency): String {
val derivationPath = deriveOrAddTokens(currency)
return userWalletManager.getWalletAddress(currency.networkId, derivationPath)
}
private suspend fun deriveOrAddTokens(currency: Currency): String {
val derivationPath = derivationManager.getDerivationPathForBlockchain(currency.networkId)
if (derivationPath.isNullOrEmpty()) error("derivationPath shouldn't be empty")
if (!derivationManager.hasDerivation(currency.networkId, derivationPath)) {
derivationManager.deriveMissingBlockchains(currency)
}
if (!userWalletManager.isTokenAdded(currency, derivationPath)) {
userWalletManager.addToken(currency, derivationPath)
}
return derivationPath
}
private fun handlePromotionError(error: PromotionError) {
when (error) {
is PromotionError.CodeNotFound -> {
updateUserData { it.copy(promoCode = null) }
}
is PromotionError.CardAlreadyHasAward, is PromotionError.WalletAlreadyHasAward,
is PromotionError.CodeWasAlreadyUsed,
-> {
updateUserData { it.copy(isAlreadyReceivedAward = true) }
}
else -> Unit
}
}
override fun buildUriForNewUser(): Uri {
return webViewUriBuilder.buildUriForNewUser()
}
override fun buildUriForOldUser(): Uri {
return webViewUriBuilder.buildUriForOldUser()
}
override fun getBasicAuthHeaders(): ArrayList<String> {
return HeadersConverter().convert(webViewUriBuilder.getBasicAuthHeaders())
}
override fun handleRedirect(uri: Uri): RedirectConsequences {
if (webViewUriBuilder.isReadyForExistedCardAwardRedirect(uri)) {
updateUserData { it.copy(isRegisteredInPromotion = true) }
webViewResultHandler?.handleResult(WebViewResult.ReadyForAward)
return RedirectConsequences.FINISH_SESSION
}
return if (webViewUriBuilder.isPromoCodeRedirect(uri)) {
webViewUriBuilder.extractPromoCode(uri)?.let { code ->
updateUserData {
it.copy(
promoCode = code,
isRegisteredInPromotion = true,
)
}
webViewResultHandler?.handleResult(WebViewResult.PromoCodeReceived)
}
RedirectConsequences.NOTHING
} else {
RedirectConsequences.PROCEED
}
}
private fun promotionIsActive(): Boolean {
val userData = repository.getUserData()
val isActive = when {
userData.isAlreadyReceivedAward -> false
promotion.isError() -> false
else -> {
val data = promotion.getPromotionInfo().getData(userData.promoCode)
data.status == PromotionInfoResponse.Status.ACTIVE
}
}
return isActive
}
private suspend fun initPromotionInfo() {
promotion = repository.getPromotionInfo()
.fold(
onSuccess = { response ->
val responseError = response.error
if (responseError == null) {
val npeMessage = { "Shouldn't be null" }
Promotion(
info = Promotion.PromotionInfo(
newCard = requireNotNull(response.newCard, npeMessage),
oldCard = requireNotNull(response.oldCard, npeMessage),
awardPaymentToken = requireNotNull(response.awardPaymentToken, npeMessage),
),
error = null,
)
} else {
Promotion(
info = null,
error = responseError.toDomainError(),
)
}
},
onFailure = {
Promotion(
info = null,
error = PromotionError.NetworkUnreachable,
)
},
)
}
private fun getCurrencyForAward(): Currency? {
val token = promotion.info?.awardPaymentToken ?: return null
return Currency.NonNativeToken(
id = token.id,
name = token.name,
symbol = token.symbol,
networkId = token.networkId,
contractAddress = token.contractAddress,
decimalCount = token.decimalCount,
)
}
private fun updateUserData(updateBlock: (PromoUserData) -> PromoUserData): PromoUserData {
return updateBlock(repository.getUserData()).apply {
repository.updateUserData(this)
}
}
}
private fun Promotion.PromotionInfo.getData(promoCode: String?): PromotionInfoResponse.Data {
return if (promoCode == null) {
newCard
} else {
oldCard
}
}

View file

@ -0,0 +1,28 @@
package com.tangem.feature.learn2earn.domain
import com.tangem.utils.converter.TwoWayConverter
/**
* A converter that helps prepare a list of headers to be added to the Bundle.
* ArrayList is used to prevent cast exceptions when the convert result is placed in a Bundle.
* @see Learn2earnRouter#openWebView() putStringArrayListExtra
*
[REDACTED_AUTHOR]
*/
internal class HeadersConverter : TwoWayConverter<Map<String, String>, ArrayList<String>> {
override fun convert(value: Map<String, String>): ArrayList<String> {
val list = value.toList().map {
"${it.first}$SPLIT_SIGN${it.second}"
}
return ArrayList(list)
}
override fun convertBack(value: ArrayList<String>): Map<String, String> {
return value.map { it.split(SPLIT_SIGN) }.associate { it[0] to it[1] }
}
private companion object {
const val SPLIT_SIGN = ":"
}
}

View file

@ -0,0 +1,94 @@
package com.tangem.feature.learn2earn.domain
import android.net.Uri
import com.tangem.feature.learn2earn.impl.BuildConfig
/**
[REDACTED_AUTHOR]
*/
internal class WebViewUriBuilder(
private val authCredentialsProvider: () -> String?,
private val userCountryCodeProvider: () -> String,
private val promoCodeProvider: () -> String?,
) {
fun buildUriForNewUser(): Uri {
val builder = makeWebViewUriBuilder()
.appendQueryParameter("type", QUERY_NEW_CARD)
promoCodeProvider.invoke()?.let {
builder.appendQueryParameter("code", it)
}
return builder.build()
}
fun buildUriForOldUser(): Uri {
val builder = makeWebViewUriBuilder()
.appendQueryParameter("type", QUERY_EXISTED_CARD)
return builder.build()
}
fun getBasicAuthHeaders(): Map<String, String> {
val credentials = authCredentialsProvider.invoke()
return if (credentials == null) {
mapOf()
} else {
mapOf("Authorization" to "Basic $credentials")
}
}
fun isPromoCodeRedirect(uri: Uri): Boolean {
return uri.toString().contains(SUFFIX_CODE_CREATED)
}
fun isReadyForExistedCardAwardRedirect(uri: Uri): Boolean {
return uri.toString().endsWith(SUFFIX_READY_FOR_AWARD)
}
fun extractPromoCode(uri: Uri): String? {
val url = uri.toString()
return if (isPromoCodeRedirect(uri)) {
val replaceString = makeWebViewUriBuilder().build().toString() + SUFFIX_CODE_CREATED
val code = url.replace(replaceString, "")
code
} else {
null
}
}
private fun makeWebViewUriBuilder(): Uri.Builder {
val builder = Uri.Builder()
builder.scheme(SCHEME)
if (BuildConfig.DEBUG) {
builder.authority(DEV_BASE_URL)
builder.appendPath(userCountryCodeProvider.invoke())
builder.appendPath(DEV_PATH_PROMOTION)
} else {
builder.authority(BASE_URL)
builder.appendPath(userCountryCodeProvider.invoke())
builder.appendPath(PATH_PROMOTION)
}
return builder
}
private companion object {
const val SCHEME = "https"
const val BASE_URL = "tangem.com"
const val PATH_PROMOTION = "promotion"
const val QUERY_NEW_CARD = "new-card"
const val QUERY_EXISTED_CARD = "existed-card"
const val DEV_BASE_URL = "devweb.tangem.com"
const val DEV_PATH_PROMOTION = "promotion-test"
const val SUFFIX_CODE_CREATED = "/code-created?code="
const val SUFFIX_READY_FOR_AWARD = "/ready-for-existed-card-award"
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.feature.learn2earn.domain.api
/**
* Interface, a wrapper that allows us to get values from the AppStateHolder located in the app module
*
[REDACTED_AUTHOR]
*/
interface Learn2earnDependencyProvider {
fun getUserCountryCodeProvider(): () -> String
fun getWebViewAuthCredentialsProvider(): () -> String?
}

View file

@ -0,0 +1,32 @@
package com.tangem.feature.learn2earn.domain.api
import android.net.Uri
/**
[REDACTED_AUTHOR]
*/
interface Learn2earnInteractor : WebViewRedirectHandler {
var webViewResultHandler: WebViewResultHandler?
suspend fun init()
fun isUserHadPromoCode(): Boolean
fun isNeedToShowViewOnStoriesScreen(): Boolean
suspend fun isNeedToShowViewOnMainScreen(): Boolean
fun isUserRegisteredInPromotion(): Boolean
fun getAwardAmount(): Int
@Throws(IllegalArgumentException::class)
suspend fun requestAward(): Result<Unit>
fun buildUriForNewUser(): Uri
fun buildUriForOldUser(): Uri
fun getBasicAuthHeaders(): ArrayList<String>
}

View file

@ -0,0 +1,19 @@
package com.tangem.feature.learn2earn.domain.api
import android.net.Uri
/**
* Handler that helps determine what actions to take inside the WebView of the Learn2earnWebViewActivity after
* a redirect has been processed by WebViewRedirectHandler
*
[REDACTED_AUTHOR]
*/
interface WebViewRedirectHandler {
fun handleRedirect(uri: Uri): RedirectConsequences
}
enum class RedirectConsequences {
NOTHING,
PROCEED,
FINISH_SESSION,
}

View file

@ -0,0 +1,15 @@
package com.tangem.feature.learn2earn.domain.api
/**
* Handler that helps determine a result of the Learn2earnWebViewActivity webView actions.
*
[REDACTED_AUTHOR]
*/
interface WebViewResultHandler {
fun handleResult(result: WebViewResult)
}
sealed class WebViewResult {
object PromoCodeReceived : WebViewResult()
object ReadyForAward : WebViewResult()
}

View file

@ -0,0 +1,50 @@
package com.tangem.feature.learn2earn.domain.di
import android.content.Context
import com.tangem.feature.learn2earn.data.api.Learn2earnRepository
import com.tangem.feature.learn2earn.domain.DefaultLearn2earnInteractor
import com.tangem.feature.learn2earn.domain.api.Learn2earnDependencyProvider
import com.tangem.feature.learn2earn.domain.api.Learn2earnInteractor
import com.tangem.feature.learn2earn.domain.api.WebViewRedirectHandler
import com.tangem.feature.learn2earn.presentation.Learn2earnRouter
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.UserWalletManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class Learn2earnDomainModule {
@Provides
@Singleton
fun provideRouter(@ApplicationContext context: Context): Learn2earnRouter {
return Learn2earnRouter(context)
}
@Provides
@Singleton
fun provideInteractor(
repository: Learn2earnRepository,
dependencyProvider: Learn2earnDependencyProvider,
userWalletManager: UserWalletManager,
derivationManager: DerivationManager,
): Learn2earnInteractor {
return DefaultLearn2earnInteractor(
repository = repository,
userWalletManager = userWalletManager,
derivationManager = derivationManager,
dependencyProvider = dependencyProvider,
)
}
@Provides
@Singleton
fun provideWebViewHandler(interactor: Learn2earnInteractor): WebViewRedirectHandler {
return interactor
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.feature.learn2earn.domain.models
import com.tangem.datasource.api.promotion.models.PromotionInfoResponse
/**
[REDACTED_AUTHOR]
*/
data class Promotion(
val info: PromotionInfo?,
val error: PromotionError?,
) {
@Throws(NullPointerException::class)
fun getPromotionInfo(): PromotionInfo = info!!
fun isError(): Boolean = error != null
data class PromotionInfo(
val newCard: PromotionInfoResponse.Data,
val oldCard: PromotionInfoResponse.Data,
val awardPaymentToken: PromotionInfoResponse.TokenData,
)
}

View file

@ -0,0 +1,35 @@
package com.tangem.feature.learn2earn.domain.models
import com.tangem.datasource.api.promotion.models.AbstractPromotionResponse
/**
[REDACTED_AUTHOR]
*/
sealed class PromotionError(val code: Int, val description: String) : Throwable(
message = "code: $code, description: $description",
) {
object NetworkUnreachable : PromotionError(code = 0, "Network or service not available")
class CodeNotFound(code: Int, description: String) : PromotionError(code, description)
class CodeWasNotAppliedInShop(code: Int, description: String) : PromotionError(code, description)
class CodeWasAlreadyUsed(code: Int, description: String) : PromotionError(code, description)
class WalletAlreadyHasAward(code: Int, description: String) : PromotionError(code, description)
class CardAlreadyHasAward(code: Int, description: String) : PromotionError(code, description)
class ProgramNotFound(code: Int, description: String) : PromotionError(code, description)
class ProgramWasEnd(code: Int, description: String) : PromotionError(code, description)
class UnknownError(description: String) : PromotionError(code = -1, description)
}
@Suppress("MagicNumber")
internal fun AbstractPromotionResponse.Error.toDomainError(): PromotionError = when (code) {
101 -> PromotionError.CodeNotFound(code, message)
102 -> PromotionError.CodeWasNotAppliedInShop(code, message)
103 -> PromotionError.CodeWasAlreadyUsed(code, message)
104 -> PromotionError.WalletAlreadyHasAward(code, message)
105 -> PromotionError.CardAlreadyHasAward(code, message)
106 -> PromotionError.ProgramNotFound(code, message)
107 -> PromotionError.ProgramWasEnd(code, message)
else -> PromotionError.UnknownError("$code:$message")
}

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