Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-19 18:11:56 +00:00
commit b76e1bbc45
219 changed files with 4585 additions and 5271 deletions

View file

@ -64,6 +64,9 @@ dependencies {
implementation(projects.domain.analytics)
implementation(projects.domain.visa)
implementation(projects.domain.onboarding)
implementation(projects.domain.feedback)
implementation(projects.domain.qrScanning)
implementation(projects.domain.qrScanning.models)
implementation(projects.common)
implementation(projects.core.analytics)
@ -93,6 +96,8 @@ dependencies {
implementation(projects.data.visa)
implementation(projects.data.promo)
implementation(projects.data.onboarding)
implementation(projects.data.feedback)
implementation(projects.data.qrScanning)
/** Features */
implementation(projects.features.onboarding)
@ -110,9 +115,9 @@ dependencies {
implementation(projects.features.wallet.impl)
implementation(projects.features.tokendetails.api)
implementation(projects.features.tokendetails.impl)
implementation(projects.features.send.api)
implementation(projects.features.manageTokens.api)
implementation(projects.features.manageTokens.impl)
implementation(projects.features.send.api)
implementation(projects.features.send.impl)
implementation(projects.features.qrScanning.api)
implementation(projects.features.qrScanning.impl)
@ -183,10 +188,6 @@ dependencies {
implementation(deps.kotsonGson)
implementation(deps.spongecastle.core)
implementation(deps.lottie)
implementation(deps.shopify.buy) {
exclude(group = "com.shopify.graphql.support")
exclude(module = "joda-time")
}
implementation(deps.compose.accompanist.appCompatTheme)
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.compose.accompanist.webView)
@ -230,6 +231,9 @@ dependencies {
implementation(deps.listenableFuture)
implementation(deps.mlKit.barcodeScanning)
/** Leakcanary */
debugImplementation(deps.leakcanary)
/** Excluded dependencies */
implementation("com.google.guava:guava:30.0-android") {
// excludes version 9999.0-empty-to-avoid-conflict-with-guava

View file

@ -130,7 +130,7 @@
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="sell-request.tangem.com"
android:host="redirect_sell"
android:scheme="tangem" />
</intent-filter>

View file

@ -52,9 +52,6 @@ import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.common.shop.googlepay.GooglePayService
import com.tangem.tap.common.shop.googlepay.GooglePayService.Companion.LOAD_PAYMENT_DATA_REQUEST_CODE
import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
@ -64,7 +61,6 @@ import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHan
import com.tangem.tap.features.main.MainViewModel
import com.tangem.tap.features.main.model.Toast
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.features.welcome.ui.WelcomeFragment
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphAction
@ -145,9 +141,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles
@Inject
lateinit var generalUserWalletsListManager: UserWalletsListManager
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode?>
@ -173,8 +166,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
setContentView(R.layout.activity_main)
initContent()
checkGooglePayAvailability()
checkForNotificationPermission()
observeStateUpdates()
@ -256,14 +247,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
private fun checkGooglePayAvailability() {
store.dispatch(
ShopAction.CheckIfGooglePayAvailable(
GooglePayService(createPaymentsClient(this), this),
),
)
}
private fun createAppThemeModeFlow(): SharedFlow<AppThemeMode?> {
val tapApplication = application as TapApplication
@ -377,13 +360,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
super.onActivityResult(requestCode, resultCode, data)
onActivityResultCallbacks.forEach { it(requestCode, resultCode, data) }
when (requestCode) {
LOAD_PAYMENT_DATA_REQUEST_CODE -> {
store.dispatch(
ShopAction.BuyWithGooglePay.HandleGooglePayResponse(resultCode, data),
)
}
}
}
override fun addOnActivityResultCallback(callback: OnActivityResultCallback) {
@ -432,10 +408,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
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 -> {
!backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive -> {
navigateToInitialScreen(intentWhichStartedActivity)
}
backStackIsEmpty -> {

View file

@ -10,8 +10,10 @@ import com.orhanobut.logger.AndroidLogAdapter
import com.orhanobut.logger.Logger
import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.TangemSdkLogger
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.filter.OneTimeEventFilter
@ -57,7 +59,6 @@ import com.tangem.tap.common.log.TimberFormatStrategy
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.tasks.product.DerivationsFinder
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
@ -82,7 +83,6 @@ lateinit var foregroundActivityObserver: ForegroundActivityObserver
lateinit var activityResultCaller: ActivityResultCaller
lateinit var preferencesStorage: PreferencesDataSource
lateinit var walletConnectRepository: WalletConnectRepository
lateinit var shopService: TangemShopService
internal lateinit var derivationsFinder: DerivationsFinder
@HiltAndroidApp
@ -178,6 +178,12 @@ internal class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles
@Inject
lateinit var blockchainSDKLogger: BlockchainSDKLogger
@Inject
lateinit var tangemSdkLogger: TangemSdkLogger
// endregion Injected
override fun onCreate() {
@ -266,6 +272,8 @@ internal class TapApplication : Application(), ImageLoaderFactory {
saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase,
cardRepository = cardRepository,
feedbackManagerFeatureToggles = feedbackManagerFeatureToggles,
tangemSdkLogger = tangemSdkLogger,
blockchainSDKLogger = blockchainSDKLogger,
),
),
)
@ -290,7 +298,6 @@ internal class TapApplication : Application(), ImageLoaderFactory {
}
private fun initWithConfigDependency(config: Config) {
shopService = TangemShopService(this, config.shopify!!)
initAnalytics(this, config)
initFeedbackManager(this, foregroundActivityObserver, store)
}
@ -343,13 +350,17 @@ internal class TapApplication : Application(), ImageLoaderFactory {
Log.Level.View,
Log.Level.Network,
Log.Level.Error,
Log.Level.Biometric,
)
return TangemLogCollector(logLevels, LogFormat.StairsFormatter())
}
val additionalFeedbackInfo = initAdditionalFeedbackInfo(context)
val tangemLogCollector = initTangemLogCollector()
Log.addLogger(tangemLogCollector)
Log.addLogger(
logger = if (feedbackManagerFeatureToggles.isLocalLogsEnabled) tangemSdkLogger else tangemLogCollector,
)
val feedbackManager = FeedbackManager(
infoHolder = additionalFeedbackInfo,

View file

@ -1,28 +0,0 @@
package com.tangem.tap.common.analytics.converters
import com.shopify.buy3.Storefront
import com.tangem.tap.common.analytics.events.Shop
import com.tangem.tap.features.shop.domain.models.ProductType
import com.tangem.utils.converter.Converter
/**
[REDACTED_AUTHOR]
*/
class ShopOrderToEventConverter : Converter<Pair<Storefront.Checkout, ProductType>, Shop.Purchased> {
override fun convert(value: Pair<Storefront.Checkout, ProductType>): Shop.Purchased {
val checkout = value.first
val productType = value.second
val sku = checkout.lineItems?.edges?.firstOrNull()?.node?.variant?.sku ?: productType.sku
val count = when (productType) {
ProductType.WALLET_2_CARDS -> "2"
ProductType.WALLET_3_CARDS -> "3"
}
val amount = "${checkout.totalPriceV2.amount} ${checkout.totalPriceV2.currencyCode.name}"
val code = (checkout.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)
?.code
return Shop.Purchased(sku, count, amount, code)
}
}

View file

@ -17,6 +17,7 @@ import java.io.File
* </intent>
* </queries>
*/
@Deprecated("Use EmailSender instead")
fun Activity.sendEmail(
email: String,
subject: String,

View file

@ -24,7 +24,6 @@ import com.tangem.tap.features.onboarding.products.twins.ui.OnboardingTwinsFragm
import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment
import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.shop.ui.ShopFragment
import com.tangem.tap.features.tokens.impl.presentation.TokensListFragment
import com.tangem.tap.features.welcome.ui.WelcomeFragment
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -122,7 +121,6 @@ fun FragmentActivity.getPreviousScreen(): AppScreen? {
private fun fragmentFactory(screen: AppScreen): Fragment {
return when (screen) {
AppScreen.Home -> HomeFragment()
AppScreen.Shop -> ShopFragment()
AppScreen.OnboardingNote -> OnboardingNoteFragment()
AppScreen.OnboardingWallet -> OnboardingWalletFragment()
AppScreen.OnboardingTwins -> OnboardingTwinsFragment()

View file

@ -73,15 +73,9 @@ class SendTransactionFailedEmail(
class FeedbackEmail : FeedbackData {
override val subjectResId: Int
get() = if (isS2CCard) s2cSubject else tangemSubject
override val mainMessageResId: Int
get() = if (isS2CCard) s2cMainMessage else tangemMainMessage
get() = if (isS2CCard) R.string.feedback_subject_support else R.string.feedback_subject_support_tangem
private val tangemSubject: Int = R.string.feedback_subject_support_tangem
private val tangemMainMessage: Int = R.string.feedback_preface_support
private val s2cSubject: Int = R.string.feedback_subject_support
private val s2cMainMessage: Int = R.string.feedback_preface_support
override val mainMessageResId: Int = R.string.feedback_preface_support
private var isS2CCard = false

View file

@ -12,7 +12,6 @@ import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsReducer
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer
import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.shop.redux.ShopReducer
import com.tangem.tap.features.signin.redux.SignInReducer
import com.tangem.tap.features.tokens.legacy.redux.TokensReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer
@ -37,7 +36,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder)
disclaimerState = DisclaimerReducer.reduce(action, state),
tokensState = TokensReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
shopState = ShopReducer.reduce(action, state.shopState),
welcomeState = WelcomeReducer.reduce(action, state),
saveWalletState = SaveWalletReducer.reduce(action, state),
signInState = SignInReducer.reduce(action, state),

View file

@ -29,8 +29,6 @@ import com.tangem.tap.features.saveWallet.redux.SaveWalletMiddleware
import com.tangem.tap.features.saveWallet.redux.SaveWalletState
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.shop.redux.ShopMiddleware
import com.tangem.tap.features.shop.redux.ShopState
import com.tangem.tap.features.signin.redux.SignInMiddleware
import com.tangem.tap.features.signin.redux.SignInState
import com.tangem.tap.features.tokens.legacy.redux.TokensState
@ -56,7 +54,6 @@ data class AppState(
val disclaimerState: DisclaimerState = DisclaimerState(),
val tokensState: TokensState = TokensState(),
val walletConnectState: WalletConnectState = WalletConnectState(),
val shopState: ShopState = ShopState(),
val welcomeState: WelcomeState = WelcomeState(),
val saveWalletState: SaveWalletState = SaveWalletState(),
val signInState: SignInState = SignInState(),
@ -92,7 +89,6 @@ data class AppState(
DisclaimerMiddleware().disclaimerMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,
ShopMiddleware().shopMiddleware,
WelcomeMiddleware().middleware,
SaveWalletMiddleware().middleware,
LockUserWalletsTimerMiddleware().middleware,

View file

@ -1,222 +0,0 @@
package com.tangem.tap.common.shop
import android.app.Application
import android.content.Intent
import com.google.android.gms.wallet.PaymentData
import com.shopify.buy3.Storefront
import com.tangem.core.analytics.Analytics
import com.tangem.datasource.config.models.ShopifyShop
import com.tangem.tap.common.analytics.converters.ShopOrderToEventConverter
import com.tangem.tap.common.extensions.filterNotNull
import com.tangem.tap.common.shop.data.TangemProduct
import com.tangem.tap.common.shop.data.TotalSum
import com.tangem.tap.common.shop.googlepay.GooglePayService
import com.tangem.tap.common.shop.shopify.ShopifyService
import com.tangem.tap.common.shop.shopify.data.CheckoutItem
import com.tangem.tap.features.shop.domain.models.ProductType
import com.tangem.tap.features.shop.domain.models.ProductType.Companion.SKUS_TO_DISPLAY
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import java.math.BigDecimal
import java.util.Currency
import java.util.UUID
class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
private val shopifyService = ShopifyService(application, shopifyShop)
private val checkouts = mutableMapOf<ProductType, Storefront.Checkout>()
private val variants = mutableMapOf<ProductType, Storefront.ProductVariant>()
private lateinit var googlePayService: GooglePayService
suspend fun getProducts(): Result<List<TangemProduct>> {
val result = shopifyService.getProducts()
return result.mapCatching { product ->
val availableVariants = product
.flatMap { it.variants.edges.map { it.node } }
.filter { SKUS_TO_DISPLAY.contains(it.sku) }
.associateBy { ProductType.fromSku(it.sku) }
.filterNotNull()
variants.putAll(availableVariants)
if (variants.size < SKUS_TO_DISPLAY.size) {
return Result.failure(
Exception(
"Shopify: products are missing, " +
"\nproducts available: ${variants.keys.map { it.sku }}",
),
)
}
val twoCardsProduct = TangemProduct(
type = ProductType.WALLET_2_CARDS,
totalSum = TotalSum(
finalValue = variants[ProductType.WALLET_2_CARDS]?.priceV2?.format(),
beforeDiscount = variants[ProductType.WALLET_2_CARDS]?.compareAtPriceV2?.format(),
),
)
val threeCardsProduct = TangemProduct(
type = ProductType.WALLET_3_CARDS,
totalSum = TotalSum(
finalValue = variants[ProductType.WALLET_3_CARDS]?.priceV2?.format(),
beforeDiscount = variants[ProductType.WALLET_3_CARDS]?.compareAtPriceV2?.format(),
),
)
createCheckouts()
return Result.success(listOf(twoCardsProduct, threeCardsProduct))
}
}
private suspend fun createCheckouts() {
variants.keys.map { coroutineScope { async { createCheckout(it) } } }.awaitAll()
}
private suspend fun createCheckout(productType: ProductType) {
val checkoutItem = CheckoutItem(variants[productType]!!.id, 1)
val result = shopifyService.createCheckout(listOf(checkoutItem))
result.onSuccess { checkout ->
checkouts[productType] = checkout
}
}
suspend fun checkIfGooglePayAvailable(googlePayService: GooglePayService): Result<Boolean> {
this.googlePayService = googlePayService
return googlePayService.checkIfGooglePayAvailable(shopifyService.shop.merchantID != null)
}
fun buyWithGooglePay(productType: ProductType) {
shopifyService.shop.merchantID?.let { merchantId ->
val totalPrice = checkouts[productType]!!.totalPriceV2.amount
googlePayService.payWithGooglePay(
totalPriceCents = totalPrice,
currencyCode = checkouts[productType]!!.currencyCode.name,
merchantID = merchantId,
)
}
}
// fun subscribeToGooglePayResult(
// productType: ProductType,
// resultCallback: (Result<PaymentData>) -> Unit
// ) {
// googlePayService.responseCallback = { result ->
// result.onFailure { }
// result.onSuccess {
// completeTokenizedPayment(it, productType)
// }
// }
// }
suspend fun handleGooglePayResult(resultCode: Int, data: Intent?, productType: ProductType): Result<Unit> {
val result = googlePayService.handleResponseFromGooglePay(resultCode, data)
result.onSuccess {
val finalizePaymentResult = completeTokenizedPayment(it, productType)
finalizePaymentResult.onSuccess {
return Result.success(Unit)
}
return Result.failure(finalizePaymentResult.exceptionOrNull()!!)
}
return Result.failure(result.exceptionOrNull()!!)
}
private suspend fun completeTokenizedPayment(
paymentData: PaymentData,
productType: ProductType,
): Result<Storefront.Checkout> {
val checkout = checkouts[productType]!!
val googlePayResponse =
googlePayService.parsePaymentData(paymentData)
?: return Result.failure(Exception("cannot parse GPay result"))
val amount =
Storefront.MoneyInput(checkout.totalPriceV2.amount, checkout.totalPriceV2.currencyCode)
val idempotencyKey = UUID.randomUUID().toString()
val addressGPay = googlePayResponse.billingAddress
val address = Storefront.MailingAddressInput().apply {
lastName = addressGPay.name
address1 = addressGPay.address1
address2 = addressGPay.address2 + addressGPay.address3
province = addressGPay.administrativeArea
zip = addressGPay.postalCode
phone = addressGPay.phoneNumber
}
val payment = Storefront.TokenizedPaymentInputV3(
amount,
idempotencyKey,
address,
paymentData.toJson(),
Storefront.PaymentTokenType.GOOGLE_PAY,
)
.setTest(true)
return shopifyService.completeWithTokenizedPayment(
payment = payment,
checkoutID = checkout.id,
)
}
suspend fun applyPromoCode(promoCode: String): Result<List<TangemProduct>> {
val products = variants.keys
.map { coroutineScope { async { applyPromoCode(promoCode, it) } } }
.awaitAll()
.map { result -> result.getOrElse { return Result.failure(it) } }
return Result.success(products)
}
private suspend fun applyPromoCode(promoCode: String, productType: ProductType): Result<TangemProduct> {
val checkout = checkouts[productType] ?: return Result.failure(Exception("No checkout"))
val result = if (promoCode.isBlank()) {
shopifyService.removeDiscount(checkout.id)
} else {
shopifyService.applyDiscount(promoCode, checkout.id)
}
result.onSuccess {
checkouts[productType] = it
return Result.success(
TangemProduct(
productType,
TotalSum(
finalValue = it.totalPriceV2.format(),
beforeDiscount = variants[productType]!!.compareAtPriceV2.format(),
),
appliedDiscount = it.getAppliedDiscount(),
),
)
}
return Result.failure(result.exceptionOrNull()!!)
}
fun getCheckoutUrl(productType: ProductType): String {
return checkouts[productType]!!.webUrl
}
suspend fun waitForCheckout(productType: ProductType) {
val result = shopifyService.checkout(true, checkouts[productType]!!.id)
result.onSuccess { checkout ->
if (checkout.order != null && checkout.lineItems != null) {
val event = ShopOrderToEventConverter().convert(checkout to productType)
Analytics.send(event)
}
}
}
}
private fun Storefront.MoneyV2.format(): String {
val currencySymbol = Currency.getInstance(currencyCode.name).symbol
val amountFormatted = BigDecimal(amount).setScale(2)
return currencySymbol + amountFormatted
}
private fun Storefront.Checkout.getAppliedDiscount(): String? {
val discountApplication =
discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication
return discountApplication?.code
}

View file

@ -1,9 +0,0 @@
package com.tangem.tap.common.shop.data
import com.tangem.tap.features.shop.domain.models.ProductType
data class TangemProduct(
val type: ProductType,
val totalSum: TotalSum? = null,
val appliedDiscount: String? = null,
)

View file

@ -1,6 +0,0 @@
package com.tangem.tap.common.shop.data
data class TotalSum(
val finalValue: String? = null,
val beforeDiscount: String? = null,
)

View file

@ -1,147 +0,0 @@
package com.tangem.tap.common.shop.googlepay
import android.app.Activity
import android.app.Activity.RESULT_CANCELED
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.util.Log
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.wallet.*
import com.tangem.common.core.TangemSdkError
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONException
import org.json.JSONObject
import timber.log.Timber
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class GooglePayService(private val paymentsClient: PaymentsClient, private val activity: Activity) {
// var responseCallback: ((Result<PaymentData>) -> Unit)? = null
suspend fun checkIfGooglePayAvailable(isMerchantAvailable: Boolean): Result<Boolean> {
if (!isMerchantAvailable) return Result.success(false)
val isReadyToPayJson = GooglePayUtil.isReadyToPayRequest() ?: return Result.success(false)
val request = IsReadyToPayRequest.fromJson(isReadyToPayJson.toString())
val task = paymentsClient.isReadyToPay(request)
return withContext(Dispatchers.IO) {
suspendCoroutine { continuation ->
task.addOnCompleteListener { completedTask ->
try {
val result = completedTask.getResult(ApiException::class.java)
continuation.resume(Result.success(true))
} catch (exception: ApiException) {
// Process error
Timber.w("isReadyToPay failed: $exception")
continuation.resume(Result.failure(exception))
}
}
}
}
}
fun payWithGooglePay(totalPriceCents: String, currencyCode: String, merchantID: String) {
val paymentDataRequestJson = GooglePayUtil.getPaymentDataRequest(
totalPriceCents,
currencyCode = currencyCode,
countryCode = "RU",
merchantID = merchantID,
)
if (paymentDataRequestJson == null) {
Timber.e("RequestPayment: can't fetch payment data request")
return
}
val request = PaymentDataRequest.fromJson(paymentDataRequestJson.toString())
AutoResolveHelper.resolveTask(
paymentsClient.loadPaymentData(request),
activity,
LOAD_PAYMENT_DATA_REQUEST_CODE,
)
}
fun handleResponseFromGooglePay(resultCode: Int, data: Intent?): Result<PaymentData> {
val result = when (resultCode) {
RESULT_OK -> {
val paymentData = data?.let { intent -> PaymentData.getFromIntent(intent) }
if (paymentData == null) {
Result.failure(Exception("No payment data"))
} else {
Result.success(paymentData)
}
}
RESULT_CANCELED -> {
Result.failure(TangemSdkError.UserCancelled())
}
AutoResolveHelper.RESULT_ERROR -> {
val statusCode = AutoResolveHelper.getStatusFromIntent(data)?.statusCode
if (statusCode == null) {
Result.failure(Exception("Unknown Status"))
} else {
Result.failure(Exception("$statusCode"))
}
}
else -> Result.failure(Exception("Unknown Status"))
}
// responseCallback?.invoke(result)
return result
}
fun parsePaymentData(paymentData: PaymentData): GooglePayResponse? {
val paymentInformation = paymentData.toJson()
try {
// Token will be null if PaymentDataRequest was not constructed using fromJson(String).
val paymentMethodData =
JSONObject(paymentInformation).getJSONObject("paymentMethodData")
val addressJson = paymentMethodData.getJSONObject("info")
.getJSONObject("billingAddress")
val address = Address(
name = addressJson.getString("name"),
postalCode = addressJson.getString("postalCode"),
countryCode = addressJson.getString("countryCode"),
phoneNumber = addressJson.getString("phoneNumber"),
address1 = addressJson.getString("address1"),
address2 = addressJson.getString("address2"),
address3 = addressJson.getString("address3"),
locality = addressJson.getString("locality"),
administrativeArea = addressJson.getString("administrativeArea"),
sortingCode = addressJson.getString("sortingCode"),
)
val token = paymentMethodData
.getJSONObject("tokenizationData")
.getString("token")
return GooglePayResponse(address, token)
} catch (e: JSONException) {
Log.e("handlePaymentSuccess", "Error: " + e.toString())
}
return null
}
companion object {
const val LOAD_PAYMENT_DATA_REQUEST_CODE = 315
}
}
data class GooglePayResponse(
val billingAddress: Address,
val token: String,
)
data class Address(
val name: String,
val postalCode: String,
val countryCode: String,
val phoneNumber: String,
val address1: String,
val address2: String,
val address3: String,
val locality: String,
val administrativeArea: String,
val sortingCode: String,
)

View file

@ -1,133 +0,0 @@
package com.tangem.tap.common.shop.googlepay
import android.app.Activity
import com.google.android.gms.wallet.PaymentsClient
import com.google.android.gms.wallet.Wallet
import com.google.android.gms.wallet.WalletConstants
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
object GooglePayUtil {
private val baseRequest = JSONObject().apply {
put("apiVersion", 2)
put("apiVersionMinor", 0)
}
@Suppress("MagicNumber")
private val allowedCardNetworks = JSONArray(
listOf(
"AMEX",
"DISCOVER",
"INTERAC",
"JCB",
"MASTERCARD",
"VISA",
),
)
private val allowedCardAuthMethods = JSONArray(
listOf(
"PAN_ONLY",
"CRYPTOGRAM_3DS",
),
)
private val merchantInfo: JSONObject = JSONObject().put("merchantName", "Example Merchant")
private fun gatewayTokenizationSpecification(merchantID: String): JSONObject {
return JSONObject().apply {
put("type", "PAYMENT_GATEWAY")
put(
"parameters",
JSONObject(
mapOf(
"gateway" to "shopify",
"gatewayMerchantId" to merchantID,
),
),
)
}
}
private fun baseCardPaymentMethod(): JSONObject {
return JSONObject().apply {
val parameters = JSONObject().apply {
put("allowedAuthMethods", allowedCardAuthMethods)
put("allowedCardNetworks", allowedCardNetworks)
put("billingAddressRequired", true)
put(
"billingAddressParameters",
JSONObject().apply {
put("format", "FULL")
},
)
}
put("type", "CARD")
put("parameters", parameters)
}
}
private fun cardPaymentMethod(merchantID: String): JSONObject {
val cardPaymentMethod = baseCardPaymentMethod()
cardPaymentMethod.put("tokenizationSpecification", gatewayTokenizationSpecification(merchantID))
return cardPaymentMethod
}
fun createPaymentsClient(activity: Activity): PaymentsClient {
val walletOptions = Wallet.WalletOptions.Builder()
.setEnvironment(PAYMENTS_ENVIRONMENT)
.build()
return Wallet.getPaymentsClient(activity, walletOptions)
}
fun isReadyToPayRequest(): JSONObject? {
return try {
baseRequest.apply {
put("allowedPaymentMethods", JSONArray().put(baseCardPaymentMethod()))
}
} catch (e: JSONException) {
null
}
}
private fun getTransactionInfo(price: String, countryCode: String, currencyCode: String): JSONObject {
return JSONObject().apply {
put("totalPrice", price)
put("totalPriceStatus", "FINAL")
put("countryCode", countryCode)
put("currencyCode", currencyCode)
}
}
fun getPaymentDataRequest(
price: String,
countryCode: String,
currencyCode: String,
merchantID: String,
): JSONObject? {
try {
return baseRequest.apply {
put("allowedPaymentMethods", JSONArray().put(cardPaymentMethod(merchantID)))
put("transactionInfo", getTransactionInfo(price, countryCode, currencyCode))
put("merchantInfo", merchantInfo)
// An optional shipping address requirement is a top-level property of the
// PaymentDataRequest JSON object.
val shippingAddressParameters = JSONObject().apply {
put("phoneNumberRequired", false)
// put("allowedCountryCodes", JSONArray(listOf("US", "GB")))
}
put("shippingAddressParameters", shippingAddressParameters)
put("shippingAddressRequired", true)
}
} catch (e: JSONException) {
return null
}
}
}
const val PAYMENTS_ENVIRONMENT = WalletConstants.ENVIRONMENT_TEST

View file

@ -1,267 +0,0 @@
package com.tangem.tap.common.shop.shopify
import android.app.Application
import com.shopify.buy3.GraphCallResult
import com.shopify.buy3.GraphClient
import com.shopify.buy3.RetryHandler
import com.shopify.buy3.Storefront.*
import com.shopify.graphql.support.ID
import com.shopify.graphql.support.Input
import com.tangem.datasource.config.models.ShopifyShop
import com.tangem.tap.common.shop.shopify.data.CheckoutItem
import com.tangem.tap.common.shop.shopify.data.checkoutFieldsFragment
import com.tangem.tap.common.shop.shopify.data.collectionFieldsFragment
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@Suppress("LargeClass")
class ShopifyService(private val application: Application, val shop: ShopifyShop) {
private val client: GraphClient by lazy { initClient() }
@Suppress("MagicNumber")
suspend fun getProducts(collectionTitleFilter: String? = null): Result<List<Product>> {
val filter = collectionTitleFilter?.let { "title:\"$it\"" }
val query = query { rootQuery: QueryRootQuery ->
rootQuery.collections(
{ arg -> arg.first(250).query(filter) },
CollectionConnectionQuery::collectionFieldsFragment,
)
}
return when (val result = queryAsync(query)) {
is GraphCallResult.Success -> {
val products = result.response.data!!.collections.edges
.map { it.node.products }
.flatMap { it.edges }
.map { it.node }
Result.success(products)
}
is GraphCallResult.Failure -> {
Result.failure(result.error)
}
}
}
suspend fun checkout(pollUntilOrder: Boolean, checkoutID: ID): Result<Checkout> {
val query = query { rootQuery: QueryRootQuery ->
rootQuery
.node(checkoutID) { query ->
query.onCheckout { checkoutQuery ->
with(checkoutQuery) {
checkoutFieldsFragment()
}
}
}
}
val retryHandler = RetryHandler.build<QueryRoot>(
delay = 1,
timeUnit = TimeUnit.SECONDS,
) {
this.retryWhen { result ->
when (result) {
is GraphCallResult.Success -> {
val checkout = result.response.data?.node as? Checkout
checkout?.order == null
}
is GraphCallResult.Failure -> false
}
}
}
val result = if (pollUntilOrder) queryAsync(query, retryHandler) else queryAsync(query)
return when (result) {
is GraphCallResult.Success -> {
val checkout = result.response.data!!.node as? Checkout
if (checkout != null) {
Result.success(checkout)
} else {
Result.failure(ShopifyError.Unknown)
}
}
is GraphCallResult.Failure -> {
Result.failure(result.error)
}
}
}
suspend fun createCheckout(checkoutItems: List<CheckoutItem>, checkoutID: ID? = null): Result<Checkout> {
val storefrontLineItems: MutableList<CheckoutLineItemInput> = checkoutItems
.map { CheckoutLineItemInput(it.quantity, it.id) }.toMutableList()
val query = if (checkoutID != null) {
mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutLineItemsReplace(
storefrontLineItems,
checkoutID,
) { payloadQuery: CheckoutLineItemsReplacePayloadQuery ->
payloadQuery
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery.checkoutFieldsFragment()
}
.userErrors { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
} else {
val input = CheckoutCreateInput()
.setLineItemsInput(
Input.value(storefrontLineItems),
)
mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutCreate(
input,
) { payloadQuery: CheckoutCreatePayloadQuery ->
payloadQuery
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery.checkoutFieldsFragment()
}
.checkoutUserErrors { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
}
return runCheckoutMutation(query)
}
suspend fun applyDiscount(discountCode: String, checkoutID: ID): Result<Checkout> {
val query = mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutDiscountCodeApplyV2(
discountCode,
checkoutID,
) { payloadQuery: CheckoutDiscountCodeApplyV2PayloadQuery ->
payloadQuery
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery.checkoutFieldsFragment()
}
.checkoutUserErrors { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
return runCheckoutMutation(query)
}
suspend fun removeDiscount(checkoutID: ID): Result<Checkout> {
val query = mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutDiscountCodeRemove(
checkoutID,
) { payloadQuery: CheckoutDiscountCodeRemovePayloadQuery ->
payloadQuery
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery.checkoutFieldsFragment()
}
.checkoutUserErrors { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
return runCheckoutMutation(query)
}
suspend fun completeWithTokenizedPayment(payment: TokenizedPaymentInputV3, checkoutID: ID): Result<Checkout> {
val query = mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutCompleteWithTokenizedPaymentV3(
checkoutID,
payment,
) { payloadQuery: CheckoutCompleteWithTokenizedPaymentV3PayloadQuery ->
payloadQuery
.payment { paymentQuery: PaymentQuery ->
paymentQuery
.ready()
.errorMessage()
}
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery
.ready()
}
.checkoutUserErrors { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
return runCheckoutMutation(query)
}
private suspend fun runCheckoutMutation(mutation: MutationQuery): Result<Checkout> {
return when (val result = mutationQueryAsync(mutation)) {
is GraphCallResult.Success -> {
val checkout = result.response.data!!.checkoutCreate?.checkout
?: result.response.data!!.checkoutDiscountCodeApplyV2?.checkout
?: result.response.data!!.checkoutDiscountCodeRemove?.checkout
?: result.response.data!!.checkoutShippingAddressUpdateV2?.checkout
?: result.response.data!!.checkoutEmailUpdateV2?.checkout
?: result.response.data!!.checkoutShippingLineUpdate?.checkout
?: result.response.data!!.checkoutCompleteWithTokenizedPaymentV3.checkout
Result.success(checkout)
}
is GraphCallResult.Failure -> Result.failure(result.error)
}
}
private suspend fun queryAsync(
query: QueryRootQuery,
retryHandler: RetryHandler<QueryRoot>,
): GraphCallResult<QueryRoot> = withContext(Dispatchers.IO) {
suspendCoroutine { continuation ->
client.queryGraph(query).enqueue(retryHandler = retryHandler) { result ->
continuation.resume(result)
}
}
}
private suspend fun queryAsync(query: QueryRootQuery): GraphCallResult<QueryRoot> = withContext(Dispatchers.IO) {
suspendCoroutine { continuation ->
client.queryGraph(query).enqueue { result ->
continuation.resume(result)
}
}
}
private suspend fun mutationQueryAsync(query: MutationQuery): GraphCallResult<Mutation> =
withContext(Dispatchers.IO) {
suspendCoroutine { continuation ->
client.mutateGraph(query).enqueue { result ->
continuation.resume(result)
}
}
}
private fun initClient(): GraphClient {
return GraphClient.build(
application,
shop.domain,
shop.storefrontApiKey,
) {
// httpCache(application.filesDir) {
// cacheMaxSizeBytes = (1024 * 1024 * 10)
// defaultCachePolicy =
// HttpCachePolicy.Default.CACHE_FIRST.expireAfter(20, TimeUnit.MINUTES)
// }
}
}
}
sealed class ShopifyError : Throwable() {
object Unknown : ShopifyError()
}

View file

@ -1,135 +0,0 @@
package com.tangem.tap.common.shop.shopify.data
import com.shopify.buy3.Storefront
@Suppress("LongMethod", "MagicNumber")
fun Storefront.CheckoutQuery.checkoutFieldsFragment() {
ready()
webUrl()
currencyCode()
lineItemsSubtotalPrice { it.amount() }
totalPriceV2 {
it.currencyCode()
it.amount()
}
lineItems({ arg -> arg.first(250) }) {
it.edges {
it.node {
// it.id()
it.title()
it.quantity()
it.variant {
it.priceV2 { it.amount() }
}
}
}
}
shippingLine {
it.handle()
it.title()
it.priceV2 { it.amount() }
}
availableShippingRates {
it.ready()
it.shippingRates {
it.handle()
it.title()
it.priceV2 { it.amount() }
}
}
shippingAddress {
it.address1()
it.address2()
it.city()
// .company()
it.country()
// .countryCodeV2()
it.firstName()
// .formatted()
// .formattedArea()
// .id()
it.lastName()
// .latitude()
// .longitude()
// .name()
it.phone()
it.province()
// .provinceCode()
it.zip()
}
discountApplications({ arg -> arg.first(250) }) {
it.edges {
it.node {
it.onDiscountCodeApplication {
it.code()
// .applicable()
// .allocationMethod()
// .targetSelection()
// .targetType()
it.value {
it.onMoneyV2 {
it.amount()
}
it.onPricingPercentageValue {
it.percentage()
}
}
}
}
}
}
order {
it.cancelReason()
it.canceledAt()
it.currencyCode()
// .currentSubtotalPrice()
// .currentTotalDuties()
// .currentTotalPrice()
// .currentTotalTax()
it.customerLocale()
it.customerUrl()
// .discountApplications()
it.edited()
it.email()
it.financialStatus()
it.fulfillmentStatus()
// .id()
// .lineItems()
// .metafield()
// .metafields()
it.name()
it.orderNumber()
// .originalTotalDuties()
// .originalTotalPrice()
it.phone()
it.processedAt()
it.shippingAddress {
it.address1()
it.address2()
it.city()
it.company()
it.country()
it.countryCodeV2()
it.firstName()
it.formatted()
it.formattedArea()
// .id()
it.lastName()
it.latitude()
it.longitude()
it.name()
it.phone()
it.province()
it.provinceCode()
it.zip()
}
// .shippingDiscountAllocations()
it.statusUrl()
// .subtotalPriceV2()
// .successfulFulfillments()
// .totalPriceV2()
// .totalRefundedV2()
// .totalShippingPriceV2()
// .totalTaxV2()
}
}

View file

@ -1,8 +0,0 @@
package com.tangem.tap.common.shop.shopify.data
import com.shopify.graphql.support.ID
data class CheckoutItem(
val id: ID,
val quantity: Int,
)

View file

@ -1,52 +0,0 @@
package com.tangem.tap.common.shop.shopify.data
import com.shopify.buy3.Storefront
@Suppress("MagicNumber")
fun Storefront.CollectionConnectionQuery.collectionFieldsFragment() {
edges { collectionEdgeQuery ->
collectionEdgeQuery
.node { collectionQuery ->
collectionQuery
.title()
.products(
{ arg -> arg.first(250) },
) { productConnectionQuery ->
productConnectionQuery
.edges { productEdgeQuery ->
productEdgeQuery
.node { productQuery ->
productQuery.title()
.productType()
.description()
.variants({ arg -> arg.first(10) }) { variantConnectionQuery ->
variantConnectionQuery.edges { variantQuery ->
variantQuery.node {
it.title()
it.sku()
it.currentlyNotInStock()
it.priceV2 {
it.amount()
it.currencyCode()
}
it.compareAtPriceV2 {
it.amount()
it.currencyCode()
}
it.compareAtPriceV2 {
it.amount()
}
it.product {
it.title()
.productType()
.description()
}
}
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.di.domain
import android.content.Context
import com.tangem.domain.feedback.GetSupportFeedbackEmailUseCase
import com.tangem.domain.feedback.repository.FeedbackRepository
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)
internal object FeedbackDomainModule {
@Provides
@Singleton
fun provideGetFeedbackToSupportUseCase(
feedbackRepository: FeedbackRepository,
@ApplicationContext context: Context,
): GetSupportFeedbackEmailUseCase {
return GetSupportFeedbackEmailUseCase(feedbackRepository = feedbackRepository, resources = context.resources)
}
}

View file

@ -1,7 +1,9 @@
package com.tangem.tap.di.domain
import com.tangem.feature.qrscanning.repo.QrScanningEventsRepository
import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase
import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -17,4 +19,16 @@ internal object QrScanningDomainModule {
fun provideListenToQrScanUseCase(repository: QrScanningEventsRepository): ListenToQrScanningUseCase {
return ListenToQrScanningUseCase(repository)
}
@Provides
@Singleton
fun provideEmitQrScannedEventUseCase(repository: QrScanningEventsRepository): EmitQrScannedEventUseCase {
return EmitQrScannedEventUseCase(repository)
}
@Provides
@Singleton
fun provideParseQrCodeUseCase(repository: QrScanningEventsRepository): ParseQrCodeUseCase {
return ParseQrCodeUseCase(repository)
}
}

View file

@ -120,4 +120,10 @@ internal object SettingsDomainModule {
): ShouldShowSwapPromoTokenUseCase {
return ShouldShowSwapPromoTokenUseCase(swapPromoRepository)
}
@Provides
@ViewModelScoped
fun provideDeleteDeprecatedLogsUseCase(settingsRepository: SettingsRepository): DeleteDeprecatedLogsUseCase {
return DeleteDeprecatedLogsUseCase(settingsRepository)
}
}

View file

@ -278,9 +278,17 @@ internal object TokensDomainModule {
@Provides
@ViewModelScoped
fun provideCheckTokenCompatibilityUseCase(
repository: NetworksCompatibilityRepository,
networksCompatibilityRepository: NetworksCompatibilityRepository,
): CheckCurrencyCompatibilityUseCase {
return CheckCurrencyCompatibilityUseCase(repository)
return CheckCurrencyCompatibilityUseCase(networksCompatibilityRepository)
}
@Provides
@ViewModelScoped
fun provideNeedHardenedDerivationUseCase(
networksCompatibilityRepository: NetworksCompatibilityRepository,
): RequiresHardenedDerivationOnlyUseCase {
return RequiresHardenedDerivationOnlyUseCase(networksCompatibilityRepository)
}
@Provides

View file

@ -3,11 +3,13 @@ package com.tangem.tap.di.domain
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.walletmanager.DefaultWalletManagersFacade
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.feature.onboarding.data.MnemonicRepository
@ -32,6 +34,8 @@ internal object WalletManagersFacadeModule {
mnemonicRepository: MnemonicRepository,
assetReader: AssetReader,
@SdkMoshi moshi: Moshi,
blockchainSDKLogger: BlockchainSDKLogger,
feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles,
): WalletManagersFacade {
return DefaultWalletManagersFacade(
walletManagersStore = walletManagersStore,
@ -42,6 +46,8 @@ internal object WalletManagersFacadeModule {
moshi = moshi,
mnemonic = mnemonicRepository.generateDefaultMnemonic(),
accountCreator = accountCreator,
blockchainSDKLogger = blockchainSDKLogger,
feedbackManagerFeatureToggles = feedbackManagerFeatureToggles,
)
}
}

View file

@ -47,6 +47,11 @@ class TapWalletManager(
config = blockchainSdkConfig,
accountCreator = store.inject(DaggerGraphState::accountCreator),
blockchainDataStorage = store.inject(DaggerGraphState::blockchainDataStorage),
loggers = if (store.inject(DaggerGraphState::feedbackManagerFeatureToggles).isLocalLogsEnabled) {
listOf(store.inject(DaggerGraphState::blockchainSDKLogger))
} else {
emptyList()
},
)
}

View file

@ -30,7 +30,6 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
analyticsEvent: AnalyticsEvent?,
cardId: String?,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
onScanStateChange: suspend (scanInProgress: Boolean) -> Unit,
onWalletNotCreated: suspend () -> Unit,
disclaimerWillShow: () -> Unit,
onFailure: suspend (error: TangemError) -> Unit,
@ -41,7 +40,6 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
analyticsEvent,
cardId,
onProgressStateChange,
onScanStateChange,
onWalletNotCreated,
disclaimerWillShow,
onFailure,
@ -52,7 +50,6 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
analyticsEvent,
cardId,
onProgressStateChange,
onScanStateChange,
onWalletNotCreated,
disclaimerWillShow,
onFailure,

View file

@ -48,14 +48,12 @@ internal object LegacyScanProcessor {
analyticsEvent: AnalyticsEvent?,
cardId: String?,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
onScanStateChange: suspend (scanInProgress: Boolean) -> Unit,
onWalletNotCreated: suspend () -> Unit,
disclaimerWillShow: () -> Unit,
onFailure: suspend (error: TangemError) -> Unit,
onSuccess: suspend (scanResponse: ScanResponse) -> Unit,
) = withMainContext {
onProgressStateChange(true)
onScanStateChange(true)
tangemSdkManager.changeDisplayedCardIdNumbersCount(null)
@ -65,13 +63,11 @@ internal object LegacyScanProcessor {
result
.doOnFailure { error ->
onScanStateChange(false)
onFailure(error)
}
.doOnSuccess { scanResponse ->
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
onScanStateChange(false)
sendAnalytics(analyticsEvent, scanResponse)
showDisclaimerIfNeed(

View file

@ -3,11 +3,15 @@ package com.tangem.tap.domain.scanCard
import arrow.fx.coroutines.resourceScope
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.Basic
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.StateDialog
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.scanCard.chains.*
@ -25,9 +29,15 @@ internal object UseCaseScanProcessor {
allowsRequestAccessCodeFromRepository: Boolean = false,
): CompletionResult<ScanResponse> {
val scanCardUseCase = store.inject(DaggerGraphState::scanCardUseCase)
return scanCardUseCase(cardId, allowsRequestAccessCodeFromRepository)
.fold(
ifLeft = { CompletionResult.Failure(scanCardExceptionConverter.convertBack(it)) },
ifLeft = {
val error = scanCardExceptionConverter.convertBack(it)
Analytics.send(Basic.ScanError(error))
CompletionResult.Failure(error)
},
ifRight = { CompletionResult.Success(it) },
)
}
@ -37,17 +47,14 @@ internal object UseCaseScanProcessor {
analyticsEvent: AnalyticsEvent?,
cardId: String?,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
onScanStateChange: suspend (scanInProgress: Boolean) -> Unit,
onWalletNotCreated: suspend () -> Unit,
disclaimerWillShow: () -> Unit,
onFailure: suspend (error: TangemError) -> Unit,
onSuccess: suspend (scanResponse: ScanResponse) -> Unit,
) = progressScope(onProgressStateChange) {
onScanStateChange(true)
val scanCardUseCase = store.inject(DaggerGraphState::scanCardUseCase)
val chains = buildList {
add(ScanningFinishedChain { onScanStateChange(false) })
add(FailedScansCounterChain(UseCaseScanProcessor::showMaxUnsuccessfulScansReachedDialog))
if (analyticsEvent != null) {
add(AnalyticsChain(analyticsEvent))
}
@ -55,9 +62,14 @@ internal object UseCaseScanProcessor {
add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager))
}
scanCardUseCase(cardId, afterScanChains = chains)
.map { onSuccess(it) }
.mapLeft { proceedWithException(it, onWalletNotCreated, onFailure) }
scanCardUseCase(cardId, afterScanChains = chains).fold(
ifLeft = { proceedWithException(it, onWalletNotCreated, onFailure) },
ifRight = { onSuccess(it) },
)
}
private fun showMaxUnsuccessfulScansReachedDialog() {
store.dispatchDialogShow(StateDialog.ScanFailsDialog)
}
private suspend fun proceedWithException(
@ -75,7 +87,12 @@ internal object UseCaseScanProcessor {
is ScanCardException.UserCancelled,
is ScanCardException.WrongAccessCode,
is ScanCardException.WrongCardId,
-> onFailure(scanCardExceptionConverter.convertBack(exception))
-> {
val error = scanCardExceptionConverter.convertBack(exception)
Analytics.send(Basic.ScanError(error))
onFailure(error)
}
}
}
@ -89,8 +106,9 @@ internal object UseCaseScanProcessor {
navigateTo(exception.onboardingRoute)
onWalletNotCreated()
}
is ScanChainException.DisclaimerWasCanceled,
-> onFailure(scanCardExceptionConverter.convertBack(exception))
is ScanChainException.DisclaimerWasCanceled -> {
onFailure(scanCardExceptionConverter.convertBack(exception))
}
}
}

View file

@ -1,11 +1,11 @@
package com.tangem.tap.domain.scanCard.chains
import arrow.core.Either
import arrow.core.right
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.core.chain.ResultChain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
@ -19,11 +19,9 @@ import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
*/
class AnalyticsChain(
private val event: AnalyticsEvent,
) : Chain<ScanCardException.ChainException, ScanResponse> {
) : ResultChain<ScanCardException, ScanResponse>() {
override suspend fun invoke(
previousChainResult: ScanResponse,
): Either<ScanCardException.ChainException, ScanResponse> {
override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult {
val interceptor = CardContextInterceptor(previousChainResult)
val params = event.params.toMutableMap()
interceptor.intercept(params)

View file

@ -1,6 +1,5 @@
package com.tangem.tap.domain.scanCard.chains
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.core.analytics.Analytics
@ -9,6 +8,7 @@ import com.tangem.domain.card.ScanCardException
import com.tangem.domain.common.TapWorkarounds.canSkipBackup
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.core.chain.ResultChain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.dispatchOnMain
@ -39,11 +39,9 @@ import org.rekotlin.Store
class CheckForOnboardingChain(
private val store: Store<AppState>,
private val tapWalletManager: TapWalletManager,
) : Chain<ScanCardException.ChainException, ScanResponse> {
) : ResultChain<ScanCardException, ScanResponse>() {
override suspend fun invoke(
previousChainResult: ScanResponse,
): Either<ScanCardException.ChainException, ScanResponse> {
override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult {
tapWalletManager.updateConfigManager(previousChainResult)
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(previousChainResult))

View file

@ -1,11 +1,11 @@
package com.tangem.tap.domain.scanCard.chains
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.core.navigation.AppScreen
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.core.chain.ResultChain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
@ -31,11 +31,9 @@ import kotlin.coroutines.resume
internal class DisclaimerChain(
private val store: Store<AppState>,
private val disclaimerWillShow: () -> Unit = {},
) : Chain<ScanCardException.ChainException, ScanResponse> {
) : ResultChain<ScanCardException, ScanResponse>() {
override suspend fun invoke(
previousChainResult: ScanResponse,
): Either<ScanCardException.ChainException, ScanResponse> {
override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult {
val disclaimer = previousChainResult.card.createDisclaimer()
return if (disclaimer.isAccepted()) {
@ -46,10 +44,7 @@ internal class DisclaimerChain(
}
}
private suspend fun showDisclaimer(
disclaimer: Disclaimer,
response: ScanResponse,
): Either<ScanCardException.ChainException, ScanResponse> {
private suspend fun showDisclaimer(disclaimer: Disclaimer, response: ScanResponse): ScanChainResult {
store.dispatchOnMain(DisclaimerAction.SetDisclaimer(disclaimer))
return suspendCancellableCoroutine { continuation ->

View file

@ -0,0 +1,29 @@
package com.tangem.tap.domain.scanCard.chains
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.models.scan.ScanResponse
internal class FailedScansCounterChain(
private val onMaxUnsuccessfulScansReached: () -> Unit,
private val maxUnsuccessfulScans: Int = 3,
) : Chain<ScanCardException, ScanResponse> {
override suspend fun launch(previousChainResult: ScanChainResult): ScanChainResult {
if (previousChainResult.isLeft()) {
unsuccessfulScansCounter = unsuccessfulScansCounter.inc().coerceAtMost(maxUnsuccessfulScans)
if (unsuccessfulScansCounter == maxUnsuccessfulScans) {
onMaxUnsuccessfulScansReached()
}
} else {
unsuccessfulScansCounter = 0
}
return previousChainResult
}
private companion object {
var unsuccessfulScansCounter = 0
}
}

View file

@ -8,7 +8,11 @@ sealed class ScanChainException : ScanCardException.ChainException() {
/**
* May be returned from [DisclaimerChain]
* */
object DisclaimerWasCanceled : ScanChainException()
data object DisclaimerWasCanceled : ScanChainException() {
@Suppress("UnusedPrivateMember")
private fun readResolve(): Any = DisclaimerWasCanceled
}
/**
* May be returned from [CheckForOnboardingChain]

View file

@ -0,0 +1,7 @@
package com.tangem.tap.domain.scanCard.chains
import arrow.core.Either
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.models.scan.ScanResponse
internal typealias ScanChainResult = Either<ScanCardException, ScanResponse>

View file

@ -1,25 +0,0 @@
package com.tangem.tap.domain.scanCard.chains
import arrow.core.Either
import arrow.core.right
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.models.scan.ScanResponse
/**
* Responsible for invoking the callback at the end of the card scanning operation. Should be passed as last chain in
* after card scanning chains. Always returns result of previous chain.
*
* @param onScanningFinished a suspending function to be called when the scanning process has finished.
*
* @see Chain for more information about the Chain interface.
*/
internal class ScanningFinishedChain(
private val onScanningFinished: suspend () -> Unit,
) : Chain<ScanCardException.ChainException, ScanResponse> {
override suspend fun invoke(previousChainResult: ScanResponse): Either<ScanChainException, ScanResponse> {
onScanningFinished()
return previousChainResult.right()
}
}

View file

@ -1,32 +1,27 @@
package com.tangem.tap.domain.scanCard.repository
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.common.CompletionResult
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.card.repository.ScanCardRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter
// TODO: Move to the :data:card module
internal class DefaultScanCardRepository(
private val tangemSdkManager: TangemSdkManager,
) : ScanCardRepository {
private val exceptionConverter = ScanCardExceptionConverter()
override suspend fun scanCard(
cardId: String?,
allowRequestAccessCodeFromStorage: Boolean,
): Either<ScanCardException, ScanResponse> = either {
when (
override suspend fun scanCard(cardId: String?, allowRequestAccessCodeFromStorage: Boolean): ScanResponse {
return when (
val result = tangemSdkManager.scanProduct(
cardId = cardId,
allowsRequestAccessCodeFromRepository = allowRequestAccessCodeFromStorage,
)
) {
is CompletionResult.Success -> result.data
is CompletionResult.Failure -> raise(exceptionConverter.convert(result.error))
is CompletionResult.Failure -> throw exceptionConverter.convert(result.error)
}
}
}

View file

@ -18,4 +18,5 @@ internal data class UserWalletPublicInformation(
val cardsInWallet: Set<String>,
val scanResponse: ScanResponse,
val isMultiCurrency: Boolean,
val hasBackupError: Boolean = false,
)

View file

@ -20,6 +20,7 @@ internal val UserWallet.publicInformation: UserWalletPublicInformation
wallets = emptyList(),
),
),
hasBackupError = hasBackupError,
)
internal fun UserWalletPublicInformation.toUserWallet(): UserWallet {
@ -30,6 +31,7 @@ internal fun UserWalletPublicInformation.toUserWallet(): UserWallet {
cardsInWallet = cardsInWallet,
scanResponse = scanResponse,
isMultiCurrency = isMultiCurrency,
hasBackupError = hasBackupError,
)
}

View file

@ -291,12 +291,12 @@ enum class DerivationPathSelectorType {
internal sealed class AddCustomTokenWarning(val description: TextReference) {
/** Potential scam warning */
object PotentialScamToken : AddCustomTokenWarning(
data object PotentialScamToken : AddCustomTokenWarning(
description = TextReference.Res(R.string.custom_token_validation_error_not_found),
)
/** Token already added warning */
object TokenAlreadyAdded : AddCustomTokenWarning(
data object TokenAlreadyAdded : AddCustomTokenWarning(
description = TextReference.Res(R.string.custom_token_validation_error_already_added),
)
@ -305,7 +305,7 @@ internal sealed class AddCustomTokenWarning(val description: TextReference) {
description = TextReference.Res(R.string.alert_manage_tokens_unsupported_message, networkName),
)
object WrongDerivationPath : AddCustomTokenWarning(
data object WrongDerivationPath : AddCustomTokenWarning(
description = TextReference.Res(R.string.custom_token_invalid_derivation_path),
)
}

View file

@ -15,7 +15,6 @@ import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.HDWalletError
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.*
import com.tangem.domain.common.util.cardTypesResolver
@ -674,7 +673,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
private fun createDerivationPathOrNull(rawPath: String): DerivationPath? {
return try {
DerivationPath(rawPath)
} catch (error: HDWalletError) {
} catch (error: Throwable) {
null
}
}

View file

@ -13,12 +13,12 @@ import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.walletconnect.WalletConnectActions
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.qrscanning.SourceType
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState

View file

@ -5,8 +5,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.email.EmailSender
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.feedback.GetSupportFeedbackEmailUseCase
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.features.details.redux.DetailsState
@ -24,11 +27,26 @@ internal class DetailsFragment : ComposeFragment(), StoreSubscriber<DetailsState
@Inject
lateinit var walletsRepository: WalletsRepository
@Inject
lateinit var feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles
@Inject
lateinit var getSupportFeedbackEmailUseCase: GetSupportFeedbackEmailUseCase
@Inject
lateinit var emailSender: EmailSender
private lateinit var detailsViewModel: DetailsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
detailsViewModel = DetailsViewModel(store, walletsRepository)
detailsViewModel = DetailsViewModel(
store = store,
walletsRepository = walletsRepository,
feedbackManagerFeatureToggles = feedbackManagerFeatureToggles,
getSupportFeedbackEmailUseCase = getSupportFeedbackEmailUseCase,
emailSender = emailSender,
)
Analytics.send(Settings.ScreenOpened())
}

View file

@ -7,11 +7,14 @@ import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.email.EmailSender
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.feedback.GetSupportFeedbackEmailUseCase
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.addContext
@ -25,6 +28,7 @@ import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.home.LocaleRegionProvider
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.mainScope
import com.tangem.tap.scope
import com.tangem.tap.userWalletsListManager
import com.tangem.wallet.BuildConfig
@ -36,6 +40,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.rekotlin.Store
import timber.log.Timber
@ -43,6 +48,9 @@ import timber.log.Timber
internal class DetailsViewModel(
private val store: Store<AppState>,
private val walletsRepository: WalletsRepository,
private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles,
private val getSupportFeedbackEmailUseCase: GetSupportFeedbackEmailUseCase,
private val emailSender: EmailSender,
) {
var detailsScreenState: MutableState<DetailsScreenState> = mutableStateOf(updateState(store.state.detailsState))
@ -139,8 +147,22 @@ internal class DetailsViewModel(
private fun sendFeedback() {
Analytics.send(Basic.ButtonSupport())
if (feedbackManagerFeatureToggles.isLocalLogsEnabled) {
mainScope.launch {
val email = getSupportFeedbackEmailUseCase()
emailSender.send(
email = EmailSender.Email(
address = email.address,
subject = email.subject,
message = email.message,
attachment = email.file,
),
)
}
} else {
store.dispatchOnMain(GlobalAction.SendEmail(FeedbackEmail()))
}
}
private fun navigateToAppSettings() {
Analytics.send(Settings.ButtonAppSettings())

View file

@ -2,8 +2,8 @@ package com.tangem.tap.features.details.ui.walletconnect
import androidx.lifecycle.*
import arrow.core.getOrElse
import com.tangem.feature.qrscanning.SourceType
import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState

View file

@ -7,7 +7,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.qrscanning.SourceType
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.store

View file

@ -3,7 +3,6 @@ package com.tangem.tap.features.home.redux
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.tap.common.entities.IndeterminateProgressButton
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
@ -29,6 +28,4 @@ sealed class HomeAction : Action {
data class GoToShop(val userCountryCode: String?) : HomeAction()
data class UpdateCountryCode(val userCountryCode: String) : HomeAction()
data class ChangeScanCardButtonState(val state: IndeterminateProgressButton) : HomeAction()
}

View file

@ -13,12 +13,10 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.analytics.events.Shop
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.features.signin.redux.SignInAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -31,6 +29,8 @@ import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
private const val HIDE_PROGRESS_DELAY = 400L
object HomeMiddleware {
val handler = homeMiddleware
@ -84,17 +84,16 @@ private suspend fun readCard(analyticsEvent: AnalyticsEvent?) {
analyticsEvent = analyticsEvent,
onProgressStateChange = { showProgress ->
if (showProgress) {
changeButtonState(ButtonState.PROGRESS)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = true))
} else {
changeButtonState(ButtonState.ENABLED)
delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
}
},
onScanStateChange = { scanInProgress ->
store.dispatch(HomeAction.ScanInProgress(scanInProgress))
},
onFailure = {
Timber.e(it, "Unable to scan card")
changeButtonState(ButtonState.ENABLED)
delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
},
onSuccess = { scanResponse ->
proceedWithScanResponse(scanResponse)
@ -113,7 +112,7 @@ private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch {
Timber.e(error, "Unable to save user wallet")
}
.doOnSuccess {
scope.launch { store.onUserWalletSelected(userWallet = userWallet) }
scope.launch { store.onUserWalletSelected(userWallet) }
}
.doOnResult {
store.dispatchOnMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Card))
@ -123,10 +122,6 @@ private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch {
private suspend fun navigateTo(appScreen: AppScreen) {
store.dispatchOnMain(NavigationAction.NavigateTo(appScreen))
delay(timeMillis = 200)
changeButtonState(ButtonState.ENABLED)
}
private fun changeButtonState(state: ButtonState) {
store.dispatchOnMain(HomeAction.ChangeScanCardButtonState(IndeterminateProgressButton(state)))
delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
}

View file

@ -22,9 +22,6 @@ private fun internalReduce(action: Action, appState: AppState): HomeState {
is HomeAction.ScanInProgress -> {
state = state.copy(scanInProgress = action.scanInProgress)
}
is HomeAction.ChangeScanCardButtonState -> {
state = state.copy(btnScanState = action.state)
}
is HomeAction.UpdateCountryCode -> {
state.onCountryCodeUpdate(state, action.userCountryCode)
}

View file

@ -1,15 +1,14 @@
package com.tangem.tap.features.home.redux
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.features.send.redux.states.ButtonState
import org.rekotlin.StateType
import java.util.Locale
@Immutable
data class HomeState(
val scanInProgress: Boolean = false,
val btnScanState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.ENABLED),
val stories: List<Stories> = initDefaultStories(),
) : StateType {

View file

@ -10,6 +10,7 @@ import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase
import com.tangem.tap.features.main.model.MainScreenState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
@ -17,12 +18,14 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@HiltViewModel
internal class MainViewModel @Inject constructor(
private val updateBalanceHidingSettingsUseCase: UpdateBalanceHidingSettingsUseCase,
private val listenToFlipsUseCase: ListenToFlipsUseCase,
private val reduxNavController: ReduxNavController,
private val fetchAppCurrenciesUseCase: FetchAppCurrenciesUseCase,
private val deleteDeprecatedLogsUseCase: DeleteDeprecatedLogsUseCase,
private val dispatchers: CoroutineDispatcherProvider,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel(), MainIntents {
@ -41,6 +44,10 @@ internal class MainViewModel @Inject constructor(
observeFlips()
displayBalancesHidingStatusToast()
displayHiddenBalancesModalNotification()
viewModelScope.launch(dispatchers.main) {
deleteDeprecatedLogsUseCase()
}
}
private fun updateAppCurrencies() {

View file

@ -72,13 +72,14 @@ object OnboardingHelper {
scanResponse: ScanResponse,
accessCode: String? = null,
backupCardsIds: List<String>? = null,
hasBackupError: Boolean = false,
) {
Analytics.setContext(scanResponse)
scope.launch {
when {
// When should save user wallets, then save card without navigate to save wallet screen
store.inject(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> {
proceedWithScanResponse(scanResponse, backupCardsIds)
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
store.dispatchOnMain(
SaveWalletAction.ProvideBackupInfo(
@ -92,7 +93,7 @@ object OnboardingHelper {
// When should not save user wallets but device has biometry and save wallet screen has not been shown,
// then open save wallet screen
tangemSdkManager.canUseBiometry && preferencesStorage.shouldShowSaveUserWalletScreen -> {
proceedWithScanResponse(scanResponse, backupCardsIds)
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
delay(timeMillis = 1_200)
@ -109,7 +110,7 @@ object OnboardingHelper {
}
// If device has no biometry and save wallet screen has been shown, then go through old scenario
else -> {
proceedWithScanResponse(scanResponse, backupCardsIds)
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
@ -129,8 +130,13 @@ object OnboardingHelper {
}
}
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse, backupCardsIds: List<String>?) {
val userWallet = UserWalletBuilder(scanResponse)
private suspend fun proceedWithScanResponse(
scanResponse: ScanResponse,
backupCardsIds: List<String>?,
hasBackupError: Boolean,
) {
val userWallet = UserWalletBuilder(scanResponse = scanResponse)
.hasBackupError(hasBackupError)
.backupCardsIds(backupCardsIds?.toSet())
.build()
.guard {

View file

@ -9,23 +9,23 @@ import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
sealed class OnboardingWalletAction : Action {
object Init : OnboardingWalletAction()
object GetToCreateWalletStep : OnboardingWalletAction()
object CreateWallet : OnboardingWalletAction()
data object Init : OnboardingWalletAction()
data object GetToCreateWalletStep : OnboardingWalletAction()
data object CreateWallet : OnboardingWalletAction()
data class WalletWasCreated(
val shouldSendAnalyticsEvent: Boolean,
val result: CompletionResult<CreateProductWalletTaskResponse>,
) : OnboardingWalletAction()
object Done : OnboardingWalletAction()
data object Done : OnboardingWalletAction()
data class FinishOnboarding(val scope: CoroutineScope) : OnboardingWalletAction()
object ResumeBackup : OnboardingWalletAction()
data object ResumeBackup : OnboardingWalletAction()
data class LoadArtwork(val cardArtworkUriForUnfinishedBackup: Uri? = null) : OnboardingWalletAction()
class SetArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction()
object OnBackPressed : OnboardingWalletAction()
data object OnBackPressed : OnboardingWalletAction()
}
sealed class OnboardingWallet2Action : OnboardingWalletAction() {
@ -49,45 +49,46 @@ sealed class OnboardingWallet2Action : OnboardingWalletAction() {
sealed class BackupAction : Action {
object IntroduceBackup : BackupAction()
object StartBackup : BackupAction()
object SkipBackup : BackupAction()
data object IntroduceBackup : BackupAction()
data object StartBackup : BackupAction()
data object SkipBackup : BackupAction()
object StartAddingPrimaryCard : BackupAction()
object ScanPrimaryCard : BackupAction()
data object ErrorInBackupCard : BackupAction()
data object StartAddingPrimaryCard : BackupAction()
data object ScanPrimaryCard : BackupAction()
/**
* Check for unfinished backup of standard Wallets
* See more GlobalAction.Onboarding.StartForUnfinishedBackup
*/
object CheckForUnfinishedBackup : BackupAction()
data object CheckForUnfinishedBackup : BackupAction()
object StartAddingBackupCards : BackupAction()
object AddBackupCard : BackupAction() {
object Success : BackupAction()
data object StartAddingBackupCards : BackupAction()
data object AddBackupCard : BackupAction() {
data object Success : BackupAction()
data class ChangeButtonLoading(val isLoading: Boolean) : BackupAction()
}
object FinishAddingBackupCards : BackupAction()
data object FinishAddingBackupCards : BackupAction()
object ShowAccessCodeInfoScreen : BackupAction()
object ShowEnterAccessCodeScreen : BackupAction()
data object ShowAccessCodeInfoScreen : BackupAction()
data object ShowEnterAccessCodeScreen : BackupAction()
data class CheckAccessCode(val accessCode: String) : BackupAction()
data class SetAccessCodeError(val error: AccessCodeError?) : BackupAction()
data class SaveFirstAccessCode(val accessCode: String) : BackupAction()
data class SaveAccessCodeConfirmation(val accessCodeConfirmation: String) : BackupAction()
object OnAccessCodeDialogClosed : BackupAction()
data object OnAccessCodeDialogClosed : BackupAction()
object PrepareToWritePrimaryCard : BackupAction()
object WritePrimaryCard : BackupAction()
data object PrepareToWritePrimaryCard : BackupAction()
data object WritePrimaryCard : BackupAction()
data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction()
data class WriteBackupCard(val cardNumber: Int) : BackupAction()
data class FinishBackup(val withAnalytics: Boolean = true) : BackupAction()
object DiscardBackup : BackupAction()
object DiscardSavedBackup : BackupAction()
object ResumeFoundUnfinishedBackup : BackupAction()
data object DiscardBackup : BackupAction()
data object DiscardSavedBackup : BackupAction()
data object ResumeFoundUnfinishedBackup : BackupAction()
data class ResetBackupCard(val cardId: String) : BackupAction()
}

View file

@ -20,6 +20,7 @@ import com.tangem.domain.userwallets.Artwork
import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.feature.wallet.presentation.wallet.domain.BackupValidator
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.operations.backup.BackupService
import com.tangem.tap.*
@ -173,6 +174,7 @@ private fun handleWalletAction(action: Action) {
scanResponse = updatedScanResponse,
accessCode = backupState.accessCode,
backupCardsIds = backupState.backupCardIds,
hasBackupError = backupState.hasBackupError,
)
}
}
@ -484,6 +486,10 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
backupService.proceedBackup { result ->
when (result) {
is CompletionResult.Success -> {
val backupValidator = BackupValidator()
if (!backupValidator.isValid(CardDTO(result.data))) {
store.dispatchOnMain(BackupAction.ErrorInBackupCard)
}
if (backupService.currentState == BackupService.State.Finished) {
store.dispatchOnMain(BackupAction.FinishBackup())
} else {

View file

@ -112,6 +112,7 @@ private object BackupReducer {
} else {
state.copy(backupStep = BackupStep.WriteBackupCard(action.cardNumber))
}
is BackupAction.ErrorInBackupCard -> state.copy(hasBackupError = true)
is BackupAction.SkipBackup -> state.copy(backupStep = BackupStep.Finished)
is BackupAction.FinishBackup -> state.copy(backupStep = BackupStep.Finished)
BackupAction.OnAccessCodeDialogClosed -> state.copy(backupStep = BackupStep.AddBackupCards)

View file

@ -70,6 +70,7 @@ data class BackupState(
val canSkipBackup: Boolean = true,
val isInterruptedBackup: Boolean = false,
val showBtnLoading: Boolean = false,
val hasBackupError: Boolean = false,
)
enum class AccessCodeError {

View file

@ -25,10 +25,14 @@ import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.qrscanning.models.QrResult
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.qrscanning.SourceType
import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase
import com.tangem.features.send.api.navigation.SendRouter.Companion.CRYPTO_CURRENCY_KEY
import com.tangem.sdk.extensions.hideSoftKeyboard
import com.tangem.tap.common.KeyboardObserver
import com.tangem.tap.common.analytics.events.Token
@ -60,6 +64,7 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.text.DecimalFormatSymbols
import javax.inject.Inject
@ -82,11 +87,17 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
private val sendSubscriber = SendStateSubscriber(this)
private lateinit var keyboardObserver: KeyboardObserver
private val cryptoCurrency: CryptoCurrency?
get() = arguments?.getParcelable(CRYPTO_CURRENCY_KEY)
val binding: FragmentSendBinding by viewBinding(FragmentSendBinding::bind)
@Inject
lateinit var listenToQrScanningUseCase: ListenToQrScanningUseCase
@Inject
lateinit var parseQrCodeUseCase: ParseQrCodeUseCase
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(viewModel)
@ -177,13 +188,21 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
listenToQrScanningUseCase(SourceType.SEND)
.getOrElse { emptyFlow() }
.flowWithLifecycle(this@SendFragment.lifecycle, minActiveState = Lifecycle.State.CREATED)
.collect {
.collect { rawQr ->
delay(200)
// Delayed launch is needed in order for the UI to be drawn and to process the sent events.
// If do not use the delay, then etAmount error field is not displayed when
// inserting an incorrect amount by shareUri
onCodeScanned(it)
cryptoCurrency?.let { cryptoCurrency ->
parseQrCodeUseCase(rawQr, cryptoCurrency = cryptoCurrency).fold(
ifLeft = {
onCodeScanned(QrResult(address = rawQr))
Timber.w(it)
},
ifRight = { onCodeScanned(it) },
)
} ?: onCodeScanned(QrResult(address = rawQr))
}
}
}
@ -254,15 +273,18 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
.launchIn(mainScope)
}
private fun onCodeScanned(scannedCode: String) {
if (scannedCode.isEmpty()) return
private fun onCodeScanned(parsedQr: QrResult) {
if (parsedQr.address.isEmpty()) return
store.dispatch(
PasteAddress(
data = scannedCode,
data = parsedQr.address,
sourceType = Token.Send.AddressEntered.SourceType.QRCode,
),
)
parsedQr.amount?.let { amount ->
store.dispatchOnMain(AmountAction.SetAmount(amount, isUserInput = false))
}
store.dispatch(TruncateOrRestore(!binding.lSendAddress.etAddress.isFocused))
}

View file

@ -2,17 +2,20 @@ package com.tangem.tap.features.send.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.store
import com.tangem.wallet.R
object KaspaWarningDialog {
fun create(context: Context, dialog: SendAction.Dialog.KaspaWarningDialog): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.common_warning)
setMessage(
context.getString(
R.string.kaspa_withdrawal_message_warning,
R.string.common_utxo_validate_withdrawal_message_warning,
Blockchain.Kaspa.fullName,
dialog.maxOutputs,
dialog.maxAmount.toPlainString(),
),
@ -23,7 +26,6 @@ object KaspaWarningDialog {
setOnDismissListener {
store.dispatch(SendAction.Dialog.Hide)
}
}
.create()
}.create()
}
}

View file

@ -1,59 +0,0 @@
package com.tangem.tap.features.shop.data
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.ShopResponse
import com.tangem.domain.common.extensions.withIOContext
import com.tangem.tap.features.shop.domain.ShopRepository
import com.tangem.tap.features.shop.domain.models.SalesProduct
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import timber.log.Timber
import java.util.Locale
/**
* Default implementation of shop feature repository
*
* @property tangemTechApi TangemTech API
* @property dispatchers coroutine dispatchers provider
*
[REDACTED_AUTHOR]
*/
internal class DefaultShopRepository(
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
) : ShopRepository {
private val salesProductConverter = SalesProductConverter()
override suspend fun isShopifyOrderingAvailable(): Boolean {
return runCatching(dispatchers.io) { tangemTechApi.getShopInfo(name = SHOPIFY_NAME) }
.fold(
onSuccess = ShopResponse::isOrderingAvailable,
onFailure = {
Timber.e("Server error. isShopifyOrderingAvailable returns default value (true)")
true
},
)
}
override suspend fun getSalesProductInfo(): List<SalesProduct> {
return withIOContext {
val salesInfo = tangemTechApi.getSalesInfo(locale = getLocaleName(), shops = SHOPIFY_NAME)
salesProductConverter.convert(salesInfo)
}
}
private fun getLocaleName(): String {
return if (Locale.getDefault().language == "ru") {
RU_LOCALE
} else {
EN_LOCALE
}
}
private companion object {
private const val SHOPIFY_NAME = "shopify"
private const val RU_LOCALE = "ru"
private const val EN_LOCALE = "en"
}
}

View file

@ -1,40 +0,0 @@
package com.tangem.tap.features.shop.data
import com.tangem.datasource.api.tangemTech.models.SalesResponse
import com.tangem.tap.features.shop.domain.models.Notification
import com.tangem.tap.features.shop.domain.models.ProductState
import com.tangem.tap.features.shop.domain.models.ProductType
import com.tangem.tap.features.shop.domain.models.SalesProduct
import com.tangem.utils.converter.Converter
internal class SalesProductConverter : Converter<SalesResponse, List<SalesProduct>> {
override fun convert(value: SalesResponse): List<SalesProduct> {
return value.sales.map { sales ->
val productState = when (sales.state) {
"order" -> ProductState.ORDER
"pre-order" -> ProductState.PRE_ORDER
"sold-out" -> ProductState.SOLD_OUT
else -> ProductState.SOLD_OUT
}
val productType = when (sales.product.code) {
"pack2" -> ProductType.WALLET_2_CARDS
"pack3" -> ProductType.WALLET_3_CARDS
else -> ProductType.WALLET_3_CARDS
}
SalesProduct(
id = sales.id,
productType = productType,
state = productState,
name = sales.product.name,
notification = sales.notification?.let { notification ->
Notification(
type = notification.type,
title = notification.title,
description = notification.description,
)
},
)
}
}
}

View file

@ -1,44 +0,0 @@
package com.tangem.tap.features.shop.di
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.tap.features.shop.data.DefaultShopRepository
import com.tangem.tap.features.shop.domain.DefaultShopifyOrderingAvailabilityUseCase
import com.tangem.tap.features.shop.domain.GetShopifySalesProductsUseCase
import com.tangem.tap.features.shop.domain.ShopRepository
import com.tangem.tap.features.shop.domain.ShopifyOrderingAvailabilityUseCase
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ViewModelComponent::class)
internal object ShopUseCaseModule {
@Provides
@ViewModelScoped
fun provideShopifyOrderingAvailabilityUseCase(shopRepository: ShopRepository): ShopifyOrderingAvailabilityUseCase {
return DefaultShopifyOrderingAvailabilityUseCase(
shopRepository = shopRepository,
)
}
@Provides
@ViewModelScoped
fun provideGetShopifySalesProductsUseCase(shopRepository: ShopRepository): GetShopifySalesProductsUseCase {
return GetShopifySalesProductsUseCase(
shopRepository = shopRepository,
)
}
@Provides
@ViewModelScoped
fun provideDefaultShopRepository(
tangemTechApi: TangemTechApi,
dispatchers: CoroutineDispatcherProvider,
): ShopRepository {
return DefaultShopRepository(tangemTechApi = tangemTechApi, dispatchers = dispatchers)
}
}

View file

@ -1,23 +0,0 @@
package com.tangem.tap.features.shop.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.tap.features.shop.toggles.DefaultShopifyFeatureToggleManager
import com.tangem.tap.features.shop.toggles.ShopifyFeatureToggleManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object ShopifyTogglesModule {
@Provides
@Singleton
fun provideDefaultShopifyFeatureToggleManager(
featureToggleManager: FeatureTogglesManager,
): ShopifyFeatureToggleManager {
return DefaultShopifyFeatureToggleManager(featureToggleManager)
}
}

View file

@ -1,15 +0,0 @@
package com.tangem.tap.features.shop.domain
/**
* Default implementation of use case to define shopify ordering availability
*
* @property shopRepository shop feature repository
*
[REDACTED_AUTHOR]
*/
internal class DefaultShopifyOrderingAvailabilityUseCase(
private val shopRepository: ShopRepository,
) : ShopifyOrderingAvailabilityUseCase {
override suspend fun invoke() = shopRepository.isShopifyOrderingAvailable()
}

View file

@ -1,25 +0,0 @@
package com.tangem.tap.features.shop.domain
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.tap.features.shop.domain.models.SalesError
import com.tangem.tap.features.shop.domain.models.SalesProduct
/**
* Use case to get shopify available products
*
* @property shopRepository shop feature repository
*/
class GetShopifySalesProductsUseCase(
private val shopRepository: ShopRepository,
) {
suspend operator fun invoke(): Either<SalesError, List<SalesProduct>> {
return try {
shopRepository.getSalesProductInfo().right()
} catch (e: Exception) {
SalesError.DataError(e).left()
}
}
}

View file

@ -1,17 +0,0 @@
package com.tangem.tap.features.shop.domain
import com.tangem.tap.features.shop.domain.models.SalesProduct
/**
* Shop feature repository
*
[REDACTED_AUTHOR]
*/
interface ShopRepository {
/** Get shopify ordering availability */
suspend fun isShopifyOrderingAvailable(): Boolean
/** Get actual sales product info */
suspend fun getSalesProductInfo(): List<SalesProduct>
}

View file

@ -1,12 +0,0 @@
package com.tangem.tap.features.shop.domain
/**
* Use case to define shopify ordering availability
*
[REDACTED_AUTHOR]
*/
internal interface ShopifyOrderingAvailabilityUseCase {
/** Get availability */
suspend operator fun invoke(): Boolean
}

View file

@ -1,22 +0,0 @@
package com.tangem.tap.features.shop.domain.models
private const val TANGEM_WALLET_2_CARDS_SKU = "TG115X2-S"
private const val TANGEM_WALLET_3_CARDS_SKU = "TG115X3-S"
enum class ProductType(val sku: String) {
WALLET_2_CARDS(TANGEM_WALLET_2_CARDS_SKU),
WALLET_3_CARDS(TANGEM_WALLET_3_CARDS_SKU),
;
companion object {
val SKUS_TO_DISPLAY = listOf(TANGEM_WALLET_2_CARDS_SKU, TANGEM_WALLET_3_CARDS_SKU)
fun fromSku(sku: String): ProductType? {
return when (sku) {
WALLET_2_CARDS.sku -> WALLET_2_CARDS
WALLET_3_CARDS.sku -> WALLET_3_CARDS
else -> null
}
}
}
}

View file

@ -1,5 +0,0 @@
package com.tangem.tap.features.shop.domain.models
sealed class SalesError {
data class DataError(val cause: Throwable) : SalesError()
}

View file

@ -1,30 +0,0 @@
package com.tangem.tap.features.shop.domain.models
/**
* Sales product
*
* @property id product id
* @property productType shows TW2 cards or 3 cards
* @property state state as order available etc
* @property name product name
* @property notification optional notification
*/
data class SalesProduct(
val id: String,
val productType: ProductType,
val state: ProductState,
val name: String,
val notification: Notification?,
)
data class Notification(
val type: String,
val title: String,
val description: String,
)
enum class ProductState {
ORDER,
SOLD_OUT,
PRE_ORDER,
}

View file

@ -1,60 +0,0 @@
package com.tangem.tap.features.shop.presentation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.tap.features.shop.domain.GetShopifySalesProductsUseCase
import com.tangem.tap.features.shop.domain.ShopifyOrderingAvailabilityUseCase
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* Shop screen view model
*
* @property shopifyOrderingAvailabilityUseCase use case to define shopify ordering availability
* @property getShopifySalesProductsUseCase use case to get actual sales info
* @property dispatchers coroutine dispatchers provider
* @property appStateHolder redux state holder
*
[REDACTED_AUTHOR]
*/
@HiltViewModel
internal class ShopViewModel @Inject constructor(
private val shopifyOrderingAvailabilityUseCase: ShopifyOrderingAvailabilityUseCase,
private val getShopifySalesProductsUseCase: GetShopifySalesProductsUseCase,
private val dispatchers: CoroutineDispatcherProvider,
private val appStateHolder: AppStateHolder,
) : ViewModel() {
/** Check ordering delay block visibility */
fun checkOrderingDelayBlockVisibility() {
viewModelScope.launch(dispatchers.main) {
val visibility = runCatching(dispatchers.io) { shopifyOrderingAvailabilityUseCase() }
.fold(onSuccess = { !it }, onFailure = { false })
appStateHolder.mainStore?.dispatch(action = ShopAction.SetOrderingDelayBlockVisibility(visibility))
}
}
/**
* Get actual sales products info
* to configure view dynamically
*/
fun getActualSalesInfo() {
viewModelScope.launch(dispatchers.main) {
val action = getShopifySalesProductsUseCase().fold(
ifLeft = {
ShopAction.SalesProductsError
},
ifRight = {
ShopAction.SalesProductsLoaded(it)
},
)
appStateHolder.mainStore?.dispatch(action)
}
}
}

View file

@ -1,52 +0,0 @@
package com.tangem.tap.features.shop.redux
import android.content.Intent
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.features.shop.domain.models.ProductType
import com.tangem.tap.common.shop.data.TangemProduct
import com.tangem.tap.common.shop.googlepay.GooglePayService
import com.tangem.tap.features.shop.domain.models.SalesProduct
import com.tangem.wallet.R
import org.rekotlin.Action
sealed interface ShopAction : Action {
object LoadProducts : ShopAction {
data class Success(val products: List<TangemProduct>) : ShopAction
object Failure : ShopAction, NotificationAction {
override val messageResource = R.string.common_server_unavailable
}
}
data class ApplyPromoCode(val promoCode: String) : ShopAction {
data class Success(val promoCode: String?, val products: List<TangemProduct>) : ShopAction
object InvalidPromoCode : ShopAction
}
object BuyWithGooglePay : ShopAction {
object UserCancelled : ShopAction
data class HandleGooglePayResponse(val resultCode: Int, val data: Intent?) : ShopAction
data class Failure(val exception: Throwable) : ShopAction
object Success : ShopAction
}
object StartWebCheckout : ShopAction
data class CheckIfGooglePayAvailable(val googlePayService: GooglePayService) : ShopAction {
object Success : ShopAction
object Failure : ShopAction
}
data class SelectProduct(val productType: ProductType) : ShopAction
object FinishSuccessfulOrder : ShopAction
object ResetState : ShopAction
data class SetOrderingDelayBlockVisibility(val visibility: Boolean) : ShopAction
data class SalesProductsLoaded(val salesProducts: List<SalesProduct>) : ShopAction
object SalesProductsError : ShopAction
}

View file

@ -1,128 +0,0 @@
package com.tangem.tap.features.shop.redux
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.tap.common.analytics.events.Shop
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.scope
import com.tangem.tap.shopService
import com.tangem.tap.store
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
class ShopMiddleware {
val shopMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handle(action)
next(action)
}
}
}
}
@Suppress("LongMethod", "ComplexMethod")
private fun handle(action: Action) {
val shopState = store.state.shopState
if (action is NavigationAction.NavigateTo && action.screen == AppScreen.Shop) {
store.dispatch(ShopAction.LoadProducts)
}
if (action !is ShopAction) return
when (action) {
is ShopAction.ApplyPromoCode -> {
scope.launch {
if (action.promoCode.isBlank() && shopState.promoCode == null) {
store.dispatchOnMain(ShopAction.ApplyPromoCode.InvalidPromoCode)
return@launch
}
val result = shopService.applyPromoCode(action.promoCode)
result.onSuccess { products ->
store.dispatchOnMain(
ShopAction.ApplyPromoCode.Success(
promoCode = products.first { it.type == shopState.selectedProduct }.appliedDiscount,
products = products,
),
)
}
result.onFailure { store.dispatchOnMain(ShopAction.ApplyPromoCode.InvalidPromoCode) }
}
}
ShopAction.BuyWithGooglePay -> {
shopService.buyWithGooglePay(shopState.selectedProduct)
// shopService.subscribeToGooglePayResult(productType = shopState.selectedProduct) { result ->
// result.onSuccess {
// store.dispatch(ShopAction.BuyWithGooglePay.Success)
// }
// result.onFailure { error ->
// if (error is TangemSdkError.UserCancelled) {
// store.dispatch(ShopAction.BuyWithGooglePay.UserCancelled)
// } else {
// store.dispatch(ShopAction.BuyWithGooglePay.Failure(error))
// }
// }
// }
}
is ShopAction.BuyWithGooglePay.HandleGooglePayResponse -> {
scope.launch {
val result = shopService.handleGooglePayResult(
action.resultCode,
action.data,
shopState.selectedProduct,
)
result.onSuccess {
store.dispatchOnMain(ShopAction.BuyWithGooglePay.Success)
}
result.onFailure {
store.dispatchOnMain(ShopAction.BuyWithGooglePay.Failure(it))
}
}
}
ShopAction.LoadProducts -> {
scope.launch {
shopService.getProducts().fold(
onSuccess = { store.dispatchOnMain(ShopAction.LoadProducts.Success(it)) },
onFailure = {
Timber.e(it)
store.dispatchOnMain(ShopAction.LoadProducts.Failure)
},
)
}
}
is ShopAction.CheckIfGooglePayAvailable -> {
scope.launch {
val isAvailable =
shopService.checkIfGooglePayAvailable(action.googlePayService).getOrNull()
?: false
val newAction = if (isAvailable) {
ShopAction.CheckIfGooglePayAvailable.Success
} else {
ShopAction.CheckIfGooglePayAvailable.Failure
}
store.dispatchOnMain(newAction)
}
}
ShopAction.StartWebCheckout -> {
Analytics.send(Shop.Redirected(null))
store.dispatchOpenUrl(shopService.getCheckoutUrl(shopState.selectedProduct))
store.dispatch(ShopAction.FinishSuccessfulOrder)
}
is ShopAction.FinishSuccessfulOrder -> {
scope.launch {
shopService.waitForCheckout(shopState.selectedProduct)
}
}
else -> {}
}
}

View file

@ -1,49 +0,0 @@
package com.tangem.tap.features.shop.redux
import org.rekotlin.Action
object ShopReducer {
fun reduce(action: Action, state: ShopState): ShopState = internalReduce(action, state)
}
private fun internalReduce(action: Action, state: ShopState): ShopState {
if (action !is ShopAction) return state
return when (action) {
is ShopAction.ApplyPromoCode -> state.copy(promoCode = action.promoCode, promoCodeLoading = true)
is ShopAction.LoadProducts.Success -> state.copy(availableProducts = action.products)
is ShopAction.ApplyPromoCode.InvalidPromoCode -> state.copy(promoCode = null, promoCodeLoading = false)
is ShopAction.ApplyPromoCode.Success -> {
state.copy(
promoCode = action.promoCode,
availableProducts = action.products,
promoCodeLoading = false,
)
}
is ShopAction.SelectProduct -> state.copy(selectedProduct = action.productType)
// TODO: change when we add support for GPay
is ShopAction.CheckIfGooglePayAvailable.Failure -> state.copy(isGooglePayAvailable = false)
is ShopAction.CheckIfGooglePayAvailable.Success -> state.copy(isGooglePayAvailable = false)
is ShopAction.ResetState -> ShopState()
is ShopAction.SetOrderingDelayBlockVisibility -> state.copy(isOrderingDelayBlockVisible = action.visibility)
is ShopAction.BuyWithGooglePay,
is ShopAction.LoadProducts,
is ShopAction.StartWebCheckout,
is ShopAction.CheckIfGooglePayAvailable,
is ShopAction.BuyWithGooglePay.Failure,
is ShopAction.BuyWithGooglePay.HandleGooglePayResponse,
is ShopAction.BuyWithGooglePay.Success,
is ShopAction.BuyWithGooglePay.UserCancelled,
is ShopAction.FinishSuccessfulOrder,
is ShopAction.LoadProducts.Failure,
-> state
is ShopAction.SalesProductsLoaded -> state.copy(
salesProducts = action.salesProducts,
)
is ShopAction.SalesProductsError -> state.copy(
salesProducts = emptyList(),
)
}
}

View file

@ -1,28 +0,0 @@
package com.tangem.tap.features.shop.redux
import com.tangem.tap.features.shop.domain.models.ProductType
import com.tangem.tap.common.shop.data.TangemProduct
import com.tangem.tap.features.shop.domain.models.SalesProduct
import org.rekotlin.StateType
data class ShopState(
val availableProducts: List<TangemProduct> = emptyList(),
val selectedProduct: ProductType = ProductType.WALLET_3_CARDS,
val salesProducts: List<SalesProduct> = emptyList(),
val promoCode: String? = null,
val promoCodeLoading: Boolean = false,
val isGooglePayAvailable: Boolean = false, // TODO: change when we add support for GPay
val isOrderingDelayBlockVisible: Boolean = false,
) : StateType {
val total: String?
get() = availableProducts.firstOrNull { it.type == selectedProduct }?.totalSum?.finalValue
val priceBeforeDiscount: String?
get() {
val totalSum = availableProducts.firstOrNull { it.type == selectedProduct }?.totalSum
if (totalSum?.finalValue != totalSum?.beforeDiscount) {
return totalSum?.beforeDiscount
}
return null
}
}

View file

@ -1,11 +0,0 @@
package com.tangem.tap.features.shop.toggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
internal class DefaultShopifyFeatureToggleManager(
private val featureTogglesManager: FeatureTogglesManager,
) : ShopifyFeatureToggleManager {
override val isDynamicSalesProductsEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("SHOPIFY_DYNAMIC_ENABLED")
}

View file

@ -1,10 +0,0 @@
package com.tangem.tap.features.shop.toggles
/**
* Shopify feature toggle manager that provides info about shopify toggle availability
*
*/
interface ShopifyFeatureToggleManager {
val isDynamicSalesProductsEnabled: Boolean
}

View file

@ -1,251 +0,0 @@
package com.tangem.tap.features.shop.ui
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.content.Context
import android.os.Bundle
import android.view.View
import android.view.View.OnFocusChangeListener
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.viewModels
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.core.navigation.NavigationAction
import com.tangem.tap.common.GlobalLayoutStateHandler
import com.tangem.tap.common.KeyboardObserver
import com.tangem.tap.common.extensions.getQuantityString
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.shop.domain.models.ProductState
import com.tangem.tap.features.shop.domain.models.ProductType
import com.tangem.tap.features.shop.domain.models.SalesProduct
import com.tangem.tap.features.shop.presentation.ShopViewModel
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.features.shop.redux.ShopState
import com.tangem.tap.features.shop.toggles.ShopifyFeatureToggleManager
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentShopBinding
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<ShopState> {
@Inject
lateinit var shopifyFeatureToggleManager: ShopifyFeatureToggleManager
private val binding: FragmentShopBinding by viewBinding(FragmentShopBinding::bind)
private var cardTranslationY = 70f
private lateinit var keyboardObserver: KeyboardObserver
private val viewModel by viewModels<ShopViewModel>()
override fun subscribeToStore() {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.shopState == newState.shopState
}.select { it.shopState }
}
storeSubscribersList.add(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (shopifyFeatureToggleManager.isDynamicSalesProductsEnabled) {
viewModel.getActualSalesInfo()
} else {
viewModel.checkOrderingDelayBlockVisibility()
}
activity?.onBackPressedDispatcher?.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(ShopAction.ResetState)
}
},
)
}
override fun onDestroyView() {
super.onDestroyView()
keyboardObserver.unregisterListener()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupCardsImages()
setupProductSelection()
setupPromoCodeEditText()
binding.toolbar.setNavigationOnClickListener {
requireActivity().onBackPressed()
}
keyboardObserver = KeyboardObserver(requireActivity()).apply {
registerListener { isVisible ->
binding.flCards.show(!isVisible)
}
}
}
@Suppress("MagicNumber")
private fun setupCardsImages() {
GlobalLayoutStateHandler(binding.imvSecond).apply {
onStateChanged = {
cardTranslationY = it.height * 0.15f
binding.imvSecond.animate()
.translationY(cardTranslationY)
.scaleX(0.9f)
.scaleY(0.9f)
.start()
binding.imvThird.animate()
.translationY(cardTranslationY * 2)
.scaleX(0.8f)
.scaleY(0.8f)
.start()
detach()
}
}
}
private fun setupProductSelection() = with(binding) {
chipProduct1.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) store.dispatch(ShopAction.SelectProduct(ProductType.WALLET_3_CARDS))
}
chipProduct2.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) store.dispatch(ShopAction.SelectProduct(ProductType.WALLET_2_CARDS))
}
chipProduct1.text = chipProduct1.getQuantityString(R.plurals.card_label_card_count, quantity = 3)
chipProduct2.text = chipProduct2.getQuantityString(R.plurals.card_label_card_count, quantity = 2)
}
private fun setupPromoCodeEditText() = with(binding) {
etPromoCode.setOnEditorActionListener { view, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
val imm: InputMethodManager =
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view.windowToken, 0)
view.clearFocus()
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
etPromoCode.onFocusChangeListener = OnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
store.dispatch(ShopAction.ApplyPromoCode(etPromoCode.text.toString()))
}
}
}
override fun newState(state: ShopState) {
if (activity == null || view == null) return
animateProductSelection(state.selectedProduct)
handlePriceState(state)
handlePromoCodeState(state)
// TODO: https://tangem.slack.com/archives/C01HARKDLQ0/p1691421861756069
// if (shopifyFeatureToggleManager.isDynamicSalesProductsEnabled) {
// handleNotificationBlock(state)
// } else {
// handleOrderingDelayBlock(isVisible = state.isOrderingDelayBlockVisible)
// }
handleButtonsState(state)
}
private fun animateProductSelection(selectedProduct: ProductType) {
val show = when (selectedProduct) {
ProductType.WALLET_2_CARDS -> false
ProductType.WALLET_3_CARDS -> true
}
showOrHideThirdCardWithAnimation(show)
}
private fun showOrHideThirdCardWithAnimation(show: Boolean) = with(binding) {
val translationY = if (show) cardTranslationY * 2 else cardTranslationY
if (show) imvThird.show()
imvThird.animate()
.translationY(translationY)
.setListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
imvThird.show(show)
}
},
)
}
private fun handlePriceState(state: ShopState) = with(binding) {
tvTotal.text = state.total
tvTotalBeforeDiscount.text = state.priceBeforeDiscount
pbPrice.show(state.total == null)
}
private fun handlePromoCodeState(state: ShopState) = with(binding) {
if (state.promoCode == null && !etPromoCode.hasFocus()) {
etPromoCode.setText("")
}
pbPromoCode.show(state.promoCodeLoading)
}
// private fun handleOrderingDelayBlock(isVisible: Boolean) {
// if (isVisible) binding.tvSoldOutDesc.show() else binding.tvSoldOutDesc.hide()
// }
//
// private fun handleNotificationBlock(state: ShopState) {
// if (isVisible) {
// binding.tvSoldOutDesc.show()
// getSelectedSalesProduct(state)?.notification?.let { notification ->
// binding.tvSoldOutDesc.text = notification.description
// }
// } else {
// binding.tvSoldOutDesc.hide()
// }
// }
private fun handleButtonsState(state: ShopState) = with(binding) {
btnPayGooglePay.root.show(state.isGooglePayAvailable)
btnAlternativePayment.show(state.isGooglePayAvailable)
btnMainAction.show(!state.isGooglePayAvailable)
if (state.total != null) {
btnAlternativePayment.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) }
btnPayGooglePay.root.setOnClickListener { store.dispatch(ShopAction.BuyWithGooglePay) }
btnMainAction.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) }
if (state.salesProducts.isNotEmpty()) {
getSelectedSalesProduct(state)?.let { selectedProduct ->
btnMainAction.text = getMainBtnTextByProductState(
productState = selectedProduct.state,
)
}
}
}
}
override fun handleOnBackPressed() {
store.dispatch(ShopAction.ResetState)
super.handleOnBackPressed()
}
private fun getSelectedSalesProduct(state: ShopState): SalesProduct? {
return state.salesProducts.find {
it.productType == state.selectedProduct
}
}
private fun getMainBtnTextByProductState(productState: ProductState): String = when (productState) {
ProductState.ORDER -> getString(R.string.shop_buy_now)
ProductState.SOLD_OUT -> "Sold out" // getString(R.string.sold_out) // todo finalize in next PR
ProductState.PRE_ORDER -> "Pre order" // getString(R.string.pre_order) // todo finalize in next PR
}
}

View file

@ -318,6 +318,7 @@ object TradeCryptoMiddleware {
SendRouter.TRANSACTION_ID_KEY to txInfo?.transactionId,
SendRouter.DESTINATION_ADDRESS_KEY to txInfo?.destinationAddress,
SendRouter.AMOUNT_KEY to txInfo?.amount,
SendRouter.TAG_KEY to txInfo?.tag,
)
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle))
}

View file

@ -0,0 +1,37 @@
package com.tangem.tap.network.exchangeServices.moonpay
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Blockchain.*
import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPaySupportedCurrency
internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency?
get() = when (this) {
Algorand -> MoonPaySupportedCurrency(networkCode = "algorand", currencyCode = "algo")
Aptos -> MoonPaySupportedCurrency(networkCode = "aptos", currencyCode = "apt")
Arbitrum -> MoonPaySupportedCurrency(networkCode = "arbitrum", currencyCode = "eth_arbitrum")
Avalanche -> MoonPaySupportedCurrency(networkCode = "avalanche_c_chain", currencyCode = "avax_cchain")
Binance -> MoonPaySupportedCurrency(networkCode = "bnb_chain", currencyCode = "bnb")
Bitcoin -> MoonPaySupportedCurrency(networkCode = "bitcoin", currencyCode = "btc")
BitcoinCash -> MoonPaySupportedCurrency(networkCode = "bitcoin_cash", currencyCode = "bch")
BSC -> MoonPaySupportedCurrency(networkCode = "binance_smart_chain", currencyCode = "bnb_bsc")
Cardano -> MoonPaySupportedCurrency(networkCode = "cardano", currencyCode = "ada")
Cosmos -> MoonPaySupportedCurrency(networkCode = "cosmos", currencyCode = "atom")
Dogecoin -> MoonPaySupportedCurrency(networkCode = "dogecoin", currencyCode = "doge")
Ethereum -> MoonPaySupportedCurrency(networkCode = "ethereum", currencyCode = "eth")
EthereumClassic -> MoonPaySupportedCurrency(networkCode = "ethereum_classic", currencyCode = "etc")
Hedera -> MoonPaySupportedCurrency(networkCode = "hedera", currencyCode = "hbar")
Litecoin -> MoonPaySupportedCurrency(networkCode = "litecoin", currencyCode = "ltc")
Near -> MoonPaySupportedCurrency(networkCode = "near", currencyCode = "near")
Optimism -> MoonPaySupportedCurrency(networkCode = "optimism", currencyCode = "eth_optimism")
Polkadot -> MoonPaySupportedCurrency(networkCode = "polkadot", currencyCode = "dot")
Polygon -> MoonPaySupportedCurrency(networkCode = "polygon", currencyCode = "matic_polygon")
Ravencoin -> MoonPaySupportedCurrency(networkCode = "ravencoin", currencyCode = "rvn")
Solana -> MoonPaySupportedCurrency(networkCode = "solana", currencyCode = "sol")
Stellar -> MoonPaySupportedCurrency(networkCode = "stellar", currencyCode = "xlm")
Tezos -> MoonPaySupportedCurrency(networkCode = "tezos", currencyCode = "xtz")
TON -> MoonPaySupportedCurrency(networkCode = "ton", currencyCode = "ton")
Tron -> MoonPaySupportedCurrency(networkCode = "tron", currencyCode = "trx")
VeChain -> MoonPaySupportedCurrency(networkCode = "vechain", currencyCode = "vet")
XRP -> MoonPaySupportedCurrency(networkCode = "ripple", currencyCode = "xrp")
else -> null
}

View file

@ -12,6 +12,7 @@ import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SCHEME
import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
@ -35,42 +36,46 @@ class MoonPayService(
override suspend fun update() {
withIOContext {
performRequest {
val userStatusResult = performRequest { api.getUserStatus(apiKey) }
if (userStatusResult is Result.Failure) return@performRequest
val currenciesResult = performRequest { api.getCurrencies(apiKey) }
if (currenciesResult is Result.Failure) return@performRequest
val userStatus = (userStatusResult as Result.Success).data
val currencies = (currenciesResult as Result.Success).data
// val currenciesToBuy = mutableListOf<String>()
val currenciesToSell = mutableListOf<String>()
currencies.forEach { currencyStatus ->
if (currencyStatus.type != "crypto" || currencyStatus.isSuspended ||
!currencyStatus.supportsLiveMode
) {
return@forEach
val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) {
is Result.Failure -> return@performRequest
is Result.Success -> result.data
}
if (userStatus.countryCode == "USA") {
if (!currencyStatus.isSupportedInUS) return@forEach
if (currencyStatus.notAllowedUSStates.contains(userStatus.stateCode)) return@forEach
val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) {
is Result.Failure -> return@performRequest
is Result.Success -> result.data
}
val currencyCode = currencyStatus.code.uppercase()
// currenciesToBuy.add(currencyCode)
if (currencyStatus.isSellSupported) currenciesToSell.add(currencyCode)
val currenciesToSell = currencies
.filter { currency ->
checkGeneralRequirements(currency) && checkUSARequirements(userStatus, currency)
}
.mapNotNull { currency ->
MoonPayAvailableCurrency(
currencyCode = currency.code,
networkCode = currency.metadata?.networkCode ?: return@mapNotNull null,
contractAddress = currency.metadata.contractAddress,
)
}
// currenciesToBuy.sort()
currenciesToSell.sort()
status = MoonPayStatus(currenciesToSell, userStatus, currencies)
}
}
}
private fun checkGeneralRequirements(currency: MoonPayCurrencies): Boolean {
return currency.type == "crypto" && !currency.isSuspended && currency.supportsLiveMode &&
currency.isSellSupported
}
private fun checkUSARequirements(userStatus: MoonPayUserStatus, currency: MoonPayCurrencies): Boolean {
return if (userStatus.countryCode == "USA") {
currency.isSupportedInUS && !currency.notAllowedUSStates.contains(userStatus.stateCode)
} else {
true
}
}
override fun isBuyAllowed(): Boolean = false
override fun isSellAllowed(): Boolean {
@ -80,21 +85,21 @@ class MoonPayService(
override fun availableForBuy(currency: Currency): Boolean = false
override fun availableForSell(currency: Currency): Boolean {
val availableForSell = status?.availableForSell ?: return false
val metadata = status?.responseCurrencies?.filter { it.isSellSupported }?.map { it.metadata }
if (!isSellAllowed()) return false
return when (currency) {
val availableForSell = status?.availableForSell ?: return false
val supportedCurrency = currency.blockchain.moonPaySupportedCurrency ?: return false
return availableForSell.any {
when (currency) {
is Currency.Blockchain -> {
val blockchain = currency.blockchain
when {
blockchain.isTestnet() -> false
blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC -> false
else -> availableForSell.contains(currency.currencySymbol)
}
it.networkCode.equals(other = supportedCurrency.networkCode, ignoreCase = true) &&
it.currencyCode.equals(other = supportedCurrency.currencyCode, ignoreCase = true)
}
is Currency.Token -> {
metadata?.any { it?.contractAddress.equals(currency.token.contractAddress, ignoreCase = true) } ?: false
it.networkCode.equals(other = supportedCurrency.networkCode, ignoreCase = true) &&
it.contractAddress.equals(other = currency.token.contractAddress, ignoreCase = true)
}
}
}
}
@ -103,7 +108,7 @@ class MoonPayService(
action: CurrencyExchangeManager.Action,
blockchain: Blockchain,
cryptoCurrencyName: CryptoCurrencyName,
fatCurrency: String,
fiatCurrencyName: String,
walletAddress: String,
isDarkTheme: Boolean,
): String {
@ -115,7 +120,8 @@ class MoonPayService(
.appendQueryParameter("apiKey", apiKey)
.appendQueryParameter("baseCurrencyCode", cryptoCurrencyName)
.appendQueryParameter("refundWalletAddress", walletAddress)
.appendQueryParameter("redirectURL", "tangem://sell-request.tangem.com")
.appendQueryParameter("redirectURL", "tangem://redirect_sell")
if (isDarkTheme) uri.appendQueryParameter("theme", "dark")
val originalQuery = uri.build().encodedQuery ?: uri.build().toString()
@ -147,7 +153,7 @@ class MoonPayService(
}
private data class MoonPayStatus(
val availableForSell: List<String>,
val availableForSell: List<MoonPayAvailableCurrency>,
val responseUserStatus: MoonPayUserStatus,
val responseCurrencies: List<MoonPayCurrencies>,
)

View file

@ -0,0 +1,7 @@
package com.tangem.tap.network.exchangeServices.moonpay.models
internal data class MoonPayAvailableCurrency(
val currencyCode: String,
val networkCode: String,
val contractAddress: String?,
)

View file

@ -0,0 +1,3 @@
package com.tangem.tap.network.exchangeServices.moonpay.models
internal data class MoonPaySupportedCurrency(val networkCode: String, val currencyCode: String?)

View file

@ -1,7 +1,9 @@
package com.tangem.tap.proxy.redux
import com.tangem.TangemSdkLogger
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@ -67,4 +69,6 @@ data class DaggerGraphState(
val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null,
val cardRepository: CardRepository? = null,
val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles? = null,
val tangemSdkLogger: TangemSdkLogger? = null,
val blockchainSDKLogger: BlockchainSDKLogger? = null,
) : StateType

View file

@ -1,336 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinator_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_secondary"
android:clipChildren="false"
android:clipToPadding="false"
android:focusableInTouchMode="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
style="@style/ThemeOverlay.MyTheme.Toolbar.AccentColorMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background_secondary"
android:fitsSystemWindows="true"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_clear_24"
app:title="@string/home_button_order"
app:titleCentered="true" />
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:clickable="true"
android:fitsSystemWindows="true"
android:focusable="true"
android:focusableInTouchMode="true">
<FrameLayout
android:id="@+id/fl_cards"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="36dp"
android:layout_marginEnd="36dp"
android:layout_weight="1"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingTop="16dp"
android:paddingBottom="44dp"
app:layout_constraintBottom_toTopOf="@+id/tv_header"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/imv_third"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:src="@drawable/card_placeholder_wallet"
android:visibility="visible"
app:layout_constraintBottom_toTopOf="@id/tv_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/darkGray2" />
<ImageView
android:id="@+id/imv_second"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:src="@drawable/card_placeholder_wallet"
android:visibility="visible"
app:layout_constraintBottom_toTopOf="@id/tv_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/darkGray4" />
<ImageView
android:id="@+id/imv_first"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:src="@drawable/card_placeholder_wallet"
android:visibility="visible"
app:layout_constraintBottom_toTopOf="@id/tv_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</FrameLayout>
<TextView
android:id="@+id/tv_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="14dp"
android:fontFamily="sans-serif-medium"
android:text="@string/shop_one_wallet"
android:textColor="#060606"
android:textSize="24sp"
app:layout_constraintBottom_toTopOf="@+id/chip_group_product"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/fl_cards" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/chip_group_product"
android:layout_width="wrap_content"
android:layout_height="42dp"
android:layout_marginBottom="24dp"
android:background="@drawable/shape_rectangle_rounded_100"
android:backgroundTint="@color/backgroundGray"
android:paddingStart="3dp"
android:paddingTop="3dp"
android:paddingEnd="3dp"
app:layout_constraintBottom_toTopOf="@id/ll_total"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:selectionRequired="true"
app:singleLine="true"
app:singleSelection="true">
<com.google.android.material.chip.Chip
android:id="@+id/chip_product_1"
style="@style/ShopChips"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:checked="true"
app:chipMinTouchTargetSize="0dp" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_product_2"
style="@style/ShopChips"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:checked="false"
app:chipMinTouchTargetSize="0dp" />
</com.google.android.material.chip.ChipGroup>
<LinearLayout
android:id="@+id/ll_total"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="21dp"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@id/ll_buttons">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@drawable/shape_rectangle_rounded_4">
<EditText
android:id="@+id/et_promo_code"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:hint="@string/shop_i_have_a_promo_code"
android:imeOptions="actionDone"
android:inputType="textCapCharacters"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:singleLine="true"
android:textColor="@color/textBlack"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/pb_promo_code"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:indeterminateTint="@color/darkGray2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:id="@+id/v_divider"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="#BEBEBE" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@drawable/shape_rectangle_rounded_4">
<TextView
android:id="@+id/tv_total_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="@string/shop_total"
android:textColor="@color/textBlack"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_total_before_discount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:background="@drawable/shape_line"
android:textColor="@color/darkGray6"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_total"
app:layout_constraintTop_toTopOf="parent"
tools:text="$44.20" />
<TextView
android:id="@+id/tv_total"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="6dp"
android:paddingEnd="12dp"
android:textColor="@color/darkGray6"
android:textSize="22sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="$44.20" />
<ProgressBar
android:id="@+id/pb_price"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_marginEnd="16dp"
android:indeterminateTint="@color/darkGray2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!--TODO: https://tangem.slack.com/archives/C01HARKDLQ0/p1691421861756069-->
<!--<TextView-->
<!-- android:id="@+id/tv_sold_out_desc"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginTop="14dp"-->
<!-- android:background="@drawable/shape_rectangle_rounded_4"-->
<!-- android:padding="16dp"-->
<!-- android:text="@string/shop_sold_out_description"-->
<!-- android:textColor="@color/text_tertiary"-->
<!-- android:textSize="16sp" />-->
</LinearLayout>
<LinearLayout
android:id="@+id/ll_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent">
<include
android:id="@+id/btn_pay_google_pay"
layout="@layout/buy_with_googlepay_button"
android:layout_width="match_parent"
android:layout_height="48sp"
android:layout_gravity="bottom"
android:layout_marginStart="14dp"
android:layout_marginEnd="14dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_main_action"
style="@style/TapPrimaryButton"
android:layout_width="match_parent"
android:layout_height="48sp"
android:layout_marginStart="14dp"
android:layout_marginEnd="14dp"
android:layout_marginBottom="20dp"
android:text="@string/shop_buy_now"
android:textAlignment="center"
android:textSize="16sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_alternative_payment"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="match_parent"
android:layout_height="42dp"
android:layout_gravity="bottom"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="20dp"
android:paddingTop="0dp"
android:paddingBottom="0dp"
android:text="@string/shop_other_payment_methods"
android:textAllCaps="false"
android:textColor="@color/textBlack"
android:textSize="16sp"
android:visibility="gone" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -37,6 +37,7 @@ internal class DefaultDerivationsRepositoryTest {
cardsInWallet = setOf(),
isMultiCurrency = false,
scanResponse = ScanResponseMockFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()),
hasBackupError = false,
)
@Test

View file

@ -102,7 +102,6 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
),
appsFlyerDevKey = configValues.appsFlyer.appsFlyerDevKey,
amplitudeApiKey = configValues.amplitudeApiKey,
shopify = configValues.shopifyShop,
sprinklr = configValues.sprinklr,
walletConnectProjectId = configValues.walletConnectProjectId,
tangemComAuthorization = configValues.tangemComAuthorization,

View file

@ -14,7 +14,6 @@ data class Config(
val isTopUpEnabled: Boolean = false,
@Deprecated("Not relevant since version 3.23")
val isCreatingTwinCardsAllowed: Boolean = false,
val shopify: ShopifyShop? = null,
val sprinklr: SprinklrConfig? = null,
val walletConnectProjectId: String = "",
val tangemComAuthorization: String? = null,

View file

@ -31,7 +31,6 @@ class ConfigValueModel(
val blockcypherTokens: Set<String>?,
val infuraProjectId: String?,
val appsFlyer: AppsFlyer,
val shopifyShop: ShopifyShop?,
val sprinklr: SprinklrConfig?,
val tronGridApiKey: String,
val amplitudeApiKey: String,

View file

@ -1,14 +0,0 @@
package com.tangem.datasource.config.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ShopifyShop(
@Json(name = "domain")
val domain: String,
@Json(name = "storefrontApiKeyAndroid")
val storefrontApiKey: String,
@Json(name = "merchantID")
val merchantID: String?,
)

View file

@ -4,13 +4,14 @@ import com.tangem.core.deeplink.DeepLink
class SellCurrencyDeepLink(val onReceive: (data: Data) -> Unit) : DeepLink {
override val uri: String = "tangem://sell-request.tangem.com"
override val uri: String = "tangem://redirect_sell"
override fun onReceive(params: Map<String, String>) {
val data = Data(
transactionId = params["transactionId"] ?: return,
baseCurrencyAmount = params["baseCurrencyAmount"] ?: return,
depositWalletAddress = params["depositWalletAddress"] ?: return,
depositWalletAddressTag = params["depositWalletAddressTag"],
)
onReceive(data)
@ -20,5 +21,6 @@ class SellCurrencyDeepLink(val onReceive: (data: Data) -> Unit) : DeepLink {
val transactionId: String,
val baseCurrencyAmount: String,
val depositWalletAddress: String,
val depositWalletAddressTag: String?,
)
}

View file

@ -3,10 +3,6 @@
"name": "NEW_CARD_SCANNING_ENABLED",
"version": "undefined"
},
{
"name": "SHOPIFY_DYNAMIC_ENABLED",
"version": "undefined"
},
{
"name": "REDESIGNED_MANAGE_TOKENS_SCREEN_ENABLED",
"version": "undefined"

View file

@ -1,6 +1,8 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
@ -9,6 +11,10 @@ android {
}
dependencies {
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
implementation(deps.material)
implementation(deps.reKotlin)
implementation(deps.timber)
}

View file

@ -11,7 +11,6 @@ data class NavigationState(
enum class AppScreen(val isDialogFragment: Boolean = false) {
Home,
Shop,
Disclaimer,
OnboardingNote,
OnboardingWallet,

View file

@ -0,0 +1,22 @@
package com.tangem.core.navigation.di
import android.content.Context
import com.tangem.core.navigation.email.AndroidEmailSender
import com.tangem.core.navigation.email.EmailSender
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped
@Module
@InstallIn(ActivityComponent::class)
internal object EmailSenderModule {
@Provides
@ActivityScoped
fun provideEmailSender(@ActivityContext context: Context): EmailSender {
return AndroidEmailSender(context)
}
}

View file

@ -0,0 +1,65 @@
package com.tangem.core.navigation.email
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.app.ShareCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import timber.log.Timber
/**
* Implementation of email sender for Android
*
* @property context context
*
[REDACTED_AUTHOR]
*/
internal class AndroidEmailSender(private val context: Context) : EmailSender {
override fun send(email: EmailSender.Email, onFail: ((Exception) -> Unit)?) {
val originalIntent = createEmailShareIntent(email)
val emailFilterIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
val packageManager = context.packageManager
val originalIntentResults = packageManager.queryIntentActivities(originalIntent, 0)
val emailFilterIntentResults = packageManager.queryIntentActivities(emailFilterIntent, 0)
val targetedIntents = originalIntentResults
.filter { originalResult ->
emailFilterIntentResults.any {
originalResult.activityInfo.packageName == it.activityInfo.packageName
}
}
.map {
createEmailShareIntent(email).apply { setPackage(it.activityInfo.packageName) }
}
.toMutableList()
try {
val chooserIntent = Intent.createChooser(targetedIntents.removeAt(0), "Send mail...").apply {
putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toTypedArray())
}
ContextCompat.startActivity(context, chooserIntent, null)
} catch (ex: Exception) {
Timber.e("Failed to send email: $ex")
}
}
private fun createEmailShareIntent(email: EmailSender.Email): Intent {
val builder = ShareCompat.IntentBuilder(context)
.setType("message/rfc822")
.setEmailTo(arrayOf(email.address))
.setSubject(email.subject)
.setText(email.message)
email.attachment?.let {
builder.setStream(
FileProvider.getUriForFile(context, "${context.packageName}.provider", it),
)
}
return builder.intent
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.core.navigation.email
import java.io.File
/**
* Email sender
*
[REDACTED_AUTHOR]
*/
interface EmailSender {
/** Send [email] or handle exception [onFail] */
fun send(email: Email, onFail: ((Exception) -> Unit)? = null)
data class Email(
val address: String,
val subject: String,
val message: String,
val attachment: File? = null,
)
}

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="alert_unsupported_card">Diese Karte ist für die Zusammenarbeit mit Tangem nicht geeignet</string>
<string name="chat_bot_name">Tangem Bot</string>
<string name="common_accept">Akzeptieren</string>
<string name="common_balance">Bilanz: %s</string>
<string name="common_camera_denied_alert_message">Sie haben keinen Zugang zur Kamera erteilt, bitte passen Sie Ihre Datenschutzeinstellungen an</string>
@ -9,11 +8,12 @@
<string name="common_delete">Entfernen</string>
<string name="common_done">Erledigt</string>
<string name="common_error">Fehler</string>
<string name="common_fee_label">Gebühr</string>
<string name="common_network_fee_title">Netzgebühr</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="common_fee_label">Gebühr</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>
<string name="details_manage_security_long_tap">Langes Tippen</string>
@ -33,7 +33,6 @@
<string name="initial_message_scan_header">Legen Sie die Karte zum Scannen an</string>
<string name="initial_message_sign_header">Tippen um zu signieren</string>
<string name="initial_message_tap_header">Legen Sie die Karte an</string>
<string name="main_processing_full_amount">Der Betrag enthält nicht einige Ihrer Mittel</string>
<string name="onboarding_create_wallet_button_create_wallet">Ein wallet erstellen</string>
<string name="send_amount_label">Betrag</string>
<string name="send_destination_hint_address">Adresse</string>
@ -45,28 +44,16 @@
<string name="send_fee_picker_normal">Normal</string>
<string name="send_fee_picker_priority">Priorität</string>
<string name="send_max_amount_label">Höchstbetrag</string>
<string name="common_network_fee_title">Netzgebühr</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>
<string name="send_total_subtitle_format">%s wird gesendet</string>
<string name="send_transaction_success">Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert</string>
<string name="send_validation_invalid_address">Ungültige Adresse</string>
<string name="shop_one_wallet">Tangem Wallet</string>
<string name="twins_recreate_toolbar">Tangem Twin</string>
<string name="wallet_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_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>
<string name="wallet_error_unsupported_blockchain_subtitle">Ihre Tangem-Karte wurde für die Arbeit mit einer anderen App ausgestellt. Bitte sehen Sie sich den Namen und die Anleitung auf Ihrer Karte an und installieren Sie eine korrekte App</string>
<string name="wallet_notification_address_copied">Die Adresse wurde erfolgreich kopiert</string>
<string name="wallet_notification_no_internet">Keine Internetverbindung</string>
<string name="wallet_pending_tx_receiving">Erhalten</string>
<string name="wallet_pending_tx_receiving_address_format">von %s</string>
<string name="wallet_pending_tx_sending">Senden</string>
<string name="wallet_pending_tx_sending_address_format">an %s</string>
<string name="wallet_title">Tangem</string>
<string name="warning_button_ok">OK, ich hab\'s!</string>
</resources>

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="alert_unsupported_card">Cette carte n\'est pas conçue pour fonctionner avec Tangem</string>
<string name="chat_bot_name">Tangem Bot</string>
<string name="common_accept">J\'accepte</string>
<string name="common_balance">Solde : %s</string>
<string name="common_camera_denied_alert_message">Vous n\'avez pas octroyé l\'accès à votre caméra, veuillez modifier vos paramètres de confidentialité</string>
@ -9,11 +8,12 @@
<string name="common_delete">Supprimer</string>
<string name="common_done">Exécuté</string>
<string name="common_error">Erreur</string>
<string name="common_fee_label">Commissions</string>
<string name="common_network_fee_title">Commissions du réseau</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="common_fee_label">Commissions</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>
<string name="details_manage_security_long_tap">Tenez la carte fermement</string>
@ -33,7 +33,6 @@
<string name="initial_message_scan_header">Posez pour scanner</string>
<string name="initial_message_sign_header">Touchez pour signer</string>
<string name="initial_message_tap_header">Posez la carte</string>
<string name="main_processing_full_amount">Le montant n\'inclut pas certains de vos fonds</string>
<string name="onboarding_create_wallet_button_create_wallet">Créer un portefeuille</string>
<string name="send_amount_label">Somme</string>
<string name="send_destination_hint_address">Adresse</string>
@ -45,28 +44,16 @@
<string name="send_fee_picker_normal">Normal</string>
<string name="send_fee_picker_priority">Priorité</string>
<string name="send_max_amount_label">Somme maximale</string>
<string name="common_network_fee_title">Commissions du réseau</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>
<string name="send_total_subtitle_format">Sera envoyé %s</string>
<string name="send_transaction_success">La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps</string>
<string name="send_validation_invalid_address">Adresse incorrecte</string>
<string name="shop_one_wallet">Tangem Wallet</string>
<string name="twins_recreate_toolbar">Tangem Twin</string>
<string name="wallet_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_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>
<string name="wallet_error_unsupported_blockchain_subtitle">Votre carte Tangem a été créée pour fonctionner avec une autre application. Regardez le nom et les instructions sur votre carte et installez l\'application pertinente</string>
<string name="wallet_notification_address_copied">L\'adresse a été copiée avec succès</string>
<string name="wallet_notification_no_internet">Pas de connexion internet</string>
<string name="wallet_pending_tx_receiving">Réception</string>
<string name="wallet_pending_tx_receiving_address_format">de %s</string>
<string name="wallet_pending_tx_sending">Envoi</string>
<string name="wallet_pending_tx_sending_address_format">jusqu\'à %s</string>
<string name="wallet_title">Tangem</string>
<string name="warning_button_ok">Ok, je l\'ai!</string>
</resources>

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="alert_unsupported_card">Questa carta non è progettata per funzionare con Tangem</string>
<string name="chat_bot_name">Tangem Bot</string>
<string name="common_accept">Accetta</string>
<string name="common_balance">Saldo: %s</string>
<string name="common_camera_denied_alert_message">Non hai fornito l\'accesso alla tua videocamera, modifica le tue impostazioni sulla privacy</string>
@ -9,11 +8,12 @@
<string name="common_delete">Rimuovere</string>
<string name="common_done">Fatto</string>
<string name="common_error">Errore</string>
<string name="common_fee_label">Commissione</string>
<string name="common_network_fee_title">Costi della rete</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="common_fee_label">Commissione</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>
<string name="details_manage_security_long_tap">Mantenimento della carta</string>
@ -33,7 +33,6 @@
<string name="initial_message_scan_header">Avvicina per scansionare</string>
<string name="initial_message_sign_header">Avvicina per firmare</string>
<string name="initial_message_tap_header">Avvicina la carta</string>
<string name="main_processing_full_amount">L\'importo non include alcuni dei tuoi fondi</string>
<string name="onboarding_create_wallet_button_create_wallet">Crea portafoglio</string>
<string name="send_amount_label">Importo</string>
<string name="send_destination_hint_address">Indirizzo</string>
@ -45,28 +44,16 @@
<string name="send_fee_picker_normal">Normale</string>
<string name="send_fee_picker_priority">Prioritario</string>
<string name="send_max_amount_label">Importo totale</string>
<string name="common_network_fee_title">Costi della rete</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>
<string name="send_total_subtitle_format">Sarà inviato %s</string>
<string name="send_transaction_success">La transazione è stata firmata con successo e inviata al nodo blockchain. Il saldo del portafoglio verrà aggiornato dopo un po\' di tempo</string>
<string name="send_validation_invalid_address">Indirizzo non valido</string>
<string name="shop_one_wallet">Tangem Wallet</string>
<string name="twins_recreate_toolbar">Tangem Twin</string>
<string name="wallet_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_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>
<string name="wallet_error_unsupported_blockchain_subtitle">La tua carta Tangem è stata creata per funzionare con un\'altra applicazione. Leggere il nome e le istruzioni sulla carta e installare l\'applicazione corretta</string>
<string name="wallet_notification_address_copied">L\'indirizzo è stato copiato con successo</string>
<string name="wallet_notification_no_internet">Nessuna connessione a Internet</string>
<string name="wallet_pending_tx_receiving">Riceviu</string>
<string name="wallet_pending_tx_receiving_address_format">da %s</string>
<string name="wallet_pending_tx_sending">Inviau</string>
<string name="wallet_pending_tx_sending_address_format">su %s</string>
<string name="wallet_title">Tangem</string>
<string name="warning_button_ok">Ok, ho capito!</string>
</resources>

View file

@ -6,9 +6,9 @@
<string name="common_utxo_validate_withdrawal_message_warning">Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму.</string>
<string name="eth_gas_required_exceeds_allowance">Недостаточно средств для совершения транзакции. Пожалуйста, пополните свой аккаунт.</string>
<string name="generic_error_code">Произошла ошибка. Код: %s.</string>
<string name="no_account_bnb">Для создания аккаунта отправьте средства на этот адрес</string>
<string name="no_account_generic">Чтобы использовать сеть %1$s, вы должны оплатить резерв аккаунта (%2$s %3$s), который блокируется и не используется в вашем балансе</string>
<string name="no_account_polkadot">Аккаунт получателя не активирован. Отправьте %s или более для активации аккаунта.</string>
<string name="no_account_send_to_create">Для создания аккаунта отправьте средства на этот адрес</string>
<string name="send_error_dust_amount_format">Минимальная сумма: %s</string>
<string name="send_error_dust_change">Сдача слишком мала</string>
<string name="send_error_invalid_fee_value">Неверная комиссия</string>

View file

@ -3,12 +3,8 @@
<string name="add_custom_token_title">Добавить токен</string>
<string name="add_tokens_title">Валюты</string>
<string name="address_qr_code_message_format">Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств.</string>
<string name="alert_app_feedback_sent_message">Спасибо за ваш отзыв</string>
<string name="alert_app_feedback_sent_title">Отправлено успешно</string>
<string name="alert_button_request_support">Обратиться в поддержку</string>
<string name="alert_button_send_feedback">Отправить отзыв</string>
<string name="alert_demo_feature_disabled">Эта функция недоступна в демонстрационном режиме</string>
<string name="alert_failed_to_send_email_title">Не удалось отправить письмо</string>
<string name="alert_failed_to_send_transaction_message">Причина: %s</string>
<string name="alert_failed_to_send_transaction_title">Не могу отправить транзакцию</string>
<string name="alert_manage_tokens_unsupported_blockchain_by_card_message">Выбранный кошелёк не поддерживает сеть %1$s</string>
@ -16,6 +12,7 @@
<string name="alert_manage_tokens_unsupported_message">Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки.</string>
<string name="alert_troubleshooting_scan_card_title">У вас возникли трудности со сканированием карты?</string>
<string name="alert_unsupported_card">Эта карта не предназначена для работы с этим приложением</string>
<string name="app_settings_default_fee_footer">Подключите функцию комиссии по умолчанию и при формировании транзакции на отправку средств комиссия будет выставлена автоматически, а экран комиссии пропущен. Вы всегда сможете на него вернуться.</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>
@ -46,7 +43,6 @@
</plurals>
<string name="card_settings_access_code_recovery_disabled_description">Отключите эту опцию, если не хотите, чтобы эта карта использовалась для сброса кодов доступа на другие карты этого кошелька. Обратите внимание, сброс кода также не будет доступен на этой карте.</string>
<string name="card_settings_access_code_recovery_enabled_description">Использовать эту карту для сброса кода доступа на других картах в этом кошельке</string>
<string name="card_settings_access_code_recovery_footer">Отключить возможность сброса кода доступа на этой карте или других картах этого кошелька</string>
<string name="card_settings_access_code_recovery_title">Восстановление кода доступа</string>
<string name="card_settings_action_sheet_reset">Сбросить</string>
<string name="card_settings_action_sheet_title">Вы уверены, что хотите это сделать?</string>
@ -54,14 +50,9 @@
<string name="card_settings_change_access_code_footer">Код доступа будет изменен только на данной карте</string>
<string name="card_settings_reset_card_to_factory">Заводские настройки</string>
<string name="card_settings_security_mode">Тип безопасности</string>
<string name="card_settings_security_mode_footer">Выбранный способ защиты приложения</string>
<string name="card_settings_title">Настройки карты</string>
<string name="chat_bot_name">Tangem Bot</string>
<string name="chat_button_title">Чат</string>
<string name="chat_user_action_send_log">Отправить логи</string>
<string name="common_accept">Принять</string>
<string name="common_access_denied">Доступ запрещен</string>
<string name="common_add">Добавить</string>
<string name="common_apply">Применить</string>
<string name="common_approval">Одобрение</string>
<string name="common_attention">Внимание</string>
@ -80,14 +71,11 @@
<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_error">Ошибка</string>
<string name="common_exchange">Обменять</string>
<string name="common_explore">Обозреватель</string>
<string name="common_explore_history">Посмотреть историю</string>
<string name="common_explore_transaction_history">Посмотреть историю транзакций</string>
<string name="common_explorer">Обозреватель</string>
<string name="common_fee_label">Комиссия</string>
@ -100,16 +88,15 @@
<string name="common_generate_addresses">Получить адреса</string>
<string name="common_go_to_provider">К провайдеру</string>
<string name="common_import">Импортировать</string>
<string name="common_like">Нравится</string>
<string name="common_locked">Заблокирован</string>
<string name="common_main_network">Основная сеть</string>
<string name="common_network_fee_title">Сетевая комиссия</string>
<string name="common_next">Далее</string>
<string name="common_no">Нет</string>
<string name="common_no_address">Нет адреса</string>
<string name="common_no_data">Нет данных</string>
<string name="common_ok">OK</string>
<string name="common_origin_card">Основная карта</string>
<string name="common_paste">Вставить</string>
<string name="common_passphrase">Кодовая фраза</string>
<string name="common_read_more">Подробнее</string>
<string name="common_receive">Получить</string>
<string name="common_reject">Отклонить</string>
@ -126,7 +113,6 @@
<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>
@ -137,18 +123,16 @@
<string name="common_transactions">Транзакции</string>
<string name="common_transfer">Перевод</string>
<string name="common_understand">Я понял</string>
<string name="common_unreachable">Недоступно</string>
<string name="common_unknown_error">Произошла ошибка. Пожалуйста, попробуйте снова.</string>
<string name="common_unreachable">Недоступно</string>
<string name="common_yes">Да</string>
<string name="contract_address_copied_message">Адрес контракта скопирован!</string>
<string name="currency_subtitle_expanded">Доступные сети</string>
<string name="custom_token_add_token">Добавить токен</string>
<string name="custom_token_contract_address_input_title">Адрес контракта</string>
<string name="custom_token_creation_error_invalid_contract_address">Адрес контракта некорректен</string>
<string name="custom_token_creation_error_invalid_derivation_path">Путь деривации некорректен</string>
<string name="custom_token_creation_error_network_not_selected">Пожалуйста, выберите сеть</string>
<string name="custom_token_creation_error_required_field">Обязательное поле</string>
<string name="custom_token_creation_error_wrong_decimals">Количество знаков после запятой должно быть корректным числом не больше %d</string>
<string name="custom_token_creation_error_wrong_decimals">Десятичное число должно быть действительным целым числом, до %li</string>
<string name="custom_token_custom_derivation">Своя деривация</string>
<string name="custom_token_custom_derivation_placeholder">Например m/00\'/0000\'/0\'/0/0</string>
<string name="custom_token_custom_derivation_title">Введите свою деривацию</string>
@ -179,7 +163,6 @@
<string name="details_manage_security_passcode_description">Перед выполнением любой команды, влекущей за собой изменение состояния карты, вам необходимо будет ввести пароль.</string>
<string name="details_referral_title">Реферальная программа</string>
<string name="details_row_description_flip_to_hide">Переверните экран вашего устройства вниз, чтобы быстро скрыть и отобразить балансы</string>
<string name="details_row_privacy_policy">Privacy policy</string>
<string name="details_row_subtitle_signed_hashes_format">%s хэшей</string>
<string name="details_row_title_cid">Номер карты</string>
<string name="details_row_title_contact_to_support">Обратиться в поддержку</string>
@ -191,18 +174,17 @@
<string name="details_title">Подробности</string>
<string name="disclaimer_error_loading">Проверьте подключение с интернетом или переключитесь на другую сеть</string>
<string name="disclaimer_title">Условия использования</string>
<string name="error_update_app">К сожалению, текущая версия приложения не готова к работе с этой картой, проверьте наличие обновлений</string>
<string name="error_wrong_wallet_tapped">Вы использовали карту от другого кошелька. Приложите карту, связанную с этим кошельком.</string>
<string name="exchange_tokens_available_tokens_header">Мои токены</string>
<string name="exchange_tokens_empty_tokens">У вас нет добавленных токенов. Добавьте токены для обмена</string>
<string name="express_token_list_empty_search">Токены не найдены. Пожалуйста, попробуйте другой запрос</string>
<string name="exchange_tokens_unavailable_tokens_header">Недоступен для обмена с %s</string>
<string name="express_cex_fee_explanation">Кроме того, в курс обмена включена комиссия сети за отправку обмененных средств на ваш адрес</string>
<string name="express_by_provider">Предоставлено</string>
<string name="express_cex_status_button_title">Статус</string>
<string name="express_choose_providers_subtitle">Провайдеры проводят транзакции, обеспечивая плавный и эффективный обмен токенами</string>
<string name="express_choose_providers_subtitle">Tangem предоставляет доступ к обмену через сторонних поставщиков в соответствии с их правилами</string>
<string name="express_choose_providers_title">Выберите провайдера</string>
<string name="express_error_code">Произошла ошибка. Код: %s</string>
<string name="express_estimated_amount">Курс обмена</string>
<string name="express_more_providers_soon">Больше провайдеров на подходе.\nСледите за обновлениями!</string>
<string name="express_exchange_by">Обмен через %s</string>
<string name="express_exchange_notification_failed_text">Чтобы вернуть ваши деньги, посетите сайт провайдера</string>
<string name="express_exchange_notification_failed_title">Операция не выполнена провайдером</string>
<string name="express_exchange_notification_verification_text">Посетите сайт провайдера для проверки</string>
@ -222,23 +204,25 @@
<string name="express_exchange_status_sending">Отправляем </string>
<string name="express_exchange_status_sending_active">Отправка средств...</string>
<string name="express_exchange_status_sent">Отправлено</string>
<string name="express_exchange_status_verified">Верифицировано</string>
<string name="express_exchange_status_subtitle">Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий.</string>
<string name="express_exchange_status_title">Статус обмена</string>
<string name="express_exchange_status_verifying">Требуется верификация</string>
<string name="express_exchange_token_list_subtitle">Список токенов в вашем кошельке</string>
<string name="express_fetch_best_rates">Получение наилучших курсов...</string>
<string name="express_floating_rate">Плавающая ставка</string>
<string name="express_go_to_provider">К провайдеру</string>
<string name="express_legal_one_placeholder">Пользуясь сервисом вы соглашаетесь с %s</string>
<string name="express_legal_two_placeholders">Пользуясь сервисом вы соглашаетесь с %1$s и %2$s</string>
<string name="express_legal_one_placeholder">Пользуясь сервисом, вы соглашаетесь с %s</string>
<string name="express_legal_two_placeholders">Пользуясь сервисом, вы соглашаетесь с %1$s и %2$s</string>
<string name="express_more_providers_soon">Больше провайдеров на подходе.\nСледите за обновлениями!</string>
<string name="express_privacy_policy">Политикой конфиденциальности</string>
<string name="express_provider">Провайдер</string>
<string name="express_provider_best_rate">Лучший курс</string>
<string name="express_provider_min_amount">Доступно с %s</string>
<string name="express_provider_max_amount">Доступно до %s</string>
<string name="express_provider_min_amount">Доступно с %s</string>
<string name="express_provider_not_available">Недоступно для этой пары</string>
<string name="express_provider_permission_needed">Требуется разрешение</string>
<string name="express_terms_of_use">Условиями использования</string>
<string name="express_by_provider">Предоставлено</string>
<string name="express_token_list_empty_search">Токены не найдены. Пожалуйста, попробуйте другой запрос</string>
<string name="express_transaction_id_copied">ID транзакции скопирован</string>
<string name="feedback_data_collection_message">Информация ниже не является обязательной. Вы можете стереть её, если хотите.</string>
<string name="feedback_preface_rate_negative">Расскажите, каких функций вам не хватает, и мы постараемся вам помочь.</string>
<string name="feedback_preface_scan_failed">Скажите, пожалуйста, какая у вас карта?</string>
@ -254,25 +238,12 @@
<string name="initial_message_change_access_code_body">Чтобы изменить код доступа, приложите карту как показано выше и не убирайте до окончания операции</string>
<string name="initial_message_change_passcode_body">Чтобы изменить пароль, приложите карту как показано выше и не убирайте до окончания операции</string>
<string name="initial_message_create_wallet_body">Чтобы создать кошелек, приложите карту как показано выше и не убирайте до окончания операции</string>
<string name="initial_message_purge_wallet_body">Чтобы сбросить настройки до заводских, приложите карту как показано выше и не убирайте до окончания операции</string>
<string name="initial_message_scan_header">Приложите, чтобы отсканировать</string>
<string name="initial_message_sign_body">Чтобы подписать транзакцию, приложите карту как показано выше и не убирайте до окончания операции</string>
<string name="initial_message_sign_header">Приложите, чтобы подписать</string>
<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_empty_tokens_list_message">Чтобы начать отслеживать свои криптоактивы и транзакции, добавьте токены</string>
<string name="main_get_bonus_subtitle">Вы успешно прошли все уроки и теперь можете получить 1INCH токены</string>
<plurals name="main_learn_subtitle">
<item quantity="one">Пройдите 3 урока и получите %d 1INCH токен на свой кошелек</item>
<item quantity="few">Пройдите 3 урока и получите %d 1INCH токена на свой кошелек</item>
<item quantity="many">Пройдите 3 урока и получите %d 1INCH токенов на свой кошелек</item>
<item quantity="other">Пройдите 3 урока и получите %d 1INCH токенов на свой кошелек</item>
</plurals>
<string name="main_manage_tokens">Управление токенами</string>
<string name="main_page_balance">Баланс</string>
<string name="main_processing_full_amount">В сумме учтены не все монеты</string>
<string name="main_promotion_credited">1INCH токены будут зачислены на адрес вашего кошелька в сети %s в течение 48 часов</string>
<string name="main_scan_card_warning_view_subtitle">Чтобы получить доступ ко всем сетям, вам необходимо отсканировать карту</string>
<string name="main_scan_card_warning_view_title">Отсканируйте карту</string>
<string name="main_swap_changelly_promotion_message">Обменивайте свои токены с %1$s комиссии провайдера через Changelly с %2$s по %3$s февраля.</string>
@ -280,10 +251,10 @@
<string name="main_tokens">Токены</string>
<string name="manage_tokens_add">Добавить</string>
<string name="manage_tokens_edit">Изменить</string>
<string name="manage_tokens_list_header_title">Рыночная капитализация</string>
<string name="manage_tokens_network_selector_native_title">Основная сеть</string>
<string name="manage_tokens_network_selector_non_native_subtitle">Не основной или основной блокчейн, на котором размещен токен</string>
<string name="manage_tokens_network_selector_non_native_title">Не основные сети</string>
<string name="manage_tokens_network_selector_other_title">Сети</string>
<string name="manage_tokens_network_selector_title">Выберите сети</string>
<string name="manage_tokens_network_selector_wallet">Кошелек</string>
<string name="manage_tokens_nothing_found">Не удалось найти этот токен, вы можете добавить его вручную.</string>
@ -294,7 +265,6 @@
<item quantity="other">%1$d из %2$d кошельков</item>
</plurals>
<string name="manage_tokens_search_placeholder">например Bitcoin</string>
<string name="manage_tokens_title">Рыночная капитализация</string>
<string name="manage_tokens_unavailable_description">Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление.</string>
<string name="manage_tokens_unavailable_vote">Голосовать</string>
<string name="manage_tokens_wallet_selector_title">Выберите кошелек</string>
@ -314,14 +284,13 @@
<string name="onboarding_activation_error_title">Ошибка активации</string>
<string name="onboarding_alert_message_not_max_backup_cards_added">Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить?</string>
<string name="onboarding_backup_exit_warning">Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас.</string>
<string name="onboarding_bottom_sheet_passphrase_description">Кодовая фраза — это расширенная функция безопасности, которую используют криптокошельки. Она добавляет дополнительное слово или фразу по вашему выбору к уже существующей seed - фразе, чтобы разблокировать совершенно новый набор адресов.</string>
<string name="onboarding_button_add_backup_card">Добавить резервную карту</string>
<string name="onboarding_button_backup_card_format">Сканировать карту #%d</string>
<string name="onboarding_button_backup_now">Создать резервную копию</string>
<string name="onboarding_button_backup_origin">Сканировать основную карту</string>
<string name="onboarding_button_claim">Запросить</string>
<string name="onboarding_button_continue_wallet">Перейти к моему кошельку</string>
<string name="onboarding_button_finalize_backup">Завершение бэкапа</string>
<string name="onboarding_button_pin">Установить Код доступа</string>
<string name="onboarding_button_receive_crypto">Получить криптовалюту</string>
<string name="onboarding_button_scan_origin_card">Сканировать основную карту</string>
<string name="onboarding_button_skip_backup">Пропустить</string>
@ -338,12 +307,22 @@
<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_pin">Код доступа</string>
<string name="onboarding_navbar_register_wallet">Подключиться</string>
<string name="onboarding_navbar_title_creating_backup">Резервная копия</string>
<string name="onboarding_seed_button_read_more">Прочитать о seed-фразе</string>
<string name="onboarding_seed_generate_message">Запишите эти 12 слов в порядке, указанном ниже, и сохраните их в надежном месте.</string>
<plurals name="onboarding_seed_generate_message_words_count">
<item quantity="one"></item>
<item quantity="few">Запишите эти %d слова в порядке, указанном ниже, и сохраните их в надежном месте.</item>
<item quantity="many">Запишите эти %d слов в порядке, указанном ниже, и сохраните их в надежном месте.</item>
<item quantity="other">Запишите эти %d слов в порядке, указанном ниже, и сохраните их в надежном месте.</item>
</plurals>
<string name="onboarding_seed_generate_title">Ваша seed-фраза</string>
<plurals name="onboarding_seed_generate_words_count">
<item quantity="one"></item>
<item quantity="few">%d слова</item>
<item quantity="many">%d слов</item>
<item quantity="other">%d слов</item>
</plurals>
<string name="onboarding_seed_import_message">Чтобы импортировать кошелек, введите seed-фразу в поле ниже</string>
<string name="onboarding_seed_intro_button_generate">Создать seed-фразу</string>
<string name="onboarding_seed_intro_button_import">Импорт кошелька</string>
@ -354,23 +333,17 @@
<string name="onboarding_seed_phrase_intro_legacy">Устаревший</string>
<string name="onboarding_seed_user_validation_message">Чтобы проверить, правильно ли вы записали seed-фразу, введите 2-е, 7-е и 11-е слова</string>
<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_no_backup_cards">Чтобы начать процесс резервного копирования, добавьте одну или две резервные карты.</string>
<string name="onboarding_subtitle_one_backup_card">Вы можете добавить еще одну карту или завершить процесс резервного копирования</string>
<string name="onboarding_subtitle_scan_backup_card_format">Подготовьте резервную карту с номером %s</string>
<string name="onboarding_subtitle_scan_primary">Отсканируйте основную карту, чтобы начать процесс резервного копирования.</string>
<string name="onboarding_subtitle_scan_primary_card_format">Подготовьте основную карту с номером %s</string>
<string name="onboarding_subtitle_success_claim">Поздравляем! Ваша платежная крипто карта теперь активирована!</string>
<string name="onboarding_subtitle_success_tangem_wallet_onboarding">Ваша карта настроена и готова к использованию.</string>
<string name="onboarding_subtitle_two_backup_cards">Добавлено максимальное количество карт. Завершите процесс резервного копирования.</string>
<string name="onboarding_title">Активация карты</string>
<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_no_backup_cards">Нет резервных карт</string>
<string name="onboarding_title_one_backup_card">Добавлена ​​одна резервная карта</string>
<string name="onboarding_title_pin">Код доступа</string>
<string name="onboarding_title_scan_origin_card">Подготовьте свою карту</string>
<string name="onboarding_title_two_backup_cards">Добавлены две резервные карты</string>
<string name="onboarding_top_up_body">Пополните кошелек на любую сумму, чтобы начать пользоваться картой</string>
@ -392,7 +365,7 @@
<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="rate_app_sheet_title">Нравится наше приложение?</string>
<string name="receive_bottom_sheet_warning_message">%1$s (%2$s) в сети %3$s</string>
<string name="receive_bottom_sheet_warning_message_full">Отправляйте только %s на этот адрес. Использование другой сети может привести к утрате средств.</string>
<string name="referral_button_participate">Участвовать</string>
@ -410,8 +383,6 @@
<item quantity="other">за %d кошельков</item>
</plurals>
<string name="referral_point_currencies_description">Получите ^^%1$s^^ на ваш адрес в сети %2$s %3$s ^^спустя 30 дней^^ за каждый кошелек, который купит ваш друг</string>
<string name="referral_point_currencies_description_prefix">Получите</string>
<string name="referral_point_currencies_description_suffix">на ваш адрес в сети %1$s%2$s за каждый кошелек, который купит ваш друг</string>
<string name="referral_point_currencies_title">Вы</string>
<string name="referral_point_discount_description_prefix">Получит</string>
<string name="referral_point_discount_description_suffix">при покупке кошелька на сайте tangem.com</string>
@ -430,7 +401,6 @@
<item quantity="many">%d кошельков</item>
<item quantity="other">%d кошельков</item>
</plurals>
<string name="registration_task_alert_message">Пожалуйста, удерживайте карту до завершения операции</string>
<string name="reset_card_to_factory_button_title">Сбросить карту</string>
<string name="reset_card_to_factory_condition_1">Я понимаю, что после выполнения этого действия у меня больше не будет доступа к текущему кошельку</string>
<string name="reset_card_to_factory_condition_2">Я понимаю, что не смогу этой картой восстановить пароль на остальных картах этого кошелька, если я его забуду</string>
@ -447,17 +417,18 @@
<string name="save_user_wallet_agreement_enroll_biometrics_title">Включите биометрическую аутентификацию</string>
<string name="save_user_wallet_agreement_header_biometrics">Вы хотите использовать биометрию?</string>
<string name="save_user_wallet_agreement_notice">Обратите внимание, что для совершения транзакции с вашими средствами по-прежнему потребуется ваша карта</string>
<string name="scan_card_error">Ошибка сканирования карты. Попробуй снова.</string>
<string name="scan_card_settings_button">Сканировать</string>
<string name="scan_card_settings_message">Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку.</string>
<string name="scan_card_settings_title">Приготовьте свою карту</string>
<string name="send_additional_field_already_included">Уже содержится в введенном адресе</string>
<string name="send_amount_label">Сумма</string>
<string name="send_alert_transaction_failed_title">Транзакция не выполнена</string>
<string name="send_alert_transaction_failed_text">Причина: %1$s\nКод: %2$s</string>
<string name="send_alert_fee_coverage_title">Недостаточно средств для покрытия комиссии сети. Вычесть комиссию из отправляемой сумму?</string>
<string name="send_alert_fee_coverage_subract_text">Вычесть</string>
<string name="send_date_format">%1$s в %2$s</string>
<string name="send_alert_fee_coverage_title">Недостаточно средств для покрытия комиссии сети. Вычесть комиссию из отправляемой сумму?</string>
<string name="send_alert_fee_too_low_text">Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить?</string>
<string name="send_alert_transaction_failed_text">Причина: %1$s\nКод: %2$s</string>
<string name="send_alert_transaction_failed_title">Транзакция не выполнена</string>
<string name="send_amount_label">Сумма</string>
<string name="send_confirm_label">Подтвердить</string>
<string name="send_date_format">%1$s, %2$s</string>
<string name="send_destination_hint_address">Адрес</string>
<string name="send_destination_tag_field">Код назначения</string>
<string name="send_enter_address_field">Введите адрес</string>
@ -477,30 +448,30 @@
<string name="send_max_amount_label">Максимальная сумма</string>
<string name="send_max_fee">Комиссия не превысит </string>
<string name="send_max_fee_footer">Максимальная cумма комиссии</string>
<string name="common_network_fee_title">Сетевая комиссия</string>
<string name="send_network_fee_warning_content">Сумма отправки будет уменьшена на %1$s для покрытия выбранного уровня комиссии. Получателю будет отправлено %2$s.</string>
<string name="send_network_fee_warning_title">Покрытие сетевой комиссии</string>
<string name="send_notification_exceed_balance_text">Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса</string>
<string name="send_notification_exceed_balance_title">Недостаточно средств</string>
<string name="send_notification_high_fee_title">Увеличение комиссии</string>
<string name="send_notification_high_fee_text">Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01.</string>
<string name="send_notification_fee_too_high_title">Установлена высокая комиссия</string>
<string name="send_notification_fee_too_high_text">Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.</string>
<string name="send_notification_fee_too_high_title">Установлена высокая комиссия</string>
<string name="send_notification_high_fee_text">Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %s.</string>
<string name="send_notification_high_fee_title">Комиссия увеличилась</string>
<string name="send_notification_invalid_amount_text">Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению</string>
<string name="send_notification_invalid_amount_title">Недопустимая сумма</string>
<string name="send_notification_invalid_minimum_amount_text">Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s.</string>
<string name="send_notification_invalid_reserve_amount_text">Адрес получателя не активирован. Пожалуйста, измените сумму отправки, чтобы продолжить.</string>
<string name="send_notification_invalid_reserve_amount_title">Сумма отправки не может быть менее %1$s</string>
<string name="send_notification_invalid_minimum_amount_text">Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %2$s.</string>
<string name="send_notification_invalid_reserve_amount_text">Адрес получателя не активирован. \nПожалуйста, измените сумму отправки, чтобы продолжить.</string>
<string name="send_notification_invalid_reserve_amount_title">Сумма отправки не может быть менее %s</string>
<string name="send_notification_reduce_by">Уменьшить на %s</string>
<string name="send_notification_reduce_to">Уменьшить до %s</string>
<string name="send_notification_transaction_delay_text">Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции</string>
<string name="send_notification_transaction_delay_title">Возможны задержки по транзакции</string>
<string name="send_notifiaction_transaction_limit_title">Лимит транзакции</string>
<string name="send_notification_transaction_limit_text">Из-за ограничений %1$s в одну транзакцию может поместиться только %2$s UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму.</string>
<string name="send_notification_reduce_by">Уменьшить до %s</string>
<string name="send_notification_reduce_to">Уменьшить на %s</string>
<string name="send_notification_transaction_limit_title">Лимит транзакции</string>
<string name="send_optional_field">Необязательное</string>
<string name="send_recent_transactions">Последние</string>
<string name="send_recipient">Получатель</string>
<string name="send_recipient_address_footer">Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов</string>
<string name="send_recipient_label">Отправить</string>
<string name="send_recipient_memo_footer">Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств.</string>
<string name="send_recipient_wallets_title">Мои кошельки</string>
<string name="send_title_currency_format">Отправка %s</string>
@ -510,71 +481,44 @@
<string name="send_total_subtitle_format">%s будет отправлено</string>
<string name="send_transaction_success">Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время</string>
<string name="send_validation_invalid_address">Неверный адрес</string>
<string name="shop_buy_now">Купить сейчас</string>
<string name="shop_i_have_a_promo_code">У меня есть промо-код…</string>
<string name="shop_one_wallet">Tangem Wallet</string>
<string name="shop_other_payment_methods">Другие способы оплаты</string>
<string name="shop_pre_order_now">Сделать предзаказ</string>
<string name="shop_total">Итого</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>
<string name="story_backup_title">Все ключи в безопасности</string>
<string name="story_currencies_description">Аппаратный кошелек для ваших биткоинов, эфира и многих других валют одновременно — все в одной карте</string>
<string name="story_currencies_title">Тысячи криптовалют</string>
<string name="story_finish_description">Используйте его на ходу, в любом месте, в любое время. Без проводов и батареек. Как только понадобится крипта, просто приложите карту к телефону.</string>
<string name="story_finish_title">Кошелек для каждого</string>
<string name="story_learn_description">Пройдите 3 урока, получите скидку на покупку Tangem Wallet и 1INCH токены</string>
<string name="story_learn_learn">Пройти обучение</string>
<string name="story_meet_title">Встречайте Tangem</string>
<string name="story_web3_description">Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах</string>
<string name="story_web3_title">Поддержка Web 3.0</string>
<string name="swapping_alert_cex_description">В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя</string>
<string name="swapping_alert_dex_description">В сумму включена комиссия провайдера сервиса.</string>
<string name="swapping_alert_title">Комиссии</string>
<string name="swapping_approve_information_text">Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен.</string>
<string name="swapping_approve_information_title">Подтвердить</string>
<string name="swapping_error_wrapper">Ошибка: %s</string>
<string name="swapping_from_title">Вы отправляете</string>
<string name="swapping_give_permission">Дать разрешение</string>
<string name="swapping_high_price_impact_description">Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму.</string>
<string name="swapping_insufficient_funds">Недостаточно средств</string>
<string name="swapping_alert_title">Комиссии</string>
<string name="swapping_alert_cex_description">В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя</string>
<string name="swapping_alert_dex_description">В сумму включена комиссия провайдера сервиса.</string>
<string name="swapping_network_fee_warning_content">Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии</string>
<string name="swapping_not_enough_funds_for_fee">Недостаточно средств для оплаты комиссии на вашем %1$s кошельке для создания транзакции. Сначала пополните свой %2$s кошелек.</string>
<string name="swapping_pending_transaction_subtitle">Транзакция в процессе…</string>
<string name="swapping_pending_transaction_title">Подождите</string>
<string name="swapping_permission_buttons_approve">Подтвердить</string>
<string name="swapping_permission_current_transaction">Текущая транзакция</string>
<string name="swapping_permission_fee_footer">Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена.</string>
<string name="swapping_permission_header">Дать разрешение</string>
<string name="swapping_permission_policy_type_footer">Укажите лимит доступа к выбранному токену</string>
<string name="swapping_permission_rows_amount">Количество %s</string>
<string name="swapping_permission_rows_spender">Отправитель</string>
<string name="swapping_permission_rows_your_wallet">Ваш кошелек</string>
<string name="swapping_permission_subheader">Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s</string>
<string name="swapping_permission_unlimited">Безлимитно</string>
<string name="swapping_success_view_explorer_button_title">Открыть в обозревателе</string>
<string name="swapping_success_view_title">В процессе</string>
<string name="swapping_swap_action">Обменять</string>
<string name="swapping_swap_of_to">Обмен %s на</string>
<string name="swapping_tangem_fee_disclaimer">Котировки включают дополнительную комиссию Tangem в размере %s. Это помогает нам предоставлять первоклассный продукт.</string>
<string name="swapping_to_title">Вы получите</string>
<string name="swapping_token_list_other_tokens">Другие токены</string>
<string name="swapping_token_list_title">Выберите токен</string>
<string name="swapping_token_list_your_tokens">Ваши токены</string>
<string name="swapping_token_not_available">не доступен</string>
<string name="toast_balances_hidden">Балансы скрыты</string>
<string name="toast_balances_shown">Балансы показаны</string>
<string name="toast_undo">Отменить</string>
<plurals name="token_count">
<item quantity="one">%d токен</item>
<item quantity="few">%d токена</item>
<item quantity="many">%d токенов</item>
<item quantity="other">%d токенов</item>
</plurals>
<string name="token_details_generate_xpub">Сгенерировать XPUB</string>
<string name="token_details_hide_alert_hide">Скрыть</string>
<string name="token_details_hide_alert_message">Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами.</string>
<string name="token_details_hide_alert_title">Скрыть %s</string>
@ -582,7 +526,6 @@
<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>
<string name="token_swap_changelly_promotion_message">Обменивайте этот токен на другие с %1$s комиссии за обслуживание с %2$s по %3$s февраля.</string>
<string name="token_swap_changelly_promotion_title">Обмен с Changelly, %s комиссии</string>
<string name="token_swap_promotion_button">Обменять</string>
@ -594,7 +537,6 @@
<string name="transaction_history_operation">Операция</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="twin_error_wrong_twin">Вы отсканировали не ту twin-карту. Пожалуйста, попробуйте отсканировать другую</string>
<string name="twins_onboarding_description_format">Это карта, которую вы держите в руках. У парной карты номер %s.\n\nОбе карты можно использовать для вывода средств из этого кошелька.</string>
@ -609,30 +551,18 @@
<string name="unlock_wallet_description_full">Используйте %s или отсканируйте карту, чтобы получить доступ к своему кошельку</string>
<string name="user_wallet_list_add_button">Добавить новый кошелек</string>
<string name="user_wallet_list_delete_prompt">Вы уверены, что хотите удалить этот кошелек?</string>
<string name="user_wallet_list_editing_count">%d выбрано</string>
<string name="user_wallet_list_error_unable_to_unlock">Произошла ошибка, пожалуйста, отсканируйте свою карту для входа</string>
<string name="user_wallet_list_error_wallet_already_saved">Этот кошелек уже был сохранен, вы можете добавить другой</string>
<string name="user_wallet_list_multi_header">Мультивалютные</string>
<string name="user_wallet_list_rename_popup_placeholder">Имя кошелька</string>
<string name="user_wallet_list_rename_popup_title">Переименование кошелька</string>
<string name="user_wallet_list_single_header">Одновалютные</string>
<string name="user_wallet_list_title">Мои кошельки</string>
<string name="user_wallet_list_unlock_all">Разблокировать все</string>
<string name="user_wallet_list_unlock_all_with">Разблокировать все с %s</string>
<string name="wallet_address_button_explore">История транзакций</string>
<string name="wallet_balance_blockchain_unreachable_try_later">Блокчейн недоступен. Попробуйте позже.</string>
<string name="wallet_balance_loading">Баланс загружается…</string>
<string name="wallet_balance_missing_derivation">Отсканируйте карту</string>
<string name="wallet_balance_tx_in_progress">Транзакция подтверждается…</string>
<string name="wallet_balance_verified">Подтвержденный баланс</string>
<string name="wallet_button_actions">Действия</string>
<string name="wallet_choice_wallet_option_title">Что отправить?</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>
@ -646,8 +576,6 @@
<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_no_sessions_message">Нет открытых сессий WalletConnect</string>
<string name="wallet_connect_no_sessions_title">Упс. Нет сессий.</string>
<string name="wallet_connect_pairing_error">Не удалось создать пару WalletConnect: %1$s</string>
@ -659,31 +587,19 @@
<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_session_not_found">Не удалось найти сессию для обработки запроса</string>
<string name="wallet_connect_sessions_title">Сессии 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>
<string name="wallet_error_unsupported_blockchain_subtitle">Ваша карта Tangem предназначена для работы с другим приложением. Пожалуйста, ознакомьтесь с названием и инструкциями на вашей карте и установите правильное приложение.</string>
<string name="wallet_marketplace_block_title">Рыночная цена %s</string>
<string name="wallet_marketprice_block_update_time">за 24 часа</string>
<string name="wallet_network_group_title">Сеть %s</string>
<string name="wallet_notification_address_copied">Адрес скопирован в буфер обмена</string>
<string name="wallet_notification_no_internet">Нет соединения с интернетом</string>
<string name="wallet_pending_tx_receiving">Получение</string>
<string name="wallet_pending_tx_receiving_address_format">от %s</string>
<string name="wallet_pending_tx_sending">Отправка</string>
<string name="wallet_pending_tx_sending_address_format">к %s</string>
<string name="wallet_title">Tangem</string>
<string name="warning_access_denied_message">Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку</string>
<string name="warning_backup_errors_message">Пожалуйста, выведите все средства из этого кошелька, сбросьте его к заводским настройкам и создайте новый. Доступ к текущему кошельку будет утерян.</string>
<string name="warning_backup_errors_title">Ошибка активации</string>
<string name="warning_button_could_be_better">Можно лучше</string>
<string name="warning_button_learn_more">Узнать больше</string>
<string name="warning_button_like_it">Нравится</string>
<string name="warning_button_ok">Понятно!</string>
<string name="warning_button_really_cool">Очень круто!</string>
@ -696,14 +612,17 @@
<string name="warning_existential_deposit_title">Для работы с сетью необходим депозит</string>
<string name="warning_express_active_transaction_message">Обмен будет доступен после завершения %s транзакции</string>
<string name="warning_express_active_transaction_title">У вас есть активная транзакция</string>
<string name="warning_express_approval_in_progress_message">Разрешение обмена в процессе и будет скоро завершено</string>
<string name="warning_express_approval_in_progress_title">Разрешение в процессе</string>
<string name="warning_express_dust_message">Минимальная сумма обмена - %1$s. Пожалуйста, убедитесь, что остаток после обмена также не будет меньше %2$s.</string>
<string name="warning_express_no_exchangeable_coins_description">У вас в списке нет монет доступных для обмена с %s</string>
<string name="warning_express_no_exchangeable_coins_title">Нет доступных токенов для обмена</string>
<string name="warning_express_no_exchangeable_coins_title">Нет доступных для обмена токенов</string>
<string name="warning_express_not_enough_fee_for_token_tx_description">Чтобы совершить транзакцию, вам необходимо внести немного %1$s %2$s</string>
<string name="warning_express_not_enough_fee_for_token_tx_title">Невозможно покрыть комиссию %s</string>
<string name="warning_express_refresh_required_title">Cервис временно недоступен</string>
<string name="warning_express_wrong_amount_description">Пожалуйста, измените сумму для обмена</string>
<string name="warning_express_too_minimal_amount_title">Сумма для обмена должна быть не менее %s</string>
<string name="warning_express_too_maximum_amount_title">Сумма для обмена должна быть не более %s</string>
<string name="warning_express_too_minimal_amount_title">Сумма для обмена должна быть не менее %s</string>
<string name="warning_express_wrong_amount_description">Пожалуйста, измените сумму для обмена</string>
<string name="warning_failed_to_verify_card_message">Возможно, данная карта - образец или подделка</string>
<string name="warning_failed_to_verify_card_title">Ошибка проверки подлинности</string>
<string name="warning_low_signatures_message">На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства.</string>
@ -723,14 +642,16 @@
<string name="warning_no_backup_title">Резервная копия отсутствует</string>
<string name="warning_number_of_signed_hashes_incorrect_message">Эта карта ранее использовалась для подписи транзакций. Если она получена от ненадежного источника, рассмотрите возможность вывода своих средств. Если это ваша карта, дополнительных действий не требуется.</string>
<string name="warning_number_of_signed_hashes_incorrect_title">Карта уже подписывала транзакции</string>
<string name="warning_old_device_old_card_title">Устройство несовместимо</string>
<string name="warning_rate_app_message">Ваш отзыв мотивирует нас сделать кошелек Tangem еще лучше</string>
<string name="warning_rate_app_title">Нравится Tangem?</string>
<string name="warning_rent_fee_title">Необходима плата за аренду сети</string>
<string name="warning_send_blocked_funds_for_fee_message">%1$s - это монета в сети %2$s. Для совершения транзакции %3$s, вам необходимо внести немного %4$s (%5$s), чтобы покрыть комиссию сети.</string>
<string name="warning_send_blocked_funds_for_fee_title">Недостаточно %1$s для оплаты комиссии сети</string>
<string name="warning_send_blocked_pending_transactions_in_blockchain_message">Отправка средств станет доступной после завершения транзакции(-ий) в сети %s</string>
<string name="warning_send_blocked_pending_transactions_message">Отправка средств станет доступной после завершения транзакции %s</string>
<string name="warning_send_blocked_pending_transactions_title">Транзакция в обработке</string>
<string name="warning_solana_fee_message">Сеть Солана испытывает высокую нагрузку. Если Ваша транзакция не прошла в течение 2 минут, повторите её отправку.</string>
<string name="warning_solana_fee_title">Оповещение сети Солана</string>
<string name="warning_solana_rent_fee_message">Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату.</string>
<string name="warning_some_networks_unreachable_message">Некоторые сети в настоящее время недоступны. Пожалуйста, повторите попытку позже.</string>
<string name="warning_some_networks_unreachable_title">Некоторые сети недоступны</string>
@ -747,7 +668,4 @@
<string name="welcome_unlock_card">Сканировать карту</string>
<string name="welcome_unlock_description">Используйте %s или отсканируйте карту для входа в приложение</string>
<string name="welcome_unlock_title">C возвращением!</string>
<string name="express_exchange_by">Обмен через %s</string>
<string name="express_exchange_status_subtitle">Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий.</string>
<string name="express_exchange_status_title">Статус обмена</string>
</resources>

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