Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-14 15:21:59 +03:00
commit 285e72a250
404 changed files with 8122 additions and 3606 deletions

View file

@ -9,17 +9,28 @@ plugins {
id("configuration")
}
//conflict of dependencies when adding WalletConnectV2.0 library
configurations {
all {
exclude(group = "org.bouncycastle", module = "bcprov-jdk15to18")
resolutionStrategy {
force("org.bouncycastle:bcpkix-jdk15on:1.70")
configurations.all {
exclude(group = "org.bouncycastle", module = "bcprov-jdk15to18")
exclude(group = "com.github.komputing.kethereum")
resolutionStrategy {
dependencySubstitution {
substitute(module("com.facebook.react:react-native"))
.using(module("com.facebook.react:react-android:0.72.4"))
substitute(module("com.facebook.react:hermes-engine"))
.using(module("com.facebook.react:hermes-android:0.72.4"))
}
exclude(group = "com.github.komputing.kethereum")
force(
"org.bouncycastle:bcpkix-jdk15on:1.70",
"com.facebook.react:react-android:0.72.4",
"com.facebook.react:hermes-android:0.72.4",
)
}
}
dependencies {
implementation(files("libs/walletconnect-1.5.6.aar"))
implementation(projects.domain.legacy)
@ -94,6 +105,7 @@ dependencies {
implementation(deps.androidx.activity.compose)
implementation(deps.androidx.browser)
implementation(deps.androidx.paging.runtime)
implementation(deps.androidx.swipeRefreshLayout)
implementation(deps.lifecycle.runtime.ktx)
implementation(deps.lifecycle.common.java8)
implementation(deps.lifecycle.viewModel.ktx)
@ -142,14 +154,11 @@ dependencies {
implementation(deps.timber)
implementation(deps.reKotlin)
implementation(deps.zxing.qrCore)
implementation(deps.zxing.qrBarcodeScanner)
implementation(deps.otaliastudiosCameraView)
implementation(deps.coil)
implementation(deps.appsflyer)
implementation(deps.amplitude)
implementation(deps.kotsonGson)
implementation(deps.zendesk.chat)
implementation(deps.zendesk.messaging)
implementation(deps.spongecastle.core)
implementation(deps.lottie)
implementation(deps.shopify.buy) {
@ -167,6 +176,10 @@ dependencies {
implementation(deps.walletConnectCore)
implementation(deps.walletConnectWeb3)
implementation(deps.prettyLogger)
implementation("com.facebook.react:react-android:0.72.4")
implementation(deps.sprClient) {
exclude(group = "com.github.stephenc.jcip")
}
/** Testing libraries */
testImplementation(deps.test.junit)
@ -179,4 +192,19 @@ dependencies {
externalImplementation(deps.chuckerStub)
internalImplementation(deps.chuckerStub)
releaseImplementation(deps.chuckerStub)
/** Camera */
implementation(deps.camera.camera2)
implementation(deps.camera.lifecycle)
implementation(deps.camera.view)
implementation(deps.listenableFuture)
implementation(deps.mlKit.barcodeScanning)
/** Excluded dependencies */
implementation("com.google.guava:guava:30.0-android") {
// excludes version 9999.0-empty-to-avoid-conflict-with-guava
exclude(group="com.google.guava", module = "listenablefuture")
}
}

View file

@ -12,7 +12,16 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
@ -40,7 +49,7 @@
android:extractNativeLibs="true"
android:supportsRtl="true"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:allowBackup, android:fullBackupContent">
tools:replace="android:allowBackup, android:fullBackupContent, android:label">
<meta-data
android:name="com.google.android.gms.wallet.api.enabled"
@ -132,14 +141,6 @@
android:name="com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity"
android:theme="@style/AppTheme" />
<activity
android:name="zendesk.messaging.MessagingActivity"
android:theme="@style/ZendeskTheme" />
<activity
android:name="com.tangem.tap.features.sprinklr.ui.SprinklrActivity"
android:theme="@style/AppTheme" />
<activity
android:name="com.tangem.feature.learn2earn.presentation.webView.Learn2earnWebViewActivity"
android:theme="@style/AppTheme" />

View file

@ -511,12 +511,31 @@
"id": "aleph-zero",
"symbol": "AZERO",
"name": "Aleph Zero",
"networks":
[
"networks": [
{
"networkId": "aleph-zero/test"
}
]
},
{
"id": "near",
"symbol": "NEAR",
"name": "NEAR",
"networks": [
{
"networkId": "near-protocol/test"
}
]
},
{
"id": "decimal",
"name": "Decimal",
"symbol": "tDEL",
"networks": [
{
"networkId": "decimal/test"
}
]
}
]
}

View file

@ -9,6 +9,8 @@ import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.activity.viewModels
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode
@ -21,9 +23,13 @@ import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import arrow.core.getOrElse
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.data.card.sdk.CardSdkLifecycleObserver
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.card.ScanCardUseCase
@ -46,12 +52,15 @@ import com.tangem.tap.common.shop.googlepay.GooglePayService
import com.tangem.tap.common.shop.googlepay.GooglePayService.Companion.LOAD_PAYMENT_DATA_REQUEST_CODE
import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.features.intentHandler.IntentProcessor
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
import com.tangem.tap.features.intentHandler.handlers.BuyCurrencyIntentHandler
import com.tangem.tap.features.intentHandler.handlers.SellCurrencyIntentHandler
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.features.main.MainViewModel
import com.tangem.tap.features.main.model.Toast
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.features.welcome.ui.WelcomeFragment
@ -125,6 +134,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var sendRouter: SendRouter
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode?>
// TODO: fixme: inject through DI
@ -151,6 +162,27 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
checkGooglePayAvailability()
checkForNotificationPermission()
observeStateUpdates()
}
private fun observeStateUpdates() {
viewModel.state
.flowWithLifecycle(lifecycle)
.onEach { state ->
if (state.toast is StateEvent.Triggered) {
showToast(state.toast.data)
state.toast.onConsume()
}
}
.launchIn(lifecycleScope)
}
private fun showToast(toast: Toast) {
dismissSnackbar()
showSnackbar(toast.message, Snackbar.LENGTH_LONG, toast.action.text) {
toast.action.onClick()
dismissSnackbar()
}
}
private fun installActivityDependencies() {
@ -276,6 +308,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
setDefaultNightMode(mode)
delegate.localNightMode = mode
}
private fun isDarkTheme(): Boolean {
@ -301,18 +334,22 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
}
override fun showSnackbar(text: Int, buttonTitle: Int?, action: View.OnClickListener?) {
if (snackbar != null) return
override fun showSnackbar(
@StringRes text: Int,
length: Int,
@StringRes buttonTitle: Int?,
action: View.OnClickListener?,
) {
showSnackbar(getString(text), length, buttonTitle?.let(::getString), action)
}
snackbar = Snackbar.make(
binding.fragmentContainer,
getString(text),
Snackbar.LENGTH_INDEFINITE,
)
if (buttonTitle != null && action != null) {
snackbar?.setAction(getString(buttonTitle), action)
}
snackbar?.show()
override fun showSnackbar(
text: TextReference,
length: Int,
buttonTitle: TextReference?,
action: View.OnClickListener?,
) {
showSnackbar(text.resolveReference(resources), length, buttonTitle?.resolveReference(resources), action)
}
override fun dismissSnackbar() {
@ -348,6 +385,33 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
lockUserWalletsTimer?.restart()
}
private fun showSnackbar(text: String, length: Int, buttonTitle: String?, action: View.OnClickListener?) {
if (snackbar != null) return
snackbar = Snackbar.make(binding.fragmentContainer, text, length).apply {
val textColor = getColor(R.color.text_primary_2)
setBackgroundTint(getColor(R.color.button_primary))
setActionTextColor(textColor)
setTextColor(textColor)
if (buttonTitle != null && action != null) {
setAction(buttonTitle, action)
}
addCallback(
object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
snackbar = null
removeCallback(this)
}
},
)
}
snackbar?.show()
}
private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) {
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
val isNotScannedBefore = store.state.globalState.scanResponse == null
@ -366,7 +430,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) {
if (store.state.globalState.userWalletsListManager?.hasUserWallets == true) {
val canSaveWallets = userWalletsListManager is BiometricUserWalletsListManager
val hasSavedWallets = userWalletsListManager.hasUserWallets
if (canSaveWallets && hasSavedWallets) {
store.dispatch(
NavigationAction.NavigateTo(
screen = AppScreen.Welcome,

View file

@ -350,7 +350,7 @@ internal class TapApplication : Application(), ImageLoaderFactory {
private fun initWithConfigDependency(config: Config) {
shopService = TangemShopService(this, config.shopify!!)
initAnalytics(this, config)
initFeedbackManager(this, preferencesStorage, foregroundActivityObserver, store)
initFeedbackManager(this, foregroundActivityObserver, store)
}
private fun initAnalytics(application: Application, config: Config) {
@ -372,7 +372,6 @@ internal class TapApplication : Application(), ImageLoaderFactory {
private fun initFeedbackManager(
context: Context,
preferencesStorage: PreferencesDataSource,
foregroundActivityObserver: ForegroundActivityObserver,
store: Store<AppState>,
) {
@ -411,7 +410,7 @@ internal class TapApplication : Application(), ImageLoaderFactory {
val feedbackManager = FeedbackManager(
infoHolder = additionalFeedbackInfo,
logCollector = tangemLogCollector,
chatManager = ChatManager(preferencesStorage, foregroundActivityObserver, store),
chatManager = ChatManager(foregroundActivityObserver),
)
store.dispatch(GlobalAction.SetFeedbackManager(feedbackManager))
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.common
import android.app.Dialog
import android.content.Context
import com.tangem.core.navigation.StateDialog
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.ui.SimpleAlertDialog
@ -53,7 +54,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
is AppDialog.SimpleOkDialogRes -> SimpleOkDialog.create(state.dialog, context)
is AppDialog.SimpleOkErrorDialog -> SimpleOkDialog.create(state.dialog, context)
is AppDialog.SimpleOkWarningDialog -> SimpleOkDialog.create(state.dialog, context)
is AppDialog.ScanFailsDialog -> ScanFailsDialog.create(context)
is StateDialog.ScanFailsDialog -> ScanFailsDialog.create(context)
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context)
is OnboardingDialog.TwinningProcessNotCompleted -> TwinningProcessNotCompletedDialog.create(context)
@ -105,7 +106,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
context.getString(R.string.wallet_connect_scanner_error_unsupported_network)
} else {
context.getString(R.string.wallet_connect_error_unsupported_blockchains) +
state.dialog.networks
state.dialog.networks.joinToString()
}
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,

View file

@ -1,8 +1,25 @@
package com.tangem.tap.common
import android.view.View
import androidx.annotation.StringRes
import com.google.android.material.snackbar.Snackbar
import com.tangem.core.ui.extensions.TextReference
interface SnackbarHandler {
fun showSnackbar(text: Int, buttonTitle: Int? = null, action: View.OnClickListener? = null)
fun showSnackbar(
@StringRes text: Int,
length: Int = Snackbar.LENGTH_INDEFINITE,
@StringRes buttonTitle: Int? = null,
action: View.OnClickListener? = null,
)
fun showSnackbar(
text: TextReference,
length: Int = Snackbar.LENGTH_INDEFINITE,
buttonTitle: TextReference? = null,
action: View.OnClickListener? = null,
)
fun dismissSnackbar()
}

View file

@ -21,8 +21,9 @@ sealed class AnalyticsParam {
sealed class CardBalanceState(val value: String) {
object Empty : CardBalanceState("Empty")
object Full : CardBalanceState("Full")
object CustomToken : CardBalanceState("Custom token")
object BlockchainError : CardBalanceState("Blockchain error")
object CustomToken : CardBalanceState("Custom Token")
object BlockchainError : CardBalanceState("Blockchain Error")
object NoRate : CardBalanceState("No Rate")
companion object
}

View file

@ -80,7 +80,7 @@ sealed class Settings(
object ButtonEnableBiometricAuthentication : AppSettings(event = "Button - Enable Biometric Authentication")
class MainCurrencyChanged(currencyType: String) : MainScreen(
class MainCurrencyChanged(currencyType: String) : AppSettings(
event = "Main Currency Changed",
params = mapOf("Currency Type" to currencyType),
)

View file

@ -46,6 +46,7 @@ class CardContextInterceptor(
ProductType.Twins -> "Twin"
ProductType.Wallet -> "Wallet"
ProductType.Wallet2 -> "Wallet 2.0"
ProductType.Ring -> "Ring"
ProductType.Start2Coin -> "Start2Coin"
else -> if (DemoHelper.isDemoCard(scanResponse)) {
if (DemoHelper.isTestDemoCard(scanResponse)) {

View file

@ -141,6 +141,8 @@ class TopUpController(
DataSourceTopupInfo.CardBalanceState.Empty
AnalyticsParam.CardBalanceState.Full ->
DataSourceTopupInfo.CardBalanceState.Full
AnalyticsParam.CardBalanceState.NoRate ->
DataSourceTopupInfo.CardBalanceState.NoRate
},
)
topupWalletStorage.save(topupInfo)

View file

@ -1,57 +1,23 @@
package com.tangem.tap.common.chat
import android.content.Context
import android.os.Build
import com.tangem.datasource.config.models.ChatConfig
import com.tangem.datasource.config.models.SprinklrConfig
import com.tangem.datasource.config.models.ZendeskConfig
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.tap.ForegroundActivityObserver
import com.tangem.tap.common.chat.opener.ChatOpener
import com.tangem.tap.common.chat.opener.implementation.SprinklrChatOpener
import com.tangem.tap.common.chat.opener.implementation.ZendeskChatOpener
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Store
import java.io.File
class ChatManager(
private val preferencesStorage: PreferencesDataSource,
private val foregroundActivityObserver: ForegroundActivityObserver,
private val store: Store<AppState>,
) {
class ChatManager(private val foregroundActivityObserver: ForegroundActivityObserver) {
private val openers = mutableMapOf<ChatConfig, ChatOpener>()
fun open(config: ChatConfig, createLogsFile: (Context) -> File?, createFeedbackFile: (Context) -> File?) {
val opener = openers.getOrPut(config) {
when (config) {
is SprinklrConfig -> SprinklrChatOpener(getSprinklrUserId(), config, store, foregroundActivityObserver)
is ZendeskConfig -> ZendeskChatOpener(getZendeskUserId(), config, foregroundActivityObserver)
is SprinklrConfig -> SprinklrChatOpener(config, foregroundActivityObserver)
}
}
opener.open(
createFeedbackFile = createFeedbackFile,
createLogsFile = createLogsFile,
)
}
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()
opener.open(createFeedbackFile, createLogsFile)
}
}

View file

@ -1,28 +1,57 @@
package com.tangem.tap.common.chat.opener.implementation
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.content.Intent
import android.provider.Settings
import com.spr.messengerclient.config.SPRMessenger
import com.spr.messengerclient.config.bean.SPRMessengerConfig
import com.tangem.common.extensions.guard
import com.tangem.datasource.config.models.SprinklrConfig
import com.tangem.tap.ForegroundActivityObserver
import com.tangem.tap.common.chat.opener.ChatOpener
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.sprinklr.redux.SprinklrAction
import com.tangem.tap.features.sprinklr.ui.SprinklrActivity
import com.tangem.tap.withForegroundActivity
import org.rekotlin.Store
import timber.log.Timber
import java.io.File
import java.util.Locale
internal class SprinklrChatOpener(
private val userId: String,
private val config: SprinklrConfig,
private val store: Store<AppState>,
private val foregroundActivityObserver: ForegroundActivityObserver,
) : ChatOpener {
override fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?) {
store.dispatch(SprinklrAction.Init(userId, config))
foregroundActivityObserver.withForegroundActivity { activity ->
val intent = Intent(activity, SprinklrActivity::class.java)
activity.startActivity(intent)
val messenger = SPRMessenger.shared()
if (messenger.config == null) {
initSprConfig(messenger)
}
messenger.startApplication()
}
private fun initSprConfig(messenger: SPRMessenger) {
val application = foregroundActivityObserver.foregroundActivity?.application.guard {
Timber.e("The SPR chat cannot be opened because there are no activities in foreground")
return
}
messenger.takeOff(application, createSprConfig(application, config))
}
@SuppressLint("HardwareIds")
private fun createSprConfig(application: Application, config: SprinklrConfig): SPRMessengerConfig {
return SPRMessengerConfig().apply {
appId = config.appId
appKey = CHAT_APP_KEY
deviceId = Settings.Secure.getString(application.contentResolver, Settings.Secure.ANDROID_ID)
environment = config.environment
skin = CHAT_SKIN
locale = Locale.getDefault().language
}
}
private companion object {
const val CHAT_APP_KEY = "com.sprinklr.messenger.release"
const val CHAT_SKIN = "MODERN"
}
}

View file

@ -1,87 +0,0 @@
package com.tangem.tap.common.chat.opener.implementation
import android.content.Context
import com.tangem.core.analytics.Analytics
import com.tangem.datasource.config.models.ZendeskConfig
import com.tangem.domain.common.LogConfig
import com.tangem.tap.ForegroundActivityObserver
import com.tangem.tap.common.chat.opener.ChatOpener
import com.tangem.tap.withForegroundActivity
import com.tangem.wallet.R
import com.zendesk.logger.Logger
import timber.log.Timber
import zendesk.chat.*
import zendesk.configurations.Configuration
import zendesk.messaging.MessagingActivity
import java.io.File
internal class ZendeskChatOpener(
private val userId: String,
private val config: ZendeskConfig,
private val foregroundActivityObserver: ForegroundActivityObserver,
) : ChatOpener {
private var isInitialized = false
private var isFilesSent = false
override fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?) {
foregroundActivityObserver.withForegroundActivity { activity ->
initZendeskIfNeeded(activity.applicationContext)
setChatVisitorInfo()
showMessagingActivity(activity)
sendFeedbackFile(createFeedbackFile(activity))
sendLogsFile(createLogsFile(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 sendFeedbackFile(feedbackFile: File?) {
if (isInitialized && feedbackFile != null && !isFilesSent) {
Chat.INSTANCE.providers()?.chatProvider()?.sendFile(feedbackFile) { _, bytesUploaded, _ ->
Timber.d("Log file sent", "bytesUploaded: $bytesUploaded")
}
isFilesSent = true
}
}
private fun sendLogsFile(logsFile: File?) {
if (isInitialized && logsFile != null && !isFilesSent) {
Chat.INSTANCE.providers()?.chatProvider()?.sendFile(logsFile) { _, bytesUploaded, _ ->
Timber.d("Log file sent", "bytesUploaded: $bytesUploaded")
}
isFilesSent = true
}
}
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()
}
}

View file

@ -18,6 +18,7 @@ import com.tangem.tap.features.details.ui.walletconnect.QrScanFragment
import com.tangem.tap.features.details.ui.walletconnect.WalletConnectFragment
import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment
import com.tangem.tap.features.home.HomeFragment
import com.tangem.tap.features.main.ui.ModalNotificationBottomSheetFragment
import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment
import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCardsFragment
import com.tangem.tap.features.onboarding.products.twins.ui.TwinsCardsFragment
@ -194,5 +195,6 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.SaveWallet -> SaveWalletBottomSheetFragment()
AppScreen.WalletSelector -> WalletSelectorBottomSheetFragment()
AppScreen.AppCurrencySelector -> AppCurrencySelectorFragment()
AppScreen.ModalNotification -> ModalNotificationBottomSheetFragment()
}
}

View file

@ -1,10 +1,10 @@
package com.tangem.tap.common.extensions
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.StateDialog
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.scope

View file

@ -26,10 +26,8 @@ import timber.log.Timber
"or WalletCurrenciesManager.update(...) (to update only one user wallet blockchain and its tokens) instead",
)
@Suppress("MagicNumber")
suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
val scanResponse = store.state.globalState.scanResponse
if (scanResponse?.isDemoCard() == true || TestActions.testAmountInjectionForWalletManagerEnabled) {
suspend fun WalletManager.safeUpdate(isDemoCard: Boolean): Result<Wallet> = try {
if (isDemoCard || TestActions.testAmountInjectionForWalletManagerEnabled) {
delay(500)
TestActions.testAmountInjectionForWalletManagerEnabled = false
Result.Success(wallet)

View file

@ -7,17 +7,24 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletIdBuilder
import com.tangem.tap.common.extensions.stripZeroPlainString
import kotlinx.coroutines.flow.*
import java.util.concurrent.CopyOnWriteArrayList
class AdditionalFeedbackInfo {
class EmailWalletInfo(
var blockchain: Blockchain = Blockchain.Unknown,
var derivationPath: String = "",
var outputsCount: String? = null,
var host: String = "",
var addresses: String = "",
var explorerLink: String = "",
val blockchain: Blockchain = Blockchain.Unknown,
val derivationPath: String = "",
val outputsCount: String? = null,
val host: String = "",
val addresses: String = "",
val explorerLink: String = "",
val tokens: List<EmailTokenInfo> = emptyList(),
)
class EmailTokenInfo(
val id: String?,
val name: String,
val contractAddress: String,
)
var appVersion: String = ""
@ -30,20 +37,27 @@ class AdditionalFeedbackInfo {
var userWalletId: String = ""
// wallets
internal val walletsInfo = mutableListOf<EmailWalletInfo>()
internal val tokens = mutableMapOf<Blockchain, Collection<Token>>()
internal var onSendErrorWalletInfo: EmailWalletInfo? = null
val walletsInfo = CopyOnWriteArrayList<EmailWalletInfo>()
var onSendErrorWalletInfo: EmailWalletInfo? = null
private set
var signedHashesCount: String = ""
private set
// device
var phoneModel: String = Build.MODEL
private set
var osVersion: String = Build.VERSION.SDK_INT.toString()
private set
// send error
var destinationAddress: String = ""
private set
var amount: String = ""
private set
var fee: String = ""
private set
var token: String = ""
private set
private val Address.name: String
get() = type.javaClass.simpleName
@ -60,26 +74,26 @@ class AdditionalFeedbackInfo {
@Deprecated("Don't use it directly")
fun setWalletsInfo(walletManagers: List<WalletManager>) {
walletsInfo.clear()
tokens.clear()
walletManagers.forEach { manager ->
walletsInfo.add(createEmailWalletInfo(manager))
if (manager.cardTokens.isNotEmpty()) {
tokens[manager.wallet.blockchain] = manager.cardTokens
}
synchronized(walletsInfo) {
walletsInfo.clear()
walletsInfo.addAll(elements = walletManagers.map(::createEmailWalletInfo))
}
}
fun updateOnSendError(
walletManager: WalletManager,
amountToSend: Amount,
feeAmount: Amount,
feeAmount: Amount?,
destinationAddress: String,
) {
onSendErrorWalletInfo = createEmailWalletInfo(walletManager)
this.destinationAddress = destinationAddress
amount = amountToSend.value?.stripZeroPlainString() ?: "0"
fee = feeAmount.value?.stripZeroPlainString() ?: "0"
fee = if (feeAmount != null) {
feeAmount.value?.stripZeroPlainString() ?: "0"
} else {
"Unable to receive"
}
token = if (amountToSend.type is AmountType.Token) amountToSend.currencySymbol else ""
}
@ -91,6 +105,9 @@ class AdditionalFeedbackInfo {
host = walletManager.currentHost,
addresses = formatAddresses(walletManager.wallet),
explorerLink = formatExploreUrls(walletManager.wallet),
tokens = walletManager.cardTokens.map { token ->
EmailTokenInfo(token.id, token.name, token.contractAddress)
},
)
}

View file

@ -33,10 +33,10 @@ class FeedbackDataBuilder(
builder.appendKeyValue("Derivation path", it.derivationPath)
builder.appendKeyValue("Outputs count", it.outputsCount)
infoHolder.tokens[it.blockchain]?.let { tokens ->
if (it.tokens.isNotEmpty()) {
builder.append("Tokens:")
breakLine()
tokens.forEach { token ->
it.tokens.forEach { token ->
builder.appendKeyValue("ID", token.id ?: "[custom token]")
builder.appendKeyValue("Name", token.name)
builder.appendKeyValue("Contract address", token.contractAddress)

View file

@ -0,0 +1,39 @@
package com.tangem.tap.common.qrCodeScan
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.common.InputImage
class MLKitBarcodeAnalyzer(private val onScanned: (String) -> Unit) : ImageAnalysis.Analyzer {
private var isScanning: Boolean = false
@ExperimentalGetImage
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image
if (mediaImage != null && !isScanning) {
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
val scanner = BarcodeScanning.getClient()
isScanning = true
scanner.process(image)
.addOnSuccessListener { codes ->
codes.firstOrNull().let { barcode ->
val rawValue = barcode?.rawValue
rawValue?.let {
onScanned.invoke(it)
}
}
isScanning = false
imageProxy.close()
}
.addOnFailureListener {
isScanning = false
imageProxy.close()
}
}
}
}

View file

@ -3,48 +3,63 @@ package com.tangem.tap.common.qrCodeScan
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.zxing.BarcodeFormat
import com.google.zxing.Result
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.common.util.concurrent.ListenableFuture
import com.otaliastudios.cameraview.CameraView.PERMISSION_REQUEST_CODE
import me.dm7.barcodescanner.zxing.ZXingScannerView
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PreviewBinder
import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutQrScanningBinding
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
/**
[REDACTED_AUTHOR]
*/
class ScanQrCodeActivity : AppCompatActivity(), ZXingScannerView.ResultHandler {
class ScanQrCodeActivity : AppCompatActivity() {
private lateinit var mScannerView: ZXingScannerView
private val binding: LayoutQrScanningBinding by viewBinding(LayoutQrScanningBinding::bind)
private var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>? = null
private var cameraExecutor: ExecutorService? = null
private val binder = PreviewBinder()
override fun onCreate(state: Bundle?) {
super.onCreate(state)
mScannerView = ZXingScannerView(this).apply {
setFormats(listOf(BarcodeFormat.QR_CODE))
}
setContentView(mScannerView)
if (!permissionIsGranted()) requestPermission()
}
override fun onResume() {
super.onResume()
mScannerView.setResultHandler(this)
mScannerView.startCamera()
}
setContentView(R.layout.layout_qr_scanning)
override fun onPause() {
super.onPause()
mScannerView.stopCamera()
}
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraExecutor = Executors.newSingleThreadExecutor()
override fun handleResult(result: Result) {
setResult(SCAN_QR_REQUEST_CODE, Intent().apply { putExtra(SCAN_RESULT, result.text) })
finish()
cameraProviderFuture?.addListener(
{
val cameraProvider = cameraProviderFuture?.get()
binder.bindPreview(
context = this,
binding = binding,
lifecycleOwner = this,
cameraProvider = requireNotNull(cameraProvider),
cameraExecutor = requireNotNull(cameraExecutor),
onScanned = { result ->
setResult(SCAN_QR_REQUEST_CODE, Intent().apply { putExtra(SCAN_RESULT, result) })
finish()
},
)
},
ContextCompat.getMainExecutor(this),
)
binding.overlay.post {
binding.overlay.setViewFinder()
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
@ -56,12 +71,8 @@ class ScanQrCodeActivity : AppCompatActivity(), ZXingScannerView.ResultHandler {
}
private fun permissionIsGranted(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val cameraPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
cameraPermission == PackageManager.PERMISSION_GRANTED
} else {
true
}
val cameraPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
return cameraPermission == PackageManager.PERMISSION_GRANTED
}
private fun requestPermission() {

View file

@ -0,0 +1,42 @@
package com.tangem.tap.common.qrCodeScan
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import androidx.core.content.ContextCompat
import com.tangem.wallet.R
class ViewFinderOverlay(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val boxPaint: Paint = Paint().apply {
color = ContextCompat.getColor(context, R.color.white)
style = Paint.Style.STROKE
strokeWidth = context.resources.getDimensionPixelOffset(R.dimen.qr_border_stroke_width).toFloat()
}
private val boxWidthRatio = 0.8F
private val boxCornerRadius: Float =
context.resources.getDimensionPixelOffset(R.dimen.qr_border_corner_radius).toFloat()
private var boxRect: RectF? = null
@Suppress("MagicNumber")
fun setViewFinder() {
val overlayWidth = width.toFloat()
val overlayHeight = height.toFloat()
val boxSize = overlayWidth * boxWidthRatio
val cx = overlayWidth / 2
val cy = overlayHeight / 2
boxRect = RectF(cx - boxSize / 2, cy - boxSize / 2, cx + boxSize / 2, cy + boxSize / 2)
invalidate()
}
override fun draw(canvas: Canvas) {
super.draw(canvas)
boxRect?.let {
canvas.drawRoundRect(it, boxCornerRadius, boxCornerRadius, boxPaint)
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.common.redux
import com.tangem.common.extensions.VoidCallback
import com.tangem.core.navigation.StateDialog
import com.tangem.tap.common.TestAction
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.features.wallet.models.Currency
@ -8,8 +9,6 @@ import com.tangem.tap.features.wallet.models.Currency
/**
[REDACTED_AUTHOR]
*/
interface StateDialog
sealed class AppDialog : StateDialog {
data class SimpleOkDialog(val header: String, val message: String, val onOk: VoidCallback? = null) : AppDialog()
data class SimpleOkErrorDialog(val message: String, val onOk: VoidCallback? = null) : AppDialog()
@ -33,8 +32,6 @@ sealed class AppDialog : StateDialog {
val action: VoidCallback? = null,
)
object ScanFailsDialog : AppDialog()
data class AddressInfoDialog(
val currency: Currency,
val addressData: WalletDataModel.AddressData,

View file

@ -14,7 +14,6 @@ import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.shop.redux.ShopReducer
import com.tangem.tap.features.signin.redux.SignInReducer
import com.tangem.tap.features.sprinklr.redux.SprinklrReducer
import com.tangem.tap.features.tokens.legacy.redux.TokensReducer
import com.tangem.tap.features.wallet.redux.reducers.WalletReducer
import com.tangem.tap.features.walletSelector.redux.WalletSelectorReducer
@ -45,7 +44,6 @@ 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),
signInState = SignInReducer.reduce(action, state),
daggerGraphState = DaggerGraphReducer.reduce(action, state),
)

View file

@ -33,8 +33,6 @@ import com.tangem.tap.features.shop.redux.ShopMiddleware
import com.tangem.tap.features.shop.redux.ShopState
import com.tangem.tap.features.signin.redux.SignInMiddleware
import com.tangem.tap.features.signin.redux.SignInState
import com.tangem.tap.features.sprinklr.redux.SprinklrMiddleware
import com.tangem.tap.features.sprinklr.redux.SprinklrState
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
import com.tangem.tap.features.tokens.legacy.redux.TokensState
import com.tangem.tap.features.wallet.redux.WalletState
@ -67,7 +65,6 @@ data class AppState(
val welcomeState: WelcomeState = WelcomeState(),
val saveWalletState: SaveWalletState = SaveWalletState(),
val walletSelectorState: WalletSelectorState = WalletSelectorState(),
val sprinklrState: SprinklrState = SprinklrState(),
val signInState: SignInState = SignInState(),
val daggerGraphState: DaggerGraphState = DaggerGraphState(),
) : StateType {
@ -109,7 +106,6 @@ data class AppState(
WalletSelectorMiddleware().middleware,
LockUserWalletsTimerMiddleware().middleware,
AccessCodeRequestPolicyMiddleware().middleware,
SprinklrMiddleware().middleware,
SignInMiddleware.middleware,
DaggerGraphMiddleware.daggerGraphMiddleware,
LegacyMiddleware.legacyMiddleware,

View file

@ -2,6 +2,7 @@ package com.tangem.tap.common.redux.global
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.CompletionResult
import com.tangem.core.navigation.StateDialog
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.models.ChatConfig
import com.tangem.domain.apptheme.model.AppThemeMode
@ -11,7 +12,10 @@ import com.tangem.tap.common.analytics.topup.TopUpController
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackData
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.redux.*
import com.tangem.tap.common.redux.DebugErrorAction
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.ToastNotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager

View file

@ -4,17 +4,18 @@ import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.StateDialog
import com.tangem.datasource.config.models.Config
import com.tangem.domain.common.LogConfig
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.redux.WalletAction
@ -25,17 +26,13 @@ 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.preferencesStorage
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.walletCurrenciesManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
import timber.log.Timber
import java.util.Locale
object GlobalMiddleware {
@ -45,14 +42,14 @@ object GlobalMiddleware {
private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState ->
{ nextDispatch ->
{ action ->
handleAction(action, appState, dispatch)
handleAction(action, appState)
nextDispatch(action)
}
}
}
@Suppress("LongMethod", "ComplexMethod")
private fun handleAction(action: Action, appState: () -> AppState?, dispatch: DispatchFunction) {
private fun handleAction(action: Action, appState: () -> AppState?) {
when (action) {
is GlobalAction.ScanFailsCounter.ChooseBehavior -> {
when (action.result) {
@ -61,7 +58,7 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
if (action.result.error is TangemSdkError.UserCancelled) {
store.dispatch(GlobalAction.ScanFailsCounter.Increment)
if (store.state.globalState.scanCardFailsCounter >= 2) {
store.dispatchDialogShow(AppDialog.ScanFailsDialog)
store.dispatchDialogShow(StateDialog.ScanFailsDialog)
}
} else {
store.dispatch(GlobalAction.ScanFailsCounter.Reset)
@ -110,10 +107,10 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
}
// if config not set -> try to get it based on a scanResponse.productType
val unsafeChatConfig = action.chatConfig ?: config.zendesk
val unsafeChatConfig = action.chatConfig ?: config.sprinklr
val chatConfig = unsafeChatConfig.guard {
store.dispatchDebugErrorNotification("ZendeskConfig not initialized")
store.dispatchDebugErrorNotification("The chat config is not initialized")
return
}
feedbackManager.openChat(chatConfig, action.feedbackData)
@ -131,8 +128,7 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
scope.launch {
val scanResponseProvider: () -> ScanResponse? = {
store.state.globalState.scanResponse
?: store.state.globalState.onboardingState.onboardingManager?.scanResponse
userWalletsListManager.selectedUserWalletSync?.scanResponse
}
val cardProvider: () -> CardDTO? = { scanResponseProvider.invoke()?.card }
@ -177,22 +173,26 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
walletCurrenciesManager.addListener(action.topUpController)
}
is GlobalAction.UpdateUserWalletsListManager -> {
val walletManagersFacade = store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade)
/*
* If UserWalletsListManager's implementation is changed,
* then all selectedUserWallet's observers is became irrelevant
* If implementation of the UserWalletsListManager is changed,
* then all observers of selectedUserWallet become irrelevant.
*/
action.manager.selectedUserWallet
.distinctUntilChanged()
.onEach { userWallet ->
Analytics.send(event = Basic.WalletOpened())
Analytics.send(Basic.WalletOpened())
store.state.globalState.feedbackManager?.infoHolder?.let { infoHolder ->
infoHolder.setCardInfo(data = userWallet.scanResponse)
infoHolder.setCardInfo(userWallet.scanResponse)
store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade)
.getAll(userWalletId = userWallet.walletId)
walletManagersFacade
.getAll(userWallet.walletId)
.distinctUntilChanged()
.onEach(infoHolder::setWalletsInfo)
.launchIn(scope)
.catch { Timber.e(it) }
.launchIn(mainScope)
}
}
.flowOn(Dispatchers.IO)

View file

@ -1,5 +1,6 @@
package com.tangem.tap.common.redux.global
import com.tangem.core.navigation.StateDialog
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse
@ -7,7 +8,6 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.tap.common.analytics.topup.TopUpController
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.onboarding.OnboardingManager

View file

@ -8,10 +8,7 @@ import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.tap.activityResultCaller
import com.tangem.tap.common.CustomTabsManager
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.openFragment
import com.tangem.tap.common.extensions.popBackTo
import com.tangem.tap.common.extensions.shareText
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.store
import org.rekotlin.Middleware
@ -60,6 +57,7 @@ val navigationMiddleware: Middleware<AppState> = { _, state ->
intent.data = action.url
navState?.activity?.get()?.startActivity(intent)
}
is NavigationAction.OpenDialog -> store.dispatchDialogShow(action.stateDialog)
is NavigationAction.OpenBiometricsSettings -> {
val settingsAction = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {

View file

@ -1,8 +1,9 @@
package com.tangem.tap.di.domain
import com.tangem.domain.balancehiding.DeviceFlipDetector
import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.settings.*
import com.tangem.domain.settings.repositories.AppRatingRepository
@ -63,8 +64,10 @@ internal object SettingsDomainModule {
@Provides
@ViewModelScoped
fun providesIsBalanceHiddenUseCase(balanceHidingRepository: BalanceHidingRepository): IsBalanceHiddenUseCase {
return IsBalanceHiddenUseCase(
fun providesGetBalanceHidingSettingsUseCase(
balanceHidingRepository: BalanceHidingRepository,
): GetBalanceHidingSettingsUseCase {
return GetBalanceHidingSettingsUseCase(
balanceHidingRepository = balanceHidingRepository,
)
}
@ -80,4 +83,12 @@ internal object SettingsDomainModule {
balanceHidingRepository = balanceHidingRepository,
)
}
@Provides
@ViewModelScoped
fun provideUpdateHideBalancesSettingsUseCase(
balanceHidingRepository: BalanceHidingRepository,
): UpdateBalanceHidingSettingsUseCase {
return UpdateBalanceHidingSettingsUseCase(balanceHidingRepository)
}
}

View file

@ -63,9 +63,9 @@ internal object TokensDomainModule {
@ViewModelScoped
fun provideRemoveCurrencyUseCase(
currenciesRepository: CurrenciesRepository,
dispatchers: CoroutineDispatcherProvider,
walletManagersFacade: WalletManagersFacade,
): RemoveCurrencyUseCase {
return RemoveCurrencyUseCase(currenciesRepository, dispatchers)
return RemoveCurrencyUseCase(currenciesRepository, walletManagersFacade)
}
@Provides
@ -167,9 +167,19 @@ internal object TokensDomainModule {
fun provideGetCryptoCurrencyActionsUseCase(
rampStateManager: RampStateManager,
marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
dispatchers: CoroutineDispatcherProvider,
): GetCryptoCurrencyActionsUseCase {
return GetCryptoCurrencyActionsUseCase(rampStateManager, marketCryptoCurrencyRepository, dispatchers)
return GetCryptoCurrencyActionsUseCase(
rampManager = rampStateManager,
marketCryptoCurrencyRepository = marketCryptoCurrencyRepository,
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
dispatchers = dispatchers,
)
}
@Provides

View file

@ -1,6 +1,9 @@
package com.tangem.tap.di.domain
import com.squareup.moshi.Moshi
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.walletmanager.DefaultWalletManagersFacade
@ -21,7 +24,15 @@ internal object WalletManagersFacadeModule {
walletManagersStore: WalletManagersStore,
userWalletsStore: UserWalletsStore,
configManager: ConfigManager,
assetReader: AssetReader,
@SdkMoshi moshi: Moshi,
): WalletManagersFacade {
return DefaultWalletManagersFacade(walletManagersStore, userWalletsStore, configManager)
return DefaultWalletManagersFacade(
walletManagersStore = walletManagersStore,
userWalletsStore = userWalletsStore,
configManager = configManager,
assetReader = assetReader,
moshi = moshi,
)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.di.domain
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.repository.WalletsRepository
@ -58,8 +59,11 @@ internal object WalletsDomainModule {
@Provides
@ViewModelScoped
fun providesSelectWalletUseCase(walletsStateHolder: WalletsStateHolder): SelectWalletUseCase {
return SelectWalletUseCase(walletsStateHolder = walletsStateHolder)
fun providesSelectWalletUseCase(
walletsStateHolder: WalletsStateHolder,
reduxStateHolder: ReduxStateHolder,
): SelectWalletUseCase {
return SelectWalletUseCase(walletsStateHolder, reduxStateHolder)
}
@Provides

View file

@ -1,74 +0,0 @@
package com.tangem.tap.domain
import com.tangem.common.services.Result
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.domain.common.ThrottlerWithValues
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import java.math.BigDecimal
class RatesRepository(
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
) {
private val throttler = ThrottlerWithValues<Currency, Result<BigDecimal>?>(60000)
suspend fun loadFiatRate(currencyId: String, coinsList: List<Currency>): Result<RatesResult> =
withContext(dispatchers.io) {
// get and submit previous result of equivalents.
val throttledResult = coinsList.filter { throttler.isStillThrottled(it) }.map {
Pair(it, throttler.geValue(it))
}
val currenciesToUpdate = coinsList.filter { !throttler.isStillThrottled(it) }
val coinIds = currenciesToUpdate.mapNotNull { it.coinId }.distinct()
if (coinIds.isEmpty()) return@withContext handleFiatRatesResult(throttledResult.toMap())
runCatching { tangemTechApi.getRates(currencyId.lowercase(), coinIds.joinToString(",")) }
.onSuccess { response ->
val ratesResultList: Map<String, Result<BigDecimal>> = response.rates.mapValues {
Result.Success(it.value.toBigDecimal())
}
val updatedCurrencies = throttledResult.toMap().toMutableMap()
coinsList.forEach { currency ->
ratesResultList[currency.coinId]?.let {
updatedCurrencies[currency] = it
throttler.updateThrottlingTo(currency)
throttler.setValue(currency, it)
}
}
return@withContext handleFiatRatesResult(updatedCurrencies)
}
.onFailure { Result.Failure(it) }
error("Unreachable code because runCatching must return result")
}
private fun handleFiatRatesResult(rates: Map<Currency, Result<BigDecimal>?>): Result.Success<RatesResult> {
val success = mutableMapOf<Currency, BigDecimal>()
val failures = mutableMapOf<Currency, Throwable>()
rates.mapNotNull { (currency, priceResult) ->
when (priceResult) {
is Result.Success -> success[currency] = priceResult.data
is Result.Failure -> failures[currency] = priceResult.error
else -> null
}
}
return Result.Success(success to failures)
}
fun clear() {
throttler.clear()
}
}
typealias RatesResult = Pair<MutableMap<Currency, BigDecimal>, MutableMap<Currency, Throwable>>
val RatesResult.loadedRates
get() = this.first
val RatesResult.failedRates
get() = this.second

View file

@ -1,6 +1,7 @@
package com.tangem.tap.domain
import android.content.res.Resources
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.tangem.Message
import com.tangem.TangemSdk
@ -87,12 +88,13 @@ class TangemSdkManager(
suspend fun createProductWallet(scanResponse: ScanResponse): CompletionResult<CreateProductWalletTaskResponse> {
return runTaskAsync(
CreateProductWalletTask(
runnable = CreateProductWalletTask(
cardTypesResolver = scanResponse.cardTypesResolver,
derivationStyleProvider = scanResponse.derivationStyleProvider,
),
scanResponse.card.cardId,
Message(resources.getString(R.string.initial_message_create_wallet_body)),
cardId = scanResponse.card.cardId,
initialMessage = Message(resources.getString(R.string.initial_message_create_wallet_body)),
iconScanRes = if (scanResponse.cardTypesResolver.isRing()) R.drawable.img_hand_scan_ring else null,
)
}
@ -100,7 +102,7 @@ class TangemSdkManager(
scanResponse: ScanResponse,
mnemonic: String,
): CompletionResult<CreateProductWalletTaskResponse> {
val mnemonic = try {
val defaultMnemonic = try {
DefaultMnemonic(mnemonic, tangemSdk.wordlist)
} catch (e: TangemSdkError.MnemonicException) {
return CompletionResult.Failure(e)
@ -109,7 +111,7 @@ class TangemSdkManager(
CreateProductWalletTask(
scanResponse.cardTypesResolver,
derivationStyleProvider = scanResponse.derivationStyleProvider,
mnemonic,
defaultMnemonic,
),
scanResponse.card.cardId,
Message(resources.getString(R.string.initial_message_create_wallet_body)),
@ -221,9 +223,10 @@ class TangemSdkManager(
cardId: String? = null,
initialMessage: Message? = null,
accessCode: String? = null,
@DrawableRes iconScanRes: Int? = null,
): CompletionResult<T> = withContext(Dispatchers.Main) {
suspendCancellableCoroutine { continuation ->
tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage, accessCode) { result ->
tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage, accessCode, iconScanRes) { result ->
if (continuation.isActive) continuation.resume(result)
}
}

View file

@ -209,9 +209,7 @@ private class ScanWalletProcessor(
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
val isWallet2 = card.settings.isKeysImportAllowed || card.firmwareVersion >= FirmwareVersion.KeysImportAvailable
val productType = if (isWallet2) ProductType.Wallet2 else ProductType.Wallet
val productType = getWalletProductType(card)
val config = CardConfig.createConfig(card)
scope.launch {
val scanResponse = ScanResponse(
@ -239,6 +237,19 @@ private class ScanWalletProcessor(
}
}
private fun getWalletProductType(card: CardDTO): ProductType {
if (card.batchId == CardDTO.RING_BATCH_ID) {
return ProductType.Ring
}
return if (card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available &&
card.settings.isKeysImportAllowed
) {
ProductType.Wallet2
} else {
ProductType.Wallet
}
}
private suspend fun collectDerivations(
card: CardDTO,
config: CardConfig,

View file

@ -108,9 +108,9 @@ class UserTokensRepository(
.fold(
onSuccess = { response ->
response.getOrThrow()
.also { storageService.saveUserTokens(userWalletId, it) }
.tokens
.mapNotNull(Currency.Companion::fromTokenResponse)
.also { storageService.saveUserTokens(userWalletId, it.toUserTokensResponse()) }
.distinct()
},
onFailure = { handleGetUserTokensFailure(userWalletId = userWalletId, error = it) },

View file

@ -1,7 +1,6 @@
package com.tangem.tap.domain.userWalletList.implementation
import com.tangem.common.*
import com.tangem.common.extensions.guard
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
@ -16,6 +15,7 @@ import com.tangem.tap.domain.userWalletList.utils.toUserWallets
import com.tangem.tap.domain.userWalletList.utils.updateWith
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import timber.log.Timber
@OptIn(ExperimentalCoroutinesApi::class)
internal class BiometricUserWalletsListManager(
@ -56,12 +56,30 @@ internal class BiometricUserWalletsListManager(
override val walletsCount: Int
get() = state.value.userWallets.size
override suspend fun unlock(): CompletionResult<UserWallet> {
return unlockWithBiometryInternal().mapUnlockResult()
}
override suspend fun unlock(throwIfNotAllWalletsUnlocked: Boolean): CompletionResult<UserWallet> {
return unlockWithBiometryInternal()
.mapFailure { error ->
Timber.e(error, "Unable to unlock user wallets")
if (error is UserWalletsListError) {
error
} else {
UserWalletsListError.UnableToUnlockUserWallets(error)
}
}
.map {
if (throwIfNotAllWalletsUnlocked && state.value.userWallets.any(UserWallet::isLocked)) {
Timber.e("Not all user wallets have been unlocked")
throw UserWalletsListError.NotAllUserWalletsUnlocked
}
override suspend fun unlockAndSelect(selectedWalletId: UserWalletId): CompletionResult<UserWallet> {
return unlockWithBiometryInternal(selectedWalletId = selectedWalletId).mapUnlockResult()
val selectedUserWallet = selectedUserWalletSync
if (selectedUserWallet == null || selectedUserWallet.isLocked) {
Timber.e("Unable to find selected user wallet")
throw UserWalletsListError.NoUserWalletSelected
} else {
selectedUserWallet
}
}
}
override fun lock() {
@ -86,7 +104,7 @@ internal class BiometricUserWalletsListManager(
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
return if (canOverride) {
saveInternal(userWallet, changeSelectedUserWallet = true)
saveInternal(userWallet, changeSelectedUserWallet = true, canOverridePublicInfo = false)
} else {
val isWalletSaved = state.value.userWallets
.any {
@ -96,7 +114,7 @@ internal class BiometricUserWalletsListManager(
if (isWalletSaved) {
CompletionResult.Failure(UserWalletsListError.WalletAlreadySaved)
} else {
saveInternal(userWallet, changeSelectedUserWallet = true)
saveInternal(userWallet, changeSelectedUserWallet = true, canOverridePublicInfo = false)
}
}
}
@ -110,7 +128,7 @@ internal class BiometricUserWalletsListManager(
update(storedUserWallet)
}
.flatMap { updatedUserWallet ->
saveInternal(updatedUserWallet, changeSelectedUserWallet = false)
saveInternal(updatedUserWallet, changeSelectedUserWallet = false, canOverridePublicInfo = true)
}
.flatMap {
get(userWalletId)
@ -164,10 +182,11 @@ internal class BiometricUserWalletsListManager(
private suspend fun saveInternal(
userWallet: UserWallet,
changeSelectedUserWallet: Boolean,
canOverridePublicInfo: Boolean,
): CompletionResult<Unit> {
return saveEncryptionKeyIfNotNull(userWallet)
.flatMap { sensitiveInformationRepository.save(userWallet, encryptionKey = it) }
.flatMap { publicInformationRepository.save(userWallet) }
.flatMap { publicInformationRepository.save(userWallet, canOverridePublicInfo) }
.map {
if (changeSelectedUserWallet) {
selectedUserWalletRepository.set(userWallet.walletId)
@ -188,7 +207,7 @@ internal class BiometricUserWalletsListManager(
}
}
private suspend fun unlockWithBiometryInternal(selectedWalletId: UserWalletId? = null): CompletionResult<Unit> {
private suspend fun unlockWithBiometryInternal(): CompletionResult<Unit> {
return keysRepository.getAll()
.map { keys ->
state.update { prevState ->
@ -197,7 +216,7 @@ internal class BiometricUserWalletsListManager(
)
}
}
.flatMap { loadModels(selectedWalletId = selectedWalletId) }
.flatMap { loadModels() }
.map {
state.update { prevState ->
val hasLockedUserWallets = prevState.userWallets.any { it.isLocked }
@ -206,24 +225,6 @@ internal class BiometricUserWalletsListManager(
}
}
private fun CompletionResult<Unit>.mapUnlockResult(): CompletionResult<UserWallet> {
return this
.mapFailure { error ->
if (error is UserWalletsListError) {
error
} else {
UserWalletsListError.UnableToUnlockUserWallets(cause = error)
}
}
.map {
selectedUserWalletSync.guard {
throw UserWalletsListError.UnableToUnlockUserWallets(
cause = IllegalStateException("No user wallet selected"),
)
}
}
}
private suspend fun saveEncryptionKeyIfNotNull(userWallet: UserWallet): CompletionResult<ByteArray?> {
val encryptionKey = userWallet.scanResponse.card.encryptionKey
?.let { UserWalletEncryptionKey(userWallet.walletId, it) }
@ -245,7 +246,7 @@ internal class BiometricUserWalletsListManager(
}
}
private suspend fun loadModels(selectedWalletId: UserWalletId? = null): CompletionResult<Unit> {
private suspend fun loadModels(): CompletionResult<Unit> {
return getSavedUserWallets()
.map { userWallets ->
if (userWallets.isNotEmpty()) {
@ -255,7 +256,7 @@ internal class BiometricUserWalletsListManager(
prevState.copy(
userWallets = wallets,
selectedUserWalletId = findOrSetSelectedUserWalletId(
prevSelectedWalletId = selectedWalletId ?: prevState.selectedUserWalletId,
prevSelectedWalletId = prevState.selectedUserWalletId,
userWallets = wallets,
),
)
@ -316,7 +317,7 @@ internal class BiometricUserWalletsListManager(
private fun findSelectedUserWallet(userWallets: List<UserWallet> = state.value.userWallets): UserWallet? {
return userWallets.firstOrNull {
it.walletId == state.value.selectedUserWalletId && !it.isLocked
it.walletId == state.value.selectedUserWalletId
}
}

View file

@ -6,7 +6,7 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation
internal interface UserWalletsPublicInformationRepository {
suspend fun save(userWallet: UserWallet): CompletionResult<Unit>
suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit>
suspend fun getAll(): CompletionResult<List<UserWalletPublicInformation>>

View file

@ -38,7 +38,7 @@ internal class BiometricUserWalletsKeysRepository(
is TangemSdkError.AuthenticationPermanentLockout ->
UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = true)
is TangemSdkError.KeystoreInvalidated ->
UserWalletsListError.EncryptionKeyInvalidated
UserWalletsListError.AllKeysInvalidated
is TangemSdkError.AuthenticationUnavailable ->
UserWalletsListError.BiometricsAuthenticationDisabled
else -> error

View file

@ -24,18 +24,23 @@ internal class DefaultUserWalletsPublicInformationRepository(
Types.newParameterizedType(List::class.java, UserWalletPublicInformation::class.java),
)
override suspend fun save(userWallet: UserWallet): CompletionResult<Unit> {
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
return withContext(Dispatchers.IO) {
getAll()
.flatMap { savedInformation ->
val infoToSave = withContext(Dispatchers.Default) {
savedInformation.addOrReplace(userWallet.publicInformation) {
userWallet.walletId == it.walletId
}
getAll().flatMap { savedWalletsInformation ->
val infoToSave = if (canOverride) {
savedWalletsInformation.addOrReplace(userWallet.publicInformation) {
it.walletId == userWallet.walletId
}
} else {
if (savedWalletsInformation.none { it.walletId == userWallet.walletId }) {
savedWalletsInformation + userWallet.publicInformation
} else {
return@withContext CompletionResult.Success(Unit)
}
save(infoToSave)
}
save(infoToSave)
}
}
}

View file

@ -16,7 +16,6 @@ import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.BlockchainNetwork
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.common.analytics.events.AnalyticsParam
@ -31,12 +30,14 @@ import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction
import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.details.redux.walletconnect.*
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage.WCSignType.*
import timber.log.Timber
import java.math.BigDecimal
@ -49,7 +50,7 @@ class WalletConnectSdkHelper {
val blockchain = Blockchain.fromNetworkId(data.networkId) ?: return null
val walletManager = getWalletManager(blockchain, data.rawDerivationPath) ?: return null
walletManager.safeUpdate()
walletManager.safeUpdate(isDemoCard())
val wallet = walletManager.wallet
val balance = wallet.amounts[AmountType.Coin]?.value ?: return null
@ -108,13 +109,20 @@ class WalletConnectSdkHelper {
)
}
private fun getWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager? {
val blockchainNetwork = BlockchainNetwork(
fun isDemoCard(): Boolean {
val userWallet = userWalletsListManager.selectedUserWalletSync ?: return false
return userWallet.scanResponse.isDemoCard()
}
private suspend fun getWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager? {
val userWallet = userWalletsListManager.selectedUserWalletSync ?: return null
val walletManagerFacade = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
return walletManagerFacade.getOrCreateWalletManager(
userWalletId = userWallet.walletId,
blockchain = blockchain,
derivationPath = derivationPath,
tokens = emptyList(),
)
return store.state.walletState.getWalletManager(blockchainNetwork)
}
suspend fun completeTransaction(data: WcTransactionData, cardId: String?): String? {
@ -170,7 +178,12 @@ class WalletConnectSdkHelper {
SimpleResult.Success -> {
val sentFrom = AnalyticsParam.TxSentFrom.WalletConnect
Analytics.send(Basic.TransactionSent(sentFrom = sentFrom, memoType = MemoType.Null))
HEX_PREFIX + data.walletManager.wallet.recentTransactions.last().hash
val hash = data.walletManager.wallet.recentTransactions.last().hash
if (hash?.startsWith(HEX_PREFIX) == true) {
hash
} else {
HEX_PREFIX + hash
}
}
is SimpleResult.Failure -> {
Timber.e(result.error as BlockchainSdkError)
@ -194,12 +207,17 @@ class WalletConnectSdkHelper {
val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId)
return when (result) {
is CompletionResult.Success -> {
HEX_PREFIX + EthereumUtils.prepareTransactionToSend(
val hash = EthereumUtils.prepareTransactionToSend(
signature = result.data.signature,
transactionToSign = dataToSign,
walletPublicKey = data.walletManager.wallet.publicKey,
blockchain = data.walletManager.wallet.blockchain,
).toHexString()
if (hash.startsWith(HEX_PREFIX)) {
hash
} else {
HEX_PREFIX + hash
}
}
is CompletionResult.Failure -> {
Timber.e(result.error.customMessage)

View file

@ -1,23 +1,28 @@
package com.tangem.tap.domain.walletconnect2.app
import com.tangem.blockchain.common.*
import com.tangem.domain.common.extensions.*
import com.tangem.tap.domain.walletconnect2.domain.*
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.tap.domain.walletconnect2.domain.WcBlockchainHelper
class TangemWcBlockchainHelper : WcBlockchainHelper {
override fun chainIdToNetworkIdOrNull(chainId: String): String? {
val parsed = chainId.split(CHAIN_SEPARATOR)
if (parsed.size != 2) return null
when {
parsed.first() == EVM_NAMESPACE -> {
val chainIdInt = parsed[1].toIntOrNull() ?: return null
return Blockchain.fromChainId(chainIdInt)?.toNetworkId()
val parsedId = chainId.parseId() ?: return null
val blockchain = parsedId.chainIdToBlockchain()
return blockchain?.toNetworkId()
}
override fun chainIdToMissingNetworkNameOrNull(chainId: String): String? {
val parsedId = chainId.parseId() ?: return null
val blockchain = parsedId.chainIdToBlockchain()
return blockchain?.fullName
?: if (parsedId.first == EVM_NAMESPACE) {
chainId
} else {
parsedId.first.replaceFirstChar(Char::titlecase)
}
else -> {
val blockchain = Blockchain.fromNetworkId(parsed.first())
return if (supportedNonEvmBlockchains.contains(blockchain)) blockchain?.toNetworkId() else null
}
}
}
override fun networkIdToChainIdOrNull(networkId: String): String? {
@ -57,6 +62,26 @@ private fun Blockchain.getCaip2Namespace(): String? {
}
}
private fun String.parseId(): Pair<String, String>? {
val parsed = this.split(CHAIN_SEPARATOR)
if (parsed.size != 2) return null
return parsed[0] to parsed[1]
}
private fun Pair<String, String>.chainIdToBlockchain(): Blockchain? {
return when (first) {
EVM_NAMESPACE -> {
second.toIntOrNull()
?.let(Blockchain::fromChainId)
}
else -> {
Blockchain.fromNetworkId(networkId = first)
.takeIf(supportedNonEvmBlockchains::contains)
}
}
}
private val supportedNonEvmBlockchains = emptySet<Blockchain>() // TODO: add supported networks
private const val EVM_NAMESPACE = "eip155"

View file

@ -75,9 +75,11 @@ class WalletConnectInteractor(
is WalletConnectEvents.SessionApprovalError -> {
val error = when (wcEvent.error) {
is WalletConnectError.ApprovalErrorMissingNetworks -> {
val missingNetworks = wcEvent.error.missingChains
.map { blockchainHelper.chainIdToNetworkIdOrNull(it) }
WalletConnectError.ApprovalErrorAddNetwork(missingNetworks.filterNotNull())
val missingNetworks = wcEvent.error.missingChains.mapNotNull {
blockchainHelper.chainIdToMissingNetworkNameOrNull(it)
}
WalletConnectError.ApprovalErrorAddNetwork(missingNetworks)
}
else -> wcEvent.error
}

View file

@ -3,6 +3,8 @@ package com.tangem.tap.domain.walletconnect2.domain
interface WcBlockchainHelper {
fun chainIdToNetworkIdOrNull(chainId: String): String?
fun chainIdToMissingNetworkNameOrNull(chainId: String): String?
fun networkIdToChainIdOrNull(networkId: String): String?
fun getNamespaceFromFullChainIdOrNull(chainId: String): String?

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.customtoken.impl.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.core.TangemError
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
@ -15,5 +16,6 @@ interface CustomTokenInteractor {
suspend fun findToken(address: String, blockchain: Blockchain): FoundToken
/** Save token [customCurrency] */
@Throws(TangemError::class)
suspend fun saveToken(customCurrency: CustomCurrency)
}

View file

@ -4,6 +4,7 @@ import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.TangemError
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toMapKey
@ -16,6 +17,7 @@ import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
@ -47,6 +49,14 @@ class DefaultCustomTokenInteractor(
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
) : CustomTokenInteractor {
// TODO: Move to DI
private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) {
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val networksRepository = store.state.daggerGraphState.get(DaggerGraphState::networksRepository)
AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository)
}
override suspend fun findToken(address: String, blockchain: Blockchain): FoundToken {
return featureRepository.findToken(
address = address,
@ -60,8 +70,12 @@ class DefaultCustomTokenInteractor(
val currency = Currency.fromCustomCurrency(customCurrency)
val isNeedToDerive = isNeedToDerive(userWallet, currency)
if (isNeedToDerive) {
deriveMissingBlockchains(userWallet = userWallet, currencyList = listOf(currency)) {
submitAdd(userWallet = userWallet, currency = currency)
deriveMissingBlockchains(
userWallet = userWallet,
currencyList = listOf(currency),
onSuccess = { submitAdd(userWallet = userWallet.copy(scanResponse = it), currency = currency) },
) {
throw it
}
} else {
submitAdd(userWallet, currency)
@ -77,6 +91,7 @@ class DefaultCustomTokenInteractor(
userWallet: UserWallet,
currencyList: List<Currency>,
onSuccess: suspend (ScanResponse) -> Unit,
onFailure: suspend (TangemError) -> Unit,
) {
val scanResponse = userWallet.scanResponse
val config = CardConfig.createConfig(scanResponse.card)
@ -121,6 +136,7 @@ class DefaultCustomTokenInteractor(
onSuccess(updatedScanResponse)
}
is CompletionResult.Failure -> {
onFailure.invoke(result.error)
store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens"))
}
}
@ -219,21 +235,13 @@ class DefaultCustomTokenInteractor(
updatedScanResponse: ScanResponse,
currencyList: List<CryptoCurrency>,
) {
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val networksRepository = store.state.daggerGraphState.get(DaggerGraphState::networksRepository)
scope.launch {
userWalletsListManager.update(
userWalletId = userWalletId,
update = { it.copy(scanResponse = updatedScanResponse) },
)
currenciesRepository.addCurrencies(userWalletId = userWalletId, currencies = currencyList)
val networks = currencyList.map { it.network }.toSet()
networksRepository.getNetworkStatusesSync(
userWalletId = userWalletId,
networks = networks,
refresh = true,
)
addCryptoCurrenciesUseCase(userWalletId, currencyList)
}
}
}

View file

@ -55,7 +55,7 @@ internal object AddCustomTokenPreviewData {
value = "",
onValueChange = {},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
label = TextReference.Res(R.string.custom_token_token_symbol_input_title),
label = TextReference.Res(R.string.custom_token_token_symbol_input_title_old),
placeholder = TextReference.Res(id = R.string.custom_token_token_symbol_input_placeholder),
isEnabled = false,
),

View file

@ -270,7 +270,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
value = "",
onValueChange = actionsHandler::onTokenSymbolValueChange,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
label = TextReference.Res(R.string.custom_token_token_symbol_input_title),
label = TextReference.Res(R.string.custom_token_token_symbol_input_title_old),
placeholder = TextReference.Res(id = R.string.custom_token_token_symbol_input_placeholder),
isEnabled = false,
)
@ -602,7 +602,8 @@ internal class AddCustomTokenViewModel @Inject constructor(
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
val networkId = Blockchain.fromNetworkId(networkSelectorValue.toNetworkId())?.id
val savedTokenId = if (token.isCustom) null else token.id.value
// todo after move foundToken to CryptoCurrency model, use only id
val savedTokenId = if (token.isCustom) null else token.id.rawCurrencyId
val sameId = foundToken?.id == savedTokenId
val sameAddress = contractAddress == token.contractAddress
@ -804,7 +805,9 @@ internal class AddCustomTokenViewModel @Inject constructor(
networkSelectorField = uiState.form.networkSelectorField.copy(
selectedItem = selectedItem,
),
showTokenFields = selectedItem.blockchain.canHandleTokens(),
showTokenFields = selectedItem.blockchain.canHandleTokens() &&
// workaround cause in Terra we support only 1 token
selectedItem.blockchain != Blockchain.TerraV1,
),
)
onContactAddressValueChange(uiState.form.contractAddressInputField.value)
@ -898,7 +901,9 @@ internal class AddCustomTokenViewModel @Inject constructor(
viewModelScope.launch(dispatchers.io) {
runCatching { featureInteractor.saveToken(currency) }
.onSuccess { featureRouter.openWalletScreen() }
.onFailure(Timber::e)
.onFailure {
Timber.e(it)
}
}
}
}

View file

@ -13,7 +13,6 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
@ -280,7 +279,7 @@ class DetailsMiddleware {
val repository = store.state.daggerGraphState.get(DaggerGraphState::balanceHidingRepository)
scope.launch {
val newState = BalanceHidingSettings(
val newState = repository.getBalanceHidingSettings().copy(
isHidingEnabledInSettings = hideBalance,
isBalanceHidden = false,
)
@ -559,15 +558,10 @@ class DetailsMiddleware {
return userWalletsListManager.save(userWallet)
.doOnSuccess {
store.onUserWalletSelected(userWallet)
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success)
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet))
val walletFeatureToggles = store.state.daggerGraphState
.get(DaggerGraphState::walletFeatureToggles)
if (!walletFeatureToggles.isRedesignedScreenEnabled) {
store.onUserWalletSelected(userWallet)
}
}
}

View file

@ -93,6 +93,8 @@ private fun handlePrepareCardSettingsScreen(
cardTypesResolver: CardTypesResolver,
state: DetailsState,
): DetailsState {
val isTangemWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2()
val isShowPasswordResetRadioButton = isTangemWallet && card.backupStatus is CardDTO.BackupStatus.Active
val cardSettingsState = CardSettingsState(
cardInfo = card.toCardInfo(cardTypesResolver),
manageSecurityState = prepareSecurityOptions(card, cardTypesResolver),
@ -110,6 +112,7 @@ private fun handlePrepareCardSettingsScreen(
} else {
null
},
isShowPasswordResetRadioButton = isShowPasswordResetRadioButton,
)
return state.copy(cardSettingsState = cardSettingsState)
}
@ -152,22 +155,28 @@ private fun isResetToFactoryAllowedByCard(card: CardDTO, cardTypesResolver: Card
}
private fun handleEraseWallet(action: DetailsAction.ResetToFactory, state: DetailsState): DetailsState {
val cardSettingsState = state.cardSettingsState
return when (action) {
is DetailsAction.ResetToFactory.AcceptCondition1 -> {
val warning1Checked = action.accepted
val resetButtonEnabled = if (cardSettingsState?.isShowPasswordResetRadioButton == true) {
warning1Checked && cardSettingsState.condition2Checked
} else {
warning1Checked
}
state.copy(
cardSettingsState = state.cardSettingsState?.copy(
cardSettingsState = cardSettingsState?.copy(
condition1Checked = action.accepted,
resetButtonEnabled = warning1Checked && state.cardSettingsState.condition2Checked,
resetButtonEnabled = resetButtonEnabled,
),
)
}
is DetailsAction.ResetToFactory.AcceptCondition2 -> {
val warning2Checked = action.accepted
state.copy(
cardSettingsState = state.cardSettingsState?.copy(
cardSettingsState = cardSettingsState?.copy(
condition2Checked = action.accepted,
resetButtonEnabled = warning2Checked && state.cardSettingsState.condition1Checked,
resetButtonEnabled = warning2Checked && cardSettingsState.condition1Checked,
),
)
}

View file

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

View file

@ -8,7 +8,6 @@ import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.BlockchainNetwork
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
@ -324,10 +323,9 @@ class WalletConnectMiddleware {
is WalletConnectAction.SessionRejected -> {
when (action.error) {
is WalletConnectError.ApprovalErrorAddNetwork -> {
val blockchains = action.error.networks.mapNotNull { Blockchain.fromNetworkId(it)?.fullName }
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.AddNetwork(blockchains.toString()),
WalletConnectDialog.UnsupportedNetwork(action.error.networks),
),
)
}

View file

@ -5,9 +5,9 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.core.navigation.StateDialog
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumSignMessage
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents

View file

@ -6,16 +6,13 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.userwallets.Artwork
import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@ -32,7 +29,7 @@ internal fun CardSettingsScreen(
modifier = modifier,
content = {
if (needReadCard) {
CardSettingsReadCard(state.onScanCardClick, state.cardImage)
CardSettingsReadCard(state.onScanCardClick)
} else {
CardSettings(state = state)
}
@ -44,39 +41,39 @@ internal fun CardSettingsScreen(
@Suppress("MagicNumber")
@Composable
private fun CardSettingsReadCard(onScanCardClick: () -> Unit, cardArtwork: Artwork?) {
private fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(top = TangemTheme.dimens.size70),
modifier = Modifier.fillMaxSize(),
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(start = TangemTheme.dimens.size16, end = TangemTheme.dimens.size16),
.padding(bottom = TangemTheme.dimens.spacing40),
) {
val circleColor = TangemTheme.colors.stroke.primary
Canvas(
Image(
modifier = Modifier
.size(300.dp)
.align(Alignment.Center),
) {
drawCircle(
color = circleColor,
radius = size.minDimension / 2.0f,
)
}
SubcomposeAsyncImage(
model = ImageRequest.Builder(context = LocalContext.current)
.data(cardArtwork?.artworkId)
.crossfade(enable = true)
.build(),
modifier = Modifier
.align(Alignment.Center)
.fillMaxWidth(),
loading = { /* no-op */ },
error = { /* no-op */ },
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing80,
end = TangemTheme.dimens.spacing80,
top = TangemTheme.dimens.spacing70,
)
.rotate(-15f),
painter = painterResource(id = R.drawable.card_placeholder_secondary),
contentDescription = null,
contentScale = ContentScale.FillWidth,
)
Image(
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing60,
end = TangemTheme.dimens.spacing60,
)
.rotate(-1f),
painter = painterResource(id = R.drawable.card_placeholder_black),
contentDescription = null,
contentScale = ContentScale.FillWidth,
)
}
Spacer(modifier = Modifier.weight(1f))
@ -84,9 +81,9 @@ private fun CardSettingsReadCard(onScanCardClick: () -> Unit, cardArtwork: Artwo
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.size16,
end = TangemTheme.dimens.size16,
bottom = TangemTheme.dimens.size32,
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing32,
),
) {
Text(
@ -103,7 +100,7 @@ private fun CardSettingsReadCard(onScanCardClick: () -> Unit, cardArtwork: Artwo
.verticalScroll(rememberScrollState())
.weight(weight = 1f, fill = false),
)
Spacer(modifier = Modifier.size(TangemTheme.dimens.size28))
Spacer(modifier = Modifier.size(TangemTheme.dimens.size32))
DetailsMainButton(
title = stringResource(id = R.string.scan_card_settings_button),
onClick = onScanCardClick,
@ -122,21 +119,21 @@ private fun CardSettings(state: CardSettingsScreenState) {
) {
items(state.cardDetails) {
val paddingBottom = when (it) {
is CardInfo.CardId, is CardInfo.Issuer -> 12.dp
is CardInfo.SignedHashes -> 14.dp
is CardInfo.SecurityMode -> 16.dp
is CardInfo.ChangeAccessCode -> 16.dp
is CardInfo.AccessCodeRecovery -> 16.dp
is CardInfo.ResetToFactorySettings -> 28.dp
is CardInfo.CardId, is CardInfo.Issuer -> TangemTheme.dimens.spacing12
is CardInfo.SignedHashes -> TangemTheme.dimens.spacing14
is CardInfo.SecurityMode -> TangemTheme.dimens.spacing16
is CardInfo.ChangeAccessCode -> TangemTheme.dimens.spacing16
is CardInfo.AccessCodeRecovery -> TangemTheme.dimens.spacing16
is CardInfo.ResetToFactorySettings -> TangemTheme.dimens.spacing28
}
val paddingTop = when (it) {
is CardInfo.CardId -> 0.dp
is CardInfo.Issuer -> 12.dp
is CardInfo.SignedHashes -> 12.dp
is CardInfo.SecurityMode -> 14.dp
is CardInfo.ChangeAccessCode -> 16.dp
is CardInfo.AccessCodeRecovery -> 16.dp
is CardInfo.ResetToFactorySettings -> 16.dp
is CardInfo.CardId -> TangemTheme.dimens.spacing0
is CardInfo.Issuer -> TangemTheme.dimens.spacing12
is CardInfo.SignedHashes -> TangemTheme.dimens.spacing12
is CardInfo.SecurityMode -> TangemTheme.dimens.spacing14
is CardInfo.ChangeAccessCode -> TangemTheme.dimens.spacing16
is CardInfo.AccessCodeRecovery -> TangemTheme.dimens.spacing16
is CardInfo.ResetToFactorySettings -> TangemTheme.dimens.spacing16
}
Column(
modifier = Modifier
@ -145,7 +142,12 @@ private fun CardSettings(state: CardSettingsScreenState) {
enabled = it.clickable,
onClick = { state.onElementClick(it) },
)
.padding(start = 20.dp, end = 20.dp, bottom = paddingBottom, top = paddingTop),
.padding(
start = TangemTheme.dimens.spacing20,
end = TangemTheme.dimens.spacing20,
bottom = paddingBottom,
top = paddingTop,
),
) {
val titleColor = if (it.clickable) {
TangemTheme.colors.text.primary1
@ -162,7 +164,7 @@ private fun CardSettings(state: CardSettingsScreenState) {
color = titleColor,
style = TangemTheme.typography.subtitle1,
)
Spacer(modifier = Modifier.size(4.dp))
Spacer(modifier = Modifier.size(TangemTheme.dimens.size4))
Text(
text = it.subtitle.resolveReference(),
color = subtitleColor,

View file

@ -1,48 +1,35 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import androidx.compose.ui.Modifier
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import com.tangem.wallet.R
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
class AccessCodeRecoveryFragment : Fragment(), StoreSubscriber<DetailsState> {
@AndroidEntryPoint
class AccessCodeRecoveryFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
private val viewModel = AccessCodeRecoveryViewModel(store)
private var screenState: MutableState<AccessCodeRecoveryScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.accessCodeRecovery))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.fade)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
TangemTheme {
AccessCodeRecoveryScreen(
state = screenState.value,
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
)
}
}
}
@Composable
override fun ScreenContent(modifier: Modifier) {
AccessCodeRecoveryScreen(
state = screenState.value,
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
)
}
override fun onStart() {

View file

@ -5,7 +5,6 @@ import androidx.compose.runtime.Immutable
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.wallet.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -47,7 +46,7 @@ internal sealed class SettingsItem(
override val onClick: () -> Unit,
) : SettingsItem(
iconResId = R.drawable.ic_plus_24,
title = stringReference(value = "Add new wallet"),
title = resourceReference(R.string.user_wallet_list_add_button),
)
data class ScanWallet(
@ -55,7 +54,7 @@ internal sealed class SettingsItem(
override val onClick: () -> Unit,
) : SettingsItem(
iconResId = R.drawable.ic_plus_24,
title = stringReference(value = "Scan new wallet"),
title = resourceReference(R.string.scan_card_settings_button),
)
data class LinkMoreCards(

View file

@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.feedback.FeedbackEmail
import com.tangem.tap.common.feedback.SupportInfo
@ -128,51 +129,51 @@ internal class DetailsViewModel(
}
private fun navigateToToS() {
store.dispatch(DisclaimerAction.Show(AppScreen.Details))
store.dispatchOnMain(DisclaimerAction.Show(AppScreen.Details))
}
private fun navigateToReferralProgram() {
store.dispatch(NavigationAction.NavigateTo(AppScreen.ReferralProgram))
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.ReferralProgram))
}
private fun sendFeedback() {
Analytics.send(Settings.ButtonSendFeedback())
store.dispatch(GlobalAction.SendEmail(FeedbackEmail()))
store.dispatchOnMain(GlobalAction.SendEmail(FeedbackEmail()))
}
private fun navigateToChat() {
Analytics.send(Settings.ButtonChat())
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
store.dispatchOnMain(GlobalAction.OpenChat(SupportInfo()))
}
private fun navigateToAppSettings() {
Analytics.send(Settings.ButtonAppSettings())
store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings))
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.AppSettings))
}
private fun navigateToCardSettings() {
Analytics.send(Settings.ButtonCardSettings())
store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings))
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.CardSettings))
}
private fun linkMoreCards() {
Analytics.send(Settings.ButtonCreateBackup())
store.dispatch(WalletAction.MultiWallet.BackupWallet)
store.dispatchOnMain(WalletAction.MultiWallet.BackupWallet)
}
private fun scanAndSaveUserWallet() {
Analytics.send(Settings.ScanNewCard)
store.dispatch(DetailsAction.ScanAndSaveUserWallet)
store.dispatchOnMain(DetailsAction.ScanAndSaveUserWallet)
}
private fun navigateToWalletConnect() {
Analytics.send(Settings.ButtonWalletConnect())
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
}
private fun handleSocialNetworkClick(link: SocialNetworkLink) {
Analytics.send(Settings.ButtonSocialNetwork(link.network))
store.dispatch(NavigationAction.OpenUrl(link.url))
store.dispatchOnMain(NavigationAction.OpenUrl(link.url))
}
private fun getSocialLinks(): ImmutableList<SocialNetworkLink> {

View file

@ -22,7 +22,7 @@ internal class ResetCardFragment : ComposeFragment(), StoreSubscriber<DetailsSta
private val viewModel = ResetCardViewModel(store)
private var screenState: MutableState<ResetCardScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState))
mutableStateOf(ResetCardScreenState.InitialState)
@Composable
override fun ScreenContent(modifier: Modifier) {

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.details.ui.resetcard
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -30,15 +29,23 @@ import com.tangem.wallet.R
internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
SettingsScreensScaffold(
modifier = modifier,
content = { ResetCardView(state = state) },
content = {
when (state) {
is ResetCardScreenState.ResetCardScreenContent -> {
ResetCardView(state = state)
}
ResetCardScreenState.InitialState -> {
// do nothing for now, just white screen
}
}
},
onBackClick = onBackClick,
)
}
@OptIn(ExperimentalAnimationApi::class)
@Suppress("LongMethod", "MagicNumber")
@Composable
private fun ResetCardView(state: ResetCardScreenState) {
private fun ResetCardView(state: ResetCardScreenState.ResetCardScreenContent) {
Column(
modifier = Modifier
.fillMaxSize()
@ -80,17 +87,24 @@ private fun ResetCardView(state: ResetCardScreenState) {
Spacer(modifier = Modifier.size(28.dp))
ConditionCheckBox(
checkedState = state.acceptCondition1Checked,
onCheckedChange = state.onAcceptCondition1ToggleClick,
description = TextReference.Res(R.string.reset_card_to_factory_condition_1),
)
ConditionCheckBox(
checkedState = state.acceptCondition2Checked,
onCheckedChange = state.onAcceptCondition2ToggleClick,
description = TextReference.Res(R.string.reset_card_to_factory_condition_2),
)
state.warningsToShow.forEach {
when (it) {
ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS -> {
ConditionCheckBox(
checkedState = state.acceptCondition1Checked,
onCheckedChange = state.onAcceptCondition1ToggleClick,
description = TextReference.Res(R.string.reset_card_to_factory_condition_1),
)
}
ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE -> {
ConditionCheckBox(
checkedState = state.acceptCondition2Checked,
onCheckedChange = state.onAcceptCondition2ToggleClick,
description = TextReference.Res(R.string.reset_card_to_factory_condition_2),
)
}
}
}
Spacer(modifier = Modifier.size(16.dp))
Box(
@ -111,7 +125,6 @@ private fun ResetCardView(state: ResetCardScreenState) {
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun ConditionCheckBox(checkedState: Boolean, onCheckedChange: (Boolean) -> Unit, description: TextReference) {
Row(
@ -162,8 +175,9 @@ private fun ResetCardScreenSample(modifier: Modifier = Modifier) {
.background(TangemTheme.colors.background.secondary),
) {
ResetCardScreen(
state = ResetCardScreenState(
state = ResetCardScreenState.ResetCardScreenContent(
accepted = true,
warningsToShow = listOf(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS),
descriptionText = TextReference.Res(R.string.reset_card_with_backup_to_factory_message),
onAcceptCondition1ToggleClick = {},
onAcceptCondition2ToggleClick = {},

View file

@ -2,15 +2,25 @@ package com.tangem.tap.features.details.ui.resetcard
import com.tangem.tap.features.details.ui.cardsettings.TextReference
internal data class ResetCardScreenState(
val accepted: Boolean = false,
val descriptionText: TextReference,
val acceptCondition1Checked: Boolean = false,
val acceptCondition2Checked: Boolean = false,
val onAcceptCondition1ToggleClick: (Boolean) -> Unit,
val onAcceptCondition2ToggleClick: (Boolean) -> Unit,
val onResetButtonClick: () -> Unit,
) {
val resetButtonEnabled: Boolean
get() = accepted
internal sealed class ResetCardScreenState {
object InitialState : ResetCardScreenState()
data class ResetCardScreenContent(
val accepted: Boolean = false,
val descriptionText: TextReference,
val warningsToShow: List<WarningsToReset>,
val acceptCondition1Checked: Boolean = false,
val acceptCondition2Checked: Boolean = false,
val onAcceptCondition1ToggleClick: (Boolean) -> Unit,
val onAcceptCondition2ToggleClick: (Boolean) -> Unit,
val onResetButtonClick: () -> Unit,
) : ResetCardScreenState() {
val resetButtonEnabled: Boolean
get() = accepted
}
internal enum class WarningsToReset {
LOST_WALLET_ACCESS, LOST_PASSWORD_RESTORE
}
}

View file

@ -9,14 +9,23 @@ import org.rekotlin.Store
internal class ResetCardViewModel(private val store: Store<AppState>) {
fun updateState(state: CardSettingsState?): ResetCardScreenState {
fun updateState(state: CardSettingsState?): ResetCardScreenState.ResetCardScreenContent {
val descriptionText = state?.cardInfo
?.toResetCardDescriptionText()
?: TextReference.Str(value = "")
return ResetCardScreenState(
val warningsToShow = buildList {
add(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS)
if (state?.isShowPasswordResetRadioButton == true) {
add(ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE)
}
}
return ResetCardScreenState.ResetCardScreenContent(
accepted = state?.resetButtonEnabled ?: false,
descriptionText = descriptionText,
warningsToShow = warningsToShow,
acceptCondition1Checked = state?.condition1Checked ?: false,
acceptCondition2Checked = state?.condition2Checked ?: false,
onAcceptCondition1ToggleClick = { store.dispatch(DetailsAction.ResetToFactory.AcceptCondition1(it)) },

View file

@ -2,26 +2,33 @@ package com.tangem.tap.features.details.ui.walletconnect
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.*
import androidx.activity.OnBackPressedCallback
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import com.google.zxing.BarcodeFormat
import com.google.zxing.Result
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.common.util.concurrent.ListenableFuture
import com.otaliastudios.cameraview.CameraView
import com.tangem.core.navigation.NavigationAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PreviewBinder
import com.tangem.tap.store
import me.dm7.barcodescanner.zxing.ZXingScannerView
import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutQrScanningBinding
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
internal class QrScanFragment : Fragment(R.layout.layout_qr_scanning) {
private var scannerView: ZXingScannerView? = null
private val binding: LayoutQrScanningBinding by viewBinding(LayoutQrScanningBinding::bind)
private val binder = PreviewBinder()
private var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>? = null
private var cameraExecutor: ExecutorService? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -36,25 +43,38 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (!permissionIsGranted()) requestPermission()
scannerView = ZXingScannerView(activity).apply {
setFormats(listOf(BarcodeFormat.QR_CODE))
cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
cameraExecutor = Executors.newSingleThreadExecutor()
cameraProviderFuture?.addListener(
{
val cameraProvider = cameraProviderFuture?.get()
binder.bindPreview(
context = requireContext(),
binding = binding,
lifecycleOwner = this,
cameraProvider = requireNotNull(cameraProvider),
cameraExecutor = requireNotNull(cameraExecutor),
onScanned = { result ->
store.dispatch(NavigationAction.PopBackTo())
setFitSystemWindows(fit = false)
if (result.isNotBlank()) {
store.dispatch(WalletConnectAction.OpenSession(result))
}
},
)
},
ContextCompat.getMainExecutor(requireContext()),
)
binding.overlay.post {
binding.overlay.setViewFinder()
}
return scannerView
}
override fun onResume() {
super.onResume()
scannerView?.setResultHandler(this)
scannerView?.startCamera()
}
override fun onPause() {
super.onPause()
scannerView?.stopCamera()
}
override fun onDestroy() {
@ -62,14 +82,6 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
setFitSystemWindows(fit = false)
}
override fun handleResult(result: Result) {
store.dispatch(NavigationAction.PopBackTo())
setFitSystemWindows(fit = false)
if (!result.text.isNullOrBlank()) {
store.dispatch(WalletConnectAction.OpenSession(result.text))
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (requestCode != CameraView.PERMISSION_REQUEST_CODE) return
@ -80,13 +92,8 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
}
private fun permissionIsGranted(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val cameraPermission =
ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)
cameraPermission == PackageManager.PERMISSION_GRANTED
} else {
true
}
val cameraPermission = ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)
return cameraPermission == PackageManager.PERMISSION_GRANTED
}
private fun requestPermission() {

View file

@ -0,0 +1,69 @@
package com.tangem.tap.features.details.ui.walletconnect.dialogs
import android.content.Context
import android.util.Size
import android.view.OrientationEventListener
import android.view.Surface
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.lifecycle.LifecycleOwner
import com.tangem.tap.common.qrCodeScan.MLKitBarcodeAnalyzer
import com.tangem.wallet.databinding.LayoutQrScanningBinding
import java.util.concurrent.ExecutorService
internal class PreviewBinder {
@Suppress("LongParameterList")
fun bindPreview(
context: Context,
binding: LayoutQrScanningBinding,
lifecycleOwner: LifecycleOwner,
cameraProvider: ProcessCameraProvider,
cameraExecutor: ExecutorService,
onScanned: (String) -> Unit,
) {
cameraProvider.unbindAll()
val preview: Preview = Preview.Builder()
.build()
val imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(Size(binding.cameraPreview.width, binding.cameraPreview.height))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
val orientationEventListener = object : OrientationEventListener(context) {
@Suppress("MagicNumber")
override fun onOrientationChanged(orientation: Int) {
val rotation: Int = when (orientation) {
in 45..134 -> Surface.ROTATION_270
in 135..224 -> Surface.ROTATION_180
in 225..314 -> Surface.ROTATION_90
else -> Surface.ROTATION_0
}
imageAnalysis.targetRotation = rotation
}
}
orientationEventListener.enable()
val analyzer: ImageAnalysis.Analyzer = MLKitBarcodeAnalyzer {
imageAnalysis.clearAnalyzer()
onScanned.invoke(it)
}
cameraExecutor.let {
imageAnalysis.setAnalyzer(it, analyzer)
}
preview.setSurfaceProvider(binding.cameraPreview.surfaceProvider)
val cameraSelector: CameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, imageAnalysis, preview)
}
}

View file

@ -27,6 +27,5 @@ private fun internalReduce(action: Action, state: AppState): DisclaimerState {
is DisclaimerAction.OnProgressStateChanged -> disclaimerState.copy(
progressState = action.state,
)
else -> disclaimerState
}
}

View file

@ -1,36 +1,36 @@
package com.tangem.tap.features.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.domain.tokens.TokensAction
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.home.compose.StoriesScreen
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
@ -42,38 +42,28 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
store.dispatch(HomeAction.Init)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@Composable
override fun ScreenContent(modifier: Modifier) {
// sync adding story before screen creation
// if (learn2earnViewModel.uiState.storyScreenState.isVisible) {
// store.dispatch(HomeAction.InsertStory(position = 0, Stories.OneInchPromo))
// homeState.value = store.state.homeState
// }
return ComposeView(inflater.context).apply {
setContent {
TangemTheme {
BackHandler {
requireActivity().finish()
}
SystemBarsEffect {
setSystemBarsColor(
color = Color.Transparent,
darkIcons = false,
)
}
ScreenContent()
}
}
BackHandler(onBack = requireActivity()::finish)
SystemBarsEffect {
setSystemBarsColor(color = Color.Transparent, darkIcons = false)
}
ScreenContent()
}
override fun onStart() {
super.onStart()
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, false) }
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.homeState == newState.homeState
}.select { it.homeState }
store.subscribe(subscriber = this) { state ->
state
.skipRepeats { oldState, newState -> oldState.homeState == newState.homeState }
.select(AppState::homeState)
}
}
@ -82,11 +72,6 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
store.unsubscribe(this)
}
override fun onDestroyView() {
super.onDestroyView()
rollbackStatusBarIconsColor()
}
override fun newState(state: HomeState) {
if (activity == null || view == null) return
@ -101,11 +86,7 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
onLearn2earnClick = {}, // learn2earnViewModel.uiState.storyScreenState.onClick,
onScanButtonClick = {
Analytics.send(IntroductionProcess.ButtonScanCard())
lifecycleScope.launch {
store.dispatch(
HomeAction.ReadCard(scope = this),
)
}
store.dispatch(action = HomeAction.ReadCard(scope = requireActivity().lifecycleScope))
},
onShopButtonClick = {
Analytics.send(IntroductionProcess.ButtonBuyCards())
@ -118,17 +99,4 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
},
)
}
/*
* !!! Workaround !!!
* Used to roll back the color of icons in the system bars after the stories screen
*/
private fun rollbackStatusBarIconsColor() {
val windowInsetsController = WindowInsetsControllerCompat(
activity?.window ?: return,
view ?: return,
)
windowInsetsController.isAppearanceLightStatusBars = true
windowInsetsController.isAppearanceLightNavigationBars = true
}
}

View file

@ -96,6 +96,8 @@ fun StoriesTextAnimation(
@Composable
fun StoriesBottomImageAnimation(
initialScale: Float = 2.5f,
secondStageScale: Float = SCALE_SWITCH_BARRIER,
targetScale: Float = 1.0f,
firstStepDuration: Int,
totalDuration: Int,
content: @Composable (Modifier) -> Unit,
@ -117,7 +119,7 @@ fun StoriesBottomImageAnimation(
)
},
label = "Appearing scale",
) { value -> if (value) SCALE_SWITCH_BARRIER else initialScale }
) { value -> if (value) secondStageScale else initialScale }
val secondTransition = updateTransition(
targetState = isSecondStepLaunched.value,
@ -131,14 +133,14 @@ fun StoriesBottomImageAnimation(
)
},
label = "Outgoing scale",
) { value -> if (value) 1f else SCALE_SWITCH_BARRIER }
) { value -> if (value) targetScale else secondStageScale }
val fadeIn = firstTransition.animateFloat(
transitionSpec = { tween(durationMillis = 400) },
label = "Fade in on start",
) { value -> if (value) 1f else 0f }
if (firstScaleStep.value == SCALE_SWITCH_BARRIER) {
if (firstScaleStep.value == secondStageScale) {
isSecondStepLaunched.value = true
}

View file

@ -105,6 +105,9 @@ fun StoriesWalletForEveryone(stepDuration: Int) {
SpacerH32()
BoxWithGradient {
StoriesBottomImageAnimation(
initialScale = 2.6f,
secondStageScale = 1.2f,
targetScale = 1.1f,
totalDuration = stepDuration,
firstStepDuration = 500,
) { modifier ->
@ -184,8 +187,8 @@ private fun StoriesImage(@DrawableRes drawableResId: Int, modifier: Modifier = M
Image(
painter = painterResource(id = drawableResId),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = modifier.fillMaxWidth(),
contentScale = ContentScale.Inside,
modifier = modifier.fillMaxSize(),
)
}

View file

@ -4,8 +4,8 @@ import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@ -54,34 +54,30 @@ fun FirstStoriesContent(isPaused: Boolean, duration: Int) {
textAlign = TextAlign.Center,
)
Box {
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SpacerH(TangemTheme.dimens.spacing94)
StoriesTextAnimation(
slideInDuration = 500,
slideInDelay = 150,
) { modifier ->
Text(
modifier = modifier
.weight(1f),
text = stringResource(R.string.story_meet_title),
style = style,
color = TangemColorPalette.White,
textAlign = TextAlign.Center,
)
}
SpacerH(TangemTheme.dimens.spacing46)
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SpacerH(TangemTheme.dimens.spacing94)
StoriesTextAnimation(
slideInDuration = 500,
slideInDelay = 150,
) { modifier ->
Text(
modifier = modifier,
text = stringResource(R.string.story_meet_title),
style = style,
color = TangemColorPalette.White,
textAlign = TextAlign.Center,
)
}
SpacerH(TangemTheme.dimens.spacing46)
Image(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth(),
painter = painterResource(R.drawable.img_meet_tangem),
contentScale = ContentScale.FillWidth,
contentScale = ContentScale.Inside,
contentDescription = "Tangem Wallet card",
)
}

View file

@ -36,24 +36,26 @@ fun StoriesProgressBar(
val animatorSpeed = Settings.Global.getFloat(
context.contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE,
0f,
1f,
)
LaunchedEffect(paused, currentStep) {
LaunchedEffect(paused, currentStep, animatorSpeed) {
if (paused) {
progress.stop()
} else {
if (animatorSpeed == 0f) {
progress.snapTo(1f)
delay(STORIES_ANIMATION_SPEED_ZERO_DURATION)
} else {
progress.animateTo(
targetValue = 1f,
animationSpec = tween(
durationMillis = (stepDuration * (1f - progress.value)).toInt(),
easing = LinearEasing,
),
)
progress.snapTo(0f)
}
progress.animateTo(
targetValue = 1f,
animationSpec = tween(
durationMillis = (stepDuration * (1f - progress.value)).toInt(),
easing = LinearEasing,
),
)
progress.snapTo(0f)
onStepFinish()
}
}

View file

@ -17,9 +17,8 @@ sealed class HomeAction : Action {
/**
* Action for scanning card
*
* @property analyticsEvent analytics event
* @property lifecycleCoroutineScope lifecycle scope. It will be canceled when lifecycle-aware component is
* destroyed.
* @property analyticsEvent analytics event
* @property scope lifecycle scope. It will be canceled when lifecycle-aware component is destroyed
*/
data class ReadCard(
val analyticsEvent: AnalyticsEvent? = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Introduction),

View file

@ -3,7 +3,7 @@ package com.tangem.tap.features.intentHandler.handlers
import android.content.Intent
import android.net.Uri
import com.tangem.core.analytics.Analytics
import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenScreenEvent
import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
@ -21,7 +21,7 @@ class BuyCurrencyIntentHandler : IntentHandler {
val successUri = Uri.parse(ExchangeUrlBuilder.SUCCESS_URL)
return if (data.host == successUri.host && data.authority == successUri.authority) {
val currencyType = AnalyticsParam.CurrencyType.Currency(currency)
Analytics.send(TokenScreenEvent.Bought(currencyType.value))
Analytics.send(TokenScreenAnalyticsEvent.Bought(currencyType.value))
true
} else {
false

View file

@ -0,0 +1,12 @@
package com.tangem.tap.features.main
internal interface MainIntents {
fun onHiddenBalanceToastAction()
fun onShownBalanceToastAction()
fun onHiddenBalanceNotificationAction(isPermanent: Boolean)
fun onDismissBottomSheet()
}

View file

@ -0,0 +1,62 @@
package com.tangem.tap.features.main
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.tap.features.main.model.MainScreenState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
internal class MainScreenStateHolder(private val intents: MainIntents) {
private val notificationsFactory = NotificationsFactory(intents)
private val stateFlowInternal: MutableStateFlow<MainScreenState> = MutableStateFlow(getInitialState())
val stateFlow: StateFlow<MainScreenState> = stateFlowInternal
fun updateWithHiddenBalancesToast(isBalanceHidden: Boolean) {
stateFlowInternal.update { state ->
state.copy(
toast = triggeredEvent(
data = if (isBalanceHidden) {
notificationsFactory.createBalancesAreHiddenToast()
} else {
notificationsFactory.createBalancesAreShownToast()
},
onConsume = ::consumeToastEvent,
),
)
}
}
fun updateWithHiddenBalancesNotification() {
stateFlowInternal.update { state ->
state.copy(
modalNotification = TangemBottomSheetConfig(
isShow = true,
onDismissRequest = intents::onDismissBottomSheet,
content = notificationsFactory.createBalancesAreHiddenModalNotification(),
),
)
}
}
fun updateWithoutModalNotification() {
stateFlowInternal.update { state ->
state.copy(modalNotification = state.modalNotification?.copy(isShow = false))
}
}
private fun consumeToastEvent() {
stateFlowInternal.update { state ->
state.copy(toast = consumedEvent())
}
}
private fun getInitialState() = MainScreenState(
toast = consumedEvent(),
modalNotification = null,
)
}

View file

@ -0,0 +1,127 @@
package com.tangem.tap.features.main
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.ReduxNavController
import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.tap.features.main.model.MainScreenState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
internal class MainViewModel @Inject constructor(
private val updateBalanceHidingSettingsUseCase: UpdateBalanceHidingSettingsUseCase,
private val listenToFlipsUseCase: ListenToFlipsUseCase,
private val reduxNavController: ReduxNavController,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel(), MainIntents {
private val stateHolder = MainScreenStateHolder(
intents = this,
)
private val balanceHidingSettingsFlow: SharedFlow<BalanceHidingSettings> = getBalanceHidingSettingsUseCase()
.shareIn(viewModelScope, SharingStarted.WhileSubscribed())
val state: StateFlow<MainScreenState> = stateHolder.stateFlow
init {
observeFlips()
displayBalancesHidingStatusToast()
displayHiddenBalancesModalNotification()
}
private fun observeFlips() {
listenToFlipsUseCase().launchIn(viewModelScope)
}
private fun displayHiddenBalancesModalNotification() {
balanceHidingSettingsFlow
.drop(count = 1) // Skip initial emit
.distinctUntilChanged()
.filter {
it.isBalanceHidingNotificationEnabled && it.isBalanceHidden
}
.onEach {
if (state.value.modalNotification?.isShow != true) {
stateHolder.updateWithHiddenBalancesNotification()
reduxNavController.navigate(NavigationAction.NavigateTo(AppScreen.ModalNotification))
}
}
.launchIn(viewModelScope)
}
private fun displayBalancesHidingStatusToast() {
var previousSettings: BalanceHidingSettings? = null
balanceHidingSettingsFlow
.onEach { settings ->
if (previousSettings == null) {
previousSettings = settings
return@onEach
}
/*
* Skip if the feature to hide balances has just been
* enabled or disabled in the settings, or if the hide balances status has not been changed
* */
if (settings.isHidingEnabledInSettings != previousSettings?.isHidingEnabledInSettings ||
settings.isBalanceHidden == previousSettings?.isBalanceHidden
) {
previousSettings = settings
return@onEach
}
displayBalancesHiddenStatusToast(settings)
previousSettings = settings
}
.launchIn(viewModelScope)
}
private fun displayBalancesHiddenStatusToast(settings: BalanceHidingSettings) {
// If modal notification is enabled and balances are hidden, the toast will not show
if (!settings.isBalanceHidingNotificationEnabled || !settings.isBalanceHidden) {
stateHolder.updateWithHiddenBalancesToast(settings.isBalanceHidden)
}
}
override fun onHiddenBalanceToastAction() {
viewModelScope.launch {
updateBalanceHidingSettingsUseCase.invoke {
copy(isBalanceHidden = false)
}
}
}
override fun onShownBalanceToastAction() {
viewModelScope.launch {
updateBalanceHidingSettingsUseCase.invoke {
copy(isBalanceHidden = true)
}
}
}
override fun onHiddenBalanceNotificationAction(isPermanent: Boolean) {
onDismissBottomSheet()
if (isPermanent) {
viewModelScope.launch {
updateBalanceHidingSettingsUseCase.invoke {
copy(isBalanceHidingNotificationEnabled = false)
}
}
}
}
override fun onDismissBottomSheet() {
stateHolder.updateWithoutModalNotification()
}
}

View file

@ -0,0 +1,48 @@
package com.tangem.tap.features.main
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.tap.features.main.model.ActionConfig
import com.tangem.tap.features.main.model.ModalNotification
import com.tangem.tap.features.main.model.Toast
import com.tangem.wallet.R
internal class NotificationsFactory(
private val intents: MainIntents,
) {
fun createBalancesAreHiddenToast(): Toast {
return Toast(
message = resourceReference(R.string.toast_balances_hidden),
action = ActionConfig(
text = resourceReference(R.string.toast_undo),
onClick = intents::onHiddenBalanceToastAction,
),
)
}
fun createBalancesAreShownToast(): Toast {
return Toast(
message = resourceReference(R.string.toast_balances_shown),
action = ActionConfig(
text = resourceReference(R.string.toast_undo),
onClick = intents::onShownBalanceToastAction,
),
)
}
fun createBalancesAreHiddenModalNotification(): ModalNotification {
return ModalNotification(
iconResId = R.drawable.ic_eye_off_outline_24,
title = resourceReference(R.string.balance_hidden_title),
message = resourceReference(R.string.balance_hidden_description),
primaryAction = ActionConfig(
text = resourceReference(R.string.balance_hidden_got_it_button),
onClick = { intents.onHiddenBalanceNotificationAction(isPermanent = false) },
),
secondaryAction = ActionConfig(
text = resourceReference(R.string.balance_hidden_do_not_show_button),
onClick = { intents.onHiddenBalanceNotificationAction(isPermanent = true) },
),
)
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.tap.features.main.model
import com.tangem.core.ui.extensions.TextReference
internal data class ActionConfig(
val text: TextReference,
val onClick: () -> Unit,
)

View file

@ -0,0 +1,9 @@
package com.tangem.tap.features.main.model
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.event.StateEvent
internal data class MainScreenState(
val toast: StateEvent<Toast>,
val modalNotification: TangemBottomSheetConfig?,
)

View file

@ -0,0 +1,13 @@
package com.tangem.tap.features.main.model
import androidx.annotation.DrawableRes
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
internal data class ModalNotification(
@DrawableRes val iconResId: Int,
val title: TextReference,
val message: TextReference,
val primaryAction: ActionConfig,
val secondaryAction: ActionConfig?,
) : TangemBottomSheetConfigContent

View file

@ -0,0 +1,8 @@
package com.tangem.tap.features.main.model
import com.tangem.core.ui.extensions.TextReference
internal data class Toast(
val message: TextReference,
val action: ActionConfig,
)

View file

@ -0,0 +1,57 @@
package com.tangem.tap.features.main.ui
import android.content.DialogInterface
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.screen.ComposeBottomSheetFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.tap.features.main.MainViewModel
import com.tangem.tap.features.main.model.ModalNotification
import com.tangem.tap.features.main.ui.components.ModalNotificationContent
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class ModalNotificationBottomSheetFragment : ComposeBottomSheetFragment() {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
private val viewModel: MainViewModel by activityViewModels()
@Composable
override fun ScreenContent(modifier: Modifier) {
val state by viewModel.state.collectAsStateWithLifecycle()
val notification = state.modalNotification
if (notification == null) {
dismiss()
return
}
BackHandler(onBack = notification.onDismissRequest)
when (val content = notification.content) {
is ModalNotification -> ModalNotificationContent(
modifier = modifier,
notification = content,
)
else -> Unit
}
LaunchedEffect(notification) {
if (!notification.isShow) {
dismiss()
}
}
}
override fun onDismiss(dialog: DialogInterface) {
viewModel.state.value.modalNotification?.onDismissRequest?.invoke()
super.onDismiss(dialog)
}
}

View file

@ -0,0 +1,133 @@
package com.tangem.tap.features.main.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.main.model.ActionConfig
import com.tangem.tap.features.main.model.ModalNotification
import com.tangem.wallet.R
@Composable
internal fun ModalNotificationContent(notification: ModalNotification, modifier: Modifier = Modifier) {
Column(
modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing40),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Hand()
Icon(
modifier = Modifier.size(TangemTheme.dimens.size48),
painter = painterResource(notification.iconResId),
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
Column(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = notification.title.resolveReference(),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
Text(
text = notification.message.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
}
Column(
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
horizontalAlignment = Alignment.CenterHorizontally,
) {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = notification.primaryAction.text.resolveReference(),
onClick = notification.primaryAction.onClick,
)
when (val secondaryAction = notification.secondaryAction) {
null -> Unit
else -> SecondaryButton(
modifier = Modifier.fillMaxWidth(),
text = secondaryAction.text.resolveReference(),
onClick = secondaryAction.onClick,
)
}
}
}
}
// region Preview
@Preview(widthDp = 360, heightDp = 404)
@Composable
private fun ModalNotificationContentPreview_Light(
@PreviewParameter(GlobalNotificationProvider::class) param: ModalNotification,
) {
TangemTheme(isDark = false) {
ModalNotificationContent(
param,
modifier = Modifier.background(
color = TangemTheme.colors.background.plain,
shape = TangemTheme.shapes.bottomSheet,
),
)
}
}
@Preview(widthDp = 360, heightDp = 404)
@Composable
private fun ModalNotificationContentPreview_Dark(
@PreviewParameter(GlobalNotificationProvider::class) param: ModalNotification,
) {
TangemTheme(isDark = true) {
ModalNotificationContent(
param,
modifier = Modifier.background(
color = TangemTheme.colors.background.plain,
shape = TangemTheme.shapes.bottomSheet,
),
)
}
}
private class GlobalNotificationProvider : CollectionPreviewParameterProvider<ModalNotification>(
collection = listOf(
ModalNotification(
iconResId = R.drawable.ic_eye_off_outline_24,
title = resourceReference(R.string.balance_hidden_title),
message = resourceReference(R.string.balance_hidden_description),
primaryAction = ActionConfig(
text = resourceReference(R.string.balance_hidden_got_it_button),
onClick = {},
),
secondaryAction = ActionConfig(
text = resourceReference(R.string.balance_hidden_do_not_show_button),
onClick = {},
),
),
),
)
// endregion Preview

View file

@ -1,7 +1,7 @@
package com.tangem.tap.features.onboarding
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.redux.StateDialog
import com.tangem.core.navigation.StateDialog
/**
[REDACTED_AUTHOR]

View file

@ -38,7 +38,7 @@ object OnboardingHelper {
}
}
response.cardTypesResolver.isWallet2() -> {
response.cardTypesResolver.isWallet2() || response.cardTypesResolver.isShibaWallet() -> {
val emptyWallets = response.card.wallets.isEmpty()
val activationInProgress = cardInfoStorage.isActivationInProgress(cardId)
val backupNotActive = response.card.backupStatus?.isActive != true
@ -53,7 +53,10 @@ object OnboardingHelper {
fun whereToNavigate(scanResponse: ScanResponse): AppScreen {
return when (scanResponse.productType) {
ProductType.Note -> AppScreen.OnboardingNote
ProductType.Wallet, ProductType.Wallet2 -> if (scanResponse.card.settings.isBackupAllowed) {
ProductType.Wallet,
ProductType.Wallet2,
ProductType.Ring,
-> if (scanResponse.card.settings.isBackupAllowed) {
AppScreen.OnboardingWallet
} else {
AppScreen.OnboardingOther

View file

@ -16,6 +16,7 @@ import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.hasPendingTransactions
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.data.source.preferences.storage.UsedCardsPrefStorage
import com.tangem.tap.features.demo.isDemoCard
import timber.log.Timber
import java.math.BigDecimal
@ -38,7 +39,8 @@ class OnboardingManager(
}
suspend fun updateBalance(walletManager: WalletManager): OnboardingWalletBalance {
val balance = when (val result = walletManager.safeUpdate()) {
val isDemoCard = scanResponse.isDemoCard()
val balance = when (val result = walletManager.safeUpdate(isDemoCard)) {
is Result.Success -> {
val wallet = walletManager.wallet
val valueOfAmount = wallet.amounts[AmountType.Coin]?.value

View file

@ -1,11 +1,11 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import android.net.Uri
import androidx.lifecycle.LifecycleCoroutineScope
import com.tangem.common.CompletionResult
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
sealed class OnboardingWalletAction : Action {
@ -18,7 +18,7 @@ sealed class OnboardingWalletAction : Action {
) : OnboardingWalletAction()
object Done : OnboardingWalletAction()
data class FinishOnboarding(val lifecycleCoroutineScope: LifecycleCoroutineScope) : OnboardingWalletAction()
data class FinishOnboarding(val scope: CoroutineScope) : OnboardingWalletAction()
object ResumeBackup : OnboardingWalletAction()

View file

@ -4,7 +4,10 @@ import android.net.Uri
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.ifNotNull
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
@ -16,8 +19,10 @@ import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.Artwork
import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.operations.backup.BackupService
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
@ -33,9 +38,11 @@ import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.wallet.models.toCurrencies
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.wallet.R
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
object OnboardingWalletMiddleware {
val handler = onboardingWalletMiddleware
@ -92,7 +99,21 @@ private fun handleWalletAction(action: Action) {
is OnboardingWalletAction.LoadArtwork -> {
scope.launch {
val cardArtwork = when (onboardingManager) {
null -> action.cardArtworkUriForUnfinishedBackup
null -> {
// onboardingManager is null when backup started from previously interrupted
val primaryCardId = backupService.primaryCardId
val cardPublicKey = backupService.primaryPublicKey
if (primaryCardId != null && cardPublicKey != null) {
// uses when no scanResponse and backup state restored
loadArtworkForUnfinishedBackup(
cardId = primaryCardId,
cardPublicKey = cardPublicKey,
defaultArtwork = action.cardArtworkUriForUnfinishedBackup,
)
} else {
action.cardArtworkUriForUnfinishedBackup
}
}
else -> onboardingManager.loadArtworkUrl()
.takeIf { it != Artwork.DEFAULT_IMG_URL }
?.let { Uri.parse(it) }
@ -152,7 +173,7 @@ private fun handleWalletAction(action: Action) {
if (scanResponse == null) {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(HomeAction.ReadCard(scope = action.lifecycleCoroutineScope))
store.dispatch(HomeAction.ReadCard(scope = action.scope))
} else {
val backupState = store.state.onboardingWalletState.backupState
val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState)
@ -185,6 +206,24 @@ private fun handleWalletAction(action: Action) {
}
}
private suspend fun loadArtworkForUnfinishedBackup(
cardId: String,
cardPublicKey: ByteArray,
defaultArtwork: Uri?,
): Uri {
return when (val cardInfo = OnlineCardVerifier().getCardInfo(cardId, cardPublicKey)) {
is Result.Success -> {
val artworkId = cardInfo.data.artwork?.id
if (artworkId.isNullOrEmpty()) {
defaultArtwork ?: Uri.EMPTY
} else {
Uri.parse(OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId))
}
}
is Result.Failure -> defaultArtwork ?: Uri.EMPTY
}
}
@Suppress("LongMethod", "ComplexMethod")
private fun handleWallet2Action(action: OnboardingWallet2Action) {
val globalState = store.state.globalState
@ -331,7 +370,12 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
}
}
is BackupAction.ScanPrimaryCard -> {
backupService.readPrimaryCard(cardId = card?.cardId) { result ->
val iconScanRes = if (scanResponse?.cardTypesResolver?.isRing() == true) {
R.drawable.img_hand_scan_ring
} else {
null
}
backupService.readPrimaryCard(iconScanRes = iconScanRes, cardId = card?.cardId) { result ->
when (result) {
is CompletionResult.Success -> {
store.dispatchOnMain(BackupAction.StartAddingBackupCards)
@ -398,7 +442,12 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
}
}
is BackupAction.WritePrimaryCard -> {
backupService.proceedBackup { result ->
val iconScanRes = if (scanResponse?.cardTypesResolver?.isRing() == true) {
R.drawable.img_hand_scan_ring
} else {
null
}
backupService.proceedBackup(iconScanRes = iconScanRes) { result ->
when (result) {
is CompletionResult.Success -> {
store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(1))
@ -455,10 +504,18 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber))
}
userWalletsListManager.selectedUserWalletSync?.walletId?.let {
if (scanResponse != null) {
scope.launch {
val userWallet = UserWalletBuilder(scanResponse)
.backupCardsIds(backupState.backupCardIds.toSet())
.build()
.guard {
Timber.e("User wallet not created")
return@launch
}
userWalletsListManager.update(
userWalletId = it,
userWalletId = userWallet.walletId,
update = { wallet ->
wallet.copy(
scanResponse = updateScanResponseAfterBackup(

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.tap.backupService
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
@ -40,6 +41,7 @@ private object ReducerForGlobalAction {
maxBackupCards = MAX_BACKUP_CARDS,
canSkipBackup = action.canSkipBackup,
),
isRingOnboarding = action.scanResponse.cardTypesResolver.isRing(),
)
}
is GlobalAction.Onboarding.StartForUnfinishedBackup -> {

View file

@ -2,7 +2,7 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
import android.graphics.Bitmap
import android.net.Uri
import com.tangem.tap.common.redux.StateDialog
import com.tangem.core.navigation.StateDialog
import org.rekotlin.StateType
/**
@ -14,6 +14,7 @@ data class OnboardingWalletState(
val backupState: BackupState = BackupState(),
val cardArtworkUri: Uri? = null,
val showConfetti: Boolean = false,
val isRingOnboarding: Boolean = false,
) : StateType {
@Suppress("MagicNumber")

View file

@ -4,9 +4,7 @@ import com.google.android.material.button.MaterialButton
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.postUiDelayBg
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupState
import com.tangem.tap.features.onboarding.products.wallet.ui.BackupCardType.FIRST_BACKUP
import com.tangem.tap.features.onboarding.products.wallet.ui.BackupCardType.ORIGIN
import com.tangem.tap.features.onboarding.products.wallet.ui.BackupCardType.SECOND_BACKUP
import com.tangem.tap.features.onboarding.products.wallet.ui.BackupCardType.*
import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding
/**
@ -29,7 +27,7 @@ class WalletBackupAnimator(
private val viewState: State = when (cardsWidget.leapfrogWidget.getViewsCount()) {
2 -> State.TwoCards
3 -> State.TreeCards
3 -> State.ThreeCards
else -> throw UnsupportedOperationException()
}
@ -131,7 +129,7 @@ class WalletBackupAnimator(
}
}
}
State.TreeCards -> {
State.ThreeCards -> {
when (backupCard) {
1 -> {
if (firstBackupCardAnimated) return
@ -147,6 +145,15 @@ class WalletBackupAnimator(
if (secondBackupCardAnimated) return
secondBackupCardAnimated = true
/*
* !!! Workaround !!!
* For interrupted backup
* */
if (!firstBackupCardAnimated) {
showWriteBackupCard(state, backupCard = 1)
return
}
SECOND_BACKUP.alpha(1f)
cardsWidget.leapfrogWidget.leap {
ORIGIN.alpha(0.4f)
@ -165,7 +172,7 @@ class WalletBackupAnimator(
}
private enum class State {
TreeCards,
ThreeCards,
TwoCards,
}
@ -186,7 +193,7 @@ class WalletBackupAnimator(
}
}
SECOND_BACKUP -> {
if (viewState != State.TreeCards) return
if (viewState != State.ThreeCards) return
cardsWidget.getSecondBackupCardView().animate().apply {
alpha(alpha)
this.startDelay = startDelay
@ -201,7 +208,7 @@ class WalletBackupAnimator(
ORIGIN -> cardsWidget.getOriginCardView().alpha = alpha
FIRST_BACKUP -> cardsWidget.getFirstBackupCardView().alpha = alpha
SECOND_BACKUP -> {
if (viewState != State.TreeCards) return
if (viewState != State.ThreeCards) return
cardsWidget.getSecondBackupCardView().alpha = alpha
}
}
@ -212,7 +219,7 @@ class WalletBackupAnimator(
ORIGIN -> cardsWidget.getOriginCardView().show()
FIRST_BACKUP -> cardsWidget.getFirstBackupCardView().show()
SECOND_BACKUP -> {
if (viewState != State.TreeCards) return
if (viewState != State.ThreeCards) return
cardsWidget.getSecondBackupCardView().show()
}
}

View file

@ -177,13 +177,21 @@ class OnboardingWalletFragment :
seedPhraseStateHandler.newState(this, state, seedPhraseViewModel)
state.cardArtworkUri?.let {
seedPhraseViewModel.setCardArtworkUri(it.toString())
loadImageIntoImageView(it, binding.imvFrontCard)
if (state.isRingOnboarding) {
binding.imvFrontCard.load(R.drawable.img_ring_placeholder)
} else {
loadImageIntoImageView(state.cardArtworkUri, binding.imvFrontCard)
}
loadImageIntoImageView(it, binding.imvFirstBackupCard)
loadImageIntoImageView(it, binding.imvSecondBackupCard)
}
}
else -> {
loadImageIntoImageView(state.cardArtworkUri, binding.imvFrontCard)
if (state.isRingOnboarding) {
binding.imvFrontCard.load(R.drawable.img_ring_placeholder)
} else {
loadImageIntoImageView(state.cardArtworkUri, binding.imvFrontCard)
}
loadImageIntoImageView(state.cardArtworkUri, binding.imvFirstBackupCard)
loadImageIntoImageView(state.cardArtworkUri, binding.imvSecondBackupCard)
handleOnboardingStep(state)
@ -390,9 +398,9 @@ class OnboardingWalletFragment :
}
private fun showWriteBackupCard(state: BackupState) = with(binding) {
reInitCardsWidgetIfNeeded(state.backupCardsNumber)
prepareViewForFinalizeStep()
reInitCardsWidgetIfNeeded(state.backupCardsNumber)
val cardNumber = (state.backupStep as? BackupStep.WriteBackupCard)?.cardNumber ?: 1
val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4))
tvHeader.text = getString(R.string.onboarding_title_backup_card_format, cardNumber)
@ -427,7 +435,7 @@ class OnboardingWalletFragment :
layoutButtonsCommon.btnWalletAlternativeAction.hide()
layoutButtonsCommon.btnWalletMainAction.setOnClickListener {
showConfetti(false)
store.dispatch(OnboardingWalletAction.FinishOnboarding(lifecycleCoroutineScope = lifecycleScope))
store.dispatch(OnboardingWalletAction.FinishOnboarding(scope = requireActivity().lifecycleScope))
}
animator.showSuccess {

View file

@ -6,8 +6,8 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.core.TangemSdkError
import com.tangem.core.navigation.StateDialog
import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.ToastNotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.send.redux.middlewares
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered
import com.tangem.tap.common.redux.AppState
@ -9,6 +10,10 @@ import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification.SetAddressError
import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification.SetWalletAddress
import com.tangem.tap.features.send.redux.AddressVerifyAction.Error
import com.tangem.tap.mainScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
@ -17,6 +22,8 @@ import org.rekotlin.DispatchFunction
*/
internal class AddressMiddleware {
private val addressValidator = AddressValidator()
fun handle(action: AddressActionUi, appState: AppState?, dispatch: (Action) -> Unit) {
when (action) {
is AddressActionUi.HandleUserInput -> handleUserInput(action.data, appState, dispatch)
@ -54,13 +61,13 @@ internal class AddressMiddleware {
dispatch: (Action) -> Unit,
) {
val sendState = appState?.sendState ?: return
val wallet = sendState.walletManager?.wallet ?: return
val walletManager = sendState.walletManager ?: return
val address = sendState.addressState.normalFieldValue ?: return
val isUserInput = sendState.addressState.viewFieldValue.isFromUserInput
verifyAddress(
address = address,
wallet = wallet,
walletManager = walletManager,
isUserInput = isUserInput,
dispatch = dispatch,
sourceType = sourceType,
@ -68,6 +75,38 @@ internal class AddressMiddleware {
}
private fun verifyAddress(
address: String,
walletManager: WalletManager,
sourceType: AddressEntered.SourceType?,
isUserInput: Boolean,
dispatch: (Action) -> Unit,
) {
val wallet = walletManager.wallet
mainScope.launch {
val failReason = withContext(Dispatchers.IO) {
addressValidator.validateAddress(walletManager, address)
}
if (failReason == null) {
dispatchSuccessValidationActions(
address = address,
wallet = wallet,
sourceType = sourceType,
isUserInput = isUserInput,
dispatch = dispatch,
)
} else {
dispatchFailedValidationActions(
failReason = failReason,
sourceType = sourceType,
dispatch = dispatch,
)
}
}
}
private fun dispatchSuccessValidationActions(
address: String,
wallet: Wallet,
sourceType: AddressEntered.SourceType?,
@ -113,57 +152,49 @@ internal class AddressMiddleware {
val supposedAddress = noSchemeAddress.removeShareUriQuery() // TODO: parse query?
val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(wallet, supposedAddress)
if (failReason == null) {
noSchemeAddress.getQueryParameter("amount")?.toBigDecimalOrNull()?.let {
dispatch(AmountAction.SetAmount(it, false))
dispatch(AmountActionUi.CheckAmountToSend)
}
dispatch(SetWalletAddress(supposedAddress, isUserInput))
dispatch(TransactionExtrasAction.Prepare(wallet.blockchain, address, null))
dispatch(FeeAction.RequestFee)
sourceType?.let {
Analytics.send(
event = AddressEntered(
sourceType = sourceType,
validationResult = AddressEntered.ValidationResult.Success,
),
)
}
} else {
dispatch(SetAddressError(failReason))
dispatch(TransactionExtrasAction.Release)
sourceType?.let {
Analytics.send(
event = AddressEntered(
sourceType = sourceType,
validationResult = AddressEntered.ValidationResult.Fail,
),
)
}
noSchemeAddress.getQueryParameter("amount")?.toBigDecimalOrNull()?.let {
dispatch(AmountAction.SetAmount(it, false))
dispatch(AmountActionUi.CheckAmountToSend)
}
dispatch(SetWalletAddress(supposedAddress, isUserInput))
dispatch(TransactionExtrasAction.Prepare(wallet.blockchain, address, null))
dispatch(FeeAction.RequestFee)
sourceType?.let {
Analytics.send(
event = AddressEntered(
sourceType = sourceType,
validationResult = AddressEntered.ValidationResult.Success,
),
)
}
}
private fun isValidBlockchainAddressAndNotTheSameAsWallet(wallet: Wallet, address: String): Error? {
return if (wallet.blockchain.validateAddress(address)) {
if (wallet.addresses.all { it.value != address }) {
null
} else {
Error.ADDRESS_SAME_AS_WALLET
}
} else {
Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN
private fun dispatchFailedValidationActions(
failReason: Error,
sourceType: AddressEntered.SourceType?,
dispatch: (Action) -> Unit,
) {
dispatch(SetAddressError(failReason))
dispatch(TransactionExtrasAction.Release)
sourceType?.let {
Analytics.send(
event = AddressEntered(
sourceType = sourceType,
validationResult = AddressEntered.ValidationResult.Fail,
),
)
}
}
private fun String.removeShareUriQuery(): String = this.substringBefore("?")
private fun String.getQueryParameter(name: String): String? {
return this.substringAfter("?").splitToMap("&", "=")[name]
}
private fun verifyClipboard(input: String?, appState: AppState?, dispatch: DispatchFunction) {
val address = input ?: return
val wallet = appState?.sendState?.walletManager?.wallet ?: return
val walletManager = appState?.sendState?.walletManager ?: return
val internalDispatcher: (Action) -> Unit = {
when (it) {
@ -178,7 +209,7 @@ internal class AddressMiddleware {
verifyAddress(
address = address,
wallet = wallet,
walletManager = walletManager,
sourceType = null,
isUserInput = false,
dispatch = internalDispatcher,

View file

@ -0,0 +1,70 @@
package com.tangem.tap.features.send.redux.middlewares
import com.tangem.blockchain.blockchains.near.NearWalletManager
import com.tangem.blockchain.blockchains.near.network.NearAccount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.extensions.Result
import com.tangem.tap.features.send.redux.AddressVerifyAction
internal class AddressValidator {
suspend fun validateAddress(walletManager: WalletManager, address: String): AddressVerifyAction.Error? {
val blockchain = walletManager.wallet.blockchain
val wallet = walletManager.wallet
return if (blockchain.isNear()) {
validateNearAddress(walletManager, address)
} else {
validateAddress(wallet, address)
}
}
private suspend fun validateNearAddress(
walletManager: WalletManager,
address: String,
): AddressVerifyAction.Error? {
// implicit address validation
if (address.length == NEAR_IMPLICIT_ADDRESS_LENGTH && hexRegex.matches(address)) {
return validateAddress(walletManager.wallet, address)
}
// named address validation
if (address.length in NEAR_MIN_ADDRESS_LENGTH until NEAR_IMPLICIT_ADDRESS_LENGTH &&
nearAddressRegex.matches(address)
) {
val result = (walletManager as? NearWalletManager)?.getAccount(address)
return if (result is Result.Success && result.data is NearAccount.Full) {
null
} else {
AddressVerifyAction.Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN
}
}
return AddressVerifyAction.Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN
}
private fun Blockchain.isNear(): Boolean {
return this == Blockchain.Near || this == Blockchain.NearTestnet
}
private fun validateAddress(wallet: Wallet, address: String): AddressVerifyAction.Error? {
return if (wallet.blockchain.validateAddress(address)) {
if (wallet.addresses.all { it.value != address }) {
null
} else {
AddressVerifyAction.Error.ADDRESS_SAME_AS_WALLET
}
} else {
AddressVerifyAction.Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN
}
}
companion object {
private const val NEAR_MIN_ADDRESS_LENGTH = 2
private const val NEAR_IMPLICIT_ADDRESS_LENGTH = 64
private val hexRegex = Regex("^[0-9a-f]+$")
private val nearAddressRegex = Regex("^(([a-z\\d]+[\\-_])*[a-z\\d]+\\.)*([a-z\\d]+[\\-_])*[a-z\\d]+\$")
}
}

View file

@ -15,6 +15,7 @@ import com.tangem.tap.features.send.redux.ReceiptAction
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -72,6 +73,13 @@ class RequestFeeMiddleware {
dispatch(FeeAction.FeeCalculation.ClearResult)
dispatch(FeeAction.ChangeLayoutVisibility(main = false))
store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError(
walletManager = walletManager,
amountToSend = destinationAmount,
feeAmount = null,
destinationAddress = destinationAddress,
)
val blockchainSdkError = feeResult.error as? BlockchainSdkError ?: return@withContext
dispatch(
SendAction.Dialog.RequestFeeError(

View file

@ -4,10 +4,10 @@ import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.isZero
import com.tangem.core.navigation.StateDialog
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.text.DecimalDigitsInputFilter
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
@ -57,7 +57,7 @@ data class SendState(
return if (!this.coinIsConvertible()) value else coinConverter!!.toCrypto(value)
}
fun convertFiatToToken(value: BigDecimal): BigDecimal {
private fun convertFiatToToken(value: BigDecimal): BigDecimal {
return if (!this.tokenIsConvertible()) value else tokenConverter!!.toCrypto(value)
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.features.send.ui
import androidx.lifecycle.*
import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.tokens.FetchPendingTransactionsUseCase
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
@ -29,7 +29,7 @@ import javax.inject.Inject
internal class SendViewModel @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
private val appStateHolder: AppStateHolder,
private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val listenToFlipsUseCase: ListenToFlipsUseCase,
private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
@ -41,11 +41,11 @@ internal class SendViewModel @Inject constructor(
private val cryptoCurrency: CryptoCurrency? = savedStateHandle[SendRouter.CRYPTO_CURRENCY_KEY]
override fun onCreate(owner: LifecycleOwner) {
isBalanceHiddenUseCase()
getBalanceHidingSettingsUseCase()
.flowWithLifecycle(owner.lifecycle)
.onEach { isBalanceHidden ->
.onEach {
withContext(dispatchers.main) {
appStateHolder.mainStore?.dispatch(AmountAction.HideBalance(isBalanceHidden))
appStateHolder.mainStore?.dispatch(AmountAction.HideBalance(it.isBalanceHidden))
}
}
.launchIn(viewModelScope)

View file

@ -243,7 +243,7 @@ internal class SendStateSubscriber(
}
val filter = DecimalDigitsInputFilter(
digitsBeforeDecimal = 12,
digitsBeforeDecimal = 40,
digitsAfterDecimal = state.maxLengthOfAmount,
decimalSeparator = state.decimalSeparator,
)

View file

@ -1,10 +0,0 @@
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
}

View file

@ -1,42 +0,0 @@
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))
}
}

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