Updated on 2026-08-14
This commit is contained in:
commit
285e72a250
404 changed files with 8122 additions and 3606 deletions
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -141,6 +141,8 @@ class TopUpController(
|
|||
DataSourceTopupInfo.CardBalanceState.Empty
|
||||
AnalyticsParam.CardBalanceState.Full ->
|
||||
DataSourceTopupInfo.CardBalanceState.Full
|
||||
AnalyticsParam.CardBalanceState.NoRate ->
|
||||
DataSourceTopupInfo.CardBalanceState.NoRate
|
||||
},
|
||||
)
|
||||
topupWalletStorage.save(topupInfo)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ data class CardSettingsState(
|
|||
val condition1Checked: Boolean,
|
||||
val condition2Checked: Boolean,
|
||||
val accessCodeRecovery: AccessCodeRecoveryState? = null,
|
||||
val isShowPasswordResetRadioButton: Boolean,
|
||||
)
|
||||
|
||||
data class ManageSecurityState(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)) },
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,5 @@ private fun internalReduce(action: Action, state: AppState): DisclaimerState {
|
|||
is DisclaimerAction.OnProgressStateChanged -> disclaimerState.copy(
|
||||
progressState = action.state,
|
||||
)
|
||||
else -> disclaimerState
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.tap.features.main
|
||||
|
||||
internal interface MainIntents {
|
||||
|
||||
fun onHiddenBalanceToastAction()
|
||||
|
||||
fun onShownBalanceToastAction()
|
||||
|
||||
fun onHiddenBalanceNotificationAction(isPermanent: Boolean)
|
||||
|
||||
fun onDismissBottomSheet()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
127
app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt
Normal file
127
app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]+\$")
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ internal class SendStateSubscriber(
|
|||
}
|
||||
|
||||
val filter = DecimalDigitsInputFilter(
|
||||
digitsBeforeDecimal = 12,
|
||||
digitsBeforeDecimal = 40,
|
||||
digitsAfterDecimal = state.maxLengthOfAmount,
|
||||
decimalSeparator = state.decimalSeparator,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue