Updated on 2026-08-14

This commit is contained in:
Tangem 2024-04-16 15:50:37 +01:00
commit 71672895bb
440 changed files with 11794 additions and 8447 deletions

View file

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

View file

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

@ -1 +1 @@
Subproject commit 82f4a8aebcab924047705ad3ecd5f008f07ca0b5
Subproject commit 980cc4cb2fb9ba44cd6798191ae7d208e4f7f74c

View file

@ -638,6 +638,56 @@
"networkId": "pulsechain/test"
}
]
},
{
"id": "zksync-ethereum",
"name": "zkSync",
"symbol": "ETH",
"networks": [
{
"networkId": "zksync/test"
}
]
},
{
"id": "moonbeam",
"name": "Moonbeam",
"symbol": "GLMR",
"networks": [
{
"networkId": "moonbeam/test"
}
]
},
{
"id": "manta-network-ethereum",
"name": "Manta Testnet",
"symbol": "ETH",
"networks": [
{
"networkId": "manta-network/test"
}
]
},
{
"id": "polygon-zkevm-ethereum",
"name": "Polygon zkEvm Testnet",
"symbol": "ETH",
"networks": [
{
"networkId": "polygon-zkevm/test"
}
]
},
{
"id": "base-ethereum",
"name": "Base",
"symbol": "ETH",
"networks": [
{
"networkId": "base/test"
}
]
}
]
}

View file

