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,7 +147,21 @@ internal class DetailsViewModel(
|
|||
|
||||
private fun sendFeedback() {
|
||||
Analytics.send(Basic.ButtonSupport())
|
||||
store.dispatchOnMain(GlobalAction.SendEmail(FeedbackEmail()))
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
if (userStatus.countryCode == "USA") {
|
||||
if (!currencyStatus.isSupportedInUS) return@forEach
|
||||
if (currencyStatus.notAllowedUSStates.contains(userStatus.stateCode)) return@forEach
|
||||
}
|
||||
val currencyCode = currencyStatus.code.uppercase()
|
||||
// currenciesToBuy.add(currencyCode)
|
||||
|
||||
if (currencyStatus.isSellSupported) currenciesToSell.add(currencyCode)
|
||||
val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) {
|
||||
is Result.Failure -> return@performRequest
|
||||
is Result.Success -> result.data
|
||||
}
|
||||
// currenciesToBuy.sort()
|
||||
currenciesToSell.sort()
|
||||
|
||||
val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) {
|
||||
is Result.Failure -> return@performRequest
|
||||
is Result.Success -> result.data
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
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) {
|
||||
is Currency.Blockchain -> {
|
||||
val blockchain = currency.blockchain
|
||||
when {
|
||||
blockchain.isTestnet() -> false
|
||||
blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC -> false
|
||||
else -> availableForSell.contains(currency.currencySymbol)
|
||||
val availableForSell = status?.availableForSell ?: return false
|
||||
|
||||
val supportedCurrency = currency.blockchain.moonPaySupportedCurrency ?: return false
|
||||
return availableForSell.any {
|
||||
when (currency) {
|
||||
is Currency.Blockchain -> {
|
||||
it.networkCode.equals(other = supportedCurrency.networkCode, ignoreCase = true) &&
|
||||
it.currencyCode.equals(other = supportedCurrency.currencyCode, ignoreCase = true)
|
||||
}
|
||||
is Currency.Token -> {
|
||||
it.networkCode.equals(other = supportedCurrency.networkCode, ignoreCase = true) &&
|
||||
it.contractAddress.equals(other = currency.token.contractAddress, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
is Currency.Token -> {
|
||||
metadata?.any { it?.contractAddress.equals(currency.token.contractAddress, ignoreCase = true) } ?: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,72 +1,59 @@
|
|||
<?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>
|
||||
<string name="common_cancel">Abbrechen</string>
|
||||
<string name="common_delete">Entfernen</string>
|
||||
<string name="common_done">Erledigt</string>
|
||||
<string name="common_error">Fehler</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>
|
||||
<string name="details_manage_security_long_tap_description">Dieser Mechanismus schützt vor Annäherungsangriffen auf eine Karte. Es wird eine Verzögerung zwischen dem Empfang und der Ausführung eines Befehls erzwungen. Nach der ersten signierten Transaktion wird dieses Telefon mit der Karte verknüpft und die Transaktionen werden sofort signiert</string>
|
||||
<string name="details_manage_security_passcode">Passcode</string>
|
||||
<string name="details_manage_security_passcode_description">Bevor Sie einen Befehl ausführen, der eine Änderung des Kartenstatus zur Folge hat, müssen Sie den Passcode eingeben.</string>
|
||||
<string name="details_row_subtitle_signed_hashes_format">%s Hasch</string>
|
||||
<string name="details_row_title_cid">KartenID</string>
|
||||
<string name="details_row_title_currency">App Währung</string>
|
||||
<string name="details_row_title_issuer">Emittent</string>
|
||||
<string name="details_row_title_signed_hashes">Signiert</string>
|
||||
<string name="details_title">Details</string>
|
||||
<string name="disclaimer_title">Nutzungsbedingungen</string>
|
||||
<string name="home_button_scan">Karte scannen</string>
|
||||
<string name="initial_message_change_access_code_body">Tippen Sie um den Zugangscode zu ändern</string>
|
||||
<string name="initial_message_change_passcode_body">Tippen Sie um den Passcode zu ändern</string>
|
||||
<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>
|
||||
<string name="send_error_address_same_as_wallet">Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein</string>
|
||||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_include_description">inkl. Gebühr</string>
|
||||
<string name="send_fee_picker_low">Niedrig</string>
|
||||
<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>
|
||||
<string name="alert_unsupported_card">Diese Karte ist für die Zusammenarbeit mit Tangem nicht geeignet</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>
|
||||
<string name="common_cancel">Abbrechen</string>
|
||||
<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="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>
|
||||
<string name="details_manage_security_long_tap_description">Dieser Mechanismus schützt vor Annäherungsangriffen auf eine Karte. Es wird eine Verzögerung zwischen dem Empfang und der Ausführung eines Befehls erzwungen. Nach der ersten signierten Transaktion wird dieses Telefon mit der Karte verknüpft und die Transaktionen werden sofort signiert</string>
|
||||
<string name="details_manage_security_passcode">Passcode</string>
|
||||
<string name="details_manage_security_passcode_description">Bevor Sie einen Befehl ausführen, der eine Änderung des Kartenstatus zur Folge hat, müssen Sie den Passcode eingeben.</string>
|
||||
<string name="details_row_subtitle_signed_hashes_format">%s Hasch</string>
|
||||
<string name="details_row_title_cid">KartenID</string>
|
||||
<string name="details_row_title_currency">App Währung</string>
|
||||
<string name="details_row_title_issuer">Emittent</string>
|
||||
<string name="details_row_title_signed_hashes">Signiert</string>
|
||||
<string name="details_title">Details</string>
|
||||
<string name="disclaimer_title">Nutzungsbedingungen</string>
|
||||
<string name="home_button_scan">Karte scannen</string>
|
||||
<string name="initial_message_change_access_code_body">Tippen Sie um den Zugangscode zu ändern</string>
|
||||
<string name="initial_message_change_passcode_body">Tippen Sie um den Passcode zu ändern</string>
|
||||
<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="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>
|
||||
<string name="send_error_address_same_as_wallet">Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein</string>
|
||||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_include_description">inkl. Gebühr</string>
|
||||
<string name="send_fee_picker_low">Niedrig</string>
|
||||
<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="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="twins_recreate_toolbar">Tangem Twin</string>
|
||||
<string name="wallet_connect_title">WalletConnect</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_title">Tangem</string>
|
||||
<string name="warning_button_ok">OK, ich hab\'s!</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,72 +1,59 @@
|
|||
<?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>
|
||||
<string name="common_cancel">Annuler</string>
|
||||
<string name="common_delete">Supprimer</string>
|
||||
<string name="common_done">Exécuté</string>
|
||||
<string name="common_error">Erreur</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>
|
||||
<string name="details_manage_security_long_tap_description">Ce mécanisme protège contre les attaques sans contact sur la carte. Il y a un délai entre la réception et l\'exécution de la commande. Après la première transaction signée, ce téléphone sera associé à la carte et les transactions seront signées immédiatement.</string>
|
||||
<string name="details_manage_security_passcode">Mot de passe</string>
|
||||
<string name="details_manage_security_passcode_description">Avant d\'exécuter une commande qui modifie l\'état de la carte, vous devrez entrer un mot de passe.</string>
|
||||
<string name="details_row_subtitle_signed_hashes_format">%s hashes</string>
|
||||
<string name="details_row_title_cid">ID de la carte</string>
|
||||
<string name="details_row_title_currency">Monnaie de l\'application</string>
|
||||
<string name="details_row_title_issuer">Emetteur</string>
|
||||
<string name="details_row_title_signed_hashes">Signé</string>
|
||||
<string name="details_title">Référénces</string>
|
||||
<string name="disclaimer_title">Conditions d\'utilisation</string>
|
||||
<string name="home_button_scan">Scannez la carte</string>
|
||||
<string name="initial_message_change_access_code_body">Touchez, pour modifier le code d\'accès</string>
|
||||
<string name="initial_message_change_passcode_body">Touchez, pour modifier le mot de passe</string>
|
||||
<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>
|
||||
<string name="send_error_address_same_as_wallet">L\'adresse est la même que celle de votre portefeuille</string>
|
||||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_include_description">Inclure les commissions</string>
|
||||
<string name="send_fee_picker_low">Bas</string>
|
||||
<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>
|
||||
<string name="alert_unsupported_card">Cette carte n\'est pas conçue pour fonctionner avec Tangem</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>
|
||||
<string name="common_cancel">Annuler</string>
|
||||
<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="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>
|
||||
<string name="details_manage_security_long_tap_description">Ce mécanisme protège contre les attaques sans contact sur la carte. Il y a un délai entre la réception et l\'exécution de la commande. Après la première transaction signée, ce téléphone sera associé à la carte et les transactions seront signées immédiatement.</string>
|
||||
<string name="details_manage_security_passcode">Mot de passe</string>
|
||||
<string name="details_manage_security_passcode_description">Avant d\'exécuter une commande qui modifie l\'état de la carte, vous devrez entrer un mot de passe.</string>
|
||||
<string name="details_row_subtitle_signed_hashes_format">%s hashes</string>
|
||||
<string name="details_row_title_cid">ID de la carte</string>
|
||||
<string name="details_row_title_currency">Monnaie de l\'application</string>
|
||||
<string name="details_row_title_issuer">Emetteur</string>
|
||||
<string name="details_row_title_signed_hashes">Signé</string>
|
||||
<string name="details_title">Référénces</string>
|
||||
<string name="disclaimer_title">Conditions d\'utilisation</string>
|
||||
<string name="home_button_scan">Scannez la carte</string>
|
||||
<string name="initial_message_change_access_code_body">Touchez, pour modifier le code d\'accès</string>
|
||||
<string name="initial_message_change_passcode_body">Touchez, pour modifier le mot de passe</string>
|
||||
<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="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>
|
||||
<string name="send_error_address_same_as_wallet">L\'adresse est la même que celle de votre portefeuille</string>
|
||||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_include_description">Inclure les commissions</string>
|
||||
<string name="send_fee_picker_low">Bas</string>
|
||||
<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="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="twins_recreate_toolbar">Tangem Twin</string>
|
||||
<string name="wallet_connect_title">WalletConnect</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_title">Tangem</string>
|
||||
<string name="warning_button_ok">Ok, je l\'ai!</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,72 +1,59 @@
|
|||
<?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>
|
||||
<string name="common_cancel">Annulla</string>
|
||||
<string name="common_delete">Rimuovere</string>
|
||||
<string name="common_done">Fatto</string>
|
||||
<string name="common_error">Errore</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>
|
||||
<string name="details_manage_security_long_tap_description">Questo meccanismo protegge dagli avvicinamenti senza contatto sulla carta. Attiva un ritardo tra la ricezione e l\'esecuzione di un comando. Dopo la prima transazione firmata, questo telefono verrà associato alla carta e le transazioni verranno firmate immediatamente.</string>
|
||||
<string name="details_manage_security_passcode">Password</string>
|
||||
<string name="details_manage_security_passcode_description">Dovrai inserire una password prima di eseguire qualsiasi comando che modifichi lo stato della carta.</string>
|
||||
<string name="details_row_subtitle_signed_hashes_format">Hash %s</string>
|
||||
<string name="details_row_title_cid">ID carta</string>
|
||||
<string name="details_row_title_currency">Valuta dell\'applicazione</string>
|
||||
<string name="details_row_title_issuer">Emittente</string>
|
||||
<string name="details_row_title_signed_hashes">Firmato</string>
|
||||
<string name="details_title">Requisiti</string>
|
||||
<string name="disclaimer_title">Termini del servizio</string>
|
||||
<string name="home_button_scan">Scansiona carta</string>
|
||||
<string name="initial_message_change_access_code_body">Avvicina per modificare il codice di accesso</string>
|
||||
<string name="initial_message_change_passcode_body">Avvicina per modificare la password</string>
|
||||
<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>
|
||||
<string name="send_error_address_same_as_wallet">L\'indirizzo corrisponde all\'indirizzo del tuo portafoglio</string>
|
||||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_include_description">Includi commissione</string>
|
||||
<string name="send_fee_picker_low">Insufficiente</string>
|
||||
<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>
|
||||
<string name="alert_unsupported_card">Questa carta non è progettata per funzionare con Tangem</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>
|
||||
<string name="common_cancel">Annulla</string>
|
||||
<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="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>
|
||||
<string name="details_manage_security_long_tap_description">Questo meccanismo protegge dagli avvicinamenti senza contatto sulla carta. Attiva un ritardo tra la ricezione e l\'esecuzione di un comando. Dopo la prima transazione firmata, questo telefono verrà associato alla carta e le transazioni verranno firmate immediatamente.</string>
|
||||
<string name="details_manage_security_passcode">Password</string>
|
||||
<string name="details_manage_security_passcode_description">Dovrai inserire una password prima di eseguire qualsiasi comando che modifichi lo stato della carta.</string>
|
||||
<string name="details_row_subtitle_signed_hashes_format">Hash %s</string>
|
||||
<string name="details_row_title_cid">ID carta</string>
|
||||
<string name="details_row_title_currency">Valuta dell\'applicazione</string>
|
||||
<string name="details_row_title_issuer">Emittente</string>
|
||||
<string name="details_row_title_signed_hashes">Firmato</string>
|
||||
<string name="details_title">Requisiti</string>
|
||||
<string name="disclaimer_title">Termini del servizio</string>
|
||||
<string name="home_button_scan">Scansiona carta</string>
|
||||
<string name="initial_message_change_access_code_body">Avvicina per modificare il codice di accesso</string>
|
||||
<string name="initial_message_change_passcode_body">Avvicina per modificare la password</string>
|
||||
<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="onboarding_create_wallet_button_create_wallet">Crea portafoglio</string>
|
||||
<string name="send_amount_label">Importo</string>
|
||||
<string name="send_destination_hint_address">Indirizzo</string>
|
||||
<string name="send_error_address_same_as_wallet">L\'indirizzo corrisponde all\'indirizzo del tuo portafoglio</string>
|
||||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_include_description">Includi commissione</string>
|
||||
<string name="send_fee_picker_low">Insufficiente</string>
|
||||
<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="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="twins_recreate_toolbar">Tangem Twin</string>
|
||||
<string name="wallet_connect_title">WalletConnect</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_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>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
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