diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1c44bd0851..1fff3a2a88 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -196,6 +196,7 @@ dependencies { } implementation(Library.accompanistAppCompatTheme) implementation(Library.accompanistSystemUiController) + implementation(Library.accompanistWebView) implementation(Library.xmlShimmer) implementation(Library.viewBindingDelegate) implementation(Library.armadillo) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5fa1e04d9d..93e65b1cc3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -139,9 +139,12 @@ - + + @@ -146,7 +146,11 @@ class TapApplication : Application(), ImageLoaderFactory { initConfigManager(configLoader, ::initWithConfigDependency) initWarningMessagesManager() - BlockchainSdkRetrofitBuilder.enableNetworkLogging = LogConfig.network.blockchainSdkNetwork + if (LogConfig.network.blockchainSdkNetwork) { + BlockchainSdkRetrofitBuilder.interceptors = listOf( + HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }, + ) + } userTokensRepository = UserTokensRepository.init( context = this, @@ -174,7 +178,7 @@ class TapApplication : Application(), ImageLoaderFactory { private fun initWithConfigDependency(config: Config) { shopService = TangemShopService(this, config.shopify!!) initAnalytics(this, config) - initFeedbackManager(this, preferencesStorage) + initFeedbackManager(this, preferencesStorage, foregroundActivityObserver, store) } private fun initAnalytics(application: Application, config: Config) { @@ -196,7 +200,12 @@ class TapApplication : Application(), ImageLoaderFactory { factory.build(Analytics, buildData) } - private fun initFeedbackManager(context: Context, preferencesStorage: PreferencesStorage) { + private fun initFeedbackManager( + context: Context, + preferencesStorage: PreferencesStorage, + foregroundActivityObserver: ForegroundActivityObserver, + store: Store, + ) { fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo = AdditionalFeedbackInfo().apply { appVersion = try { val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) @@ -229,12 +238,8 @@ class TapApplication : Application(), ImageLoaderFactory { val feedbackManager = FeedbackManager( infoHolder = additionalFeedbackInfo, logCollector = tangemLogCollector, - preferencesStorage = preferencesStorage, + chatManager = ChatManager(preferencesStorage, foregroundActivityObserver, store), ) - feedbackManager.chatInitializer = { zendeskConfig -> - Chat.INSTANCE.init(context, zendeskConfig.accountKey, zendeskConfig.appId) - Logger.setLoggable(LogConfig.zendesk) - } store.dispatch(GlobalAction.SetFeedbackManager(feedbackManager)) } diff --git a/app/src/main/java/com/tangem/tap/common/ShimmerRecyclerAdapter.kt b/app/src/main/java/com/tangem/tap/common/ShimmerRecyclerAdapter.kt new file mode 100644 index 0000000000..ff09e08de7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/ShimmerRecyclerAdapter.kt @@ -0,0 +1,34 @@ +package com.tangem.tap.common + +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView + +/** +[REDACTED_AUTHOR] + */ +open class ShimmerRecyclerAdapter( + private val viewHolderViewFactory: (ViewGroup) -> ViewGroup, +) : ListAdapter(DiffUtilCallback) { + + override fun getItemId(position: Int): Long { + return if (currentList.isEmpty()) 0 else currentList[position].hashCode().toLong() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ShimmerVH { + return ShimmerVH(viewHolderViewFactory.invoke(parent)) + } + + override fun onBindViewHolder(holder: ShimmerVH, position: Int) {} + + object DiffUtilCallback : DiffUtil.ItemCallback() { + override fun areContentsTheSame(oldItem: ShimmerData, newItem: ShimmerData) = oldItem == newItem + override fun areItemsTheSame(oldItem: ShimmerData, newItem: ShimmerData) = oldItem == newItem + } +} + +class ShimmerVH(viewGroup: ViewGroup) : RecyclerView.ViewHolder(viewGroup) + +@Suppress("UnusedPrivateMember") +data class ShimmerData(private val any: String = "") \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt index 1f227fc391..79ce7f700a 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt @@ -114,19 +114,53 @@ data class BasicEventsSourceData( val paramCardBalanceState: AnalyticsParam.CardBalanceState by lazy { calculateAmount().toCardBalanceState() } private fun calculateAmount(): BigDecimal { - return biometricsWalletDataModels?.calculateTotalCryptoAmount() - ?: walletState.walletsDataFromStores.calculateTotalCryptoAmount() + val calculator: IBalanceCalculator = biometricsWalletDataModels + ?.let { BiometricsBalanceCalculator(it) } + ?: BalanceCalculator(walletState) + + return calculator.calculate() } private fun BigDecimal.toCardBalanceState(): AnalyticsParam.CardBalanceState = when { isZero() -> AnalyticsParam.CardBalanceState.Empty else -> AnalyticsParam.CardBalanceState.Full } +} - private fun List.calculateTotalCryptoAmount(): BigDecimal { - return this - .map { it.status.amount } - .reduce(BigDecimal::plus) +private interface IBalanceCalculator { + fun calculate(): BigDecimal +} + +private class BiometricsBalanceCalculator( + private val walletDataModel: List, +) : IBalanceCalculator { + + override fun calculate(): BigDecimal { + val singleToken = walletDataModel + .filter { it.currency.isToken() } + .firstOrNull { it.isCardSingleToken } + + val totalAmount = singleToken?.status?.amount + ?: walletDataModel.calculateTotalCryptoAmount() + + return totalAmount + } + + private fun List.calculateTotalCryptoAmount(): BigDecimal = this + .map { it.status.amount } + .reduce(BigDecimal::plus) +} + +private class BalanceCalculator( + private val walletState: WalletState, +) : IBalanceCalculator { + + override fun calculate(): BigDecimal { + val singleTokenData = walletState.primaryTokenData + val totalAmount = singleTokenData?.currencyData?.amount + ?: walletState.walletsDataFromStores.calculateTotalCryptoAmount() + + return totalAmount } } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index 2e5a925560..cebc4c405e 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -10,7 +10,7 @@ sealed class AnalyticsParam { class Token(token: com.tangem.blockchain.common.Token) : CurrencyType(token.symbol) class FiatCurrency( fiatCurrency: com.tangem.tap.common.entities.FiatCurrency, - ) : CurrencyType(fiatCurrency.symbol) + ) : CurrencyType(fiatCurrency.code) class Amount(amount: com.tangem.blockchain.common.Amount) : CurrencyType(amount.currencySymbol) } @@ -63,8 +63,18 @@ sealed class AnalyticsParam { object BlockchainSdk : Error("Blockchain Sdk Error") } + sealed class ScannedFrom(val value: String) { + object Introduction : ScannedFrom("Introduction") + object Main : ScannedFrom("Main") + object SignIn : ScannedFrom("Sign In") + object MyWallets : ScannedFrom("My Wallets") + } + companion object Key { - const val BatchId = "Batch" + const val Batch = "Batch" + const val ProductType = "Product Type" + const val Firmware = "Firmware" + const val Currency = "Currency" const val ErrorDescription = "Error Description" const val ErrorCode = "Error Code" const val ErrorKey = "Error Key" diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt index 0d9a529469..465cfe98bf 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt @@ -11,6 +11,15 @@ sealed class Basic( error: Throwable? = null, ) : AnalyticsEvent("Basic", event, params, error) { + class CardWasScanned( + source: AnalyticsParam.ScannedFrom, + ) : Basic( + event = "Card Was Scanned", + params = mapOf( + "Source" to source.value, + ), + ) + class SignedIn( state: AnalyticsParam.CardBalanceState, currency: AnalyticsParam.CardCurrency, @@ -19,14 +28,14 @@ sealed class Basic( event = "Signed in", params = mapOf( "State" to state.value, - "Currency" to currency.value, - AnalyticsParam.BatchId to batch, + AnalyticsParam.Currency to currency.value, + AnalyticsParam.Batch to batch, ), ) class ToppedUp(currency: AnalyticsParam.CardCurrency) : Basic( event = "Topped up", - params = mapOf("Currency" to currency.value), + params = mapOf(AnalyticsParam.Currency to currency.value), ) class ScanError(error: Throwable) : Basic( diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt index 0d799d379f..602365c0c6 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt @@ -14,6 +14,5 @@ sealed class IntroductionProcess( class ButtonTokensList : IntroductionProcess("Button - Tokens List") class ButtonBuyCards : IntroductionProcess("Button - Buy Cards") class ButtonScanCard : IntroductionProcess("Button - Scan Card") - class CardWasScanned : IntroductionProcess("Card Was Scanned") class ButtonRequestSupport : IntroductionProcess("Button - Request Support") } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt index 6f718755f0..6c8580df05 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt @@ -13,8 +13,8 @@ sealed class MainScreen( class ScreenOpened : MainScreen("Screen opened") class ButtonScanCard : MainScreen("Button - Scan Card") - class CardWasScanned : MainScreen("Card Was Scanned") class ButtonMyWallets : MainScreen("Button - My Wallets") + class ButtonBuy : MainScreen("Button - Buy") class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreen( event = "Enable Biometric", diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/MyWallets.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/MyWallets.kt index 4db0526ad2..65ab7937c2 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/MyWallets.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/MyWallets.kt @@ -8,7 +8,6 @@ sealed class MyWallets( ) : AnalyticsEvent("My Wallets", event, params) { class MyWalletsScreenOpened : MyWallets(event = "My Wallets Screen Opened") - class CardWasScanned : MyWallets(event = "Card Was Scanned") sealed class Button { class ScanNewCard : MyWallets(event = "Button - Scan New Card") diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt index 4ec37c4806..c0d61c52d1 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt @@ -51,7 +51,7 @@ sealed class Onboarding( class ButtonBuyCrypto(currency: AnalyticsParam.CurrencyType) : Topup( event = "Button - Buy Crypto", - params = mapOf("Currency" to currency.value), + params = mapOf(AnalyticsParam.Currency to currency.value), ) class ButtonShowWalletAddress : Topup("Button - Show the Wallet Address") @@ -67,16 +67,26 @@ sealed class Onboarding( class SetupFinished : Twins("Twin Setup Finished") } - class PinCodeSet : Onboarding("Onboarding", "PIN code set") + class PinScreenOpened : Onboarding("Onboarding", "PIN screen opened") + class ButtonSetPinCode : Onboarding("Onboarding", "Button - Set PIN Code") + class CardConnectionScreenOpened : Onboarding("Onboarding", "Card Connection Screen Opened") class ButtonConnect : Onboarding("Onboarding", "Button - Connect") + class PinCodeSet : Onboarding("Onboarding", "PIN code set") + + class KYCScreenOpened : Onboarding("Onboarding", "KYC screen opened") class KYCStarted : Onboarding("Onboarding", "KYC started") class KYCInProgress : Onboarding("Onboarding", "KYC in progress") class KYCRejected : Onboarding("Onboarding", "KYC rejected") + class ClaimScreenOpened : Onboarding("Onboarding", "Claim screen opened") class ButtonClaim : Onboarding("Onboarding", "Button - Claim") class ClaimWasSuccessfully : Onboarding("Onboarding", "Claim was successfully") + class ButtonChat : Onboarding("Onboarding", "Button - Chat") + class NotEnoughGasError : Onboarding("Onboarding", "Not Enough Gas Error") + class CardNotPassedError : Onboarding("Onboarding", "Card Not Passed Error") + class EnableBiometrics(state: AnalyticsParam.OnOffState) : Onboarding( category = "Onboarding / Biometric", event = "Enable Biometric", diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt index cad34430d9..a5417d775b 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt @@ -12,7 +12,6 @@ sealed class SignIn( ) : AnalyticsEvent("Sign In", event, params, error) { class ScreenOpened : SignIn(event = "Sing In Screen Opened") - class CardWasScanned : SignIn(event = "Card Was Scanned") class ButtonBiometricSignIn : SignIn(event = "Button - Biometric Sign In") class ButtonCardSignIn : SignIn(event = "Button - Card Sign In") diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/BatchIdParamsInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/BatchIdParamsInterceptor.kt deleted file mode 100644 index b7e7ec6fbf..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/BatchIdParamsInterceptor.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.tap.common.analytics.paramsInterceptor - -import com.tangem.core.analytics.api.ParamsInterceptor -import com.tangem.core.analytics.AnalyticsEvent -import com.tangem.tap.common.analytics.events.AnalyticsParam - -/** -[REDACTED_AUTHOR] - */ -class BatchIdParamsInterceptor( - val batchId: String, -) : ParamsInterceptor { - - override fun id(): String = this::class.java.simpleName - - override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = true - - override fun intercept(params: MutableMap) { - params[AnalyticsParam.BatchId] = batchId - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt new file mode 100644 index 0000000000..5c087a4f20 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt @@ -0,0 +1,72 @@ +package com.tangem.tap.common.analytics.paramsInterceptor + +import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.api.ParamsInterceptor +import com.tangem.domain.common.ProductType +import com.tangem.domain.common.ScanResponse +import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter +import com.tangem.tap.common.analytics.events.AnalyticsParam +import com.tangem.tap.common.analytics.events.IntroductionProcess +import com.tangem.tap.common.analytics.events.MainScreen +import com.tangem.tap.features.demo.DemoHelper + +/** +[REDACTED_AUTHOR] + */ +class CardContextInterceptor( + private val scanResponse: ScanResponse?, +) : ParamsInterceptor { + + override fun id(): String = CardContextInterceptor.id() + + override fun canBeAppliedTo(event: AnalyticsEvent): Boolean { + return when (event) { + is IntroductionProcess.ButtonScanCard, is MainScreen.ButtonScanCard -> false + else -> true + } + } + + override fun intercept(params: MutableMap) { + scanResponse ?: return + + val card = scanResponse.card + params[AnalyticsParam.Batch] = card.batchId + params[AnalyticsParam.ProductType] = getProductType(scanResponse) + params[AnalyticsParam.Firmware] = card.firmwareVersion.stringValue + + ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)?.let { + params[AnalyticsParam.Currency] = it.value + } + } + + private fun getProductType(scanResponse: ScanResponse): String { + return when (scanResponse.productType) { + ProductType.Note -> "Note" + ProductType.Twins -> "Twin" + ProductType.Wallet -> "Wallet" + ProductType.SaltPay -> if (scanResponse.cardTypesResolver.isSaltPayVisa()) { + "Visa" + } else { + "Visa Backup" + } + ProductType.Start2Coin -> "Start2Coin" + else -> if (DemoHelper.isDemoCard(scanResponse)) { + if (DemoHelper.isTestDemoCard(scanResponse)) { + "Demo Test" + } else { + when (scanResponse.card.cardId.substring(0..1)) { + "AC" -> "Demo Wallet" + "AB" -> "Demo Note" + else -> "Demo Other" + } + } + } else { + "Other" + } + } + } + + companion object { + fun id(): String = CardContextInterceptor::class.java.simpleName + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/LinkedCardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/LinkedCardContextInterceptor.kt new file mode 100644 index 0000000000..9dcfd80714 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/LinkedCardContextInterceptor.kt @@ -0,0 +1,26 @@ +package com.tangem.tap.common.analytics.paramsInterceptor + +import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.api.ParamsInterceptor +import com.tangem.domain.common.ScanResponse + +/** +[REDACTED_AUTHOR] + */ +class LinkedCardContextInterceptor( + private val scanResponse: ScanResponse, + val parent: LinkedCardContextInterceptor? = null, +) : ParamsInterceptor { + + private val contextInterceptor = CardContextInterceptor(scanResponse) + + override fun id(): String = LinkedCardContextInterceptor.id() + + override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = contextInterceptor.canBeAppliedTo(event) + + override fun intercept(params: MutableMap) = contextInterceptor.intercept(params) + + companion object { + fun id(): String = LinkedCardContextInterceptor::class.java.simpleName + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt b/app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt new file mode 100644 index 0000000000..bc69217fa1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt @@ -0,0 +1,53 @@ +package com.tangem.tap.common.chat + +import android.content.Context +import android.os.Build +import com.tangem.tap.ForegroundActivityObserver +import com.tangem.datasource.config.models.ChatConfig +import com.tangem.tap.common.chat.opener.ChatOpener +import com.tangem.datasource.config.models.SprinklrConfig +import com.tangem.datasource.config.models.ZendeskConfig +import com.tangem.tap.common.chat.opener.implementation.SprinklrChatOpener +import com.tangem.tap.common.chat.opener.implementation.ZendeskChatOpener +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.persistence.PreferencesStorage +import org.rekotlin.Store + +class ChatManager( + private val preferencesStorage: PreferencesStorage, + private val foregroundActivityObserver: ForegroundActivityObserver, + private val store: Store, +) { + private val openers = mutableMapOf() + + fun open(config: ChatConfig, feedbackDataBuilder: (Context) -> String) { + val opener = openers.getOrPut(config) { + when (config) { + is SprinklrConfig -> SprinklrChatOpener(getSprinklrUserId(), config, store, foregroundActivityObserver) + is ZendeskConfig -> ZendeskChatOpener(getZendeskUserId(), config, foregroundActivityObserver) + } + } + + opener.open(feedbackDataBuilder) + } + + private fun getZendeskUserId(): String { + if (preferencesStorage.zendeskFirstLaunchTime == null) { + preferencesStorage.zendeskFirstLaunchTime = System.currentTimeMillis() + } + + return getChatUserId(preferencesStorage.zendeskFirstLaunchTime!!) + } + + private fun getSprinklrUserId(): String { + if (preferencesStorage.sprinklrFirstLaunchTime == null) { + preferencesStorage.sprinklrFirstLaunchTime = System.currentTimeMillis() + } + + return getChatUserId(preferencesStorage.sprinklrFirstLaunchTime!!) + } + + private fun getChatUserId(firstLaunchTimeMillis: Long): String { + return "${firstLaunchTimeMillis}${Build.MODEL}".hashCode().toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/chat/opener/ChatOpener.kt b/app/src/main/java/com/tangem/tap/common/chat/opener/ChatOpener.kt new file mode 100644 index 0000000000..e3757286e7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/chat/opener/ChatOpener.kt @@ -0,0 +1,7 @@ +package com.tangem.tap.common.chat.opener + +import android.content.Context + +internal interface ChatOpener { + fun open(feedbackDataBuilder: (Context) -> String) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/SprinklrChatOpener.kt b/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/SprinklrChatOpener.kt new file mode 100644 index 0000000000..c9b8fdb4f9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/SprinklrChatOpener.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.common.chat.opener.implementation + +import android.content.Context +import android.content.Intent +import com.tangem.tap.ForegroundActivityObserver +import com.tangem.tap.common.chat.opener.ChatOpener +import com.tangem.datasource.config.models.SprinklrConfig +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.features.sprinklr.redux.SprinklrAction +import com.tangem.tap.features.sprinklr.ui.SprinklrActivity +import com.tangem.tap.withForegroundActivity +import org.rekotlin.Store + +internal class SprinklrChatOpener( + private val userId: String, + private val config: SprinklrConfig, + private val store: Store, + private val foregroundActivityObserver: ForegroundActivityObserver, +) : ChatOpener { + override fun open(feedbackDataBuilder: (Context) -> String) { + store.dispatch(SprinklrAction.Init(userId, config)) + foregroundActivityObserver.withForegroundActivity { activity -> + val intent = Intent(activity, SprinklrActivity::class.java) + activity.startActivity(intent) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/ZendeskChatOpener.kt b/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/ZendeskChatOpener.kt new file mode 100644 index 0000000000..0e5d38b0da --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/ZendeskChatOpener.kt @@ -0,0 +1,72 @@ +package com.tangem.tap.common.chat.opener.implementation + +import android.content.Context +import com.tangem.core.analytics.Analytics +import com.tangem.domain.common.LogConfig +import com.tangem.tap.ForegroundActivityObserver +import com.tangem.tap.common.chat.opener.ChatOpener +import com.tangem.datasource.config.models.ZendeskConfig +import com.tangem.tap.withForegroundActivity +import com.tangem.wallet.R +import com.zendesk.logger.Logger +import zendesk.chat.Chat +import zendesk.chat.ChatConfiguration +import zendesk.chat.ChatEngine +import zendesk.chat.ChatProvidersConfiguration +import zendesk.chat.VisitorInfo +import zendesk.configurations.Configuration +import zendesk.messaging.MessagingActivity + +internal class ZendeskChatOpener( + private val userId: String, + private val config: ZendeskConfig, + private val foregroundActivityObserver: ForegroundActivityObserver, +) : ChatOpener { + private var isInitialized = false + + override fun open(feedbackDataBuilder: (Context) -> String) { + foregroundActivityObserver.withForegroundActivity { activity -> + initZendeskIfNeeded(activity.applicationContext) + setChatVisitorInfo() + setChatVisitorNote(feedbackDataBuilder(activity)) + showMessagingActivity(activity) + } + } + + private fun initZendeskIfNeeded(context: Context) { + if (isInitialized) return + isInitialized = true + + Chat.INSTANCE.init(context, config.accountKey, config.appId) + Logger.setLoggable(LogConfig.zendesk) + } + + private fun setChatVisitorInfo() { + val visitorInfo = VisitorInfo.builder().withName("User $userId").build() + + Chat.INSTANCE.chatProvidersConfiguration = + ChatProvidersConfiguration.builder().withVisitorInfo(visitorInfo).build() + } + + private fun setChatVisitorNote(note: String) { + Chat.INSTANCE.providers()?.profileProvider()?.setVisitorNote(note) + } + + private fun showMessagingActivity(context: Context) { + Analytics.send(com.tangem.tap.common.analytics.events.Chat.ScreenOpened()) + MessagingActivity.builder() + .withMultilineResponseOptionsEnabled(false) + .withBotLabelStringRes(R.string.chat_bot_name) + .withBotAvatarDrawable(R.mipmap.ic_launcher) + .withEngines(ChatEngine.engine()) + .show(context, buildChatConfig()) + } + + private fun buildChatConfig(): Configuration { + return ChatConfiguration.builder() + .withOfflineFormEnabled(true) + .withAgentAvailabilityEnabled(true) + .withPreChatFormEnabled(false) + .build() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt b/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt new file mode 100644 index 0000000000..8d3ce8e5ad --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt @@ -0,0 +1,43 @@ +package com.tangem.tap.common.extensions + +import com.tangem.core.analytics.Analytics +import com.tangem.domain.common.ScanResponse +import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterceptor + +/** +[REDACTED_AUTHOR] + */ + +/** + * Sets the new context + */ +fun Analytics.setContext(scanResponse: ScanResponse) { + addParamsInterceptor(LinkedCardContextInterceptor(scanResponse)) +} + +/** + * Erases the context + */ +fun Analytics.eraseContext() { + removeParamsInterceptor(LinkedCardContextInterceptor.id()) +} + +/** + * Adds a new context and keeps a previous context as the parent of the new one + */ +fun Analytics.addContext(scanResponse: ScanResponse) { + val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id()) as? LinkedCardContextInterceptor + val newContext = LinkedCardContextInterceptor(scanResponse, parent = currentContext) + + addParamsInterceptor(newContext) +} + +/** + * Removes the current context and restores the previous one if it was present. + */ +fun Analytics.removeContext() { + val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id()) as? LinkedCardContextInterceptor + val previousContext = currentContext?.parent ?: return + + addParamsInterceptor(previousContext) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/ImageRequest.kt b/app/src/main/java/com/tangem/tap/common/extensions/ImageRequest.kt new file mode 100644 index 0000000000..9da1448d9c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/ImageRequest.kt @@ -0,0 +1,32 @@ +package com.tangem.tap.common.extensions + +import android.widget.ImageView +import coil.ImageLoader +import coil.imageLoader +import coil.load +import coil.request.Disposable +import coil.request.ImageRequest +import com.tangem.tap.features.wallet.redux.Artwork +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +fun ImageRequest.Builder.cardImageData(any: Any?): ImageRequest.Builder = apply { + data(any?.parseData()) +} + +fun ImageView.loadCardImage( + data: Any?, + imageLoader: ImageLoader = context.imageLoader, + builder: ImageRequest.Builder.() -> Unit = {}, +): Disposable = this.load( + data = data?.parseData(), + imageLoader = imageLoader, + builder = builder, +) + +private fun Any?.parseData(): Any? = when { + this is String && this == Artwork.SALT_PAY_URL -> R.drawable.img_salt_pay_visa + else -> this +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index 97997ba97f..1426f3a118 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -2,6 +2,9 @@ package com.tangem.tap.common.extensions import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.TransactionHistoryProvider import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager import com.tangem.common.services.Result @@ -10,6 +13,7 @@ import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.amountToCreateAccount import com.tangem.tap.domain.getFirstToken import com.tangem.tap.features.demo.isDemoCard +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.AddressData import com.tangem.tap.features.wallet.redux.reducers.createAddressesData import com.tangem.tap.network.NetworkConnectivity @@ -31,6 +35,9 @@ suspend fun WalletManager.safeUpdate(): Result = try { Result.Success(wallet) } else { update() + if (wallet.blockchain == Blockchain.SaltPay && this is TransactionHistoryProvider) { + this.getTransactionHistory(wallet.address, wallet.blockchain, wallet.getTokens()) + } Result.Success(wallet) } } catch (exception: Exception) { @@ -76,10 +83,33 @@ fun WalletManager?.getAddressData(): AddressData? { return if (addressDataList.isEmpty()) null else addressDataList[0] } +fun WalletManager.getTxHistory(currency: Currency): List = wallet.getTxHistory(currency) + +fun WalletManager.getBlockchainTxHistory(): List = wallet.getBlockchainTxHistory() + +fun WalletManager.getTokenTxHistory(token: Token): List = wallet.getTokenTxHistory(token) + fun WalletManager.Companion.stub(): T { val wallet = Wallet(Blockchain.Unknown, setOf(), Wallet.PublicKey(byteArrayOf(), null, null), setOf()) return object : WalletManager(wallet) { override val currentHost: String = "" override suspend fun update() {} } as T +} + +fun Wallet.getTxHistory(currency: Currency): List { + return (currency as? Currency.Token)?.let { this.getTokenTxHistory(it.token) } + ?: getBlockchainTxHistory() +} + +fun Wallet.getBlockchainTxHistory(): List { + return historyTransactions.filter { + it.contractAddress.isNullOrEmpty() + } +} + +fun Wallet.getTokenTxHistory(token: Token): List { + return historyTransactions.filter { + it.contractAddress == token.contractAddress + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt index c6513c9188..8de0b8e686 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt @@ -1,24 +1,14 @@ package com.tangem.tap.common.feedback import android.content.Context -import android.os.Build -import com.tangem.core.analytics.Analytics import com.tangem.domain.common.TapWorkarounds +import com.tangem.tap.common.chat.ChatManager +import com.tangem.datasource.config.models.ChatConfig import com.tangem.tap.common.extensions.sendEmail import com.tangem.tap.common.log.TangemLogCollector -import com.tangem.datasource.config.models.ZendeskConfig import com.tangem.tap.foregroundActivityObserver -import com.tangem.tap.persistence.PreferencesStorage import com.tangem.tap.withForegroundActivity -import com.tangem.wallet.R import timber.log.Timber -import zendesk.chat.Chat -import zendesk.chat.ChatConfiguration -import zendesk.chat.ChatEngine -import zendesk.chat.ChatProvidersConfiguration -import zendesk.chat.VisitorInfo -import zendesk.configurations.Configuration -import zendesk.messaging.MessagingActivity import java.io.File import java.io.FileWriter import java.io.StringWriter @@ -29,19 +19,8 @@ import java.io.StringWriter class FeedbackManager( val infoHolder: AdditionalFeedbackInfo, private val logCollector: TangemLogCollector, - private val preferencesStorage: PreferencesStorage, + private val chatManager: ChatManager, ) { - private var lastUsedConfigForInitialization: ZendeskConfig? = null - - var chatInitializer: ((ZendeskConfig) -> Unit)? = null - - fun initChat(zendeskConfig: ZendeskConfig) { - // prevent double initialization with the same config - if (lastUsedConfigForInitialization == zendeskConfig) return - - lastUsedConfigForInitialization = zendeskConfig - chatInitializer?.invoke(zendeskConfig) - } fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) { feedbackData.prepare(infoHolder) @@ -57,12 +36,12 @@ class FeedbackManager( } } - fun openChat(feedbackData: FeedbackData) { - feedbackData.prepare(infoHolder) - foregroundActivityObserver.withForegroundActivity { activity -> - setChatVisitorInfo() - setChatVisitorNote(activity, feedbackData) - showMessagingActivity(activity) + fun openChat(config: ChatConfig, feedbackData: FeedbackData) { + chatManager.open(config) { context -> + feedbackData.run { + prepare(infoHolder) + joinTogether(context, infoHolder) + } } } @@ -85,47 +64,6 @@ class FeedbackManager( } } - private fun setChatVisitorInfo() { - if (preferencesStorage.chatFirstLaunchTime == null) { - preferencesStorage.chatFirstLaunchTime = System.currentTimeMillis() - } - val chatUserId = (preferencesStorage.chatFirstLaunchTime.toString() + Build.MODEL).hashCode() - val visitorInfo = VisitorInfo.builder() - .withName("User $chatUserId") - .build() - - Chat.INSTANCE.chatProvidersConfiguration = ChatProvidersConfiguration.builder() - .withVisitorInfo(visitorInfo) - .build() - } - - private fun setChatVisitorNote( - context: Context, - feedbackData: FeedbackData, - ) { - Chat.INSTANCE.providers() - ?.profileProvider() - ?.setVisitorNote(feedbackData.joinTogether(context, infoHolder)) - } - - private fun showMessagingActivity(context: Context) { - Analytics.send(com.tangem.tap.common.analytics.events.Chat.ScreenOpened()) - MessagingActivity.builder() - .withMultilineResponseOptionsEnabled(false) - .withBotLabelStringRes(R.string.chat_bot_name) - .withBotAvatarDrawable(R.mipmap.ic_launcher) - .withEngines(ChatEngine.engine()) - .show(context, buildChatConfig()) - } - - private fun buildChatConfig(): Configuration { - return ChatConfiguration.builder() - .withOfflineFormEnabled(true) - .withAgentAvailabilityEnabled(true) - .withPreChatFormEnabled(false) - .build() - } - private fun getSupportEmail(): String { return if (TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)) { S2C_SUPPORT_EMAIL diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index f03dfc70ca..3bcb47a092 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -13,11 +13,12 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWallet 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.sprinklr.redux.SprinklrReducer import com.tangem.tap.features.tokens.redux.TokensReducer import com.tangem.tap.features.wallet.redux.reducers.WalletReducer -import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.features.walletSelector.redux.WalletSelectorReducer import com.tangem.tap.features.welcome.redux.WelcomeReducer +import com.tangem.tap.proxy.AppStateHolder import org.rekotlin.Action fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder): AppState { @@ -42,6 +43,7 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder) welcomeState = WelcomeReducer.reduce(action, state), saveWalletState = SaveWalletReducer.reduce(action, state), walletSelectorState = WalletSelectorReducer.reduce(action, state), + sprinklrState = SprinklrReducer.reduce(action, state), ) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 820f5b3d3f..e9b253d90d 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -31,6 +31,8 @@ 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.sprinklr.redux.SprinklrMiddleware +import com.tangem.tap.features.sprinklr.redux.SprinklrState import com.tangem.tap.features.tokens.redux.TokensMiddleware import com.tangem.tap.features.tokens.redux.TokensState import com.tangem.tap.features.wallet.redux.WalletState @@ -61,6 +63,7 @@ data class AppState( val welcomeState: WelcomeState = WelcomeState(), val saveWalletState: SaveWalletState = SaveWalletState(), val walletSelectorState: WalletSelectorState = WalletSelectorState(), + val sprinklrState: SprinklrState = SprinklrState(), ) : StateType { val domainState: DomainState @@ -101,6 +104,7 @@ data class AppState( WalletSelectorMiddleware().middleware, LockUserWalletsTimerMiddleware().middleware, AccessCodeRequestPolicyMiddleware().middleware, + SprinklrMiddleware().middleware, ) } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index 4017f9b32f..c4c8a33527 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -4,6 +4,8 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError +import com.tangem.datasource.config.ConfigManager +import com.tangem.datasource.config.models.ChatConfig import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.feedback.FeedbackData @@ -13,9 +15,7 @@ import com.tangem.tap.common.redux.ErrorAction import com.tangem.tap.common.redux.NotificationAction import com.tangem.tap.common.redux.StateDialog import com.tangem.tap.common.redux.ToastNotificationAction -import com.tangem.datasource.config.models.ZendeskConfig import com.tangem.tap.domain.TapError -import com.tangem.datasource.config.ConfigManager import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.features.details.redux.SecurityOption @@ -36,18 +36,20 @@ sealed class GlobalAction : Action { sealed class Onboarding : GlobalAction() { /** * Initiate an onboarding process. - * For SaltPay cards it's additionally checks for unfinished backup. - * For resuming unfinished backup for standard Wallet cards see CheckForUnfinishedBackup and - * StartForUnfinishedBackup + * For resuming unfinished backup of standard Wallet and SaltPay cards see + * BackupAction.CheckForUnfinishedBackup, GlobalAction.Onboarding.StartForUnfinishedBackup */ data class Start(val scanResponse: ScanResponse, val canSkipBackup: Boolean = true) : Onboarding() /** - * Initiate resuming of unfinished backup only for standard Wallet cards. - * For SaltPay cards unfinished backup resumed after scanning the card on HomeScreen through Onboarding.Start. - * See more Onboarding.Start, CheckForUnfinishedBackup + * Initiate resuming of unfinished backup for standard Wallet and SaltPay cards. + * See more BackupAction.CheckForUnfinishedBackup */ - data class StartForUnfinishedBackup(val addedBackupCardsCount: Int) : Onboarding() + data class StartForUnfinishedBackup( + val addedBackupCardsCount: Int, + val isSaltPayVisa: Boolean, + ) : Onboarding() + object Stop : Onboarding() } @@ -87,7 +89,7 @@ sealed class GlobalAction : Action { data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction() data class SendEmail(val feedbackData: FeedbackData) : GlobalAction() - data class OpenChat(val feedbackData: FeedbackData, val zendeskConfig: ZendeskConfig? = null) : GlobalAction() + data class OpenChat(val feedbackData: FeedbackData, val chatConfig: ChatConfig? = null) : GlobalAction() data class UpdateFeedbackInfo(val walletManagers: List) : GlobalAction() object ExchangeManager : GlobalAction() { diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index b880c71872..b7fd1db3fe 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -3,8 +3,11 @@ package com.tangem.tap.common.redux.global import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard -import com.tangem.common.extensions.ifNotNull +import com.tangem.datasource.config.models.Config +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.LogConfig +import com.tangem.domain.common.ProductType +import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.common.extensions.dispatchDebugErrorNotification import com.tangem.tap.common.extensions.dispatchDialogShow @@ -14,16 +17,22 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.features.send.redux.SendAction import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.network.exchangeServices.BuyExchangeService import com.tangem.tap.network.exchangeServices.CardExchangeRules import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager -import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoApi +import com.tangem.tap.network.exchangeServices.ExchangeService +import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoEnvironment import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService +import com.tangem.tap.network.exchangeServices.utorg.UtorgAuthProvider +import com.tangem.tap.network.exchangeServices.utorg.UtorgEnvironment +import com.tangem.tap.network.exchangeServices.utorg.UtorgExchangeService import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager import com.tangem.tap.userTokensRepository +import com.tangem.wallet.BuildConfig import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.DispatchFunction @@ -98,17 +107,16 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di val scanResponse = globalState.scanResponse ?: globalState.onboardingState.onboardingManager?.scanResponse // if config not set -> try to get it based on a scanResponse.productType - val unsafeZendeskConfig = action.zendeskConfig ?: when { - scanResponse?.cardTypesResolver?.isSaltPay() == true -> config.saltPayConfig?.zendesk + val unsafeChatConfig = action.chatConfig ?: when { + scanResponse?.cardTypesResolver?.isSaltPay() == true -> config.saltPayConfig?.sprinklr else -> config.zendesk } - val zendeskConfig = unsafeZendeskConfig.guard { + val chatConfig = unsafeChatConfig.guard { store.dispatchDebugErrorNotification("ZendeskConfig not initialized") return } - feedbackManager.initChat(zendeskConfig) - feedbackManager.openChat(action.feedbackData) + feedbackManager.openChat(chatConfig, action.feedbackData) } is GlobalAction.UpdateWalletSignedHashes -> { store.dispatch(WalletAction.Warnings.CheckRemainingSignatures(action.remainingSignatures)) @@ -119,38 +127,23 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di } is GlobalAction.ExchangeManager.Init -> { val appStateSafe = appState() ?: return - val config = appStateSafe.globalState.configManager?.config - ifNotNull( - config?.mercuryoWidgetId, - config?.mercuryoSecret, - config?.moonPayApiKey, - config?.moonPayApiSecretKey, - ) { mercuryoWidgetId, mercuryoSecret, moonPayKey, moonPaySecretKey -> - scope.launch { - val buyService = MercuryoService( - apiVersion = MercuryoApi.API_VERSION, - mercuryoWidgetId = mercuryoWidgetId, - secret = mercuryoSecret, - logEnabled = LogConfig.network.mercuryoService, - ) - val sellService = MoonPayService( - apiKey = moonPayKey, - secretKey = moonPaySecretKey, - logEnabled = LogConfig.network.moonPayService, - ) - val cardProvider = { - store.state.globalState.scanResponse?.card - ?: store.state.globalState.onboardingState.onboardingManager?.scanResponse?.card - } + val config = appStateSafe.globalState.configManager?.config ?: return - val exchangeManager = CurrencyExchangeManager( - buyService = buyService, - sellService = sellService, - primaryRules = CardExchangeRules(cardProvider), - ) - store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager)) - store.dispatchOnMain(GlobalAction.ExchangeManager.Update) + scope.launch { + val scanResponseProvider: () -> ScanResponse? = { + store.state.globalState.scanResponse + ?: store.state.globalState.onboardingState.onboardingManager?.scanResponse } + val productTypeProvider: () -> ProductType? = { scanResponseProvider.invoke()?.productType } + val cardProvider: () -> CardDTO? = { scanResponseProvider.invoke()?.card } + + val exchangeManager = CurrencyExchangeManager( + buyService = makeBuyExchangeService(config, productTypeProvider), + sellService = makeSellExchangeService(config), + primaryRules = CardExchangeRules(cardProvider), + ) + store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager)) + store.dispatchOnMain(GlobalAction.ExchangeManager.Update) } } is GlobalAction.ExchangeManager.Init.Success -> {} @@ -202,4 +195,38 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di } } } +} + +private fun makeSellExchangeService(config: Config): ExchangeService { + return MoonPayService( + apiKey = config.moonPayApiKey, + secretKey = config.moonPayApiSecretKey, + logEnabled = LogConfig.network.moonPayService, + ) +} + +private fun makeBuyExchangeService(config: Config, productTypeProvider: () -> ProductType?): ExchangeService { + return BuyExchangeService( + productTypeProvider = productTypeProvider, + mercuryoService = makeMercuryoExchangeService(config), + utorgService = makeUtorgExchangeService(config), + ) +} + +private fun makeMercuryoExchangeService(config: Config): MercuryoService { + val mercuryoEnvironment = MercuryoEnvironment.prod(config.mercuryoWidgetId, config.mercuryoSecret) + return MercuryoService(mercuryoEnvironment) +} + +private fun makeUtorgExchangeService(config: Config): UtorgExchangeService { + val saltPayConfig = requireNotNull(config.saltPayConfig) + + val utorgAuthProvider = UtorgAuthProvider(saltPayConfig.kycProvider.sidValue) + val utorgEnvironment = if (BuildConfig.DEBUG) { + UtorgEnvironment.stage(utorgAuthProvider, LogConfig.network.utorgService) + // UtorgEnvironment.mock() + } else { + UtorgEnvironment.prod(utorgAuthProvider) + } + return UtorgExchangeService(utorgEnvironment) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 7bbfdc4f1d..24c1384e66 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -10,6 +10,7 @@ import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.common.services.Result +import com.tangem.core.analytics.Analytics import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.isTestCard @@ -18,6 +19,7 @@ import com.tangem.domain.common.extensions.withMainContext import com.tangem.operations.attestation.Attestation import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.safeUpdate +import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.datasource.config.ConfigManager import com.tangem.tap.domain.extensions.makePrimaryWalletManager @@ -100,6 +102,7 @@ class TapWalletManager { } suspend fun onWalletSelected(userWallet: UserWallet, refresh: Boolean) { + Analytics.setContext(userWallet.scanResponse) val scanResponse = userWallet.scanResponse val card = scanResponse.card val attestationFailed = card.attestation.status == Attestation.Status.Failed diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt index 07e92a313f..59647cbde5 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt @@ -1,19 +1,15 @@ package com.tangem.tap.domain.extensions -import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationParams import com.tangem.blockchain.common.DerivationStyle -import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.common.card.EllipticCurve -import com.tangem.common.extensions.guard import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toMapKey import com.tangem.common.hdWallet.DerivationPath import com.tangem.domain.common.CardDTO -import com.tangem.domain.common.SaltPayWorkaround import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation @@ -138,24 +134,4 @@ private fun selectWallet(wallets: List): CardDTO.Wallet? { 1 -> wallets[0] else -> wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: wallets[0] } -} - -fun WalletManagerFactory.makeSaltPayWalletManager( - scanResponse: ScanResponse, -): EthereumWalletManager { - val blockchain = scanResponse.cardTypesResolver.getBlockchain() - if (blockchain != Blockchain.SaltPay) { - error("WalletManager for the SaltPay can be created based only on Blockchain.SaltPay") - } - - val token = SaltPayWorkaround.tokenFrom(blockchain) - val cardWallet = scanResponse.card.wallets.firstOrNull().guard { - error("SaltPay card must have one wallet at least") - } - - return makeWalletManager( - blockchain = blockchain, - publicKey = Wallet.PublicKey(cardWallet.publicKey, null, null), - tokens = listOf(token), - ) as EthereumWalletManager } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt b/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt index 3429afa07c..b9e83390a0 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt +++ b/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt @@ -1,5 +1,6 @@ package com.tangem.tap.domain.model +import com.tangem.blockchain.common.TransactionData import com.tangem.tap.domain.model.WalletDataModel.Status import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.PendingTransaction @@ -23,6 +24,7 @@ data class WalletDataModel( val existentialDeposit: BigDecimal?, val fiatRate: BigDecimal?, val isCardSingleToken: Boolean, + val historyTransactions: List?, ) { /** diff --git a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt b/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt index a2c699667a..b74821a988 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt +++ b/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt @@ -4,6 +4,7 @@ import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ProductType +import com.tangem.domain.common.SaltPayWorkaround import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.TwinCardNumber @@ -77,6 +78,7 @@ class UserWalletBuilder( return when { cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL + SaltPayWorkaround.isSaltPayCardId(cardId) -> Artwork.SALT_PAY_URL else -> when (TwinsHelper.getTwinCardNumber(cardId)) { TwinCardNumber.First -> Artwork.TWIN_CARD_1 TwinCardNumber.Second -> Artwork.TWIN_CARD_2 diff --git a/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt b/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt index b48ae715a2..aef5c1ea76 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt +++ b/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt @@ -6,6 +6,8 @@ import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.WalletManager import com.tangem.common.hdWallet.DerivationPath import com.tangem.domain.common.util.UserWalletId +import com.tangem.tap.common.extensions.getBlockchainTxHistory +import com.tangem.tap.common.extensions.getTokenTxHistory import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.domain.model.WalletStoreModel import com.tangem.tap.domain.tokens.models.BlockchainNetwork @@ -98,6 +100,7 @@ private fun BlockchainNetwork.getBlockchainWalletData(walletManager: WalletManag existentialDeposit = getExistentialDeposit(walletManager), fiatRate = null, isCardSingleToken = false, + historyTransactions = walletManager?.getBlockchainTxHistory(), ) } @@ -115,6 +118,7 @@ private fun BlockchainNetwork.getTokensWalletsData(walletManager: WalletManager? existentialDeposit = getExistentialDeposit(walletManager), fiatRate = null, isCardSingleToken = walletManager?.cardTokens?.contains(token) ?: false, + historyTransactions = walletManager?.getTokenTxHistory(token), ) } } @@ -131,6 +135,7 @@ private fun Blockchain.toBlockchainWalletData(walletManager: WalletManager): Wal existentialDeposit = getExistentialDeposit(walletManager), fiatRate = null, isCardSingleToken = false, + historyTransactions = walletManager.getBlockchainTxHistory(), ) } @@ -147,6 +152,7 @@ private fun Token.toTokenWalletData(walletManager: WalletManager): WalletDataMod existentialDeposit = getExistentialDeposit(walletManager), fiatRate = null, isCardSingleToken = walletManager.cardTokens.contains(this), + historyTransactions = walletManager.getTokenTxHistory(this), ) } diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt index 36f4635c4a..f7c823369f 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt @@ -13,9 +13,11 @@ import com.tangem.domain.common.extensions.withMainContext import com.tangem.operations.backup.BackupService import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.backupService -import com.tangem.tap.common.analytics.paramsInterceptor.BatchIdParamsInterceptor +import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor +import com.tangem.tap.common.extensions.addContext import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa +import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction @@ -27,10 +29,10 @@ import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.features.onboarding.OnboardingSaltPayHelper import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep +import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManagerFactory import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction -import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store @@ -75,7 +77,7 @@ object ScanCardProcessor { tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse) onScanStateChange(false) - sendAnalytics(analyticsEvent, scanResponse.card.batchId) + sendAnalytics(analyticsEvent, scanResponse) checkForUnfinishedBackupForSaltPay( backupService = backupService, @@ -103,10 +105,17 @@ object ScanCardProcessor { private fun sendAnalytics( analyticsEvent: AnalyticsEvent?, - batchId: String, + scanResponse: ScanResponse, ) { - Analytics.addParamsInterceptor(BatchIdParamsInterceptor(batchId)) - analyticsEvent?.let { Analytics.send(it) } + analyticsEvent?.let { + // this workaround needed to send CardWasScannedEvent without adding a context + val interceptor = CardContextInterceptor(scanResponse) + val params = it.params.toMutableMap() + interceptor.intercept(params) + it.params = params.toMap() + + Analytics.send(it) + } } /** @@ -188,20 +197,25 @@ object ScanCardProcessor { store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(scanResponse)) - if (scanResponse.cardTypesResolver.isSaltPay()) { - if (scanResponse.cardTypesResolver.isSaltPayVisa()) { - val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse) + val cardTypesResolver = scanResponse.cardTypesResolver + if (cardTypesResolver.isSaltPay()) { + if (cardTypesResolver.isSaltPayVisa()) { + val manager = SaltPayActivationManagerFactory( + blockchain = scanResponse.cardTypesResolver.getBlockchain(), + card = scanResponse.card, + ).create() val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse, manager) delay(500) withMainContext { when (result) { is Result.Success -> { + Analytics.addContext(scanResponse) val isOnboardingCase = result.data if (isOnboardingCase) { onWalletNotCreated() store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false)) - store.dispatch(OnboardingSaltPayAction.SetDependencies(manager, config)) - store.dispatch(OnboardingSaltPayAction.Update) + store.dispatch(OnboardingSaltPayAction.SetDependencies(manager)) + store.dispatch(OnboardingSaltPayAction.Update(withAnalytics = false)) navigateTo(AppScreen.OnboardingWallet) { onProgressStateChange(it) } } else { delay(DELAY_SDK_DIALOG_CLOSE) @@ -222,16 +236,19 @@ object ScanCardProcessor { SaltPayExceptionHandler.handle(error) onFailure(TangemSdkError.ExceptionError(error)) } else { + Analytics.setContext(scanResponse) onSuccess(scanResponse) } } } else { if (OnboardingHelper.isOnboardingCase(scanResponse)) { + Analytics.addContext(scanResponse) onWalletNotCreated() store.dispatchOnMain(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = true)) val appScreen = OnboardingHelper.whereToNavigate(scanResponse) navigateTo(appScreen) { onProgressStateChange(it) } } else { + Analytics.setContext(scanResponse) if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) { onWalletNotCreated() store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse))) diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt index a7dc0a853d..c6519653d2 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt @@ -19,6 +19,7 @@ import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.extensions.replaceByOrAdd +import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.domain.model.UserWallet import com.tangem.tap.domain.model.WalletStoreModel import com.tangem.tap.domain.walletStores.WalletStoresError @@ -31,6 +32,7 @@ import com.tangem.tap.domain.walletStores.repository.implementation.utils.update import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithFiatRates import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithMissedDerivation import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithRent +import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithTxHistories import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithUnreachable import com.tangem.tap.domain.walletStores.storage.WalletManagerStorage import com.tangem.tap.domain.walletStores.storage.WalletStoresStorage @@ -204,7 +206,7 @@ internal class DefaultWalletAmountsRepository( updateWalletStoreWithUnreachable(walletStore) } else -> { - withInternetConnection { walletManager.update() } + withInternetConnection { walletManager.safeUpdate() } .map { updateWalletManagerWithAmounts(userWalletId, walletManager) } .flatMap { updateWalletStoreWithAmounts( @@ -319,7 +321,7 @@ internal class DefaultWalletAmountsRepository( it.updateWithDemoAmounts(wallet = updatedWallet) } else { it.updateWithAmounts(wallet = updatedWallet) - } + }.updateWithTxHistories(wallet = updatedWallet) }, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt index cb49fe33c1..48b0c4d71e 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt @@ -4,6 +4,8 @@ import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.Wallet import com.tangem.common.core.TangemError +import com.tangem.tap.common.extensions.getBlockchainTxHistory +import com.tangem.tap.common.extensions.getTokenTxHistory import com.tangem.tap.domain.extensions.amountToCreateAccount import com.tangem.tap.domain.getFirstToken import com.tangem.tap.domain.model.WalletDataModel @@ -29,6 +31,29 @@ internal fun List.updateWithFiatRates( } } +internal fun WalletDataModel.updateWithTxHistory(wallet: Wallet): WalletDataModel { + return this.copy( + historyTransactions = when (currency) { + is Currency.Blockchain -> wallet.getBlockchainTxHistory() + is Currency.Token -> wallet.getTokenTxHistory(currency.token) + }, + ) +} + +internal fun List.updateWithTxHistories( + wallet: Wallet, +): List { + return this.map { walletData -> + walletData.updateWithTxHistory(wallet) + } +} + +internal fun List.updateWithAmounts(wallet: Wallet): List { + return this.map { walletData -> + walletData.updateWithAmount(wallet) + } +} + internal fun WalletDataModel.updateWithAmount(wallet: Wallet): WalletDataModel { val pendingTransactions = wallet.getPendingTransactions() return this.copy( @@ -82,12 +107,6 @@ internal fun WalletDataModel.updateWithDemoAmount(wallet: Wallet): WalletDataMod ) } -internal fun List.updateWithAmounts(wallet: Wallet): List { - return this.map { walletData -> - walletData.updateWithAmount(wallet) - } -} - internal fun List.updateWithDemoAmounts(wallet: Wallet): List { return this.map { walletData -> walletData.updateWithDemoAmount(wallet) diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletStoreOperations.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletStoreOperations.kt index 47ef539e1f..d052401b0c 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletStoreOperations.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletStoreOperations.kt @@ -41,6 +41,14 @@ internal fun WalletStoreModel.updateWithError( ) } +internal fun WalletStoreModel.updateWithTxHistories( + wallet: Wallet, +): WalletStoreModel { + return this.copy( + walletsData = walletsData.updateWithTxHistories(wallet = wallet), + ) +} + internal fun WalletStoreModel.updateWithAmounts( wallet: Wallet, ): WalletStoreModel { diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt index d763e26339..8ac99d6590 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt @@ -48,6 +48,8 @@ object DemoHelper { fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId) + fun isTestDemoCard(scanResponse: ScanResponse): Boolean = config.isTestDemoCardId(scanResponse.card.cardId) + fun isDemoCardId(cardId: String): Boolean = config.isDemoCardId(cardId) fun tryHandle(appState: () -> AppState?, action: Action): Boolean { @@ -108,6 +110,8 @@ class DemoConfig { fun isDemoCardId(cardId: String): Boolean = demoCardIds.contains(cardId) + fun isTestDemoCardId(cardId: String): Boolean = testDemoCardIds.contains(cardId) + fun getBalance(blockchain: Blockchain): Amount = walletBalances[blockchain]?.copy() ?: Amount(BigDecimal.ZERO, blockchain).copy() @@ -118,9 +122,6 @@ class DemoConfig { @Suppress("ClassOrdering") private val releaseDemoCardIds = mutableListOf( // === Not from the Google Sheet table === - "FB10000000000196", // Note BTC - "FB20000000000186", // Note ETH - "FB30000000000176", // Wallet "AC01000000041225", "AC01000000041472", "AB01000000046498", diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index 6e64e29b98..6fe638daca 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -24,8 +24,6 @@ sealed class DetailsAction : Action { object Success : ResetToFactory() } - object CreateBackup : DetailsAction() - object ScanCard : DetailsAction() data class PrepareCardSettingsData(val card: CardDTO, val cardTypesResolver: CardTypesResolver) : DetailsAction() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 203af20870..f8626aa139 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -66,12 +66,6 @@ class DetailsMiddleware { store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet)) store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins)) } - is DetailsAction.CreateBackup -> { - state.scanResponse?.let { - store.dispatch(GlobalAction.Onboarding.Start(it, canSkipBackup = false)) - store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) - } - } DetailsAction.ScanCard -> { scope.launch { tangemSdkManager.scanProduct(userTokensRepository) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index 2a3c6aea7d..051a9148f2 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -8,7 +8,6 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.disclaimer.redux.DisclaimerAction import com.tangem.tap.features.home.LocaleRegionProvider @@ -86,7 +85,7 @@ class DetailsViewModel(private val store: Store) { } SettingsElement.LinkMoreCards -> { Analytics.send(Settings.ButtonCreateBackup()) - store.dispatch(DetailsAction.CreateBackup) + store.dispatch(WalletAction.MultiWallet.BackupWallet) } SettingsElement.TermsOfService -> { store.dispatch(DisclaimerAction.Show(AppScreen.Details)) diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index f01fdaeb7c..b32b6a6ab2 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -29,7 +29,7 @@ class HomeFragment : Fragment(), StoreSubscriber { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Analytics.send(IntroductionProcess.ScreenOpened()) + store.dispatch(HomeAction.OnCreate) store.dispatch(HomeAction.Init) } diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt index 59e169ee04..bb9be2edc3 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt @@ -1,16 +1,18 @@ package com.tangem.tap.features.home.redux import com.tangem.core.analytics.AnalyticsEvent -import com.tangem.tap.common.analytics.events.IntroductionProcess +import com.tangem.tap.common.analytics.events.AnalyticsParam +import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.entities.IndeterminateProgressButton import org.rekotlin.Action sealed class HomeAction : Action { + object OnCreate : HomeAction() object Init : HomeAction() data class ReadCard( - val analyticsEvent: AnalyticsEvent? = IntroductionProcess.CardWasScanned(), + val analyticsEvent: AnalyticsEvent? = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Introduction), ) : HomeAction() data class ScanInProgress(val scanInProgress: Boolean) : HomeAction() diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 902bb0ef47..72c24acdb4 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -5,10 +5,12 @@ import com.tangem.common.doOnResult import com.tangem.common.doOnSuccess import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.AnalyticsEvent +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.dispatchOnMain import com.tangem.tap.common.extensions.dispatchOpenUrl +import com.tangem.tap.common.extensions.eraseContext import com.tangem.tap.common.extensions.onCardScanned import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState @@ -49,6 +51,10 @@ private val homeMiddleware: Middleware = { _, _ -> private fun handleHomeAction(action: Action) { when (action) { + is HomeAction.OnCreate -> { + Analytics.eraseContext() + Analytics.send(IntroductionProcess.ScreenOpened()) + } is HomeAction.Init -> { store.dispatch(GlobalAction.RestoreAppCurrency) store.dispatch(GlobalAction.ExchangeManager.Init) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 5ed844549d..e0d51ccbf7 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -1,9 +1,12 @@ package com.tangem.tap.features.onboarding +import com.tangem.core.analytics.Analytics import com.tangem.domain.common.ProductType import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.onCardScanned +import com.tangem.tap.common.extensions.removeContext +import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.features.saveWallet.redux.SaveWalletAction @@ -55,6 +58,7 @@ object OnboardingHelper { accessCode: String? = null, backupCardsIds: List? = null, ) { + Analytics.setContext(scanResponse) when { // When should save user wallets, then save card without navigate to save wallet screen preferencesStorage.shouldSaveUserWallets -> scope.launch { @@ -92,4 +96,8 @@ object OnboardingHelper { store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) } + + fun onInterrupted() { + Analytics.removeContext() + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingSaltPayHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingSaltPayHelper.kt index b9aa35f096..5a6132c72a 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingSaltPayHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingSaltPayHelper.kt @@ -13,16 +13,20 @@ import com.tangem.tap.preferencesStorage [REDACTED_AUTHOR] */ object OnboardingSaltPayHelper { - suspend fun isOnboardingCase(scanResponse: ScanResponse, manager: SaltPayActivationManager): Result { + + suspend fun isOnboardingCase( + scanResponse: ScanResponse, + manager: SaltPayActivationManager, + ): Result { return try { - var updatedStep = manager.update(SaltPayActivationStep.None, null).successOr { return it } + val updatedStep = manager.update(SaltPayActivationStep.None, null).successOr { return it } val cardStorage = preferencesStorage.usedCardsPrefStorage val activationIsFinished = cardStorage.isActivationFinished(scanResponse.card.cardId) - if (updatedStep == SaltPayActivationStep.Success && activationIsFinished) { - updatedStep = SaltPayActivationStep.Finished + if (updatedStep == SaltPayActivationStep.Success && !activationIsFinished) { + cardStorage.activationFinished(scanResponse.card.cardId) } - val isActivationCase = updatedStep != SaltPayActivationStep.Finished + val isActivationCase = updatedStep != SaltPayActivationStep.Success val isBackupCase = scanResponse.card.backupStatus?.isActive == false Result.Success(isActivationCase || isBackupCase) } catch (ex: Exception) { @@ -31,4 +35,16 @@ object OnboardingSaltPayHelper { Result.Failure(error) } } + + @Suppress("UnusedPrivateMember") + fun testProceedToOnboarding( + scanResponse: ScanResponse, + manager: SaltPayActivationManager, + ): Result = Result.Success(true) + + @Suppress("UnusedPrivateMember") + fun testProceedToMainScreen( + scanResponse: ScanResponse, + manager: SaltPayActivationManager, + ): Result = Result.Success(false) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index 169e5dfb7f..28d62cf134 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt @@ -196,6 +196,7 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch store.dispatchDialogShow( OnboardingDialog.InterruptOnboarding( onOk = { + OnboardingHelper.onInterrupted() store.dispatch(NavigationAction.PopBackTo()) }, ), diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt index a0a6b8b76b..5080265552 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt @@ -118,4 +118,9 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment { -// do nothing - } + is CompletionResult.Failure -> Unit } } } @@ -130,9 +128,9 @@ private fun handleOtherCardsAction(action: Action) { store.dispatch(GlobalAction.Onboarding.Stop) OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(onboardingManager.scanResponse) } - is OnboardingOtherCardsAction.Confetti.Hide, - is OnboardingOtherCardsAction.SetArtworkUrl, - is OnboardingOtherCardsAction.Confetti.Show, - -> Unit + OnboardingOtherCardsAction.OnBackPressed -> { + OnboardingHelper.onInterrupted() + } + else -> Unit } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt index d6d15cec4c..ac58136a05 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt @@ -3,8 +3,8 @@ package com.tangem.tap.features.onboarding.products.twins.redux import com.tangem.Message import com.tangem.blockchain.common.WalletManager import com.tangem.common.extensions.VoidCallback -import com.tangem.domain.common.ScanResponse import com.tangem.datasource.utils.AssetReader +import com.tangem.domain.common.ScanResponse import com.tangem.tap.domain.TapError import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.features.onboarding.OnboardingWalletBalance diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt index df7d042f2b..de2c204c65 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -320,6 +320,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) { twinCardsState.currentStep != TwinCardsStep.TopUpWallet && twinCardsState.currentStep != TwinCardsStep.Done + OnboardingHelper.onInterrupted() store.dispatch(TwinCardsAction.CardsManager.Release) action.shouldResetTwinCardsWidget(shouldReturnCardBack) { store.dispatch(NavigationAction.PopBackTo(AppScreen.Home)) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt index 86fb3f8c57..dbc7e3672d 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt @@ -411,7 +411,11 @@ class TwinsCardsFragment : BaseOnboardingFragment() { TwinCardsAction.OnBackPressed { should, popAction -> store.dispatch(TwinCardsAction.Confetti.Hide) showConfetti(false) - if (should) switchToCard(TwinCardNumber.First, true, popAction) else popAction() + if (should) { + switchToCard(TwinCardNumber.First, true, popAction) + } else { + popAction() + } }, ) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt index eabcd70384..df5882a35c 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt @@ -31,9 +31,8 @@ sealed class BackupAction : Action { object ScanPrimaryCard : BackupAction() /** - * Check for unfinished backup of standard Wallet cards. - * For SaltPay cards unfinished backup resumed after scanning the card on HomeScreen through Onboarding.Start. - * See more Onboarding.Start, CheckForUnfinishedBackup, StartForUnfinishedBackup + * Check for unfinished backup of standard Wallets and SaltPay cards + * See more GlobalAction.Onboarding.StartForUnfinishedBackup */ object CheckForUnfinishedBackup : BackupAction() @@ -65,7 +64,11 @@ sealed class BackupAction : Action { data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction() data class WriteBackupCard(val cardNumber: Int) : BackupAction() - object FinishBackup : BackupAction() + /** + * It always calls for the SaltPay cards when resuming the activation process. + */ + data class FinishBackup(val withAnalytics: Boolean = true) : BackupAction() + object DiscardBackup : BackupAction() object DiscardSavedBackup : BackupAction() object ResumeFoundUnfinishedBackup : BackupAction() diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index e240879094..1ea9f428aa 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -24,8 +24,8 @@ import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.onboarding.OnboardingDialog import com.tangem.tap.features.onboarding.OnboardingHelper +import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManagerFactory import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction -import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState import com.tangem.tap.features.wallet.redux.Artwork import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.preferencesStorage @@ -72,7 +72,7 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: } when { card == null -> { - // it's possible when found unfinished backup for standard Wallet cards + // it's possible when found unfinished backup store.dispatch(OnboardingWalletAction.ResumeBackup) } card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup -> { @@ -87,10 +87,11 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: scanResponse.cardTypesResolver.isSaltPay() -> { store.dispatch(OnboardingWalletAction.GetToSaltPayStep) - store.dispatch(BackupAction.FinishBackup) + store.dispatch(BackupAction.FinishBackup(withAnalytics = false)) + store.dispatch(OnboardingSaltPayAction.OnSwitchedToSaltPayProcess) } - else -> store.dispatch(BackupAction.FinishBackup) + else -> store.dispatch(BackupAction.FinishBackup()) } } else -> { @@ -317,7 +318,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) is CompletionResult.Success -> { if (backupService.currentState == BackupService.State.Finished) { initSaltPayOnBackupFinishedIfNeeded(scanResponse, onboardingWalletState) - store.dispatchOnMain(BackupAction.FinishBackup) + store.dispatchOnMain(BackupAction.FinishBackup()) } else { store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(action.cardNumber + 1)) } @@ -328,7 +329,9 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) } } is BackupAction.FinishBackup -> { - Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber)) + if (action.withAnalytics) { + Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber)) + } if (!onboardingWalletState.isSaltPay) { Analytics.send(Onboarding.Finished()) finishCardActivation(backupState, card) @@ -345,18 +348,23 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) backupService.discardSavedBackup() } is BackupAction.CheckForUnfinishedBackup -> { - if (backupService.hasIncompletedBackup && !backupService.primaryCardIsSaltPayVisa()) { + if (backupService.hasIncompletedBackup) { store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound)) } } is BackupAction.ResumeFoundUnfinishedBackup -> { - store.dispatch(GlobalAction.Onboarding.StartForUnfinishedBackup(backupService.addedBackupCardsCount)) + store.dispatch( + GlobalAction.Onboarding.StartForUnfinishedBackup( + addedBackupCardsCount = backupService.addedBackupCardsCount, + isSaltPayVisa = backupService.primaryCardIsSaltPayVisa(), + ), + ) store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) } is BackupAction.DismissBackup -> { Analytics.send(Onboarding.Backup.Skipped()) if (onboardingWalletState.isSaltPay) throw UnsupportedOperationException() - store.dispatch(BackupAction.FinishBackup) + store.dispatch(BackupAction.FinishBackup()) } else -> {} } @@ -395,14 +403,18 @@ private fun finishCardsActivationForDiscardedUnfinishedBackup(cardId: String) { */ private fun initSaltPayOnBackupFinishedIfNeeded( scanResponse: ScanResponse?, - onboardingWalletState: OnboardingWalletState, + state: OnboardingWalletState, ) { - if (onboardingWalletState.isSaltPay && onboardingWalletState.onboardingSaltPayState == null) { - if (scanResponse == null) error("scanning response is null") + if (state.backupState.isInterruptedBackup) return + if (scanResponse == null) return - val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse) - store.dispatchOnMain(OnboardingSaltPayAction.SetDependencies(manager, config)) - store.dispatchOnMain(OnboardingSaltPayAction.Update) + if (state.isSaltPay && state.onboardingSaltPayState == null) { + val manager = SaltPayActivationManagerFactory( + blockchain = scanResponse.cardTypesResolver.getBlockchain(), + card = scanResponse.card, + ).create() + store.dispatchOnMain(OnboardingSaltPayAction.SetDependencies(manager)) + store.dispatchOnMain(OnboardingSaltPayAction.Update(false)) } } @@ -420,6 +432,7 @@ private fun handleOnBackPressed(state: OnboardingWalletState) { if (state.isSaltPay) { showInterruptOnboardingDialog() } else { + OnboardingHelper.onInterrupted() store.dispatch(NavigationAction.PopBackTo()) } } @@ -430,6 +443,7 @@ private fun showInterruptOnboardingDialog() { store.dispatchDialogShow( OnboardingDialog.InterruptOnboarding( onOk = { + OnboardingHelper.onInterrupted() store.dispatch(BackupAction.DiscardBackup) store.dispatch(NavigationAction.PopBackTo()) }, diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt index 68099fff3f..5359efde9b 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt @@ -38,13 +38,13 @@ private object ReducerForGlobalAction { fun reduce(action: GlobalAction.Onboarding, state: OnboardingWalletState): OnboardingWalletState { return when (action) { is GlobalAction.Onboarding.Start -> { + val isSaltPay = action.scanResponse.cardTypesResolver.isSaltPay() OnboardingWalletState( backupState = state.backupState.copy( - maxBackupCards = if (action.scanResponse.cardTypesResolver.isSaltPay()) 1 else 2, - canSkipBackup = - if (action.scanResponse.cardTypesResolver.isSaltPay()) false else action.canSkipBackup, + maxBackupCards = maxBackupCards(isSaltPay), + canSkipBackup = if (isSaltPay) false else action.canSkipBackup, ), - isSaltPay = action.scanResponse.cardTypesResolver.isSaltPay(), + isSaltPay = isSaltPay, ) } is GlobalAction.Onboarding.StartForUnfinishedBackup -> { @@ -52,13 +52,16 @@ private object ReducerForGlobalAction { backupState = state.backupState.copy( maxBackupCards = action.addedBackupCardsCount, canSkipBackup = false, + isInterruptedBackup = true, ), - isSaltPay = false, + isSaltPay = action.isSaltPayVisa, ) } else -> state } } + + private fun maxBackupCards(isSaltPay: Boolean): Int = if (isSaltPay) 1 else 2 } private object BackupReducer { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt index 64af52ecac..d2ef761e70 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt @@ -62,7 +62,7 @@ data class OnboardingWalletState( -> 9 SaltPayActivationStep.Claim -> 10 SaltPayActivationStep.ClaimInProgress -> 11 - SaltPayActivationStep.ClaimSuccess, SaltPayActivationStep.Success, SaltPayActivationStep.Finished, + SaltPayActivationStep.ClaimSuccess, SaltPayActivationStep.Success, -> getMaxProgress() } } @@ -87,6 +87,7 @@ data class BackupState( val backupStep: BackupStep = BackupStep.InitBackup, val maxBackupCards: Int = 2, val canSkipBackup: Boolean = true, + val isInterruptedBackup: Boolean = false, ) enum class AccessCodeError { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt index 8882281ca9..b5dfe53817 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/GnosisRegistrator.kt @@ -23,6 +23,8 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import java.math.BigDecimal import java.math.BigInteger +import java.math.MathContext +import java.math.RoundingMode import java.util.concurrent.atomic.AtomicLong /** @@ -33,11 +35,12 @@ class GnosisRegistrator( ) { private val walletAddress = walletManager.wallet.address + private val blockchain = walletManager.wallet.blockchain private val token = walletManager.wallet.getFirstToken().guard { throw NullPointerException("WalletManager for GnosisRegistrator must contain token") } - private val otpProcessorContractAddress: String = when (walletManager.wallet.blockchain) { + private val otpProcessorContractAddress: String = when (blockchain) { Blockchain.SaltPay -> "0xc659f4FEd7A84a188F54cBA4A7a49D77c1a20522" else -> throw IllegalArgumentException("GnosisRegistrator supports only the SaltPay blockchain") } @@ -47,6 +50,7 @@ class GnosisRegistrator( private val atomicNonce = AtomicLong() suspend fun checkHasGas(): Result { + // return Result.Success(true) // test val wallet = walletManager.safeUpdate().successOr { return Result.Failure(BlockchainSdkError.WrappedThrowable(it.error)) } @@ -145,6 +149,7 @@ class GnosisRegistrator( } suspend fun getAllowance(): Result { + // return Result.Success(Amount(BigDecimal(0.1), walletManager.wallet.blockchain)) // test return walletManager.getAllowance(addressTreasureSafe, token) } @@ -154,6 +159,13 @@ class GnosisRegistrator( return Result.Failure(BlockchainSdkError.WrappedThrowable(it.error)) } } + + // return transferFromStandardWay(amountToClaim, signer) + return transferFromHardcodeWay(amountToClaim, signer) + } + + @Suppress("UnusedPrivateMember") + private suspend fun transferFromStandardWay(amountToClaim: BigDecimal, signer: TransactionSigner): Result { val amount = Amount(token, amountToClaim) val feeAmount = walletManager.getFeeToTransferFrom(amount, addressTreasureSafe) .extractFeeAmount().successOr { return it } @@ -164,6 +176,34 @@ class GnosisRegistrator( return Result.Success(Unit) } + private suspend fun transferFromHardcodeWay(amountToClaim: BigDecimal, signer: TransactionSigner): Result { + val amount = Amount(token, amountToClaim) + val gasPrice = walletManager.getGasPrice().successOr { return it } + + val hardcodeGasLimit = BigInteger("300000") + val hardcodeFeeValue = (hardcodeGasLimit * gasPrice).toBigDecimal( + scale = blockchain.decimals(), + mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN), + ).movePointLeft(blockchain.decimals()) + val hardcodeFeeAmount = Amount(hardcodeFeeValue, blockchain) + + val transactionData = walletManager.createTransferFromTransaction( + amount = amount, + fee = hardcodeFeeAmount, + source = addressTreasureSafe, + ) + val transactionToSign = walletManager.transactionBuilder.buildTransferFromToSign( + transactionData, + walletManager.txCount.toBigInteger(), + hardcodeGasLimit, + ) ?: return Result.Failure(BlockchainSdkError.CustomError("Not enough data")) + + return when (val result = walletManager.signAndSend(transactionToSign, signer)) { + SimpleResult.Success -> Result.Success(Unit) + is SimpleResult.Failure -> Result.Failure(result.error) + } + } + @Suppress("MagicNumber") private fun Result>.extractFeeAmount(): Result { return when (this) { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayActivationManager.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayActivationManager.kt index e99c85ae58..dcb4044499 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayActivationManager.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayActivationManager.kt @@ -1,14 +1,22 @@ +@file:Suppress("MaximumLineLength") + package com.tangem.tap.features.onboarding.products.wallet.saltPay import android.net.Uri +import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.blockchains.ethereum.SignedEthereumTransaction import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.TransactionSigner +import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.extensions.successOr import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard +import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.isZero import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result @@ -23,15 +31,28 @@ import com.tangem.datasource.api.paymentology.models.response.RegistrationRespon import com.tangem.datasource.api.paymentology.models.response.tryExtractError import com.tangem.datasource.config.models.KYCProvider import com.tangem.datasource.config.models.SaltPayConfig +import com.tangem.domain.common.CardDTO +import com.tangem.domain.common.SaltPayWorkaround import com.tangem.domain.common.extensions.successOr import com.tangem.operations.attestation.AttestWalletKeyResponse import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.domain.getFirstToken import com.tangem.tap.domain.model.builders.UserWalletIdBuilder import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError +import com.tangem.tap.store import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.math.BigDecimal +import kotlin.ByteArray +import kotlin.Exception +import kotlin.IllegalStateException +import kotlin.NullPointerException +import kotlin.String +import kotlin.Suppress +import kotlin.Unit +import kotlin.UnsupportedOperationException +import kotlin.byteArrayOf +import kotlin.minus import com.tangem.blockchain.extensions.Result as BlockchainResult /** @@ -227,4 +248,97 @@ class KYCUrlProvider( .appendQueryParameter(kycProvider.externalIdParameterKey, kycRefId) .build().toString() } +} + +class SaltPayActivationManagerFactory( + private val blockchain: Blockchain, + private val card: CardDTO, +) { + + fun create(): SaltPayActivationManager { + val wallet = card.wallets.first() + val saltPayConfig = store.state.globalState.configManager?.config?.saltPayConfig.guard { + throw NullPointerException("SaltPayConfig is not initialized") + } + val wmFactory = store.state.globalState.tapWalletManager.walletManagerFactory + val paymentologyService = store.state.domainNetworks.paymentologyService + + // return createDummyActivationManager(saltPayConfig, wmFactory, paymentologyService) + return createActivationPayManager( + cardId = card.cardId, + cardPublicKey = card.cardPublicKey, + kycProvider = saltPayConfig.kycProvider, + gnosisRegistrator = GnosisRegistrator( + walletManager = makeSaltPayWalletManager( + blockchain = blockchain, + walletPublicKey = wallet.publicKey, + wmFactory = wmFactory, + ), + ), + paymentologyService = paymentologyService, + ) + } + + private fun makeSaltPayWalletManager( + blockchain: Blockchain, + walletPublicKey: ByteArray, + wmFactory: WalletManagerFactory, + ): EthereumWalletManager { + return wmFactory.makeWalletManager( + blockchain = blockchain, + publicKey = Wallet.PublicKey(walletPublicKey, null, null), + tokens = listOf(SaltPayWorkaround.tokenFrom(blockchain)), + ) as EthereumWalletManager + } + + private fun createActivationPayManager( + cardId: String, + cardPublicKey: ByteArray, + kycProvider: KYCProvider, + gnosisRegistrator: GnosisRegistrator, + paymentologyService: PaymentologyApiService, + ): SaltPayActivationManager { + return SaltPayActivationManager( + cardId = cardId, + cardPublicKey = cardPublicKey, + kycProvider = kycProvider, + paymentologyService = paymentologyService, + gnosisRegistrator = gnosisRegistrator, + ) + } + + @Suppress("UnusedPrivateMember") + private fun createDummyActivationManager( + saltPayConfig: SaltPayConfig, + wmFactory: WalletManagerFactory, + paymentologyService: PaymentologyApiService, + ): SaltPayActivationManager { + val fakeSource = DummySaltPayCardSource() + + val walletManager = wmFactory.makeWalletManager( + blockchain = fakeSource.blockchain, + publicKey = fakeSource.blockchainPublicKey, + tokens = listOf(fakeSource.token), + ) as EthereumWalletManager + + return createActivationPayManager( + cardId = fakeSource.cardId, + cardPublicKey = fakeSource.cardPublicKey, + kycProvider = saltPayConfig.kycProvider, + gnosisRegistrator = GnosisRegistrator(walletManager), + paymentologyService = paymentologyService, + ) + } + + private data class DummySaltPayCardSource( + val blockchain: Blockchain = Blockchain.SaltPay, + val cardId: String = "FF03000001001057", + private val cardPublicKeyString: String = "039C2D2F9B68003766BFC29766761F55F4084E48B8B012BF439E115A79AB122D48", + private val walletPublicKeyString: String = "0399B2DF5B129FBF69097DAA7C44FB5849E578CCFC22228A409870C74B56C4C3D1", + ) { + val cardPublicKey: ByteArray = cardPublicKeyString.hexToBytes() + val walletPublicKey: ByteArray = walletPublicKeyString.hexToBytes() + val blockchainPublicKey = Wallet.PublicKey(walletPublicKey, null, null) + val token: Token = SaltPayWorkaround.tokenFrom(blockchain) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayExceptionHandler.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayExceptionHandler.kt index 6236bf847e..8d116a7385 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayExceptionHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayExceptionHandler.kt @@ -2,6 +2,8 @@ package com.tangem.tap.features.onboarding.products.wallet.saltPay import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.common.core.TangemSdkError +import com.tangem.core.analytics.Analytics +import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog @@ -12,12 +14,20 @@ import com.tangem.tap.store [REDACTED_AUTHOR] */ object SaltPayExceptionHandler { + fun handle(throwable: Throwable) { when (throwable) { is SaltPayActivationError -> { val dialog = when (throwable) { - is SaltPayActivationError.NoGas -> SaltPayDialog.Activation.NoGas + is SaltPayActivationError.NoGas -> { + Analytics.send(Onboarding.NotEnoughGasError()) + SaltPayDialog.Activation.NoGas + } is SaltPayActivationError.PutVisaCard -> SaltPayDialog.Activation.PutVisaCard + is SaltPayActivationError.CardNotPassed -> { + Analytics.send(Onboarding.CardNotPassedError()) + SaltPayDialog.Activation.OnError(throwable) + } else -> SaltPayDialog.Activation.OnError(throwable) } store.dispatchDialogShow(dialog) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/NoFundsForActivationDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/NoFundsForActivationDialog.kt index 8280d87aab..5121db94db 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/NoFundsForActivationDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/dialog/NoFundsForActivationDialog.kt @@ -14,16 +14,20 @@ import com.tangem.wallet.R [REDACTED_AUTHOR] */ object NoFundsForActivationDialog { + fun create(context: Context): Dialog { return AlertDialog.Builder(context).apply { setTitle(R.string.saltpay_error_no_gas_title) setMessage(R.string.saltpay_error_no_gas_message) setPositiveButton(R.string.chat_button_title) { _, _ -> - val config = store.state.globalState.configManager?.config?.saltPayConfig?.zendesk.guard { - store.dispatchDebugErrorNotification("SaltPayConfig not initialized") - return@setPositiveButton - } - store.dispatch(GlobalAction.OpenChat(SupportInfo(), config)) + val sprinklrConfig = store.state.globalState.configManager?.config + ?.saltPayConfig + ?.sprinklr + .guard { + store.dispatchDebugErrorNotification("SaltPayConfig not initialized") + return@setPositiveButton + } + store.dispatch(GlobalAction.OpenChat(SupportInfo(), sprinklrConfig)) } setNegativeButton(R.string.common_cancel) { _, _ -> } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayAction.kt index ea8b58f85c..91b791925d 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayAction.kt @@ -2,7 +2,6 @@ package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux import com.tangem.blockchain.common.Amount import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager -import com.tangem.datasource.config.models.SaltPayConfig import org.rekotlin.Action import java.math.BigDecimal @@ -12,9 +11,13 @@ import java.math.BigDecimal sealed class OnboardingSaltPayAction : Action { object Init : OnboardingSaltPayAction() + /** + * Notification about the switching from the Wallet view to the SaltPay view. + */ + object OnSwitchedToSaltPayProcess : OnboardingSaltPayAction() + data class SetDependencies( val registrationManager: SaltPayActivationManager, - val saltPayConfig: SaltPayConfig, ) : OnboardingSaltPayAction() data class SetInProgress(val isInProgress: Boolean) : OnboardingSaltPayAction() @@ -22,7 +25,7 @@ sealed class OnboardingSaltPayAction : Action { data class SetAccessCode(val accessCode: String?) : OnboardingSaltPayAction() - object Update : OnboardingSaltPayAction() + data class Update(val withAnalytics: Boolean = true) : OnboardingSaltPayAction() object RegisterCard : OnboardingSaltPayAction() object OpenUtorgKYC : OnboardingSaltPayAction() object UtorgKYCRedirectSuccess : OnboardingSaltPayAction() @@ -33,7 +36,11 @@ sealed class OnboardingSaltPayAction : Action { data class TrySetPin(val pin: String) : OnboardingSaltPayAction() data class SetPin(val pin: String) : OnboardingSaltPayAction() - data class SetStep(val newStep: SaltPayActivationStep) : OnboardingSaltPayAction() + data class SetStep( + val newStep: SaltPayActivationStep, + val withAnalytics: Boolean = true, + ) : OnboardingSaltPayAction() + data class SetAmountToClaim(val amount: Amount?) : OnboardingSaltPayAction() data class SetTokenBalance(val balanceValue: BigDecimal) : OnboardingSaltPayAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt index 90ebec0825..57b1f17f3c 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt @@ -78,6 +78,9 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App onboardingManager.activationStarted(card.cardId) } } + is OnboardingSaltPayAction.OnSwitchedToSaltPayProcess -> { + sendAnalyticsScreenOpened(getState().step, newStep = SaltPayActivationStep.None) + } is OnboardingSaltPayAction.Update -> { handleInProgress = true @@ -89,7 +92,18 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App } handleInProgress = false - dispatchOnMain(OnboardingSaltPayAction.SetStep(updateStep)) + dispatchOnMain(OnboardingSaltPayAction.SetStep(updateStep, action.withAnalytics)) + + if (updateStep == SaltPayActivationStep.Claim) { + handleClaimRefreshInProgress = true + val tokenAmountValue = state.saltPayManager.getTokenAmount().successOr { + SaltPayExceptionHandler.handle(it.error) + handleClaimRefreshInProgress = false + return@launch + } + dispatchOnMain(OnboardingSaltPayAction.SetTokenBalance(tokenAmountValue)) + handleClaimRefreshInProgress = false + } } } is OnboardingSaltPayAction.RegisterCard -> { @@ -136,8 +150,16 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App return@launch } + Analytics.send(Onboarding.PinCodeSet()) + + var nextStep = SaltPayActivationStep.KycIntro + nextStep = saltPayManager.update(nextStep, state.amountToClaim).successOr { + onException(it.error) + return@launch + } + handleInProgress = false - dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.KycIntro)) + dispatchOnMain(OnboardingSaltPayAction.SetStep(nextStep)) } } is OnboardingSaltPayAction.OpenUtorgKYC -> { @@ -159,13 +181,13 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App } handleInProgress = false - dispatchOnMain(OnboardingSaltPayAction.Update) + dispatchOnMain(OnboardingSaltPayAction.Update()) } } is OnboardingSaltPayAction.TrySetPin -> { try { assertPinValid(action.pin, getState().pinLength) - Analytics.send(Onboarding.PinCodeSet()) + Analytics.send(Onboarding.ButtonSetPinCode()) store.dispatch(OnboardingSaltPayAction.SetPin(action.pin)) store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.CardRegistration)) } catch (error: SaltPayActivationError) { @@ -219,54 +241,48 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App val state = getState() scope.launch { - when (val amountToClaimResult = state.saltPayManager.getAmountToClaim()) { - is Result.Success -> { - // need to re claim - handleClaimRefreshInProgress = false - dispatchOnMain(OnboardingSaltPayAction.SetAmountToClaim(amountToClaimResult.data)) - dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.Claim)) - } - is Result.Failure -> { - when (amountToClaimResult.error) { - is SaltPayActivationError.NoFundsToClaim -> { - val tokenAmountValue = state.saltPayManager.getTokenAmount().successOr { - SaltPayExceptionHandler.handle(it.error) - handleClaimRefreshInProgress = false - return@launch - } + val tokenAmountValue = state.saltPayManager.getTokenAmount().successOr { + SaltPayExceptionHandler.handle(it.error) + handleClaimRefreshInProgress = false + return@launch + } + dispatchOnMain(OnboardingSaltPayAction.SetTokenBalance(tokenAmountValue)) - handleClaimRefreshInProgress = false - if (tokenAmountValue.isPositive()) { - dispatchOnMain(OnboardingSaltPayAction.SetTokenBalance(tokenAmountValue)) - dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimSuccess)) - } - } - else -> { - handleClaimRefreshInProgress = false - SaltPayExceptionHandler.handle(amountToClaimResult.error) - } - } + val amountToClaim = state.saltPayManager.getAmountToClaim().successOr { + if (it.error !is SaltPayActivationError.NoFundsToClaim) { + handleClaimRefreshInProgress = false + SaltPayExceptionHandler.handle(it.error) + return@launch + } else { + null } } + handleClaimRefreshInProgress = false + + if (tokenAmountValue.isPositive() && amountToClaim == null) { + dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimSuccess)) + } else { + // dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimSuccess)) + } } } is OnboardingSaltPayAction.SetStep -> { - when (action.newStep) { - SaltPayActivationStep.KycStart -> Analytics.send(Onboarding.KYCStarted()) - SaltPayActivationStep.KycWaiting -> Analytics.send(Onboarding.KYCInProgress()) - SaltPayActivationStep.KycReject -> Analytics.send(Onboarding.KYCRejected()) - SaltPayActivationStep.Claim -> Analytics.send(Onboarding.ClaimScreenOpened()) - SaltPayActivationStep.ClaimSuccess, SaltPayActivationStep.Success -> { - Analytics.send(Onboarding.Finished()) - val onboardingManager = getOnboardingManager().guard { - // Null is possible if it is started from a standard pending backup and in this case - // it is impossible to get here - store.dispatchDebugErrorNotification("OnboardingManager can't be NULL") - return - } - finishCardActivation(getOnboardingWalletState().backupState, onboardingManager.scanResponse.card) + if (action.withAnalytics) { + sendAnalyticsScreenOpened(getState().step, action.newStep) + } + if (action.newStep == SaltPayActivationStep.ClaimSuccess || + action.newStep == SaltPayActivationStep.Success + ) { + val onboardingManager = getOnboardingManager().guard { + // Null is possible if it is started from a standard pending backup and in this case + // it is impossible to get here + store.dispatchDebugErrorNotification("OnboardingManager can't be NULL") + return } - else -> {} + finishCardActivation( + getOnboardingWalletState().backupState, + onboardingManager.scanResponse.card, + ) } } else -> { @@ -275,6 +291,22 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App } } +private fun sendAnalyticsScreenOpened(currentStep: SaltPayActivationStep, newStep: SaltPayActivationStep) { + if (currentStep == newStep) return + + when (newStep) { + SaltPayActivationStep.NeedPin -> Analytics.send(Onboarding.PinScreenOpened()) + SaltPayActivationStep.CardRegistration -> Analytics.send(Onboarding.CardConnectionScreenOpened()) + SaltPayActivationStep.KycIntro -> Analytics.send(Onboarding.KYCScreenOpened()) + SaltPayActivationStep.KycStart -> Analytics.send(Onboarding.KYCStarted()) + SaltPayActivationStep.KycWaiting -> Analytics.send(Onboarding.KYCInProgress()) + SaltPayActivationStep.KycReject -> Analytics.send(Onboarding.KYCRejected()) + SaltPayActivationStep.Claim -> Analytics.send(Onboarding.ClaimScreenOpened()) + SaltPayActivationStep.ClaimSuccess, SaltPayActivationStep.Success -> Analytics.send(Onboarding.Finished()) + else -> {} + } +} + suspend fun SaltPayActivationManager.update( currentStep: SaltPayActivationStep, currentAmountToClaim: Amount?, @@ -362,6 +394,7 @@ private suspend fun getAmountToClaimIfNeeded( return amountToClaim } + // return Amount(BigDecimal(0.1), Blockchain.SaltPay) return when (val result = saltPayManager.getAmountToClaim()) { is Result.Success -> { Timber.d("getAmountToClaimIfNeeded: success: %s", result.data) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayReducer.kt index 9d4e8774c0..ad3c493336 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayReducer.kt @@ -20,7 +20,6 @@ private fun internalReduce(anyAction: Action, onboardingWalletState: OnboardingW return onboardingWalletState.copy( onboardingSaltPayState = OnboardingSaltPayState( saltPayManager = action.registrationManager, - saltPayConfig = action.saltPayConfig, ), ) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayState.kt index 344834807f..7b68518634 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayState.kt @@ -2,19 +2,10 @@ package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.WalletManagerFactory -import com.tangem.common.extensions.guard -import com.tangem.datasource.api.paymentology.PaymentologyApiService import com.tangem.domain.common.SaltPayWorkaround -import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.toggleWidget.WidgetState -import com.tangem.tap.domain.extensions.makeSaltPayWalletManager -import com.tangem.tap.features.onboarding.products.wallet.saltPay.GnosisRegistrator -import com.tangem.datasource.config.models.KYCProvider import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager -import com.tangem.datasource.config.models.SaltPayConfig import com.tangem.tap.features.wallet.redux.ProgressState -import com.tangem.tap.store import java.math.BigDecimal /** @@ -23,7 +14,6 @@ import java.math.BigDecimal data class OnboardingSaltPayState( @Transient val saltPayManager: SaltPayActivationManager = SaltPayActivationManager.stub(), - val saltPayConfig: SaltPayConfig = SaltPayConfig.stub(), val pinCode: String? = null, val accessCode: String? = null, val amountToClaim: Amount? = null, @@ -38,54 +28,6 @@ data class OnboardingSaltPayState( get() = if (inProgress) ProgressState.Loading else ProgressState.Done val pinLength: Int = 4 - - companion object { - fun initDependency(scanResponse: ScanResponse): Pair { - val globalState = store.state.globalState - val saltPayConfig = globalState.configManager?.config?.saltPayConfig.guard { - throw NullPointerException("SaltPayConfig is not initialized") - } - val gnosisRegistrator = makeGnosisRegistrator( - scanResponse = scanResponse, - wmFactory = globalState.tapWalletManager.walletManagerFactory, - ) - val registrationManager = makeSaltPayRegistrationManager( - scanResponse = scanResponse, - gnosisRegistrator = gnosisRegistrator, - paymentologyService = store.state.domainNetworks.paymentologyService, - kycProvider = saltPayConfig.kycProvider, - ) - return registrationManager to saltPayConfig - } - - fun makeSaltPayRegistrationManager( - scanResponse: ScanResponse, - gnosisRegistrator: GnosisRegistrator, - paymentologyService: PaymentologyApiService, - kycProvider: KYCProvider, - ): SaltPayActivationManager { - if (!scanResponse.cardTypesResolver.isSaltPay()) { - throw IllegalArgumentException("Can't initialize the OnboardingSaltPayMiddleware if card is not SalPay") - } - - return SaltPayActivationManager( - cardId = scanResponse.card.cardId, - cardPublicKey = scanResponse.card.cardPublicKey, - kycProvider = kycProvider, - paymentologyService = paymentologyService, - gnosisRegistrator = gnosisRegistrator, - ) - } - - fun makeGnosisRegistrator( - scanResponse: ScanResponse, - wmFactory: WalletManagerFactory, - ): GnosisRegistrator { - return GnosisRegistrator( - walletManager = wmFactory.makeSaltPayWalletManager(scanResponse), - ) - } - } } enum class SaltPayActivationStep { @@ -101,5 +43,4 @@ enum class SaltPayActivationStep { ClaimInProgress, ClaimSuccess, Success, - Finished; } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/ui/OnboardingSaltPayView.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/ui/OnboardingSaltPayView.kt index d4549041e1..394e03adb4 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/ui/OnboardingSaltPayView.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/ui/OnboardingSaltPayView.kt @@ -80,9 +80,10 @@ internal class OnboardingSaltPayView( when (state.step) { OnboardingWalletStep.SaltPay -> setSaltPayStep(state.onboardingSaltPayState) OnboardingWalletStep.Backup -> { - if (state.backupState.backupStep == BackupStep.Finished) { + if (state.backupState.backupStep == BackupStep.Finished && !state.backupState.isInterruptedBackup) { // if backup finished -> switch OnboardingWalletStep to the SaltPay store.dispatch(OnboardingWalletAction.GetToSaltPayStep) + store.dispatch(OnboardingSaltPayAction.OnSwitchedToSaltPayProcess) } else { // if not -> back to the standard backup process walletFragment.handleOnboardingStep(state) @@ -103,7 +104,7 @@ internal class OnboardingSaltPayView( val uri = Uri.parse(state.onboardingSaltPayState.saltPayCardArtworkUrl) walletFragment.loadImageIntoImageView(uri, imvFrontCard) } - imvFirstBackupCard.load(R.drawable.img_salt_pay_visa) + imvFirstBackupCard.load(R.drawable.card_placeholder_wallet) // if (state.onboardingSaltPayState?.saltPayCardArtworkUrl == null) { // //if saltPay url not loaded -> load from resource @@ -233,7 +234,7 @@ internal class OnboardingSaltPayView( btnKycAction.text = getText(R.string.onboarding_button_kyc_waiting) btnKycAction.setOnClickListener { - store.dispatch(OnboardingSaltPayAction.Update) + store.dispatch(OnboardingSaltPayAction.Update()) } progressButton = SaltPayProgressButton(root) } @@ -249,7 +250,7 @@ internal class OnboardingSaltPayView( btnOpenSupportChat.hide() btnKycAction.text = getText(R.string.onboarding_button_kyc_start) btnKycAction.setOnClickListener { - store.dispatch(OnboardingSaltPayAction.Update) + store.dispatch(OnboardingSaltPayAction.Update()) } progressButton = SaltPayProgressButton(root) } diff --git a/app/src/main/java/com/tangem/tap/features/sprinklr/redux/SprinklrAction.kt b/app/src/main/java/com/tangem/tap/features/sprinklr/redux/SprinklrAction.kt new file mode 100644 index 0000000000..b781ee102f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/sprinklr/redux/SprinklrAction.kt @@ -0,0 +1,10 @@ +package com.tangem.tap.features.sprinklr.redux + +import com.tangem.datasource.config.models.SprinklrConfig +import org.rekotlin.Action + +sealed interface SprinklrAction : Action { + data class Init(val userId: String, val config: SprinklrConfig) : SprinklrAction + data class UpdateUrl(val url: String) : SprinklrAction + data class UpdateSprinklrDomains(val domains: List) : SprinklrAction +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/sprinklr/redux/SprinklrMiddleware.kt b/app/src/main/java/com/tangem/tap/features/sprinklr/redux/SprinklrMiddleware.kt new file mode 100644 index 0000000000..922e3a2e67 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/sprinklr/redux/SprinklrMiddleware.kt @@ -0,0 +1,42 @@ +package com.tangem.tap.features.sprinklr.redux + +import com.tangem.datasource.config.models.SprinklrConfig +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.features.sprinklr.redux.model.SprinklrUrl +import com.tangem.tap.store +import org.rekotlin.Middleware + +internal class SprinklrMiddleware { + val middleware: Middleware = { _, appStateProvider -> + { next -> + { action -> + val appState = appStateProvider() + if (action is SprinklrAction && appState != null) { + handleAction(action) + } + next(action) + } + } + } + + private fun handleAction(action: SprinklrAction) { + when (action) { + is SprinklrAction.Init -> updateUrl(action.userId, action.config) + is SprinklrAction.UpdateUrl, + is SprinklrAction.UpdateSprinklrDomains, + -> Unit + } + } + + private fun updateUrl(userId: String, config: SprinklrConfig) { + updateSprinklrDomains(config.baseUrl) + val url = SprinklrUrl.Prod(userId, config.baseUrl, config.appId).url + store.dispatchOnMain(SprinklrAction.UpdateUrl(url)) + } + + private fun updateSprinklrDomains(baseUrl: String) { + val domains = listOf(baseUrl, SprinklrUrl.Static.url) + store.dispatchOnMain(SprinklrAction.UpdateSprinklrDomains(domains)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/sprinklr/redux/SprinklrReducer.kt b/app/src/main/java/com/tangem/tap/features/sprinklr/redux/SprinklrReducer.kt new file mode 100644 index 0000000000..8ec0fc117c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/sprinklr/redux/SprinklrReducer.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.features.sprinklr.redux + +import com.tangem.tap.common.redux.AppState +import org.rekotlin.Action + +internal object SprinklrReducer { + fun reduce(action: Action, state: AppState): SprinklrState { + return if (action is SprinklrAction) { + internalReduce(action, state.sprinklrState) + } else state.sprinklrState + } + + private fun internalReduce(action: SprinklrAction, state: SprinklrState): SprinklrState { + return when (action) { + is SprinklrAction.Init -> state + is SprinklrAction.UpdateUrl -> state.copy(url = action.url) + is SprinklrAction.UpdateSprinklrDomains -> state.copy(sprinklrDomains = action.domains) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/sprinklr/redux/SprinklrState.kt b/app/src/main/java/com/tangem/tap/features/sprinklr/redux/SprinklrState.kt new file mode 100644 index 0000000000..12ba5b8672 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/sprinklr/redux/SprinklrState.kt @@ -0,0 +1,6 @@ +package com.tangem.tap.features.sprinklr.redux + +data class SprinklrState( + val url: String = "", + val sprinklrDomains: List = emptyList(), +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/sprinklr/redux/model/SprinklrUrl.kt b/app/src/main/java/com/tangem/tap/features/sprinklr/redux/model/SprinklrUrl.kt new file mode 100644 index 0000000000..2d1eb1be87 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/sprinklr/redux/model/SprinklrUrl.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.features.sprinklr.redux.model + +internal sealed class SprinklrUrl { + abstract val url: String + + class Prod(userId: String, baseUrl: String, appId: String) : SprinklrUrl() { + private val locale = java.util.Locale.getDefault().language + + override val url: String = "$baseUrl/page" + + "?appId=$appId" + + "&device=$Device" + + "&enableClose=$CloseEnabled" + + "&zoom=$ZoomEnabled" + + "&locale=$locale" + + "&user_id=$userId" + + companion object { + private const val Device = "MOBILE" + private const val CloseEnabled = true + private const val ZoomEnabled = false + } + } + + object Static : SprinklrUrl() { + override val url: String = "live-chat-static.sprinklr.com" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/sprinklr/ui/SprinklrActivity.kt b/app/src/main/java/com/tangem/tap/features/sprinklr/ui/SprinklrActivity.kt new file mode 100644 index 0000000000..685a52d600 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/sprinklr/ui/SprinklrActivity.kt @@ -0,0 +1,45 @@ +package com.tangem.tap.features.sprinklr.ui + +import android.os.Bundle +import androidx.activity.compose.BackHandler +import androidx.activity.viewModels +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import com.tangem.core.analytics.Analytics +import com.tangem.core.ui.fragments.ComposeActivity +import com.tangem.tap.common.analytics.events.Chat + +internal class SprinklrActivity : ComposeActivity() { + private val viewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Analytics.send(Chat.ScreenOpened()) + viewModel.setNavigateBackCallback { + this.finish() + } + } + + @Composable + override fun provideState(): State { + return viewModel.state.collectAsState() + } + + @Composable + override fun ScreenContent( + state: SprinklrScreenState, + modifier: Modifier, + ) { + BackHandler(onBack = state.onNavigateBack) + SprinklrScreenContent( + modifier = modifier + .systemBarsPadding() + .imePadding(), + state = state, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/sprinklr/ui/SprinklrScreenContent.kt b/app/src/main/java/com/tangem/tap/features/sprinklr/ui/SprinklrScreenContent.kt new file mode 100644 index 0000000000..f80a4b43f3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/sprinklr/ui/SprinklrScreenContent.kt @@ -0,0 +1,28 @@ +package com.tangem.tap.features.sprinklr.ui + +import android.annotation.SuppressLint +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.google.accompanist.web.WebView +import com.google.accompanist.web.rememberWebViewState +import com.tangem.tap.features.sprinklr.ui.webview.SprinklrWebViewClient + +@SuppressLint("SetJavaScriptEnabled") +@Composable +internal fun SprinklrScreenContent( + state: SprinklrScreenState, + modifier: Modifier = Modifier, +) { + WebView( + modifier = modifier, + state = rememberWebViewState(url = state.initialUrl), + onCreated = { webView -> + with(webView.settings) { + javaScriptEnabled = true + domStorageEnabled = true + } + }, + client = remember { SprinklrWebViewClient(state.onNewUrl) }, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/sprinklr/ui/SprinklrScreenState.kt b/app/src/main/java/com/tangem/tap/features/sprinklr/ui/SprinklrScreenState.kt new file mode 100644 index 0000000000..8757f6e167 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/sprinklr/ui/SprinklrScreenState.kt @@ -0,0 +1,10 @@ +package com.tangem.tap.features.sprinklr.ui + +import com.google.accompanist.web.WebContent + +internal data class SprinklrScreenState( + val initialUrl: String = "", + val sprinklrDomains: List = emptyList(), + val onNavigateBack: () -> Unit = {}, + val onNewUrl: WebContent.(String) -> WebContent = { this }, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/sprinklr/ui/SprinklrViewModel.kt b/app/src/main/java/com/tangem/tap/features/sprinklr/ui/SprinklrViewModel.kt new file mode 100644 index 0000000000..989b813d19 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/sprinklr/ui/SprinklrViewModel.kt @@ -0,0 +1,64 @@ +package com.tangem.tap.features.sprinklr.ui + +import androidx.lifecycle.ViewModel +import com.google.accompanist.web.WebContent +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.sprinklr.redux.SprinklrState +import com.tangem.tap.store +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import org.rekotlin.StoreSubscriber + +internal class SprinklrViewModel : ViewModel(), StoreSubscriber { + private val stateInternal = MutableStateFlow(SprinklrScreenState()) + val state = stateInternal.asStateFlow() + + init { + subscribeToStoreChanges() + } + + override fun newState(state: SprinklrState) { + stateInternal.update { prevState -> + prevState.copy( + initialUrl = state.url, + sprinklrDomains = state.sprinklrDomains, + onNewUrl = { updateWebContentOrOpenExternalUrl(it) }, + ) + } + } + + override fun onCleared() { + store.unsubscribe(this) + } + + fun setNavigateBackCallback(callback: () -> Unit) { + stateInternal.update { prevState -> + prevState.copy( + onNavigateBack = callback, + ) + } + } + + private fun subscribeToStoreChanges() { + store.subscribe(this) { appState -> + appState.skip { old, new -> old.sprinklrState == new.sprinklrState } + .select { it.sprinklrState } + } + } + + private fun WebContent.updateWebContentOrOpenExternalUrl(url: String): WebContent { + return if (isExternalUrl(url)) { + store.dispatchOnMain(NavigationAction.OpenUrl(url)) + this + } else when (this) { + is WebContent.Url -> copy(url = url) + else -> WebContent.Url(url) + } + } + + private fun isExternalUrl(url: String): Boolean { + return state.value.sprinklrDomains.none { url.contains(it) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/sprinklr/ui/webview/SprinklrWebViewClient.kt b/app/src/main/java/com/tangem/tap/features/sprinklr/ui/webview/SprinklrWebViewClient.kt new file mode 100644 index 0000000000..2f2487c88a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/sprinklr/ui/webview/SprinklrWebViewClient.kt @@ -0,0 +1,24 @@ +package com.tangem.tap.features.sprinklr.ui.webview + +import android.webkit.WebResourceRequest +import android.webkit.WebView +import com.google.accompanist.web.AccompanistWebViewClient +import com.google.accompanist.web.WebContent + +internal class SprinklrWebViewClient( + private val onNewUrl: WebContent.(String) -> WebContent, +) : AccompanistWebViewClient() { + + override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { + // If the url hasn't changed, this is probably an internal event like + // a javascript reload. We should let it happen. + if (view?.url == request?.url.toString()) { + return false + } + + request?.let { + state.content = onNewUrl(state.content, it.url.toString()) + } + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt index 1d57e7472d..3dc5ab9c7b 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt @@ -57,6 +57,12 @@ import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestCasesList import com.tangem.wallet.R import kotlinx.coroutines.launch +/** +[REDACTED_AUTHOR] + */ +@Suppress("UnusedPrivateClass") +private class AddCustomTokenScreen // for simple search + @OptIn(ExperimentalMaterialApi::class) @Composable fun AddCustomTokenScreen( diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletData.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletData.kt index a2fc562b3a..b6ed172e1a 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletData.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletData.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.wallet.redux +import com.tangem.blockchain.common.TransactionData import com.tangem.common.extensions.isZero import com.tangem.domain.common.extensions.toNetworkId import com.tangem.feature.swap.domain.SwapInteractor @@ -14,6 +15,7 @@ import java.math.BigDecimal data class WalletData( val pendingTransactions: List = emptyList(), + val historyTransactions: List? = null, val hashesCountVerified: Boolean? = null, val walletAddresses: WalletAddresses? = null, val currencyData: BalanceWidgetData = BalanceWidgetData(), diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt index 0f4f29e860..73c49d69ef 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt @@ -81,7 +81,7 @@ data class WalletState( walletsStores[0] } - private val primaryWalletManager: WalletManager? + val primaryWalletManager: WalletManager? get() = primaryWalletStore?.walletManager val primaryWalletData: WalletData? @@ -317,6 +317,7 @@ data class Artwork( const val MARTA_CARD_ID = "BC02" const val TWIN_CARD_1 = "https://app.tangem.com/cards/card_tg085.png" const val TWIN_CARD_2 = "https://app.tangem.com/cards/card_tg086.png" + const val SALT_PAY_URL = "key_for_switch_url_to_drawableId_of_salt_pay_card" } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/Mapper.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/Mapper.kt index 8b8f7ec97d..84bb0f1212 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/Mapper.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/Mapper.kt @@ -117,6 +117,7 @@ private fun WalletDataModel.mapToReduxModel( ?.toString(), errorMessage = status.errorMessage, ), + historyTransactions = historyTransactions, ) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index 1f322a6152..b3743b0360 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -9,6 +9,7 @@ import com.tangem.core.analytics.Analytics import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Token.ButtonRemoveToken +import com.tangem.tap.common.extensions.addContext import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.dispatchOnMain @@ -169,6 +170,7 @@ class MultiWalletMiddleware { is WalletAction.MultiWallet.ShowWalletBackupWarning -> Unit is WalletAction.MultiWallet.BackupWallet -> { store.state.globalState.scanResponse?.let { + Analytics.addContext(it) store.dispatch(GlobalAction.Onboarding.Start(it, canSkipBackup = false)) store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 7d257fb896..96967b136a 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -3,6 +3,7 @@ package com.tangem.tap.features.wallet.redux.middlewares import androidx.core.os.bundleOf import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.common.AmountType +import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId @@ -22,6 +23,7 @@ import com.tangem.tap.features.send.redux.PrepareSendScreen import com.tangem.tap.features.send.redux.SendAction import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens import com.tangem.tap.scope @@ -49,10 +51,9 @@ class TradeCryptoMiddleware { action: WalletAction.TradeCryptoAction.Buy, ) { val selectedWalletData = store.state.walletState.selectedWalletData ?: return + val currency = chooseAppropriateCurrency(store.state.walletState) ?: return - val currency = selectedWalletData.currency Analytics.send(Token.ButtonBuy(AnalyticsParam.CurrencyType.Currency(currency))) - if (action.checkUserLocation && state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE) { store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog()) return @@ -96,12 +97,12 @@ class TradeCryptoMiddleware { private fun proceedSellAction() { val selectedWalletData = store.state.walletState.selectedWalletData ?: return + val currency = chooseAppropriateCurrency(store.state.walletState) ?: return val appCurrency = store.state.globalState.appCurrency val addresses = selectedWalletData.walletAddresses?.list.orEmpty() if (addresses.isEmpty()) return - val currency = selectedWalletData.currency Analytics.send(Token.ButtonSell(AnalyticsParam.CurrencyType.Currency(currency))) store.state.globalState.exchangeManager.getUrl( @@ -116,6 +117,17 @@ class TradeCryptoMiddleware { } } + private fun chooseAppropriateCurrency(walletState: WalletState): Currency? { + return if (walletState.primaryTokenData == null) { + walletState.selectedWalletData?.currency + } else { + walletState.primaryTokenData?.currency as? Currency.Token + }.guard { + store.dispatchDebugErrorNotification("Can't select an appropriate currency for a Trade action") + return null + } + } + private fun preconfigureAndOpenSendScreen(action: WalletAction.TradeCryptoAction.SendCrypto) { val selectedWalletData = store.state.walletState.selectedWalletData ?: return diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index c68b906b9d..41a28fa9c9 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -15,6 +15,7 @@ import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.tap.common.analytics.converters.BasicEventsPreChecker import com.tangem.tap.common.analytics.converters.BasicEventsSourceData import com.tangem.tap.common.analytics.events.AnalyticsParam +import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.extensions.copyToClipboard @@ -249,6 +250,8 @@ class WalletMiddleware { } is NetworkStateChanged -> { store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline) + if (!action.isOnline) return + val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync if (selectedUserWallet != null) { scope.launch { globalState.tapWalletManager.loadData(selectedUserWallet) } @@ -380,7 +383,7 @@ class WalletMiddleware { } else -> { Analytics.send(MainScreen.ButtonScanCard()) - store.dispatch(WalletAction.Scan(MainScreen.CardWasScanned())) + store.dispatch(WalletAction.Scan(Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Main))) } } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt index 3a535e0375..7cb9f0b6a6 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt @@ -6,6 +6,8 @@ import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.WalletManager import com.tangem.tap.common.extensions.dispatchToastNotification +import com.tangem.tap.common.extensions.getBlockchainTxHistory +import com.tangem.tap.common.extensions.getTokenTxHistory import com.tangem.tap.common.extensions.toFiatString import com.tangem.tap.common.extensions.toFormattedCurrencyString import com.tangem.tap.common.redux.navigation.AppScreen @@ -54,6 +56,7 @@ class MultiWalletReducer { blockchain.derivationPath, ), existentialDepositString = getExistentialDeposit(walletManager), + historyTransactions = walletManager?.getBlockchainTxHistory(), ) WalletStore( @@ -89,6 +92,7 @@ class MultiWalletReducer { action.blockchain.derivationPath, ), existentialDepositString = getExistentialDeposit(walletManager), + historyTransactions = walletManager?.getBlockchainTxHistory(), ) val walletStore = WalletStore( walletManager = walletManager, @@ -153,6 +157,7 @@ class MultiWalletReducer { derivationPath = action.blockchain.derivationPath, ), walletRent = findWalletRent(state.getWalletStore(walletManager.wallet)), + historyTransactions = walletManager.getTokenTxHistory(action.token), ) state.updateWalletData(newTokenWalletData) } @@ -206,8 +211,8 @@ fun Token.toWallet(state: WalletState, blockchain: BlockchainNetwork): WalletDat val currency = Currency.fromBlockchainNetwork(blockchain, this) if (state.currencies.contains(currency)) return null - val walletManager = state.getWalletManager(currency)?.wallet - val walletAddresses = createAddressList(walletManager) + val walletManager = state.getWalletManager(currency) + val walletAddresses = createAddressList(walletManager?.wallet) return WalletData( currencyData = BalanceWidgetData( @@ -218,5 +223,6 @@ fun Token.toWallet(state: WalletState, blockchain: BlockchainNetwork): WalletDat walletAddresses = walletAddresses, mainButton = WalletMainButton.SendButton(false), currency = currency, + historyTransactions = walletManager?.getTokenTxHistory(this), ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TotalBalanceOperations.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TotalBalanceOperations.kt index 1e15f2460a..d5865ab287 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TotalBalanceOperations.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TotalBalanceOperations.kt @@ -5,7 +5,9 @@ import com.tangem.tap.features.wallet.redux.WalletData import com.tangem.tap.features.wallet.ui.BalanceStatus import java.math.BigDecimal -fun List.findProgressState(): ProgressState { +fun List.findProgressState(initialState: ProgressState = ProgressState.Done): ProgressState { + if (this.isEmpty()) return initialState + return this .mapToProgressState() .reduce(ProgressState::or) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt index 1dfbfbd164..c1157544cb 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt @@ -348,6 +348,7 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS } is WalletAction.WalletStoresChanged.UpdateWalletStores -> { newState = newState.copy( + state = action.reduxWalletStores.flatMap { it.walletsData }.findProgressState(newState.state), walletsStores = action.reduxWalletStores, ) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index ae7efb71bf..b170637f3b 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt @@ -20,7 +20,6 @@ import coil.size.Scale import com.tangem.core.analytics.Analytics import com.tangem.core.ui.fragments.setStatusBarColor import com.tangem.core.ui.utils.OneTouchClickListener -import com.tangem.domain.common.TapWorkarounds.isSaltPay import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.tap.MainActivity import com.tangem.tap.common.analytics.events.MainScreen @@ -40,9 +39,9 @@ import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter import com.tangem.tap.features.wallet.ui.wallet.MultiWalletView -import com.tangem.tap.features.wallet.ui.wallet.SaltPayWalletView import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView import com.tangem.tap.features.wallet.ui.wallet.WalletView +import com.tangem.tap.features.wallet.ui.wallet.saltPay.SaltPayWalletView import com.tangem.tap.store import com.tangem.tap.userWalletsListManager import com.tangem.wallet.BuildConfig @@ -155,19 +154,22 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { + walletView.onViewDestroy() walletView = SaltPayWalletView() walletView.changeWalletView(this, binding) } state.isMultiwalletAllowed && state.primaryWalletData?.currencyData?.status != BalanceStatus.EmptyCard && walletView !is MultiWalletView -> { + walletView.onViewDestroy() walletView = MultiWalletView() walletView.changeWalletView(this, binding) } !state.isMultiwalletAllowed && !isSaltPay && walletView !is SingleWalletView -> { + walletView.onViewDestroy() walletView = SingleWalletView() walletView.changeWalletView(this, binding) } @@ -185,7 +187,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber(500, mainScope) { store.dispatch(it) } - - override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) { - saltPayBinding = binding.lSaltPayWallet - setFragment(fragment, binding) - onViewCreated() - showSaltPayView(binding) - } - - private fun showSaltPayView(binding: FragmentWalletBinding) = with(binding) { - rvWarningMessages.hide() - rvPendingTransaction.hide() - rvMultiwallet.hide() - tvTwinCardNumber.hide() - lCardBalance.root.hide() - lSingleWalletBalance.root.hide() - lAddress.root.hide() - rowButtons.hide() - btnAddToken.hide() - pbLoadingUserTokens.hide() - lSaltPayWallet.root.show() - } - - override fun onViewCreated() { - } - - override fun onNewState(state: WalletState) { - setupBalanceWidget(state) - } - - private fun setupBalanceWidget(state: WalletState) = with(saltPayBinding.lSaltPayBalance) { - val tokenData = state.primaryTokenData ?: return@with - - val appCurrency = store.state.globalState.appCurrency - val mainProgressState = state.state - - if (mainProgressState == ProgressState.Loading) { - veilBalance.veil() - veilBalanceCrypto.veil() - } else { - veilBalanceCrypto.unVeil() - } - - tvProcessing.animateVisibility(show = mainProgressState == ProgressState.Error) - veilBalanceCrypto.animateVisibility(show = mainProgressState != ProgressState.Error) - - if (tokenData.currencyData.fiatAmount == null) { - actionDebouncer(WalletAction.LoadFiatRate()) - } else { - veilBalance.unVeil() - tvBalance.text = tokenData.currencyData.fiatAmount.formatAmountAsSpannedString( - currencySymbol = appCurrency.symbol, - ) - } - - tvBalanceCrypto.text = tokenData.currencyData.amountFormatted - - tvCurrencyName.text = appCurrency.code - tvCurrencyName.setOnClickListener { - store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency) - } - - btnBuy.show(tokenData.isAvailableToBuy(store.state.globalState.exchangeManager)) - btnBuy.setOnClickListener { - store.dispatch(WalletAction.TradeCryptoAction.Buy(false)) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt index 393f1ea97d..27b263ae5e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt @@ -6,10 +6,12 @@ import com.tangem.tap.features.wallet.ui.WalletFragment import com.tangem.wallet.databinding.FragmentWalletBinding abstract class WalletView { + var swapInteractor: SwapInteractor? = null protected var fragment: WalletFragment? = null protected var binding: FragmentWalletBinding? = null + fun setFragment(fragment: WalletFragment, binding: FragmentWalletBinding) { this.fragment = fragment this.binding = binding @@ -20,6 +22,10 @@ abstract class WalletView { binding = null } + open fun onViewDestroy() { + removeFragment() + } + open fun onDestroyFragment() {} abstract fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/HistoryTransactionData.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/HistoryTransactionData.kt new file mode 100644 index 0000000000..50b80f0b37 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/HistoryTransactionData.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.features.wallet.ui.wallet.saltPay + +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.TransactionStatus +import com.tangem.tap.features.wallet.models.PendingTransactionType + +/** +[REDACTED_AUTHOR] + */ +data class HistoryTransactionData( + val transactionData: TransactionData, + private val walletAddress: String, +) { + fun isInProgress(): Boolean = transactionData.status == TransactionStatus.Unconfirmed + + fun getTransactionType(): PendingTransactionType = when { + transactionData.sourceAddress.lowercase() == walletAddress.lowercase() -> PendingTransactionType.Outgoing + transactionData.destinationAddress.lowercase() == walletAddress.lowercase() -> PendingTransactionType.Incoming + else -> PendingTransactionType.Unknown + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/SaltPayWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/SaltPayWalletView.kt new file mode 100644 index 0000000000..e73d4ab074 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/SaltPayWalletView.kt @@ -0,0 +1,254 @@ +package com.tangem.tap.features.wallet.ui.wallet.saltPay + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.tangem.core.analytics.Analytics +import com.tangem.domain.common.extensions.debounce +import com.tangem.domain.common.extensions.withMainContext +import com.tangem.tap.common.ShimmerData +import com.tangem.tap.common.ShimmerRecyclerAdapter +import com.tangem.tap.common.analytics.events.MainScreen +import com.tangem.tap.common.extensions.animateVisibility +import com.tangem.tap.common.extensions.formatAmountAsSpannedString +import com.tangem.tap.common.extensions.hide +import com.tangem.tap.common.extensions.show +import com.tangem.tap.common.recyclerView.SpaceItemDecoration +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.ui.WalletFragment +import com.tangem.tap.features.wallet.ui.wallet.WalletView +import com.tangem.tap.features.wallet.ui.wallet.saltPay.rv.HistoryItemData +import com.tangem.tap.features.wallet.ui.wallet.saltPay.rv.HistoryTransactionData +import com.tangem.tap.features.wallet.ui.wallet.saltPay.rv.TxHistoryAdapter +import com.tangem.tap.mainScope +import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.wallet.databinding.FragmentWalletBinding +import com.tangem.wallet.databinding.ItemSaltPayTxHistoryShimmerBinding +import com.tangem.wallet.databinding.LayoutSaltPayBalanceBinding +import com.tangem.wallet.databinding.LayoutSaltPayTxHistoryBinding +import com.tangem.wallet.databinding.LayoutSaltPayWalletBinding +import kotlinx.coroutines.launch +import org.rekotlin.Action +import timber.log.Timber + +/** +[REDACTED_AUTHOR] + */ +class SaltPayWalletView : WalletView() { + + private var saltPayBinding: LayoutSaltPayWalletBinding? = null + + private val actionDebouncer = debounce(500, mainScope) { store.dispatch(it) } + + private val balanceWidget: LayoutSaltPayBalanceBinding? + get() = saltPayBinding?.lSaltPayBalance + + private val txWidget: LayoutSaltPayTxHistoryBinding? + get() = saltPayBinding?.lSaltPayTxHistory + + private val itemDecorator = SpaceItemDecoration.vertical(10F) + + override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) { + Timber.d("changeWalletView") + saltPayBinding = binding.lSaltPayWallet + showSaltPayView(binding) + onViewCreated() + } + + override fun onViewCreated() { + Timber.d("onViewCreated") + prepareTxRecyclers(txWidget!!) + } + + private fun showSaltPayView(binding: FragmentWalletBinding) = with(binding) { + Timber.d("showSaltPayView") + rvWarningMessages.hide() + rvPendingTransaction.hide() + rvMultiwallet.hide() + tvTwinCardNumber.hide() + lCardBalance.root.hide() + lSingleWalletBalance.root.hide() + lAddress.root.hide() + rowButtons.hide() + btnAddToken.hide() + pbLoadingUserTokens.hide() + lSaltPayWallet.root.show() + } + + private fun prepareTxRecyclers(txWidget: LayoutSaltPayTxHistoryBinding) = with(txWidget) { + Timber.d("prepareTxRecyclers") + val vhViewFactory: (ViewGroup) -> ViewGroup = { + val inflater = LayoutInflater.from(it.context) + ItemSaltPayTxHistoryShimmerBinding.inflate(inflater, it, false).root + } + val adapter = ShimmerRecyclerAdapter(vhViewFactory) + rvTxHistoryShimmer.adapter = adapter + rvTxHistoryShimmer.addItemDecoration(itemDecorator) + adapter.submitList( + listOf( + ShimmerData(), + ShimmerData(), + ShimmerData(), + ), + ) + + rvTxHistory.adapter = TxHistoryAdapter() + rvTxHistory.addItemDecoration(itemDecorator) + } + + override fun onNewState(state: WalletState) { + val balanceWidget = balanceWidget ?: return + val txWidget = txWidget ?: return + + val exchangeManager = store.state.globalState.exchangeManager + setupBalanceWidget(balanceWidget, exchangeManager, state) + setupTxHistoryWidget(txWidget, state) + } + + private fun setupBalanceWidget( + balanceWidget: LayoutSaltPayBalanceBinding, + exchangeManager: CurrencyExchangeManager, + state: WalletState, + ) = with(balanceWidget) { + Timber.d("setupBalanceWidget") + val tokenData = state.primaryTokenData ?: return + + val appCurrency = store.state.globalState.appCurrency + val mainProgressState = state.state + + if (mainProgressState == ProgressState.Loading) { + veilBalance.veil() + veilBalanceCrypto.veil() + } else { + veilBalanceCrypto.unVeil() + } + + tvUnreachable.animateVisibility(show = mainProgressState == ProgressState.Error) + veilBalanceCrypto.animateVisibility(show = mainProgressState != ProgressState.Error) + + if (tokenData.currencyData.fiatAmount == null) { + veilBalance.veil() + actionDebouncer(WalletAction.LoadFiatRate()) + } else { + veilBalance.unVeil() + tvBalance.text = tokenData.currencyData.fiatAmount.formatAmountAsSpannedString( + currencySymbol = appCurrency.symbol, + ) + } + + tvBalanceCrypto.text = tokenData.currencyData.amountFormatted + + tvCurrencyName.text = appCurrency.code + tvCurrencyName.setOnClickListener { + store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency) + } + + btnBuy.show(tokenData.isAvailableToBuy(exchangeManager)) + btnBuy.setOnClickListener { + Analytics.send(MainScreen.ButtonBuy()) + store.dispatch(WalletAction.TradeCryptoAction.Buy(false)) + } + } + + private fun setupTxHistoryWidget(txWidget: LayoutSaltPayTxHistoryBinding, state: WalletState) { + Timber.d("setupTxHistoryWidget") + if (state.state == ProgressState.Loading) { + handleTxLoading(txWidget) + return + } + if (state.state == ProgressState.Error) { + handleTxError(txWidget) + return + } + if (state.state != ProgressState.Done) return + val walletAddress = state.primaryWalletManager?.wallet?.address ?: return + val tokenData = state.primaryTokenData ?: return + val historyTransactions = tokenData.historyTransactions ?: return + if (historyTransactions.isEmpty()) { + handleTxEmpty(txWidget) + return + } + + scope.launch { + val dateAssociatedHistory = mutableListOf>>() + historyTransactions.forEach { tx -> + val txHistoryData = HistoryTransactionData(tx, walletAddress) + val item = dateAssociatedHistory.firstOrNull { it.first == txHistoryData.date } + if (item == null) { + dateAssociatedHistory.add(Pair(txHistoryData.date, mutableListOf(txHistoryData))) + } else { + item.second.add(txHistoryData) + } + } + + val rvDataList = mutableListOf() + dateAssociatedHistory.forEach { (txData, txHistoryData) -> + rvDataList.add(HistoryItemData.Date(txData)) + rvDataList.addAll(txHistoryData.map { HistoryItemData.TransactionData(it) }) + } + + withMainContext { handleTxSuccess(txWidget, rvDataList) } + } + } + + private fun handleTxLoading(txWidget: LayoutSaltPayTxHistoryBinding) = with(txWidget) { + Timber.d("handleTxLoading") + groupEmpty.hide() + groupError.hide() + groupSuccess.hide() + // root.beginDelayedTransition() + groupShimmer.show() + } + + private fun handleTxSuccess( + txWidget: LayoutSaltPayTxHistoryBinding, + dataList: List, + ) = with(txWidget) { + Timber.d("handleTxSuccess") + groupShimmer.hide() + groupEmpty.hide() + groupError.hide() + // root.beginDelayedTransition() + updateTxHistoryWidget(txWidget, dataList) + groupSuccess.show() + } + + private fun handleTxEmpty(txWidget: LayoutSaltPayTxHistoryBinding) = with(txWidget) { + Timber.d("handleTxEmpty") + groupShimmer.hide() + groupError.hide() + groupSuccess.hide() + // root.beginDelayedTransition() + updateTxHistoryWidget(txWidget, emptyList()) + groupEmpty.show() + } + + private fun handleTxError(txWidget: LayoutSaltPayTxHistoryBinding) = with(txWidget) { + Timber.d("handleTxError") + groupShimmer.hide() + groupEmpty.hide() + groupSuccess.hide() + // root.beginDelayedTransition() + updateTxHistoryWidget(txWidget, emptyList()) + groupError.show() + } + + private fun updateTxHistoryWidget( + txWidget: LayoutSaltPayTxHistoryBinding, + dataList: List, + ) = with(txWidget) { + (rvTxHistory.adapter as TxHistoryAdapter).submitList(dataList) + } + + override fun onViewDestroy() { + Timber.d("onViewDestroy") + txWidget?.apply { + rvTxHistoryShimmer.removeItemDecoration(itemDecorator) + rvTxHistory.removeItemDecoration(itemDecorator) + } + super.onViewDestroy() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/rv/HistoryItemData.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/rv/HistoryItemData.kt new file mode 100644 index 0000000000..7f194e047e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/rv/HistoryItemData.kt @@ -0,0 +1,79 @@ +package com.tangem.tap.features.wallet.ui.wallet.saltPay.rv + +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.TransactionStatus +import com.tangem.common.extensions.guard +import com.tangem.tap.common.extensions.toFormattedCurrencyString +import com.tangem.tap.features.wallet.models.PendingTransactionType +import java.text.SimpleDateFormat +import java.util.* + +/** +[REDACTED_AUTHOR] + */ +sealed class HistoryItemData( + val viewType: Int, + val itemId: Long, +) { + data class Date(val date: String) : HistoryItemData(0, date.hashCode().toLong()) + data class TransactionData(val data: HistoryTransactionData) : HistoryItemData(1, data.address.hashCode().toLong()) +} + +data class HistoryTransactionData( + private val transactionData: TransactionData, + private val walletAddress: String, +) { + + val isInProgress: Boolean = transactionData.status == TransactionStatus.Unconfirmed + + val transactionType: PendingTransactionType = when { + transactionData.sourceAddress.lowercase() == walletAddress.lowercase() -> PendingTransactionType.Outgoing + transactionData.destinationAddress.lowercase() == walletAddress.lowercase() -> PendingTransactionType.Incoming + else -> PendingTransactionType.Unknown + } + + val address: String = when (transactionType) { + PendingTransactionType.Incoming -> transactionData.sourceAddress.reduceAddress() + PendingTransactionType.Outgoing -> transactionData.destinationAddress.reduceAddress() + PendingTransactionType.Unknown -> "Unknown address" + } + + val time: String = transactionData.date?.let { + SimpleDateFormat("HH:mm").format(it.time) + } ?: "00:00" + + val date: String = calculateDate() + + val txSign: String = when (transactionType) { + PendingTransactionType.Incoming -> "+" + PendingTransactionType.Outgoing -> "-" + PendingTransactionType.Unknown -> "" + } + + val amountValue: String = transactionData.amount.value + ?.toFormattedCurrencyString(8, transactionData.amount.currencySymbol) ?: "0" + + private fun calculateDate(): String { + val calendarTx = transactionData.date.guard { return "Unknown" } + + val calendarNow = Calendar.getInstance() + val oldDateFormatter = SimpleDateFormat("dd.MM.yyyy") + + val date = if (calendarTx.get(Calendar.YEAR) == calendarNow.get(Calendar.YEAR)) { + val dayOfYearTx = calendarTx.get(Calendar.DAY_OF_YEAR) + val dayOfYearNow = calendarNow.get(Calendar.DAY_OF_YEAR) + when { + dayOfYearNow == dayOfYearTx -> "Today" + dayOfYearNow - dayOfYearTx == 1 -> "Tomorrow" + else -> oldDateFormatter.format(calendarTx.time) + } + } else { + oldDateFormatter.format(calendarTx.time) + } + + return date + } + + @Suppress("MagicNumber") + private fun String.reduceAddress(): String = "${substring(0..5)}...${substring(length - 4)}" +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/rv/TxHistoryAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/rv/TxHistoryAdapter.kt new file mode 100644 index 0000000000..0363cffefe --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/rv/TxHistoryAdapter.kt @@ -0,0 +1,113 @@ +package com.tangem.tap.features.wallet.ui.wallet.saltPay.rv + +import android.graphics.PorterDuff +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.tangem.tap.common.extensions.getColor +import com.tangem.tap.common.extensions.setDrawable +import com.tangem.tap.common.extensions.show +import com.tangem.tap.features.wallet.models.PendingTransactionType +import com.tangem.wallet.R +import com.tangem.wallet.databinding.ItemSaltPayTxHistoryBinding +import com.tangem.wallet.databinding.ItemSaltPayTxHistoryDateBinding + +/** +[REDACTED_AUTHOR] + */ +internal class TxHistoryAdapter : ListAdapter(DiffUtilCallback) { + + override fun getItemId(position: Int): Long = if (currentList.isEmpty()) { + 0L + } else { + currentList[position].itemId + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseTxHistoryVH { + val inflater = LayoutInflater.from(parent.context) + + return when (viewType) { + 0 -> { + val layout = ItemSaltPayTxHistoryDateBinding.inflate(inflater, parent, false) + DateItemVH(layout) + } + 1 -> { + val layout = ItemSaltPayTxHistoryBinding.inflate(inflater, parent, false) + TransactionItemVH(layout) + } + else -> throw UnsupportedOperationException() + } + } + + override fun getItemViewType(position: Int): Int = currentList[position].viewType + + override fun onBindViewHolder(holder: BaseTxHistoryVH, position: Int) { + holder.bind(currentList[position]) + } + + private object DiffUtilCallback : DiffUtil.ItemCallback() { + override fun areContentsTheSame(oldItem: HistoryItemData, newItem: HistoryItemData) = oldItem == newItem + override fun areItemsTheSame(oldItem: HistoryItemData, newItem: HistoryItemData) = oldItem == newItem + } +} + +internal abstract class BaseTxHistoryVH(layout: View) : RecyclerView.ViewHolder(layout) { + abstract fun bind(data: HistoryItemData) +} + +private class DateItemVH( + private val binding: ItemSaltPayTxHistoryDateBinding, +) : BaseTxHistoryVH(binding.root) { + + override fun bind(data: HistoryItemData) = with(binding) { + val vhData = (data as? HistoryItemData.Date)?.date ?: return + + tvTitleDate.text = vhData + } +} + +private class TransactionItemVH( + private val binding: ItemSaltPayTxHistoryBinding, +) : BaseTxHistoryVH(binding.root) { + + override fun bind(data: HistoryItemData) { + val vhData = (data as? HistoryItemData.TransactionData)?.data ?: return + setupImage(vhData) + setupTitle(vhData) + setupSubtitle(vhData) + setupBalance(vhData) + } + + private fun setupImage(data: HistoryTransactionData) = with(binding) { + val drawable = when (data.transactionType) { + PendingTransactionType.Incoming -> R.drawable.ic_tx_incoming + PendingTransactionType.Outgoing -> R.drawable.ic_tx_outgoing + PendingTransactionType.Unknown -> null + } + drawable?.let { imvTx.setDrawable(it) } + + if (data.isInProgress) { + imvTx.setColorFilter(imvTx.getColor(R.color.icon_attention), PorterDuff.Mode.SRC_ATOP) + } else { + imvTx.clearColorFilter() + } + } + + private fun setupTitle(data: HistoryTransactionData) = with(binding) { + tvTxHash.text = data.address + } + + private fun setupSubtitle(data: HistoryTransactionData) = with(binding) { + tvTxStatus.show(data.isInProgress) + tvTxTime.show(!data.isInProgress) + tvTxTime.text = data.time + } + + private fun setupBalance(data: HistoryTransactionData) = with(binding) { + tvTxAmountSign.text = data.txSign + tvTxAmountValue.text = data.amountValue + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt index 33e8bcae4b..828150fa17 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt @@ -9,8 +9,9 @@ import com.tangem.common.map import com.tangem.core.analytics.Analytics import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.util.UserWalletId +import com.tangem.tap.common.analytics.events.AnalyticsParam +import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.MyWallets -import com.tangem.tap.common.analytics.paramsInterceptor.BatchIdParamsInterceptor import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState @@ -140,7 +141,7 @@ internal class WalletSelectorMiddleware { ) ScanCardProcessor.scan( - analyticsEvent = MyWallets.CardWasScanned(), + analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.MyWallets), onWalletNotCreated = { // No need to rollback policy, continue with the policy set before the card scan store.dispatchOnMain(WalletSelectorAction.AddWallet.Success) @@ -209,9 +210,6 @@ internal class WalletSelectorMiddleware { .doOnSuccess { val selectedUserWallet = userWalletsListManager.selectedUserWalletSync if (selectedUserWallet != null) { - val batchId = selectedUserWallet.scanResponse.card.batchId - Analytics.addParamsInterceptor(BatchIdParamsInterceptor(batchId)) - store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet)) store.onUserWalletSelected(selectedUserWallet) } diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt index 696367fcd0..11fec17c6d 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt @@ -47,7 +47,10 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment { + Analytics.eraseContext() + Analytics.send(SignIn.ScreenOpened()) + } is WelcomeAction.ProceedWithBiometrics -> { proceedWithBiometrics(state) } @@ -107,7 +115,7 @@ internal class WelcomeMiddleware { useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes, ) ScanCardProcessor.scan( - analyticsEvent = SignIn.CardWasScanned(), + analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.SignIn), onSuccess = { scanResponse -> scope.launch { onCardScanned(scanResponse) } }, diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt index d7d71b7ce0..f44447f2a9 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt @@ -26,6 +26,7 @@ internal object WelcomeReducer { is WelcomeAction.ProceedWithBiometrics.Success -> state.copy(isUnlockWithBiometricsInProgress = false) is WelcomeAction.ProceedWithCard.Success -> state.copy(isUnlockWithCardInProgress = false) is WelcomeAction.CloseError -> state.copy(error = null) + else -> state } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt index 0ae230fba6..b80d65ce7a 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt @@ -19,14 +19,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource import androidx.fragment.app.viewModels -import com.tangem.core.analytics.Analytics import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.fragments.ComposeFragment import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.common.analytics.events.SignIn import com.tangem.tap.features.details.ui.cardsettings.resolveReference +import com.tangem.tap.features.welcome.redux.WelcomeAction import com.tangem.tap.features.welcome.ui.components.BiometricsLockoutDialog import com.tangem.tap.features.welcome.ui.components.WelcomeScreenContent +import com.tangem.tap.store import com.tangem.wallet.R internal class WelcomeFragment : ComposeFragment() { @@ -34,7 +34,7 @@ internal class WelcomeFragment : ComposeFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Analytics.send(SignIn.ScreenOpened()) + store.dispatch(WelcomeAction.OnCreate) } @Composable @@ -43,7 +43,10 @@ internal class WelcomeFragment : ComposeFragment() { } @Composable - override fun ScreenContent(state: WelcomeScreenState, modifier: Modifier) { + override fun ScreenContent( + state: WelcomeScreenState, + modifier: Modifier, + ) { val snackbarHostState = remember { SnackbarHostState() } val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference()) val biometricsLockoutDialog by rememberUpdatedState(newValue = state.biometricsLockoutDialog) diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt new file mode 100644 index 0000000000..d77245b45c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt @@ -0,0 +1,62 @@ +package com.tangem.tap.network.exchangeServices + +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.ProductType +import com.tangem.tap.features.wallet.models.Currency +import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService +import com.tangem.tap.network.exchangeServices.utorg.UtorgExchangeService +import com.tangem.tap.scope +import kotlinx.coroutines.launch + +/** +[REDACTED_AUTHOR] + * Temporary wrapper for the buy services. Service switches based on selected product type. + * Just now - UtorgService used only for the SaltPay cards + */ +class BuyExchangeService( + private val productTypeProvider: () -> ProductType?, + private val mercuryoService: MercuryoService, + private val utorgService: UtorgExchangeService, +) : ExchangeService, ExchangeUrlBuilder { + + init { + scope.launch { + mercuryoService.update() + utorgService.update() + } + } + + private val currentService: ExchangeService + get() = when (productTypeProvider.invoke()) { + ProductType.SaltPay -> utorgService + else -> mercuryoService + } + + override suspend fun update() { + currentService.update() + } + + override fun featureIsSwitchedOn(): Boolean = currentService.featureIsSwitchedOn() + + override fun isBuyAllowed(): Boolean = currentService.isBuyAllowed() + + override fun isSellAllowed(): Boolean = currentService.isSellAllowed() + + override fun availableForBuy(currency: Currency): Boolean = currentService.availableForBuy(currency) + + override fun availableForSell(currency: Currency): Boolean = currentService.availableForSell(currency) + + override fun getUrl( + action: CurrencyExchangeManager.Action, + blockchain: Blockchain, + cryptoCurrencyName: String, + fiatCurrencyName: String, + walletAddress: String, + ): String? { + return currentService.getUrl(action, blockchain, cryptoCurrencyName, fiatCurrencyName, walletAddress) + } + + override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? { + return currentService.getSellCryptoReceiptUrl(action, transactionId) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt index 671c45af11..f7beab3570 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt @@ -24,7 +24,7 @@ class CurrencyExchangeManager( private val buyService: ExchangeService, private val sellService: ExchangeService, private val primaryRules: ExchangeRules, -) : ExchangeService, ExchangeUrlBuilder { +) : ExchangeService { override fun featureIsSwitchedOn(): Boolean = primaryRules.featureIsSwitchedOn() diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt index 0309ce9ee9..f38a8c64a1 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt @@ -11,7 +11,7 @@ interface Exchanger { fun availableForSell(currency: Currency): Boolean } -interface ExchangeService : Feature, Exchanger { +interface ExchangeService : Feature, Exchanger, ExchangeUrlBuilder { suspend fun update() companion object { @@ -22,6 +22,18 @@ interface ExchangeService : Feature, Exchanger { override fun isSellAllowed(): Boolean = false override fun availableForBuy(currency: Currency): Boolean = false override fun availableForSell(currency: Currency): Boolean = false + override fun getUrl( + action: CurrencyExchangeManager.Action, + blockchain: Blockchain, + cryptoCurrencyName: String, + fiatCurrencyName: String, + walletAddress: String, + ): String? = null + + override fun getSellCryptoReceiptUrl( + action: CurrencyExchangeManager.Action, + transactionId: String, + ): String? = null } } } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt index 87565c3640..89308da1ce 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt @@ -11,11 +11,6 @@ interface MercuryoApi { suspend fun currencies( @Path("apiVersion") apiVersion: String, ): MercuryoCurrenciesResponse - - companion object { - const val BASE_URL = "https://api.mercuryo.io/" - const val API_VERSION = "v1.6" - } } data class MercuryoCurrenciesResponse( @@ -25,7 +20,7 @@ data class MercuryoCurrenciesResponse( data class Data( val fiat: List, val crypto: List, - val config: Config + val config: Config, ) data class Config( diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoEnvironment.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoEnvironment.kt new file mode 100644 index 0000000000..af3cda029a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoEnvironment.kt @@ -0,0 +1,36 @@ +package com.tangem.tap.network.exchangeServices.mercuryo + +import com.tangem.datasource.api.common.createRetrofitInstance + +/** +[REDACTED_AUTHOR] + */ +data class MercuryoEnvironment( + val baseUrl: String, + val apiVersion: String, + val widgetId: String, + val secret: String, + val mercuryoApi: MercuryoApi, +) { + companion object { + private const val BASE_URL = "https://api.mercuryo.io/" + private const val API_VERSION = "v1.6" + + fun prod( + widgetId: String, + secret: String, + apiVersion: String = API_VERSION, + ): MercuryoEnvironment { + return MercuryoEnvironment( + baseUrl = BASE_URL, + apiVersion = apiVersion, + widgetId = widgetId, + secret = secret, + mercuryoApi = createRetrofitInstance( + baseUrl = BASE_URL, + logEnabled = false, + ).create(MercuryoApi::class.java), + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt index 48700b18ea..9561664182 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt @@ -6,7 +6,6 @@ import com.tangem.common.extensions.calculateSha512 import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result import com.tangem.common.services.performRequest -import com.tangem.datasource.api.common.createRetrofitInstance import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager @@ -17,16 +16,10 @@ import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder [REDACTED_AUTHOR] */ class MercuryoService( - private val apiVersion: String, - private val mercuryoWidgetId: String, - private val secret: String, - private val logEnabled: Boolean, -) : ExchangeService, ExchangeUrlBuilder { + private val environment: MercuryoEnvironment, +) : ExchangeService { - private val api: MercuryoApi = createRetrofitInstance( - baseUrl = MercuryoApi.BASE_URL, - logEnabled = logEnabled, - ).create(MercuryoApi::class.java) + private val api: MercuryoApi = environment.mercuryoApi private val blockchainsAvailableToBuy = mutableListOf() private val tokensAvailableToBy = mutableMapOf>() @@ -35,7 +28,7 @@ class MercuryoService( @Suppress("NestedBlockDepth") override suspend fun update() { - when (val result = performRequest { api.currencies(apiVersion) }) { + when (val result = performRequest { api.currencies(environment.apiVersion) }) { is Result.Success -> { val response = result.data if (response.status == RESPONSE_SUCCESS_STATUS_CODE) { @@ -116,7 +109,7 @@ class MercuryoService( val builder = Uri.Builder() .scheme(ExchangeUrlBuilder.SCHEME) .authority("exchange.mercuryo.io") - .appendQueryParameter("widget_id", mercuryoWidgetId) + .appendQueryParameter("widget_id", environment.widgetId) .appendQueryParameter("type", action.name.lowercase()) .appendQueryParameter("currency", cryptoCurrencyName) .appendQueryParameter("address", walletAddress) @@ -129,7 +122,7 @@ class MercuryoService( } private fun signature(address: String): String { - return (address + secret).calculateSha512().toHexString().lowercase() + return (address + environment.secret).calculateSha512().toHexString().lowercase() } override fun getSellCryptoReceiptUrl( diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index e6752f1272..0754f6c305 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -12,7 +12,6 @@ import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.ExchangeService -import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SCHEME import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec @@ -21,7 +20,7 @@ class MoonPayService( private val apiKey: String, private val secretKey: String, private val logEnabled: Boolean, -) : ExchangeService, ExchangeUrlBuilder { +) : ExchangeService { private val api: MoonPayApi by lazy { createRetrofitInstance( @@ -103,7 +102,7 @@ class MoonPayService( blockchain: Blockchain, cryptoCurrencyName: CryptoCurrencyName, fatCurrency: String, - walletAddress: String + walletAddress: String, ): String? { if (action == CurrencyExchangeManager.Action.Buy) throw UnsupportedOperationException() @@ -148,5 +147,5 @@ class MoonPayService( private data class MoonPayStatus( val availableForSell: List, val responseUserStatus: MoonPayUserStatus, - val responseCurrencies: List + val responseCurrencies: List, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/UtorgEnvironment.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/UtorgEnvironment.kt new file mode 100644 index 0000000000..b31e8aa8bf --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/UtorgEnvironment.kt @@ -0,0 +1,88 @@ +package com.tangem.tap.network.exchangeServices.utorg + +import android.net.Uri +import com.tangem.datasource.api.common.createRetrofitInstance +import com.tangem.tap.network.exchangeServices.utorg.api.UtorgApi +import com.tangem.tap.network.exchangeServices.utorg.mock.MockUtorgApi +import okhttp3.Interceptor +import okhttp3.Response +import java.util.* + +/** +[REDACTED_AUTHOR] + */ +data class UtorgEnvironment( + val baseUri: Uri, + val sidValue: String, + val apiVersion: String, + val utorgApi: UtorgApi, + val successUrl: String = "https://success.tangem.com", +) { + companion object { + private val PROD_BASE_URL = Uri.parse("https://app.utorg.pro") + private const val PROD_VERSION = "v1" + + private val STAGE_BASE_URL = Uri.parse("https://app-stage.utorg.pro") + private const val STAGE_VERSION = "v1" + + fun prod( + authProvider: UtorgAuthProvider, + apiVersion: String = PROD_VERSION, + ): UtorgEnvironment = UtorgEnvironment( + baseUri = PROD_BASE_URL, + sidValue = authProvider.sidValue, + apiVersion = apiVersion, + utorgApi = createApi(PROD_BASE_URL, authProvider, false), + ) + + fun stage( + authProvider: UtorgAuthProvider, + logEnabled: Boolean, + apiVersion: String = STAGE_VERSION, + ): UtorgEnvironment = UtorgEnvironment( + baseUri = STAGE_BASE_URL, + sidValue = authProvider.sidValue, + apiVersion = apiVersion, + utorgApi = createApi(STAGE_BASE_URL, authProvider, logEnabled), + ) + + fun mock(): UtorgEnvironment = UtorgEnvironment( + baseUri = Uri.EMPTY, + sidValue = "", + apiVersion = "", + utorgApi = MockUtorgApi(), + ) + + private fun createApi(baseUri: Uri, authProvider: UtorgAuthProvider, logEnabled: Boolean): UtorgApi { + return createRetrofitInstance( + baseUrl = "$baseUri/", + interceptors = listOf(UtorgAuthHeaderInterceptor(authProvider)), + logEnabled = logEnabled, + ).create(UtorgApi::class.java) + } + } +} + +data class UtorgAuthProvider( + val sidValue: String, + val headerSidKey: String = "X-AUTH-SID", + val headerNonceKey: String = "X-AUTH-NONCE", +) { + val clientNonce: String + get() = UUID.randomUUID().toString() +} + +private class UtorgAuthHeaderInterceptor( + private val authProvider: UtorgAuthProvider, +) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + .newBuilder() + .addHeader(authProvider.headerSidKey, authProvider.sidValue) + .addHeader(authProvider.headerNonceKey, authProvider.clientNonce) + .build() + + return chain.proceed(request) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/UtorgExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/UtorgExchangeService.kt new file mode 100644 index 0000000000..820751a14b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/UtorgExchangeService.kt @@ -0,0 +1,116 @@ +package com.tangem.tap.network.exchangeServices.utorg + +import android.net.Uri +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.services.Result +import com.tangem.common.services.performRequest +import com.tangem.tap.common.redux.global.CryptoCurrencyName +import com.tangem.tap.features.wallet.models.Currency +import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager +import com.tangem.tap.network.exchangeServices.ExchangeService +import com.tangem.tap.network.exchangeServices.utorg.api.RequestSuccessUrl +import com.tangem.tap.network.exchangeServices.utorg.api.model.UtorgCurrencyData +import com.tangem.tap.network.exchangeServices.utorg.api.model.UtorgCurrencyType +import com.tangem.utils.converter.Converter + +/** +[REDACTED_AUTHOR] + */ +class UtorgExchangeService( + private val environment: UtorgEnvironment, + private val currencyConverter: Converter = ChainToBlockchainConverter(), +) : ExchangeService { + + private val api = environment.utorgApi + + private val utorgCurrencies = mutableListOf() + + override fun featureIsSwitchedOn(): Boolean = true + + override suspend fun update() { + when (val result = performRequest { api.getCurrency(environment.apiVersion) }) { + is Result.Success -> { + if (!result.data.isSuccess()) return + + val utorgCryptos = result.data.toSuccess().data.filter { + it.enabled && it.type == UtorgCurrencyType.CRYPTO + } + utorgCurrencies.clear() + utorgCurrencies.addAll(utorgCryptos) + + performRequest { + api.setSuccessUrl(environment.apiVersion, RequestSuccessUrl(environment.successUrl)) + } + } + is Result.Failure -> { + utorgCurrencies.clear() + } + } + } + + override fun isBuyAllowed(): Boolean = true + + override fun isSellAllowed(): Boolean = false + + override fun availableForBuy(currency: Currency): Boolean { + if (!isBuyAllowed()) return false + + val foundUtorgCurrency = utorgCurrencies.firstOrNull { utorgCurrency -> + val utorgBlockchain = currencyConverter.convert(utorgCurrency) ?: return@firstOrNull false + + val isSameBlockchain = utorgBlockchain == currency.blockchain + val isSameSymbol = utorgCurrency.symbol.lowercase() == currency.currencySymbol.lowercase() + isSameBlockchain && isSameSymbol + } + + return foundUtorgCurrency != null + } + + override fun availableForSell(currency: Currency): Boolean = false + + override fun getUrl( + action: CurrencyExchangeManager.Action, + blockchain: Blockchain, + cryptoCurrencyName: CryptoCurrencyName, + fiatCurrencyName: String, + walletAddress: String, + ): String { + if (action == CurrencyExchangeManager.Action.Sell) throw UnsupportedOperationException() + + // https://app-stage.utorg.pro/direct/testSID/mvmeaWyWiuVYdZdySgofAt6CmKhJbRhaWA/?¤cy=BTC + val builder = Uri.Builder() + .scheme(environment.baseUri.scheme) + .authority(environment.baseUri.authority) + .appendPath("direct") + .appendPath(environment.sidValue) + .appendPath(walletAddress) + .appendQueryParameter("currency", cryptoCurrencyName) + // if we set the paymentCurrency, then the Utorg widget didn't work + // .appendQueryParameter("paymentCurrency", "USD") + + val url = builder.build().toString() + return url + } + + override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? = null +} + +private class ChainToBlockchainConverter : Converter { + override fun convert(value: UtorgCurrencyData): Blockchain? { + return when (value.chain?.uppercase()) { + "ARBITRUM" -> Blockchain.Arbitrum + "AVALANCHE" -> Blockchain.Avalanche + "BINANCE_SMART_CHAIN" -> Blockchain.BSC + "BITCOIN" -> Blockchain.Bitcoin + "BNB" -> Blockchain.Binance + "ETHEREUM" -> Blockchain.Ethereum + "GNOSIS" -> Blockchain.Gnosis + "POLYGON" -> Blockchain.Polygon + "RIPPLE" -> Blockchain.XRP + "RSK" -> Blockchain.RSK + "SOLANA" -> Blockchain.Solana + else -> null + // "APTOS", "ATOM", "NEAR", "ZKSYNC", "VECHAIN", "VELAS", "VELAS_EVM" -> null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/api/UtorgApi.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/api/UtorgApi.kt new file mode 100644 index 0000000000..6244d39a5e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/api/UtorgApi.kt @@ -0,0 +1,67 @@ +package com.tangem.tap.network.exchangeServices.utorg.api + +import com.squareup.moshi.Json +import com.tangem.tap.network.exchangeServices.utorg.api.model.UtorgCurrencyResponse +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.http.Path + +/** +[REDACTED_AUTHOR] + */ +interface UtorgApi { + + @POST("api/merchant/{apiVersion}/settings/currency") + suspend fun getCurrency( + @Path("apiVersion") apiVersion: String, + ): UtorgCurrencyResponse + + @POST("api/merchant/{apiVersion}/settings/successUrl") + suspend fun setSuccessUrl( + @Path("apiVersion") apiVersion: String, + @Body request: RequestSuccessUrl, + ) +} + +data class RequestSuccessUrl( + val url: String, +) + +interface UtorgResponse { + val success: Boolean + val timestamp: Long + val data: Data? + val error: UtorgErrorResponse? + + fun isSuccess(): Boolean = success && data != null && error == null + + @Throws(NullPointerException::class) + fun toSuccess(): UtorgSuccessResponse { + val internalTimestamp = this.timestamp + val internalData = this.data + + return object : UtorgSuccessResponse { + override val timestamp: Long = internalTimestamp + override val data: Data = internalData!! + } + } + + @Throws(NullPointerException::class) + fun toError(): UtorgErrorResponse = error!! +} + +interface UtorgSuccessResponse { + val timestamp: Long + val data: T +} + +data class UtorgErrorResponse( + @Json(name = "message") val message: String?, + @Json(name = "type") val type: UtorgErrorType, +) + +enum class UtorgErrorType { + UNAUTHORIZED, + UNKNOWN_ERROR, + BAD_REQUEST, +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/api/model/UtorgCurrencyResponse.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/api/model/UtorgCurrencyResponse.kt new file mode 100644 index 0000000000..5df98687d8 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/api/model/UtorgCurrencyResponse.kt @@ -0,0 +1,25 @@ +package com.tangem.tap.network.exchangeServices.utorg.api.model + +import com.squareup.moshi.Json +import com.tangem.tap.network.exchangeServices.utorg.api.UtorgErrorResponse +import com.tangem.tap.network.exchangeServices.utorg.api.UtorgResponse + +class UtorgCurrencyResponse( + @Json(name = "success") override val success: Boolean, + @Json(name = "timestamp") override val timestamp: Long, + @Json(name = "data") override val data: List?, + @Json(name = "error") override val error: UtorgErrorResponse?, +) : UtorgResponse> + +data class UtorgCurrencyData( + @Json(name = "currency") val currency: String, + @Json(name = "symbol") val symbol: String, + @Json(name = "enabled") val enabled: Boolean, + @Json(name = "type") val type: UtorgCurrencyType, + @Json(name = "caption") val caption: String?, + @Json(name = "chain") val chain: String?, +) + +enum class UtorgCurrencyType { + FIAT, CRYPTO +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/mock/MockUtorgApi.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/mock/MockUtorgApi.kt new file mode 100644 index 0000000000..e50b8b8833 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/mock/MockUtorgApi.kt @@ -0,0 +1,58 @@ +package com.tangem.tap.network.exchangeServices.utorg.mock + +import com.tangem.common.json.MoshiJsonConverter +import com.tangem.tap.network.exchangeServices.utorg.api.RequestSuccessUrl +import com.tangem.tap.network.exchangeServices.utorg.api.UtorgApi +import com.tangem.tap.network.exchangeServices.utorg.api.UtorgErrorResponse +import com.tangem.tap.network.exchangeServices.utorg.api.UtorgErrorType +import com.tangem.tap.network.exchangeServices.utorg.api.model.UtorgCurrencyResponse +import kotlinx.coroutines.delay + +/** +[REDACTED_AUTHOR] + */ +class MockUtorgApi : UtorgApi { + + var nextDelay = 500L + + var nextResponseType = ResponseType.Success + var nextErrorType = UtorgErrorType.UNKNOWN_ERROR + + override suspend fun getCurrency(apiVersion: String): UtorgCurrencyResponse { + delay(nextDelay) + return UtorgCurrencyResponse( + success = createIsSuccess(), + timestamp = createTime(), + data = createData(MockUtorgSuccessDataResponse.GetCurrency), + error = createError(), + ) + } + + override suspend fun setSuccessUrl(apiVersion: String, request: RequestSuccessUrl) { + delay(nextDelay) + } + + private fun createIsSuccess(): Boolean = when (nextResponseType) { + ResponseType.Success -> true + ResponseType.Error -> false + } + + private fun createTime(): Long = System.currentTimeMillis() + + private inline fun createData(response: MockUtorgSuccessDataResponse): T? = when (nextResponseType) { + ResponseType.Success -> MoshiJsonConverter.INSTANCE.fromJson(response.json)!! + ResponseType.Error -> null + } + + private fun createError(): UtorgErrorResponse? = when (nextResponseType) { + ResponseType.Success -> null + ResponseType.Error -> UtorgErrorResponse( + message = "Optional message", + type = nextErrorType, + ) + } + + enum class ResponseType { + Success, Error + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/mock/MockUtorgSuccessDataResponse.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/mock/MockUtorgSuccessDataResponse.kt new file mode 100644 index 0000000000..959e65a4d5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/mock/MockUtorgSuccessDataResponse.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.network.exchangeServices.utorg.mock + +import com.tangem.tap.network.exchangeServices.utorg.mock.json.getCurrencyFull + +/** +[REDACTED_AUTHOR] + */ +sealed class MockUtorgSuccessDataResponse( + val json: String, +) { + object GetCurrency : MockUtorgSuccessDataResponse(getCurrencyFull) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/mock/json/CurrencyResponseJson.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/mock/json/CurrencyResponseJson.kt new file mode 100644 index 0000000000..3d50462305 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/utorg/mock/json/CurrencyResponseJson.kt @@ -0,0 +1,1025 @@ +package com.tangem.tap.network.exchangeServices.utorg.mock.json + +/** +[REDACTED_AUTHOR] + */ +internal val getCurrencyFull = """ +[ + { + "currency": "APT", + "symbol": "APT", + "chain": "APTOS", + "display": "APT", + "caption": "Aptos", + "explorerTx": "https://aptoscan.com/version/", + "explorerAddr": "https://aptoscan.com/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 1.8492, + "depositMax": 739.6673, + "withdrawalMin": 1.8492, + "withdrawalMax": 739.6673, + "addressValidator": "APT", + "precision": 4, + "allowTag": false + }, + { + "currency": "ATOM", + "symbol": "ATOM", + "chain": "ATOM", + "display": "ATOM", + "caption": "Cosmos", + "explorerTx": "https://atomscan.com/transactions/", + "explorerAddr": "https://atomscan.com/accounts/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 1.8020, + "depositMax": 720.7911, + "withdrawalMin": 1.8020, + "withdrawalMax": 720.7911, + "addressValidator": "COSMOS", + "precision": 4, + "allowTag": true, + "udKey": "crypto.ATOM.address" + }, + { + "currency": "AUD", + "symbol": "AUD", + "display": "AUD", + "type": "FIAT", + "enabled": true, + "depositMin": 43.73, + "depositMax": 15013.68, + "withdrawalMin": 43.73, + "withdrawalMax": 15013.68, + "addressValidator": "LUHN", + "precision": 2, + "allowTag": false + }, + { + "currency": "AVAXAVA", + "symbol": "AVAX", + "chain": "AVALANCHE", + "display": "Avax", + "caption": "Avalanche", + "explorerTx": "", + "explorerAddr": "", + "type": "CRYPTO", + "enabled": true, + "depositMin": 1.337428, + "depositMax": 534.971102, + "withdrawalMin": 1.337428, + "withdrawalMax": 534.971102, + "addressValidator": "ETH", + "precision": 6, + "allowTag": false, + "udKey": "crypto.AVAX.address" + }, + { + "currency": "BNB", + "symbol": "BNB", + "chain": "BNB", + "display": "BNB", + "caption": "BEP2", + "explorerTx": "https://explorer.binance.org/tx/", + "explorerAddr": "https://explorer.binance.org/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 0.082738, + "depositMax": 33.095138, + "withdrawalMin": 0.082738, + "withdrawalMax": 33.095138, + "addressValidator": "BNB", + "precision": 6, + "allowTag": true, + "udKey": "crypto.BNB.address" + }, + { + "currency": "BNBBSC", + "symbol": "BNB", + "chain": "BINANCE_SMART_CHAIN", + "display": "BNB", + "caption": "BEP20", + "explorerTx": "https://www.bscscan.com/tx/", + "explorerAddr": "https://www.bscscan.com/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 0.0828, + "depositMax": 33.0951, + "withdrawalMin": 0.0828, + "withdrawalMax": 33.0951, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false + }, + { + "currency": "BRL", + "symbol": "BRL", + "display": "BRL", + "type": "FIAT", + "enabled": true, + "depositMin": 158.08, + "depositMax": 54293.21, + "withdrawalMin": 158.08, + "withdrawalMax": 54293.21, + "addressValidator": "LUHN", + "precision": 2, + "allowTag": false + }, + { + "currency": "BTC", + "symbol": "BTC", + "chain": "BITCOIN", + "display": "BTC", + "caption": "", + "explorerTx": "https://blockstream.info/tx/", + "explorerAddr": "https://blockstream.info/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 528764.805415, + "depositMax": 211505922.165820, + "withdrawalMin": 528764.805415, + "withdrawalMax": 211505922.165820, + "addressValidator": "BTC", + "precision": 6, + "allowTag": false, + "udKey": "crypto.BTC.address" + }, + { + "currency": "BUSDAVA", + "symbol": "BUSD", + "chain": "AVALANCHE", + "display": "BUSD", + "caption": "on Avalanche", + "explorerTx": "https://snowtrace.io/tx/", + "explorerAddr": "https://snowtrace.io/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 26.6382, + "depositMax": 10655.2775, + "withdrawalMin": 26.6382, + "withdrawalMax": 10655.2775, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false + }, + { + "currency": "BUSDBSC", + "symbol": "BUSD", + "chain": "BINANCE_SMART_CHAIN", + "display": "BUSD", + "caption": "BEP20", + "explorerTx": "https://www.bscscan.com/tx/", + "explorerAddr": "https://www.bscscan.com/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 26.6414, + "depositMax": 10656.5563, + "withdrawalMin": 26.6414, + "withdrawalMax": 10656.5563, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false, + "udKey": "crypto.BUSD.version.BEP20.address" + }, + { + "currency": "BUSDETH", + "symbol": "BUSD", + "chain": "ETHEREUM", + "display": "BUSD", + "caption": "ERC20", + "explorerTx": "https://etherscan.io/tx/", + "explorerAddr": "https://etherscan.io/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 26.6414, + "depositMax": 10656.5563, + "withdrawalMin": 26.6414, + "withdrawalMax": 10656.5563, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false, + "udKey": "crypto.BUSD.version.ERC20.address" + }, + { + "currency": "CAD", + "symbol": "CAD", + "display": "CAD", + "type": "FIAT", + "enabled": true, + "depositMin": 40.85, + "depositMax": 14024.33, + "withdrawalMin": 40.85, + "withdrawalMax": 14024.33, + "addressValidator": "LUHN", + "precision": 2, + "allowTag": false + }, + { + "currency": "CZK", + "symbol": "CZK", + "display": "CZK", + "type": "FIAT", + "enabled": true, + "depositMin": 672.71, + "depositMax": 231068.12, + "withdrawalMin": 672.71, + "withdrawalMax": 231068.12, + "addressValidator": "LUHN", + "precision": 2, + "allowTag": false + }, + { + "currency": "DAIBSC", + "symbol": "DAI", + "chain": "BINANCE_SMART_CHAIN", + "display": "DAI", + "caption": "BEP20", + "explorerTx": "https://www.bscscan.com/tx/", + "explorerAddr": "https://www.bscscan.com/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 26.6462, + "depositMax": 10658.4634, + "withdrawalMin": 26.6462, + "withdrawalMax": 10658.4634, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false + }, + { + "currency": "DAIEAVA", + "symbol": "DAIE", + "chain": "AVALANCHE", + "display": "DAI.e", + "caption": "on Avalanche", + "explorerTx": "https://snowtrace.io/tx/", + "explorerAddr": "https://snowtrace.io/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 26.6052, + "depositMax": 10642.0513, + "withdrawalMin": 26.6052, + "withdrawalMax": 10642.0513, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false + }, + { + "currency": "DAIETH", + "symbol": "DAI", + "chain": "ETHEREUM", + "display": "DAI", + "caption": "ERC20", + "explorerTx": "https://etherscan.io/tx/", + "explorerAddr": "https://etherscan.io/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 26.6436, + "depositMax": 10657.4090, + "withdrawalMin": 26.6436, + "withdrawalMax": 10657.4090, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false, + "udKey": "crypto.DAI.address" + }, + { + "currency": "DAIPOL", + "symbol": "DAI", + "chain": "POLYGON", + "display": "DAI", + "caption": "on Polygon", + "explorerTx": "https://polygonscan.com/tx/", + "explorerAddr": "https://polygonscan.com/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 26.6436, + "depositMax": 10657.4090, + "withdrawalMin": 26.6436, + "withdrawalMax": 10657.4090, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false + }, + { + "currency": "DKK", + "symbol": "DKK", + "display": "DKK", + "type": "FIAT", + "enabled": true, + "depositMin": 210.64, + "depositMax": 72348.14, + "withdrawalMin": 210.64, + "withdrawalMax": 72348.14, + "addressValidator": "LUHN", + "precision": 2, + "allowTag": false + }, + { + "currency": "ETH", + "symbol": "ETH", + "chain": "ETHEREUM", + "display": "ETH", + "caption": "", + "explorerTx": "https://ropsten.etherscan.io/tx/", + "explorerAddr": "https://ropsten.etherscan.io/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 37688.767964, + "depositMax": 15075507.185288, + "withdrawalMin": 37688.767964, + "withdrawalMax": 15075507.185288, + "addressValidator": "ETH", + "precision": 6, + "allowTag": false, + "udKey": "crypto.ETH.address" + }, + { + "currency": "EUR", + "symbol": "EUR", + "display": "EUR", + "caption": "", + "type": "FIAT", + "enabled": true, + "depositMin": 27.24, + "depositMax": 9349.53, + "withdrawalMin": 27.24, + "withdrawalMax": 9349.53, + "addressValidator": "LUHN", + "precision": 2, + "allowTag": false + }, + { + "currency": "GBP", + "symbol": "GBP", + "display": "GBP", + "type": "FIAT", + "enabled": true, + "depositMin": 25.14, + "depositMax": 8630.60, + "withdrawalMin": 25.14, + "withdrawalMax": 8630.60, + "addressValidator": "LUHN", + "precision": 2, + "allowTag": false + }, + { + "currency": "GMTBSC", + "symbol": "GMT", + "chain": "BINANCE_SMART_CHAIN", + "display": "GMT", + "caption": "BEP20", + "explorerTx": "https://bscscan.com/tx/", + "explorerAddr": "https://bscscan.com/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 53.9899, + "depositMax": 21595.9496, + "withdrawalMin": 53.9899, + "withdrawalMax": 21595.9496, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false + }, + { + "currency": "GNOETH", + "symbol": "GNO", + "chain": "ETHEREUM", + "display": "GNO", + "caption": "ERC20", + "explorerTx": "https://etherscan.io/tx/", + "explorerAddr": "https://etherscan.io/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 0.2250, + "depositMax": 89.9938, + "withdrawalMin": 0.2250, + "withdrawalMax": 89.9938, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false, + "udKey": "crypto.GNO.address" + }, + { + "currency": "HAIBSC", + "symbol": "HAI", + "chain": "BINANCE_SMART_CHAIN", + "display": "HAI", + "caption": "on BSC", + "explorerTx": "https://bscscan.com/tx/", + "explorerAddr": "https://bscscan.com/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 937.548948, + "depositMax": 375019.579098, + "withdrawalMin": 937.548948, + "withdrawalMax": 375019.579098, + "addressValidator": "ETH", + "precision": 6, + "allowTag": false + }, + { + "currency": "HAIVET", + "symbol": "HAI", + "chain": "VECHAIN", + "display": "HAI", + "caption": "VET", + "explorerTx": "https://vechainstats.com/transaction/", + "explorerAddr": "https://vechainstats.com/account/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 949.3974, + "depositMax": 379758.9569, + "withdrawalMin": 949.3974, + "withdrawalMax": 379758.9569, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false + }, + { + "currency": "IDR", + "symbol": "IDR", + "display": "IDR", + "type": "FIAT", + "enabled": true, + "depositMin": 459558, + "depositMax": 157855988, + "withdrawalMin": 459558, + "withdrawalMax": 157855988, + "addressValidator": "LUHN", + "precision": 0, + "allowTag": false + }, + { + "currency": "INR", + "symbol": "INR", + "display": "INR", + "type": "FIAT", + "enabled": true, + "depositMin": 2511.35, + "depositMax": 862635.95, + "withdrawalMin": 2511.35, + "withdrawalMax": 862635.95, + "addressValidator": "LUHN", + "precision": 2, + "allowTag": false + }, + { + "currency": "JPY", + "symbol": "JPY", + "display": "JPY", + "type": "FIAT", + "enabled": true, + "depositMin": 3992, + "depositMax": 1370884, + "withdrawalMin": 3992, + "withdrawalMax": 1370884, + "addressValidator": "LUHN", + "precision": 0, + "allowTag": false + }, + { + "currency": "KZT", + "symbol": "KZT", + "display": "KZT", + "type": "FIAT", + "enabled": true, + "depositMin": 13843.18, + "depositMax": 4755080.66, + "withdrawalMin": 13843.18, + "withdrawalMax": 4755080.66, + "addressValidator": "LUHN", + "precision": 2, + "allowTag": false + }, + { + "currency": "MH3ETH", + "symbol": "MH3", + "chain": "ETHEREUM", + "display": "NFT", + "caption": "Meta History", + "explorerTx": "https://rinkeby.etherscan.io/tx/", + "explorerAddr": "https://rinkeby.etherscan.io/address/", + "type": "CRYPTO", + "enabled": false, + "depositMin": 0, + "depositMax": 0, + "withdrawalMin": 0, + "withdrawalMax": 0, + "addressValidator": "ETH", + "precision": 0, + "allowTag": false, + "nftContract": "0xf0192b97135abaa218bcff255b8f4b22171a7590" + }, + { + "currency": "MXN", + "symbol": "MXN", + "display": "MXN", + "type": "FIAT", + "enabled": true, + "depositMin": 574.83, + "depositMax": 197445.63, + "withdrawalMin": 574.83, + "withdrawalMax": 197445.63, + "addressValidator": "LUHN", + "precision": 2, + "allowTag": false + }, + { + "currency": "MYR", + "symbol": "MYR", + "display": "MYR", + "type": "FIAT", + "enabled": true, + "depositMin": 131.04, + "depositMax": 45006.86, + "withdrawalMin": 131.04, + "withdrawalMax": 45006.86, + "addressValidator": "LUHN", + "precision": 2, + "allowTag": false + }, + { + "currency": "NEAR", + "symbol": "NEAR", + "chain": "NEAR", + "display": "NEAR", + "caption": "", + "explorerTx": "", + "explorerAddr": "", + "type": "CRYPTO", + "enabled": true, + "depositMin": 10.560163, + "depositMax": 4224.064906, + "withdrawalMin": 10.560163, + "withdrawalMax": 4224.064906, + "addressValidator": "NEAR", + "precision": 6, + "allowTag": false, + "udKey": "crypto.NEAR.address" + }, + { + "currency": "NOK", + "symbol": "NOK", + "display": "NOK", + "type": "FIAT", + "enabled": true, + "depositMin": 312.23, + "depositMax": 107245.60, + "withdrawalMin": 312.23, + "withdrawalMax": 107245.60, + "addressValidator": "LUHN", + "precision": 2, + "allowTag": false + }, + { + "currency": "NXUSDAVA", + "symbol": "NXUSD", + "chain": "AVALANCHE", + "display": "NXUSD", + "caption": "Avalanche", + "explorerTx": "https://snowtrace.io/tx/", + "explorerAddr": "https://snowtrace.io/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 26.8766, + "depositMax": 10750.6217, + "withdrawalMin": 26.8766, + "withdrawalMax": 10750.6217, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false + }, + { + "currency": "NZD", + "symbol": "NZD", + "display": "NZD", + "type": "FIAT", + "enabled": true, + "depositMin": 47.90, + "depositMax": 16448.49, + "withdrawalMin": 47.90, + "withdrawalMax": 16448.49, + "addressValidator": "LUHN", + "precision": 2, + "allowTag": false + }, + { + "currency": "PLN", + "symbol": "PLN", + "display": "PLN", + "type": "FIAT", + "enabled": true, + "depositMin": 134.06, + "depositMax": 46047.85, + "withdrawalMin": 134.06, + "withdrawalMax": 46047.85, + "addressValidator": "LUHN", + "precision": 2, + "allowTag": false + }, + { + "currency": "RIF", + "symbol": "RIF", + "chain": "RSK", + "display": "RIF", + "caption": "RSK", + "explorerTx": "https://explorer.rsk.co/tx/", + "explorerAddr": "https://explorer.rsk.co/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 429.65, + "depositMax": 171859.31, + "withdrawalMin": 429.65, + "withdrawalMax": 171859.31, + "addressValidator": "ETH", + "precision": 2, + "allowTag": false, + "udKey": "crypto.RIF.address" + }, + { + "currency": "RUB", + "symbol": "RUB", + "display": "RUB", + "type": "FIAT", + "enabled": true, + "depositMin": 2217.40, + "depositMax": 761666.68, + "withdrawalMin": 2217.40, + "withdrawalMax": 761666.68, + "addressValidator": "LUHN", + "precision": 2, + "allowTag": false + }, + { + "currency": "SOL", + "symbol": "SOL", + "chain": "SOLANA", + "display": "SOL", + "caption": "Solana", + "explorerTx": "https://explorer.solana.com/tx/", + "explorerAddr": "https://explorer.solana.com/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 1.155094, + "depositMax": 462.037519, + "withdrawalMin": 1.155094, + "withdrawalMax": 462.037519, + "addressValidator": "SOLANA", + "precision": 6, + "allowTag": false, + "udKey": "crypto.SOL.address" + }, + { + "currency": "TRY", + "symbol": "TRY", + "display": "TRY", + "type": "FIAT", + "enabled": true, + "depositMin": 572.28, + "depositMax": 196571.08, + "withdrawalMin": 572.28, + "withdrawalMax": 196571.08, + "addressValidator": "LUHN", + "precision": 2, + "allowTag": false + }, + { + "currency": "TWTBNB", + "symbol": "TWT", + "chain": "BNB", + "display": "TWT", + "caption": "BEP2", + "explorerTx": "https://explorer.binance.org/tx/", + "explorerAddr": "https://explorer.binance.org/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 17.4457, + "depositMax": 6978.2637, + "withdrawalMin": 17.4457, + "withdrawalMax": 6978.2637, + "addressValidator": "BNB", + "precision": 4, + "allowTag": true, + "udKey": "crypto.TWT.address" + }, + { + "currency": "UAH", + "symbol": "UAH", + "display": "UAH", + "caption": "", + "type": "FIAT", + "enabled": true, + "depositMin": 1118.00, + "depositMax": 14005.59, + "withdrawalMin": 1118.00, + "withdrawalMax": 14005.59, + "addressValidator": "LUHN", + "precision": 2, + "allowTag": false + }, + { + "currency": "USD", + "symbol": "USD", + "display": "USD", + "type": "FIAT", + "enabled": true, + "depositMin": 30.57, + "depositMax": 10382.96, + "withdrawalMin": 30.57, + "withdrawalMax": 10382.96, + "addressValidator": "LUHN", + "precision": 2, + "allowTag": false + }, + { + "currency": "USDCARB", + "symbol": "USDC", + "chain": "ARBITRUM", + "display": "USDC", + "caption": "on arbitrum", + "explorerTx": "https://arbiscan.io/tx/", + "explorerAddr": "https://arbiscan.io/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 26.6382, + "depositMax": 10655.2775, + "withdrawalMin": 26.6382, + "withdrawalMax": 10655.2775, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false + }, + { + "currency": "USDCBSC", + "symbol": "USDC", + "chain": "BINANCE_SMART_CHAIN", + "display": "USDC", + "caption": "BEP20", + "explorerTx": "https://www.bscscan.com/tx/", + "explorerAddr": "https://www.bscscan.com/address/", + "type": "CRYPTO", + "enabled": false, + "depositMin": 0, + "depositMax": 0, + "withdrawalMin": 0, + "withdrawalMax": 0, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false + }, + { + "currency": "USDCEAVA", + "symbol": "USDCE", + "chain": "AVALANCHE", + "display": "USDC.e", + "caption": "on Avalanche", + "explorerTx": "", + "explorerAddr": "", + "type": "CRYPTO", + "enabled": true, + "depositMin": 26.6078, + "depositMax": 10643.1155, + "withdrawalMin": 26.6078, + "withdrawalMax": 10643.1155, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false + }, + { + "currency": "USDCETH", + "symbol": "USDC", + "chain": "ETHEREUM", + "display": "USDC", + "caption": "ERC20", + "explorerTx": "https://etherscan.io/tx/", + "explorerAddr": "https://etherscan.io/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 26.6382, + "depositMax": 10655.2775, + "withdrawalMin": 26.6382, + "withdrawalMax": 10655.2775, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false, + "udKey": "crypto.USDC.address" + }, + { + "currency": "USDCZK", + "symbol": "USDC", + "chain": "ZKSYNC", + "display": "USDC", + "caption": "on zkSync", + "explorerTx": "https://zkscan.io/explorer/transactions/", + "explorerAddr": "https://zkscan.io/explorer/accounts/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 26.6078, + "depositMax": 10643.1155, + "withdrawalMin": 26.6078, + "withdrawalMax": 10643.1155, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false + }, + { + "currency": "USDTAVA", + "symbol": "USDT", + "chain": "AVALANCHE", + "display": "USDT", + "caption": "on Avalanche", + "explorerTx": "https://snowtrace.io/tx/", + "explorerAddr": "https://snowtrace.io/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 26.6649, + "depositMax": 10665.9328, + "withdrawalMin": 26.6649, + "withdrawalMax": 10665.9328, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false + }, + { + "currency": "USDTE", + "symbol": "USDT", + "chain": "ETHEREUM", + "display": "USDT", + "caption": "", + "explorerTx": "", + "explorerAddr": "", + "type": "CRYPTO", + "enabled": true, + "depositMin": 26.66483213, + "depositMax": 10665.93285048, + "withdrawalMin": 26.66483213, + "withdrawalMax": 10665.93285048, + "addressValidator": "ETH", + "precision": 8, + "allowTag": false, + "udKey": "crypto.USDT.version.ERC20.address" + }, + { + "currency": "USDTPOL", + "symbol": "USDT", + "chain": "POLYGON", + "display": "USDT", + "caption": "on Polygon", + "explorerTx": "https://polygonscan.com/tx/", + "explorerAddr": "https://polygonscan.com/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 26.6649, + "depositMax": 10665.9328, + "withdrawalMin": 26.6649, + "withdrawalMax": 10665.9328, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false + }, + { + "currency": "USDVEL", + "symbol": "USDV", + "chain": "VELAS_EVM", + "display": "USDV", + "caption": "Velas EVM", + "explorerTx": "https://evmexplorer.velas.com/tx/", + "explorerAddr": "https://evmexplorer.velas.com/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 26611608.908400, + "depositMax": 10644643563.359833, + "withdrawalMin": 26611608.908400, + "withdrawalMax": 10644643563.359833, + "addressValidator": "ETH", + "precision": 6, + "allowTag": false + }, + { + "currency": "USNNEAR", + "symbol": "USN", + "chain": "NEAR", + "display": "USN", + "caption": "Near", + "explorerTx": "https://explorer.near.org/transactions/", + "explorerAddr": "https://explorer.near.org/accounts/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 26.6969, + "depositMax": 10678.7473, + "withdrawalMin": 26.6969, + "withdrawalMax": 10678.7473, + "addressValidator": "NEAR", + "precision": 4, + "allowTag": false + }, + { + "currency": "VLX", + "symbol": "VLX", + "chain": "VELAS", + "display": "VLX", + "caption": "Velas", + "explorerTx": "https://explorer.velas.com/tx/", + "explorerAddr": "https://explorer.velas.com/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 1021.951187, + "depositMax": 408780.474783, + "withdrawalMin": 1021.951187, + "withdrawalMax": 408780.474783, + "addressValidator": "SOLANA", + "precision": 6, + "allowTag": false, + "udKey": "crypto.VLX.address" + }, + { + "currency": "VLXETH", + "symbol": "VLX", + "chain": "VELAS_EVM", + "display": "VLX", + "caption": "Velas EVM", + "explorerTx": "https://evmexplorer.velas.com/tx/", + "explorerAddr": "https://evmexplorer.velas.com/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 1021.9512, + "depositMax": 408780.4747, + "withdrawalMin": 1021.9512, + "withdrawalMax": 408780.4747, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false + }, + { + "currency": "WLKNSOL", + "symbol": "WLKN", + "chain": "SOLANA", + "display": "WLKN", + "caption": "Solana", + "explorerTx": "https://explorer.solana.com/tx/", + "explorerAddr": "https://explorer.solana.com/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 531.89, + "depositMax": 212755.45, + "withdrawalMin": 531.89, + "withdrawalMax": 212755.45, + "addressValidator": "SOLANA", + "precision": 2, + "allowTag": false + }, + { + "currency": "XDAIGNO", + "symbol": "xDAI", + "chain": "GNOSIS", + "display": "xDAI", + "caption": "Gnosis chain", + "explorerTx": "https://blockscout.com/xdai/mainnet/tx/", + "explorerAddr": "https://blockscout.com/xdai/mainnet/address/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 26.6782, + "depositMax": 10671.2684, + "withdrawalMin": 26.6782, + "withdrawalMax": 10671.2684, + "addressValidator": "ETH", + "precision": 4, + "allowTag": false + }, + { + "currency": "XRP", + "symbol": "XRP", + "chain": "RIPPLE", + "display": "XRP", + "caption": "Ripple", + "explorerTx": "https://xrpscan.com/tx/", + "explorerAddr": "https://xrpscan.com/account/", + "type": "CRYPTO", + "enabled": true, + "depositMin": 67.7645, + "depositMax": 27105.7684, + "withdrawalMin": 67.7645, + "withdrawalMax": 27105.7684, + "addressValidator": "XRP", + "precision": 4, + "allowTag": true, + "udKey": "crypto.XRP.address" + }, + { + "currency": "ZAR", + "symbol": "ZAR", + "display": "ZAR", + "type": "FIAT", + "enabled": true, + "depositMin": 538.27, + "depositMax": 184892.03, + "withdrawalMin": 538.27, + "withdrawalMax": 184892.03, + "addressValidator": "LUHN", + "precision": 2, + "allowTag": false + } + ] +""".trimIndent() \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt index a6ff52c139..3f5c655ec2 100644 --- a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt +++ b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt @@ -35,9 +35,13 @@ class PreferencesStorage(applicationContext: Application) { toppedUpWalletStorage = ToppedUpWalletStorage(preferences, moshiConverter) } - var chatFirstLaunchTime: Long? - get() = preferences.getLong(CHAT_FIRST_LAUNCH_KEY, 0).takeIf { it != 0L } - set(value) = preferences.edit { putLong(CHAT_FIRST_LAUNCH_KEY, value ?: 0) } + var zendeskFirstLaunchTime: Long? + get() = preferences.getLong(ZENDESK_FIRST_LAUNCH_KEY, 0).takeIf { it != 0L } + set(value) = preferences.edit { putLong(ZENDESK_FIRST_LAUNCH_KEY, value ?: 0) } + + var sprinklrFirstLaunchTime: Long? + get() = preferences.getLong(SPRINKLR_FIRST_LAUNCH_KEY, 0).takeIf { it != 0L } + set(value) = preferences.edit { putLong(SPRINKLR_FIRST_LAUNCH_KEY, value ?: 0) } var shouldShowSaveUserWalletScreen: Boolean get() = preferences.getBoolean(SAVE_WALLET_DIALOG_SHOWN_KEY, true) @@ -88,7 +92,8 @@ class PreferencesStorage(applicationContext: Application) { private const val PREFERENCES_NAME = "tapPrefs" private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown" private const val APP_LAUNCH_COUNT_KEY = "launchCount" - private const val CHAT_FIRST_LAUNCH_KEY = "chatFirstLaunchKey" + private const val ZENDESK_FIRST_LAUNCH_KEY = "chatFirstLaunchKey" + private const val SPRINKLR_FIRST_LAUNCH_KEY = "sprinklrFirstLaunch" private const val SAVE_WALLET_DIALOG_SHOWN_KEY = "saveUserWalletShown" private const val SAVE_USER_WALLETS_KEY = "saveUserWallets" private const val SAVE_ACCESS_CODES_KEY = "saveAccessCodes" diff --git a/app/src/main/res/drawable/ic_accept_coin.xml b/app/src/main/res/drawable/ic_accept_coin.xml new file mode 100644 index 0000000000..6df78b4b77 --- /dev/null +++ b/app/src/main/res/drawable/ic_accept_coin.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_tx_explore.xml b/app/src/main/res/drawable/ic_tx_explore.xml new file mode 100644 index 0000000000..0c0c7403f5 --- /dev/null +++ b/app/src/main/res/drawable/ic_tx_explore.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_tx_incoming.xml b/app/src/main/res/drawable/ic_tx_incoming.xml new file mode 100644 index 0000000000..1f8d16c0ff --- /dev/null +++ b/app/src/main/res/drawable/ic_tx_incoming.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_tx_outgoing.xml b/app/src/main/res/drawable/ic_tx_outgoing.xml new file mode 100644 index 0000000000..e6b391e594 --- /dev/null +++ b/app/src/main/res/drawable/ic_tx_outgoing.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/layout/item_salt_pay_tx_history.xml b/app/src/main/res/layout/item_salt_pay_tx_history.xml new file mode 100644 index 0000000000..5e41bb38d1 --- /dev/null +++ b/app/src/main/res/layout/item_salt_pay_tx_history.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_salt_pay_tx_history_date.xml b/app/src/main/res/layout/item_salt_pay_tx_history_date.xml new file mode 100644 index 0000000000..a21c2dbd6e --- /dev/null +++ b/app/src/main/res/layout/item_salt_pay_tx_history_date.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/layout/item_salt_pay_tx_history_shimmer.xml b/app/src/main/res/layout/item_salt_pay_tx_history_shimmer.xml new file mode 100644 index 0000000000..72aff47fa0 --- /dev/null +++ b/app/src/main/res/layout/item_salt_pay_tx_history_shimmer.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_salt_pay_balance.xml b/app/src/main/res/layout/layout_salt_pay_balance.xml index ff1772d79b..8ce65d0b6b 100644 --- a/app/src/main/res/layout/layout_salt_pay_balance.xml +++ b/app/src/main/res/layout/layout_salt_pay_balance.xml @@ -21,12 +21,10 @@ android:id="@+id/tv_title" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginEnd="8dp" android:text="@string/onboarding_balance_title" android:textColor="@color/text_tertiary" android:textSize="14sp" android:textStyle="bold" - app:layout_constraintEnd_toStartOf="@id/tv_currency_name" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -34,8 +32,8 @@ android:id="@+id/veil_balance" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="4dp" - app:layout_constraintBottom_toTopOf="@id/tv_processing" + android:layout_marginTop="6dp" + app:layout_constraintBottom_toTopOf="@id/tv_unreachable" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_title" @@ -53,7 +51,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:maxLines="1" - android:minWidth="152dp" android:textColor="@color/text_primary_1" android:textSize="24sp" android:textStyle="bold" @@ -65,7 +62,7 @@ android:id="@+id/veil_balance_crypto" android:layout_width="wrap_content" android:layout_height="18dp" - android:layout_marginTop="4dp" + android:layout_marginTop="6dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/veil_balance" app:veilLayout_baseColor="@color/lightGray0" @@ -81,7 +78,6 @@ android:id="@+id/tv_balance_crypto" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginTop="2dp" android:maxLines="1" android:minWidth="152dp" android:textColor="@color/text_tertiary" @@ -92,10 +88,10 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_salt_pay_wallet.xml b/app/src/main/res/layout/layout_salt_pay_wallet.xml index c27f1b8c8a..4712e76937 100644 --- a/app/src/main/res/layout/layout_salt_pay_wallet.xml +++ b/app/src/main/res/layout/layout_salt_pay_wallet.xml @@ -19,4 +19,14 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> + + diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml index 2bbb4f525b..d5f3e649d4 100644 --- a/app/src/main/res/xml/network_security_config.xml +++ b/app/src/main/res/xml/network_security_config.xml @@ -16,4 +16,6 @@ - \ No newline at end of file + + + diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index 946bc67130..32ba2da6ab 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -24,7 +24,7 @@ object Versions { // endregion Compose // region Other libraries - const val accompanist = "0.27.1" + const val accompanist = "0.28.0" const val amplitude = "2.36.1" const val appsflyer = "6.5.1" const val armadillo = "0.9.0" @@ -61,7 +61,7 @@ object Versions { // endregion Other libraries // region Tangem - const val tangemBlockchainSdk = "develop-159" + const val tangemBlockchainSdk = "develop-168" // const val tangemBlockchainSdk = "0.0.1" // Keep it! - used for local builds const val tangemCardSdk = "develop-191" diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt index 20fcb92c92..0e0f40b6c9 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt @@ -54,8 +54,8 @@ object Analytics : GlobalAnalyticsEventHandler { paramsInterceptors[interceptor.id()] = interceptor } - override fun removeParamsInterceptor(interceptor: ParamsInterceptor): ParamsInterceptor? { - return paramsInterceptors.remove(interceptor.id()) + override fun removeParamsInterceptor(interceptorId: String): ParamsInterceptor? { + return paramsInterceptors.remove(interceptorId) } override fun send(event: AnalyticsEvent) { diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/api/ParamsInterceptorApi.kt b/core/analytics/src/main/java/com/tangem/core/analytics/api/ParamsInterceptorApi.kt index e2f9dd5860..683a1c45a0 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/api/ParamsInterceptorApi.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/api/ParamsInterceptorApi.kt @@ -13,5 +13,5 @@ interface ParamsInterceptor { interface ParamsInterceptorHolder { fun addParamsInterceptor(interceptor: ParamsInterceptor) - fun removeParamsInterceptor(interceptor: ParamsInterceptor): ParamsInterceptor? + fun removeParamsInterceptor(interceptorId: String): ParamsInterceptor? } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt index e7a9d62ab0..63e503173e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.config import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.BlockchairCredentials +import com.tangem.blockchain.common.BlockscoutCredentials import com.tangem.blockchain.common.GetBlockCredentials import com.tangem.blockchain.common.NowNodeCredentials import com.tangem.blockchain.common.QuickNodeCredentials @@ -82,6 +83,10 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { authToken = configValues.blockchairAuthorizationToken, ), blockcypherTokens = configValues.blockcypherTokens, + blockscoutCredentials = BlockscoutCredentials( + userName = configValues.saltPay.blockscoutCredentials.user, + password = configValues.saltPay.blockscoutCredentials.password, + ), quickNodeSolanaCredentials = QuickNodeCredentials( apiKey = configValues.quiknodeApiKey, subdomain = configValues.quiknodeSubdomain, @@ -94,7 +99,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { tronGridApiKey = configValues.tronGridApiKey, saltPayAuthToken = configValues.saltPay.credentials.token, nowNodeCredentials = NowNodeCredentials(configValues.nowNodesApiKey), - getBlockCredentials = GetBlockCredentials(configValues.getBlockApiKey) + getBlockCredentials = GetBlockCredentials(configValues.getBlockApiKey), ), appsFlyerDevKey = configValues.appsFlyer.appsFlyerDevKey, amplitudeApiKey = configValues.amplitudeApiKey, diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/ZendeskConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/ChatConfig.kt similarity index 67% rename from core/datasource/src/main/java/com/tangem/datasource/config/models/ZendeskConfig.kt rename to core/datasource/src/main/java/com/tangem/datasource/config/models/ChatConfig.kt index 84e9f9c786..d34275da0f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/ZendeskConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/ChatConfig.kt @@ -3,6 +3,8 @@ package com.tangem.datasource.config.models import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +sealed interface ChatConfig + @JsonClass(generateAdapter = true) data class ZendeskConfig( @Json(name = "zendeskApiKey") @@ -15,4 +17,12 @@ data class ZendeskConfig( val accountKey: String, @Json(name = "zendeskUrl") val url: String, -) \ No newline at end of file +) : ChatConfig + +@JsonClass(generateAdapter = true) +data class SprinklrConfig( + @Json(name = "appID") + val appId: String, + @Json(name = "baseURL") + val baseUrl: String, +) : ChatConfig \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/SaltPayConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/SaltPayConfig.kt index 65efeb65d7..f8148bcf9e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/SaltPayConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/SaltPayConfig.kt @@ -6,16 +6,18 @@ import org.spongycastle.util.encoders.Base64.toBase64String [REDACTED_AUTHOR] */ data class SaltPayConfig( - val zendesk: ZendeskConfig, + val sprinklr: SprinklrConfig, val kycProvider: KYCProvider, val credentials: Credentials, + val blockscoutCredentials: Credentials, ) { companion object { fun stub(): SaltPayConfig { return SaltPayConfig( - zendesk = ZendeskConfig("", "", "", "", ""), + sprinklr = SprinklrConfig("", ""), kycProvider = KYCProvider("", "", "", ""), credentials = Credentials("", ""), + blockscoutCredentials = Credentials("", ""), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeActivity.kt b/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeActivity.kt new file mode 100644 index 0000000000..b14a8f3c9d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeActivity.kt @@ -0,0 +1,11 @@ +package com.tangem.core.ui.fragments + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +abstract class ComposeActivity : AppCompatActivity(), ComposeScreen { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(createComposeView(context = this)) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeBottomSheetFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeBottomSheetFragment.kt index f4759a6ed8..a88b3a2ff7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeBottomSheetFragment.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeBottomSheetFragment.kt @@ -10,8 +10,6 @@ import androidx.annotation.FloatRange import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -20,7 +18,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.tangem.core.ui.R import com.tangem.core.ui.res.TangemTheme -abstract class ComposeBottomSheetFragment : BottomSheetDialogFragment() { +abstract class ComposeBottomSheetFragment : BottomSheetDialogFragment(), ComposeScreen { open val initialBottomSheetState = BottomSheetBehavior.STATE_EXPANDED @FloatRange(from = 0.0, to = 1.0) @@ -47,7 +45,7 @@ abstract class ComposeBottomSheetFragment : BottomSheetDialogFragme return dialog } - private fun createComposeView(context: Context): View { + override fun createComposeView(context: Context): View { return ComposeView(context).apply { setContent { TangemTheme { @@ -67,15 +65,4 @@ abstract class ComposeBottomSheetFragment : BottomSheetDialogFragme } } } - - @Suppress("TopLevelComposableFunctions") - @Composable - protected abstract fun provideState(): State - - @Suppress("TopLevelComposableFunctions") - @Composable - protected abstract fun ScreenContent( - state: ScreenState, - modifier: Modifier, - ) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeFragment.kt index 6b6865094a..d7ab04a268 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeFragment.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeFragment.kt @@ -1,21 +1,12 @@ package com.tangem.core.ui.fragments -import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment -import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.res.TangemTheme -abstract class ComposeFragment : Fragment() { +abstract class ComposeFragment : Fragment(), ComposeScreen { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -23,34 +14,4 @@ abstract class ComposeFragment : Fragment() { ): View { return createComposeView(inflater.context) } - - private fun createComposeView(context: Context): View { - return ComposeView(context).apply { - setContent { - TangemTheme { - val backgroundColor = TangemTheme.colors.background.primary - SystemBarsEffect { - setSystemBarsColor( - color = backgroundColor, - ) - } - - ScreenContent( - state = provideState().value, - modifier = Modifier - .fillMaxSize() - .background(color = backgroundColor), - ) - } - } - } - } - - @Suppress("TopLevelComposableFunctions") - @Composable - protected abstract fun provideState(): State - - @Suppress("TopLevelComposableFunctions") - @Composable - protected abstract fun ScreenContent(state: ScreenState, modifier: Modifier) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeScreen.kt new file mode 100644 index 0000000000..20d6bf7bf1 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeScreen.kt @@ -0,0 +1,47 @@ +package com.tangem.core.ui.fragments + +import android.content.Context +import android.view.View +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.res.TangemTheme + +internal interface ComposeScreen { + fun createComposeView(context: Context): View { + return ComposeView(context).apply { + setContent { + TangemTheme { + val backgroundColor = TangemTheme.colors.background.primary + SystemBarsEffect { + setSystemBarsColor( + color = backgroundColor, + ) + } + + ScreenContent( + state = provideState().value, + modifier = Modifier + .fillMaxSize() + .background(color = backgroundColor), + ) + } + } + } + } + + @Suppress("TopLevelComposableFunctions") + @Composable + fun provideState(): State + + @Suppress("TopLevelComposableFunctions") + @Composable + fun ScreenContent( + state: ScreenState, + modifier: Modifier, + ) +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/LogConfig.kt b/domain/src/main/java/com/tangem/domain/common/LogConfig.kt index f0626bf715..6ad07afbf2 100644 --- a/domain/src/main/java/com/tangem/domain/common/LogConfig.kt +++ b/domain/src/main/java/com/tangem/domain/common/LogConfig.kt @@ -13,6 +13,9 @@ object LogConfig { object NetworkLogConfig { const val mercuryoService: Boolean = false const val moonPayService: Boolean = false + const val utorgService: Boolean = false + val tangemTechService: Boolean = BuildConfig.DEBUG + val paymentologyApiService: Boolean = BuildConfig.DEBUG val blockchainSdkNetwork: Boolean = BuildConfig.DEBUG } diff --git a/domain/src/main/java/com/tangem/domain/common/SaltPayWorkaround.kt b/domain/src/main/java/com/tangem/domain/common/SaltPayWorkaround.kt index 58c03985a6..b18597f282 100644 --- a/domain/src/main/java/com/tangem/domain/common/SaltPayWorkaround.kt +++ b/domain/src/main/java/com/tangem/domain/common/SaltPayWorkaround.kt @@ -6,8 +6,12 @@ import com.tangem.common.CardIdRange import com.tangem.common.contains object SaltPayWorkaround { + @Suppress("MagicNumber") - val visaBatches = listOf("AE02", "AE03") + attachTestVisaBatches() + val visaBatches = listOf( + "AE02", + "AE03", + ) + attachTestVisaBatches() val walletCardIds = listOf( "AC01000000033503", @@ -57,21 +61,24 @@ object SaltPayWorkaround { } } + @Suppress("MagicNumber") + fun isSaltPayCardId(cardId: String): Boolean = isVisaBatchId(cardId.take(4)) || isWalletCardId(cardId) + fun isVisaBatchId(batchId: String): Boolean = visaBatches.contains(batchId) fun isWalletCardId(cardId: String): Boolean { return if (walletCardIds.contains(cardId)) true else walletCardIdRanges.contains(cardId) } - private fun attachTestVisaBatches(): List { - return listOf("FF03") - } + private fun attachTestVisaBatches(): List = listOf( + "FF03", + ) - private fun attachTestWalletCardIds(): List { - return listOf("FF04000000000232") - } + private fun attachTestWalletCardIds(): List = listOf( + "FF04000000000232", + ) - private fun attachTestWalletCardIdRanges(): List { - return listOf(CardIdRange("FF04000000000000", "FF04999999999999")!!) - } + private fun attachTestWalletCardIdRanges(): List = listOf( + CardIdRange("FF04000000000000", "FF04999999999999")!!, + ) } \ No newline at end of file