@ -57,9 +57,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
@ -69,7 +66,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
@ -151,9 +147,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles
@Inject
lateinit var generalUserWalletsListManager: UserWalletsListManager
@Inject
lateinit var getPolkadotCheckHasResetUseCase: GetPolkadotCheckHasResetUseCase
@ -188,8 +181,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
setContentView(R.layout.activity_main)
initContent()
checkGooglePayAvailability()
checkForNotificationPermission()
observeStateUpdates()
observePolkadotAccountHealthCheck()
@ -272,14 +263,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
@ -393,13 +376,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) {
@ -447,11 +423,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) {
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
val isNotScannedBefore = store.state.globalState.scanResponse == null
val isOnboardingServiceNotActive = store.state.globalState.onboardingState.onboardingStarted
val isShopNotOpened = store.state.shopState.total != null
val isOnboardingServiceNotActive = !store.state.globalState.onboardingState.onboardingStarted
when {
!backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive && isShopNotOpened -> {
!backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive -> {
navigateToInitialScreen(intentWhichStartedActivity)
}
backStackIsEmpty -> {

View file

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

View file

@ -112,6 +112,11 @@ class DialogManager : StoreSubscriber<GlobalState> {
context = context,
)
}
is WalletConnectDialog.UnsupportedDapp -> SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_error_unsupported_dapp,
context = context,
)
is WalletConnectDialog.SessionProposalDialog -> {
SessionProposalDialog.create(
sessionProposal = state.dialog.sessionProposal,
@ -121,6 +126,10 @@ class DialogManager : StoreSubscriber<GlobalState> {
onReject = state.dialog.onReject,
)
}
is WalletConnectDialog.SignTransactionDialog -> SignTransactionDialog.create(
preparedData = state.dialog.data,
context = context,
)
is BackupDialog.AttestationFailed -> AttestationFailedDialog.create(context)
is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context)
is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context)

View file

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

View file

@ -5,7 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent
/**
[REDACTED_AUTHOR]
*/
sealed class WalletConnect(
internal sealed class WalletConnect(
event: String,
params: Map<String, String> = mapOf(),
error: Throwable? = null,
@ -65,7 +65,7 @@ sealed class WalletConnect(
FAIL("Fail"),
}
companion object {
private const val SUCCESS_CODE = "0"
private companion object {
const val SUCCESS_CODE = "0"
}
}

View file

@ -15,7 +15,7 @@ import com.tangem.tap.features.demo.DemoHelper
[REDACTED_AUTHOR]
*/
class CardContextInterceptor(
private val scanResponse: ScanResponse?,
private val scanResponse: ScanResponse,
) : ParamsInterceptor {
override fun id(): String = CardContextInterceptor.id()
@ -28,8 +28,6 @@ class CardContextInterceptor(
}
override fun intercept(params: MutableMap<String, String>) {
scanResponse ?: return
val card = scanResponse.card
params[AnalyticsParam.BATCH] = card.batchId
params[AnalyticsParam.PRODUCT_TYPE] = getProductType(scanResponse)

View file

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

View file

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

View file

@ -6,6 +6,7 @@ import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.amountToCreateAccount
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.common.TestActions
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.domain.TapError
@ -58,14 +59,13 @@ suspend fun WalletManager.safeUpdate(isDemoCard: Boolean): Result<Wallet> = try
}
}
internal fun WalletManager.getTopUpUrl(): String? {
internal fun WalletManager.getTopUpUrl(cryptoCurrency: CryptoCurrency): String? {
val globalState = store.state.globalState
val defaultAddress = wallet.address
return globalState.exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Buy,
blockchain = wallet.blockchain,
cryptoCurrencyName = wallet.blockchain.currency,
cryptoCurrency = cryptoCurrency,
fiatCurrencyName = globalState.appCurrency.code,
walletAddress = defaultAddress,
isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive,

View file

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

View file

@ -12,7 +12,6 @@ import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsReducer
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer
import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.shop.redux.ShopReducer
import com.tangem.tap.features.tokens.legacy.redux.TokensReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.AppStateHolder
@ -36,7 +35,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),
daggerGraphState = DaggerGraphReducer.reduce(action, state),

View file

@ -29,8 +29,6 @@ import com.tangem.tap.features.saveWallet.redux.SaveWalletMiddleware
import com.tangem.tap.features.saveWallet.redux.SaveWalletState
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.shop.redux.ShopMiddleware
import com.tangem.tap.features.shop.redux.ShopState
import com.tangem.tap.features.tokens.legacy.redux.TokensState
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
@ -54,7 +52,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 daggerGraphState: DaggerGraphState = DaggerGraphState(),
@ -89,7 +86,6 @@ data class AppState(
DisclaimerMiddleware().disclaimerMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,
ShopMiddleware().shopMiddleware,
WelcomeMiddleware().middleware,
SaveWalletMiddleware().middleware,
LockUserWalletsTimerMiddleware().middleware,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -22,6 +22,12 @@ internal class RuntimeUserWalletsStore(
?.singleOrNull { it.walletId == key }
}
override suspend fun getAllSyncOrNull(): List<UserWallet>? {
return walletsStateHolder.userWalletsListManager
?.userWallets
?.firstOrNull()
}
override suspend fun update(userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet) {
walletsStateHolder.userWalletsListManager?.update(userWalletId, update)
}

View file

@ -69,4 +69,12 @@ internal object CardDomainModule {
fun provideIsNeedToBackupUseCase(walletStateHolder: WalletsStateHolder): IsNeedToBackupUseCase {
return IsNeedToBackupUseCase(walletStateHolder)
}
@Provides
@ViewModelScoped
fun provideGetExtendedPublicKeyForCurrencyUseCase(
derivationsRepository: DerivationsRepository,
): GetExtendedPublicKeyForCurrencyUseCase {
return GetExtendedPublicKeyForCurrencyUseCase(derivationsRepository)
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.di.domain
import android.content.Context
import com.tangem.domain.feedback.GetSupportFeedbackEmailUseCase
import com.tangem.domain.feedback.repository.FeedbackRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object FeedbackDomainModule {
@Provides
@Singleton
fun provideGetFeedbackToSupportUseCase(
feedbackRepository: FeedbackRepository,
@ApplicationContext context: Context,
): GetSupportFeedbackEmailUseCase {
return GetSupportFeedbackEmailUseCase(feedbackRepository = feedbackRepository, resources = context.resources)
}
}

View file

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

View file

@ -120,4 +120,24 @@ internal object SettingsDomainModule {
): ShouldShowSwapPromoTokenUseCase {
return ShouldShowSwapPromoTokenUseCase(swapPromoRepository)
}
@Provides
@ViewModelScoped
fun provideDeleteDeprecatedLogsUseCase(settingsRepository: SettingsRepository): DeleteDeprecatedLogsUseCase {
return DeleteDeprecatedLogsUseCase(settingsRepository)
}
@Provides
@ViewModelScoped
fun provideIsSendTapHelpPreviewEnabledUseCase(
settingsRepository: SettingsRepository,
): IsSendTapHelpEnabledUseCase {
return IsSendTapHelpEnabledUseCase(settingsRepository = settingsRepository)
}
@Provides
@ViewModelScoped
fun provideNeverShowTapHelpUseCase(settingsRepository: SettingsRepository): NeverShowTapHelpUseCase {
return NeverShowTapHelpUseCase(settingsRepository = settingsRepository)
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -15,7 +16,7 @@ import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ViewModelComponent::class)
@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "LargeClass")
internal object TokensDomainModule {
@Provides
@ -191,6 +192,7 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
sendFeatureToggles: SendFeatureToggles,
dispatchers: CoroutineDispatcherProvider,
): GetCryptoCurrencyActionsUseCase {
return GetCryptoCurrencyActionsUseCase(
@ -199,6 +201,7 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
sendFeatureToggles = sendFeatureToggles,
dispatchers = dispatchers,
)
}
@ -278,9 +281,17 @@ internal object TokensDomainModule {
@Provides
@ViewModelScoped
fun provideCheckTokenCompatibilityUseCase(
repository: NetworksCompatibilityRepository,
networksCompatibilityRepository: NetworksCompatibilityRepository,
): CheckCurrencyCompatibilityUseCase {
return CheckCurrencyCompatibilityUseCase(repository)
return CheckCurrencyCompatibilityUseCase(networksCompatibilityRepository)
}
@Provides
@ViewModelScoped
fun provideNeedHardenedDerivationUseCase(
networksCompatibilityRepository: NetworksCompatibilityRepository,
): RequiresHardenedDerivationOnlyUseCase {
return RequiresHardenedDerivationOnlyUseCase(networksCompatibilityRepository)
}
@Provides

View file

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

View file

@ -26,6 +26,8 @@ import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.pins.SetUserCodeCommand
import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.tap.derivationsFinder
import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
@ -104,6 +106,7 @@ class TangemSdkManager(
suspend fun importWallet(
scanResponse: ScanResponse,
mnemonic: String,
passphrase: String?,
shouldReset: Boolean,
): CompletionResult<CreateProductWalletTaskResponse> {
val defaultMnemonic = try {
@ -116,6 +119,7 @@ class TangemSdkManager(
cardTypesResolver = scanResponse.cardTypesResolver,
derivationStyleProvider = scanResponse.derivationStyleProvider,
mnemonic = defaultMnemonic,
passphrase = passphrase,
shouldReset = shouldReset,
),
scanResponse.card.cardId,
@ -138,6 +142,17 @@ class TangemSdkManager(
return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId)
}
suspend fun deriveExtendedPublicKey(
cardId: String?,
walletPublicKey: ByteArray,
derivation: DerivationPath,
): CompletionResult<ExtendedPublicKey> = withContext(Dispatchers.Main) {
runTaskAsyncReturnOnMain(
DeriveWalletPublicKeyTask(walletPublicKey, derivation),
cardId,
)
}
suspend fun resetToFactorySettings(
cardId: String,
allowsRequestAccessCodeFromRepository: Boolean,
@ -264,7 +279,7 @@ class TangemSdkManager(
allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(),
maxFirmwareVersion = FirmwareVersion(major = 6, minor = 33),
batchIdFilter = CardFilter.Companion.ItemFilter.Deny(
items = setOf("0027", "0030", "0031", "0035", "DA88"),
items = setOf("0027", "0030", "0031", "0035"),
),
),
)

View file

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

View file

@ -1,12 +1,15 @@
package com.tangem.tap.domain.card
import com.tangem.common.CompletionResult
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.userwallets.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
@ -51,6 +54,28 @@ internal class DefaultDerivationsRepository(
error("This code should never be reached")
}
override suspend fun deriveExtendedPublicKey(
userWalletId: UserWalletId,
derivation: DerivationPath,
): ExtendedPublicKey? {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
val walletCard = userWallet.scanResponse.card.wallets.firstOrNull {
UserWalletIdBuilder.scanResponse(userWallet.scanResponse).build()?.value
.contentEquals(userWallet.walletId.value)
} ?: return null
val result = tangemSdkManager.deriveExtendedPublicKey(
cardId = null,
walletPublicKey = walletCard.publicKey,
derivation = derivation,
)
return when (result) {
is CompletionResult.Failure -> throw result.error
is CompletionResult.Success -> result.data
}
}
private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): Result<Unit> {
return runCatching(dispatchers.io) {
userWalletsStore.update(

View file

@ -30,7 +30,6 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
analyticsSource: AnalyticsParam.ScreensSources,
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 {
analyticsSource,
cardId,
onProgressStateChange,
onScanStateChange,
onWalletNotCreated,
disclaimerWillShow,
onFailure,
@ -52,7 +50,6 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
analyticsSource,
cardId,
onProgressStateChange,
onScanStateChange,
onWalletNotCreated,
disclaimerWillShow,
onFailure,

View file

@ -50,14 +50,12 @@ internal object LegacyScanProcessor {
analyticsSource: AnalyticsParam.ScreensSources,
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)
@ -68,13 +66,11 @@ internal object LegacyScanProcessor {
result
.doOnFailure { error ->
onScanStateChange(false)
onFailure(error)
}
.doOnSuccess { scanResponse ->
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
onScanStateChange(false)
sendAnalytics(analyticsEvent, scanResponse)
showDisclaimerIfNeed(

View file

@ -3,12 +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.AnalyticsParam
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.*
@ -26,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) },
)
}
@ -38,25 +47,37 @@ internal object UseCaseScanProcessor {
analyticsSource: AnalyticsParam.ScreensSources,
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(
{ showMaxUnsuccessfulScansReachedDialog(analyticsSource) },
),
)
add(AnalyticsChain(Basic.CardWasScanned(analyticsSource)))
add(DisclaimerChain(store, disclaimerWillShow))
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(source: AnalyticsParam.ScreensSources) {
val scanFailsSource = when (source) {
is AnalyticsParam.ScreensSources.SignIn -> StateDialog.ScanFailsSource.SIGN_IN
is AnalyticsParam.ScreensSources.Settings -> StateDialog.ScanFailsSource.SETTINGS
is AnalyticsParam.ScreensSources.Intro -> StateDialog.ScanFailsSource.INTRO
else -> StateDialog.ScanFailsSource.MAIN
}
store.dispatchDialogShow(StateDialog.ScanFailsDialog(scanFailsSource))
}
private suspend fun proceedWithException(
@ -74,7 +95,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)
}
}
}
@ -88,8 +114,9 @@ internal object UseCaseScanProcessor {
navigateTo(exception.onboardingRoute)
onWalletNotCreated()
}
is ScanChainException.DisclaimerWasCanceled,
-> onFailure(scanCardExceptionConverter.convertBack(exception))
is ScanChainException.DisclaimerWasCanceled -> {
onFailure(scanCardExceptionConverter.convertBack(exception))
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -62,6 +62,7 @@ class CreateProductWalletTask(
private val cardTypesResolver: CardTypesResolver,
private val derivationStyleProvider: DerivationStyleProvider,
private val mnemonic: Mnemonic? = null,
private val passphrase: String? = null,
private val shouldReset: Boolean,
) : CardSessionRunnable<CreateProductWalletTaskResponse> {
@ -82,7 +83,7 @@ class CreateProductWalletTask(
cardTypesResolver.isTangemTwins() ->
throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet")
else -> CreateWalletTangemWallet(mnemonic, shouldReset, derivationStyleProvider, cardDto)
else -> CreateWalletTangemWallet(mnemonic, passphrase, shouldReset, derivationStyleProvider, cardDto)
}
commandProcessor.proceed(cardDto, session) {
when (it) {
@ -142,6 +143,7 @@ private class CreateWalletTangemNote(private val cardTypesResolver: CardTypesRes
*/
private class CreateWalletTangemWallet(
private val mnemonic: Mnemonic?,
private val passphrase: String?,
private val shouldReset: Boolean,
private val derivationStyleProvider: DerivationStyleProvider,
cardDTO: CardDTO,
@ -170,7 +172,7 @@ private class CreateWalletTangemWallet(
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
) {
CreateWalletsTask(cardConfig.mandatoryCurves, mnemonic).run(session) { result ->
CreateWalletsTask(cardConfig.mandatoryCurves, mnemonic, passphrase).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
checkIfAllWalletsCreated(card, session, result.data, callback)

View file

@ -21,6 +21,7 @@ class CreateWalletsResponse(
class CreateWalletsTask(
private val curves: List<EllipticCurve>,
private val mnemonic: Mnemonic? = null,
private val passphrase: String?,
) : CardSessionRunnable<CreateWalletsResponse> {
private val createdWalletsResponses = mutableListOf<CreateWalletResponse>()
@ -41,7 +42,7 @@ class CreateWalletsTask(
callback: (result: CompletionResult<CreateWalletsResponse>) -> Unit,
) {
val extendedPrivateKey = mnemonic?.let {
AnyMasterKeyFactory(mnemonic = it, passphrase = "").makeMasterKey(curve)
AnyMasterKeyFactory(mnemonic = it, passphrase = passphrase ?: "").makeMasterKey(curve)
}
CreateWalletTask(curve, extendedPrivateKey).run(session) { result ->
when (result) {

View file

@ -33,7 +33,7 @@ class ResetToFactorySettingsTask(
}
private fun resetBackup(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
if (session.environment.card?.backupStatus?.isActive != true) {
if (session.environment.card?.backupStatus == Card.BackupStatus.NoBackup) {
callback(CompletionResult.Success(session.environment.card!!))
return
}

View file

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

View file

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

View file

@ -14,7 +14,8 @@ import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder
import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder
import timber.log.Timber
object BnbHelper {
internal object BnbHelper {
fun createMessageData(order: WCBinanceTransferOrder): BinanceMessageData.Transfer {
val input = order.msgs.first().inputs.first()
val output = order.msgs.first().inputs.first()

View file

@ -41,7 +41,7 @@ import java.util.concurrent.TimeUnit
import kotlin.collections.set
@Suppress("LargeClass")
class WalletConnectManager {
internal class WalletConnectManager {
private var cardId: String? = null
@ -348,6 +348,7 @@ class WalletConnectManager {
val hash = WalletConnectSdkHelper().signPersonalMessage(
hashToSign = data.hash,
networkId = activeData.wallet.blockchain?.toNetworkId() ?: "",
type = data.type,
derivationPath = activeData.wallet.derivationPath?.rawPath,
cardId = cardId,
)
@ -522,9 +523,9 @@ class WalletConnectManager {
}
}
typealias Topic = String
internal typealias Topic = String
data class WalletConnectActiveData(
internal data class WalletConnectActiveData(
val peerId: String,
val remotePeerId: String?,
val client: WCClient,
@ -547,7 +548,7 @@ data class WalletConnectActiveData(
}
@Suppress("MagicNumber")
class RetryInterceptor : Interceptor {
internal class RetryInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request: Request = chain.request()
val response = chain.proceed(request)

View file

@ -7,10 +7,7 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.blockchain.extensions.hexToBigDecimal
import com.tangem.blockchain.extensions.isAscii
import com.tangem.blockchain.extensions.*
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toDecompressedPublicKey
@ -25,8 +22,9 @@ import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTradeOrder
import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTransferOrder
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumSignMessage
import com.tangem.tap.domain.walletconnect2.domain.TransactionType
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction
import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage
import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder
@ -45,6 +43,7 @@ import timber.log.Timber
import java.math.BigDecimal
import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam
@Suppress("LargeClass")
class WalletConnectSdkHelper {
@Suppress("MagicNumber")
@ -206,8 +205,7 @@ class WalletConnectSdkHelper {
walletPublicKey = data.walletManager.wallet.publicKey.seedKey,
derivationPath = data.walletManager.wallet.publicKey.derivationPath,
)
val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId)
return when (result) {
return when (val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId)) {
is CompletionResult.Success -> {
val hash = EthereumUtils.prepareTransactionToSend(
signature = result.data.signature,
@ -250,8 +248,7 @@ class WalletConnectSdkHelper {
walletPublicKey = wallet.publicKey.seedKey,
derivationPath = wallet.publicKey.derivationPath,
)
val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId)
return when (result) {
return when (val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId)) {
is CompletionResult.Success -> {
val key = wallet.publicKey.blockchainKey.toDecompressedPublicKey()
getBnbResultString(
@ -267,16 +264,17 @@ class WalletConnectSdkHelper {
}
fun prepareDataForPersonalSign(
message: WcEthereumSignMessage,
message: WcSignMessage,
topic: String,
metaName: String,
id: Long,
): WcPersonalSignData {
val messageData = when (message.type) {
WcEthereumSignMessage.WCSignType.MESSAGE,
WcEthereumSignMessage.WCSignType.PERSONAL_MESSAGE,
WcSignMessage.WCSignType.MESSAGE,
WcSignMessage.WCSignType.PERSONAL_MESSAGE,
-> createMessageData(message)
WcEthereumSignMessage.WCSignType.TYPED_MESSAGE -> EthereumUtils.makeTypedDataHash(message.data)
WcSignMessage.WCSignType.TYPED_MESSAGE -> EthereumUtils.makeTypedDataHash(message.data)
WcSignMessage.WCSignType.SOLANA_MESSAGE -> message.data.decodeBase58()
}
val messageString = message.data.hexToAscii()
@ -290,7 +288,7 @@ class WalletConnectSdkHelper {
id = id,
)
return WcPersonalSignData(
hash = messageData,
hash = requireNotNull(messageData) { "Message data must not be null" },
topic = topic,
id = id,
dialogData = dialogData,
@ -298,7 +296,7 @@ class WalletConnectSdkHelper {
)
}
private fun createMessageData(message: WcEthereumSignMessage): ByteArray {
private fun createMessageData(message: WcSignMessage): ByteArray {
val messageData = try {
message.data.removePrefix(HEX_PREFIX).hexToBytes()
} catch (exception: Exception) {
@ -332,6 +330,7 @@ class WalletConnectSdkHelper {
suspend fun signPersonalMessage(
hashToSign: ByteArray,
networkId: String,
type: WcSignMessage.WCSignType,
derivationPath: String?,
cardId: String?,
): String? {
@ -345,12 +344,16 @@ class WalletConnectSdkHelper {
)
return when (val result = tangemSdkManager.runTaskAsync(command, cardId)) {
is CompletionResult.Success -> {
val hash = result.data.signature
return EthereumUtils.prepareSignedMessageData(
signedHash = hash,
hashToSign = hashToSign,
publicKey = wallet.publicKey.blockchainKey.toDecompressedPublicKey(),
)
val signedHash = result.data.signature
return when (type) {
WcSignMessage.WCSignType.SOLANA_MESSAGE -> getSolanaResultString(signedHash)
else -> EthereumUtils.prepareSignedMessageData(
signedHash = signedHash,
hashToSign = hashToSign,
publicKey = wallet.publicKey.blockchainKey.toDecompressedPublicKey(),
)
}
}
is CompletionResult.Failure -> {
Timber.e(result.error.customMessage)
@ -359,12 +362,46 @@ class WalletConnectSdkHelper {
}
}
companion object {
private const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"
private const val HEX_PREFIX = "0x"
private const val DEFAULT_MAX_GASLIMIT = 350000
fun getBnbResultString(publicKey: String, signature: String): String {
return "{\"signature\":\"$signature\",\"publicKey\":\"$publicKey\"}"
suspend fun signTransaction(
hashToSign: ByteArray,
networkId: String,
type: TransactionType,
derivationPath: String?,
cardId: String?,
): String? {
val blockchain = Blockchain.fromNetworkId(networkId) ?: return null
val wallet = getWalletManager(blockchain, derivationPath)?.wallet ?: return null
val command = SignHashCommand(
hash = hashToSign,
walletPublicKey = wallet.publicKey.seedKey,
derivationPath = wallet.publicKey.derivationPath,
)
return when (val result = tangemSdkManager.runTaskAsync(command, cardId)) {
is CompletionResult.Success -> {
val signedHash = result.data.signature
when (type) {
TransactionType.SOLANA_TX -> {
getSolanaResultString(signedHash)
}
}
}
is CompletionResult.Failure -> {
Timber.e(result.error.customMessage)
null
}
}
}
private fun getSolanaResultString(signedHash: ByteArray) = "{ signature: \"${signedHash.encodeBase58()}\" }"
private fun getBnbResultString(publicKey: String, signature: String) =
"{\"signature\":\"$signature\",\"publicKey\":\"$publicKey\"}"
private companion object {
const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"
const val HEX_PREFIX = "0x"
const val DEFAULT_MAX_GASLIMIT = 350000
}
}

View file

@ -1,18 +1,18 @@
package com.tangem.tap.domain.walletconnect.extensions
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumSignMessage
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction
import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage
import com.trustwallet.walletconnect.models.WCPeerMeta
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction
fun WCPeerMeta.isDappSupported(): Boolean {
internal fun WCPeerMeta.isDappSupported(): Boolean {
return !unsupportedDappsList.any { this.url.contains(it) }
}
private val unsupportedDappsList: List<String> = listOf("dydx.exchange")
fun WCEthereumTransaction.toWcEthTransaction(): WcEthereumTransaction {
internal fun WCEthereumTransaction.toWcEthTransaction(): WcEthereumTransaction {
return WcEthereumTransaction(
from = from,
to = to,
@ -27,13 +27,13 @@ fun WCEthereumTransaction.toWcEthTransaction(): WcEthereumTransaction {
)
}
fun WCEthereumSignMessage.toWcEthereumSignMessage(): WcEthereumSignMessage {
return WcEthereumSignMessage(
internal fun WCEthereumSignMessage.toWcEthereumSignMessage(): WcSignMessage {
return WcSignMessage(
raw = raw,
type = when (type) {
WCEthereumSignMessage.WCSignType.MESSAGE -> WcEthereumSignMessage.WCSignType.MESSAGE
WCEthereumSignMessage.WCSignType.PERSONAL_MESSAGE -> WcEthereumSignMessage.WCSignType.PERSONAL_MESSAGE
WCEthereumSignMessage.WCSignType.TYPED_MESSAGE -> WcEthereumSignMessage.WCSignType.TYPED_MESSAGE
WCEthereumSignMessage.WCSignType.MESSAGE -> WcSignMessage.WCSignType.MESSAGE
WCEthereumSignMessage.WCSignType.PERSONAL_MESSAGE -> WcSignMessage.WCSignType.PERSONAL_MESSAGE
WCEthereumSignMessage.WCSignType.TYPED_MESSAGE -> WcSignMessage.WCSignType.TYPED_MESSAGE
},
)
}

View file

@ -4,8 +4,18 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.tap.domain.walletconnect2.domain.WcBlockchainHelper
import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles
internal class TangemWcBlockchainHelper(
private val featureToggles: WalletConnectFeatureToggles,
) : WcBlockchainHelper {
private val supportedNonEvmBlockchains = if (featureToggles.isSolanaTxSignEnabled) {
setOf(Blockchain.Solana, Blockchain.SolanaTestnet)
} else {
emptySet()
}
class TangemWcBlockchainHelper : WcBlockchainHelper {
override fun chainIdToNetworkIdOrNull(chainId: String): String? {
val parsedId = chainId.parseId() ?: return null
val blockchain = parsedId.chainIdToBlockchain()
@ -41,48 +51,57 @@ class TangemWcBlockchainHelper : WcBlockchainHelper {
val networkId = chainIdToNetworkIdOrNull(chainId) ?: return null
return Blockchain.fromNetworkId(networkId)?.fullName
}
}
private fun Blockchain.getCaip2ChainId(): String? {
if (this.isEvm()) return this.getChainId()?.toString()
private fun Blockchain.getCaip2ChainId(): String? {
if (this.isEvm()) return this.getChainId()?.toString()
return when (this) {
Blockchain.Solana -> "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ"
Blockchain.Polkadot -> "91b171bb158e2d3848fa23a9f1c25182"
Blockchain.Tron -> "0x2b6653dc"
else -> null
}
}
private fun Blockchain.getCaip2Namespace(): String? {
return when {
this.isEvm() -> EVM_NAMESPACE
supportedNonEvmBlockchains.contains(this) -> this.toNetworkId()
else -> null
}
}
private fun String.parseId(): Pair<String, String>? {
val parsed = this.split(CHAIN_SEPARATOR)
if (parsed.size != 2) return null
return parsed[0] to parsed[1]
}
private fun Pair<String, String>.chainIdToBlockchain(): Blockchain? {
return when (first) {
EVM_NAMESPACE -> {
second.toIntOrNull()
?.let(Blockchain::fromChainId)
}
else -> {
Blockchain.fromNetworkId(networkId = first)
.takeIf(supportedNonEvmBlockchains::contains)
return when (this) {
/*
* The WC sample application and documentation use a commented out chain ID. However, in real dApps
* uncommented is used.
* Docs: https://docs.walletconnect.com/advanced/multichain/chain-list
*
* Blockchain.Solana -> "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
* */
Blockchain.Solana -> "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ"
Blockchain.SolanaTestnet -> "z4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z"
Blockchain.Polkadot -> "91b171bb158e2d3848fa23a9f1c25182"
Blockchain.Tron -> "0x2b6653dc"
else -> null
}
}
}
private val supportedNonEvmBlockchains = emptySet<Blockchain>() // TODO: add supported networks
private fun Blockchain.getCaip2Namespace(): String? {
return when {
this.isEvm() -> EVM_NAMESPACE
supportedNonEvmBlockchains.contains(this) -> this.toNetworkId().substringBefore(TESTNET_SEPARATOR)
else -> null
}
}
private const val EVM_NAMESPACE = "eip155"
private const val CHAIN_SEPARATOR = ":"
private fun String.parseId(): Pair<String, String>? {
val parsed = this.split(CHAIN_SEPARATOR)
if (parsed.size != 2) return null
return parsed[0] to parsed[1]
}
private fun Pair<String, String>.chainIdToBlockchain(): Blockchain? {
return when (first) {
EVM_NAMESPACE -> {
second.toIntOrNull()
?.let(Blockchain::fromChainId)
}
else -> {
Blockchain.fromNetworkId(networkId = first)
.takeIf(supportedNonEvmBlockchains::contains)
}
}
}
private companion object {
const val EVM_NAMESPACE = "eip155"
const val CHAIN_SEPARATOR = ":"
const val TESTNET_SEPARATOR = "/"
}
}

View file

@ -12,7 +12,7 @@ import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen
import com.tangem.tap.store
import timber.log.Timber
class WalletConnectEventsHandlerImpl : WalletConnectEventsHandler {
internal class WalletConnectEventsHandlerImpl : WalletConnectEventsHandler {
override fun onProposalReceived(proposal: WalletConnectEvents.SessionProposal, networksFormatted: String) {
store.dispatchOnMain(
GlobalAction.ShowDialog(

View file

@ -18,9 +18,8 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import timber.log.Timber
import javax.inject.Inject
class WalletConnectRepositoryImpl @Inject constructor(
internal class DefaultWalletConnectRepository(
private val application: Application,
private val wcRequestDeserializer: WcJrpcRequestsDeserializer,
private val analyticsHandler: AnalyticsEventHandler,
@ -95,11 +94,23 @@ class WalletConnectRepositoryImpl @Inject constructor(
) {
// Triggered when wallet receives the session proposal sent by a Dapp
Timber.d("sessionProposal: $sessionProposal")
this@WalletConnectRepositoryImpl.sessionProposal = sessionProposal
this@DefaultWalletConnectRepository.sessionProposal = sessionProposal
if (sessionProposal.name in unsupportedDApps) {
Timber.w("Unsupported DApp")
scope.launch {
_events.emit(
WalletConnectEvents.SessionApprovalError(
WalletConnectError.UnsupportedDApp,
),
)
}
return
}
val missingNetworks = findMissingNetworks(
namespaces = sessionProposal.requiredNamespaces,
userNamespaces = this@WalletConnectRepositoryImpl.userNamespaces ?: emptyMap(),
userNamespaces = this@DefaultWalletConnectRepository.userNamespaces ?: emptyMap(),
)
if (missingNetworks.isNotEmpty()) {
@ -116,7 +127,7 @@ class WalletConnectRepositoryImpl @Inject constructor(
val optionalWithoutMissingNetworks = removeMissingNetworks(
namespaces = sessionProposal.optionalNamespaces,
userNamespaces = this@WalletConnectRepositoryImpl.userNamespaces ?: emptyMap(),
userNamespaces = this@DefaultWalletConnectRepository.userNamespaces ?: emptyMap(),
)
scope.launch {
@ -334,6 +345,7 @@ class WalletConnectRepositoryImpl @Inject constructor(
),
)
}
Web3Wallet.respondSessionRequest(
params = Wallet.Params.SessionRequestResponse(
sessionTopic = requestData.topic,
@ -342,15 +354,19 @@ class WalletConnectRepositoryImpl @Inject constructor(
result = result,
),
),
onSuccess = {},
onError = {
onSuccess = { response ->
Timber.d("Session request responded successfully: $response")
},
onError = { error ->
Timber.d(error.throwable, "Error while responging session request")
WalletConnect.RequestHandledParams(
dAppName = session?.name ?: "",
dAppUrl = session?.url ?: "",
methodName = requestData.method,
blockchain = requestData.blockchain,
errorCode = WalletConnectError.ValidationError.error,
errorDescription = it.throwable.message,
errorDescription = error.throwable.message,
)
},
)
@ -472,4 +488,9 @@ class WalletConnectRepositoryImpl @Inject constructor(
val userChains = userNamespaces.flatMap { it.value.map { account -> account.chainId } }
return wcProvidedChains.intersect(userChains.toSet())
}
private companion object {
val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro")
}
}

View file

@ -3,15 +3,13 @@ package com.tangem.tap.domain.walletconnect2.data
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.files.FileReader
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.domain.walletconnect2.domain.models.Session
import timber.log.Timber
import javax.inject.Inject
class WalletConnectSessionsRepositoryImpl @Inject constructor(
@SdkMoshi private val moshi: Moshi,
internal class DefaultWalletConnectSessionsRepository(
private val moshi: Moshi,
private val fileReader: FileReader,
) :
WalletConnectSessionsRepository {

View file

@ -1,15 +1,22 @@
package com.tangem.tap.domain.walletconnect2.di
import android.app.Application
import com.squareup.moshi.Moshi
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.files.FileReader
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
import com.tangem.tap.domain.walletconnect2.app.WalletConnectEventsHandlerImpl
import com.tangem.tap.domain.walletconnect2.data.WalletConnectRepositoryImpl
import com.tangem.tap.domain.walletconnect2.data.WalletConnectSessionsRepositoryImpl
import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectSessionsRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer
import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -20,19 +27,21 @@ import javax.inject.Singleton
@Module
@InstallIn(ActivityComponent::class)
class WalletConnectInteractorModule {
internal object WalletConnectInteractorModule {
@Provides
@ActivityScoped
fun provideWalletConnectInteractor(
wcRepository: WalletConnectRepository,
wcSessionsRepository: WalletConnectSessionsRepository,
walletConnectFeatureToggles: WalletConnectFeatureToggles,
): WalletConnectInteractor {
return WalletConnectInteractor(
handler = WalletConnectEventsHandlerImpl(),
walletConnectRepository = wcRepository,
sessionsRepository = wcSessionsRepository,
sdkHelper = WalletConnectSdkHelper(),
blockchainHelper = TangemWcBlockchainHelper(),
blockchainHelper = TangemWcBlockchainHelper(walletConnectFeatureToggles),
dispatcher = AppCoroutineDispatcherProvider(),
)
}
@ -40,15 +49,37 @@ class WalletConnectInteractorModule {
@Module
@InstallIn(SingletonComponent::class)
interface WalletConnectModule {
internal object WalletConnectModule {
@Binds
@Provides
@Singleton
fun bindWalletConnectRepository(repository: WalletConnectRepositoryImpl): WalletConnectRepository
fun provideWalletConnectFeatureToggles(featureTogglesManager: FeatureTogglesManager): WalletConnectFeatureToggles {
return WalletConnectFeatureToggles(featureTogglesManager)
}
@Binds
@Provides
@Singleton
fun bindWalletConnectSessionsRepository(
repository: WalletConnectSessionsRepositoryImpl,
): WalletConnectSessionsRepository
fun provideWalletConnectRepository(
application: Application,
wcRequestDeserializer: WcJrpcRequestsDeserializer,
analyticsHandler: AnalyticsEventHandler,
): WalletConnectRepository {
return DefaultWalletConnectRepository(
application = application,
wcRequestDeserializer = wcRequestDeserializer,
analyticsHandler = analyticsHandler,
)
}
@Provides
@Singleton
fun provideWalletConnectSessionsRepository(
@SdkMoshi moshi: Moshi,
fileReader: FileReader,
): WalletConnectSessionsRepository {
return DefaultWalletConnectSessionsRepository(
moshi = moshi,
fileReader = fileReader,
)
}
}

View file

@ -209,7 +209,7 @@ class WalletConnectInteractor(
val currentRequest = this.currentRequest
if (currentRequest == null || request.topic != currentRequest.topic) return
val networkId = blockchainHelper.chainIdToNetworkIdOrNull(currentRequest.chainId ?: "") ?: return
val networkId = blockchainHelper.chainIdToNetworkIdOrNull(currentRequest.chainId.orEmpty()) ?: return
val signedHash = when (request) {
is WcPreparedRequest.BnbTransaction -> sdkHelper.signBnbTransaction(
data = request.preparedRequestData.data.data,
@ -224,13 +224,19 @@ class WalletConnectInteractor(
is WcPreparedRequest.EthSign -> sdkHelper.signPersonalMessage(
hashToSign = request.preparedRequestData.hash,
networkId = networkId,
type = request.preparedRequestData.type,
derivationPath = request.derivationPath,
cardId = cardId,
)
is WcPreparedRequest.SignTransaction -> sdkHelper.signTransaction(
hashToSign = request.preparedRequestData.hashToSign,
networkId = networkId,
type = request.preparedRequestData.type,
derivationPath = request.derivationPath,
cardId = cardId,
)
}
Timber.d("Signed hash: $signedHash")
val requestData = RequestData(
topic = request.topic,
requestId = request.requestId,

View file

@ -2,15 +2,17 @@ package com.tangem.tap.domain.walletconnect2.domain
import com.squareup.moshi.*
import com.tangem.datasource.di.SdkMoshi
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WCBinanceTxConfirmParam
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceCancelOrder
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTxConfirmParam
import com.tangem.tap.domain.walletconnect2.domain.models.solana.SolanaSignMessage
import com.tangem.tap.domain.walletconnect2.domain.models.solana.SolanaTransactionRequest
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
enum class WcJrpcMethods(val code: String) {
internal enum class WcJrpcMethods(val code: String) {
ETH_SIGN("eth_sign"),
ETH_PERSONAL_SIGN("personal_sign"),
@ -18,10 +20,14 @@ enum class WcJrpcMethods(val code: String) {
ETH_SIGN_TYPE_DATA_V4("eth_signTypedData_v4"),
ETH_SIGN_TRANSACTION("eth_signTransaction"),
ETH_SEND_TRANSACTION("eth_sendTransaction"),
BNB_SIGN("bnb_sign"),
BNB_TRANSACTION_CONFIRM("bnb_tx_confirmation"),
SIGN_TRANSACTION("trust_signTransaction"),
WALLET_ADD_ETHEREUM_CHAIN("wallet_addEthereumChain"),
SOLANA_SIGN_TX("solana_signTransaction"),
SOLANA_SIGN_MESSAGE("solana_signMessage"),
;
companion object {
@ -30,7 +36,7 @@ enum class WcJrpcMethods(val code: String) {
}
@JsonClass(generateAdapter = true)
data class WCSignTransaction(
data class WcSignTransaction(
@Json(name = "network")
val network: Int,
@ -39,13 +45,7 @@ data class WCSignTransaction(
) : WcRequestData
@JsonClass(generateAdapter = true)
data class WcAddChain(
@Json(name = "chainId")
val chainId: String,
) : WcRequestData
@JsonClass(generateAdapter = true)
data class WcEthereumSignMessage(
data class WcSignMessage(
@Json(name = "raw")
val raw: List<String>,
@ -53,7 +53,7 @@ data class WcEthereumSignMessage(
val type: WCSignType,
) : WcRequestData {
enum class WCSignType {
MESSAGE, PERSONAL_MESSAGE, TYPED_MESSAGE
MESSAGE, PERSONAL_MESSAGE, TYPED_MESSAGE, SOLANA_MESSAGE,
}
/**
@ -63,6 +63,7 @@ data class WcEthereumSignMessage(
* - MESSAGE: `[address, data ]`
* - TYPED_MESSAGE: `[address, data]`
* - PERSONAL_MESSAGE: `[data, address]`
* - SOLANA_MESSAGE: `[publicKey (address), message]`
*
* reference: https://docs.walletconnect.org/json-rpc/ethereum#eth_signtypeddata
*/
@ -79,6 +80,21 @@ data class WcEthereumSignMessage(
}
}
@JsonClass(generateAdapter = true)
data class WcSignMessageData(
@Json(name = "address")
val address: String,
@Json(name = "message")
val message: String,
) : WcRequestData
@JsonClass(generateAdapter = true)
data class WcAddChain(
@Json(name = "chainId")
val chainId: String,
) : WcRequestData
@JsonClass(generateAdapter = true)
data class WcEthereumTransaction(
@Json(name = "from")
@ -117,20 +133,21 @@ interface WcRequestData
data class WcCustomRequestData(val data: String) : WcRequestData
sealed class WcRequest(open val data: WcRequestData) {
data class EthSign(override val data: WcEthereumSignMessage) : WcRequest(data)
data class EthSign(override val data: WcSignMessage) : WcRequest(data)
data class EthSignTransaction(override val data: WcEthereumTransaction) : WcRequest(data)
data class EthSendTransaction(override val data: WcEthereumTransaction) : WcRequest(data)
data class BnbTrade(override val data: WcBinanceTradeOrder) : WcRequest(data)
data class BnbCancel(override val data: WcBinanceCancelOrder) : WcRequest(data)
data class BnbTransfer(override val data: WcBinanceTransferOrder) : WcRequest(data)
data class BnbTxConfirm(override val data: WCBinanceTxConfirmParam) : WcRequest(data)
data class SignTransaction(override val data: WCSignTransaction) : WcRequest(data)
data class BnbTxConfirm(override val data: WcBinanceTxConfirmParam) : WcRequest(data)
data class SignTransaction(override val data: WcSignTransaction) : WcRequest(data)
data class AddChain(override val data: WcAddChain) : WcRequest(data)
data class CustomRequest(override val data: WcCustomRequestData) : WcRequest(data)
data class SolanaSignRequest(override val data: SolanaTransactionRequest) : WcRequest(data)
}
@Singleton
class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi: Moshi) {
internal class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi: Moshi) {
@Suppress("ComplexMethod", "LongMethod")
fun deserialize(method: String, params: String): WcRequest {
@ -154,9 +171,9 @@ class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi
val deserializedParams = moshi.adapter<List<String>>(
Types.newParameterizedType(List::class.java, String::class.java),
).fromJsonOrNull(params) ?: return customRequest
val data = WcEthereumSignMessage(
val data = WcSignMessage(
raw = deserializedParams,
type = WcEthereumSignMessage.WCSignType.MESSAGE,
type = WcSignMessage.WCSignType.MESSAGE,
)
WcRequest.EthSign(data = data)
}
@ -164,9 +181,9 @@ class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi
val deserializedParams = moshi.adapter<List<String>>(
Types.newParameterizedType(List::class.java, String::class.java),
).fromJsonOrNull(params) ?: return customRequest
val data = WcEthereumSignMessage(
val data = WcSignMessage(
raw = deserializedParams,
type = WcEthereumSignMessage.WCSignType.PERSONAL_MESSAGE,
type = WcSignMessage.WCSignType.PERSONAL_MESSAGE,
)
WcRequest.EthSign(data = data)
}
@ -175,9 +192,9 @@ class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi
params.substring(params.indexOf("\"") + 1, params.indexOf("\"", startIndex = 2)),
params.substring(params.indexOfFirst { it == '{' }, params.indexOfLast { it == '}' } + 1),
)
val data = WcEthereumSignMessage(
val data = WcSignMessage(
deserializedParams,
WcEthereumSignMessage.WCSignType.TYPED_MESSAGE,
WcSignMessage.WCSignType.TYPED_MESSAGE,
)
Timber.d("TypedData params: $deserializedParams")
WcRequest.EthSign(data)
@ -186,23 +203,43 @@ class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi
return deserializeBnb(moshi, params) ?: customRequest
}
WcJrpcMethods.BNB_TRANSACTION_CONFIRM -> {
val deserializedParams = moshi.adapter<List<WCBinanceTxConfirmParam>>(
Types.newParameterizedType(List::class.java, WCBinanceTxConfirmParam::class.java),
val deserializedParams = moshi.adapter<List<WcBinanceTxConfirmParam>>(
Types.newParameterizedType(List::class.java, WcBinanceTxConfirmParam::class.java),
).fromJsonFirstOrNull(params) ?: return customRequest
WcRequest.BnbTxConfirm(data = deserializedParams)
}
WcJrpcMethods.SIGN_TRANSACTION -> {
val deserializedParams = moshi.adapter<List<WCSignTransaction>>(
Types.newParameterizedType(List::class.java, WCSignTransaction::class.java),
val deserializedParams = moshi.adapter<List<WcSignTransaction>>(
Types.newParameterizedType(List::class.java, WcSignTransaction::class.java),
).fromJsonFirstOrNull(params) ?: return customRequest
WcRequest.SignTransaction(data = deserializedParams)
}
WcJrpcMethods.WALLET_ADD_ETHEREUM_CHAIN -> {
val deserializedParams: WcAddChain = moshi.adapter<List<WcAddChain>>(
Types.newParameterizedType(List::class.java, WcAddChain::class.java),
).fromJsonFirstOrNull(params) ?: return customRequest
WcRequest.AddChain(data = deserializedParams)
}
WcJrpcMethods.SOLANA_SIGN_TX -> {
val tx = moshi.adapter<SolanaTransactionRequest>(SolanaTransactionRequest::class.java)
.fromJsonOrNull(params)
?: return customRequest
WcRequest.SolanaSignRequest(data = tx)
}
WcJrpcMethods.SOLANA_SIGN_MESSAGE -> {
val signMessage = moshi.adapter<SolanaSignMessage>(SolanaSignMessage::class.java)
.fromJsonOrNull(params)
?: return customRequest
val data = WcSignMessage(
raw = listOf(signMessage.publicKey, signMessage.message),
type = WcSignMessage.WCSignType.SOLANA_MESSAGE,
)
WcRequest.EthSign(data = data)
}
}
}

View file

@ -30,4 +30,21 @@ sealed class WcPreparedRequest(
requestId: Long,
derivationPath: String?,
) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath)
class SignTransaction(
override val preparedRequestData: WcGenericTransactionData,
topic: String,
requestId: Long,
derivationPath: String?,
) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath)
}
data class WcGenericTransactionData(
val hashToSign: ByteArray,
val dAppName: String,
val type: TransactionType,
)
enum class TransactionType {
SOLANA_TX,
}

View file

@ -1,12 +1,13 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
import com.tangem.tap.domain.walletconnect2.domain.mapper.mapToTransaction
import com.tangem.tap.domain.walletconnect2.domain.models.BnbData
import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents
import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType
class WcSessionRequestConverter(
internal class WcSessionRequestConverter(
private val blockchainHelper: WcBlockchainHelper,
private val sessionsRepository: WalletConnectSessionsRepository,
private val sdkHelper: WalletConnectSdkHelper,
@ -107,6 +108,20 @@ class WcSessionRequestConverter(
derivationPath = derivationPath,
)
}
is WcRequest.SolanaSignRequest -> {
val data = request.data.mapToTransaction()
WcPreparedRequest.SignTransaction(
preparedRequestData = WcGenericTransactionData(
hashToSign = data.getSerializedMessage(),
dAppName = sessionRequest.metaName,
type = TransactionType.SOLANA_TX,
),
topic = sessionRequest.topic,
requestId = sessionRequest.id,
derivationPath = derivationPath,
)
}
else -> null
}
}
@ -118,6 +133,7 @@ class WcSessionRequestConverter(
is WcRequest.EthSendTransaction -> request.data.from
is WcRequest.EthSignTransaction -> request.data.from
is WcRequest.EthSign -> request.data.address
is WcRequest.SolanaSignRequest -> request.data.feePayer
else -> null
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.tap.domain.walletconnect2.domain.mapper
import com.tangem.blockchain.blockchains.solana.solanaj.core.SolanaTransaction
import com.tangem.tap.domain.walletconnect2.domain.models.solana.SolanaTransactionRequest
import org.p2p.solanaj.core.AccountMeta
import org.p2p.solanaj.core.PublicKey
import org.p2p.solanaj.core.TransactionInstruction
internal fun SolanaTransactionRequest.mapToTransaction(): SolanaTransaction {
val from = PublicKey(feePayer)
val instructions = instructions.map(SolanaTransactionRequest.Instruction::mapToInstruction)
return SolanaTransaction(from)
.apply {
instructions.forEach(::addInstruction)
setRecentBlockHash(recentBlockhash)
}
}
private fun SolanaTransactionRequest.Instruction.mapToInstruction() = TransactionInstruction(
/* programId = */ PublicKey(programId),
/* keys = */ keys.map(SolanaTransactionRequest.Key::mapToAccountMeta),
/* data = */ data.toByteArray(),
)
private fun SolanaTransactionRequest.Key.mapToAccountMeta() = AccountMeta(
/* publicKey = */ PublicKey(publicKey),
/* isSigner = */ isSigner,
/* isWritable = */ isWritable,
)

View file

@ -1,6 +1,9 @@
package com.tangem.tap.domain.walletconnect2.domain.models
sealed class WalletConnectError(val error: String) : Exception() {
data object UnsupportedDApp : WalletConnectError("UnsupportedDApp")
data class ApprovalErrorMissingNetworks(
val missingChains: List<String>,
) : WalletConnectError("ApprovalErrorMissingNetworks")

View file

@ -13,7 +13,7 @@ open class WcBinanceOrder<T>(
val msgs: List<T>,
) : WcRequestData
data class WCBinanceTxConfirmParam(
data class WcBinanceTxConfirmParam(
val ok: Boolean,
val errorMsg: String?,
) : WcRequestData

View file

@ -0,0 +1,13 @@
package com.tangem.tap.domain.walletconnect2.domain.models.solana
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class SolanaSignMessage(
@Json(name = "pubkey")
val publicKey: String,
@Json(name = "message")
val message: String,
)

View file

@ -0,0 +1,42 @@
package com.tangem.tap.domain.walletconnect2.domain.models.solana
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
@JsonClass(generateAdapter = true)
data class SolanaTransactionRequest(
@Json(name = "feePayer")
val feePayer: String,
@Json(name = "recentBlockhash")
val recentBlockhash: String,
@Json(name = "instructions")
val instructions: List<Instruction>,
) : WcRequestData {
@JsonClass(generateAdapter = true)
data class Instruction(
@Json(name = "programId")
val programId: String,
@Json(name = "data")
val data: List<Byte>,
@Json(name = "keys")
val keys: List<Key>,
)
@JsonClass(generateAdapter = true)
data class Key(
@Json(name = "isSigner")
val isSigner: Boolean,
@Json(name = "isWritable")
val isWritable: Boolean,
@Json(name = "pubkey")
val publicKey: String,
)
}

View file

@ -0,0 +1,11 @@
package com.tangem.tap.domain.walletconnect2.toggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
internal class WalletConnectFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) {
val isSolanaTxSignEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "WC_SOLANA_TX_SIGN_ENABLED")
}

View file

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

View file

@ -132,6 +132,10 @@ private fun TextField(model: AddCustomTokenInputField, isError: Boolean) {
}
}
/**
* Be careful with this function ExposedDropdownMenuBox can crash the app if it is open and user clicks system back
* button. It was fixed in compose-material 1.6.4.
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun SelectorField(model: AddCustomTokenSelectorField) {

View file

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

View file

@ -16,29 +16,31 @@ sealed class DetailsAction : Action {
val shouldSaveUserWallets: Boolean,
) : DetailsAction()
object ReCreateTwinsWallet : DetailsAction()
data object ReCreateTwinsWallet : DetailsAction()
sealed class ResetToFactory : DetailsAction() {
object Start : ResetToFactory()
object Proceed : ResetToFactory()
data object Start : ResetToFactory()
data object Proceed : ResetToFactory()
data class AcceptCondition1(val accepted: Boolean) : ResetToFactory()
data class AcceptCondition2(val accepted: Boolean) : ResetToFactory()
object Failure : ResetToFactory()
object Success : ResetToFactory()
data object Failure : ResetToFactory()
data object Success : ResetToFactory()
data class LastWarningDialogVisibility(val isShown: Boolean) : ResetToFactory()
}
object ScanCard : DetailsAction()
data object ScanCard : DetailsAction()
data class PrepareCardSettingsData(val card: CardDTO, val cardTypesResolver: CardTypesResolver) : DetailsAction()
object ResetCardSettingsData : DetailsAction()
object ScanAndSaveUserWallet : DetailsAction() {
data object ResetCardSettingsData : DetailsAction()
data object ScanAndSaveUserWallet : DetailsAction() {
object Success : DetailsAction()
data object Success : DetailsAction()
data class Error(val error: TextReference?) : DetailsAction()
}
object DismissError : DetailsAction()
data object DismissError : DetailsAction()
sealed class AccessCodeRecovery : DetailsAction() {
object Open : AccessCodeRecovery()
@ -50,14 +52,14 @@ sealed class DetailsAction : Action {
}
sealed class ManageSecurity : DetailsAction() {
object OpenSecurity : ManageSecurity()
data object OpenSecurity : ManageSecurity()
data class SelectOption(val option: SecurityOption) : ManageSecurity()
object SaveChanges : ManageSecurity() {
object Success : ManageSecurity()
object Failure : ManageSecurity()
data object SaveChanges : ManageSecurity() {
data object Success : ManageSecurity()
data object Failure : ManageSecurity()
}
object ChangeAccessCode : ManageSecurity()
data object ChangeAccessCode : ManageSecurity()
}
sealed class AppSettings : DetailsAction() {
@ -65,7 +67,7 @@ sealed class DetailsAction : Action {
val enable: Boolean,
val setting: AppSetting,
) : AppSettings() {
object Success : AppSettings()
data object Success : AppSettings()
data class Failure(
val prevState: Boolean,
@ -77,7 +79,7 @@ sealed class DetailsAction : Action {
val lifecycleScope: LifecycleCoroutineScope,
) : AppSettings()
object EnrollBiometrics : AppSettings()
data object EnrollBiometrics : AppSettings()
data class BiometricsStatusChanged(
val needEnrollBiometrics: Boolean,
) : AppSettings()

View file

@ -114,6 +114,7 @@ private fun handlePrepareCardSettingsScreen(
null
},
isShowPasswordResetRadioButton = isShowPasswordResetRadioButton,
isLastWarningDialogShown = false,
)
return state.copy(cardSettingsState = cardSettingsState)
}
@ -181,6 +182,13 @@ private fun handleEraseWallet(action: DetailsAction.ResetToFactory, state: Detai
),
)
}
is DetailsAction.ResetToFactory.LastWarningDialogVisibility -> {
state.copy(
cardSettingsState = cardSettingsState?.copy(
isLastWarningDialogShown = action.isShown,
),
)
}
else -> state
}

View file

@ -46,6 +46,7 @@ data class CardSettingsState(
val condition2Checked: Boolean,
val accessCodeRecovery: AccessCodeRecoveryState? = null,
val isShowPasswordResetRadioButton: Boolean,
val isLastWarningDialogShown: Boolean,
)
data class ManageSecurityState(

View file

@ -13,12 +13,12 @@ import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.walletconnect.WalletConnectActions
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.qrscanning.SourceType
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
@ -352,6 +352,13 @@ class WalletConnectMiddleware {
),
)
}
is WalletConnectError.UnsupportedDApp -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.UnsupportedDapp,
),
)
}
is WalletConnectError.ExternalApprovalError -> {
Timber.e(action.error, "ExternalApprovalError ${action.error.message}")
// do not show dialog on this event
@ -375,6 +382,7 @@ class WalletConnectMiddleware {
is WcPreparedRequest.BnbTransaction -> WalletConnectDialog.BnbTransactionDialog(request)
is WcPreparedRequest.EthTransaction -> WalletConnectDialog.RequestTransaction(request)
is WcPreparedRequest.EthSign -> WalletConnectDialog.PersonalSign(request)
is WcPreparedRequest.SignTransaction -> WalletConnectDialog.SignTransactionDialog(request)
}
store.dispatch(GlobalAction.ShowDialog(dialog))
}

View file

@ -8,8 +8,8 @@ import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.core.navigation.StateDialog
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumSignMessage
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents
import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData
@ -88,11 +88,12 @@ data class WalletForSession(
sealed class WalletConnectDialog : StateDialog {
data class ClipboardOrScanQr(val clipboardUri: String) : WalletConnectDialog()
object UnsupportedCard : WalletConnectDialog()
data object UnsupportedCard : WalletConnectDialog()
data class UnsupportedNetwork(val networks: List<String>? = null) : WalletConnectDialog()
data object UnsupportedDapp : WalletConnectDialog()
data class AddNetwork(val networks: List<String>) : WalletConnectDialog()
object OpeningSessionRejected : WalletConnectDialog()
object SessionTimeout : WalletConnectDialog()
data object OpeningSessionRejected : WalletConnectDialog()
data object SessionTimeout : WalletConnectDialog()
data class ApproveWcSession(
val session: WalletConnectSession,
val networks: List<Blockchain>,
@ -117,6 +118,10 @@ sealed class WalletConnectDialog : StateDialog {
data class BnbTransactionDialog(
val data: WcPreparedRequest.BnbTransaction,
) : WalletConnectDialog()
data class SignTransactionDialog(
val data: WcPreparedRequest.SignTransaction,
) : WalletConnectDialog()
}
data class WcTransactionData(
@ -138,7 +143,7 @@ data class WcPersonalSignData(
val topic: String,
val id: Long,
val dialogData: PersonalSignDialogData,
val type: WcEthereumSignMessage.WCSignType,
val type: WcSignMessage.WCSignType,
)
sealed class BinanceMessageData(

View file

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

View file

@ -8,11 +8,14 @@ import com.tangem.core.analytics.models.AnalyticsParam
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
@ -26,6 +29,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
@ -37,6 +41,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
@ -44,6 +49,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))
@ -140,7 +148,21 @@ internal class DetailsViewModel(
private fun sendFeedback() {
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Settings))
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() {

View file

@ -11,9 +11,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
@ -35,6 +33,8 @@ internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Uni
},
onBackClick = onBackClick,
)
LastWarningDialog(state = state)
}
@Composable
@ -181,6 +181,26 @@ private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) {
)
}
@Composable
private fun LastWarningDialog(state: ResetCardScreenState) {
if (state is ResetCardScreenState.ResetCardScreenContent && state.lastWarningDialog.isShown) {
BasicDialog(
title = stringResource(id = R.string.common_attention),
message = stringResource(id = R.string.card_settings_action_sheet_title),
dismissButton = DialogButton(
title = stringResource(id = R.string.card_settings_action_sheet_reset),
warning = true,
onClick = state.lastWarningDialog.onResetButtonClick,
),
confirmButton = DialogButton(
title = stringResource(id = R.string.common_cancel),
onClick = state.lastWarningDialog.onDismiss,
),
onDismissDialog = state.lastWarningDialog.onDismiss,
)
}
}
// region Preview
@Composable
private fun ResetCardScreenSample(modifier: Modifier = Modifier) {
@ -196,6 +216,11 @@ private fun ResetCardScreenSample(modifier: Modifier = Modifier) {
onAcceptCondition1ToggleClick = {},
onAcceptCondition2ToggleClick = {},
onResetButtonClick = {},
lastWarningDialog = ResetCardScreenState.ResetCardScreenContent.LastWarningDialog(
isShown = false,
onResetButtonClick = {},
onDismiss = {},
),
),
onBackClick = {},
)

View file

@ -15,9 +15,16 @@ internal sealed class ResetCardScreenState {
val onAcceptCondition1ToggleClick: (Boolean) -> Unit,
val onAcceptCondition2ToggleClick: (Boolean) -> Unit,
val onResetButtonClick: () -> Unit,
val lastWarningDialog: LastWarningDialog,
) : ResetCardScreenState() {
val resetButtonEnabled: Boolean
get() = accepted
data class LastWarningDialog(
val isShown: Boolean,
val onResetButtonClick: () -> Unit,
val onDismiss: () -> Unit,
)
}
internal enum class WarningsToReset {

View file

@ -30,7 +30,25 @@ internal class ResetCardViewModel(private val store: Store<AppState>) {
acceptCondition2Checked = state?.condition2Checked ?: false,
onAcceptCondition1ToggleClick = { store.dispatch(DetailsAction.ResetToFactory.AcceptCondition1(it)) },
onAcceptCondition2ToggleClick = { store.dispatch(DetailsAction.ResetToFactory.AcceptCondition2(it)) },
onResetButtonClick = { store.dispatch(DetailsAction.ResetToFactory.Proceed) },
onResetButtonClick = { showLastWarningDialog() },
lastWarningDialog = ResetCardScreenState.ResetCardScreenContent.LastWarningDialog(
isShown = state?.isLastWarningDialogShown ?: false,
onResetButtonClick = ::onLastWarningDialogResetClicked,
onDismiss = ::onLastWarningDialogDismiss,
),
)
}
private fun showLastWarningDialog() {
store.dispatch(DetailsAction.ResetToFactory.LastWarningDialogVisibility(isShown = true))
}
private fun onLastWarningDialogResetClicked() {
store.dispatch(DetailsAction.ResetToFactory.LastWarningDialogVisibility(isShown = false))
store.dispatch(DetailsAction.ResetToFactory.Proceed)
}
private fun onLastWarningDialogDismiss() {
store.dispatch(DetailsAction.ResetToFactory.LastWarningDialogVisibility(isShown = false))
}
}

View file

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

View file

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

View file

@ -0,0 +1,29 @@
package com.tangem.tap.features.details.ui.walletconnect.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.store
import com.tangem.wallet.R
internal object SignTransactionDialog {
fun create(preparedData: WcPreparedRequest.SignTransaction, context: Context): AlertDialog {
val message = context.getString(R.string.wallet_connect_alert_sign_message, "")
return AlertDialog.Builder(context).apply {
setTitle(context.getString(R.string.wallet_connect_title))
setMessage(message)
setPositiveButton(context.getText(R.string.common_sign)) { _, _ ->
store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData))
}
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
store.dispatch(WalletConnectAction.RejectRequest(preparedData.topic, preparedData.requestId))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.home.redux
import com.tangem.tap.common.entities.IndeterminateProgressButton
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
@ -24,6 +23,4 @@ sealed class HomeAction : Action {
data class GoToShop(val userCountryCode: String?) : HomeAction()
data class UpdateCountryCode(val userCountryCode: String) : HomeAction()
data class ChangeScanCardButtonState(val state: IndeterminateProgressButton) : HomeAction()
}

View file

@ -15,12 +15,10 @@ import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
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.preferencesStorage
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
@ -32,6 +30,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
@ -85,17 +85,16 @@ private suspend fun readCard() {
analyticsSource = AnalyticsParam.ScreensSources.Intro,
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)
@ -142,10 +141,6 @@ private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
private suspend fun navigateTo(appScreen: AppScreen) {
store.dispatchOnMain(NavigationAction.NavigateTo(appScreen))
delay(timeMillis = 200)
changeButtonState(ButtonState.ENABLED)
}
private fun changeButtonState(state: ButtonState) {
store.dispatchOnMain(HomeAction.ChangeScanCardButtonState(IndeterminateProgressButton(state)))
delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
}

View file

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

View file

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

View file

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

View file

@ -9,6 +9,7 @@ import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletBuilder
@ -42,8 +43,9 @@ object OnboardingHelper {
response.cardTypesResolver.isWallet2() || response.cardTypesResolver.isShibaWallet() -> {
val emptyWallets = response.card.wallets.isEmpty()
val activationInProgress = onboardingManager?.isActivationInProgress(cardId)
val backupNotActive = response.card.backupStatus?.isActive != true && !DemoHelper.isDemoCard(response)
emptyWallets || activationInProgress == true || backupNotActive
val isNoBackup = response.card.backupStatus == CardDTO.BackupStatus.NoBackup &&
!DemoHelper.isDemoCard(response)
emptyWallets || activationInProgress == true || isNoBackup
}
response.card.wallets.isNotEmpty() -> onboardingManager?.isActivationInProgress(cardId) ?: false
@ -73,6 +75,7 @@ object OnboardingHelper {
scanResponse: ScanResponse,
accessCode: String? = null,
backupCardsIds: List<String>? = null,
hasBackupError: Boolean = false,
) {
Analytics.setContext(scanResponse)
scope.launch {
@ -89,7 +92,7 @@ object OnboardingHelper {
val toggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles)
if (toggles.isGeneralManagerEnabled) {
store.dispatchWithMain(SaveWalletAction.SaveWalletAfterBackup)
store.dispatchWithMain(SaveWalletAction.SaveWalletAfterBackup(hasBackupError))
} else {
store.dispatchWithMain(SaveWalletAction.Save)
}
@ -97,7 +100,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)
@ -114,7 +117,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))
}
}
@ -134,8 +137,13 @@ object OnboardingHelper {
}
}
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse, backupCardsIds: List<String>?) {
val userWallet = UserWalletBuilder(scanResponse)
private suspend fun proceedWithScanResponse(
scanResponse: ScanResponse,
backupCardsIds: List<String>?,
hasBackupError: Boolean,
) {
val userWallet = UserWalletBuilder(scanResponse = scanResponse)
.hasBackupError(hasBackupError)
.backupCardsIds(backupCardsIds?.toSet())
.build()
.guard {

View file

@ -4,8 +4,10 @@ import com.tangem.common.CompletionResult
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.NavigationAction
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.common.extensions.makePrimaryWalletManager
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.entities.ProgressState
@ -171,8 +173,13 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
return
}
val topUpUrl = walletManager.getTopUpUrl() ?: return
val blockchain = walletManager.wallet.blockchain
val cryptoCurrency = CryptoCurrencyFactory().createCoin(
blockchain,
null,
scanResponse.derivationStyleProvider,
) ?: return
val topUpUrl = walletManager.getTopUpUrl(cryptoCurrency) ?: return
val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain)
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))

View file

@ -6,8 +6,10 @@ import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.common.extensions.makePrimaryWalletManager
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletIdBuilder
@ -293,8 +295,14 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
return
}
val topUpUrl = walletManager.getTopUpUrl() ?: return
val scanResponse = onboardingManager?.scanResponse ?: return
val blockchain = walletManager.wallet.blockchain
val cryptoCurrency = CryptoCurrencyFactory().createCoin(
blockchain,
null,
scanResponse.derivationStyleProvider,
) ?: return
val topUpUrl = walletManager.getTopUpUrl(cryptoCurrency) ?: return
val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain)
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))

View file

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

View file

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

View file

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

View file

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

View file

@ -524,10 +524,18 @@ class OnboardingWalletFragment :
override fun importWallet(
mnemonicComponents: List<String>,
passphrase: String?,
seedPhraseSource: SeedPhraseSource,
callback: (CompletionResult<CreateWalletResponse>) -> Unit,
) {
store.dispatch(OnboardingWallet2Action.ImportWallet(mnemonicComponents, seedPhraseSource, callback))
store.dispatch(
OnboardingWallet2Action.ImportWallet(
mnemonicComponents,
passphrase,
seedPhraseSource,
callback,
),
)
}
override fun allowScreenshots(allow: Boolean) {

View file

@ -28,5 +28,5 @@ internal sealed interface SaveWalletAction : Action {
data object SaveWalletWasShown : SaveWalletAction
data object SaveWalletAfterBackup : SaveWalletAction
data class SaveWalletAfterBackup(val hasBackupError: Boolean) : SaveWalletAction
}

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