Updated on 2026-08-14
This commit is contained in:
commit
2d8646ac6b
138 changed files with 3932 additions and 619 deletions
|
|
@ -196,6 +196,7 @@ dependencies {
|
|||
}
|
||||
implementation(Library.accompanistAppCompatTheme)
|
||||
implementation(Library.accompanistSystemUiController)
|
||||
implementation(Library.accompanistWebView)
|
||||
implementation(Library.xmlShimmer)
|
||||
implementation(Library.viewBindingDelegate)
|
||||
implementation(Library.armadillo)
|
||||
|
|
|
|||
|
|
@ -139,9 +139,12 @@
|
|||
|
||||
<activity android:name="com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity" />
|
||||
|
||||
<activity android:name="zendesk.messaging.MessagingActivity"
|
||||
<activity
|
||||
android:name="zendesk.messaging.MessagingActivity"
|
||||
android:theme="@style/ZendeskTheme" />
|
||||
|
||||
<activity android:name="com.tangem.tap.features.sprinklr.ui.SprinklrActivity" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 93150ef69daf244a92266d7cdfbc9294b745b4e2
|
||||
Subproject commit f5f6ac954894ef66331db8fe383b9773feb69ef4
|
||||
|
|
@ -12,9 +12,9 @@ import com.tangem.blockchain.common.WalletManagerFactory
|
|||
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.datasource.api.common.MoshiConverter
|
||||
import com.tangem.datasource.config.models.Config
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
import com.tangem.datasource.config.FeaturesLocalLoader
|
||||
import com.tangem.datasource.config.models.Config
|
||||
import com.tangem.datasource.utils.AndroidAssetReader
|
||||
import com.tangem.datasource.utils.AssetReader
|
||||
import com.tangem.domain.DomainLayer
|
||||
|
|
@ -27,6 +27,7 @@ import com.tangem.tap.common.analytics.filters.BasicTopUpFilter
|
|||
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
|
||||
import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
|
||||
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
|
||||
import com.tangem.tap.common.chat.ChatManager
|
||||
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
|
||||
import com.tangem.tap.common.feedback.FeedbackManager
|
||||
import com.tangem.tap.common.images.createCoilImageLoader
|
||||
|
|
@ -52,11 +53,10 @@ import com.tangem.tap.network.NetworkConnectivity
|
|||
import com.tangem.tap.persistence.PreferencesStorage
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import com.zendesk.logger.Logger
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.rekotlin.Store
|
||||
import timber.log.Timber
|
||||
import zendesk.chat.Chat
|
||||
import javax.inject.Inject
|
||||
|
||||
lateinit var store: Store<AppState>
|
||||
|
|
@ -146,7 +146,11 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
initConfigManager(configLoader, ::initWithConfigDependency)
|
||||
initWarningMessagesManager()
|
||||
|
||||
BlockchainSdkRetrofitBuilder.enableNetworkLogging = LogConfig.network.blockchainSdkNetwork
|
||||
if (LogConfig.network.blockchainSdkNetwork) {
|
||||
BlockchainSdkRetrofitBuilder.interceptors = listOf(
|
||||
HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY },
|
||||
)
|
||||
}
|
||||
|
||||
userTokensRepository = UserTokensRepository.init(
|
||||
context = this,
|
||||
|
|
@ -174,7 +178,7 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
private fun initWithConfigDependency(config: Config) {
|
||||
shopService = TangemShopService(this, config.shopify!!)
|
||||
initAnalytics(this, config)
|
||||
initFeedbackManager(this, preferencesStorage)
|
||||
initFeedbackManager(this, preferencesStorage, foregroundActivityObserver, store)
|
||||
}
|
||||
|
||||
private fun initAnalytics(application: Application, config: Config) {
|
||||
|
|
@ -196,7 +200,12 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
factory.build(Analytics, buildData)
|
||||
}
|
||||
|
||||
private fun initFeedbackManager(context: Context, preferencesStorage: PreferencesStorage) {
|
||||
private fun initFeedbackManager(
|
||||
context: Context,
|
||||
preferencesStorage: PreferencesStorage,
|
||||
foregroundActivityObserver: ForegroundActivityObserver,
|
||||
store: Store<AppState>,
|
||||
) {
|
||||
fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo = AdditionalFeedbackInfo().apply {
|
||||
appVersion = try {
|
||||
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
|
|
@ -229,12 +238,8 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
val feedbackManager = FeedbackManager(
|
||||
infoHolder = additionalFeedbackInfo,
|
||||
logCollector = tangemLogCollector,
|
||||
preferencesStorage = preferencesStorage,
|
||||
chatManager = ChatManager(preferencesStorage, foregroundActivityObserver, store),
|
||||
)
|
||||
feedbackManager.chatInitializer = { zendeskConfig ->
|
||||
Chat.INSTANCE.init(context, zendeskConfig.accountKey, zendeskConfig.appId)
|
||||
Logger.setLoggable(LogConfig.zendesk)
|
||||
}
|
||||
store.dispatch(GlobalAction.SetFeedbackManager(feedbackManager))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.tap.common
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
open class ShimmerRecyclerAdapter(
|
||||
private val viewHolderViewFactory: (ViewGroup) -> ViewGroup,
|
||||
) : ListAdapter<ShimmerData, ShimmerVH>(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<ShimmerData>() {
|
||||
override fun areContentsTheSame(oldItem: ShimmerData, newItem: ShimmerData) = oldItem == newItem
|
||||
override fun areItemsTheSame(oldItem: ShimmerData, newItem: ShimmerData) = oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
class ShimmerVH(viewGroup: ViewGroup) : RecyclerView.ViewHolder(viewGroup)
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
data class ShimmerData(private val any: String = "")
|
||||
|
|
@ -114,19 +114,53 @@ data class BasicEventsSourceData(
|
|||
val paramCardBalanceState: AnalyticsParam.CardBalanceState by lazy { calculateAmount().toCardBalanceState() }
|
||||
|
||||
private fun calculateAmount(): BigDecimal {
|
||||
return biometricsWalletDataModels?.calculateTotalCryptoAmount()
|
||||
?: walletState.walletsDataFromStores.calculateTotalCryptoAmount()
|
||||
val calculator: IBalanceCalculator = biometricsWalletDataModels
|
||||
?.let { BiometricsBalanceCalculator(it) }
|
||||
?: BalanceCalculator(walletState)
|
||||
|
||||
return calculator.calculate()
|
||||
}
|
||||
|
||||
private fun BigDecimal.toCardBalanceState(): AnalyticsParam.CardBalanceState = when {
|
||||
isZero() -> AnalyticsParam.CardBalanceState.Empty
|
||||
else -> AnalyticsParam.CardBalanceState.Full
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<WalletDataModel>.calculateTotalCryptoAmount(): BigDecimal {
|
||||
return this
|
||||
.map { it.status.amount }
|
||||
.reduce(BigDecimal::plus)
|
||||
private interface IBalanceCalculator {
|
||||
fun calculate(): BigDecimal
|
||||
}
|
||||
|
||||
private class BiometricsBalanceCalculator(
|
||||
private val walletDataModel: List<WalletDataModel>,
|
||||
) : IBalanceCalculator {
|
||||
|
||||
override fun calculate(): BigDecimal {
|
||||
val singleToken = walletDataModel
|
||||
.filter { it.currency.isToken() }
|
||||
.firstOrNull { it.isCardSingleToken }
|
||||
|
||||
val totalAmount = singleToken?.status?.amount
|
||||
?: walletDataModel.calculateTotalCryptoAmount()
|
||||
|
||||
return totalAmount
|
||||
}
|
||||
|
||||
private fun List<WalletDataModel>.calculateTotalCryptoAmount(): BigDecimal = this
|
||||
.map { it.status.amount }
|
||||
.reduce(BigDecimal::plus)
|
||||
}
|
||||
|
||||
private class BalanceCalculator(
|
||||
private val walletState: WalletState,
|
||||
) : IBalanceCalculator {
|
||||
|
||||
override fun calculate(): BigDecimal {
|
||||
val singleTokenData = walletState.primaryTokenData
|
||||
val totalAmount = singleTokenData?.currencyData?.amount
|
||||
?: walletState.walletsDataFromStores.calculateTotalCryptoAmount()
|
||||
|
||||
return totalAmount
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ sealed class AnalyticsParam {
|
|||
class Token(token: com.tangem.blockchain.common.Token) : CurrencyType(token.symbol)
|
||||
class FiatCurrency(
|
||||
fiatCurrency: com.tangem.tap.common.entities.FiatCurrency,
|
||||
) : CurrencyType(fiatCurrency.symbol)
|
||||
) : CurrencyType(fiatCurrency.code)
|
||||
|
||||
class Amount(amount: com.tangem.blockchain.common.Amount) : CurrencyType(amount.currencySymbol)
|
||||
}
|
||||
|
|
@ -63,8 +63,18 @@ sealed class AnalyticsParam {
|
|||
object BlockchainSdk : Error("Blockchain Sdk Error")
|
||||
}
|
||||
|
||||
sealed class ScannedFrom(val value: String) {
|
||||
object Introduction : ScannedFrom("Introduction")
|
||||
object Main : ScannedFrom("Main")
|
||||
object SignIn : ScannedFrom("Sign In")
|
||||
object MyWallets : ScannedFrom("My Wallets")
|
||||
}
|
||||
|
||||
companion object Key {
|
||||
const val BatchId = "Batch"
|
||||
const val Batch = "Batch"
|
||||
const val ProductType = "Product Type"
|
||||
const val Firmware = "Firmware"
|
||||
const val Currency = "Currency"
|
||||
const val ErrorDescription = "Error Description"
|
||||
const val ErrorCode = "Error Code"
|
||||
const val ErrorKey = "Error Key"
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ sealed class Onboarding(
|
|||
|
||||
class ButtonBuyCrypto(currency: AnalyticsParam.CurrencyType) : Topup(
|
||||
event = "Button - Buy Crypto",
|
||||
params = mapOf("Currency" to currency.value),
|
||||
params = mapOf(AnalyticsParam.Currency to currency.value),
|
||||
)
|
||||
|
||||
class ButtonShowWalletAddress : Topup("Button - Show the Wallet Address")
|
||||
|
|
@ -67,16 +67,26 @@ sealed class Onboarding(
|
|||
class SetupFinished : Twins("Twin Setup Finished")
|
||||
}
|
||||
|
||||
class PinCodeSet : Onboarding("Onboarding", "PIN code set")
|
||||
class PinScreenOpened : Onboarding("Onboarding", "PIN screen opened")
|
||||
class ButtonSetPinCode : Onboarding("Onboarding", "Button - Set PIN Code")
|
||||
class CardConnectionScreenOpened : Onboarding("Onboarding", "Card Connection Screen Opened")
|
||||
class ButtonConnect : Onboarding("Onboarding", "Button - Connect")
|
||||
class PinCodeSet : Onboarding("Onboarding", "PIN code set")
|
||||
|
||||
class KYCScreenOpened : Onboarding("Onboarding", "KYC screen opened")
|
||||
class KYCStarted : Onboarding("Onboarding", "KYC started")
|
||||
class KYCInProgress : Onboarding("Onboarding", "KYC in progress")
|
||||
class KYCRejected : Onboarding("Onboarding", "KYC rejected")
|
||||
|
||||
class ClaimScreenOpened : Onboarding("Onboarding", "Claim screen opened")
|
||||
class ButtonClaim : Onboarding("Onboarding", "Button - Claim")
|
||||
class ClaimWasSuccessfully : Onboarding("Onboarding", "Claim was successfully")
|
||||
|
||||
class ButtonChat : Onboarding("Onboarding", "Button - Chat")
|
||||
|
||||
class NotEnoughGasError : Onboarding("Onboarding", "Not Enough Gas Error")
|
||||
class CardNotPassedError : Onboarding("Onboarding", "Card Not Passed Error")
|
||||
|
||||
class EnableBiometrics(state: AnalyticsParam.OnOffState) : Onboarding(
|
||||
category = "Onboarding / Biometric",
|
||||
event = "Enable Biometric",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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<String, String>) {
|
||||
params[AnalyticsParam.BatchId] = batchId
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, String>) {
|
||||
scanResponse ?: return
|
||||
|
||||
val card = scanResponse.card
|
||||
params[AnalyticsParam.Batch] = card.batchId
|
||||
params[AnalyticsParam.ProductType] = getProductType(scanResponse)
|
||||
params[AnalyticsParam.Firmware] = card.firmwareVersion.stringValue
|
||||
|
||||
ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)?.let {
|
||||
params[AnalyticsParam.Currency] = it.value
|
||||
}
|
||||
}
|
||||
|
||||
private fun getProductType(scanResponse: ScanResponse): String {
|
||||
return when (scanResponse.productType) {
|
||||
ProductType.Note -> "Note"
|
||||
ProductType.Twins -> "Twin"
|
||||
ProductType.Wallet -> "Wallet"
|
||||
ProductType.SaltPay -> if (scanResponse.cardTypesResolver.isSaltPayVisa()) {
|
||||
"Visa"
|
||||
} else {
|
||||
"Visa Backup"
|
||||
}
|
||||
ProductType.Start2Coin -> "Start2Coin"
|
||||
else -> if (DemoHelper.isDemoCard(scanResponse)) {
|
||||
if (DemoHelper.isTestDemoCard(scanResponse)) {
|
||||
"Demo Test"
|
||||
} else {
|
||||
when (scanResponse.card.cardId.substring(0..1)) {
|
||||
"AC" -> "Demo Wallet"
|
||||
"AB" -> "Demo Note"
|
||||
else -> "Demo Other"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
"Other"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun id(): String = CardContextInterceptor::class.java.simpleName
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.tap.common.analytics.paramsInterceptor
|
||||
|
||||
import com.tangem.core.analytics.AnalyticsEvent
|
||||
import com.tangem.core.analytics.api.ParamsInterceptor
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class LinkedCardContextInterceptor(
|
||||
private val scanResponse: ScanResponse,
|
||||
val parent: LinkedCardContextInterceptor? = null,
|
||||
) : ParamsInterceptor {
|
||||
|
||||
private val contextInterceptor = CardContextInterceptor(scanResponse)
|
||||
|
||||
override fun id(): String = LinkedCardContextInterceptor.id()
|
||||
|
||||
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = contextInterceptor.canBeAppliedTo(event)
|
||||
|
||||
override fun intercept(params: MutableMap<String, String>) = contextInterceptor.intercept(params)
|
||||
|
||||
companion object {
|
||||
fun id(): String = LinkedCardContextInterceptor::class.java.simpleName
|
||||
}
|
||||
}
|
||||
53
app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt
Normal file
53
app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.tap.common.chat
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.tangem.tap.ForegroundActivityObserver
|
||||
import com.tangem.datasource.config.models.ChatConfig
|
||||
import com.tangem.tap.common.chat.opener.ChatOpener
|
||||
import com.tangem.datasource.config.models.SprinklrConfig
|
||||
import com.tangem.datasource.config.models.ZendeskConfig
|
||||
import com.tangem.tap.common.chat.opener.implementation.SprinklrChatOpener
|
||||
import com.tangem.tap.common.chat.opener.implementation.ZendeskChatOpener
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.persistence.PreferencesStorage
|
||||
import org.rekotlin.Store
|
||||
|
||||
class ChatManager(
|
||||
private val preferencesStorage: PreferencesStorage,
|
||||
private val foregroundActivityObserver: ForegroundActivityObserver,
|
||||
private val store: Store<AppState>,
|
||||
) {
|
||||
private val openers = mutableMapOf<ChatConfig, ChatOpener>()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.tap.common.chat.opener
|
||||
|
||||
import android.content.Context
|
||||
|
||||
internal interface ChatOpener {
|
||||
fun open(feedbackDataBuilder: (Context) -> String)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.tap.common.chat.opener.implementation
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.tangem.tap.ForegroundActivityObserver
|
||||
import com.tangem.tap.common.chat.opener.ChatOpener
|
||||
import com.tangem.datasource.config.models.SprinklrConfig
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.sprinklr.redux.SprinklrAction
|
||||
import com.tangem.tap.features.sprinklr.ui.SprinklrActivity
|
||||
import com.tangem.tap.withForegroundActivity
|
||||
import org.rekotlin.Store
|
||||
|
||||
internal class SprinklrChatOpener(
|
||||
private val userId: String,
|
||||
private val config: SprinklrConfig,
|
||||
private val store: Store<AppState>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package com.tangem.tap.common.chat.opener.implementation
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import com.tangem.tap.ForegroundActivityObserver
|
||||
import com.tangem.tap.common.chat.opener.ChatOpener
|
||||
import com.tangem.datasource.config.models.ZendeskConfig
|
||||
import com.tangem.tap.withForegroundActivity
|
||||
import com.tangem.wallet.R
|
||||
import com.zendesk.logger.Logger
|
||||
import zendesk.chat.Chat
|
||||
import zendesk.chat.ChatConfiguration
|
||||
import zendesk.chat.ChatEngine
|
||||
import zendesk.chat.ChatProvidersConfiguration
|
||||
import zendesk.chat.VisitorInfo
|
||||
import zendesk.configurations.Configuration
|
||||
import zendesk.messaging.MessagingActivity
|
||||
|
||||
internal class ZendeskChatOpener(
|
||||
private val userId: String,
|
||||
private val config: ZendeskConfig,
|
||||
private val foregroundActivityObserver: ForegroundActivityObserver,
|
||||
) : ChatOpener {
|
||||
private var isInitialized = false
|
||||
|
||||
override fun open(feedbackDataBuilder: (Context) -> String) {
|
||||
foregroundActivityObserver.withForegroundActivity { activity ->
|
||||
initZendeskIfNeeded(activity.applicationContext)
|
||||
setChatVisitorInfo()
|
||||
setChatVisitorNote(feedbackDataBuilder(activity))
|
||||
showMessagingActivity(activity)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initZendeskIfNeeded(context: Context) {
|
||||
if (isInitialized) return
|
||||
isInitialized = true
|
||||
|
||||
Chat.INSTANCE.init(context, config.accountKey, config.appId)
|
||||
Logger.setLoggable(LogConfig.zendesk)
|
||||
}
|
||||
|
||||
private fun setChatVisitorInfo() {
|
||||
val visitorInfo = VisitorInfo.builder().withName("User $userId").build()
|
||||
|
||||
Chat.INSTANCE.chatProvidersConfiguration =
|
||||
ChatProvidersConfiguration.builder().withVisitorInfo(visitorInfo).build()
|
||||
}
|
||||
|
||||
private fun setChatVisitorNote(note: String) {
|
||||
Chat.INSTANCE.providers()?.profileProvider()?.setVisitorNote(note)
|
||||
}
|
||||
|
||||
private fun showMessagingActivity(context: Context) {
|
||||
Analytics.send(com.tangem.tap.common.analytics.events.Chat.ScreenOpened())
|
||||
MessagingActivity.builder()
|
||||
.withMultilineResponseOptionsEnabled(false)
|
||||
.withBotLabelStringRes(R.string.chat_bot_name)
|
||||
.withBotAvatarDrawable(R.mipmap.ic_launcher)
|
||||
.withEngines(ChatEngine.engine())
|
||||
.show(context, buildChatConfig())
|
||||
}
|
||||
|
||||
private fun buildChatConfig(): Configuration {
|
||||
return ChatConfiguration.builder()
|
||||
.withOfflineFormEnabled(true)
|
||||
.withAgentAvailabilityEnabled(true)
|
||||
.withPreChatFormEnabled(false)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterceptor
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sets the new context
|
||||
*/
|
||||
fun Analytics.setContext(scanResponse: ScanResponse) {
|
||||
addParamsInterceptor(LinkedCardContextInterceptor(scanResponse))
|
||||
}
|
||||
|
||||
/**
|
||||
* Erases the context
|
||||
*/
|
||||
fun Analytics.eraseContext() {
|
||||
removeParamsInterceptor(LinkedCardContextInterceptor.id())
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new context and keeps a previous context as the parent of the new one
|
||||
*/
|
||||
fun Analytics.addContext(scanResponse: ScanResponse) {
|
||||
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id()) as? LinkedCardContextInterceptor
|
||||
val newContext = LinkedCardContextInterceptor(scanResponse, parent = currentContext)
|
||||
|
||||
addParamsInterceptor(newContext)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the current context and restores the previous one if it was present.
|
||||
*/
|
||||
fun Analytics.removeContext() {
|
||||
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id()) as? LinkedCardContextInterceptor
|
||||
val previousContext = currentContext?.parent ?: return
|
||||
|
||||
addParamsInterceptor(previousContext)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -2,6 +2,9 @@ package com.tangem.tap.common.extensions
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.TransactionHistoryProvider
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.services.Result
|
||||
|
|
@ -10,6 +13,7 @@ import com.tangem.tap.domain.TapError
|
|||
import com.tangem.tap.domain.extensions.amountToCreateAccount
|
||||
import com.tangem.tap.domain.getFirstToken
|
||||
import com.tangem.tap.features.demo.isDemoCard
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.redux.AddressData
|
||||
import com.tangem.tap.features.wallet.redux.reducers.createAddressesData
|
||||
import com.tangem.tap.network.NetworkConnectivity
|
||||
|
|
@ -31,6 +35,9 @@ suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
|
|||
Result.Success(wallet)
|
||||
} else {
|
||||
update()
|
||||
if (wallet.blockchain == Blockchain.SaltPay && this is TransactionHistoryProvider) {
|
||||
this.getTransactionHistory(wallet.address, wallet.blockchain, wallet.getTokens())
|
||||
}
|
||||
Result.Success(wallet)
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
|
|
@ -76,10 +83,33 @@ fun WalletManager?.getAddressData(): AddressData? {
|
|||
return if (addressDataList.isEmpty()) null else addressDataList[0]
|
||||
}
|
||||
|
||||
fun WalletManager.getTxHistory(currency: Currency): List<TransactionData> = wallet.getTxHistory(currency)
|
||||
|
||||
fun WalletManager.getBlockchainTxHistory(): List<TransactionData> = wallet.getBlockchainTxHistory()
|
||||
|
||||
fun WalletManager.getTokenTxHistory(token: Token): List<TransactionData> = wallet.getTokenTxHistory(token)
|
||||
|
||||
fun <T> 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<TransactionData> {
|
||||
return (currency as? Currency.Token)?.let { this.getTokenTxHistory(it.token) }
|
||||
?: getBlockchainTxHistory()
|
||||
}
|
||||
|
||||
fun Wallet.getBlockchainTxHistory(): List<TransactionData> {
|
||||
return historyTransactions.filter {
|
||||
it.contractAddress.isNullOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
fun Wallet.getTokenTxHistory(token: Token): List<TransactionData> {
|
||||
return historyTransactions.filter {
|
||||
it.contractAddress == token.contractAddress
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +1,14 @@
|
|||
package com.tangem.tap.common.feedback
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.common.TapWorkarounds
|
||||
import com.tangem.tap.common.chat.ChatManager
|
||||
import com.tangem.datasource.config.models.ChatConfig
|
||||
import com.tangem.tap.common.extensions.sendEmail
|
||||
import com.tangem.tap.common.log.TangemLogCollector
|
||||
import com.tangem.datasource.config.models.ZendeskConfig
|
||||
import com.tangem.tap.foregroundActivityObserver
|
||||
import com.tangem.tap.persistence.PreferencesStorage
|
||||
import com.tangem.tap.withForegroundActivity
|
||||
import com.tangem.wallet.R
|
||||
import timber.log.Timber
|
||||
import zendesk.chat.Chat
|
||||
import zendesk.chat.ChatConfiguration
|
||||
import zendesk.chat.ChatEngine
|
||||
import zendesk.chat.ChatProvidersConfiguration
|
||||
import zendesk.chat.VisitorInfo
|
||||
import zendesk.configurations.Configuration
|
||||
import zendesk.messaging.MessagingActivity
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.io.StringWriter
|
||||
|
|
@ -29,19 +19,8 @@ import java.io.StringWriter
|
|||
class FeedbackManager(
|
||||
val infoHolder: AdditionalFeedbackInfo,
|
||||
private val logCollector: TangemLogCollector,
|
||||
private val preferencesStorage: PreferencesStorage,
|
||||
private val chatManager: ChatManager,
|
||||
) {
|
||||
private var lastUsedConfigForInitialization: ZendeskConfig? = null
|
||||
|
||||
var chatInitializer: ((ZendeskConfig) -> Unit)? = null
|
||||
|
||||
fun initChat(zendeskConfig: ZendeskConfig) {
|
||||
// prevent double initialization with the same config
|
||||
if (lastUsedConfigForInitialization == zendeskConfig) return
|
||||
|
||||
lastUsedConfigForInitialization = zendeskConfig
|
||||
chatInitializer?.invoke(zendeskConfig)
|
||||
}
|
||||
|
||||
fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) {
|
||||
feedbackData.prepare(infoHolder)
|
||||
|
|
@ -57,12 +36,12 @@ class FeedbackManager(
|
|||
}
|
||||
}
|
||||
|
||||
fun openChat(feedbackData: FeedbackData) {
|
||||
feedbackData.prepare(infoHolder)
|
||||
foregroundActivityObserver.withForegroundActivity { activity ->
|
||||
setChatVisitorInfo()
|
||||
setChatVisitorNote(activity, feedbackData)
|
||||
showMessagingActivity(activity)
|
||||
fun openChat(config: ChatConfig, feedbackData: FeedbackData) {
|
||||
chatManager.open(config) { context ->
|
||||
feedbackData.run {
|
||||
prepare(infoHolder)
|
||||
joinTogether(context, infoHolder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -85,47 +64,6 @@ class FeedbackManager(
|
|||
}
|
||||
}
|
||||
|
||||
private fun setChatVisitorInfo() {
|
||||
if (preferencesStorage.chatFirstLaunchTime == null) {
|
||||
preferencesStorage.chatFirstLaunchTime = System.currentTimeMillis()
|
||||
}
|
||||
val chatUserId = (preferencesStorage.chatFirstLaunchTime.toString() + Build.MODEL).hashCode()
|
||||
val visitorInfo = VisitorInfo.builder()
|
||||
.withName("User $chatUserId")
|
||||
.build()
|
||||
|
||||
Chat.INSTANCE.chatProvidersConfiguration = ChatProvidersConfiguration.builder()
|
||||
.withVisitorInfo(visitorInfo)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun setChatVisitorNote(
|
||||
context: Context,
|
||||
feedbackData: FeedbackData,
|
||||
) {
|
||||
Chat.INSTANCE.providers()
|
||||
?.profileProvider()
|
||||
?.setVisitorNote(feedbackData.joinTogether(context, infoHolder))
|
||||
}
|
||||
|
||||
private fun showMessagingActivity(context: Context) {
|
||||
Analytics.send(com.tangem.tap.common.analytics.events.Chat.ScreenOpened())
|
||||
MessagingActivity.builder()
|
||||
.withMultilineResponseOptionsEnabled(false)
|
||||
.withBotLabelStringRes(R.string.chat_bot_name)
|
||||
.withBotAvatarDrawable(R.mipmap.ic_launcher)
|
||||
.withEngines(ChatEngine.engine())
|
||||
.show(context, buildChatConfig())
|
||||
}
|
||||
|
||||
private fun buildChatConfig(): Configuration {
|
||||
return ChatConfiguration.builder()
|
||||
.withOfflineFormEnabled(true)
|
||||
.withAgentAvailabilityEnabled(true)
|
||||
.withPreChatFormEnabled(false)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun getSupportEmail(): String {
|
||||
return if (TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)) {
|
||||
S2C_SUPPORT_EMAIL
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
|
|||
import com.tangem.tap.features.send.redux.states.SendState
|
||||
import com.tangem.tap.features.shop.redux.ShopMiddleware
|
||||
import com.tangem.tap.features.shop.redux.ShopState
|
||||
import com.tangem.tap.features.sprinklr.redux.SprinklrMiddleware
|
||||
import com.tangem.tap.features.sprinklr.redux.SprinklrState
|
||||
import com.tangem.tap.features.tokens.redux.TokensMiddleware
|
||||
import com.tangem.tap.features.tokens.redux.TokensState
|
||||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
|
|
@ -61,6 +63,7 @@ data class AppState(
|
|||
val welcomeState: WelcomeState = WelcomeState(),
|
||||
val saveWalletState: SaveWalletState = SaveWalletState(),
|
||||
val walletSelectorState: WalletSelectorState = WalletSelectorState(),
|
||||
val sprinklrState: SprinklrState = SprinklrState(),
|
||||
) : StateType {
|
||||
|
||||
val domainState: DomainState
|
||||
|
|
@ -101,6 +104,7 @@ data class AppState(
|
|||
WalletSelectorMiddleware().middleware,
|
||||
LockUserWalletsTimerMiddleware().middleware,
|
||||
AccessCodeRequestPolicyMiddleware().middleware,
|
||||
SprinklrMiddleware().middleware,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
import com.tangem.datasource.config.models.ChatConfig
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.common.feedback.FeedbackData
|
||||
|
|
@ -13,9 +15,7 @@ import com.tangem.tap.common.redux.ErrorAction
|
|||
import com.tangem.tap.common.redux.NotificationAction
|
||||
import com.tangem.tap.common.redux.StateDialog
|
||||
import com.tangem.tap.common.redux.ToastNotificationAction
|
||||
import com.tangem.datasource.config.models.ZendeskConfig
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||
import com.tangem.tap.features.details.redux.SecurityOption
|
||||
|
|
@ -36,18 +36,20 @@ sealed class GlobalAction : Action {
|
|||
sealed class Onboarding : GlobalAction() {
|
||||
/**
|
||||
* Initiate an onboarding process.
|
||||
* For SaltPay cards it's additionally checks for unfinished backup.
|
||||
* For resuming unfinished backup for standard Wallet cards see CheckForUnfinishedBackup and
|
||||
* StartForUnfinishedBackup
|
||||
* For resuming unfinished backup of standard Wallet and SaltPay cards see
|
||||
* BackupAction.CheckForUnfinishedBackup, GlobalAction.Onboarding.StartForUnfinishedBackup
|
||||
*/
|
||||
data class Start(val scanResponse: ScanResponse, val canSkipBackup: Boolean = true) : Onboarding()
|
||||
|
||||
/**
|
||||
* Initiate resuming of unfinished backup only for standard Wallet cards.
|
||||
* For SaltPay cards unfinished backup resumed after scanning the card on HomeScreen through Onboarding.Start.
|
||||
* See more Onboarding.Start, CheckForUnfinishedBackup
|
||||
* Initiate resuming of unfinished backup for standard Wallet and SaltPay cards.
|
||||
* See more BackupAction.CheckForUnfinishedBackup
|
||||
*/
|
||||
data class StartForUnfinishedBackup(val addedBackupCardsCount: Int) : Onboarding()
|
||||
data class StartForUnfinishedBackup(
|
||||
val addedBackupCardsCount: Int,
|
||||
val isSaltPayVisa: Boolean,
|
||||
) : Onboarding()
|
||||
|
||||
object Stop : Onboarding()
|
||||
}
|
||||
|
||||
|
|
@ -87,7 +89,7 @@ sealed class GlobalAction : Action {
|
|||
data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction()
|
||||
|
||||
data class SendEmail(val feedbackData: FeedbackData) : GlobalAction()
|
||||
data class OpenChat(val feedbackData: FeedbackData, val zendeskConfig: ZendeskConfig? = null) : GlobalAction()
|
||||
data class OpenChat(val feedbackData: FeedbackData, val chatConfig: ChatConfig? = null) : GlobalAction()
|
||||
data class UpdateFeedbackInfo(val walletManagers: List<WalletManager>) : GlobalAction()
|
||||
|
||||
object ExchangeManager : GlobalAction() {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@ package com.tangem.tap.common.redux.global
|
|||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.extensions.ifNotNull
|
||||
import com.tangem.datasource.config.models.Config
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import com.tangem.domain.common.ProductType
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
|
|
@ -14,16 +17,22 @@ import com.tangem.tap.common.redux.AppState
|
|||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||
import com.tangem.tap.features.send.redux.SendAction
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.network.exchangeServices.BuyExchangeService
|
||||
import com.tangem.tap.network.exchangeServices.CardExchangeRules
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoApi
|
||||
import com.tangem.tap.network.exchangeServices.ExchangeService
|
||||
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoEnvironment
|
||||
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService
|
||||
import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
|
||||
import com.tangem.tap.network.exchangeServices.utorg.UtorgAuthProvider
|
||||
import com.tangem.tap.network.exchangeServices.utorg.UtorgEnvironment
|
||||
import com.tangem.tap.network.exchangeServices.utorg.UtorgExchangeService
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.tap.userTokensRepository
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.DispatchFunction
|
||||
|
|
@ -98,17 +107,16 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
|
|||
val scanResponse = globalState.scanResponse ?: globalState.onboardingState.onboardingManager?.scanResponse
|
||||
|
||||
// if config not set -> try to get it based on a scanResponse.productType
|
||||
val unsafeZendeskConfig = action.zendeskConfig ?: when {
|
||||
scanResponse?.cardTypesResolver?.isSaltPay() == true -> config.saltPayConfig?.zendesk
|
||||
val unsafeChatConfig = action.chatConfig ?: when {
|
||||
scanResponse?.cardTypesResolver?.isSaltPay() == true -> config.saltPayConfig?.sprinklr
|
||||
else -> config.zendesk
|
||||
}
|
||||
|
||||
val zendeskConfig = unsafeZendeskConfig.guard {
|
||||
val chatConfig = unsafeChatConfig.guard {
|
||||
store.dispatchDebugErrorNotification("ZendeskConfig not initialized")
|
||||
return
|
||||
}
|
||||
feedbackManager.initChat(zendeskConfig)
|
||||
feedbackManager.openChat(action.feedbackData)
|
||||
feedbackManager.openChat(chatConfig, action.feedbackData)
|
||||
}
|
||||
is GlobalAction.UpdateWalletSignedHashes -> {
|
||||
store.dispatch(WalletAction.Warnings.CheckRemainingSignatures(action.remainingSignatures))
|
||||
|
|
@ -119,38 +127,23 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
|
|||
}
|
||||
is GlobalAction.ExchangeManager.Init -> {
|
||||
val appStateSafe = appState() ?: return
|
||||
val config = appStateSafe.globalState.configManager?.config
|
||||
ifNotNull(
|
||||
config?.mercuryoWidgetId,
|
||||
config?.mercuryoSecret,
|
||||
config?.moonPayApiKey,
|
||||
config?.moonPayApiSecretKey,
|
||||
) { mercuryoWidgetId, mercuryoSecret, moonPayKey, moonPaySecretKey ->
|
||||
scope.launch {
|
||||
val buyService = MercuryoService(
|
||||
apiVersion = MercuryoApi.API_VERSION,
|
||||
mercuryoWidgetId = mercuryoWidgetId,
|
||||
secret = mercuryoSecret,
|
||||
logEnabled = LogConfig.network.mercuryoService,
|
||||
)
|
||||
val sellService = MoonPayService(
|
||||
apiKey = moonPayKey,
|
||||
secretKey = moonPaySecretKey,
|
||||
logEnabled = LogConfig.network.moonPayService,
|
||||
)
|
||||
val cardProvider = {
|
||||
store.state.globalState.scanResponse?.card
|
||||
?: store.state.globalState.onboardingState.onboardingManager?.scanResponse?.card
|
||||
}
|
||||
val config = appStateSafe.globalState.configManager?.config ?: return
|
||||
|
||||
val exchangeManager = CurrencyExchangeManager(
|
||||
buyService = buyService,
|
||||
sellService = sellService,
|
||||
primaryRules = CardExchangeRules(cardProvider),
|
||||
)
|
||||
store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager))
|
||||
store.dispatchOnMain(GlobalAction.ExchangeManager.Update)
|
||||
scope.launch {
|
||||
val scanResponseProvider: () -> ScanResponse? = {
|
||||
store.state.globalState.scanResponse
|
||||
?: store.state.globalState.onboardingState.onboardingManager?.scanResponse
|
||||
}
|
||||
val productTypeProvider: () -> ProductType? = { scanResponseProvider.invoke()?.productType }
|
||||
val cardProvider: () -> CardDTO? = { scanResponseProvider.invoke()?.card }
|
||||
|
||||
val exchangeManager = CurrencyExchangeManager(
|
||||
buyService = makeBuyExchangeService(config, productTypeProvider),
|
||||
sellService = makeSellExchangeService(config),
|
||||
primaryRules = CardExchangeRules(cardProvider),
|
||||
)
|
||||
store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager))
|
||||
store.dispatchOnMain(GlobalAction.ExchangeManager.Update)
|
||||
}
|
||||
}
|
||||
is GlobalAction.ExchangeManager.Init.Success -> {}
|
||||
|
|
@ -202,4 +195,38 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeSellExchangeService(config: Config): ExchangeService {
|
||||
return MoonPayService(
|
||||
apiKey = config.moonPayApiKey,
|
||||
secretKey = config.moonPayApiSecretKey,
|
||||
logEnabled = LogConfig.network.moonPayService,
|
||||
)
|
||||
}
|
||||
|
||||
private fun makeBuyExchangeService(config: Config, productTypeProvider: () -> ProductType?): ExchangeService {
|
||||
return BuyExchangeService(
|
||||
productTypeProvider = productTypeProvider,
|
||||
mercuryoService = makeMercuryoExchangeService(config),
|
||||
utorgService = makeUtorgExchangeService(config),
|
||||
)
|
||||
}
|
||||
|
||||
private fun makeMercuryoExchangeService(config: Config): MercuryoService {
|
||||
val mercuryoEnvironment = MercuryoEnvironment.prod(config.mercuryoWidgetId, config.mercuryoSecret)
|
||||
return MercuryoService(mercuryoEnvironment)
|
||||
}
|
||||
|
||||
private fun makeUtorgExchangeService(config: Config): UtorgExchangeService {
|
||||
val saltPayConfig = requireNotNull(config.saltPayConfig)
|
||||
|
||||
val utorgAuthProvider = UtorgAuthProvider(saltPayConfig.kycProvider.sidValue)
|
||||
val utorgEnvironment = if (BuildConfig.DEBUG) {
|
||||
UtorgEnvironment.stage(utorgAuthProvider, LogConfig.network.utorgService)
|
||||
// UtorgEnvironment.mock()
|
||||
} else {
|
||||
UtorgEnvironment.prod(utorgAuthProvider)
|
||||
}
|
||||
return UtorgExchangeService(utorgEnvironment)
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import com.tangem.blockchain.common.WalletManagerFactory
|
|||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
|
|
@ -18,6 +19,7 @@ import com.tangem.domain.common.extensions.withMainContext
|
|||
import com.tangem.operations.attestation.Attestation
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.safeUpdate
|
||||
import com.tangem.tap.common.extensions.setContext
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
|
||||
|
|
@ -100,6 +102,7 @@ class TapWalletManager {
|
|||
}
|
||||
|
||||
suspend fun onWalletSelected(userWallet: UserWallet, refresh: Boolean) {
|
||||
Analytics.setContext(userWallet.scanResponse)
|
||||
val scanResponse = userWallet.scanResponse
|
||||
val card = scanResponse.card
|
||||
val attestationFailed = card.attestation.status == Attestation.Status.Failed
|
||||
|
|
|
|||
|
|
@ -1,19 +1,15 @@
|
|||
package com.tangem.tap.domain.extensions
|
||||
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.DerivationParams
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.common.WalletManagerFactory
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.common.hdWallet.DerivationPath
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.SaltPayWorkaround
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
|
||||
|
|
@ -138,24 +134,4 @@ private fun selectWallet(wallets: List<CardDTO.Wallet>): CardDTO.Wallet? {
|
|||
1 -> wallets[0]
|
||||
else -> wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: wallets[0]
|
||||
}
|
||||
}
|
||||
|
||||
fun WalletManagerFactory.makeSaltPayWalletManager(
|
||||
scanResponse: ScanResponse,
|
||||
): EthereumWalletManager {
|
||||
val blockchain = scanResponse.cardTypesResolver.getBlockchain()
|
||||
if (blockchain != Blockchain.SaltPay) {
|
||||
error("WalletManager for the SaltPay can be created based only on Blockchain.SaltPay")
|
||||
}
|
||||
|
||||
val token = SaltPayWorkaround.tokenFrom(blockchain)
|
||||
val cardWallet = scanResponse.card.wallets.firstOrNull().guard {
|
||||
error("SaltPay card must have one wallet at least")
|
||||
}
|
||||
|
||||
return makeWalletManager(
|
||||
blockchain = blockchain,
|
||||
publicKey = Wallet.PublicKey(cardWallet.publicKey, null, null),
|
||||
tokens = listOf(token),
|
||||
) as EthereumWalletManager
|
||||
}
|
||||
|
|
@ -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<TransactionData>?,
|
||||
) {
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.tangem.common.extensions.toHexString
|
|||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.ProductType
|
||||
import com.tangem.domain.common.SaltPayWorkaround
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.common.TwinCardNumber
|
||||
|
|
@ -77,6 +78,7 @@ class UserWalletBuilder(
|
|||
return when {
|
||||
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
|
||||
cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL
|
||||
SaltPayWorkaround.isSaltPayCardId(cardId) -> Artwork.SALT_PAY_URL
|
||||
else -> when (TwinsHelper.getTwinCardNumber(cardId)) {
|
||||
TwinCardNumber.First -> Artwork.TWIN_CARD_1
|
||||
TwinCardNumber.Second -> Artwork.TWIN_CARD_2
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ import com.tangem.domain.common.extensions.withMainContext
|
|||
import com.tangem.operations.backup.BackupService
|
||||
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
|
||||
import com.tangem.tap.backupService
|
||||
import com.tangem.tap.common.analytics.paramsInterceptor.BatchIdParamsInterceptor
|
||||
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
|
||||
import com.tangem.tap.common.extensions.addContext
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa
|
||||
import com.tangem.tap.common.extensions.setContext
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
|
|
@ -27,10 +29,10 @@ import com.tangem.tap.features.onboarding.OnboardingHelper
|
|||
import com.tangem.tap.features.onboarding.OnboardingSaltPayHelper
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManagerFactory
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
|
|
@ -75,7 +77,7 @@ object ScanCardProcessor {
|
|||
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
|
||||
|
||||
onScanStateChange(false)
|
||||
sendAnalytics(analyticsEvent, scanResponse.card.batchId)
|
||||
sendAnalytics(analyticsEvent, scanResponse)
|
||||
|
||||
checkForUnfinishedBackupForSaltPay(
|
||||
backupService = backupService,
|
||||
|
|
@ -103,10 +105,17 @@ object ScanCardProcessor {
|
|||
|
||||
private fun sendAnalytics(
|
||||
analyticsEvent: AnalyticsEvent?,
|
||||
batchId: String,
|
||||
scanResponse: ScanResponse,
|
||||
) {
|
||||
Analytics.addParamsInterceptor(BatchIdParamsInterceptor(batchId))
|
||||
analyticsEvent?.let { Analytics.send(it) }
|
||||
analyticsEvent?.let {
|
||||
// this workaround needed to send CardWasScannedEvent without adding a context
|
||||
val interceptor = CardContextInterceptor(scanResponse)
|
||||
val params = it.params.toMutableMap()
|
||||
interceptor.intercept(params)
|
||||
it.params = params.toMap()
|
||||
|
||||
Analytics.send(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -188,20 +197,25 @@ object ScanCardProcessor {
|
|||
|
||||
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(scanResponse))
|
||||
|
||||
if (scanResponse.cardTypesResolver.isSaltPay()) {
|
||||
if (scanResponse.cardTypesResolver.isSaltPayVisa()) {
|
||||
val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
|
||||
val cardTypesResolver = scanResponse.cardTypesResolver
|
||||
if (cardTypesResolver.isSaltPay()) {
|
||||
if (cardTypesResolver.isSaltPayVisa()) {
|
||||
val manager = SaltPayActivationManagerFactory(
|
||||
blockchain = scanResponse.cardTypesResolver.getBlockchain(),
|
||||
card = scanResponse.card,
|
||||
).create()
|
||||
val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse, manager)
|
||||
delay(500)
|
||||
withMainContext {
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
Analytics.addContext(scanResponse)
|
||||
val isOnboardingCase = result.data
|
||||
if (isOnboardingCase) {
|
||||
onWalletNotCreated()
|
||||
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
|
||||
store.dispatch(OnboardingSaltPayAction.SetDependencies(manager, config))
|
||||
store.dispatch(OnboardingSaltPayAction.Update)
|
||||
store.dispatch(OnboardingSaltPayAction.SetDependencies(manager))
|
||||
store.dispatch(OnboardingSaltPayAction.Update(withAnalytics = false))
|
||||
navigateTo(AppScreen.OnboardingWallet) { onProgressStateChange(it) }
|
||||
} else {
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
|
|
@ -222,16 +236,19 @@ object ScanCardProcessor {
|
|||
SaltPayExceptionHandler.handle(error)
|
||||
onFailure(TangemSdkError.ExceptionError(error))
|
||||
} else {
|
||||
Analytics.setContext(scanResponse)
|
||||
onSuccess(scanResponse)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
|
||||
Analytics.addContext(scanResponse)
|
||||
onWalletNotCreated()
|
||||
store.dispatchOnMain(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = true))
|
||||
val appScreen = OnboardingHelper.whereToNavigate(scanResponse)
|
||||
navigateTo(appScreen) { onProgressStateChange(it) }
|
||||
} else {
|
||||
Analytics.setContext(scanResponse)
|
||||
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
|
||||
onWalletNotCreated()
|
||||
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import com.tangem.domain.common.ScanResponse
|
|||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.common.extensions.replaceByOrAdd
|
||||
import com.tangem.tap.common.extensions.safeUpdate
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
import com.tangem.tap.domain.walletStores.WalletStoresError
|
||||
|
|
@ -31,6 +32,7 @@ import com.tangem.tap.domain.walletStores.repository.implementation.utils.update
|
|||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithFiatRates
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithMissedDerivation
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithRent
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithTxHistories
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithUnreachable
|
||||
import com.tangem.tap.domain.walletStores.storage.WalletManagerStorage
|
||||
import com.tangem.tap.domain.walletStores.storage.WalletStoresStorage
|
||||
|
|
@ -204,7 +206,7 @@ internal class DefaultWalletAmountsRepository(
|
|||
updateWalletStoreWithUnreachable(walletStore)
|
||||
}
|
||||
else -> {
|
||||
withInternetConnection { walletManager.update() }
|
||||
withInternetConnection { walletManager.safeUpdate() }
|
||||
.map { updateWalletManagerWithAmounts(userWalletId, walletManager) }
|
||||
.flatMap {
|
||||
updateWalletStoreWithAmounts(
|
||||
|
|
@ -319,7 +321,7 @@ internal class DefaultWalletAmountsRepository(
|
|||
it.updateWithDemoAmounts(wallet = updatedWallet)
|
||||
} else {
|
||||
it.updateWithAmounts(wallet = updatedWallet)
|
||||
}
|
||||
}.updateWithTxHistories(wallet = updatedWallet)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<WalletDataModel>.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<WalletDataModel>.updateWithTxHistories(
|
||||
wallet: Wallet,
|
||||
): List<WalletDataModel> {
|
||||
return this.map { walletData ->
|
||||
walletData.updateWithTxHistory(wallet)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun List<WalletDataModel>.updateWithAmounts(wallet: Wallet): List<WalletDataModel> {
|
||||
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<WalletDataModel>.updateWithAmounts(wallet: Wallet): List<WalletDataModel> {
|
||||
return this.map { walletData ->
|
||||
walletData.updateWithAmount(wallet)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun List<WalletDataModel>.updateWithDemoAmounts(wallet: Wallet): List<WalletDataModel> {
|
||||
return this.map { walletData ->
|
||||
walletData.updateWithDemoAmount(wallet)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ object DemoHelper {
|
|||
|
||||
fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId)
|
||||
|
||||
fun isTestDemoCard(scanResponse: ScanResponse): Boolean = config.isTestDemoCardId(scanResponse.card.cardId)
|
||||
|
||||
fun isDemoCardId(cardId: String): Boolean = config.isDemoCardId(cardId)
|
||||
|
||||
fun tryHandle(appState: () -> AppState?, action: Action): Boolean {
|
||||
|
|
@ -108,6 +110,8 @@ class DemoConfig {
|
|||
|
||||
fun isDemoCardId(cardId: String): Boolean = demoCardIds.contains(cardId)
|
||||
|
||||
fun isTestDemoCardId(cardId: String): Boolean = testDemoCardIds.contains(cardId)
|
||||
|
||||
fun getBalance(blockchain: Blockchain): Amount = walletBalances[blockchain]?.copy()
|
||||
?: Amount(BigDecimal.ZERO, blockchain).copy()
|
||||
|
||||
|
|
@ -118,9 +122,6 @@ class DemoConfig {
|
|||
@Suppress("ClassOrdering")
|
||||
private val releaseDemoCardIds = mutableListOf(
|
||||
// === Not from the Google Sheet table ===
|
||||
"FB10000000000196", // Note BTC
|
||||
"FB20000000000186", // Note ETH
|
||||
"FB30000000000176", // Wallet
|
||||
"AC01000000041225",
|
||||
"AC01000000041472",
|
||||
"AB01000000046498",
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ sealed class DetailsAction : Action {
|
|||
object Success : ResetToFactory()
|
||||
}
|
||||
|
||||
object CreateBackup : DetailsAction()
|
||||
|
||||
object ScanCard : DetailsAction()
|
||||
|
||||
data class PrepareCardSettingsData(val card: CardDTO, val cardTypesResolver: CardTypesResolver) : DetailsAction()
|
||||
|
|
|
|||
|
|
@ -66,12 +66,6 @@ class DetailsMiddleware {
|
|||
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet))
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins))
|
||||
}
|
||||
is DetailsAction.CreateBackup -> {
|
||||
state.scanResponse?.let {
|
||||
store.dispatch(GlobalAction.Onboarding.Start(it, canSkipBackup = false))
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
|
||||
}
|
||||
}
|
||||
DetailsAction.ScanCard -> {
|
||||
scope.launch {
|
||||
tangemSdkManager.scanProduct(userTokensRepository)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import com.tangem.tap.common.redux.AppState
|
|||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.features.details.redux.DetailsAction
|
||||
import com.tangem.tap.features.details.redux.DetailsState
|
||||
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
|
||||
import com.tangem.tap.features.home.LocaleRegionProvider
|
||||
|
|
@ -86,7 +85,7 @@ class DetailsViewModel(private val store: Store<AppState>) {
|
|||
}
|
||||
SettingsElement.LinkMoreCards -> {
|
||||
Analytics.send(Settings.ButtonCreateBackup())
|
||||
store.dispatch(DetailsAction.CreateBackup)
|
||||
store.dispatch(WalletAction.MultiWallet.BackupWallet)
|
||||
}
|
||||
SettingsElement.TermsOfService -> {
|
||||
store.dispatch(DisclaimerAction.Show(AppScreen.Details))
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Analytics.send(IntroductionProcess.ScreenOpened())
|
||||
store.dispatch(HomeAction.OnCreate)
|
||||
store.dispatch(HomeAction.Init)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@ import com.tangem.common.doOnResult
|
|||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.AnalyticsEvent
|
||||
import com.tangem.tap.common.analytics.events.IntroductionProcess
|
||||
import com.tangem.tap.common.analytics.events.Shop
|
||||
import com.tangem.tap.common.entities.IndeterminateProgressButton
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.dispatchOpenUrl
|
||||
import com.tangem.tap.common.extensions.eraseContext
|
||||
import com.tangem.tap.common.extensions.onCardScanned
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
|
|
@ -49,6 +51,10 @@ private val homeMiddleware: Middleware<AppState> = { _, _ ->
|
|||
|
||||
private fun handleHomeAction(action: Action) {
|
||||
when (action) {
|
||||
is HomeAction.OnCreate -> {
|
||||
Analytics.eraseContext()
|
||||
Analytics.send(IntroductionProcess.ScreenOpened())
|
||||
}
|
||||
is HomeAction.Init -> {
|
||||
store.dispatch(GlobalAction.RestoreAppCurrency)
|
||||
store.dispatch(GlobalAction.ExchangeManager.Init)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
package com.tangem.tap.features.onboarding
|
||||
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.common.ProductType
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.onCardScanned
|
||||
import com.tangem.tap.common.extensions.removeContext
|
||||
import com.tangem.tap.common.extensions.setContext
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
|
||||
|
|
@ -55,6 +58,7 @@ object OnboardingHelper {
|
|||
accessCode: String? = null,
|
||||
backupCardsIds: List<String>? = null,
|
||||
) {
|
||||
Analytics.setContext(scanResponse)
|
||||
when {
|
||||
// When should save user wallets, then save card without navigate to save wallet screen
|
||||
preferencesStorage.shouldSaveUserWallets -> scope.launch {
|
||||
|
|
@ -92,4 +96,8 @@ object OnboardingHelper {
|
|||
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
|
||||
}
|
||||
|
||||
fun onInterrupted() {
|
||||
Analytics.removeContext()
|
||||
}
|
||||
}
|
||||
|
|
@ -13,16 +13,20 @@ import com.tangem.tap.preferencesStorage
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object OnboardingSaltPayHelper {
|
||||
suspend fun isOnboardingCase(scanResponse: ScanResponse, manager: SaltPayActivationManager): Result<Boolean> {
|
||||
|
||||
suspend fun isOnboardingCase(
|
||||
scanResponse: ScanResponse,
|
||||
manager: SaltPayActivationManager,
|
||||
): Result<Boolean> {
|
||||
return try {
|
||||
var updatedStep = manager.update(SaltPayActivationStep.None, null).successOr { return it }
|
||||
val updatedStep = manager.update(SaltPayActivationStep.None, null).successOr { return it }
|
||||
|
||||
val cardStorage = preferencesStorage.usedCardsPrefStorage
|
||||
val activationIsFinished = cardStorage.isActivationFinished(scanResponse.card.cardId)
|
||||
if (updatedStep == SaltPayActivationStep.Success && activationIsFinished) {
|
||||
updatedStep = SaltPayActivationStep.Finished
|
||||
if (updatedStep == SaltPayActivationStep.Success && !activationIsFinished) {
|
||||
cardStorage.activationFinished(scanResponse.card.cardId)
|
||||
}
|
||||
val isActivationCase = updatedStep != SaltPayActivationStep.Finished
|
||||
val isActivationCase = updatedStep != SaltPayActivationStep.Success
|
||||
val isBackupCase = scanResponse.card.backupStatus?.isActive == false
|
||||
Result.Success(isActivationCase || isBackupCase)
|
||||
} catch (ex: Exception) {
|
||||
|
|
@ -31,4 +35,16 @@ object OnboardingSaltPayHelper {
|
|||
Result.Failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
fun testProceedToOnboarding(
|
||||
scanResponse: ScanResponse,
|
||||
manager: SaltPayActivationManager,
|
||||
): Result<Boolean> = Result.Success(true)
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
fun testProceedToMainScreen(
|
||||
scanResponse: ScanResponse,
|
||||
manager: SaltPayActivationManager,
|
||||
): Result<Boolean> = Result.Success(false)
|
||||
}
|
||||
|
|
@ -196,6 +196,7 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
|
|||
store.dispatchDialogShow(
|
||||
OnboardingDialog.InterruptOnboarding(
|
||||
onOk = {
|
||||
OnboardingHelper.onInterrupted()
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -118,4 +118,9 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCards
|
|||
transition.interpolator = OvershootInterpolator()
|
||||
TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition)
|
||||
}
|
||||
|
||||
override fun handleOnBackPressed() {
|
||||
store.dispatch(OnboardingOtherCardsAction.OnBackPressed)
|
||||
super.handleOnBackPressed()
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ sealed class OnboardingOtherCardsAction : Action {
|
|||
class SetArtworkUrl(val artworkUrl: String) : OnboardingOtherCardsAction()
|
||||
object DetermineStepOfScreen : OnboardingOtherCardsAction()
|
||||
object Done : OnboardingOtherCardsAction()
|
||||
object OnBackPressed : OnboardingOtherCardsAction()
|
||||
|
||||
data class SetStepOfScreen(val step: OnboardingOtherCardsStep) : OnboardingOtherCardsAction()
|
||||
|
||||
|
|
|
|||
|
|
@ -119,9 +119,7 @@ private fun handleOtherCardsAction(action: Action) {
|
|||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
store.dispatch(OnboardingOtherCardsAction.SetStepOfScreen(OnboardingOtherCardsStep.Done))
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
// do nothing
|
||||
}
|
||||
is CompletionResult.Failure -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -130,9 +128,9 @@ private fun handleOtherCardsAction(action: Action) {
|
|||
store.dispatch(GlobalAction.Onboarding.Stop)
|
||||
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(onboardingManager.scanResponse)
|
||||
}
|
||||
is OnboardingOtherCardsAction.Confetti.Hide,
|
||||
is OnboardingOtherCardsAction.SetArtworkUrl,
|
||||
is OnboardingOtherCardsAction.Confetti.Show,
|
||||
-> Unit
|
||||
OnboardingOtherCardsAction.OnBackPressed -> {
|
||||
OnboardingHelper.onInterrupted()
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,8 @@ package com.tangem.tap.features.onboarding.products.twins.redux
|
|||
import com.tangem.Message
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.datasource.utils.AssetReader
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.twins.TwinCardsManager
|
||||
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
|
||||
|
|
|
|||
|
|
@ -320,6 +320,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
|
|||
twinCardsState.currentStep != TwinCardsStep.TopUpWallet &&
|
||||
twinCardsState.currentStep != TwinCardsStep.Done
|
||||
|
||||
OnboardingHelper.onInterrupted()
|
||||
store.dispatch(TwinCardsAction.CardsManager.Release)
|
||||
action.shouldResetTwinCardsWidget(shouldReturnCardBack) {
|
||||
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
|
||||
|
|
|
|||
|
|
@ -411,7 +411,11 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
|
|||
TwinCardsAction.OnBackPressed { should, popAction ->
|
||||
store.dispatch(TwinCardsAction.Confetti.Hide)
|
||||
showConfetti(false)
|
||||
if (should) switchToCard(TwinCardNumber.First, true, popAction) else popAction()
|
||||
if (should) {
|
||||
switchToCard(TwinCardNumber.First, true, popAction)
|
||||
} else {
|
||||
popAction()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,9 +31,8 @@ sealed class BackupAction : Action {
|
|||
object ScanPrimaryCard : BackupAction()
|
||||
|
||||
/**
|
||||
* Check for unfinished backup of standard Wallet cards.
|
||||
* For SaltPay cards unfinished backup resumed after scanning the card on HomeScreen through Onboarding.Start.
|
||||
* See more Onboarding.Start, CheckForUnfinishedBackup, StartForUnfinishedBackup
|
||||
* Check for unfinished backup of standard Wallets and SaltPay cards
|
||||
* See more GlobalAction.Onboarding.StartForUnfinishedBackup
|
||||
*/
|
||||
object CheckForUnfinishedBackup : BackupAction()
|
||||
|
||||
|
|
@ -65,7 +64,11 @@ sealed class BackupAction : Action {
|
|||
data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction()
|
||||
data class WriteBackupCard(val cardNumber: Int) : BackupAction()
|
||||
|
||||
object FinishBackup : BackupAction()
|
||||
/**
|
||||
* It always calls for the SaltPay cards when resuming the activation process.
|
||||
*/
|
||||
data class FinishBackup(val withAnalytics: Boolean = true) : BackupAction()
|
||||
|
||||
object DiscardBackup : BackupAction()
|
||||
object DiscardSavedBackup : BackupAction()
|
||||
object ResumeFoundUnfinishedBackup : BackupAction()
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import com.tangem.tap.features.demo.DemoHelper
|
|||
import com.tangem.tap.features.home.redux.HomeAction
|
||||
import com.tangem.tap.features.onboarding.OnboardingDialog
|
||||
import com.tangem.tap.features.onboarding.OnboardingHelper
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManagerFactory
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
|
||||
import com.tangem.tap.features.wallet.redux.Artwork
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.preferencesStorage
|
||||
|
|
@ -72,7 +72,7 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
|
|||
}
|
||||
when {
|
||||
card == null -> {
|
||||
// it's possible when found unfinished backup for standard Wallet cards
|
||||
// it's possible when found unfinished backup
|
||||
store.dispatch(OnboardingWalletAction.ResumeBackup)
|
||||
}
|
||||
card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup -> {
|
||||
|
|
@ -87,10 +87,11 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
|
|||
|
||||
scanResponse.cardTypesResolver.isSaltPay() -> {
|
||||
store.dispatch(OnboardingWalletAction.GetToSaltPayStep)
|
||||
store.dispatch(BackupAction.FinishBackup)
|
||||
store.dispatch(BackupAction.FinishBackup(withAnalytics = false))
|
||||
store.dispatch(OnboardingSaltPayAction.OnSwitchedToSaltPayProcess)
|
||||
}
|
||||
|
||||
else -> store.dispatch(BackupAction.FinishBackup)
|
||||
else -> store.dispatch(BackupAction.FinishBackup())
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
|
|
@ -317,7 +318,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
is CompletionResult.Success -> {
|
||||
if (backupService.currentState == BackupService.State.Finished) {
|
||||
initSaltPayOnBackupFinishedIfNeeded(scanResponse, onboardingWalletState)
|
||||
store.dispatchOnMain(BackupAction.FinishBackup)
|
||||
store.dispatchOnMain(BackupAction.FinishBackup())
|
||||
} else {
|
||||
store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(action.cardNumber + 1))
|
||||
}
|
||||
|
|
@ -328,7 +329,9 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
}
|
||||
}
|
||||
is BackupAction.FinishBackup -> {
|
||||
Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber))
|
||||
if (action.withAnalytics) {
|
||||
Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber))
|
||||
}
|
||||
if (!onboardingWalletState.isSaltPay) {
|
||||
Analytics.send(Onboarding.Finished())
|
||||
finishCardActivation(backupState, card)
|
||||
|
|
@ -345,18 +348,23 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
backupService.discardSavedBackup()
|
||||
}
|
||||
is BackupAction.CheckForUnfinishedBackup -> {
|
||||
if (backupService.hasIncompletedBackup && !backupService.primaryCardIsSaltPayVisa()) {
|
||||
if (backupService.hasIncompletedBackup) {
|
||||
store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound))
|
||||
}
|
||||
}
|
||||
is BackupAction.ResumeFoundUnfinishedBackup -> {
|
||||
store.dispatch(GlobalAction.Onboarding.StartForUnfinishedBackup(backupService.addedBackupCardsCount))
|
||||
store.dispatch(
|
||||
GlobalAction.Onboarding.StartForUnfinishedBackup(
|
||||
addedBackupCardsCount = backupService.addedBackupCardsCount,
|
||||
isSaltPayVisa = backupService.primaryCardIsSaltPayVisa(),
|
||||
),
|
||||
)
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
|
||||
}
|
||||
is BackupAction.DismissBackup -> {
|
||||
Analytics.send(Onboarding.Backup.Skipped())
|
||||
if (onboardingWalletState.isSaltPay) throw UnsupportedOperationException()
|
||||
store.dispatch(BackupAction.FinishBackup)
|
||||
store.dispatch(BackupAction.FinishBackup())
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
|
@ -395,14 +403,18 @@ private fun finishCardsActivationForDiscardedUnfinishedBackup(cardId: String) {
|
|||
*/
|
||||
private fun initSaltPayOnBackupFinishedIfNeeded(
|
||||
scanResponse: ScanResponse?,
|
||||
onboardingWalletState: OnboardingWalletState,
|
||||
state: OnboardingWalletState,
|
||||
) {
|
||||
if (onboardingWalletState.isSaltPay && onboardingWalletState.onboardingSaltPayState == null) {
|
||||
if (scanResponse == null) error("scanning response is null")
|
||||
if (state.backupState.isInterruptedBackup) return
|
||||
if (scanResponse == null) return
|
||||
|
||||
val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
|
||||
store.dispatchOnMain(OnboardingSaltPayAction.SetDependencies(manager, config))
|
||||
store.dispatchOnMain(OnboardingSaltPayAction.Update)
|
||||
if (state.isSaltPay && state.onboardingSaltPayState == null) {
|
||||
val manager = SaltPayActivationManagerFactory(
|
||||
blockchain = scanResponse.cardTypesResolver.getBlockchain(),
|
||||
card = scanResponse.card,
|
||||
).create()
|
||||
store.dispatchOnMain(OnboardingSaltPayAction.SetDependencies(manager))
|
||||
store.dispatchOnMain(OnboardingSaltPayAction.Update(false))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -420,6 +432,7 @@ private fun handleOnBackPressed(state: OnboardingWalletState) {
|
|||
if (state.isSaltPay) {
|
||||
showInterruptOnboardingDialog()
|
||||
} else {
|
||||
OnboardingHelper.onInterrupted()
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
}
|
||||
}
|
||||
|
|
@ -430,6 +443,7 @@ private fun showInterruptOnboardingDialog() {
|
|||
store.dispatchDialogShow(
|
||||
OnboardingDialog.InterruptOnboarding(
|
||||
onOk = {
|
||||
OnboardingHelper.onInterrupted()
|
||||
store.dispatch(BackupAction.DiscardBackup)
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
},
|
||||
|
|
|
|||
|
|
@ -38,13 +38,13 @@ private object ReducerForGlobalAction {
|
|||
fun reduce(action: GlobalAction.Onboarding, state: OnboardingWalletState): OnboardingWalletState {
|
||||
return when (action) {
|
||||
is GlobalAction.Onboarding.Start -> {
|
||||
val isSaltPay = action.scanResponse.cardTypesResolver.isSaltPay()
|
||||
OnboardingWalletState(
|
||||
backupState = state.backupState.copy(
|
||||
maxBackupCards = if (action.scanResponse.cardTypesResolver.isSaltPay()) 1 else 2,
|
||||
canSkipBackup =
|
||||
if (action.scanResponse.cardTypesResolver.isSaltPay()) false else action.canSkipBackup,
|
||||
maxBackupCards = maxBackupCards(isSaltPay),
|
||||
canSkipBackup = if (isSaltPay) false else action.canSkipBackup,
|
||||
),
|
||||
isSaltPay = action.scanResponse.cardTypesResolver.isSaltPay(),
|
||||
isSaltPay = isSaltPay,
|
||||
)
|
||||
}
|
||||
is GlobalAction.Onboarding.StartForUnfinishedBackup -> {
|
||||
|
|
@ -52,13 +52,16 @@ private object ReducerForGlobalAction {
|
|||
backupState = state.backupState.copy(
|
||||
maxBackupCards = action.addedBackupCardsCount,
|
||||
canSkipBackup = false,
|
||||
isInterruptedBackup = true,
|
||||
),
|
||||
isSaltPay = false,
|
||||
isSaltPay = action.isSaltPayVisa,
|
||||
)
|
||||
}
|
||||
else -> state
|
||||
}
|
||||
}
|
||||
|
||||
private fun maxBackupCards(isSaltPay: Boolean): Int = if (isSaltPay) 1 else 2
|
||||
}
|
||||
|
||||
private object BackupReducer {
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ data class OnboardingWalletState(
|
|||
-> 9
|
||||
SaltPayActivationStep.Claim -> 10
|
||||
SaltPayActivationStep.ClaimInProgress -> 11
|
||||
SaltPayActivationStep.ClaimSuccess, SaltPayActivationStep.Success, SaltPayActivationStep.Finished,
|
||||
SaltPayActivationStep.ClaimSuccess, SaltPayActivationStep.Success,
|
||||
-> getMaxProgress()
|
||||
}
|
||||
}
|
||||
|
|
@ -87,6 +87,7 @@ data class BackupState(
|
|||
val backupStep: BackupStep = BackupStep.InitBackup,
|
||||
val maxBackupCards: Int = 2,
|
||||
val canSkipBackup: Boolean = true,
|
||||
val isInterruptedBackup: Boolean = false,
|
||||
)
|
||||
|
||||
enum class AccessCodeError {
|
||||
|
|
|
|||
|
|
@ -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<Boolean> {
|
||||
// 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<Amount> {
|
||||
// return Result.Success(Amount(BigDecimal(0.1), walletManager.wallet.blockchain)) // test
|
||||
return walletManager.getAllowance(addressTreasureSafe, token)
|
||||
}
|
||||
|
||||
|
|
@ -154,6 +159,13 @@ class GnosisRegistrator(
|
|||
return Result.Failure(BlockchainSdkError.WrappedThrowable(it.error))
|
||||
}
|
||||
}
|
||||
|
||||
// return transferFromStandardWay(amountToClaim, signer)
|
||||
return transferFromHardcodeWay(amountToClaim, signer)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private suspend fun transferFromStandardWay(amountToClaim: BigDecimal, signer: TransactionSigner): Result<Unit> {
|
||||
val amount = Amount(token, amountToClaim)
|
||||
val feeAmount = walletManager.getFeeToTransferFrom(amount, addressTreasureSafe)
|
||||
.extractFeeAmount().successOr { return it }
|
||||
|
|
@ -164,6 +176,34 @@ class GnosisRegistrator(
|
|||
return Result.Success(Unit)
|
||||
}
|
||||
|
||||
private suspend fun transferFromHardcodeWay(amountToClaim: BigDecimal, signer: TransactionSigner): Result<Unit> {
|
||||
val amount = Amount(token, amountToClaim)
|
||||
val gasPrice = walletManager.getGasPrice().successOr { return it }
|
||||
|
||||
val hardcodeGasLimit = BigInteger("300000")
|
||||
val hardcodeFeeValue = (hardcodeGasLimit * gasPrice).toBigDecimal(
|
||||
scale = blockchain.decimals(),
|
||||
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
|
||||
).movePointLeft(blockchain.decimals())
|
||||
val hardcodeFeeAmount = Amount(hardcodeFeeValue, blockchain)
|
||||
|
||||
val transactionData = walletManager.createTransferFromTransaction(
|
||||
amount = amount,
|
||||
fee = hardcodeFeeAmount,
|
||||
source = addressTreasureSafe,
|
||||
)
|
||||
val transactionToSign = walletManager.transactionBuilder.buildTransferFromToSign(
|
||||
transactionData,
|
||||
walletManager.txCount.toBigInteger(),
|
||||
hardcodeGasLimit,
|
||||
) ?: return Result.Failure(BlockchainSdkError.CustomError("Not enough data"))
|
||||
|
||||
return when (val result = walletManager.signAndSend(transactionToSign, signer)) {
|
||||
SimpleResult.Success -> Result.Success(Unit)
|
||||
is SimpleResult.Failure -> Result.Failure(result.error)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun Result<List<Amount>>.extractFeeAmount(): Result<Amount> {
|
||||
return when (this) {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,22 @@
|
|||
@file:Suppress("MaximumLineLength")
|
||||
|
||||
package com.tangem.tap.features.onboarding.products.wallet.saltPay
|
||||
|
||||
import android.net.Uri
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
|
||||
import com.tangem.blockchain.blockchains.ethereum.SignedEthereumTransaction
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.TransactionSigner
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManagerFactory
|
||||
import com.tangem.blockchain.extensions.successOr
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.services.Result
|
||||
|
|
@ -23,15 +31,28 @@ import com.tangem.datasource.api.paymentology.models.response.RegistrationRespon
|
|||
import com.tangem.datasource.api.paymentology.models.response.tryExtractError
|
||||
import com.tangem.datasource.config.models.KYCProvider
|
||||
import com.tangem.datasource.config.models.SaltPayConfig
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.SaltPayWorkaround
|
||||
import com.tangem.domain.common.extensions.successOr
|
||||
import com.tangem.operations.attestation.AttestWalletKeyResponse
|
||||
import com.tangem.tap.common.extensions.safeUpdate
|
||||
import com.tangem.tap.domain.getFirstToken
|
||||
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
|
||||
import com.tangem.tap.store
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
import kotlin.ByteArray
|
||||
import kotlin.Exception
|
||||
import kotlin.IllegalStateException
|
||||
import kotlin.NullPointerException
|
||||
import kotlin.String
|
||||
import kotlin.Suppress
|
||||
import kotlin.Unit
|
||||
import kotlin.UnsupportedOperationException
|
||||
import kotlin.byteArrayOf
|
||||
import kotlin.minus
|
||||
import com.tangem.blockchain.extensions.Result as BlockchainResult
|
||||
|
||||
/**
|
||||
|
|
@ -227,4 +248,97 @@ class KYCUrlProvider(
|
|||
.appendQueryParameter(kycProvider.externalIdParameterKey, kycRefId)
|
||||
.build().toString()
|
||||
}
|
||||
}
|
||||
|
||||
class SaltPayActivationManagerFactory(
|
||||
private val blockchain: Blockchain,
|
||||
private val card: CardDTO,
|
||||
) {
|
||||
|
||||
fun create(): SaltPayActivationManager {
|
||||
val wallet = card.wallets.first()
|
||||
val saltPayConfig = store.state.globalState.configManager?.config?.saltPayConfig.guard {
|
||||
throw NullPointerException("SaltPayConfig is not initialized")
|
||||
}
|
||||
val wmFactory = store.state.globalState.tapWalletManager.walletManagerFactory
|
||||
val paymentologyService = store.state.domainNetworks.paymentologyService
|
||||
|
||||
// return createDummyActivationManager(saltPayConfig, wmFactory, paymentologyService)
|
||||
return createActivationPayManager(
|
||||
cardId = card.cardId,
|
||||
cardPublicKey = card.cardPublicKey,
|
||||
kycProvider = saltPayConfig.kycProvider,
|
||||
gnosisRegistrator = GnosisRegistrator(
|
||||
walletManager = makeSaltPayWalletManager(
|
||||
blockchain = blockchain,
|
||||
walletPublicKey = wallet.publicKey,
|
||||
wmFactory = wmFactory,
|
||||
),
|
||||
),
|
||||
paymentologyService = paymentologyService,
|
||||
)
|
||||
}
|
||||
|
||||
private fun makeSaltPayWalletManager(
|
||||
blockchain: Blockchain,
|
||||
walletPublicKey: ByteArray,
|
||||
wmFactory: WalletManagerFactory,
|
||||
): EthereumWalletManager {
|
||||
return wmFactory.makeWalletManager(
|
||||
blockchain = blockchain,
|
||||
publicKey = Wallet.PublicKey(walletPublicKey, null, null),
|
||||
tokens = listOf(SaltPayWorkaround.tokenFrom(blockchain)),
|
||||
) as EthereumWalletManager
|
||||
}
|
||||
|
||||
private fun createActivationPayManager(
|
||||
cardId: String,
|
||||
cardPublicKey: ByteArray,
|
||||
kycProvider: KYCProvider,
|
||||
gnosisRegistrator: GnosisRegistrator,
|
||||
paymentologyService: PaymentologyApiService,
|
||||
): SaltPayActivationManager {
|
||||
return SaltPayActivationManager(
|
||||
cardId = cardId,
|
||||
cardPublicKey = cardPublicKey,
|
||||
kycProvider = kycProvider,
|
||||
paymentologyService = paymentologyService,
|
||||
gnosisRegistrator = gnosisRegistrator,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun createDummyActivationManager(
|
||||
saltPayConfig: SaltPayConfig,
|
||||
wmFactory: WalletManagerFactory,
|
||||
paymentologyService: PaymentologyApiService,
|
||||
): SaltPayActivationManager {
|
||||
val fakeSource = DummySaltPayCardSource()
|
||||
|
||||
val walletManager = wmFactory.makeWalletManager(
|
||||
blockchain = fakeSource.blockchain,
|
||||
publicKey = fakeSource.blockchainPublicKey,
|
||||
tokens = listOf(fakeSource.token),
|
||||
) as EthereumWalletManager
|
||||
|
||||
return createActivationPayManager(
|
||||
cardId = fakeSource.cardId,
|
||||
cardPublicKey = fakeSource.cardPublicKey,
|
||||
kycProvider = saltPayConfig.kycProvider,
|
||||
gnosisRegistrator = GnosisRegistrator(walletManager),
|
||||
paymentologyService = paymentologyService,
|
||||
)
|
||||
}
|
||||
|
||||
private data class DummySaltPayCardSource(
|
||||
val blockchain: Blockchain = Blockchain.SaltPay,
|
||||
val cardId: String = "FF03000001001057",
|
||||
private val cardPublicKeyString: String = "039C2D2F9B68003766BFC29766761F55F4084E48B8B012BF439E115A79AB122D48",
|
||||
private val walletPublicKeyString: String = "0399B2DF5B129FBF69097DAA7C44FB5849E578CCFC22228A409870C74B56C4C3D1",
|
||||
) {
|
||||
val cardPublicKey: ByteArray = cardPublicKeyString.hexToBytes()
|
||||
val walletPublicKey: ByteArray = walletPublicKeyString.hexToBytes()
|
||||
val blockchainPublicKey = Wallet.PublicKey(walletPublicKey, null, null)
|
||||
val token: Token = SaltPayWorkaround.tokenFrom(blockchain)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ package com.tangem.tap.features.onboarding.products.wallet.saltPay
|
|||
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.events.Onboarding
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog
|
||||
|
|
@ -12,12 +14,20 @@ import com.tangem.tap.store
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object SaltPayExceptionHandler {
|
||||
|
||||
fun handle(throwable: Throwable) {
|
||||
when (throwable) {
|
||||
is SaltPayActivationError -> {
|
||||
val dialog = when (throwable) {
|
||||
is SaltPayActivationError.NoGas -> SaltPayDialog.Activation.NoGas
|
||||
is SaltPayActivationError.NoGas -> {
|
||||
Analytics.send(Onboarding.NotEnoughGasError())
|
||||
SaltPayDialog.Activation.NoGas
|
||||
}
|
||||
is SaltPayActivationError.PutVisaCard -> SaltPayDialog.Activation.PutVisaCard
|
||||
is SaltPayActivationError.CardNotPassed -> {
|
||||
Analytics.send(Onboarding.CardNotPassedError())
|
||||
SaltPayDialog.Activation.OnError(throwable)
|
||||
}
|
||||
else -> SaltPayDialog.Activation.OnError(throwable)
|
||||
}
|
||||
store.dispatchDialogShow(dialog)
|
||||
|
|
|
|||
|
|
@ -14,16 +14,20 @@ import com.tangem.wallet.R
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object NoFundsForActivationDialog {
|
||||
|
||||
fun create(context: Context): Dialog {
|
||||
return AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.saltpay_error_no_gas_title)
|
||||
setMessage(R.string.saltpay_error_no_gas_message)
|
||||
setPositiveButton(R.string.chat_button_title) { _, _ ->
|
||||
val config = store.state.globalState.configManager?.config?.saltPayConfig?.zendesk.guard {
|
||||
store.dispatchDebugErrorNotification("SaltPayConfig not initialized")
|
||||
return@setPositiveButton
|
||||
}
|
||||
store.dispatch(GlobalAction.OpenChat(SupportInfo(), config))
|
||||
val sprinklrConfig = store.state.globalState.configManager?.config
|
||||
?.saltPayConfig
|
||||
?.sprinklr
|
||||
.guard {
|
||||
store.dispatchDebugErrorNotification("SaltPayConfig not initialized")
|
||||
return@setPositiveButton
|
||||
}
|
||||
store.dispatch(GlobalAction.OpenChat(SupportInfo(), sprinklrConfig))
|
||||
}
|
||||
setNegativeButton(R.string.common_cancel) { _, _ ->
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux
|
|||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager
|
||||
import com.tangem.datasource.config.models.SaltPayConfig
|
||||
import org.rekotlin.Action
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -12,9 +11,13 @@ import java.math.BigDecimal
|
|||
sealed class OnboardingSaltPayAction : Action {
|
||||
object Init : OnboardingSaltPayAction()
|
||||
|
||||
/**
|
||||
* Notification about the switching from the Wallet view to the SaltPay view.
|
||||
*/
|
||||
object OnSwitchedToSaltPayProcess : OnboardingSaltPayAction()
|
||||
|
||||
data class SetDependencies(
|
||||
val registrationManager: SaltPayActivationManager,
|
||||
val saltPayConfig: SaltPayConfig,
|
||||
) : OnboardingSaltPayAction()
|
||||
|
||||
data class SetInProgress(val isInProgress: Boolean) : OnboardingSaltPayAction()
|
||||
|
|
@ -22,7 +25,7 @@ sealed class OnboardingSaltPayAction : Action {
|
|||
|
||||
data class SetAccessCode(val accessCode: String?) : OnboardingSaltPayAction()
|
||||
|
||||
object Update : OnboardingSaltPayAction()
|
||||
data class Update(val withAnalytics: Boolean = true) : OnboardingSaltPayAction()
|
||||
object RegisterCard : OnboardingSaltPayAction()
|
||||
object OpenUtorgKYC : OnboardingSaltPayAction()
|
||||
object UtorgKYCRedirectSuccess : OnboardingSaltPayAction()
|
||||
|
|
@ -33,7 +36,11 @@ sealed class OnboardingSaltPayAction : Action {
|
|||
data class TrySetPin(val pin: String) : OnboardingSaltPayAction()
|
||||
data class SetPin(val pin: String) : OnboardingSaltPayAction()
|
||||
|
||||
data class SetStep(val newStep: SaltPayActivationStep) : OnboardingSaltPayAction()
|
||||
data class SetStep(
|
||||
val newStep: SaltPayActivationStep,
|
||||
val withAnalytics: Boolean = true,
|
||||
) : OnboardingSaltPayAction()
|
||||
|
||||
data class SetAmountToClaim(val amount: Amount?) : OnboardingSaltPayAction()
|
||||
data class SetTokenBalance(val balanceValue: BigDecimal) : OnboardingSaltPayAction()
|
||||
}
|
||||
|
|
@ -78,6 +78,9 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
|
|||
onboardingManager.activationStarted(card.cardId)
|
||||
}
|
||||
}
|
||||
is OnboardingSaltPayAction.OnSwitchedToSaltPayProcess -> {
|
||||
sendAnalyticsScreenOpened(getState().step, newStep = SaltPayActivationStep.None)
|
||||
}
|
||||
is OnboardingSaltPayAction.Update -> {
|
||||
handleInProgress = true
|
||||
|
||||
|
|
@ -89,7 +92,18 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
|
|||
}
|
||||
|
||||
handleInProgress = false
|
||||
dispatchOnMain(OnboardingSaltPayAction.SetStep(updateStep))
|
||||
dispatchOnMain(OnboardingSaltPayAction.SetStep(updateStep, action.withAnalytics))
|
||||
|
||||
if (updateStep == SaltPayActivationStep.Claim) {
|
||||
handleClaimRefreshInProgress = true
|
||||
val tokenAmountValue = state.saltPayManager.getTokenAmount().successOr {
|
||||
SaltPayExceptionHandler.handle(it.error)
|
||||
handleClaimRefreshInProgress = false
|
||||
return@launch
|
||||
}
|
||||
dispatchOnMain(OnboardingSaltPayAction.SetTokenBalance(tokenAmountValue))
|
||||
handleClaimRefreshInProgress = false
|
||||
}
|
||||
}
|
||||
}
|
||||
is OnboardingSaltPayAction.RegisterCard -> {
|
||||
|
|
@ -136,8 +150,16 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
|
|||
return@launch
|
||||
}
|
||||
|
||||
Analytics.send(Onboarding.PinCodeSet())
|
||||
|
||||
var nextStep = SaltPayActivationStep.KycIntro
|
||||
nextStep = saltPayManager.update(nextStep, state.amountToClaim).successOr {
|
||||
onException(it.error)
|
||||
return@launch
|
||||
}
|
||||
|
||||
handleInProgress = false
|
||||
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.KycIntro))
|
||||
dispatchOnMain(OnboardingSaltPayAction.SetStep(nextStep))
|
||||
}
|
||||
}
|
||||
is OnboardingSaltPayAction.OpenUtorgKYC -> {
|
||||
|
|
@ -159,13 +181,13 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
|
|||
}
|
||||
|
||||
handleInProgress = false
|
||||
dispatchOnMain(OnboardingSaltPayAction.Update)
|
||||
dispatchOnMain(OnboardingSaltPayAction.Update())
|
||||
}
|
||||
}
|
||||
is OnboardingSaltPayAction.TrySetPin -> {
|
||||
try {
|
||||
assertPinValid(action.pin, getState().pinLength)
|
||||
Analytics.send(Onboarding.PinCodeSet())
|
||||
Analytics.send(Onboarding.ButtonSetPinCode())
|
||||
store.dispatch(OnboardingSaltPayAction.SetPin(action.pin))
|
||||
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.CardRegistration))
|
||||
} catch (error: SaltPayActivationError) {
|
||||
|
|
@ -219,54 +241,48 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
|
|||
|
||||
val state = getState()
|
||||
scope.launch {
|
||||
when (val amountToClaimResult = state.saltPayManager.getAmountToClaim()) {
|
||||
is Result.Success -> {
|
||||
// need to re claim
|
||||
handleClaimRefreshInProgress = false
|
||||
dispatchOnMain(OnboardingSaltPayAction.SetAmountToClaim(amountToClaimResult.data))
|
||||
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.Claim))
|
||||
}
|
||||
is Result.Failure -> {
|
||||
when (amountToClaimResult.error) {
|
||||
is SaltPayActivationError.NoFundsToClaim -> {
|
||||
val tokenAmountValue = state.saltPayManager.getTokenAmount().successOr {
|
||||
SaltPayExceptionHandler.handle(it.error)
|
||||
handleClaimRefreshInProgress = false
|
||||
return@launch
|
||||
}
|
||||
val tokenAmountValue = state.saltPayManager.getTokenAmount().successOr {
|
||||
SaltPayExceptionHandler.handle(it.error)
|
||||
handleClaimRefreshInProgress = false
|
||||
return@launch
|
||||
}
|
||||
dispatchOnMain(OnboardingSaltPayAction.SetTokenBalance(tokenAmountValue))
|
||||
|
||||
handleClaimRefreshInProgress = false
|
||||
if (tokenAmountValue.isPositive()) {
|
||||
dispatchOnMain(OnboardingSaltPayAction.SetTokenBalance(tokenAmountValue))
|
||||
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimSuccess))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
handleClaimRefreshInProgress = false
|
||||
SaltPayExceptionHandler.handle(amountToClaimResult.error)
|
||||
}
|
||||
}
|
||||
val amountToClaim = state.saltPayManager.getAmountToClaim().successOr {
|
||||
if (it.error !is SaltPayActivationError.NoFundsToClaim) {
|
||||
handleClaimRefreshInProgress = false
|
||||
SaltPayExceptionHandler.handle(it.error)
|
||||
return@launch
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
handleClaimRefreshInProgress = false
|
||||
|
||||
if (tokenAmountValue.isPositive() && amountToClaim == null) {
|
||||
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimSuccess))
|
||||
} else {
|
||||
// dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimSuccess))
|
||||
}
|
||||
}
|
||||
}
|
||||
is OnboardingSaltPayAction.SetStep -> {
|
||||
when (action.newStep) {
|
||||
SaltPayActivationStep.KycStart -> Analytics.send(Onboarding.KYCStarted())
|
||||
SaltPayActivationStep.KycWaiting -> Analytics.send(Onboarding.KYCInProgress())
|
||||
SaltPayActivationStep.KycReject -> Analytics.send(Onboarding.KYCRejected())
|
||||
SaltPayActivationStep.Claim -> Analytics.send(Onboarding.ClaimScreenOpened())
|
||||
SaltPayActivationStep.ClaimSuccess, SaltPayActivationStep.Success -> {
|
||||
Analytics.send(Onboarding.Finished())
|
||||
val onboardingManager = getOnboardingManager().guard {
|
||||
// Null is possible if it is started from a standard pending backup and in this case
|
||||
// it is impossible to get here
|
||||
store.dispatchDebugErrorNotification("OnboardingManager can't be NULL")
|
||||
return
|
||||
}
|
||||
finishCardActivation(getOnboardingWalletState().backupState, onboardingManager.scanResponse.card)
|
||||
if (action.withAnalytics) {
|
||||
sendAnalyticsScreenOpened(getState().step, action.newStep)
|
||||
}
|
||||
if (action.newStep == SaltPayActivationStep.ClaimSuccess ||
|
||||
action.newStep == SaltPayActivationStep.Success
|
||||
) {
|
||||
val onboardingManager = getOnboardingManager().guard {
|
||||
// Null is possible if it is started from a standard pending backup and in this case
|
||||
// it is impossible to get here
|
||||
store.dispatchDebugErrorNotification("OnboardingManager can't be NULL")
|
||||
return
|
||||
}
|
||||
else -> {}
|
||||
finishCardActivation(
|
||||
getOnboardingWalletState().backupState,
|
||||
onboardingManager.scanResponse.card,
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
|
|
@ -275,6 +291,22 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
|
|||
}
|
||||
}
|
||||
|
||||
private fun sendAnalyticsScreenOpened(currentStep: SaltPayActivationStep, newStep: SaltPayActivationStep) {
|
||||
if (currentStep == newStep) return
|
||||
|
||||
when (newStep) {
|
||||
SaltPayActivationStep.NeedPin -> Analytics.send(Onboarding.PinScreenOpened())
|
||||
SaltPayActivationStep.CardRegistration -> Analytics.send(Onboarding.CardConnectionScreenOpened())
|
||||
SaltPayActivationStep.KycIntro -> Analytics.send(Onboarding.KYCScreenOpened())
|
||||
SaltPayActivationStep.KycStart -> Analytics.send(Onboarding.KYCStarted())
|
||||
SaltPayActivationStep.KycWaiting -> Analytics.send(Onboarding.KYCInProgress())
|
||||
SaltPayActivationStep.KycReject -> Analytics.send(Onboarding.KYCRejected())
|
||||
SaltPayActivationStep.Claim -> Analytics.send(Onboarding.ClaimScreenOpened())
|
||||
SaltPayActivationStep.ClaimSuccess, SaltPayActivationStep.Success -> Analytics.send(Onboarding.Finished())
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun SaltPayActivationManager.update(
|
||||
currentStep: SaltPayActivationStep,
|
||||
currentAmountToClaim: Amount?,
|
||||
|
|
@ -362,6 +394,7 @@ private suspend fun getAmountToClaimIfNeeded(
|
|||
return amountToClaim
|
||||
}
|
||||
|
||||
// return Amount(BigDecimal(0.1), Blockchain.SaltPay)
|
||||
return when (val result = saltPayManager.getAmountToClaim()) {
|
||||
is Result.Success -> {
|
||||
Timber.d("getAmountToClaimIfNeeded: success: %s", result.data)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ private fun internalReduce(anyAction: Action, onboardingWalletState: OnboardingW
|
|||
return onboardingWalletState.copy(
|
||||
onboardingSaltPayState = OnboardingSaltPayState(
|
||||
saltPayManager = action.registrationManager,
|
||||
saltPayConfig = action.saltPayConfig,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,19 +2,10 @@ package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux
|
|||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.WalletManagerFactory
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.datasource.api.paymentology.PaymentologyApiService
|
||||
import com.tangem.domain.common.SaltPayWorkaround
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.tap.common.toggleWidget.WidgetState
|
||||
import com.tangem.tap.domain.extensions.makeSaltPayWalletManager
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.GnosisRegistrator
|
||||
import com.tangem.datasource.config.models.KYCProvider
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager
|
||||
import com.tangem.datasource.config.models.SaltPayConfig
|
||||
import com.tangem.tap.features.wallet.redux.ProgressState
|
||||
import com.tangem.tap.store
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
|
|
@ -23,7 +14,6 @@ import java.math.BigDecimal
|
|||
data class OnboardingSaltPayState(
|
||||
@Transient
|
||||
val saltPayManager: SaltPayActivationManager = SaltPayActivationManager.stub(),
|
||||
val saltPayConfig: SaltPayConfig = SaltPayConfig.stub(),
|
||||
val pinCode: String? = null,
|
||||
val accessCode: String? = null,
|
||||
val amountToClaim: Amount? = null,
|
||||
|
|
@ -38,54 +28,6 @@ data class OnboardingSaltPayState(
|
|||
get() = if (inProgress) ProgressState.Loading else ProgressState.Done
|
||||
|
||||
val pinLength: Int = 4
|
||||
|
||||
companion object {
|
||||
fun initDependency(scanResponse: ScanResponse): Pair<SaltPayActivationManager, SaltPayConfig> {
|
||||
val globalState = store.state.globalState
|
||||
val saltPayConfig = globalState.configManager?.config?.saltPayConfig.guard {
|
||||
throw NullPointerException("SaltPayConfig is not initialized")
|
||||
}
|
||||
val gnosisRegistrator = makeGnosisRegistrator(
|
||||
scanResponse = scanResponse,
|
||||
wmFactory = globalState.tapWalletManager.walletManagerFactory,
|
||||
)
|
||||
val registrationManager = makeSaltPayRegistrationManager(
|
||||
scanResponse = scanResponse,
|
||||
gnosisRegistrator = gnosisRegistrator,
|
||||
paymentologyService = store.state.domainNetworks.paymentologyService,
|
||||
kycProvider = saltPayConfig.kycProvider,
|
||||
)
|
||||
return registrationManager to saltPayConfig
|
||||
}
|
||||
|
||||
fun makeSaltPayRegistrationManager(
|
||||
scanResponse: ScanResponse,
|
||||
gnosisRegistrator: GnosisRegistrator,
|
||||
paymentologyService: PaymentologyApiService,
|
||||
kycProvider: KYCProvider,
|
||||
): SaltPayActivationManager {
|
||||
if (!scanResponse.cardTypesResolver.isSaltPay()) {
|
||||
throw IllegalArgumentException("Can't initialize the OnboardingSaltPayMiddleware if card is not SalPay")
|
||||
}
|
||||
|
||||
return SaltPayActivationManager(
|
||||
cardId = scanResponse.card.cardId,
|
||||
cardPublicKey = scanResponse.card.cardPublicKey,
|
||||
kycProvider = kycProvider,
|
||||
paymentologyService = paymentologyService,
|
||||
gnosisRegistrator = gnosisRegistrator,
|
||||
)
|
||||
}
|
||||
|
||||
fun makeGnosisRegistrator(
|
||||
scanResponse: ScanResponse,
|
||||
wmFactory: WalletManagerFactory,
|
||||
): GnosisRegistrator {
|
||||
return GnosisRegistrator(
|
||||
walletManager = wmFactory.makeSaltPayWalletManager(scanResponse),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class SaltPayActivationStep {
|
||||
|
|
@ -101,5 +43,4 @@ enum class SaltPayActivationStep {
|
|||
ClaimInProgress,
|
||||
ClaimSuccess,
|
||||
Success,
|
||||
Finished;
|
||||
}
|
||||
|
|
@ -80,9 +80,10 @@ internal class OnboardingSaltPayView(
|
|||
when (state.step) {
|
||||
OnboardingWalletStep.SaltPay -> setSaltPayStep(state.onboardingSaltPayState)
|
||||
OnboardingWalletStep.Backup -> {
|
||||
if (state.backupState.backupStep == BackupStep.Finished) {
|
||||
if (state.backupState.backupStep == BackupStep.Finished && !state.backupState.isInterruptedBackup) {
|
||||
// if backup finished -> switch OnboardingWalletStep to the SaltPay
|
||||
store.dispatch(OnboardingWalletAction.GetToSaltPayStep)
|
||||
store.dispatch(OnboardingSaltPayAction.OnSwitchedToSaltPayProcess)
|
||||
} else {
|
||||
// if not -> back to the standard backup process
|
||||
walletFragment.handleOnboardingStep(state)
|
||||
|
|
@ -103,7 +104,7 @@ internal class OnboardingSaltPayView(
|
|||
val uri = Uri.parse(state.onboardingSaltPayState.saltPayCardArtworkUrl)
|
||||
walletFragment.loadImageIntoImageView(uri, imvFrontCard)
|
||||
}
|
||||
imvFirstBackupCard.load(R.drawable.img_salt_pay_visa)
|
||||
imvFirstBackupCard.load(R.drawable.card_placeholder_wallet)
|
||||
|
||||
// if (state.onboardingSaltPayState?.saltPayCardArtworkUrl == null) {
|
||||
// //if saltPay url not loaded -> load from resource
|
||||
|
|
@ -233,7 +234,7 @@ internal class OnboardingSaltPayView(
|
|||
|
||||
btnKycAction.text = getText(R.string.onboarding_button_kyc_waiting)
|
||||
btnKycAction.setOnClickListener {
|
||||
store.dispatch(OnboardingSaltPayAction.Update)
|
||||
store.dispatch(OnboardingSaltPayAction.Update())
|
||||
}
|
||||
progressButton = SaltPayProgressButton(root)
|
||||
}
|
||||
|
|
@ -249,7 +250,7 @@ internal class OnboardingSaltPayView(
|
|||
btnOpenSupportChat.hide()
|
||||
btnKycAction.text = getText(R.string.onboarding_button_kyc_start)
|
||||
btnKycAction.setOnClickListener {
|
||||
store.dispatch(OnboardingSaltPayAction.Update)
|
||||
store.dispatch(OnboardingSaltPayAction.Update())
|
||||
}
|
||||
progressButton = SaltPayProgressButton(root)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.tap.features.sprinklr.redux
|
||||
|
||||
import com.tangem.datasource.config.models.SprinklrConfig
|
||||
import org.rekotlin.Action
|
||||
|
||||
sealed interface SprinklrAction : Action {
|
||||
data class Init(val userId: String, val config: SprinklrConfig) : SprinklrAction
|
||||
data class UpdateUrl(val url: String) : SprinklrAction
|
||||
data class UpdateSprinklrDomains(val domains: List<String>) : SprinklrAction
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.tap.features.sprinklr.redux
|
||||
|
||||
import com.tangem.datasource.config.models.SprinklrConfig
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.sprinklr.redux.model.SprinklrUrl
|
||||
import com.tangem.tap.store
|
||||
import org.rekotlin.Middleware
|
||||
|
||||
internal class SprinklrMiddleware {
|
||||
val middleware: Middleware<AppState> = { _, 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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.tap.features.sprinklr.redux
|
||||
|
||||
data class SprinklrState(
|
||||
val url: String = "",
|
||||
val sprinklrDomains: List<String> = emptyList(),
|
||||
)
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.tap.features.sprinklr.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.ui.fragments.ComposeActivity
|
||||
import com.tangem.tap.common.analytics.events.Chat
|
||||
|
||||
internal class SprinklrActivity : ComposeActivity<SprinklrScreenState>() {
|
||||
private val viewModel by viewModels<SprinklrViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Analytics.send(Chat.ScreenOpened())
|
||||
viewModel.setNavigateBackCallback {
|
||||
this.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun provideState(): State<SprinklrScreenState> {
|
||||
return viewModel.state.collectAsState()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(
|
||||
state: SprinklrScreenState,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
BackHandler(onBack = state.onNavigateBack)
|
||||
SprinklrScreenContent(
|
||||
modifier = modifier
|
||||
.systemBarsPadding()
|
||||
.imePadding(),
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
|
|
@ -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<String> = emptyList(),
|
||||
val onNavigateBack: () -> Unit = {},
|
||||
val onNewUrl: WebContent.(String) -> WebContent = { this },
|
||||
)
|
||||
|
|
@ -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<SprinklrState> {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -57,6 +57,12 @@ import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestCasesList
|
|||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("UnusedPrivateClass")
|
||||
private class AddCustomTokenScreen // for simple search
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun AddCustomTokenScreen(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.features.wallet.redux
|
||||
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.feature.swap.domain.SwapInteractor
|
||||
|
|
@ -14,6 +15,7 @@ import java.math.BigDecimal
|
|||
|
||||
data class WalletData(
|
||||
val pendingTransactions: List<PendingTransaction> = emptyList(),
|
||||
val historyTransactions: List<TransactionData>? = null,
|
||||
val hashesCountVerified: Boolean? = null,
|
||||
val walletAddresses: WalletAddresses? = null,
|
||||
val currencyData: BalanceWidgetData = BalanceWidgetData(),
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ data class WalletState(
|
|||
walletsStores[0]
|
||||
}
|
||||
|
||||
private val primaryWalletManager: WalletManager?
|
||||
val primaryWalletManager: WalletManager?
|
||||
get() = primaryWalletStore?.walletManager
|
||||
|
||||
val primaryWalletData: WalletData?
|
||||
|
|
@ -317,6 +317,7 @@ data class Artwork(
|
|||
const val MARTA_CARD_ID = "BC02"
|
||||
const val TWIN_CARD_1 = "https://app.tangem.com/cards/card_tg085.png"
|
||||
const val TWIN_CARD_2 = "https://app.tangem.com/cards/card_tg086.png"
|
||||
const val SALT_PAY_URL = "key_for_switch_url_to_drawableId_of_salt_pay_card"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ private fun WalletDataModel.mapToReduxModel(
|
|||
?.toString(),
|
||||
errorMessage = status.errorMessage,
|
||||
),
|
||||
historyTransactions = historyTransactions,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.tangem.core.analytics.Analytics
|
|||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Token.ButtonRemoveToken
|
||||
import com.tangem.tap.common.extensions.addContext
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
|
|
@ -169,6 +170,7 @@ class MultiWalletMiddleware {
|
|||
is WalletAction.MultiWallet.ShowWalletBackupWarning -> Unit
|
||||
is WalletAction.MultiWallet.BackupWallet -> {
|
||||
store.state.globalState.scanResponse?.let {
|
||||
Analytics.addContext(it)
|
||||
store.dispatch(GlobalAction.Onboarding.Start(it, canSkipBackup = false))
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.tap.features.wallet.redux.middlewares
|
|||
import androidx.core.os.bundleOf
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.common.extensions.toCoinId
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
|
|
@ -22,6 +23,7 @@ import com.tangem.tap.features.send.redux.PrepareSendScreen
|
|||
import com.tangem.tap.features.send.redux.SendAction
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens
|
||||
import com.tangem.tap.scope
|
||||
|
|
@ -49,10 +51,9 @@ class TradeCryptoMiddleware {
|
|||
action: WalletAction.TradeCryptoAction.Buy,
|
||||
) {
|
||||
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
|
||||
val currency = chooseAppropriateCurrency(store.state.walletState) ?: return
|
||||
|
||||
val currency = selectedWalletData.currency
|
||||
Analytics.send(Token.ButtonBuy(AnalyticsParam.CurrencyType.Currency(currency)))
|
||||
|
||||
if (action.checkUserLocation && state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE) {
|
||||
store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog())
|
||||
return
|
||||
|
|
@ -96,12 +97,12 @@ class TradeCryptoMiddleware {
|
|||
|
||||
private fun proceedSellAction() {
|
||||
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
|
||||
val currency = chooseAppropriateCurrency(store.state.walletState) ?: return
|
||||
|
||||
val appCurrency = store.state.globalState.appCurrency
|
||||
val addresses = selectedWalletData.walletAddresses?.list.orEmpty()
|
||||
if (addresses.isEmpty()) return
|
||||
|
||||
val currency = selectedWalletData.currency
|
||||
Analytics.send(Token.ButtonSell(AnalyticsParam.CurrencyType.Currency(currency)))
|
||||
|
||||
store.state.globalState.exchangeManager.getUrl(
|
||||
|
|
@ -116,6 +117,17 @@ class TradeCryptoMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun chooseAppropriateCurrency(walletState: WalletState): Currency? {
|
||||
return if (walletState.primaryTokenData == null) {
|
||||
walletState.selectedWalletData?.currency
|
||||
} else {
|
||||
walletState.primaryTokenData?.currency as? Currency.Token
|
||||
}.guard {
|
||||
store.dispatchDebugErrorNotification("Can't select an appropriate currency for a Trade action")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun preconfigureAndOpenSendScreen(action: WalletAction.TradeCryptoAction.SendCrypto) {
|
||||
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.tangem.operations.attestation.OnlineCardVerifier
|
|||
import com.tangem.tap.common.analytics.converters.BasicEventsPreChecker
|
||||
import com.tangem.tap.common.analytics.converters.BasicEventsSourceData
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Basic
|
||||
import com.tangem.tap.common.analytics.events.MainScreen
|
||||
import com.tangem.tap.common.analytics.events.Token
|
||||
import com.tangem.tap.common.extensions.copyToClipboard
|
||||
|
|
@ -249,6 +250,8 @@ class WalletMiddleware {
|
|||
}
|
||||
is NetworkStateChanged -> {
|
||||
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
|
||||
if (!action.isOnline) return
|
||||
|
||||
val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync
|
||||
if (selectedUserWallet != null) {
|
||||
scope.launch { globalState.tapWalletManager.loadData(selectedUserWallet) }
|
||||
|
|
@ -380,7 +383,7 @@ class WalletMiddleware {
|
|||
}
|
||||
else -> {
|
||||
Analytics.send(MainScreen.ButtonScanCard())
|
||||
store.dispatch(WalletAction.Scan(MainScreen.CardWasScanned()))
|
||||
store.dispatch(WalletAction.Scan(Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Main)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import com.tangem.blockchain.common.AmountType
|
|||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.tap.common.extensions.dispatchToastNotification
|
||||
import com.tangem.tap.common.extensions.getBlockchainTxHistory
|
||||
import com.tangem.tap.common.extensions.getTokenTxHistory
|
||||
import com.tangem.tap.common.extensions.toFiatString
|
||||
import com.tangem.tap.common.extensions.toFormattedCurrencyString
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
|
|
@ -54,6 +56,7 @@ class MultiWalletReducer {
|
|||
blockchain.derivationPath,
|
||||
),
|
||||
existentialDepositString = getExistentialDeposit(walletManager),
|
||||
historyTransactions = walletManager?.getBlockchainTxHistory(),
|
||||
)
|
||||
|
||||
WalletStore(
|
||||
|
|
@ -89,6 +92,7 @@ class MultiWalletReducer {
|
|||
action.blockchain.derivationPath,
|
||||
),
|
||||
existentialDepositString = getExistentialDeposit(walletManager),
|
||||
historyTransactions = walletManager?.getBlockchainTxHistory(),
|
||||
)
|
||||
val walletStore = WalletStore(
|
||||
walletManager = walletManager,
|
||||
|
|
@ -153,6 +157,7 @@ class MultiWalletReducer {
|
|||
derivationPath = action.blockchain.derivationPath,
|
||||
),
|
||||
walletRent = findWalletRent(state.getWalletStore(walletManager.wallet)),
|
||||
historyTransactions = walletManager.getTokenTxHistory(action.token),
|
||||
)
|
||||
state.updateWalletData(newTokenWalletData)
|
||||
}
|
||||
|
|
@ -206,8 +211,8 @@ fun Token.toWallet(state: WalletState, blockchain: BlockchainNetwork): WalletDat
|
|||
val currency = Currency.fromBlockchainNetwork(blockchain, this)
|
||||
if (state.currencies.contains(currency)) return null
|
||||
|
||||
val walletManager = state.getWalletManager(currency)?.wallet
|
||||
val walletAddresses = createAddressList(walletManager)
|
||||
val walletManager = state.getWalletManager(currency)
|
||||
val walletAddresses = createAddressList(walletManager?.wallet)
|
||||
|
||||
return WalletData(
|
||||
currencyData = BalanceWidgetData(
|
||||
|
|
@ -218,5 +223,6 @@ fun Token.toWallet(state: WalletState, blockchain: BlockchainNetwork): WalletDat
|
|||
walletAddresses = walletAddresses,
|
||||
mainButton = WalletMainButton.SendButton(false),
|
||||
currency = currency,
|
||||
historyTransactions = walletManager?.getTokenTxHistory(this),
|
||||
)
|
||||
}
|
||||
|
|
@ -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<WalletData>.findProgressState(): ProgressState {
|
||||
fun List<WalletData>.findProgressState(initialState: ProgressState = ProgressState.Done): ProgressState {
|
||||
if (this.isEmpty()) return initialState
|
||||
|
||||
return this
|
||||
.mapToProgressState()
|
||||
.reduce(ProgressState::or)
|
||||
|
|
|
|||
|
|
@ -348,6 +348,7 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
|
|||
}
|
||||
is WalletAction.WalletStoresChanged.UpdateWalletStores -> {
|
||||
newState = newState.copy(
|
||||
state = action.reduxWalletStores.flatMap { it.walletsData }.findProgressState(newState.state),
|
||||
walletsStores = action.reduxWalletStores,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import coil.size.Scale
|
|||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.ui.fragments.setStatusBarColor
|
||||
import com.tangem.core.ui.utils.OneTouchClickListener
|
||||
import com.tangem.domain.common.TapWorkarounds.isSaltPay
|
||||
import com.tangem.feature.swap.domain.SwapInteractor
|
||||
import com.tangem.tap.MainActivity
|
||||
import com.tangem.tap.common.analytics.events.MainScreen
|
||||
|
|
@ -40,9 +39,9 @@ import com.tangem.tap.features.wallet.redux.WalletAction
|
|||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
|
||||
import com.tangem.tap.features.wallet.ui.wallet.MultiWalletView
|
||||
import com.tangem.tap.features.wallet.ui.wallet.SaltPayWalletView
|
||||
import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView
|
||||
import com.tangem.tap.features.wallet.ui.wallet.WalletView
|
||||
import com.tangem.tap.features.wallet.ui.wallet.saltPay.SaltPayWalletView
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
import com.tangem.wallet.BuildConfig
|
||||
|
|
@ -155,19 +154,22 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
override fun newState(state: WalletState) {
|
||||
if (activity == null || view == null) return
|
||||
|
||||
val isSaltPay = store.state.globalState.scanResponse?.card?.isSaltPay == true
|
||||
val isSaltPay = store.state.globalState.scanResponse?.cardTypesResolver?.isSaltPay() == true
|
||||
|
||||
when {
|
||||
isSaltPay && walletView !is SaltPayWalletView -> {
|
||||
walletView.onViewDestroy()
|
||||
walletView = SaltPayWalletView()
|
||||
walletView.changeWalletView(this, binding)
|
||||
}
|
||||
state.isMultiwalletAllowed && state.primaryWalletData?.currencyData?.status != BalanceStatus.EmptyCard &&
|
||||
walletView !is MultiWalletView -> {
|
||||
walletView.onViewDestroy()
|
||||
walletView = MultiWalletView()
|
||||
walletView.changeWalletView(this, binding)
|
||||
}
|
||||
!state.isMultiwalletAllowed && !isSaltPay && walletView !is SingleWalletView -> {
|
||||
walletView.onViewDestroy()
|
||||
walletView = SingleWalletView()
|
||||
walletView.changeWalletView(this, binding)
|
||||
}
|
||||
|
|
@ -185,7 +187,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
}
|
||||
|
||||
setupNoInternetHandling(state)
|
||||
setupCardImage(state)
|
||||
setupCardImage(state, isSaltPay)
|
||||
|
||||
if (!isSaltPay) showWarningsIfPresent(state.mainWarningsList)
|
||||
|
||||
|
|
@ -222,9 +224,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
}
|
||||
}
|
||||
|
||||
private fun setupCardImage(state: WalletState) {
|
||||
private fun setupCardImage(state: WalletState, isSaltPay: Boolean) {
|
||||
// TODO: SaltPay: remove hardCode
|
||||
if (store.state.globalState.scanResponse?.cardTypesResolver?.isSaltPay() == true) {
|
||||
if (isSaltPay) {
|
||||
binding.ivCard.load(R.drawable.img_salt_pay_visa) {
|
||||
scale(Scale.FIT)
|
||||
crossfade(enable = true)
|
||||
|
|
|
|||
|
|
@ -1,88 +0,0 @@
|
|||
package com.tangem.tap.features.wallet.ui.wallet
|
||||
|
||||
import com.tangem.domain.common.extensions.debounce
|
||||
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.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.mainScope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.databinding.FragmentWalletBinding
|
||||
import com.tangem.wallet.databinding.LayoutSaltPayWalletBinding
|
||||
import org.rekotlin.Action
|
||||
|
||||
class SaltPayWalletView : WalletView() {
|
||||
|
||||
private lateinit var saltPayBinding: LayoutSaltPayWalletBinding
|
||||
private val actionDebouncer = debounce<Action>(500, mainScope) { store.dispatch(it) }
|
||||
|
||||
override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) {
|
||||
saltPayBinding = binding.lSaltPayWallet
|
||||
setFragment(fragment, binding)
|
||||
onViewCreated()
|
||||
showSaltPayView(binding)
|
||||
}
|
||||
|
||||
private fun showSaltPayView(binding: FragmentWalletBinding) = with(binding) {
|
||||
rvWarningMessages.hide()
|
||||
rvPendingTransaction.hide()
|
||||
rvMultiwallet.hide()
|
||||
tvTwinCardNumber.hide()
|
||||
lCardBalance.root.hide()
|
||||
lSingleWalletBalance.root.hide()
|
||||
lAddress.root.hide()
|
||||
rowButtons.hide()
|
||||
btnAddToken.hide()
|
||||
pbLoadingUserTokens.hide()
|
||||
lSaltPayWallet.root.show()
|
||||
}
|
||||
|
||||
override fun onViewCreated() {
|
||||
}
|
||||
|
||||
override fun onNewState(state: WalletState) {
|
||||
setupBalanceWidget(state)
|
||||
}
|
||||
|
||||
private fun setupBalanceWidget(state: WalletState) = with(saltPayBinding.lSaltPayBalance) {
|
||||
val tokenData = state.primaryTokenData ?: return@with
|
||||
|
||||
val appCurrency = store.state.globalState.appCurrency
|
||||
val mainProgressState = state.state
|
||||
|
||||
if (mainProgressState == ProgressState.Loading) {
|
||||
veilBalance.veil()
|
||||
veilBalanceCrypto.veil()
|
||||
} else {
|
||||
veilBalanceCrypto.unVeil()
|
||||
}
|
||||
|
||||
tvProcessing.animateVisibility(show = mainProgressState == ProgressState.Error)
|
||||
veilBalanceCrypto.animateVisibility(show = mainProgressState != ProgressState.Error)
|
||||
|
||||
if (tokenData.currencyData.fiatAmount == null) {
|
||||
actionDebouncer(WalletAction.LoadFiatRate())
|
||||
} else {
|
||||
veilBalance.unVeil()
|
||||
tvBalance.text = tokenData.currencyData.fiatAmount.formatAmountAsSpannedString(
|
||||
currencySymbol = appCurrency.symbol,
|
||||
)
|
||||
}
|
||||
|
||||
tvBalanceCrypto.text = tokenData.currencyData.amountFormatted
|
||||
|
||||
tvCurrencyName.text = appCurrency.code
|
||||
tvCurrencyName.setOnClickListener {
|
||||
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
|
||||
}
|
||||
|
||||
btnBuy.show(tokenData.isAvailableToBuy(store.state.globalState.exchangeManager))
|
||||
btnBuy.setOnClickListener {
|
||||
store.dispatch(WalletAction.TradeCryptoAction.Buy(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,10 +6,12 @@ import com.tangem.tap.features.wallet.ui.WalletFragment
|
|||
import com.tangem.wallet.databinding.FragmentWalletBinding
|
||||
|
||||
abstract class WalletView {
|
||||
|
||||
var swapInteractor: SwapInteractor? = null
|
||||
|
||||
protected var fragment: WalletFragment? = null
|
||||
protected var binding: FragmentWalletBinding? = null
|
||||
|
||||
fun setFragment(fragment: WalletFragment, binding: FragmentWalletBinding) {
|
||||
this.fragment = fragment
|
||||
this.binding = binding
|
||||
|
|
@ -20,6 +22,10 @@ abstract class WalletView {
|
|||
binding = null
|
||||
}
|
||||
|
||||
open fun onViewDestroy() {
|
||||
removeFragment()
|
||||
}
|
||||
|
||||
open fun onDestroyFragment() {}
|
||||
|
||||
abstract fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
package com.tangem.tap.features.wallet.ui.wallet.saltPay
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.common.extensions.debounce
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.tap.common.ShimmerData
|
||||
import com.tangem.tap.common.ShimmerRecyclerAdapter
|
||||
import com.tangem.tap.common.analytics.events.MainScreen
|
||||
import com.tangem.tap.common.extensions.animateVisibility
|
||||
import com.tangem.tap.common.extensions.formatAmountAsSpannedString
|
||||
import com.tangem.tap.common.extensions.hide
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
|
||||
import com.tangem.tap.features.wallet.redux.ProgressState
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
import com.tangem.tap.features.wallet.ui.WalletFragment
|
||||
import com.tangem.tap.features.wallet.ui.wallet.WalletView
|
||||
import com.tangem.tap.features.wallet.ui.wallet.saltPay.rv.HistoryItemData
|
||||
import com.tangem.tap.features.wallet.ui.wallet.saltPay.rv.HistoryTransactionData
|
||||
import com.tangem.tap.features.wallet.ui.wallet.saltPay.rv.TxHistoryAdapter
|
||||
import com.tangem.tap.mainScope
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.databinding.FragmentWalletBinding
|
||||
import com.tangem.wallet.databinding.ItemSaltPayTxHistoryShimmerBinding
|
||||
import com.tangem.wallet.databinding.LayoutSaltPayBalanceBinding
|
||||
import com.tangem.wallet.databinding.LayoutSaltPayTxHistoryBinding
|
||||
import com.tangem.wallet.databinding.LayoutSaltPayWalletBinding
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class SaltPayWalletView : WalletView() {
|
||||
|
||||
private var saltPayBinding: LayoutSaltPayWalletBinding? = null
|
||||
|
||||
private val actionDebouncer = debounce<Action>(500, mainScope) { store.dispatch(it) }
|
||||
|
||||
private val balanceWidget: LayoutSaltPayBalanceBinding?
|
||||
get() = saltPayBinding?.lSaltPayBalance
|
||||
|
||||
private val txWidget: LayoutSaltPayTxHistoryBinding?
|
||||
get() = saltPayBinding?.lSaltPayTxHistory
|
||||
|
||||
private val itemDecorator = SpaceItemDecoration.vertical(10F)
|
||||
|
||||
override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) {
|
||||
Timber.d("changeWalletView")
|
||||
saltPayBinding = binding.lSaltPayWallet
|
||||
showSaltPayView(binding)
|
||||
onViewCreated()
|
||||
}
|
||||
|
||||
override fun onViewCreated() {
|
||||
Timber.d("onViewCreated")
|
||||
prepareTxRecyclers(txWidget!!)
|
||||
}
|
||||
|
||||
private fun showSaltPayView(binding: FragmentWalletBinding) = with(binding) {
|
||||
Timber.d("showSaltPayView")
|
||||
rvWarningMessages.hide()
|
||||
rvPendingTransaction.hide()
|
||||
rvMultiwallet.hide()
|
||||
tvTwinCardNumber.hide()
|
||||
lCardBalance.root.hide()
|
||||
lSingleWalletBalance.root.hide()
|
||||
lAddress.root.hide()
|
||||
rowButtons.hide()
|
||||
btnAddToken.hide()
|
||||
pbLoadingUserTokens.hide()
|
||||
lSaltPayWallet.root.show()
|
||||
}
|
||||
|
||||
private fun prepareTxRecyclers(txWidget: LayoutSaltPayTxHistoryBinding) = with(txWidget) {
|
||||
Timber.d("prepareTxRecyclers")
|
||||
val vhViewFactory: (ViewGroup) -> ViewGroup = {
|
||||
val inflater = LayoutInflater.from(it.context)
|
||||
ItemSaltPayTxHistoryShimmerBinding.inflate(inflater, it, false).root
|
||||
}
|
||||
val adapter = ShimmerRecyclerAdapter(vhViewFactory)
|
||||
rvTxHistoryShimmer.adapter = adapter
|
||||
rvTxHistoryShimmer.addItemDecoration(itemDecorator)
|
||||
adapter.submitList(
|
||||
listOf(
|
||||
ShimmerData(),
|
||||
ShimmerData(),
|
||||
ShimmerData(),
|
||||
),
|
||||
)
|
||||
|
||||
rvTxHistory.adapter = TxHistoryAdapter()
|
||||
rvTxHistory.addItemDecoration(itemDecorator)
|
||||
}
|
||||
|
||||
override fun onNewState(state: WalletState) {
|
||||
val balanceWidget = balanceWidget ?: return
|
||||
val txWidget = txWidget ?: return
|
||||
|
||||
val exchangeManager = store.state.globalState.exchangeManager
|
||||
setupBalanceWidget(balanceWidget, exchangeManager, state)
|
||||
setupTxHistoryWidget(txWidget, state)
|
||||
}
|
||||
|
||||
private fun setupBalanceWidget(
|
||||
balanceWidget: LayoutSaltPayBalanceBinding,
|
||||
exchangeManager: CurrencyExchangeManager,
|
||||
state: WalletState,
|
||||
) = with(balanceWidget) {
|
||||
Timber.d("setupBalanceWidget")
|
||||
val tokenData = state.primaryTokenData ?: return
|
||||
|
||||
val appCurrency = store.state.globalState.appCurrency
|
||||
val mainProgressState = state.state
|
||||
|
||||
if (mainProgressState == ProgressState.Loading) {
|
||||
veilBalance.veil()
|
||||
veilBalanceCrypto.veil()
|
||||
} else {
|
||||
veilBalanceCrypto.unVeil()
|
||||
}
|
||||
|
||||
tvUnreachable.animateVisibility(show = mainProgressState == ProgressState.Error)
|
||||
veilBalanceCrypto.animateVisibility(show = mainProgressState != ProgressState.Error)
|
||||
|
||||
if (tokenData.currencyData.fiatAmount == null) {
|
||||
veilBalance.veil()
|
||||
actionDebouncer(WalletAction.LoadFiatRate())
|
||||
} else {
|
||||
veilBalance.unVeil()
|
||||
tvBalance.text = tokenData.currencyData.fiatAmount.formatAmountAsSpannedString(
|
||||
currencySymbol = appCurrency.symbol,
|
||||
)
|
||||
}
|
||||
|
||||
tvBalanceCrypto.text = tokenData.currencyData.amountFormatted
|
||||
|
||||
tvCurrencyName.text = appCurrency.code
|
||||
tvCurrencyName.setOnClickListener {
|
||||
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
|
||||
}
|
||||
|
||||
btnBuy.show(tokenData.isAvailableToBuy(exchangeManager))
|
||||
btnBuy.setOnClickListener {
|
||||
Analytics.send(MainScreen.ButtonBuy())
|
||||
store.dispatch(WalletAction.TradeCryptoAction.Buy(false))
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupTxHistoryWidget(txWidget: LayoutSaltPayTxHistoryBinding, state: WalletState) {
|
||||
Timber.d("setupTxHistoryWidget")
|
||||
if (state.state == ProgressState.Loading) {
|
||||
handleTxLoading(txWidget)
|
||||
return
|
||||
}
|
||||
if (state.state == ProgressState.Error) {
|
||||
handleTxError(txWidget)
|
||||
return
|
||||
}
|
||||
if (state.state != ProgressState.Done) return
|
||||
val walletAddress = state.primaryWalletManager?.wallet?.address ?: return
|
||||
val tokenData = state.primaryTokenData ?: return
|
||||
val historyTransactions = tokenData.historyTransactions ?: return
|
||||
if (historyTransactions.isEmpty()) {
|
||||
handleTxEmpty(txWidget)
|
||||
return
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
val dateAssociatedHistory = mutableListOf<Pair<String, MutableList<HistoryTransactionData>>>()
|
||||
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<HistoryItemData>()
|
||||
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<HistoryItemData>,
|
||||
) = 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<HistoryItemData>,
|
||||
) = with(txWidget) {
|
||||
(rvTxHistory.adapter as TxHistoryAdapter).submitList(dataList)
|
||||
}
|
||||
|
||||
override fun onViewDestroy() {
|
||||
Timber.d("onViewDestroy")
|
||||
txWidget?.apply {
|
||||
rvTxHistoryShimmer.removeItemDecoration(itemDecorator)
|
||||
rvTxHistory.removeItemDecoration(itemDecorator)
|
||||
}
|
||||
super.onViewDestroy()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.tap.features.wallet.ui.wallet.saltPay.rv
|
||||
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.TransactionStatus
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.tap.common.extensions.toFormattedCurrencyString
|
||||
import com.tangem.tap.features.wallet.models.PendingTransactionType
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class HistoryItemData(
|
||||
val viewType: Int,
|
||||
val itemId: Long,
|
||||
) {
|
||||
data class Date(val date: String) : HistoryItemData(0, date.hashCode().toLong())
|
||||
data class TransactionData(val data: HistoryTransactionData) : HistoryItemData(1, data.address.hashCode().toLong())
|
||||
}
|
||||
|
||||
data class HistoryTransactionData(
|
||||
private val transactionData: TransactionData,
|
||||
private val walletAddress: String,
|
||||
) {
|
||||
|
||||
val isInProgress: Boolean = transactionData.status == TransactionStatus.Unconfirmed
|
||||
|
||||
val transactionType: PendingTransactionType = when {
|
||||
transactionData.sourceAddress.lowercase() == walletAddress.lowercase() -> PendingTransactionType.Outgoing
|
||||
transactionData.destinationAddress.lowercase() == walletAddress.lowercase() -> PendingTransactionType.Incoming
|
||||
else -> PendingTransactionType.Unknown
|
||||
}
|
||||
|
||||
val address: String = when (transactionType) {
|
||||
PendingTransactionType.Incoming -> transactionData.sourceAddress.reduceAddress()
|
||||
PendingTransactionType.Outgoing -> transactionData.destinationAddress.reduceAddress()
|
||||
PendingTransactionType.Unknown -> "Unknown address"
|
||||
}
|
||||
|
||||
val time: String = transactionData.date?.let {
|
||||
SimpleDateFormat("HH:mm").format(it.time)
|
||||
} ?: "00:00"
|
||||
|
||||
val date: String = calculateDate()
|
||||
|
||||
val txSign: String = when (transactionType) {
|
||||
PendingTransactionType.Incoming -> "+"
|
||||
PendingTransactionType.Outgoing -> "-"
|
||||
PendingTransactionType.Unknown -> ""
|
||||
}
|
||||
|
||||
val amountValue: String = transactionData.amount.value
|
||||
?.toFormattedCurrencyString(8, transactionData.amount.currencySymbol) ?: "0"
|
||||
|
||||
private fun calculateDate(): String {
|
||||
val calendarTx = transactionData.date.guard { return "Unknown" }
|
||||
|
||||
val calendarNow = Calendar.getInstance()
|
||||
val oldDateFormatter = SimpleDateFormat("dd.MM.yyyy")
|
||||
|
||||
val date = if (calendarTx.get(Calendar.YEAR) == calendarNow.get(Calendar.YEAR)) {
|
||||
val dayOfYearTx = calendarTx.get(Calendar.DAY_OF_YEAR)
|
||||
val dayOfYearNow = calendarNow.get(Calendar.DAY_OF_YEAR)
|
||||
when {
|
||||
dayOfYearNow == dayOfYearTx -> "Today"
|
||||
dayOfYearNow - dayOfYearTx == 1 -> "Tomorrow"
|
||||
else -> oldDateFormatter.format(calendarTx.time)
|
||||
}
|
||||
} else {
|
||||
oldDateFormatter.format(calendarTx.time)
|
||||
}
|
||||
|
||||
return date
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun String.reduceAddress(): String = "${substring(0..5)}...${substring(length - 4)}"
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package com.tangem.tap.features.wallet.ui.wallet.saltPay.rv
|
||||
|
||||
import android.graphics.PorterDuff
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.tangem.tap.common.extensions.getColor
|
||||
import com.tangem.tap.common.extensions.setDrawable
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.features.wallet.models.PendingTransactionType
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.ItemSaltPayTxHistoryBinding
|
||||
import com.tangem.wallet.databinding.ItemSaltPayTxHistoryDateBinding
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class TxHistoryAdapter : ListAdapter<HistoryItemData, BaseTxHistoryVH>(DiffUtilCallback) {
|
||||
|
||||
override fun getItemId(position: Int): Long = if (currentList.isEmpty()) {
|
||||
0L
|
||||
} else {
|
||||
currentList[position].itemId
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseTxHistoryVH {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
|
||||
return when (viewType) {
|
||||
0 -> {
|
||||
val layout = ItemSaltPayTxHistoryDateBinding.inflate(inflater, parent, false)
|
||||
DateItemVH(layout)
|
||||
}
|
||||
1 -> {
|
||||
val layout = ItemSaltPayTxHistoryBinding.inflate(inflater, parent, false)
|
||||
TransactionItemVH(layout)
|
||||
}
|
||||
else -> throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int = currentList[position].viewType
|
||||
|
||||
override fun onBindViewHolder(holder: BaseTxHistoryVH, position: Int) {
|
||||
holder.bind(currentList[position])
|
||||
}
|
||||
|
||||
private object DiffUtilCallback : DiffUtil.ItemCallback<HistoryItemData>() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -9,8 +9,9 @@ import com.tangem.common.map
|
|||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Basic
|
||||
import com.tangem.tap.common.analytics.events.MyWallets
|
||||
import com.tangem.tap.common.analytics.paramsInterceptor.BatchIdParamsInterceptor
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
|
|
@ -140,7 +141,7 @@ internal class WalletSelectorMiddleware {
|
|||
)
|
||||
|
||||
ScanCardProcessor.scan(
|
||||
analyticsEvent = MyWallets.CardWasScanned(),
|
||||
analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.MyWallets),
|
||||
onWalletNotCreated = {
|
||||
// No need to rollback policy, continue with the policy set before the card scan
|
||||
store.dispatchOnMain(WalletSelectorAction.AddWallet.Success)
|
||||
|
|
@ -209,9 +210,6 @@ internal class WalletSelectorMiddleware {
|
|||
.doOnSuccess {
|
||||
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
|
||||
if (selectedUserWallet != null) {
|
||||
val batchId = selectedUserWallet.scanResponse.card.batchId
|
||||
Analytics.addParamsInterceptor(BatchIdParamsInterceptor(batchId))
|
||||
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
|
||||
store.onUserWalletSelected(selectedUserWallet)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,10 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<Wa
|
|||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
override fun ScreenContent(state: WalletSelectorScreenState, modifier: Modifier) {
|
||||
override fun ScreenContent(
|
||||
state: WalletSelectorScreenState,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
|
||||
val dialog by rememberUpdatedState(newValue = state.dialog)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,17 @@ 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.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
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
|
||||
|
|
@ -29,6 +39,7 @@ import com.tangem.core.ui.components.SpacerW6
|
|||
import com.tangem.core.ui.components.SpacerW8
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
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
|
||||
|
|
@ -100,7 +111,7 @@ private fun WalletCardImage(
|
|||
SubcomposeAsyncImage(
|
||||
modifier = cardImageModifier,
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(cardImageUrl)
|
||||
.cardImageData(cardImageUrl)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
loading = { CardImageShimmer(modifier = cardImageModifier) },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -4,9 +4,13 @@ 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.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.eraseContext
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
|
|
@ -38,6 +42,10 @@ internal class WelcomeMiddleware {
|
|||
|
||||
private fun handleAction(action: WelcomeAction, state: WelcomeState) {
|
||||
when (action) {
|
||||
is WelcomeAction.OnCreate -> {
|
||||
Analytics.eraseContext()
|
||||
Analytics.send(SignIn.ScreenOpened())
|
||||
}
|
||||
is WelcomeAction.ProceedWithBiometrics -> {
|
||||
proceedWithBiometrics(state)
|
||||
}
|
||||
|
|
@ -107,7 +115,7 @@ internal class WelcomeMiddleware {
|
|||
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes,
|
||||
)
|
||||
ScanCardProcessor.scan(
|
||||
analyticsEvent = SignIn.CardWasScanned(),
|
||||
analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.SignIn),
|
||||
onSuccess = { scanResponse ->
|
||||
scope.launch { onCardScanned(scanResponse) }
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,14 +19,14 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.ui.components.SystemBarsEffect
|
||||
import com.tangem.core.ui.fragments.ComposeFragment
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.common.analytics.events.SignIn
|
||||
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
|
||||
import com.tangem.tap.features.welcome.redux.WelcomeAction
|
||||
import com.tangem.tap.features.welcome.ui.components.BiometricsLockoutDialog
|
||||
import com.tangem.tap.features.welcome.ui.components.WelcomeScreenContent
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
||||
internal class WelcomeFragment : ComposeFragment<WelcomeScreenState>() {
|
||||
|
|
@ -34,7 +34,7 @@ internal class WelcomeFragment : ComposeFragment<WelcomeScreenState>() {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Analytics.send(SignIn.ScreenOpened())
|
||||
store.dispatch(WelcomeAction.OnCreate)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -43,7 +43,10 @@ internal class WelcomeFragment : ComposeFragment<WelcomeScreenState>() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(state: WelcomeScreenState, modifier: Modifier) {
|
||||
override fun ScreenContent(
|
||||
state: WelcomeScreenState,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
|
||||
val biometricsLockoutDialog by rememberUpdatedState(newValue = state.biometricsLockoutDialog)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue