Updated on 2026-08-14
This commit is contained in:
commit
b76e1bbc45
219 changed files with 4585 additions and 5271 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import java.io.File
|
|||
* </intent>
|
||||
* </queries>
|
||||
*/
|
||||
@Deprecated("Use EmailSender instead")
|
||||
fun Activity.sendEmail(
|
||||
email: String,
|
||||
subject: String,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.tap.common.shop.data
|
||||
|
||||
data class TotalSum(
|
||||
val finalValue: String? = null,
|
||||
val beforeDiscount: String? = null,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -120,4 +120,10 @@ internal object SettingsDomainModule {
|
|||
): ShouldShowSwapPromoTokenUseCase {
|
||||
return ShouldShowSwapPromoTokenUseCase(swapPromoRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideDeleteDeprecatedLogsUseCase(settingsRepository: SettingsRepository): DeleteDeprecatedLogsUseCase {
|
||||
return DeleteDeprecatedLogsUseCase(settingsRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,4 +18,5 @@ internal data class UserWalletPublicInformation(
|
|||
val cardsInWallet: Set<String>,
|
||||
val scanResponse: ScanResponse,
|
||||
val isMultiCurrency: Boolean,
|
||||
val hasBackupError: Boolean = false,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package com.tangem.tap.features.shop.domain.models
|
||||
|
||||
sealed class SalesError {
|
||||
data class DataError(val cause: Throwable) : SalesError()
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 -> {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.tangem.tap.network.exchangeServices.moonpay.models
|
||||
|
||||
internal data class MoonPaySupportedCurrency(val networkCode: String, val currencyCode: String?)
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -37,6 +37,7 @@ internal class DefaultDerivationsRepositoryTest {
|
|||
cardsInWallet = setOf(),
|
||||
isMultiCurrency = false,
|
||||
scanResponse = ScanResponseMockFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()),
|
||||
hasBackupError = false,
|
||||
)
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -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?,
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ data class NavigationState(
|
|||
|
||||
enum class AppScreen(val isDialogFragment: Boolean = false) {
|
||||
Home,
|
||||
Shop,
|
||||
Disclaimer,
|
||||
OnboardingNote,
|
||||
OnboardingWallet,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue