diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 34d26a35ae..b9af199725 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -194,6 +194,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 f09883d42e..0680d8b5e1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -138,9 +138,12 @@ - + + @@ -147,7 +147,12 @@ 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, @@ -188,7 +193,7 @@ class TapApplication : Application(), ImageLoaderFactory { private fun initWithConfigDependency(config: Config) { shopService = TangemShopService(this, config.shopify!!) initAnalytics(this, config) - initFeedbackManager(this, preferencesStorage, config) + initFeedbackManager(this, preferencesStorage, foregroundActivityObserver, store) } private fun initAnalytics(application: Application, config: Config) { @@ -210,7 +215,12 @@ class TapApplication : Application(), ImageLoaderFactory { factory.build(Analytics, buildData) } - private fun initFeedbackManager(context: Context, preferencesStorage: PreferencesStorage, config: Config) { + 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) @@ -243,12 +253,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..4d56172588 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/ShimmerRecyclerAdapter.kt @@ -0,0 +1,33 @@ +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) + +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/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index bad36d3f0d..4a07f6a669 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 @@ -8,7 +8,7 @@ sealed class AnalyticsParam { class Currency(currency: com.tangem.tap.features.wallet.models.Currency) : CurrencyType(currency.currencySymbol) class Blockchain(blockchain: com.tangem.blockchain.common.Blockchain) : CurrencyType(blockchain.currency) class Token(token: com.tangem.blockchain.common.Token) : CurrencyType(token.symbol) - class FiatCurrency(fiatCurrency: com.tangem.tap.common.entities.FiatCurrency) : CurrencyType(fiatCurrency.symbol) + class FiatCurrency(fiatCurrency: com.tangem.tap.common.entities.FiatCurrency) : CurrencyType(fiatCurrency.code) class Amount(amount: com.tangem.blockchain.common.Amount) : CurrencyType(amount.currencySymbol) } @@ -60,8 +60,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 ecdedd9761..7a7e9f245e 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 8eb0f6490d..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,15 +67,25 @@ 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 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", 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..e723fb4a9e --- /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)?.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.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/zendesk/ZendeskConfig.kt b/app/src/main/java/com/tangem/tap/common/chat/ChatConfig.kt similarity index 62% rename from app/src/main/java/com/tangem/tap/common/zendesk/ZendeskConfig.kt rename to app/src/main/java/com/tangem/tap/common/chat/ChatConfig.kt index 66569ee1b7..01b80c8cc6 100644 --- a/app/src/main/java/com/tangem/tap/common/zendesk/ZendeskConfig.kt +++ b/app/src/main/java/com/tangem/tap/common/chat/ChatConfig.kt @@ -1,8 +1,10 @@ -package com.tangem.tap.common.zendesk +package com.tangem.tap.common.chat 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/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..2646fe74c7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt @@ -0,0 +1,50 @@ +package com.tangem.tap.common.chat + +import android.content.Context +import android.os.Build +import com.tangem.tap.ForegroundActivityObserver +import com.tangem.tap.common.chat.opener.ChatOpener +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..d5923097f4 --- /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.SprinklrConfig +import com.tangem.tap.common.chat.opener.ChatOpener +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..1c07657555 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/ZendeskChatOpener.kt @@ -0,0 +1,77 @@ +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.ZendeskConfig +import com.tangem.tap.common.chat.opener.ChatOpener +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/compose/Button.kt b/app/src/main/java/com/tangem/tap/common/compose/Button.kt index b20f7470b8..fe5aa7979a 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/Button.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/Button.kt @@ -12,9 +12,7 @@ import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.Scaffold import androidx.compose.material.Text -import androidx.compose.material.ripple.LocalRippleTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -133,18 +131,6 @@ fun ClearButton( } } -/** - * Used for disable ripple if button is enable = false - */ -@Composable -fun ToggledRippleTheme( - isEnabled: Boolean, - content: @Composable () -> Unit, -) { - val theme = LocalRippleTheme provides if (isEnabled) LocalRippleTheme.current else NoRippleTheme() - CompositionLocalProvider(theme) { content() } -} - @Preview @Composable fun ButtonTest() { diff --git a/app/src/main/java/com/tangem/tap/common/compose/Theme.kt b/app/src/main/java/com/tangem/tap/common/compose/Theme.kt deleted file mode 100644 index f0afe076ba..0000000000 --- a/app/src/main/java/com/tangem/tap/common/compose/Theme.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.tap.common.compose - -import androidx.compose.material.ripple.RippleAlpha -import androidx.compose.material.ripple.RippleTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color - -/** -[REDACTED_AUTHOR] - */ -class NoRippleTheme : RippleTheme { - @Composable - override fun defaultColor() = Color.Unspecified - - @Composable - override fun rippleAlpha(): RippleAlpha = RippleAlpha(0.0f, 0.0f, 0.0f, 0.0f) -} \ 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..3772f86a71 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt @@ -0,0 +1,16 @@ +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.CardContextInterceptor + +/** +[REDACTED_AUTHOR] + */ +fun Analytics.addCardContext(scanResponse: ScanResponse) { + addParamsInterceptor(CardContextInterceptor(scanResponse)) +} + +fun Analytics.eraseCardContext() { + removeParamsInterceptor(CardContextInterceptor.id()) +} \ 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/UI.kt b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt index 9e58542ecb..885fd337e3 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt @@ -210,6 +210,7 @@ fun View.animateVisibility( hiddenVisibility: Int = View.GONE, ) { if (show) { + if (this.visibility == View.VISIBLE) return this.animate() .alpha(1f) .setDuration(durationMillis) @@ -218,6 +219,7 @@ fun View.animateVisibility( this.isVisible = true } } else { + if (this.visibility == hiddenVisibility) return this.animate() .alpha(0f) .setDuration(durationMillis) 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 cda489f536..a07ef080da 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 @@ -30,6 +34,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? { 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 b44db4004b..666d79ee81 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.ChatConfig +import com.tangem.tap.common.chat.ChatManager import com.tangem.tap.common.extensions.sendEmail import com.tangem.tap.common.log.TangemLogCollector -import com.tangem.tap.common.zendesk.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 9f7c34b90f..3e0d617fb2 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 @@ -60,6 +62,7 @@ data class AppState( val welcomeState: WelcomeState = WelcomeState(), val saveWalletState: SaveWalletState = SaveWalletState(), val walletSelectorState: WalletSelectorState = WalletSelectorState(), + val sprinklrState: SprinklrState = SprinklrState(), ) : StateType { val domainState: DomainState @@ -94,6 +97,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 1f51374cae..dbd831e752 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 @@ -5,6 +5,7 @@ import com.tangem.blockchain.common.WalletManager import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError import com.tangem.domain.common.ScanResponse +import com.tangem.tap.common.chat.ChatConfig import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.feedback.FeedbackData import com.tangem.tap.common.feedback.FeedbackManager @@ -13,7 +14,6 @@ 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.tap.common.zendesk.ZendeskConfig import com.tangem.tap.domain.TapError import com.tangem.tap.domain.configurable.config.ConfigManager import com.tangem.tap.domain.configurable.warningMessage.WarningMessage @@ -36,18 +36,16 @@ 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 */ - object StartForUnfinishedBackup : Onboarding() + data class StartForUnfinishedBackup(val isSaltPayVisa: Boolean) : Onboarding() object Stop : Onboarding() } @@ -87,7 +85,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/GlobalMidlleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt index dfcdf942bf..4696b4a6f8 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt @@ -3,28 +3,37 @@ 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.common.services.Result +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 import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState +import com.tangem.tap.domain.configurable.config.Config 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 @@ -100,17 +109,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?.isSaltPay() == true -> config.saltPayConfig?.zendesk + val unsafeChatConfig = action.chatConfig ?: when { + scanResponse?.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)) @@ -121,38 +129,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 -> {} @@ -207,4 +200,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 62e990eeff..df19afc1c4 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -1,5 +1,6 @@ package com.tangem.tap.domain +import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.Token @@ -39,6 +40,7 @@ import com.tangem.tap.userTokensRepository import com.tangem.tap.walletStoresManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.rekotlin.Action import timber.log.Timber class TapWalletManager { @@ -223,20 +225,20 @@ class TapWalletManager { val primaryWalletManager = walletManagerFactory.makePrimaryWalletManager(data) if (blockchain != Blockchain.Unknown && primaryWalletManager != null) { - val primaryToken = data.getPrimaryToken() - - dispatchOnMain(WalletAction.MultiWallet.SetPrimaryBlockchain(blockchain)) - if (primaryToken != null) { - primaryWalletManager.addToken(primaryToken) - dispatchOnMain(WalletAction.MultiWallet.SetPrimaryToken(primaryToken)) - } - dispatchOnMain( + val blockchainNetwork = BlockchainNetwork.fromWalletManager(primaryWalletManager) + val actionsList = listOfNotNull( WalletAction.MultiWallet.AddBlockchains( blockchains = listOf(BlockchainNetwork.fromWalletManager(primaryWalletManager)), walletManagers = listOf(primaryWalletManager), ), + data.getPrimaryToken()?.let { + primaryWalletManager.addToken(it) + primaryWalletManager.wallet.setAmount(Amount(it)) + WalletAction.MultiWallet.AddToken(it, blockchainNetwork, false) + }, WalletAction.LoadFiatRate(), ) + dispatchOnMain(*actionsList.toTypedArray()) } } diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt index 8a2a6032cc..a45fc5c289 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt @@ -2,11 +2,12 @@ package com.tangem.tap.domain.configurable.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 +import com.tangem.tap.common.chat.ZendeskConfig import com.tangem.tap.common.shop.shopify.ShopifyShop -import com.tangem.tap.common.zendesk.ZendeskConfig import com.tangem.tap.domain.configurable.Loader import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig @@ -93,6 +94,10 @@ class ConfigManager { authToken = values.blockchairAuthorizationToken, ), blockcypherTokens = values.blockcypherTokens, + blockscoutCredentials = BlockscoutCredentials( + userName = values.saltPay.blockscoutCredentials.user, + password = values.saltPay.blockscoutCredentials.password, + ), quickNodeSolanaCredentials = QuickNodeCredentials( apiKey = values.quiknodeApiKey, subdomain = values.quiknodeSubdomain, diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt index aa57ec09f7..4de75aae58 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt @@ -1,7 +1,7 @@ package com.tangem.tap.domain.configurable.config +import com.tangem.tap.common.chat.ZendeskConfig import com.tangem.tap.common.shop.shopify.ShopifyShop -import com.tangem.tap.common.zendesk.ZendeskConfig import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig /** @@ -15,6 +15,7 @@ class FeatureModel( val isCreatingTwinCardsAllowed: Boolean, ) +@Suppress("LongParameterList") class ConfigValueModel( val coinMarketCapKey: String, val mercuryoWidgetId: String, @@ -37,6 +38,7 @@ class ConfigValueModel( val saltPay: SaltPayConfig, val tronGridApiKey: String, val amplitudeApiKey: String, + val swapReferrerAccount: SwapReferrerAccount?, ) data class AppsFlyer( @@ -44,6 +46,11 @@ data class AppsFlyer( val appsFlyerAppID: String, ) +data class SwapReferrerAccount( + val address: String, + val fee: String, +) + class ConfigModel( val features: FeatureModel?, val configValues: ConfigValueModel?, 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 989793e59a..a2801c5a8f 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 @@ -137,23 +133,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.getBlockchain() - if (blockchain != Blockchain.SaltPay) - throw IllegalArgumentException() - - val token = SaltPayWorkaround.tokenFrom(blockchain) - val cardWallet = scanResponse.card.wallets.firstOrNull().guard { - throw NullPointerException("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 22cc834b2a..b5dff3de1f 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 @@ -6,6 +6,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 @@ -96,6 +97,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 651ab25018..5b4f90aea3 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 6caafef969..3317a70315 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,7 +13,7 @@ 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.extensions.addCardContext import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa import com.tangem.tap.common.redux.global.GlobalAction @@ -27,10 +27,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 +75,7 @@ object ScanCardProcessor { tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse) onScanStateChange(false) - sendAnalytics(analyticsEvent, scanResponse.card.batchId) + sendAnalytics(analyticsEvent, scanResponse) checkForUnfinishedBackupForSaltPay( backupService = backupService, @@ -103,9 +103,9 @@ object ScanCardProcessor { private fun sendAnalytics( analyticsEvent: AnalyticsEvent?, - batchId: String, + scanResponse: ScanResponse, ) { - Analytics.addParamsInterceptor(BatchIdParamsInterceptor(batchId)) + Analytics.addCardContext(scanResponse) analyticsEvent?.let { Analytics.send(it) } } @@ -187,7 +187,10 @@ object ScanCardProcessor { if (scanResponse.isSaltPay()) { if (scanResponse.isSaltPayVisa()) { - val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse) + val manager = SaltPayActivationManagerFactory( + blockchain = scanResponse.getBlockchain(), + card = scanResponse.card, + ).create() val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse, manager) delay(500) withMainContext { @@ -197,8 +200,8 @@ object ScanCardProcessor { 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) 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 93c70be9ef..66c90b9f9e 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 @@ -20,6 +20,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 @@ -32,6 +33,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 @@ -203,7 +205,7 @@ internal class DefaultWalletAmountsRepository( updateWalletStoreWithUnreachable(walletStore) } else -> { - withInternetConnection { walletManager.update() } + withInternetConnection { walletManager.safeUpdate() } .map { updateWalletManagerWithAmounts(userWalletId, walletManager) } .flatMap { updateWalletStoreWithAmounts( @@ -318,7 +320,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 0c3e47861c..4bf5394553 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 b0d473cde0..d984e73f13 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 @@ -49,6 +49,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() @@ -117,9 +121,6 @@ class DemoConfig { private val releaseDemoCardIds = mutableListOf( // === Not from the Google Sheet table === - "FB10000000000196", // Note BTC - "FB20000000000186", // Note ETH - "FB30000000000176", // Wallet "AC01000000041225", "AC01000000041472", "AB01000000046498", @@ -489,17 +490,19 @@ class DemoConfig { class DemoTransactionSender( private val walletManager: WalletManager, - private val sender: TransactionSender = walletManager as TransactionSender + private val sender: TransactionSender = walletManager as TransactionSender, ) : TransactionSender { override suspend fun getFee(amount: Amount, destination: String): Result> { val blockchain = walletManager.wallet.blockchain return when (walletManager) { - is BitcoinWalletManager -> Result.Success(listOf( - Amount(0.0001.toBigDecimal(), blockchain), - Amount(0.0003.toBigDecimal(), blockchain), - Amount(0.00055.toBigDecimal(), blockchain), - )) + is BitcoinWalletManager -> Result.Success( + listOf( + Amount(0.0001.toBigDecimal(), blockchain), + Amount(0.0003.toBigDecimal(), blockchain), + Amount(0.00055.toBigDecimal(), blockchain), + ), + ) else -> sender.getFee(amount, destination) } } @@ -508,7 +511,7 @@ class DemoTransactionSender( val dataToSign = randomString(32).toByteArray() val signerResponse = signer.sign( hash = dataToSign, - publicKey = walletManager.wallet.publicKey + publicKey = walletManager.wallet.publicKey, ) return when (signerResponse) { is CompletionResult.Success -> SimpleResult.Failure(Exception(ID).toBlockchainSdkError()) @@ -529,5 +532,4 @@ class DemoTransactionSender( companion object { val ID = DemoTransactionSender::class.java.simpleName } - } \ No newline at end of file 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 60dc234e4f..3395d8b1f0 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 76048ed0f4..afcb5c7a14 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.eraseCardContext import com.tangem.tap.common.extensions.onCardScanned import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState @@ -51,6 +53,10 @@ private val homeMiddleware: Middleware = { _, _ -> private fun handleHomeAction(action: Action) { when (action) { + is HomeAction.OnCreate -> { + Analytics.eraseCardContext() + 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/OnboardingSaltPayHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingSaltPayHelper.kt index 209fc789eb..8c88cc7a84 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 @@ -20,14 +20,14 @@ class OnboardingSaltPayHelper { 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) { 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 f3ff070f97..42848b47b9 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 @@ -30,9 +30,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() @@ -64,7 +63,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 939236339c..3397e56980 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 @@ -3,13 +3,13 @@ package com.tangem.tap.features.onboarding.products.wallet.redux import com.tangem.blockchain.common.Blockchain import com.tangem.common.CompletionResult import com.tangem.common.extensions.ifNotNull +import com.tangem.core.analytics.Analytics import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.isSaltPay import com.tangem.domain.common.extensions.withMainContext import com.tangem.operations.backup.BackupService import com.tangem.tap.backupService -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.extensions.dispatchOnMain @@ -22,8 +22,8 @@ import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.redux.HomeAction 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 @@ -71,7 +71,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 -> { @@ -86,10 +86,11 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: scanResponse.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 -> { @@ -314,7 +315,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)) } @@ -325,7 +326,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) @@ -342,18 +345,22 @@ 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) + store.dispatch( + GlobalAction.Onboarding.StartForUnfinishedBackup( + 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()) } } } @@ -396,9 +403,12 @@ private fun initSaltPayOnBackupFinishedIfNeeded( if (onboardingWalletState.isSaltPay && onboardingWalletState.onboardingSaltPayState == null) { if (scanResponse == null) throw IllegalArgumentException() - val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse) - store.dispatchOnMain(OnboardingSaltPayAction.SetDependencies(manager, config)) - store.dispatchOnMain(OnboardingSaltPayAction.Update) + val manager = SaltPayActivationManagerFactory( + blockchain = scanResponse.getBlockchain(), + card = scanResponse.card, + ).create() + store.dispatchOnMain(OnboardingSaltPayAction.SetDependencies(manager)) + store.dispatchOnMain(OnboardingSaltPayAction.Update(false)) } } 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 8cca79e9dd..cb5242af39 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 @@ -43,7 +43,7 @@ private class ReducerForGlobalAction { is GlobalAction.Onboarding.Start -> { OnboardingWalletState( backupState = state.backupState.copy( - maxBackupCards = if (action.scanResponse.isSaltPay()) 1 else 2, + maxBackupCards = maxBackupCards(action.scanResponse.isSaltPay()), canSkipBackup = if (action.scanResponse.isSaltPay()) false else action.canSkipBackup, ), isSaltPay = action.scanResponse.isSaltPay(), @@ -52,15 +52,17 @@ private class ReducerForGlobalAction { is GlobalAction.Onboarding.StartForUnfinishedBackup -> { OnboardingWalletState( backupState = state.backupState.copy( - maxBackupCards = 2, + maxBackupCards = maxBackupCards(action.isSaltPayVisa), canSkipBackup = false, ), - isSaltPay = false, + isSaltPay = action.isSaltPayVisa, ) } else -> state } } + + private fun maxBackupCards(isSaltPay: Boolean): Int = if (isSaltPay) 1 else 2 } } 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 4e3cad3657..314d4ea06b 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 @@ -60,7 +60,7 @@ data class OnboardingWalletState( -> 9 SaltPayActivationStep.Claim -> 10 SaltPayActivationStep.ClaimInProgress -> 11 - SaltPayActivationStep.ClaimSuccess, SaltPayActivationStep.Success, SaltPayActivationStep.Finished, + SaltPayActivationStep.ClaimSuccess, SaltPayActivationStep.Success, -> getMaxProgress() } } 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 ddf80bdc07..85a64112cb 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,12 @@ class GnosisRegistrator( return Result.Failure(BlockchainSdkError.WrappedThrowable(it.error)) } } + + // return transferFromStandardWay(amountToClaim, signer) + return transferFromHardcodeWay(amountToClaim, signer) + } + + 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 +175,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) + } + } + private fun Result>.extractFeeAmount(): Result { return when (this) { is Result.Success -> { 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 ff6ec003d6..1ae879918a 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,20 @@ 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.services.Result import com.tangem.datasource.api.paymentology.AttestationResponse @@ -18,12 +24,15 @@ import com.tangem.datasource.api.paymentology.RegisterWalletRequest import com.tangem.datasource.api.paymentology.RegisterWalletResponse import com.tangem.datasource.api.paymentology.RegistrationResponse import com.tangem.datasource.api.paymentology.tryExtractError +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 java.math.BigDecimal /** @@ -200,4 +209,96 @@ 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, + ) + } + + 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/SaltPayConfig.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayConfig.kt index 19048168de..1bb872eebd 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayConfig.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayConfig.kt @@ -1,22 +1,24 @@ package com.tangem.tap.features.onboarding.products.wallet.saltPay -import com.tangem.tap.common.zendesk.ZendeskConfig +import com.tangem.tap.common.chat.SprinklrConfig 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/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 0f0b207431..e215cbfe92 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 @@ -17,8 +19,15 @@ class SaltPayExceptionHandler { 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 579f34d614..a75c0e9e5c 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 @@ -19,13 +19,15 @@ class NoFundsForActivationDialog { return AlertDialog.Builder(context).apply { setTitle(R.string.saltpay_error_no_gas_title) setMessage(R.string.saltpay_error_no_gas_message) - //TODO: SaltPay: change onboarding_supplement_button_kyc_waiting -> to appropriate string - setPositiveButton(R.string.onboarding_supplement_button_kyc_waiting) { _, _ -> - val config = store.state.globalState.configManager?.config?.saltPayConfig?.zendesk.guard { - store.dispatchDebugErrorNotification("SaltPayConfig not initialized") - return@setPositiveButton - } - store.dispatch(GlobalAction.OpenChat(SupportInfo(), config)) + setPositiveButton(R.string.chat_button_title) { _, _ -> + 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 a2194e9568..1fbb284524 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.tap.features.onboarding.products.wallet.saltPay.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() } 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 dfdfb619b5..33e9d7fa1b 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 @@ -79,6 +79,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 @@ -90,7 +93,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 -> { @@ -137,6 +151,7 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App return@launch } + Analytics.send(Onboarding.PinCodeSet()) handleInProgress = false dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.KycIntro)) } @@ -160,13 +175,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) { @@ -220,54 +235,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 -> { @@ -276,6 +285,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 +387,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 71570fd52e..e6ea4af732 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 @@ -21,7 +21,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 a14ad10252..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.domain.common.SaltPayWorkaround -import com.tangem.domain.common.ScanResponse -import com.tangem.datasource.api.paymentology.PaymentologyApiService 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.tap.features.onboarding.products.wallet.saltPay.KYCProvider import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager -import com.tangem.tap.features.onboarding.products.wallet.saltPay.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, @@ -37,57 +27,7 @@ data class OnboardingSaltPayState( val mainButtonState: WidgetState get() = if (inProgress) ProgressState.Loading else ProgressState.Done - fun readyToClaim(): Boolean = amountToClaim != null - 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.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 { @@ -103,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 84e012cad9..812151698a 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,6 +80,7 @@ internal class OnboardingSaltPayView( if (state.backupState.backupStep == BackupStep.Finished) { // 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) @@ -99,7 +100,7 @@ internal class OnboardingSaltPayView( } else { walletFragment.loadImageIntoImageView(state.onboardingSaltPayState.saltPayCardArtworkUrl, 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 @@ -228,7 +229,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) } @@ -244,7 +245,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..1505a42f3b --- /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.tap.common.chat.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..b9b2b41cde --- /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.tap.common.chat.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..28b3053441 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/sprinklr/ui/SprinklrActivity.kt @@ -0,0 +1,42 @@ +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(modifier: Modifier, state: SprinklrScreenState) { + 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 93066fc926..aa6d1ea2d8 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 @@ -55,7 +55,6 @@ import com.tangem.domain.redux.domainStore import com.tangem.tap.common.compose.AddCustomTokenWarning import com.tangem.tap.common.compose.ClosePopupTrigger import com.tangem.tap.common.compose.ComposeDialogManager -import com.tangem.tap.common.compose.ToggledRippleTheme import com.tangem.tap.common.compose.keyboardAsState import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestCase @@ -66,7 +65,7 @@ import kotlinx.coroutines.launch /** [REDACTED_AUTHOR] */ -private class AddCustomTokenScreen {} // for simple search +private class AddCustomTokenScreen // for simple search @OptIn(ExperimentalMaterialApi::class) @Composable @@ -77,7 +76,7 @@ fun AddCustomTokenScreen( val selectedTestCase = remember { mutableStateOf(TestCase.ContractAddress) } val bottomSheetScaffoldState = rememberBottomSheetScaffoldState( - bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed) + bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed), ) val coroutineScope = rememberCoroutineScope() val toggleBottomSheet = { coroutineScope.launch { bottomSheetScaffoldState.toggle() } } @@ -91,11 +90,13 @@ fun AddCustomTokenScreen( }, sheetPeekHeight = 0.dp, ) { - Column() { - TestCasesList(onItemClick = { - selectedTestCase.value = it - toggleBottomSheet() - }) + Column { + TestCasesList( + onItemClick = { + selectedTestCase.value = it + toggleBottomSheet() + }, + ) ScreenContent(state, closePopupTrigger) } } @@ -144,7 +145,7 @@ private fun ScreenContent( Column( modifier = Modifier .fillMaxWidth() - .padding(16.dp) + .padding(16.dp), ) { FormFields(state, closePopupTrigger) } @@ -195,7 +196,7 @@ fun Warnings(warnings: List) { AddCustomTokenWarning( modifier = modifier.fillMaxWidth(), warning = item, - converter = warningConverter + converter = warningConverter, ) } } @@ -206,7 +207,7 @@ private fun AddButton(state: MutableState) { AddCustomTokenFab( modifier = Modifier .widthIn(210.dp, 280.dp), - isEnabled = state.value.screenState.addButton.isEnabled + isEnabled = state.value.screenState.addButton.isEnabled, ) { domainStore.dispatch(AddCustomTokenAction.OnAddCustomTokenClicked) } } @@ -214,7 +215,7 @@ private fun AddButton(state: MutableState) { private fun AddCustomTokenFab( modifier: Modifier = Modifier, isEnabled: Boolean = true, - onClick: () -> Unit + onClick: () -> Unit, ) { val contentColor = Color.White val backgroundColor = if (isEnabled) { @@ -228,42 +229,40 @@ private fun AddCustomTokenFab( FloatingActionButtonDefaults.elevation() } - ToggledRippleTheme(isEnabled) { - ExtendedFloatingActionButton( - modifier = modifier, - icon = { - Icon( - imageVector = Icons.Filled.Add, - tint = contentColor, - contentDescription = "Add", - ) - }, - text = { Text(text = stringResource(id = R.string.common_add)) }, - onClick = { if (isEnabled) onClick() }, - backgroundColor = backgroundColor, - contentColor = contentColor, - elevation = elevation, - ) - } + ExtendedFloatingActionButton( + modifier = modifier, + icon = { + Icon( + imageVector = Icons.Filled.Add, + tint = contentColor, + contentDescription = "Add", + ) + }, + text = { Text(text = stringResource(id = R.string.common_add)) }, + onClick = { if (isEnabled) onClick() }, + backgroundColor = backgroundColor, + contentColor = contentColor, + elevation = elevation, + ) } data class ScreenFieldData( val field: DataField<*>, val error: AddCustomTokenError?, val errorConverter: ModuleMessageConverter, - val viewState: ViewStates.TokenField + val viewState: ViewStates.TokenField, ) { companion object { fun fromState( field: DataField<*>, state: AddCustomTokenState, - errorConverter: ModuleMessageConverter + errorConverter: ModuleMessageConverter, ): ScreenFieldData { return ScreenFieldData( field = field, error = state.getError(field.id), errorConverter = errorConverter, - viewState = selectField(field.id, state.screenState) + viewState = selectField(field.id, state.screenState), ) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt index dc3e074fac..af3edbfebd 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt @@ -2,7 +2,6 @@ package com.tangem.tap.features.wallet.redux import android.content.Context import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager @@ -98,8 +97,6 @@ sealed class WalletAction : Action { data class RemoveWallet(val currency: Currency) : MultiWallet() data class RemoveWallets(val currencies: List) : MultiWallet() - data class SetPrimaryBlockchain(val blockchain: Blockchain) : MultiWallet() - data class SetPrimaryToken(val token: Token) : MultiWallet() data class ShowWalletBackupWarning(val show: Boolean) : MultiWallet() object BackupWallet : MultiWallet() object ScheduleCheckForMissingDerivation : MultiWallet() 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 0566b5d4a4..7e7f8cc3c2 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 @@ -3,6 +3,7 @@ package com.tangem.tap.features.wallet.redux import android.graphics.Bitmap import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.address.AddressType @@ -12,6 +13,7 @@ import com.tangem.tap.common.extensions.toQrCode import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.common.toggleWidget.WidgetState import com.tangem.tap.domain.configurable.warningMessage.WarningMessage +import com.tangem.tap.domain.getFirstToken import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState import com.tangem.tap.features.wallet.models.Currency @@ -43,8 +45,6 @@ data class WalletState( val isMultiwalletAllowed: Boolean = false, val cardCurrency: CryptoCurrencyName? = null, val selectedCurrency: Currency? = null, - val primaryBlockchain: Blockchain? = null, - val primaryToken: Token? = null, val isTestnet: Boolean = false, val totalBalance: TotalBalance? = null, val showBackupWarning: Boolean = false, @@ -81,13 +81,30 @@ data class WalletState( val walletManagers: List get() = walletsStores.mapNotNull { it.walletManager } - val primaryWallet: WalletData? = walletsStores.firstOrNull()?.walletsData?.firstOrNull() + private val primaryWalletStore: WalletStore? + get() = if (isMultiwalletAllowed || walletsStores.isEmpty() || walletsStores.size > 1) null + else walletsStores[0] - val primaryWalletManager: WalletManager? = if (walletsStores.isNotEmpty()) walletsStores[0].walletManager else null + val primaryWalletManager: WalletManager? + get() = primaryWalletStore?.walletManager + + val primaryWalletData: WalletData? + get() = primaryWalletStore?.walletsData?.firstOrNull() + + val primaryBlockchain: Blockchain? + get() = primaryWalletManager?.wallet?.blockchain + + val primaryToken: Token? + get() = primaryWalletManager?.wallet?.getFirstToken() + + val primaryTokenData: WalletData? + get() = primaryWalletStore?.walletsData?.toMutableList() + ?.apply { remove(primaryWalletData) } + ?.firstOrNull() val shouldShowDetails: Boolean = - primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard && - primaryWallet?.currencyData?.status != BalanceStatus.UnknownBlockchain + primaryWalletData?.currencyData?.status != BalanceStatus.EmptyCard && + primaryWalletData?.currencyData?.status != BalanceStatus.UnknownBlockchain val hasSavedWallets: Boolean get() = userWalletsListManager.hasSavedUserWallets @@ -138,13 +155,6 @@ data class WalletState( return getWalletStore(currency)?.walletsData?.firstOrNull { it.currency == currency } } - private fun isPrimaryCurrency(walletData: WalletData): Boolean { - return (walletData.currency is Currency.Blockchain && - walletData.currency.blockchain == store.state.walletState.primaryBlockchain) - || (walletData.currency is Currency.Token && - walletData.currency.token == store.state.walletState.primaryToken) - } - fun replaceWalletStoreInWalletsStores(wallet: WalletStore?): List { if (wallet == null) return walletsStores var changed = false @@ -311,11 +321,13 @@ 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" } } 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/middlewares/Mapper.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/Mapper.kt index 155ce6ebd7..518b4aba41 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 @@ -1,6 +1,5 @@ package com.tangem.tap.features.wallet.redux.middlewares -import com.tangem.blockchain.common.Blockchain import com.tangem.common.extensions.isZero import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.common.extensions.toFiatRateString @@ -19,7 +18,6 @@ import com.tangem.tap.features.wallet.redux.WalletMainButton import com.tangem.tap.features.wallet.redux.WalletStore import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.features.wallet.ui.BalanceWidgetData -import com.tangem.tap.features.wallet.ui.TokenData import com.tangem.tap.store import java.math.BigDecimal @@ -49,15 +47,6 @@ internal fun WalletStoreModel.mapToReduxModel(): WalletStore { walletsData = walletsData.mapToReduxModels(walletRent, appCurrencySymbol), ) .updateTokenModels(blockchainWalletData.status.amount) - .setupIfHadCardSingleToken( - blockchain = blockchain, - walletsDataModel = walletsData, - appCurrencySymbol = appCurrencySymbol, - blockchainWalletData = blockchainWalletData.mapToReduxModel( - walletRent = walletRent, - appCurrencySymbol = appCurrencySymbol, - ), - ) } private fun List.mapToReduxModels( @@ -122,12 +111,12 @@ private fun WalletDataModel.mapToReduxModel( amountFormatted = amountFormatted, fiatAmount = fiatAmount, fiatAmountFormatted = fiatAmountFormatted, - token = null, amountToCreateAccount = (status as? WalletDataModel.NoAccount) ?.amountToCreateAccount ?.toString(), errorMessage = status.errorMessage, ), + historyTransactions = historyTransactions, ) } @@ -146,45 +135,4 @@ private fun WalletStore.updateTokenModels(blockchainAmount: BigDecimal): WalletS ) } return updateWallets(updatedTokensWalletData) -} - -private fun WalletStore.setupIfHadCardSingleToken( - blockchain: Blockchain, - walletsDataModel: List, - appCurrencySymbol: String, - blockchainWalletData: WalletData, -): WalletStore { - // Card with single token contains only 2 model - blockchain and token - if (walletsData.size != 2) return this - - val cardSingleTokenWalletData = walletsDataModel.firstOrNull { - it.currency.isToken() && it.currency.blockchain == blockchain && it.isCardSingleToken - } ?: return this - - val blockchainWalletDataWithSingleToken = blockchainWalletData.copy( - currencyData = blockchainWalletData.currencyData.copy( - token = cardSingleTokenWalletData.toTokenData(appCurrencySymbol), - ), - ) - return updateWallets(listOf(blockchainWalletDataWithSingleToken)) -} - -private fun WalletDataModel.toTokenData(appCurrencySymbol: String): TokenData { - val amount = status.amount - val fiatAmount = fiatRate?.let { status.amount.toFiatValue(it) } - - return TokenData( - amount = amount, - amountFormatted = amount.toFormattedCurrencyString( - decimals = currency.decimals, - currency = currency.currencySymbol, - ), - fiatAmount = fiatAmount, - fiatAmountFormatted = fiatAmount - ?.takeIf { !status.isErrorStatus } - ?.toFormattedFiatValue(appCurrencySymbol), - tokenSymbol = currency.currencySymbol, - fiatRate = fiatRate, - fiatRateString = fiatRate?.toFiatRateString(appCurrencySymbol), - ) } \ No newline at end of file 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 33b7f54788..e896251b09 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 @@ -2,6 +2,7 @@ package com.tangem.tap.features.wallet.redux.middlewares 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.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Token @@ -17,6 +18,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 @@ -40,10 +42,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 @@ -87,12 +88,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( @@ -107,6 +108,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 03b26092b2..22aba65cfc 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 @@ -49,6 +50,7 @@ import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletData import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.WalletStore +import com.tangem.tap.features.wallet.redux.reducers.findSelectedCurrency import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.network.NetworkStateChanged import com.tangem.tap.preferencesStorage @@ -153,7 +155,7 @@ class WalletMiddleware { if (walletState.isMultiwalletAllowed) { walletState.walletsDataFromStores.map { it.currency } } else { - val derivationPath = walletState.primaryWallet?.currency?.derivationPath + val derivationPath = walletState.primaryWalletData?.currency?.derivationPath val primaryBlockchain = walletState.primaryBlockchain val primaryToken = walletState.primaryToken listOfNotNull( @@ -248,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) } @@ -310,15 +314,14 @@ class WalletMiddleware { } } - private fun updateWalletStores(wallStores: List, state: WalletState) { + private fun updateWalletStores(walletsStores: List, state: WalletState) { scope.launch(Dispatchers.Default) { + val reduxWalletStores = walletsStores.mapToReduxModels() if (!state.isMultiwalletAllowed) { - wallStores.firstOrNull()?.walletsData?.firstOrNull()?.let { - store.dispatchOnMain(WalletAction.MultiWallet.SetSingleWalletCurrency(it.currency)) + findSelectedCurrency(reduxWalletStores, null, false)?.let { + store.dispatchOnMain(WalletAction.MultiWallet.SetSingleWalletCurrency(it)) } } - - val reduxWalletStores = wallStores.mapToReduxModels() store.dispatchOnMain( WalletAction.WalletStoresChanged.UpdateWalletStores( reduxWalletStores = reduxWalletStores, @@ -376,7 +379,7 @@ class WalletMiddleware { } else -> { Analytics.send(MainScreen.ButtonScanCard()) - store.dispatch(WalletAction.Scan(MainScreen.CardWasScanned())) + store.dispatch(WalletAction.Scan(Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Main))) } } } @@ -438,11 +441,7 @@ class WalletMiddleware { walletStore: WalletStore?, ): PrepareSendScreen { val coinRate = state?.getWalletData(walletStore?.blockchainNetwork)?.fiatRate - val tokenRate = if (state?.isMultiwalletAllowed == true) { - selectedWalletData?.fiatRate - } else { - selectedWalletData?.currencyData?.token?.fiatRate - } + val tokenRate = selectedWalletData?.fiatRate val coinAmount = walletStore?.walletManager?.wallet?.amounts?.get(AmountType.Coin) return PrepareSendScreen( 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 711373d52f..7fb4fb8c8a 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,11 +6,12 @@ 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 import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.getFirstToken import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.WalletRent @@ -26,7 +27,6 @@ import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT import com.tangem.tap.features.wallet.redux.WalletStore import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.features.wallet.ui.BalanceWidgetData -import com.tangem.tap.features.wallet.ui.TokenData import com.tangem.tap.store import com.tangem.tap.userWalletsListManager import com.tangem.wallet.R @@ -42,17 +42,11 @@ class MultiWalletReducer { (it.wallet.publicKey.derivationPath?.rawPath == blockchain.derivationPath) } val wallet = walletManager?.wallet - val cardToken = if (!state.isMultiwalletAllowed) { - wallet?.getFirstToken()?.symbol?.let { TokenData("", tokenSymbol = it) } - } else { - null - } val walletData = WalletData( currencyData = BalanceWidgetData( status = BalanceStatus.Loading, currency = blockchain.blockchain.fullName, currencySymbol = blockchain.blockchain.currency, - token = cardToken, ), walletAddresses = createAddressList(wallet), mainButton = WalletMainButton.SendButton(false), @@ -61,23 +55,23 @@ class MultiWalletReducer { blockchain.derivationPath, ), existentialDepositString = getExistentialDeposit(walletManager), + historyTransactions = walletManager?.getBlockchainTxHistory(), ) WalletStore( walletManager = walletManager, blockchainNetwork = blockchain, - walletsData = listOf(walletData), + walletsData = listOfNotNull(walletData), ) } - val selectedCurrency = if (state.isMultiwalletAllowed) { - state.selectedCurrency - } else { - walletStores.firstOrNull()?.walletsData?.firstOrNull()?.currency - } state.copy( walletsStores = walletStores, - selectedCurrency = selectedCurrency, + selectedCurrency = findSelectedCurrency( + walletsStores = walletStores, + currentSelectedCurrency = state.selectedCurrency, + isMultiWalletAllowed = state.isMultiwalletAllowed, + ), ) } is WalletAction.MultiWallet.AddBlockchain -> { @@ -97,6 +91,7 @@ class MultiWalletReducer { action.blockchain.derivationPath, ), existentialDepositString = getExistentialDeposit(walletManager), + historyTransactions = walletManager?.getBlockchainTxHistory(), ) val walletStore = WalletStore( walletManager = walletManager, @@ -160,6 +155,7 @@ class MultiWalletReducer { derivationPath = action.blockchain.derivationPath, ), walletRent = findWalletRent(state.getWalletStore(walletManager.wallet)), + historyTransactions = walletManager.getTokenTxHistory(action.token), ) state.updateWalletData(newTokenWalletData) } @@ -181,8 +177,6 @@ class MultiWalletReducer { action.currencies.forEach { updatedState = updatedState.removeWalletData(state.getWalletData(it)) } updatedState } - is WalletAction.MultiWallet.SetPrimaryBlockchain -> state.copy(primaryBlockchain = action.blockchain) - is WalletAction.MultiWallet.SetPrimaryToken -> state.copy(primaryToken = action.token) is WalletAction.MultiWallet.SaveCurrencies -> state is WalletAction.MultiWallet.ShowWalletBackupWarning -> state.copy(showBackupWarning = action.show) is WalletAction.MultiWallet.ScheduleCheckForMissingDerivation -> state.copy( @@ -190,7 +184,7 @@ class MultiWalletReducer { ) is WalletAction.MultiWallet.AddMissingDerivations -> state.copy( missingDerivations = action.blockchains, - derivationsCheckIsScheduled = false + derivationsCheckIsScheduled = false, ) is WalletAction.MultiWallet.BackupWallet -> state is WalletAction.MultiWallet.ScanToGetDerivations -> state.copy(state = ProgressState.Loading) @@ -212,12 +206,11 @@ class MultiWalletReducer { } fun Token.toWallet(state: WalletState, blockchain: BlockchainNetwork): WalletData? { - if (!state.isMultiwalletAllowed) return null 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( @@ -228,5 +221,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/OnWalletLoadedReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt index 68e6080883..4648ebdd54 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt @@ -3,11 +3,9 @@ package com.tangem.tap.features.wallet.redux.reducers import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Wallet import com.tangem.common.extensions.isZero -import com.tangem.tap.common.extensions.toFiatString import com.tangem.tap.common.extensions.toFiatValue import com.tangem.tap.common.extensions.toFormattedCurrencyString import com.tangem.tap.common.extensions.toFormattedFiatValue -import com.tangem.tap.domain.getFirstToken import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.filterByToken @@ -20,9 +18,7 @@ import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT import com.tangem.tap.features.wallet.redux.replaceSomeWalletsData import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.features.wallet.ui.BalanceWidgetData -import com.tangem.tap.features.wallet.ui.TokenData import com.tangem.tap.store -import java.math.BigDecimal class OnWalletLoadedReducer { @@ -37,7 +33,7 @@ class OnWalletLoadedReducer { private fun onMultiWalletLoaded( wallet: Wallet, blockchainNetwork: BlockchainNetwork, - walletState: WalletState + walletState: WalletState, ): WalletState { val walletData = walletState.getWalletData(blockchainNetwork) ?: return walletState @@ -45,7 +41,7 @@ class OnWalletLoadedReducer { val coinAmountValue = wallet.amounts[AmountType.Coin]?.value val formattedAmount = coinAmountValue?.toFormattedCurrencyString( wallet.blockchain.decimals(), - wallet.blockchain.currency + wallet.blockchain.currency, ) val pendingTransactions = wallet.getPendingTransactions() @@ -98,7 +94,7 @@ class OnWalletLoadedReducer { amount = tokenAmountValue, amountFormatted = tokenAmountValue?.toFormattedCurrencyString( token.decimals, - token.symbol + token.symbol, ), fiatAmount = tokenFiatAmount, fiatAmountFormatted = tokenFiatAmountFormatted, @@ -117,33 +113,12 @@ class OnWalletLoadedReducer { if (wallet.blockchain != walletState.primaryBlockchain) return walletState val fiatCurrencyName = store.state.globalState.appCurrency.code - val token = wallet.getFirstToken() - val tokenData = if (token != null) { - val tokenAmount = wallet.getTokenAmount(token) - if (tokenAmount != null) { - val tokenFiatRate = walletState.primaryWallet?.currencyData?.token?.fiatRate - val tokenFiatAmount = - tokenFiatRate?.let { tokenAmount.value?.toFiatString(it, fiatCurrencyName) } - TokenData( - amount = tokenAmount.value ?: BigDecimal.ZERO, - tokenSymbol = tokenAmount.currencySymbol, fiatAmountFormatted = tokenFiatAmount, - fiatAmount = tokenFiatRate?.let { tokenAmount.value?.toFiatValue(tokenFiatRate) }, - amountFormatted = tokenAmount.value?.toFormattedCurrencyString( - token.decimals, token.symbol, - ) ?: "", - ) - } else { - null - } - } else { - null - } val amount = wallet.amounts[AmountType.Coin]?.value val formattedAmount = amount?.toFormattedCurrencyString( wallet.blockchain.decimals(), - wallet.blockchain.currency + wallet.blockchain.currency, ) - val fiatAmount = walletState.primaryWallet?.fiatRate?.let { amount?.toFiatValue(it) } + val fiatAmount = walletState.primaryWalletData?.fiatRate?.let { amount?.toFiatValue(it) } val fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(fiatCurrencyName) ?: UNKNOWN_AMOUNT_SIGN val pendingTransactions = wallet.getPendingTransactions() @@ -153,16 +128,15 @@ class OnWalletLoadedReducer { } else { BalanceStatus.VerifiedOnline } - val walletData = walletState.primaryWallet?.copy( + val walletData = walletState.primaryWalletData?.copy( currencyData = BalanceWidgetData( balanceStatus, wallet.blockchain.fullName, currencySymbol = wallet.blockchain.currency, - token = tokenData, blockchainAmount = amount, amount = amount, amountFormatted = formattedAmount, fiatAmount = fiatAmount, - fiatAmountFormatted = fiatAmountFormatted + fiatAmountFormatted = fiatAmountFormatted, ), pendingTransactions = pendingTransactions.removeUnknownTransactions(), mainButton = WalletMainButton.SendButton(sendButtonEnabled), @@ -171,7 +145,8 @@ class OnWalletLoadedReducer { val updatedStore = walletState.getWalletStore(walletData?.currency)?.updateWallets(wallets) return walletState.updateWalletStore(updatedStore).copy( - state = ProgressState.Done, error = null + state = ProgressState.Done, + error = null, ) } } \ 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 08a7def4bd..9a8f4ded04 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 2505702c20..2653083ecf 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 @@ -9,7 +9,6 @@ import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.TwinCardNumber import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.extensions.toFiatRateString -import com.tangem.tap.common.extensions.toFiatString import com.tangem.tap.common.extensions.toFiatValue import com.tangem.tap.common.extensions.toFormattedCurrencyString import com.tangem.tap.common.extensions.toFormattedFiatValue @@ -17,7 +16,6 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.getArtworkUrl import com.tangem.tap.domain.extensions.isMultiwalletAllowed -import com.tangem.tap.domain.getFirstToken import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.WalletRent @@ -36,7 +34,6 @@ import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.features.wallet.ui.BalanceWidgetData import com.tangem.tap.proxy.AppStateHolder import org.rekotlin.Action -import timber.log.Timber import java.math.BigDecimal class WalletReducer { @@ -285,8 +282,9 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS newState = newState.copy(cardImage = cardImage) } - is WalletAction.LoadFiatRate.Success -> + is WalletAction.LoadFiatRate.Success -> { newState = setNewFiatRate(action.fiatRates, state.globalState.appCurrency, newState) + } is WalletAction.LoadArtwork -> { val artworkUrl = action.card.getArtworkUrl(action.artworkId) ?: when (state.twinCardsState.cardNumber) { @@ -352,6 +350,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, ) } @@ -361,18 +360,13 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS ) } is WalletAction.LoadData.Success -> { - val selectedCurrency = if (newState.isMultiwalletAllowed) { - newState.selectedCurrency - } else { - newState.walletsStores.firstOrNull() - ?.walletsData - ?.firstOrNull() - ?.currency - } - newState = newState.copy( state = ProgressState.Done, - selectedCurrency = selectedCurrency, + selectedCurrency = findSelectedCurrency( + walletsStores = newState.walletsStores, + currentSelectedCurrency = newState.selectedCurrency, + isMultiWalletAllowed = newState.isMultiwalletAllowed, + ), ) } else -> Unit @@ -381,6 +375,19 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS return newState } +fun findSelectedCurrency( + walletsStores: List, + currentSelectedCurrency: Currency?, + isMultiWalletAllowed: Boolean, +): Currency? = if (isMultiWalletAllowed) { + currentSelectedCurrency +} else { + walletsStores.firstOrNull() + ?.walletsData + ?.firstOrNull() + ?.currency +} + private fun CardDTO.findCardsCount(): Int? { return (this.backupStatus as? CardDTO.BackupStatus.Active)?.cardCount?.inc() ?.takeIf { this.isMultiwalletAllowed } @@ -439,45 +446,18 @@ private fun setNewFiatRate( state: WalletState, ): WalletState { val rateFormatter: (BigDecimal) -> String = { rate: BigDecimal -> - rate.toFiatRateString( - fiatCurrencyName = appCurrency.symbol, - ) + rate.toFiatRateString(fiatCurrencyName = appCurrency.symbol) } - return if (state.isMultiwalletAllowed) { - setMultiWalletFiatRate( - fiatRates = fiatRates.mapNotNullValues { it.value }, - rateFormatter = rateFormatter, - appCurrency = appCurrency, - state = state, - ) - } else { - setSingleWalletFiatRates( - fiatRates = fiatRates.mapNotNullValues { it.value }, - rateFormatter = rateFormatter, - appCurrency = appCurrency, - state = state, - ) - } -} - -private fun setMultiWalletFiatRate( - fiatRates: Map, - rateFormatter: (BigDecimal) -> String, - appCurrency: FiatCurrency, - state: WalletState, -): WalletState { - val newWalletsData = fiatRates.mapNotNull { (currency, rate) -> + val newWalletsData = fiatRates.mapNotNullValues { it.value }.mapNotNull { (currency, rate) -> val walletStore = state.getWalletStore(currency) ?: return@mapNotNull null val wallet = walletStore.walletManager?.wallet val walletData = state.getWalletData(currency) ?: return@mapNotNull null val currencyData = walletData.currencyData var fiatAmount = when (currency) { - is Currency.Blockchain -> - wallet?.amounts?.get(AmountType.Coin)?.value?.toFiatValue(rate) - is Currency.Token -> - wallet?.getTokenAmount(currency.token)?.value?.toFiatValue(rate) + is Currency.Blockchain -> wallet?.amounts?.get(AmountType.Coin)?.value?.toFiatValue(rate) + is Currency.Token -> wallet?.getTokenAmount(currency.token)?.value?.toFiatValue(rate) } if (currencyData.status == BalanceStatus.NoAccount && fiatAmount == null) { fiatAmount = BigDecimal.ZERO.setScale(2) @@ -494,96 +474,5 @@ private fun setMultiWalletFiatRate( ) } - return state - .updateWalletsData(newWalletsData) -} - -private fun setSingleWalletFiatRates( - fiatRates: Map, - appCurrency: FiatCurrency, - rateFormatter: (BigDecimal) -> String, - state: WalletState, -): WalletState { - val blockchainFiatRate = fiatRates.entries.firstOrNull { it.key.isBlockchain() } - val tokenFiatRate = fiatRates.entries.firstOrNull { it.key.isToken() } - Timber.e("Token Fiat Rate is ${tokenFiatRate ?: "NULL"}") - val updatedState = updateStateWithFiatRate(blockchainFiatRate?.toPair(), appCurrency, rateFormatter, state) - return updateStateWithFiatRate(tokenFiatRate?.toPair(), appCurrency, rateFormatter, updatedState) -} - -private fun updateStateWithFiatRate( - fiatRate: Pair?, - appCurrency: FiatCurrency, - rateFormatter: (BigDecimal) -> String, - state: WalletState, -): WalletState { - return if (fiatRate != null) { - val currency = fiatRate.first - val rate = fiatRate.second - - setSingleWalletFiatRate( - rate = rate, - rateFormatted = rateFormatter(rate), - currency = currency, - appCurrency = appCurrency, - state = state, - ) - } else { - state - } -} - -private fun setSingleWalletFiatRate( - rate: BigDecimal, - rateFormatted: String, - currency: Currency, - appCurrency: FiatCurrency, - state: WalletState, -): WalletState { - val wallet = state.primaryWalletManager?.wallet ?: return state - val token = wallet.getFirstToken() - Timber.e("Working with currency: ${currency.currencyName}") - - if (currency == state.primaryWallet?.currency) { - val fiatAmount = wallet.amounts[AmountType.Coin]?.value - ?.toFiatString(rate, appCurrency.code) - val walletData = state.primaryWallet.copy( - currencyData = state.primaryWallet.currencyData.copy(fiatAmountFormatted = fiatAmount), - fiatRate = rate, - fiatRateString = rateFormatted, - ) - return state.updateWalletData(walletData) - } else if (currency is Currency.Token && currency.token == token) { - Timber.e("Working with token fiat rate") - - val tokenFiatAmount = wallet.getTokenAmount(token) - ?.value - val tokenAmountFormatted = tokenFiatAmount?.toFiatString(rate, appCurrency.code) - - val tokenData = state.primaryWallet?.currencyData?.token?.copy( - fiatAmountFormatted = tokenAmountFormatted, - fiatAmount = tokenFiatAmount, - fiatRate = rate, - fiatRateString = rateFormatted, - ) - // ?: TokenData( - // fiatAmountFormatted = tokenAmountFormatted, - // fiatAmount = tokenFiatAmount, - // fiatRate = rate, - // fiatRateString = rateFormatted, - // amount = "", - // tokenSymbol = currency.currencySymbol - // ) - - Timber.e("Token Data is ${tokenData ?: "NULL"}") - - val walletData = state.primaryWallet?.copy( - currencyData = state.primaryWallet.currencyData.copy( - token = tokenData, - ), - ) - Timber.e("Wallet Data is ${walletData ?: "NULL"}") - return state.updateWalletData(walletData) - } - return state + return state.updateWalletsData(newWalletsData) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt index 4afc4c08db..9ffd48f58e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt @@ -24,31 +24,20 @@ data class BalanceWidgetData( val status: BalanceStatus? = null, val currency: String? = null, val currencySymbol: String? = null, - val blockchainAmount: BigDecimal? = BigDecimal.ZERO, val amount: BigDecimal? = null, val amountFormatted: String? = null, val fiatAmount: BigDecimal? = null, val fiatAmountFormatted: String? = null, - val token: TokenData? = null, + val blockchainAmount: BigDecimal? = BigDecimal.ZERO, val amountToCreateAccount: String? = null, - val errorMessage: String? = null + val errorMessage: String? = null, ) -data class TokenData( - val amountFormatted: String?, - val amount: BigDecimal? = null, - val tokenSymbol: String, - val fiatAmountFormatted: String? = null, - val fiatAmount: BigDecimal? = null, - val fiatRateString: String? = null, - val fiatRate: BigDecimal? = null, -) - - class BalanceWidget( private val binding: CardBalanceBinding, private val fragment: WalletFragment, private val data: BalanceWidgetData, + private val token: BalanceWidgetData?, private val isTwinCard: Boolean, ) { @@ -68,7 +57,7 @@ class BalanceWidget( showStatus(R.id.tv_status_loading) - if (data.token != null) { + if (token != null) { showBalanceWithToken(data, false) } else { showBalanceWithoutToken(data, false) @@ -87,7 +76,7 @@ class BalanceWidget( showStatus(statusView) tvStatusErrorMessage.hide() - if (data.token != null) { + if (token != null) { showBalanceWithToken(data, true) } else { showBalanceWithoutToken(data, true) @@ -99,7 +88,7 @@ class BalanceWidget( tvFiatAmount.hide() groupBaseCurrency.hide() - val currency = if (data.token != null) data.token.tokenSymbol else data.currency + val currency = if (token != null) token.currencySymbol else data.currency tvCurrency.text = currency tvAmount.text = "" @@ -130,7 +119,8 @@ class BalanceWidget( tvErrorDescriptions.text = fragment.getString( R.string.no_account_generic, - data.amountToCreateAccount, data.currencySymbol + data.amountToCreateAccount, + data.currencySymbol, ) } BalanceStatus.UnknownBlockchain -> with(binding.lBalanceError) { @@ -152,13 +142,13 @@ class BalanceWidget( private fun showBalanceWithToken(data: BalanceWidgetData, showAmount: Boolean) = with(binding.lBalance) { groupBaseCurrency.show() - tvCurrency.text = data.token?.tokenSymbol + tvCurrency.text = token?.currencySymbol tvBaseCurrency.text = data.currency - tvAmount.text = if (showAmount) data.token?.amountFormatted else "" + tvAmount.text = if (showAmount) token?.amountFormatted else "" tvBaseAmount.text = if (showAmount) data.amountFormatted else "" if (showAmount) { tvFiatAmount.show() - tvFiatAmount.text = data.token?.fiatAmountFormatted + tvFiatAmount.text = token?.fiatAmountFormatted } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt index e38d9d947b..ba1fd0b30f 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -435,7 +435,8 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), lBalanceError.tvErrorDescriptions.text = getString( R.string.no_account_generic, - data.amountToCreateAccount, data.currencySymbol, + data.amountToCreateAccount, + data.currencySymbol, ) } } 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 15eacf8c99..ab946481ec 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 @@ -39,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.SaltPaySingleWalletView 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 @@ -149,16 +149,19 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { - walletView = SaltPaySingleWalletView() + isSaltPay && (walletView !is SaltPayWalletView) -> { + walletView.onViewDestroy() + walletView = SaltPayWalletView() walletView.changeWalletView(this, binding) } - state.isMultiwalletAllowed && state.primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard && + 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) } @@ -174,7 +177,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { showErrorState( binding, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPayBalanceWidget.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPayBalanceWidget.kt deleted file mode 100644 index 49fcb89c16..0000000000 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPayBalanceWidget.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.tangem.tap.features.wallet.ui.wallet - -import com.tangem.domain.common.extensions.debounce -import com.tangem.tap.common.entities.FiatCurrency -import com.tangem.tap.common.extensions.animateVisibility -import com.tangem.tap.common.extensions.formatAmountAsSpannedString -import com.tangem.tap.features.wallet.redux.ProgressState -import com.tangem.tap.features.wallet.redux.WalletAction -import com.tangem.tap.mainScope -import com.tangem.tap.store -import com.tangem.wallet.databinding.LayoutSingleWalletBalanceBinding -import org.rekotlin.Action -import java.math.BigDecimal - -data class SaltPayBalanceWidgetData( - val state: ProgressState? = null, - val currencySymbol: String? = null, - val currency: String? = null, - val fiatAmount: BigDecimal? = null, - val fiatCurrency: FiatCurrency? = null, -) - -class SaltPayBalanceWidget( - private val binding: LayoutSingleWalletBalanceBinding, - private val data: SaltPayBalanceWidgetData, -) { - fun setup(): Unit = with(binding) { - if (data.state == ProgressState.Loading) { - veilBalance.veil() - veilBalanceCrypto.veil() - } else { - // veilBalance.unVeil() - veilBalanceCrypto.unVeil() - } - tvProcessing.animateVisibility( - show = data.state == ProgressState.Error, - ) - veilBalanceCrypto.animateVisibility( - show = data.state != ProgressState.Error, - ) - if (data.fiatAmount == null) { - //TODO: SaltPay: A tricky solution to the problem with displaying rates - // If the rates are loaded after the walletManager.update() is completely updated, - // then this problem can be avoided - actionDebouncer(WalletAction.LoadFiatRate()) - } else { - veilBalance.unVeil() - tvBalance.text = data.fiatAmount?.formatAmountAsSpannedString( - currencySymbol = data.fiatCurrency?.symbol ?: "", - ) - } - - tvBalanceCrypto.text = data.currency - - tvCurrencyName.text = data.fiatCurrency?.code - - tvCurrencyName.setOnClickListener { - store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency) - } - } -} - -private val actionDebouncer = debounce(500, mainScope) { store.dispatch(it) } - diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPaySingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPaySingleWalletView.kt deleted file mode 100644 index 44ff7329fc..0000000000 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPaySingleWalletView.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.tangem.tap.features.wallet.ui.wallet - -import com.tangem.tap.common.extensions.hide -import com.tangem.tap.common.extensions.show -import com.tangem.tap.features.wallet.redux.WalletState -import com.tangem.tap.features.wallet.ui.TokenData -import com.tangem.tap.features.wallet.ui.WalletFragment -import com.tangem.tap.store -import com.tangem.wallet.databinding.FragmentWalletBinding -import timber.log.Timber - -class SaltPaySingleWalletView : WalletView() { - override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) { - setFragment(fragment, binding) - onViewCreated() - showSingleWalletView(binding) - } - - private fun showSingleWalletView(binding: FragmentWalletBinding) = with(binding) { - rvMultiwallet.hide() - btnAddToken.hide() - rowButtons.hide() - rvPendingTransaction.hide() - tvTwinCardNumber.hide() - pbLoadingUserTokens.hide() - lCardBalance.root.hide() - lAddress.root.hide() - lSingleWalletBalance.root.show() - } - - override fun onViewCreated() { - } - - override fun onNewState(state: WalletState) { - val binding = binding ?: return - val tokenData = state.primaryWallet?.currencyData?.token ?: return - - setupBalance(state, tokenData, binding) - } - - private fun setupBalance(state: WalletState, tokenData: TokenData, binding: FragmentWalletBinding) { - binding.lSingleWalletBalance.root.show() - Timber.e("Current address is ${state.primaryWalletManager?.wallet?.address}") - SaltPayBalanceWidget( - binding = binding.lSingleWalletBalance, - data = SaltPayBalanceWidgetData( - state = state.state, - currencySymbol = tokenData.tokenSymbol, - currency = tokenData.amountFormatted, - fiatAmount = tokenData.fiatAmount, // TODO: show fiatAmount - fiatCurrency = store.state.globalState.appCurrency, - ), - ).setup() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt index e7587a6983..1723934656 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt @@ -35,6 +35,7 @@ class SingleWalletView : WalletView() { } private fun showSingleWalletView(binding: FragmentWalletBinding) = with(binding) { + lSaltPayWallet.root.hide() tvTwinCardNumber.hide() rvMultiwallet.hide() btnAddToken.hide() @@ -63,13 +64,13 @@ class SingleWalletView : WalletView() { override fun onNewState(state: WalletState) { val binding = binding ?: return - state.primaryWallet ?: return + val primaryWalletData = state.primaryWalletData ?: return setupTwinCards(state.twinCardsState, binding) - setupButtons(state.primaryWallet, binding, state.isExchangeServiceFeatureOn) + setupButtons(primaryWalletData, binding, state.isExchangeServiceFeatureOn) setupAddressCard(state, binding) - showPendingTransactionsIfPresent(state.primaryWallet.pendingTransactions) - setupBalance(state, state.primaryWallet) + showPendingTransactionsIfPresent(primaryWalletData.pendingTransactions) + setupBalance(state, primaryWalletData) } private fun showPendingTransactionsIfPresent(pendingTransactions: List) { @@ -88,6 +89,7 @@ class SingleWalletView : WalletView() { binding = this.lCardBalance, fragment = fragment, data = primaryWallet.currencyData, + token = state.primaryTokenData?.currencyData, isTwinCard = state.isTangemTwins, ).setup() } @@ -152,7 +154,7 @@ class SingleWalletView : WalletView() { } private fun setupAddressCard(state: WalletState, binding: FragmentWalletBinding) = with(binding.lAddress) { - val primaryWallet = state.primaryWallet + val primaryWallet = state.primaryWalletData if (primaryWallet?.walletAddresses != null && primaryWallet.currency is Currency.Blockchain) { binding.lAddress.root.show() if (primaryWallet.shouldShowMultipleAddress()) { @@ -187,7 +189,7 @@ class SingleWalletView : WalletView() { private fun setupCardInfo(state: WalletState) { val textView = binding?.lAddress?.tvInfo - val blockchain = state.primaryWallet?.currency?.blockchain + val blockchain = state.primaryWalletData?.currency?.blockchain if (textView != null && blockchain != null) { textView.text = textView.getString( id = R.string.address_qr_code_message_format, 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 d7d7cbb405..97c127040a 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 @@ -5,8 +5,10 @@ import com.tangem.tap.features.wallet.ui.WalletFragment import com.tangem.wallet.databinding.FragmentWalletBinding abstract class WalletView { + protected var fragment: WalletFragment? = null protected var binding: FragmentWalletBinding? = null + fun setFragment(fragment: WalletFragment, binding: FragmentWalletBinding) { this.fragment = fragment this.binding = binding @@ -17,6 +19,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..8d43ff1316 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/SaltPayWalletView.kt @@ -0,0 +1,257 @@ +package com.tangem.tap.features.wallet.ui.wallet.saltPay + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.skydoves.androidveil.VeilLayout +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.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 + + setupBalanceWidget(balanceWidget, state) + setupTxHistoryWidget(txWidget, state) + } + + private fun setupBalanceWidget( + balanceWidget: LayoutSaltPayBalanceBinding, + 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) + 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() + } +} + +private fun VeilLayout.changeVeilState(show: Boolean) { + if (show) this.veil() + else this.unVeil() +} \ 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..763f6b532e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/rv/HistoryItemData.kt @@ -0,0 +1,78 @@ +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 + } + + 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/ShimmerRecyclerAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/rv/ShimmerRecyclerAdapter.kt new file mode 100644 index 0000000000..fc59d49155 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/rv/ShimmerRecyclerAdapter.kt @@ -0,0 +1,33 @@ +package com.tangem.tap.features.wallet.ui.wallet.saltPay.rv + +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) + +data class ShimmerData(private val any: String = "") \ 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..5ac81ac30a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/saltPay/rv/TxHistoryAdapter.kt @@ -0,0 +1,112 @@ +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 { + return 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 aa4f559888..1a98a9c93d 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,10 @@ 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.addCardContext import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState @@ -136,7 +138,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) @@ -197,8 +199,7 @@ internal class WalletSelectorMiddleware { .doOnSuccess { val selectedUserWallet = userWalletsListManager.selectedUserWalletSync if (selectedUserWallet != null) { - val batchId = selectedUserWallet.scanResponse.card.batchId - Analytics.addParamsInterceptor(BatchIdParamsInterceptor(batchId)) + Analytics.addCardContext(selectedUserWallet.scanResponse) store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet)) store.onUserWalletSelected(selectedUserWallet) diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt index 6e67669815..cc02374081 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt @@ -4,7 +4,15 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon import androidx.compose.material.Text @@ -30,6 +38,7 @@ import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.common.compose.TangemTypography +import com.tangem.tap.common.extensions.cardImageData import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletItem import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem @@ -107,7 +116,7 @@ private fun WalletCardImage( SubcomposeAsyncImage( modifier = cardImageModifier, model = ImageRequest.Builder(LocalContext.current) - .data(cardImageUrl) + .cardImageData(cardImageUrl) .crossfade(true) .build(), loading = { CardImageShimmer(modifier = cardImageModifier) }, diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt index 681a3cea77..9f117cb782 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt @@ -5,6 +5,7 @@ import com.tangem.common.core.TangemError import org.rekotlin.Action internal sealed interface WelcomeAction : Action { + object OnCreate : WelcomeAction object ProceedWithBiometrics : WelcomeAction { object Success : WelcomeAction data class Error(val error: TangemError) : WelcomeAction diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index 4c39b5cb3c..76c0a2319e 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -4,9 +4,14 @@ import android.content.Intent import com.tangem.common.core.TangemSdkError import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess +import com.tangem.core.analytics.Analytics import com.tangem.domain.common.ScanResponse +import com.tangem.tap.common.analytics.events.AnalyticsParam +import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.SignIn +import com.tangem.tap.common.extensions.addCardContext import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.eraseCardContext import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.navigation.AppScreen @@ -38,6 +43,10 @@ internal class WelcomeMiddleware { private fun handleAction(action: WelcomeAction, state: WelcomeState) { when (action) { + is WelcomeAction.OnCreate -> { + Analytics.eraseCardContext() + Analytics.send(SignIn.ScreenOpened()) + } is WelcomeAction.ProceedWithBiometrics -> { proceedWithBiometry(state) } @@ -62,6 +71,7 @@ internal class WelcomeMiddleware { } .doOnSuccess { selectedUserWallet -> if (selectedUserWallet != null) { + Analytics.addCardContext(selectedUserWallet.scanResponse) store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Success) store.onUserWalletSelected(selectedUserWallet) @@ -101,7 +111,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 42a6d4d0e1..e4abfb4208 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 70477f5ad1..9ca40680ac 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,13 +19,13 @@ 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.WelcomeScreenContent +import com.tangem.tap.store import com.tangem.wallet.R internal class WelcomeFragment : ComposeFragment() { @@ -33,7 +33,7 @@ internal class WelcomeFragment : ComposeFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Analytics.send(SignIn.ScreenOpened()) + store.dispatch(WelcomeAction.OnCreate) } @Composable 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 3f61d82801..656343a092 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() @@ -59,7 +59,7 @@ class CurrencyExchangeManager( blockchain, cryptoCurrencyName, fiatCurrencyName, - walletAddress + walletAddress, ) } @@ -97,8 +97,11 @@ suspend fun CurrencyExchangeManager.buyErc20TestnetTokens( val amountToSend = Amount(walletManager.wallet.blockchain) val destinationAddress = token.contractAddress - val feeResult = walletManager.getFee(amountToSend, - destinationAddress) as? Result.Success ?: return + val feeResult = + walletManager.getFee( + amountToSend, + destinationAddress, + ) as? Result.Success ?: return val fee = feeResult.data[0] if ((walletManager.wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO) < fee.value) { @@ -116,8 +119,8 @@ suspend fun CurrencyExchangeManager.buyErc20TestnetTokens( GlobalAction.UpdateWalletSignedHashes( walletSignedHashes = signResponse.totalSignedHashes, walletPublicKey = walletManager.wallet.publicKey.seedKey, - remainingSignatures = signResponse.remainingSignatures - ) + remainingSignatures = signResponse.remainingSignatures, + ), ) } walletManager.send(transaction, signer) 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 8d6e8e2187..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 @@ -7,11 +7,11 @@ import com.tangem.tap.features.wallet.models.Currency interface Exchanger { fun isBuyAllowed(): Boolean fun isSellAllowed(): Boolean - fun availableForBuy(currency: Currency):Boolean - fun availableForSell(currency: Currency):Boolean + fun availableForBuy(currency: Currency): Boolean + fun availableForSell(currency: Currency): Boolean } -interface ExchangeService: Feature, Exchanger { +interface ExchangeService : Feature, Exchanger, ExchangeUrlBuilder { suspend fun update() companion object { @@ -22,11 +22,23 @@ 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 } } } -interface ExchangeRules: Feature, Exchanger { +interface ExchangeRules : Feature, Exchanger { companion object { fun dummy(): ExchangeRules = object : ExchangeRules { 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 9dd2b9746f..64f2ae121a 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>() @@ -34,7 +27,7 @@ class MercuryoService( override fun featureIsSwitchedOn(): Boolean = true 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 == 200) { @@ -108,14 +101,14 @@ class MercuryoService( blockchain: Blockchain, cryptoCurrencyName: CryptoCurrencyName, fiatCurrencyName: String, - walletAddress: String + walletAddress: String, ): String { if (action == CurrencyExchangeManager.Action.Sell) throw UnsupportedOperationException() 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) @@ -128,13 +121,12 @@ class MercuryoService( } private fun signature(address: String): String { - return (address + secret).calculateSha512().toHexString().lowercase() + return (address + environment.secret).calculateSha512().toHexString().lowercase() } - override fun getSellCryptoReceiptUrl( action: CurrencyExchangeManager.Action, - transactionId: String + transactionId: String, ): String? = null private fun blockchainFromCurrencyName(currencyName: String): Blockchain? = when (currencyName) { 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 645d5182ed..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 @@ -5,14 +5,13 @@ import android.util.Base64 import com.tangem.blockchain.common.Blockchain import com.tangem.common.services.Result import com.tangem.common.services.performRequest -import com.tangem.domain.common.extensions.withIOContext import com.tangem.datasource.api.common.createRetrofitInstance +import com.tangem.domain.common.extensions.withIOContext import com.tangem.tap.common.extensions.urlEncode 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() @@ -130,7 +129,6 @@ class MoonPayService( .appendPath("transaction_receipt") .appendQueryParameter("transactionId", transactionId).build().toString() return url - } private fun createSignature(data: String): String { @@ -149,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 d419aafffc..7999268acb 100644 --- a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt +++ b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt @@ -29,9 +29,13 @@ class PreferencesStorage(applicationContext: Application) { toppedUpWalletStorage = ToppedUpWalletStorage(preferences, MoshiConverter.INSTANCE) } - 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) @@ -82,7 +86,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/color/selector_saltpay_btn.xml b/app/src/main/res/color/selector_saltpay_btn.xml new file mode 100644 index 0000000000..98dd6f0aba --- /dev/null +++ b/app/src/main/res/color/selector_saltpay_btn.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/selector_saltpay_btn_text.xml b/app/src/main/res/color/selector_saltpay_btn_text.xml new file mode 100644 index 0000000000..28e2f2cfdd --- /dev/null +++ b/app/src/main/res/color/selector_saltpay_btn_text.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file 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-h680dp/layout_onboarding_saltpay_kyc_in_progress.xml b/app/src/main/res/layout-h680dp/layout_onboarding_saltpay_kyc_in_progress.xml index 4eb63182f8..862e6809ee 100644 --- a/app/src/main/res/layout-h680dp/layout_onboarding_saltpay_kyc_in_progress.xml +++ b/app/src/main/res/layout-h680dp/layout_onboarding_saltpay_kyc_in_progress.xml @@ -52,7 +52,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginBottom="92dp" - android:text="@string/onboarding_supplement_button_kyc_waiting" + android:text="@string/chat_button_title" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@id/btn_container" app:layout_constraintStart_toStartOf="@id/btn_container" /> diff --git a/app/src/main/res/layout/fragment_wallet.xml b/app/src/main/res/layout/fragment_wallet.xml index f09ddc509b..b3dc1e076b 100644 --- a/app/src/main/res/layout/fragment_wallet.xml +++ b/app/src/main/res/layout/fragment_wallet.xml @@ -177,6 +177,15 @@ android:visibility="gone" app:layout_constraintTop_toBottomOf="@id/rv_pending_transaction" /> + + + + + + + + + + + + + + + + + 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_onboarding_saltpay_kyc_in_progress.xml b/app/src/main/res/layout/layout_onboarding_saltpay_kyc_in_progress.xml index 580995fc6f..a5fbee52fa 100644 --- a/app/src/main/res/layout/layout_onboarding_saltpay_kyc_in_progress.xml +++ b/app/src/main/res/layout/layout_onboarding_saltpay_kyc_in_progress.xml @@ -55,7 +55,7 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@id/btn_container" app:layout_constraintStart_toStartOf="@id/btn_container" - tools:text="@string/onboarding_supplement_button_kyc_waiting" /> + tools:text="@string/chat_button_title" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_salt_pay_tx_history.xml b/app/src/main/res/layout/layout_salt_pay_tx_history.xml new file mode 100644 index 0000000000..2aef592910 --- /dev/null +++ b/app/src/main/res/layout/layout_salt_pay_tx_history.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_salt_pay_wallet.xml b/app/src/main/res/layout/layout_salt_pay_wallet.xml new file mode 100644 index 0000000000..4712e76937 --- /dev/null +++ b/app/src/main/res/layout/layout_salt_pay_wallet.xml @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/app/src/main/res/menu/menu_onboarding.xml b/app/src/main/res/menu/menu_onboarding.xml index bc09d08fae..28e7e73a09 100644 --- a/app/src/main/res/menu/menu_onboarding.xml +++ b/app/src/main/res/menu/menu_onboarding.xml @@ -3,6 +3,6 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 2bd13d5482..bdd37a99bb 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -131,6 +131,19 @@ 4sp + +