Updated on 2026-08-14
This commit is contained in:
commit
257fc06c52
251 changed files with 8615 additions and 2623 deletions
6
.idea/codeStyles/Project.xml
generated
6
.idea/codeStyles/Project.xml
generated
|
|
@ -29,7 +29,6 @@
|
|||
<codeStyleSettings language="Groovy">
|
||||
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
|
||||
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
|
||||
<option name="LINE_COMMENT_ADD_SPACE_ON_REFORMAT" value="true" />
|
||||
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
|
||||
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
|
||||
|
|
@ -174,9 +173,8 @@
|
|||
<option name="RIGHT_MARGIN" value="120" />
|
||||
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
|
||||
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
|
||||
<option name="LINE_COMMENT_ADD_SPACE_ON_REFORMAT" value="true" />
|
||||
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="0" />
|
||||
<option name="KEEP_BLANK_LINES_IN_CODE" value="0" />
|
||||
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
|
||||
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||
<option name="EXTENDS_LIST_WRAP" value="5" />
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ apply plugin: 'com.google.firebase.crashlytics'
|
|||
apply from: '../dependencies.gradle'
|
||||
|
||||
android {
|
||||
compileSdkVersion 32
|
||||
compileSdkVersion 33
|
||||
defaultConfig {
|
||||
applicationId "com.tangem.wallet"
|
||||
versionCode project.hasProperty('versionCode') ? project.property('versionCode') as int : 1
|
||||
versionName project.hasProperty('versionName') ? project.property('versionName') : "1.0.0.-SNAPSHOT"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 32
|
||||
targetSdkVersion 33
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false'
|
||||
|
|
@ -83,11 +83,11 @@ dependencies {
|
|||
implementation implementation(project(path: ':network'))
|
||||
implementation implementation(project(path: ':common'))
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.8.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation 'com.google.android.material:material:1.5.0'
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.5.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.3'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'com.google.android.material:material:1.6.1'
|
||||
implementation 'com.google.android.play:core:1.10.3'
|
||||
implementation 'com.google.android.play:core-ktx:1.8.1'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
|
@ -97,15 +97,15 @@ dependencies {
|
|||
implementation "com.tangem.tangem-sdk-kotlin:android:${versions.tangem_card_sdk}"
|
||||
|
||||
// WebView
|
||||
implementation "androidx.browser:browser:1.3.0"
|
||||
implementation "androidx.browser:browser:1.4.0"
|
||||
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
|
||||
//lifecycle
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:2.3.1"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:2.5.1"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1"
|
||||
|
||||
implementation "org.rekotlin:rekotlin:1.0.4"
|
||||
|
||||
|
|
@ -176,6 +176,9 @@ dependencies {
|
|||
|
||||
implementation 'com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6'
|
||||
|
||||
// Shared preferences (encrypted)
|
||||
implementation 'at.favre.lib:armadillo:0.9.0'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation "com.google.truth:truth:1.1.3"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
android:name="android.hardware.nfc"
|
||||
android:required="true" />
|
||||
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRICT" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
|
@ -131,18 +130,6 @@
|
|||
<activity android:name="zendesk.messaging.MessagingActivity"
|
||||
android:theme="@style/ZendeskTheme" />
|
||||
|
||||
<activity android:name="zendesk.support.guide.HelpCenterActivity"
|
||||
android:theme="@style/ZendeskTheme" />
|
||||
|
||||
<activity android:name="zendesk.support.guide.ViewArticleActivity"
|
||||
android:theme="@style/ZendeskTheme" />
|
||||
|
||||
<activity android:name="zendesk.support.request.RequestActivity"
|
||||
android:theme="@style/ZendeskTheme" />
|
||||
|
||||
<activity android:name="zendesk.support.requestlist.RequestListActivity"
|
||||
android:theme="@style/ZendeskTheme" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit f33f020823031f1c532cf5a9d5cf3fbecadcf050
|
||||
Subproject commit c8f577f2ab48e0d6c49f79b6cff21eabe67cd63e
|
||||
|
|
@ -430,6 +430,27 @@
|
|||
"networkId": "ethereum-pow-iou/test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id" : "polkadot",
|
||||
"symbol" : "DOT",
|
||||
"name" : "Polkadot",
|
||||
"networks" : [
|
||||
{
|
||||
"networkId" : "polkadot/test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "optimistic-ethereum",
|
||||
"name": "Optimistic Ethereum",
|
||||
"symbol": "ETH",
|
||||
"active": true,
|
||||
"networks": [
|
||||
{
|
||||
"networkId": "optimistic-ethereum/test"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
|
|
|
|||
1832
app/src/main/assets/tos.html
Normal file
1832
app/src/main/assets/tos.html
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -13,9 +13,12 @@ import com.tangem.TangemSdk
|
|||
import com.tangem.domain.common.FeatureCoroutineExceptionHandler
|
||||
import com.tangem.operations.backup.BackupService
|
||||
import com.tangem.tangem_sdk_new.extensions.init
|
||||
import com.tangem.tap.common.ActivityResultCallbackHolder
|
||||
import com.tangem.tap.common.DialogManager
|
||||
import com.tangem.tap.common.IntentHandler
|
||||
import com.tangem.tap.common.OnActivityResultCallback
|
||||
import com.tangem.tap.common.SnackbarHandler
|
||||
import com.tangem.tap.common.extensions.copyToClipboard
|
||||
import com.tangem.tap.common.redux.NotificationsHandler
|
||||
import com.tangem.tap.common.redux.global.AndroidResources
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
|
|
@ -25,7 +28,6 @@ import com.tangem.tap.common.shop.GooglePayService
|
|||
import com.tangem.tap.common.shop.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.features.details.redux.walletconnect.WalletConnectAction
|
||||
import com.tangem.tap.features.shop.redux.ShopAction
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.ActivityMainBinding
|
||||
|
|
@ -48,15 +50,18 @@ private val mainCoroutineContext: CoroutineContext
|
|||
get() = Job() + Dispatchers.Main + FeatureCoroutineExceptionHandler.create("mainScope")
|
||||
val mainScope = CoroutineScope(mainCoroutineContext)
|
||||
|
||||
class MainActivity : AppCompatActivity(), SnackbarHandler {
|
||||
class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbackHolder {
|
||||
|
||||
private var snackbar: Snackbar? = null
|
||||
private val dialogManager = DialogManager()
|
||||
private val intentHandler = IntentHandler()
|
||||
private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind)
|
||||
|
||||
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
internalActivity = this
|
||||
setContentView(R.layout.activity_main)
|
||||
systemActions()
|
||||
store.dispatch(NavigationAction.ActivityCreated(WeakReference(this)))
|
||||
|
|
@ -66,25 +71,24 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
|
|||
backupService = BackupService.init(tangemSdk, this)
|
||||
|
||||
store.dispatch(GlobalAction.SetResources(getAndroidResources()))
|
||||
store.dispatch(WalletConnectAction.RestoreSessions)
|
||||
store.dispatch(
|
||||
ShopAction.CheckIfGooglePayAvailable(
|
||||
GooglePayService(createPaymentsClient(this), this)
|
||||
)
|
||||
GooglePayService(createPaymentsClient(this), this),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun getAndroidResources(): AndroidResources {
|
||||
return AndroidResources(
|
||||
AndroidResources.RString(
|
||||
R.string.copy_toast_msg,
|
||||
R.string.details_notification_erase_wallet_not_possible
|
||||
)
|
||||
R.string.details_notification_erase_wallet_not_possible,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun systemActions() {
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
val windowInsetsController = WindowInsetsControllerCompat(window, binding.root)
|
||||
|
|
@ -93,7 +97,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
|
|||
|
||||
supportFragmentManager.registerFragmentLifecycleCallbacks(
|
||||
NavBarInsetsFragmentLifecycleCallback(),
|
||||
true
|
||||
true,
|
||||
)
|
||||
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
|
|
@ -138,7 +142,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
|
|||
if (snackbar != null) return
|
||||
|
||||
snackbar = Snackbar.make(
|
||||
binding.fragmentContainer, getString(text), Snackbar.LENGTH_INDEFINITE
|
||||
binding.fragmentContainer, getString(text), Snackbar.LENGTH_INDEFINITE,
|
||||
)
|
||||
if (buttonTitle != null && action != null) {
|
||||
snackbar?.setAction(getString(buttonTitle), action)
|
||||
|
|
@ -153,12 +157,29 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
|
|||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
onActivityResultCallbacks.forEach { it(requestCode, resultCode, data) }
|
||||
when (requestCode) {
|
||||
LOAD_PAYMENT_DATA_REQUEST_CODE -> {
|
||||
store.dispatch(
|
||||
ShopAction.BuyWithGooglePay.HandleGooglePayResponse(resultCode, data)
|
||||
ShopAction.BuyWithGooglePay.HandleGooglePayResponse(resultCode, data),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun addOnActivityResultCallback(callback: OnActivityResultCallback) {
|
||||
onActivityResultCallbacks.remove(callback)
|
||||
onActivityResultCallbacks.add(callback)
|
||||
}
|
||||
|
||||
override fun removeOnActivityResultCallback(callback: OnActivityResultCallback) {
|
||||
onActivityResultCallbacks.remove(callback)
|
||||
}
|
||||
}
|
||||
|
||||
lateinit var internalActivity: AppCompatActivity
|
||||
//TODO: delete
|
||||
fun copyToClipboard(text: String) {
|
||||
internalActivity.copyToClipboard(text)
|
||||
}
|
||||
|
|
@ -1,17 +1,19 @@
|
|||
package com.tangem.tap
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import com.appsflyer.AppsFlyerLib
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import com.google.firebase.remoteconfig.ktx.remoteConfig
|
||||
import com.google.firebase.remoteconfig.ktx.remoteConfigSettings
|
||||
import com.tangem.Log
|
||||
import com.tangem.LogFormat
|
||||
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
|
||||
import com.tangem.domain.DomainLayer
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import com.tangem.network.common.MoshiConverter
|
||||
import com.tangem.tap.common.AndroidAssetReader
|
||||
import com.tangem.tap.common.AssetReader
|
||||
import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
|
||||
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
|
||||
import com.tangem.tap.common.feedback.FeedbackManager
|
||||
|
|
@ -21,133 +23,134 @@ import com.tangem.tap.common.redux.AppState
|
|||
import com.tangem.tap.common.redux.appReducer
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.shop.TangemShopService
|
||||
import com.tangem.tap.domain.configurable.config.Config
|
||||
import com.tangem.tap.domain.configurable.config.ConfigManager
|
||||
import com.tangem.tap.domain.configurable.config.FeaturesLocalLoader
|
||||
import com.tangem.tap.domain.configurable.config.FeaturesRemoteLoader
|
||||
import com.tangem.tap.domain.configurable.warningMessage.RemoteWarningLoader
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||
import com.tangem.tap.domain.tokens.CurrenciesRepository
|
||||
import com.tangem.tap.domain.tokens.UserTokensRepository
|
||||
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
|
||||
import com.tangem.tap.network.NetworkConnectivity
|
||||
import com.tangem.tap.persistence.PreferencesStorage
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import com.zendesk.logger.Logger
|
||||
import org.rekotlin.Store
|
||||
import timber.log.Timber
|
||||
import zendesk.chat.Chat
|
||||
|
||||
val store = Store(
|
||||
reducer = ::appReducer,
|
||||
middleware = AppState.getMiddleware(),
|
||||
state = AppState()
|
||||
state = AppState(),
|
||||
)
|
||||
val logConfig = LogConfig()
|
||||
|
||||
lateinit var foregroundActivityObserver: ForegroundActivityObserver
|
||||
|
||||
lateinit var preferencesStorage: PreferencesStorage
|
||||
lateinit var currenciesRepository: CurrenciesRepository
|
||||
lateinit var walletConnectRepository: WalletConnectRepository
|
||||
lateinit var shopService: TangemShopService
|
||||
lateinit var assetReader: AssetReader
|
||||
lateinit var userTokensRepository: UserTokensRepository
|
||||
|
||||
class TapApplication : Application(), ImageLoaderFactory {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
DomainLayer.init()
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
Firebase.remoteConfig.setConfigSettingsAsync(remoteConfigSettings {
|
||||
this.minimumFetchIntervalInSeconds = 60
|
||||
})
|
||||
} else {
|
||||
Firebase.remoteConfig.setConfigSettingsAsync(remoteConfigSettings {
|
||||
this.minimumFetchIntervalInSeconds = 3600
|
||||
})
|
||||
}
|
||||
|
||||
NetworkConnectivity.createInstance(store, this)
|
||||
preferencesStorage = PreferencesStorage(this)
|
||||
currenciesRepository = CurrenciesRepository(
|
||||
this, store.state.domainNetworks.tangemTechService
|
||||
)
|
||||
walletConnectRepository = WalletConnectRepository(this)
|
||||
|
||||
foregroundActivityObserver = ForegroundActivityObserver()
|
||||
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
|
||||
initFeedbackManager()
|
||||
loadConfigs()
|
||||
|
||||
BlockchainSdkRetrofitBuilder.enableNetworkLogging = BuildConfig.DEBUG
|
||||
DomainLayer.init()
|
||||
NetworkConnectivity.createInstance(store, this)
|
||||
preferencesStorage = PreferencesStorage(this)
|
||||
walletConnectRepository = WalletConnectRepository(this)
|
||||
|
||||
initAppsFlyer()
|
||||
assetReader = AndroidAssetReader(this)
|
||||
val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.defaultMoshi())
|
||||
initConfigManager(configLoader, ::initWithConfigDependency)
|
||||
initWarningMessagesManager()
|
||||
|
||||
BlockchainSdkRetrofitBuilder.enableNetworkLogging = LogConfig.network.blockchainSdkNetwork
|
||||
|
||||
userTokensRepository = UserTokensRepository.init(
|
||||
context = this,
|
||||
tangemTechService = store.state.domainNetworks.tangemTechService,
|
||||
)
|
||||
}
|
||||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return createCoilImageLoader(context = this)
|
||||
return createCoilImageLoader(
|
||||
context = this,
|
||||
logEnabled = LogConfig.imageLoader,
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadConfigs() {
|
||||
val moshi = MoshiConverter.defaultMoshi()
|
||||
val localLoader = FeaturesLocalLoader(this, moshi)
|
||||
val remoteLoader = FeaturesRemoteLoader(moshi)
|
||||
val configManager = ConfigManager(localLoader, remoteLoader)
|
||||
configManager.load { config ->
|
||||
private fun initConfigManager(loader: FeaturesLocalLoader, onComplete: (Config) -> Unit) {
|
||||
val configManager = ConfigManager()
|
||||
configManager.load(loader) { config ->
|
||||
store.dispatch(GlobalAction.SetConfigManager(configManager))
|
||||
shopService = TangemShopService(
|
||||
application = this,
|
||||
shopifyShop = config.shopify!!
|
||||
)
|
||||
store.state.globalState.feedbackManager?.initChat(
|
||||
context = this,
|
||||
zendeskConfig = config.zendesk!!
|
||||
)
|
||||
onComplete(config)
|
||||
}
|
||||
val warningsManager = WarningMessagesManager(RemoteWarningLoader(moshi))
|
||||
warningsManager.load { store.dispatch(GlobalAction.SetWarningManager(warningsManager)) }
|
||||
}
|
||||
|
||||
private fun initFeedbackManager() {
|
||||
val infoHolder = AdditionalFeedbackInfo()
|
||||
infoHolder.setAppVersion(this)
|
||||
|
||||
val logLevels = listOf(
|
||||
Log.Level.ApduCommand,
|
||||
Log.Level.Apdu,
|
||||
Log.Level.Tlv,
|
||||
Log.Level.Nfc,
|
||||
Log.Level.Command,
|
||||
Log.Level.Session,
|
||||
Log.Level.View,
|
||||
Log.Level.Network,
|
||||
Log.Level.Error,
|
||||
)
|
||||
val logWriter = TangemLogCollector(
|
||||
levels = logLevels,
|
||||
messageFormatter = LogFormat.StairsFormatter(),
|
||||
)
|
||||
Log.addLogger(logWriter)
|
||||
|
||||
store.dispatch(
|
||||
GlobalAction.SetFeedbackManager(
|
||||
FeedbackManager(
|
||||
infoHolder = infoHolder,
|
||||
logCollector = logWriter,
|
||||
preferencesStorage = preferencesStorage,
|
||||
),
|
||||
),
|
||||
)
|
||||
private fun initWithConfigDependency(config: Config) {
|
||||
shopService = TangemShopService(this, config.shopify!!)
|
||||
initAppsFlyer(this, config)
|
||||
initFeedbackManager(this, preferencesStorage, config)
|
||||
}
|
||||
|
||||
private fun initAppsFlyer() {
|
||||
val devKey = store.state.globalState.configManager?.config?.appsFlyerDevKey ?: return
|
||||
AppsFlyerLib.getInstance().init(devKey, null, this)
|
||||
AppsFlyerLib.getInstance().start(this)
|
||||
val analyticsHandler = GlobalAnalyticsHandler.createDefaultAnalyticHandlers(this)
|
||||
private fun initAppsFlyer(context: Context, config: Config) {
|
||||
AppsFlyerLib.getInstance().init(config.appsFlyerDevKey, null, context)
|
||||
AppsFlyerLib.getInstance().start(context)
|
||||
val analyticsHandler = GlobalAnalyticsHandler.createDefaultAnalyticHandlers(context)
|
||||
store.dispatch(GlobalAction.SetAnanlyticHandlers(analyticsHandler))
|
||||
}
|
||||
}
|
||||
|
||||
data class LogConfig(
|
||||
val coil: Boolean = BuildConfig.DEBUG,
|
||||
val storeAction: Boolean = BuildConfig.DEBUG,
|
||||
val zendesk: Boolean = BuildConfig.DEBUG,
|
||||
)
|
||||
private fun initFeedbackManager(context: Context, preferencesStorage: PreferencesStorage, config: Config) {
|
||||
fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo = AdditionalFeedbackInfo().apply {
|
||||
appVersion = try {
|
||||
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
pInfo.versionName
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
e.printStackTrace()
|
||||
"x.y.z"
|
||||
}
|
||||
}
|
||||
|
||||
fun initTangemLogCollector(): TangemLogCollector {
|
||||
val logLevels = listOf(
|
||||
Log.Level.ApduCommand,
|
||||
Log.Level.Apdu,
|
||||
Log.Level.Tlv,
|
||||
Log.Level.Nfc,
|
||||
Log.Level.Command,
|
||||
Log.Level.Session,
|
||||
Log.Level.View,
|
||||
Log.Level.Network,
|
||||
Log.Level.Error,
|
||||
)
|
||||
return TangemLogCollector(logLevels, LogFormat.StairsFormatter())
|
||||
}
|
||||
|
||||
val additionalFeedbackInfo = initAdditionalFeedbackInfo(context)
|
||||
val tangemLogCollector = initTangemLogCollector()
|
||||
Log.addLogger(tangemLogCollector)
|
||||
|
||||
val feedbackManager = FeedbackManager(
|
||||
infoHolder = additionalFeedbackInfo,
|
||||
logCollector = tangemLogCollector,
|
||||
preferencesStorage = preferencesStorage,
|
||||
)
|
||||
feedbackManager.chatInitializer = { zendeskConfig ->
|
||||
Chat.INSTANCE.init(context, zendeskConfig.accountKey, zendeskConfig.appId)
|
||||
Logger.setLoggable(LogConfig.zendesk)
|
||||
}
|
||||
store.dispatch(GlobalAction.SetFeedbackManager(feedbackManager))
|
||||
}
|
||||
|
||||
private fun initWarningMessagesManager() {
|
||||
store.dispatch(GlobalAction.SetWarningManager(WarningMessagesManager()))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.tap.common
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface ActivityResultCallbackHolder {
|
||||
fun addOnActivityResultCallback(callback: OnActivityResultCallback)
|
||||
fun removeOnActivityResultCallback(callback: OnActivityResultCallback)
|
||||
}
|
||||
|
||||
typealias OnActivityResultCallback = (Int, Int, Intent?) -> Unit
|
||||
20
app/src/main/java/com/tangem/tap/common/AssetReader.kt
Normal file
20
app/src/main/java/com/tangem/tap/common/AssetReader.kt
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.tap.common
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.tap.common.extensions.readAssetAsString
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface AssetReader {
|
||||
fun readAssetAsString(name: String): String
|
||||
}
|
||||
|
||||
class AndroidAssetReader(
|
||||
private val context: Context,
|
||||
) : AssetReader {
|
||||
|
||||
override fun readAssetAsString(name: String): String {
|
||||
return context.readAssetAsString(name)
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,9 @@ import com.tangem.tap.features.onboarding.AddressInfoBottomSheetDialog
|
|||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
|
||||
import com.tangem.tap.features.onboarding.products.twins.ui.dialog.CreateWalletInterruptDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.NoFundsForActivationDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.RegistrationErrorDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AddMoreBackupCardsDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.BackupInProgressDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog
|
||||
|
|
@ -113,7 +116,6 @@ class DialogManager : StoreSubscriber<GlobalState> {
|
|||
data = state.dialog.data,
|
||||
session = state.dialog.session,
|
||||
sessionId = state.dialog.sessionId,
|
||||
cardId = state.dialog.cardId,
|
||||
dAppName = state.dialog.dAppName,
|
||||
context = context,
|
||||
)
|
||||
|
|
@ -127,6 +129,8 @@ class DialogManager : StoreSubscriber<GlobalState> {
|
|||
is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context)
|
||||
is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(context)
|
||||
is BackupDialog.ConfirmDiscardingBackup -> ConfirmDiscardingBackupDialog.create(context)
|
||||
is SaltPayDialog.NoFundsForActivation -> NoFundsForActivationDialog.create(context)
|
||||
is SaltPayDialog.RegistrationError -> RegistrationErrorDialog.create(context, state.dialog)
|
||||
is WalletDialog.CurrencySelectionDialog ->
|
||||
CurrencySelectionDialog.create(state.dialog, context)
|
||||
is WalletDialog.ChooseTradeActionDialog ->
|
||||
|
|
|
|||
20
app/src/main/java/com/tangem/tap/common/FileReader.kt
Normal file
20
app/src/main/java/com/tangem/tap/common/FileReader.kt
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.tap.common
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.tap.common.extensions.readFile
|
||||
import com.tangem.tap.common.extensions.rewriteFile
|
||||
|
||||
interface FileReader {
|
||||
fun readFile(fileName: String): String
|
||||
fun rewriteFile(content: String, fileName: String)
|
||||
}
|
||||
|
||||
class AndroidFileReader(private val context: Context) : FileReader {
|
||||
override fun readFile(fileName: String): String {
|
||||
return context.readFile(fileName)
|
||||
}
|
||||
|
||||
override fun rewriteFile(content: String, fileName: String) {
|
||||
context.rewriteFile(content, fileName)
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,10 @@ fun postBackground(ms: Long = 0, func: Runnable) {
|
|||
if (ms == 0L) backgroundHandler.post { func.run() } else backgroundHandler.postDelayed(func, ms)
|
||||
}
|
||||
|
||||
fun postUiDelayBg(ms: Long, func: Runnable) {
|
||||
backgroundHandler.postDelayed({ uiHandler.post(func) }, ms)
|
||||
}
|
||||
|
||||
fun post(ms: Long = 0, func: Runnable) {
|
||||
if (ms == 0L) {
|
||||
func.run()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.tap.common.analytics
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import com.appsflyer.AFInAppEventParameterName
|
||||
import com.appsflyer.AFInAppEventType
|
||||
import com.appsflyer.AppsFlyerLib
|
||||
|
|
@ -8,7 +8,7 @@ import com.shopify.buy3.Storefront
|
|||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
|
||||
class AppsFlyerAnalyticsHandler(val context: Application): AnalyticsHandler() {
|
||||
class AppsFlyerAnalyticsHandler(val context: Context): AnalyticsHandler() {
|
||||
|
||||
override fun triggerEvent(
|
||||
event: AnalyticsEvent,
|
||||
|
|
@ -16,8 +16,7 @@ class AppsFlyerAnalyticsHandler(val context: Application): AnalyticsHandler() {
|
|||
blockchain: String?,
|
||||
params: Map<String, String>
|
||||
) {
|
||||
AppsFlyerLib.getInstance().logEvent(context,
|
||||
event.event, prepareParams(card, blockchain))
|
||||
AppsFlyerLib.getInstance().logEvent(context, event.event, prepareParams(card, blockchain))
|
||||
}
|
||||
|
||||
override fun logCardSdkError(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.tap.common.analytics
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import com.shopify.buy3.Storefront
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
|
|
@ -42,7 +42,7 @@ class GlobalAnalyticsHandler(val analyticsHandlers: List<AnalyticsHandler>) :
|
|||
}
|
||||
|
||||
companion object {
|
||||
fun createDefaultAnalyticHandlers(context: Application): GlobalAnalyticsHandler {
|
||||
fun createDefaultAnalyticHandlers(context: Context): GlobalAnalyticsHandler {
|
||||
return GlobalAnalyticsHandler(
|
||||
listOf(
|
||||
FirebaseAnalyticsHandler,
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ object TangemSdk {
|
|||
is TangemSdkError.Busy -> TangemSdkError.Busy()
|
||||
is TangemSdkError.MissingPreflightRead -> TangemSdkError.MissingPreflightRead()
|
||||
is TangemSdkError.WrongCardNumber -> TangemSdkError.WrongCardNumber()
|
||||
is TangemSdkError.WrongCardType -> TangemSdkError.WrongCardType()
|
||||
is TangemSdkError.WrongCardType -> TangemSdkError.WrongCardType(null)
|
||||
is TangemSdkError.CardError -> TangemSdkError.CardError()
|
||||
is TangemSdkError.NotSupportedFirmwareVersion -> TangemSdkError.NotSupportedFirmwareVersion()
|
||||
is TangemSdkError.WalletError -> TangemSdkError.WalletError()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,25 @@
|
|||
package com.tangem.tap.common.compose
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
|
@ -20,7 +34,7 @@ import com.tangem.domain.DomainDialog
|
|||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.domain.redux.global.DomainGlobalAction
|
||||
import com.tangem.domain.redux.global.DomainGlobalState
|
||||
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
|
||||
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
|
||||
import com.tangem.tap.features.tokens.addCustomToken.compose.SelectTokenNetworkDialog
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
|
@ -61,7 +75,7 @@ private fun ShowTheDialog(dialogState: MutableState<DomainDialog?>) {
|
|||
when (val dialog = dialogState.value) {
|
||||
is DomainDialog.DialogError -> ErrorDialog(
|
||||
title = stringResource(id = R.string.common_error),
|
||||
body = errorConverter.convert(dialog.error),
|
||||
body = errorConverter.convert(dialog.error).message,
|
||||
onDismissRequest
|
||||
)
|
||||
is DomainDialog.SelectTokenDialog -> SelectTokenNetworkDialog(dialog, onDismissRequest)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
package com.tangem.tap.common.compose
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
|
@ -26,7 +31,7 @@ import com.tangem.common.module.ModuleError
|
|||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.tap.common.CompositionLogger
|
||||
import com.tangem.tap.common.compose.extensions.stringResourceDefault
|
||||
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
|
||||
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -67,7 +72,7 @@ fun OutlinedTextFieldWidget(
|
|||
debounce = debounceTextChanges,
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
onTextChanged = onTextChanged
|
||||
onTextChanged = onTextChanged,
|
||||
)
|
||||
errorConverter?.let { AnimatedErrorView(error, it) }
|
||||
}
|
||||
|
|
@ -105,7 +110,8 @@ private fun OutlinedProgressTextField(
|
|||
onValueChanged = {
|
||||
logger.log("DEBOUNCER: onValueChanged: >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> dispatch.toStore([$it])")
|
||||
onTextChanged(it)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
logger.log("RECOMPOSE ---------------------------------------------------------------START [${logger.count}]")
|
||||
logger.log("RECOMPOSE --data: fieldData.value: [${fieldData}]")
|
||||
|
|
@ -188,7 +194,10 @@ private fun AnimatedErrorView(
|
|||
exit = slideOutVertically() + fadeOut(),
|
||||
) {
|
||||
error?.let {
|
||||
ErrorView(errorConverter.convert(it), style = TextStyle(fontSize = 14.sp))
|
||||
ErrorView(
|
||||
text = errorConverter.convert(it).message,
|
||||
style = TextStyle(fontSize = 14.sp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -203,15 +212,13 @@ fun OutlinedTextFieldWithErrorTest() {
|
|||
override val code: Int = 1,
|
||||
override val message: String = "Error message",
|
||||
override val data: Any? = null,
|
||||
) : ModuleError
|
||||
) : ModuleError()
|
||||
|
||||
val modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
Scaffold(
|
||||
) {
|
||||
Column(
|
||||
) {
|
||||
Scaffold {
|
||||
Column {
|
||||
OutlinedTextFieldWidget(
|
||||
modifier = modifier,
|
||||
fieldData = Field.Data("", false),
|
||||
|
|
|
|||
172
app/src/main/java/com/tangem/tap/common/compose/PinCodeWidget.kt
Normal file
172
app/src/main/java/com/tangem/tap/common/compose/PinCodeWidget.kt
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
package com.tangem.tap.common.compose
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.text.isDigitsOnly
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun PinCodeWidget(
|
||||
config: PinViewConfig = tangemPinConfig,
|
||||
onPinChanged: (String, Boolean) -> Unit = { pin, isLastSymbolEntered -> },
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
val rTextFieldValue = remember { mutableStateOf(TextFieldValue("")) }
|
||||
val indexedSymbols: List<String?> = createPinSymbolsList(config.pinsCount, rTextFieldValue.value.text)
|
||||
|
||||
fun isLastSymbolEntered(): Boolean = rTextFieldValue.value.text.length == config.pinsCount
|
||||
|
||||
fun handleOnTextFieldValueChanged(value: TextFieldValue) {
|
||||
if (!value.text.isDigitsOnly()) return
|
||||
|
||||
if (value.text.length <= config.pinsCount) {
|
||||
rTextFieldValue.value = value
|
||||
onPinChanged(value.text, isLastSymbolEntered())
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = config.modifier
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures {
|
||||
focusRequester.requestFocus()
|
||||
keyboardController?.show()
|
||||
}
|
||||
},
|
||||
) {
|
||||
Row {
|
||||
for (index in 0 until config.pinsCount) {
|
||||
PinElement(
|
||||
config = config,
|
||||
pinSymbol = indexedSymbols[index] ?: "",
|
||||
)
|
||||
}
|
||||
}
|
||||
TextField(
|
||||
modifier = Modifier
|
||||
.alpha(0f)
|
||||
.size(1.dp)
|
||||
.align(Alignment.Center)
|
||||
.focusRequester(focusRequester),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Decimal,
|
||||
imeAction = if (isLastSymbolEntered()) ImeAction.Done else ImeAction.Next,
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = { keyboardController?.hide() },
|
||||
),
|
||||
value = rTextFieldValue.value,
|
||||
onValueChange = ::handleOnTextFieldValueChanged,
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PinElement(
|
||||
config: PinViewConfig,
|
||||
pinSymbol: String,
|
||||
) {
|
||||
Box(Modifier.padding(config.pinBoxPadding)) {
|
||||
Box(config.pinBoxModifier) {
|
||||
Text(
|
||||
text = pinSymbol,
|
||||
modifier = config.pinTextModifier.align(Alignment.Center),
|
||||
style = config.pinsTextStyle ?: LocalTextStyle.current,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HiddenInputField() {
|
||||
}
|
||||
|
||||
private fun createPinSymbolsList(size: Int, text: String): List<String?> = List(size) {
|
||||
try {
|
||||
text[it].toString()
|
||||
} catch (ex: IndexOutOfBoundsException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
data class PinViewConfig(
|
||||
val modifier: Modifier = Modifier,
|
||||
val pinBoxModifier: Modifier = Modifier,
|
||||
val pinBoxPadding: Dp = 0.dp,
|
||||
val pinTextModifier: Modifier = Modifier,
|
||||
val pinsCount: Int = 4,
|
||||
val pinsTextStyle: TextStyle? = null,
|
||||
)
|
||||
|
||||
private val tangemPinConfig = PinViewConfig(
|
||||
modifier = Modifier
|
||||
.wrapContentSize(),
|
||||
pinBoxModifier = Modifier
|
||||
.width(42.dp)
|
||||
.height(56.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color(0xFFF0F0F0)),
|
||||
pinBoxPadding = 6.dp,
|
||||
pinTextModifier = Modifier,
|
||||
pinsCount = 4,
|
||||
pinsTextStyle = TextStyle(
|
||||
fontWeight = FontWeight(500),
|
||||
fontSize = 24.sp,
|
||||
),
|
||||
)
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PinCodeWidgetPreview() {
|
||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
PinCodeWidget(tangemPinConfig)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.common.module.ModuleMessage
|
||||
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
|
||||
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
|
|
@ -42,7 +42,7 @@ fun AddCustomTokenWarning(
|
|||
)
|
||||
SpacerH8()
|
||||
Text(
|
||||
text = converter.convert(warning),
|
||||
text = converter.convert(warning).message,
|
||||
color = colorResource(id = R.color.white),
|
||||
fontSize = 13.sp,
|
||||
lineHeight = 18.sp
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ fun Blockchain.getRoundIconRes(): Int {
|
|||
Blockchain.Gnosis -> R.drawable.ic_gnosis_round
|
||||
Blockchain.EthereumPow, Blockchain.EthereumPowTestnet -> R.drawable.ic_ethereumpow_round
|
||||
Blockchain.EthereumFair -> R.drawable.ic_ethereumfair_round
|
||||
Blockchain.Polkadot, Blockchain.PolkadotTestnet -> R.drawable.ic_polkadot_round
|
||||
Blockchain.Kusama -> R.drawable.ic_kusama_round
|
||||
Blockchain.Optimism, Blockchain.OptimismTestnet -> R.drawable.ic_optimism
|
||||
Blockchain.Dash -> R.drawable.ic_dash
|
||||
else -> R.drawable.ic_tangem_logo
|
||||
}
|
||||
}
|
||||
|
|
@ -59,6 +63,10 @@ fun Blockchain.getGreyedOutIconRes(): Int {
|
|||
Blockchain.Gnosis -> R.drawable.ic_gnosis_no_color
|
||||
Blockchain.EthereumPow, Blockchain.EthereumPowTestnet -> R.drawable.ic_ethereumpow_no_color
|
||||
Blockchain.EthereumFair -> R.drawable.ic_ethereumfair_no_color
|
||||
Blockchain.Polkadot, Blockchain.PolkadotTestnet -> R.drawable.ic_polkadot_no_color
|
||||
Blockchain.Kusama -> R.drawable.ic_kusama_no_color
|
||||
Blockchain.Optimism, Blockchain.OptimismTestnet -> R.drawable.ic_optimism_no_color
|
||||
Blockchain.Dash -> R.drawable.ic_dash_no_color
|
||||
else -> R.drawable.ic_tangem_logo
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
fun Context.readFile(fileName: String): String =
|
||||
this.openFileInput(fileName).bufferedReader().readText()
|
||||
|
|
@ -13,4 +15,8 @@ fun Context.rewriteFile(content: String, fileName: String) {
|
|||
|
||||
fun Context.readAssetAsString(fileName: String): String {
|
||||
return this.assets.open("$fileName.json").bufferedReader().readText()
|
||||
}
|
||||
|
||||
fun Context.isPermissionGranted(permission: String): Boolean {
|
||||
return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
|
@ -13,16 +13,57 @@ import kotlinx.coroutines.launch
|
|||
import org.rekotlin.Action
|
||||
import org.rekotlin.Store
|
||||
|
||||
/**
|
||||
* Dispatch action with creating the new coroutine with the Main dispatcher
|
||||
*/
|
||||
fun Store<*>.dispatchOnMain(action: Action) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
store.dispatch(action)
|
||||
}
|
||||
}
|
||||
|
||||
fun Store<*>.dispatchNotification(resId: Int) {
|
||||
dispatchOnMain(GlobalAction.ShowNotification(resId))
|
||||
}
|
||||
|
||||
fun Store<*>.dispatchToastNotification(resId: Int) {
|
||||
dispatchOnMain(GlobalAction.ShowToastNotification(resId))
|
||||
}
|
||||
|
||||
fun Store<*>.dispatchErrorNotification(error: TapError) {
|
||||
dispatchOnMain(GlobalAction.ShowErrorNotification(error))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param fatal used to indicate errors that should not normally occur
|
||||
*/
|
||||
fun Store<*>.dispatchDebugErrorNotification(message: String, fatal: Boolean = false) {
|
||||
val prefix = if (fatal) "FATAL ERROR: " else "DEBUG ERROR: "
|
||||
dispatchDebugErrorNotification(TapError.CustomError("$prefix $message"))
|
||||
}
|
||||
|
||||
fun Store<*>.dispatchDebugErrorNotification(error: TapError) {
|
||||
dispatchOnMain(GlobalAction.DebugShowErrorNotification(error))
|
||||
}
|
||||
|
||||
fun Store<*>.dispatchDialogShow(dialog: StateDialog) {
|
||||
dispatchOnMain(GlobalAction.ShowDialog(dialog))
|
||||
}
|
||||
|
||||
fun Store<*>.dispatchDialogHide() {
|
||||
dispatchOnMain(GlobalAction.HideDialog)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch action inside a coroutine with the Main dispatcher
|
||||
*/
|
||||
suspend fun dispatchOnMain(vararg actions: Action) {
|
||||
withMainContext { actions.forEach { store.dispatch(it) } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch action
|
||||
*/
|
||||
suspend fun Store<*>.onCardScanned(scanResponse: ScanResponse) {
|
||||
store.state.globalState.tapWalletManager.onCardScanned(scanResponse)
|
||||
}
|
||||
|
|
@ -33,49 +74,4 @@ fun Store<*>.dispatchOpenUrl(url: String) {
|
|||
|
||||
fun Store<*>.dispatchShare(url: String) {
|
||||
store.dispatch(NavigationAction.Share(url))
|
||||
}
|
||||
|
||||
fun Store<*>.dispatchNotification(resId: Int) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
store.dispatch(GlobalAction.ShowNotification(resId))
|
||||
}
|
||||
}
|
||||
|
||||
fun Store<*>.dispatchToastNotification(resId: Int) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
store.dispatch(GlobalAction.ShowToastNotification(resId))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun Store<*>.dispatchErrorNotification(error: TapError) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
store.dispatch(GlobalAction.ShowErrorNotification(error))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param fatal used to indicate errors that should not normally have occurred
|
||||
*/
|
||||
fun Store<*>.dispatchDebugErrorNotification(message: String, fatal: Boolean = false) {
|
||||
val prefix = if (fatal) "FATAL ERROR: " else "DEBUG ERROR: "
|
||||
dispatchDebugErrorNotification(TapError.CustomError("$prefix $message"))
|
||||
}
|
||||
|
||||
fun Store<*>.dispatchDebugErrorNotification(error: TapError) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
store.dispatch(GlobalAction.DebugShowErrorNotification(error))
|
||||
}
|
||||
}
|
||||
|
||||
fun Store<*>.dispatchDialogShow(dialog: StateDialog) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
store.dispatch(GlobalAction.ShowDialog(dialog))
|
||||
}
|
||||
}
|
||||
|
||||
fun Store<*>.dispatchDialogHide() {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
store.dispatch(GlobalAction.HideDialog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,12 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.*
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
|
|
@ -21,7 +26,6 @@ import androidx.fragment.app.Fragment
|
|||
import com.google.android.material.card.MaterialCardView
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
|
||||
|
||||
fun Fragment.getDrawable(@DrawableRes drawableResId: Int): Drawable? {
|
||||
return ContextCompat.getDrawable(requireContext(), drawableResId)
|
||||
}
|
||||
|
|
@ -53,6 +57,10 @@ fun View.getString(@StringRes id: Int, vararg formatArgs: String): String {
|
|||
return context.getString(id, *formatArgs)
|
||||
}
|
||||
|
||||
fun View.getQuantityString(@StringRes id: Int, quantity: Int): String {
|
||||
return context.resources.getQuantityString(id, quantity, quantity)
|
||||
}
|
||||
|
||||
fun View.getResourceName(): String {
|
||||
return try {
|
||||
resources.getResourceEntryName(id)
|
||||
|
|
@ -89,15 +97,13 @@ fun View.invisible(invisible: Boolean = true, invokeBeforeStateChanged: (() -> U
|
|||
} else {
|
||||
this.show(invokeBeforeStateChanged)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun Context.dpToPixels(dp: Int): Int =
|
||||
TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics
|
||||
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics,
|
||||
).toInt()
|
||||
|
||||
|
||||
fun Context.pixelsToDp(pixels: Int): Int {
|
||||
return (pixels.toFloat() /
|
||||
(resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT))
|
||||
|
|
@ -107,7 +113,6 @@ fun Context.pixelsToDp(pixels: Int): Int {
|
|||
fun Context.dpToPixels(dp: Float): Float =
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, this.resources.displayMetrics)
|
||||
|
||||
|
||||
fun Context.pixelsToDp(pixels: Float): Float =
|
||||
(pixels / (resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT))
|
||||
|
||||
|
|
@ -118,14 +123,14 @@ fun MaterialCardView.setMargins(
|
|||
marginLeftDp: Int = 16,
|
||||
marginTopDp: Int = 8,
|
||||
marginRightDp: Int = 16,
|
||||
marginBottomDp: Int = 8
|
||||
marginBottomDp: Int = 8,
|
||||
) {
|
||||
val params = this.layoutParams
|
||||
(params as ViewGroup.MarginLayoutParams).setMargins(
|
||||
context.dpToPixels(marginLeftDp),
|
||||
context.dpToPixels(marginTopDp),
|
||||
context.dpToPixels(marginRightDp),
|
||||
context.dpToPixels(marginBottomDp)
|
||||
context.dpToPixels(marginBottomDp),
|
||||
)
|
||||
this.layoutParams = params
|
||||
}
|
||||
|
|
@ -144,7 +149,8 @@ fun Activity.setSystemBarTextColor(setTextDark: Boolean) {
|
|||
}
|
||||
|
||||
fun View.hideKeyboard() {
|
||||
val inputMethodManager = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
val inputMethodManager =
|
||||
context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
inputMethodManager?.hideSoftInputFromWindow(this.windowToken, 0)
|
||||
}
|
||||
|
||||
|
|
@ -182,7 +188,7 @@ fun Context.safeStartActivity(
|
|||
intent: Intent,
|
||||
options: Bundle? = null,
|
||||
fallback: ((ActivityNotFoundException) -> Unit)? = null,
|
||||
finally: VoidCallback? = null
|
||||
finally: VoidCallback? = null,
|
||||
) {
|
||||
try {
|
||||
this.startActivity(intent, options)
|
||||
|
|
@ -200,7 +206,7 @@ fun View.getString(resId: Int, vararg formatArgs: Any?): String {
|
|||
fun View.animateVisibility(
|
||||
show: Boolean,
|
||||
durationMillis: Long = SHORT_ANIMATION_DURATION,
|
||||
hiddenVisibility: Int = View.GONE
|
||||
hiddenVisibility: Int = View.GONE,
|
||||
) {
|
||||
if (show) {
|
||||
this.animate()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
|
|
@ -69,4 +70,12 @@ fun WalletManager?.getAddressData(): AddressData? {
|
|||
val addressDataList = wallet.createAddressesData()
|
||||
return if (addressDataList.isEmpty()) null
|
||||
else addressDataList[0]
|
||||
}
|
||||
|
||||
fun<T> WalletManager.Companion.stub(): T {
|
||||
val wallet = Wallet(Blockchain.Unknown, setOf(), Wallet.PublicKey(byteArrayOf(), null, null), setOf())
|
||||
return object : WalletManager(wallet) {
|
||||
override val currentHost: String = ""
|
||||
override suspend fun update() {}
|
||||
} as T
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
import android.webkit.WebView
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
fun WebView.configureSettings() {
|
||||
settings.apply {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
fun WebView.stop() {
|
||||
stopLoading()
|
||||
pauseTimers()
|
||||
destroy()
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
package com.tangem.tap.common.feedback
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
|
|
@ -9,16 +7,19 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.common.card.CardWallet
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
|
||||
class AdditionalFeedbackInfo {
|
||||
class EmailWalletInfo(
|
||||
var blockchain: Blockchain = Blockchain.Unknown,
|
||||
var address: String = "",
|
||||
var explorerLink: String = "",
|
||||
var host: String = "",
|
||||
var derivationPath: String = "",
|
||||
var outputsCount: String? = null,
|
||||
var host: String = "",
|
||||
var addresses: String = "",
|
||||
var explorerLink: String = "",
|
||||
)
|
||||
|
||||
var appVersion: String = ""
|
||||
|
|
@ -45,37 +46,19 @@ class AdditionalFeedbackInfo {
|
|||
var fee: String = ""
|
||||
var token: String = ""
|
||||
|
||||
fun setAppVersion(context: Context) {
|
||||
try {
|
||||
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
appVersion = pInfo.versionName
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun setCardInfo(data: ScanResponse) {
|
||||
cardId = data.card.cardId
|
||||
cardBlockchain = data.walletData?.blockchain ?: ""
|
||||
cardFirmwareVersion = data.card.firmwareVersion.stringValue
|
||||
cardIssuer = data.card.issuer.name
|
||||
signedHashesCount = data.card.wallets
|
||||
.joinToString("; ") { "${it.curve.curve} - ${it.totalSignedHashes}" }
|
||||
signedHashesCount = formatSignedHashes(data.card.wallets)
|
||||
}
|
||||
|
||||
fun setWalletsInfo(walletManagers: List<WalletManager>) {
|
||||
walletsInfo.clear()
|
||||
tokens.clear()
|
||||
walletManagers.forEach { manager ->
|
||||
walletsInfo.add(
|
||||
EmailWalletInfo(
|
||||
blockchain = manager.wallet.blockchain,
|
||||
address = getAddress(manager.wallet),
|
||||
explorerLink = getExploreUri(manager.wallet),
|
||||
host = manager.currentHost,
|
||||
derivationPath = manager.wallet.publicKey.derivationPath?.rawPath ?: ""
|
||||
)
|
||||
)
|
||||
walletsInfo.add(createEmailWalletInfo(manager))
|
||||
if (manager.cardTokens.isNotEmpty()) {
|
||||
tokens[manager.wallet.blockchain] = manager.cardTokens
|
||||
}
|
||||
|
|
@ -83,45 +66,55 @@ class AdditionalFeedbackInfo {
|
|||
}
|
||||
|
||||
fun updateOnSendError(
|
||||
wallet: Wallet,
|
||||
host: String,
|
||||
walletManager: WalletManager,
|
||||
amountToSend: Amount,
|
||||
feeAmount: Amount,
|
||||
destinationAddress: String,
|
||||
) {
|
||||
onSendErrorWalletInfo = EmailWalletInfo(
|
||||
blockchain = wallet.blockchain,
|
||||
address = getAddress(wallet),
|
||||
explorerLink = getExploreUri(wallet),
|
||||
host = host,
|
||||
derivationPath = wallet.publicKey.derivationPath?.rawPath ?: ""
|
||||
)
|
||||
|
||||
onSendErrorWalletInfo = createEmailWalletInfo(walletManager)
|
||||
this.destinationAddress = destinationAddress
|
||||
amount = amountToSend.value?.stripZeroPlainString() ?: "0"
|
||||
fee = feeAmount.value?.stripZeroPlainString() ?: "0"
|
||||
token = if (amountToSend.type is AmountType.Token) amountToSend.currencySymbol else ""
|
||||
}
|
||||
|
||||
private fun getAddress(wallet: Wallet): String {
|
||||
return if (wallet.addresses.size == 1) {
|
||||
wallet.address
|
||||
} else {
|
||||
val addresses = wallet.addresses.joinToString(", ") {
|
||||
"${it.type.javaClass.simpleName} - ${it.value}"
|
||||
}
|
||||
"Multiple address: $addresses"
|
||||
private fun createEmailWalletInfo(walletManager: WalletManager): EmailWalletInfo {
|
||||
return EmailWalletInfo(
|
||||
blockchain = walletManager.wallet.blockchain,
|
||||
derivationPath = walletManager.wallet.publicKey.derivationPath?.rawPath ?: "",
|
||||
outputsCount = walletManager.outputsCount?.toString(),
|
||||
host = walletManager.currentHost,
|
||||
addresses = formatAddresses(walletManager.wallet),
|
||||
explorerLink = formatExploreUrls(walletManager.wallet),
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatSignedHashes(wallets: List<CardWallet>): String {
|
||||
return wallets.joinToString("\n") { "Signed hashes: ${it.curve.curve} - ${it.totalSignedHashes}" }
|
||||
}
|
||||
|
||||
private fun formatAddresses(wallet: Wallet): String {
|
||||
return wallet.formatAddressWith("Multiple address:") {
|
||||
"${it.name} - ${it.value}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getExploreUri(wallet: Wallet): String {
|
||||
return if (wallet.addresses.size == 1) {
|
||||
wallet.getExploreUrl(wallet.address)
|
||||
} else {
|
||||
val links = wallet.addresses.joinToString(", ") {
|
||||
"${it.type.javaClass.simpleName} - ${wallet.getExploreUrl(it.value)}"
|
||||
}
|
||||
"Multiple explorers links: $links"
|
||||
private fun formatExploreUrls(wallet: Wallet): String {
|
||||
return wallet.formatAddressWith("Multiple explorers links:") {
|
||||
"${it.name} - ${wallet.getExploreUrl(it.value)}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun Wallet.formatAddressWith(with: String, mapAddress: (Address) -> String): String {
|
||||
return if (addresses.size == 1) {
|
||||
getExploreUrl(address)
|
||||
} else {
|
||||
addresses.map { mapAddress(it) }.toMutableList()
|
||||
.apply { add(0, with) }
|
||||
.joinToString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
private val Address.name: String
|
||||
get() = type.javaClass.simpleName
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ package com.tangem.tap.common.feedback
|
|||
import com.tangem.tap.common.extensions.breakLine
|
||||
|
||||
class FeedbackDataBuilder(
|
||||
private val infoHolder: AdditionalFeedbackInfo
|
||||
private val infoHolder: AdditionalFeedbackInfo,
|
||||
) {
|
||||
val builder = StringBuilder()
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ class FeedbackDataBuilder(
|
|||
builder.appendKeyValue("Card ID", infoHolder.cardId)
|
||||
builder.appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
|
||||
builder.appendKeyValue("Card Blockchain", infoHolder.cardBlockchain)
|
||||
builder.appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
|
||||
builder.appendKeyValue("", infoHolder.signedHashesCount)
|
||||
return this
|
||||
}
|
||||
|
||||
|
|
@ -29,20 +29,22 @@ class FeedbackDataBuilder(
|
|||
infoHolder.walletsInfo.forEach {
|
||||
builder.appendDelimiter()
|
||||
builder.appendKeyValue("Blockchain", it.blockchain.fullName)
|
||||
builder.appendKeyValue("Host", it.host)
|
||||
builder.appendKeyValue("Wallet address", it.address)
|
||||
builder.appendKeyValue("Derivation path", it.derivationPath)
|
||||
builder.appendKeyValue("Explorer link", it.explorerLink)
|
||||
builder.appendKeyValue("Outputs count", it.outputsCount)
|
||||
|
||||
infoHolder.tokens[it.blockchain]?.let { tokens ->
|
||||
builder.append("Tokens:")
|
||||
breakLine()
|
||||
tokens.forEach { token ->
|
||||
builder.appendKeyValue("Name", token.name)
|
||||
builder.appendKeyValue("ID", token.id ?: "[custom token]")
|
||||
builder.appendKeyValue("Name", token.name)
|
||||
builder.appendKeyValue("Contract address", token.contractAddress)
|
||||
}
|
||||
}
|
||||
|
||||
builder.appendKeyValue("Host", it.host)
|
||||
builder.appendKeyValue("Wallet address", it.addresses)
|
||||
builder.appendKeyValue("Explorer link", it.explorerLink)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
|
@ -55,7 +57,7 @@ class FeedbackDataBuilder(
|
|||
builder.appendKeyValue("Token", infoHolder.token)
|
||||
builder.appendKeyValue("Error", error)
|
||||
builder.appendDelimiter()
|
||||
builder.appendKeyValue("Source address", walletInfo.address)
|
||||
builder.appendKeyValue("Source address", walletInfo.addresses)
|
||||
builder.appendKeyValue("Destination address", infoHolder.destinationAddress)
|
||||
builder.appendKeyValue("Amount", infoHolder.amount)
|
||||
builder.appendKeyValue("Fee", infoHolder.fee)
|
||||
|
|
@ -72,8 +74,10 @@ class FeedbackDataBuilder(
|
|||
fun build(): String = builder.toString()
|
||||
}
|
||||
|
||||
private fun StringBuilder.appendKeyValue(key: String, value: String): StringBuilder {
|
||||
return if (value.isNotBlank()) this.append("$key: $value\n") else this
|
||||
private fun StringBuilder.appendKeyValue(key: String, value: String?): StringBuilder = when {
|
||||
value.isNullOrBlank() -> this
|
||||
key.isBlank() -> this.append("$value\n")
|
||||
else -> this.append("$key: $value\n")
|
||||
}
|
||||
|
||||
private fun StringBuilder.appendDelimiter(): StringBuilder = append("----------\n")
|
||||
|
|
@ -7,11 +7,9 @@ import com.tangem.tap.common.extensions.sendEmail
|
|||
import com.tangem.tap.common.log.TangemLogCollector
|
||||
import com.tangem.tap.common.zendesk.ZendeskConfig
|
||||
import com.tangem.tap.foregroundActivityObserver
|
||||
import com.tangem.tap.logConfig
|
||||
import com.tangem.tap.persistence.PreferencesStorage
|
||||
import com.tangem.tap.withForegroundActivity
|
||||
import com.tangem.wallet.R
|
||||
import com.zendesk.logger.Logger
|
||||
import timber.log.Timber
|
||||
import zendesk.chat.Chat
|
||||
import zendesk.chat.ChatConfiguration
|
||||
|
|
@ -32,18 +30,16 @@ class FeedbackManager(
|
|||
private val logCollector: TangemLogCollector,
|
||||
private val preferencesStorage: PreferencesStorage,
|
||||
) {
|
||||
fun initChat(
|
||||
context: Context,
|
||||
zendeskConfig: ZendeskConfig,
|
||||
) {
|
||||
Chat.INSTANCE.init(
|
||||
context,
|
||||
zendeskConfig.accountKey,
|
||||
zendeskConfig.appId
|
||||
)
|
||||
private var lastUsedConfigForInitialization: ZendeskConfig? = null
|
||||
|
||||
// Zendesk logs
|
||||
Logger.setLoggable(logConfig.zendesk)
|
||||
var chatInitializer: ((ZendeskConfig) -> Unit)? = null
|
||||
|
||||
fun initChat(zendeskConfig: ZendeskConfig) {
|
||||
// prevent double initialization with the same config
|
||||
if (lastUsedConfigForInitialization == zendeskConfig) return
|
||||
|
||||
lastUsedConfigForInitialization = zendeskConfig
|
||||
chatInitializer?.invoke(zendeskConfig)
|
||||
}
|
||||
|
||||
fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) {
|
||||
|
|
@ -55,7 +51,7 @@ class FeedbackManager(
|
|||
subject = activity.getString(feedbackData.subjectResId),
|
||||
message = feedbackData.joinTogether(activity, infoHolder),
|
||||
file = fileLog,
|
||||
onFail = onFail
|
||||
onFail = onFail,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import android.content.Context
|
|||
import android.util.Log
|
||||
import coil.ImageLoader
|
||||
import coil.util.Logger
|
||||
import com.tangem.tap.logConfig
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import timber.log.Timber
|
||||
|
|
@ -12,11 +11,12 @@ import timber.log.Timber
|
|||
private const val COIL_LOG_TAG = "COIL"
|
||||
|
||||
fun createCoilImageLoader(
|
||||
context: Context
|
||||
context: Context,
|
||||
logEnabled: Boolean = false,
|
||||
): ImageLoader {
|
||||
return ImageLoader.Builder(context)
|
||||
.apply {
|
||||
if (!logConfig.coil) return@apply
|
||||
if (!logEnabled) return@apply
|
||||
|
||||
logger(CoilTimberLogger())
|
||||
okHttpClient {
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
package com.tangem.tap.common.moduleMessage
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.common.module.ModuleMessage
|
||||
import com.tangem.common.module.ModuleMessageConverter
|
||||
import com.tangem.domain.DomainModuleMessage
|
||||
import com.tangem.tap.common.moduleMessage.domain.DomainMessageConverter
|
||||
|
||||
class ModuleMessageConverter(
|
||||
private val context: Context
|
||||
) : ModuleMessageConverter<ModuleMessage, String> {
|
||||
|
||||
override fun convert(message: ModuleMessage): String {
|
||||
val convertedMessage = when (message) {
|
||||
is DomainModuleMessage -> DomainMessageConverter(context).convert(message)
|
||||
else -> null
|
||||
}
|
||||
return convertedMessage ?: convertUnknownMessage(message)
|
||||
}
|
||||
|
||||
private fun convertUnknownMessage(message: ModuleMessage): String {
|
||||
return "Unknown message: ${message::class.java.simpleName}"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.tap.common.moduleMessage.domain
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.common.module.ModuleMessageConverter
|
||||
import com.tangem.domain.DomainError
|
||||
import com.tangem.domain.DomainModuleMessage
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class DomainMessageConverter(
|
||||
private val context: Context
|
||||
) : ModuleMessageConverter<DomainModuleMessage, String?> {
|
||||
override fun convert(message: DomainModuleMessage): String? {
|
||||
return when (message) {
|
||||
is DomainError -> DomainErrorConverter(context).convert(message)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
|
|||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayMiddleware
|
||||
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
|
||||
import com.tangem.tap.features.send.redux.states.SendState
|
||||
import com.tangem.tap.features.shop.redux.ShopMiddleware
|
||||
|
|
@ -66,6 +67,7 @@ data class AppState(
|
|||
HomeMiddleware.handler,
|
||||
OnboardingNoteMiddleware.handler,
|
||||
OnboardingWalletMiddleware.handler,
|
||||
OnboardingSaltPayMiddleware.handler,
|
||||
OnboardingOtherCardsMiddleware.handler,
|
||||
WalletMiddleware().walletMiddleware,
|
||||
TwinCardsMiddleware.handler,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.tap.common.redux
|
||||
|
||||
import com.tangem.tap.logConfig
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
|
||||
|
|
@ -10,7 +10,10 @@ import timber.log.Timber
|
|||
val logMiddleware: Middleware<AppState> = { dispatch, appState ->
|
||||
{ nextDispatch ->
|
||||
{ action ->
|
||||
if (logConfig.storeAction) Timber.d("Dispatch action: $action")
|
||||
if (LogConfig.storeAction) {
|
||||
Timber.d("Dispatch action: $action")
|
||||
// printOnboardingWalletState()
|
||||
}
|
||||
nextDispatch(action)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.tangem.tap.common.redux.ErrorAction
|
|||
import com.tangem.tap.common.redux.NotificationAction
|
||||
import com.tangem.tap.common.redux.StateDialog
|
||||
import com.tangem.tap.common.redux.ToastNotificationAction
|
||||
import com.tangem.tap.common.zendesk.ZendeskConfig
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.configurable.config.ConfigManager
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||
|
|
@ -36,9 +37,10 @@ sealed class GlobalAction : Action {
|
|||
data class ShowDialog(val stateDialog: StateDialog) : GlobalAction()
|
||||
object HideDialog : GlobalAction()
|
||||
|
||||
sealed class Onboarding {
|
||||
data class Start(val scanResponse: ScanResponse?, val fromHomeScreen: Boolean = true) : GlobalAction()
|
||||
object Stop : GlobalAction()
|
||||
sealed class Onboarding : GlobalAction() {
|
||||
data class Start(val scanResponse: ScanResponse, val canSkipBackup: Boolean = true) : Onboarding()
|
||||
data class StartForUnfinishedBackup(val isSaltPayBackup: Boolean) : Onboarding()
|
||||
object Stop : Onboarding()
|
||||
}
|
||||
|
||||
data class ScanCard(
|
||||
|
|
@ -78,7 +80,7 @@ sealed class GlobalAction : Action {
|
|||
data class SetAnanlyticHandlers(val analyticsHandlers: GlobalAnalyticsHandler) : GlobalAction()
|
||||
|
||||
data class SendEmail(val feedbackData: FeedbackData) : GlobalAction()
|
||||
data class OpenChat(val feedbackData: FeedbackData) : GlobalAction()
|
||||
data class OpenChat(val feedbackData: FeedbackData, val zendeskConfig: ZendeskConfig? = null) : GlobalAction()
|
||||
data class UpdateFeedbackInfo(val walletManagers: List<WalletManager>) : GlobalAction()
|
||||
|
||||
object ExchangeManager : GlobalAction() {
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ import com.tangem.common.core.TangemSdkError
|
|||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.extensions.ifNotNull
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.currenciesRepository
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||
import com.tangem.tap.features.send.redux.SendAction
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
|
|
@ -24,6 +24,7 @@ import com.tangem.tap.preferencesStorage
|
|||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.tap.userTokensRepository
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.DispatchFunction
|
||||
|
|
@ -86,7 +87,30 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
|
|||
store.state.globalState.feedbackManager?.sendEmail(action.feedbackData)
|
||||
}
|
||||
is GlobalAction.OpenChat -> {
|
||||
store.state.globalState.feedbackManager?.openChat(action.feedbackData)
|
||||
val globalState = store.state.globalState
|
||||
val feedbackManager = globalState.feedbackManager.guard {
|
||||
store.dispatchDebugErrorNotification("FeedbackManager not initialized")
|
||||
return
|
||||
}
|
||||
val config = globalState.configManager?.config.guard {
|
||||
store.dispatchDebugErrorNotification("Config not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
val scanResponse = globalState.scanResponse ?: globalState.onboardingState.onboardingManager?.scanResponse
|
||||
|
||||
// if config not set -> try to get it based on a scanResponse.productType
|
||||
val unsafeZendeskConfig = action.zendeskConfig ?: when {
|
||||
scanResponse?.isSaltPay() == true -> config.saltPayConfig?.zendesk
|
||||
else -> config.zendesk
|
||||
}
|
||||
|
||||
val zendeskConfig = unsafeZendeskConfig.guard {
|
||||
store.dispatchDebugErrorNotification("ZendeskConfig not initialized")
|
||||
return
|
||||
}
|
||||
feedbackManager.initChat(zendeskConfig)
|
||||
feedbackManager.openChat(action.feedbackData)
|
||||
}
|
||||
is GlobalAction.UpdateWalletSignedHashes -> {
|
||||
store.dispatch(WalletAction.Warnings.CheckRemainingSignatures(action.remainingSignatures))
|
||||
|
|
@ -96,7 +120,8 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
|
|||
?.setWalletsInfo(action.walletManagers)
|
||||
}
|
||||
is GlobalAction.ExchangeManager.Init -> {
|
||||
val config = appState()?.globalState?.configManager?.config
|
||||
val appStateSafe = appState() ?: return
|
||||
val config = appStateSafe.globalState.configManager?.config
|
||||
ifNotNull(
|
||||
config?.mercuryoWidgetId,
|
||||
config?.mercuryoSecret,
|
||||
|
|
@ -108,8 +133,13 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
|
|||
apiVersion = MercuryoApi.API_VERSION,
|
||||
mercuryoWidgetId = mercuryoWidgetId,
|
||||
secret = mercuryoSecret,
|
||||
logEnabled = LogConfig.network.mercuryoService,
|
||||
)
|
||||
val sellService = MoonPayService(
|
||||
apiKey = moonPayKey,
|
||||
secretKey = moonPaySecretKey,
|
||||
logEnabled = LogConfig.network.moonPayService,
|
||||
)
|
||||
val sellService = MoonPayService(moonPayKey, moonPaySecretKey)
|
||||
val cardProvider = { store.state.globalState.scanResponse?.card }
|
||||
|
||||
val exchangeManager = CurrencyExchangeManager(
|
||||
|
|
@ -134,7 +164,7 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
|
|||
scope.launch {
|
||||
val result = tangemSdkManager.scanProduct(
|
||||
store.state.globalState.analyticsHandlers,
|
||||
currenciesRepository,
|
||||
userTokensRepository,
|
||||
action.additionalBlockchainsToDerive,
|
||||
action.messageResId,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,14 +18,13 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
|
|||
globalState.copy(resources = action.resources)
|
||||
}
|
||||
is GlobalAction.Onboarding.Start -> {
|
||||
val onboardingManager = if (action.scanResponse != null) {
|
||||
val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage
|
||||
OnboardingManager(action.scanResponse, usedCardsPrefStorage)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage
|
||||
val onboardingManager = OnboardingManager(action.scanResponse, usedCardsPrefStorage)
|
||||
globalState.copy(onboardingState = OnboardingState(true, onboardingManager))
|
||||
}
|
||||
is GlobalAction.Onboarding.StartForUnfinishedBackup -> {
|
||||
globalState.copy(onboardingState = OnboardingState(true, null))
|
||||
}
|
||||
is GlobalAction.Onboarding.Stop -> {
|
||||
globalState.copy(onboardingState = OnboardingState(false))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,17 +11,17 @@ import com.tangem.tap.features.wallet.redux.ProgressState
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
open class IndeterminateProgressButtonWidget(
|
||||
private val button: MaterialButton,
|
||||
private val progress: View,
|
||||
initialState: ProgressState = ProgressState.Done
|
||||
private val button: MaterialButton,
|
||||
private val progress: View,
|
||||
initialState: ProgressState = ProgressState.Done,
|
||||
) : ViewStateWidget {
|
||||
|
||||
private var initialButtonText: CharSequence = button.text
|
||||
private var icon: Drawable? = null
|
||||
private var text: CharSequence = button.text
|
||||
private var icon: Drawable? = button.icon
|
||||
private var iconGravity: Int? = button.iconGravity
|
||||
|
||||
init {
|
||||
icon = button.icon
|
||||
changeState(initialState)
|
||||
if (initialState != ProgressState.Done) changeState(initialState)
|
||||
}
|
||||
|
||||
override val mainView: View = button
|
||||
|
|
@ -37,15 +37,18 @@ open class IndeterminateProgressButtonWidget(
|
|||
|
||||
protected open fun switchToNone() {
|
||||
button.isClickable = true
|
||||
button.text = text
|
||||
button.icon = icon
|
||||
button.text = initialButtonText
|
||||
iconGravity?.let { button.iconGravity = it }
|
||||
|
||||
progress.hide()
|
||||
}
|
||||
|
||||
protected open fun switchToProgress() {
|
||||
button.isClickable = false
|
||||
button.icon = null
|
||||
button.text = ""
|
||||
button.icon = null
|
||||
|
||||
progress.show()
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.tap.domain
|
|||
import CreateProductWalletTask
|
||||
import CreateProductWalletTaskResponse
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import com.tangem.Message
|
||||
import com.tangem.TangemSdk
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
|
|
@ -32,7 +33,7 @@ import com.tangem.tap.common.analytics.AnalyticsParam
|
|||
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
|
||||
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
|
||||
import com.tangem.tap.domain.tasks.product.ScanProductTask
|
||||
import com.tangem.tap.domain.tokens.CurrenciesRepository
|
||||
import com.tangem.tap.domain.tokens.UserTokensRepository
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -45,7 +46,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
|
||||
suspend fun scanProduct(
|
||||
analyticsHandler: AnalyticsHandler?,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
userTokensRepository: UserTokensRepository,
|
||||
additionalBlockchainsToDerive: Collection<Blockchain>? = null,
|
||||
messageRes: Int? = null,
|
||||
): CompletionResult<ScanResponse> {
|
||||
|
|
@ -53,38 +54,38 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
|
||||
val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header))
|
||||
return runTaskAsyncReturnOnMain(
|
||||
runnable = ScanProductTask(null, currenciesRepository, additionalBlockchainsToDerive),
|
||||
cardId = null, initialMessage = message
|
||||
runnable = ScanProductTask(null, userTokensRepository, additionalBlockchainsToDerive),
|
||||
cardId = null, initialMessage = message,
|
||||
).also { sendScanResultsToAnalytics(analyticsHandler, it) }
|
||||
}
|
||||
|
||||
suspend fun createProductWallet(
|
||||
scanResponse: ScanResponse
|
||||
scanResponse: ScanResponse,
|
||||
): CompletionResult<CreateProductWalletTaskResponse> {
|
||||
return runTaskAsync(
|
||||
CreateProductWalletTask(scanResponse.productType),
|
||||
scanResponse.card.cardId,
|
||||
Message(context.getString(R.string.initial_message_create_wallet_body))
|
||||
Message(context.getString(R.string.initial_message_create_wallet_body)),
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendScanResultsToAnalytics(
|
||||
analyticsHandler: AnalyticsHandler?,
|
||||
result: CompletionResult<ScanResponse>
|
||||
result: CompletionResult<ScanResponse>,
|
||||
) {
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
analyticsHandler?.triggerEvent(
|
||||
event = AnalyticsEvent.CARD_IS_SCANNED,
|
||||
card = result.data.card,
|
||||
blockchain = result.data.walletData?.blockchain
|
||||
blockchain = result.data.walletData?.blockchain,
|
||||
)
|
||||
if (DemoHelper.isDemoCard(result.data)) {
|
||||
analyticsHandler?.triggerEvent(
|
||||
event = AnalyticsEvent.DEMO_MODE_ACTIVATED,
|
||||
card = result.data.card,
|
||||
blockchain = result.data.walletData?.blockchain,
|
||||
params = mapOf(AnalyticsParam.CARD_ID.param to result.data.card.cardId)
|
||||
params = mapOf(AnalyticsParam.CARD_ID.param to result.data.card.cardId),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -92,20 +93,22 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
(result.error as? TangemSdkError)?.let { error ->
|
||||
analyticsHandler?.logCardSdkError(
|
||||
error = error,
|
||||
actionToLog = Analytics.ActionToLog.Scan
|
||||
actionToLog = Analytics.ActionToLog.Scan,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createWallet(cardId: String?): CompletionResult<Card> {
|
||||
return runTaskAsyncReturnOnMain(CreateWalletAndRescanTask(), cardId,
|
||||
initialMessage = Message(context.getString(R.string.initial_message_create_wallet_body)))
|
||||
return runTaskAsyncReturnOnMain(
|
||||
CreateWalletAndRescanTask(), cardId,
|
||||
initialMessage = Message(context.getString(R.string.initial_message_create_wallet_body)),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun derivePublicKeys(
|
||||
cardId: String,
|
||||
derivations: Map<ByteArrayKey, List<DerivationPath>>
|
||||
derivations: Map<ByteArrayKey, List<DerivationPath>>,
|
||||
): CompletionResult<DerivationTaskResponse> {
|
||||
return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId)
|
||||
}
|
||||
|
|
@ -114,49 +117,58 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
return runTaskAsyncReturnOnMain(
|
||||
ResetToFactorySettingsTask(),
|
||||
card.cardId,
|
||||
initialMessage = Message(context.getString(R.string.details_row_title_reset_factory_settings)))
|
||||
initialMessage = Message(context.getString(R.string.details_row_title_reset_factory_settings)),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun setPasscode(cardId: String?): CompletionResult<SuccessResponse> {
|
||||
return runTaskAsyncReturnOnMain(
|
||||
SetUserCodeCommand.changePasscode(null),
|
||||
cardId,
|
||||
initialMessage = Message(context.getString(R.string.initial_message_change_passcode_body)))
|
||||
initialMessage = Message(context.getString(R.string.initial_message_change_passcode_body)),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun setAccessCode(cardId: String?): CompletionResult<SuccessResponse> {
|
||||
return runTaskAsyncReturnOnMain(
|
||||
SetUserCodeCommand.changeAccessCode(null),
|
||||
cardId,
|
||||
initialMessage = Message(context.getString(R.string.initial_message_change_access_code_body)))
|
||||
initialMessage = Message(context.getString(R.string.initial_message_change_access_code_body)),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun setLongTap(cardId: String?): CompletionResult<SuccessResponse> {
|
||||
return runTaskAsyncReturnOnMain(
|
||||
SetUserCodeCommand.resetUserCodes(),
|
||||
cardId,
|
||||
initialMessage = Message(context.getString(R.string.initial_message_tap_header)))
|
||||
initialMessage = Message(context.getString(R.string.initial_message_tap_header)),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun checkUserCodes(cardId: String?): CompletionResult<CheckUserCodesResponse> {
|
||||
return runTaskAsyncReturnOnMain(
|
||||
CheckUserCodesCommand(),
|
||||
cardId,
|
||||
initialMessage = Message(context.getString(R.string.initial_message_tap_header)))
|
||||
initialMessage = Message(context.getString(R.string.initial_message_tap_header)),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun scanCard(): CompletionResult<Card> {
|
||||
return runTaskAsyncReturnOnMain(
|
||||
ScanTask(),
|
||||
initialMessage = Message(context.getString(R.string.initial_message_tap_header)))
|
||||
initialMessage = Message(context.getString(R.string.initial_message_tap_header)),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun <T : CommandResponse> runTaskAsync(
|
||||
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
|
||||
runnable: CardSessionRunnable<T>,
|
||||
cardId: String? = null,
|
||||
initialMessage: Message? = null,
|
||||
accessCode: String? = null,
|
||||
): CompletionResult<T> =
|
||||
withContext(Dispatchers.Main) {
|
||||
suspendCoroutine { continuation ->
|
||||
tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage) { result ->
|
||||
tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage, accessCode) { result ->
|
||||
if (continuation.context.isActive) continuation.resume(result)
|
||||
}
|
||||
}
|
||||
|
|
@ -170,20 +182,24 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
}
|
||||
|
||||
fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse) {
|
||||
tangemSdk.config.cardIdDisplayFormat = if (scanResponse.isTangemTwins()) {
|
||||
CardIdDisplayFormat.LastLuhn(4)
|
||||
} else {
|
||||
CardIdDisplayFormat.Full
|
||||
tangemSdk.config.cardIdDisplayFormat = when {
|
||||
scanResponse.isTangemTwins() -> CardIdDisplayFormat.LastLuhn(4)
|
||||
scanResponse.isSaltPay() -> CardIdDisplayFormat.None
|
||||
else -> CardIdDisplayFormat.Full
|
||||
}
|
||||
}
|
||||
|
||||
fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String {
|
||||
return context.getString(stringResId, formatArgs)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val config = Config(
|
||||
linkedTerminal = true,
|
||||
allowUntrustedCards = true,
|
||||
filter = CardFilter(
|
||||
allowedCardTypes = FirmwareVersion.FirmwareType.values().toList()
|
||||
)
|
||||
allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.domain
|
|||
|
||||
import androidx.annotation.StringRes
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.network.api.tangemTech.TangemTechError
|
||||
import com.tangem.wallet.R
|
||||
|
||||
interface TapErrors
|
||||
|
|
@ -31,13 +32,13 @@ sealed class TapError(
|
|||
object InsufficientBalance : TapError(R.string.send_error_insufficient_balance)
|
||||
object BlockchainInternalError : TapError(R.string.send_error_blockchain_internal)
|
||||
object AmountExceedsBalance : TapError(R.string.send_validation_amount_exceeds_balance)
|
||||
data class AmountLowerExistentialDeposit(override val args: List<Any>) : TapError(R.string.send_error_minimum_balance_format)
|
||||
object FeeExceedsBalance : TapError(R.string.send_validation_invalid_fee)
|
||||
object TotalExceedsBalance : TapError(R.string.send_validation_invalid_total)
|
||||
object InvalidAmountValue : TapError(R.string.send_validation_invalid_amount)
|
||||
object InvalidFeeValue : TapError(R.string.send_error_invalid_fee_value)
|
||||
data class DustAmount(override val args: List<Any>) : TapError(R.string.send_error_dust_amount_format)
|
||||
object DustChange : TapError(R.string.send_error_dust_change)
|
||||
data class CreateAccountUnderfunded(override val args: List<Any>) : TapError(R.string.send_error_no_target_account)
|
||||
|
||||
data class UnsupportedState(
|
||||
val stateError: String,
|
||||
|
|
@ -54,6 +55,7 @@ sealed class TapError(
|
|||
|
||||
sealed class WalletConnect {
|
||||
object UnsupportedDapp : TapError(R.string.wallet_connect_error_unsupported_dapp)
|
||||
object UnsupportedLink : TapError(R.string.wallet_connect_error_failed_to_connect)
|
||||
}
|
||||
|
||||
data class ValidateTransactionErrors(
|
||||
|
|
@ -70,7 +72,6 @@ sealed class TapSdkError(override val messageResId: Int?) : Throwable(), TangemE
|
|||
object CardNotSupportedByRelease : TapSdkError(R.string.error_update_app)
|
||||
}
|
||||
|
||||
|
||||
fun TapErrors.assembleErrors(): MutableList<Pair<Int, List<Any>?>> {
|
||||
val idList = mutableListOf<Pair<Int, List<Any>?>>()
|
||||
when (this) {
|
||||
|
|
@ -78,4 +79,13 @@ fun TapErrors.assembleErrors(): MutableList<Pair<Int, List<Any>?>> {
|
|||
is TapError -> idList.add(Pair(this.messageResource, this.args))
|
||||
}
|
||||
return idList
|
||||
}
|
||||
}
|
||||
|
||||
fun TangemTechError.toTapError(): TapError {
|
||||
return when (this.code) {
|
||||
404 -> NoDataError(this.description)
|
||||
else -> TapError.CustomError(customMessage = this.description)
|
||||
}
|
||||
}
|
||||
|
||||
class NoDataError(message: String) : TapError.CustomError(customMessage = message)
|
||||
|
|
|
|||
|
|
@ -16,24 +16,24 @@ import com.tangem.domain.common.extensions.withMainContext
|
|||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.safeUpdate
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.currenciesRepository
|
||||
import com.tangem.tap.domain.configurable.config.ConfigManager
|
||||
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
|
||||
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
|
||||
import com.tangem.tap.domain.extensions.makeWalletManagersForApp
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.demo.isDemoCard
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
import com.tangem.tap.features.wallet.models.toBlockchainNetworks
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.network.NetworkConnectivity
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.userTokensRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
||||
class TapWalletManager {
|
||||
val walletManagerFactory: WalletManagerFactory
|
||||
by lazy { WalletManagerFactory(blockchainSdkConfig) }
|
||||
|
||||
by lazy { WalletManagerFactory(blockchainSdkConfig) }
|
||||
val rates: RatesRepository = RatesRepository()
|
||||
|
||||
private val blockchainSdkConfig by lazy {
|
||||
|
|
@ -64,16 +64,16 @@ class TapWalletManager {
|
|||
WalletAction.LoadWallet.NoAccount(
|
||||
walletManager.wallet,
|
||||
blockchainNetwork,
|
||||
(result.error as TapError.WalletManager.NoAccountError).customMessage
|
||||
)
|
||||
(result.error as TapError.WalletManager.NoAccountError).customMessage,
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
dispatchOnMain(
|
||||
WalletAction.LoadWallet.Failure(
|
||||
walletManager.wallet,
|
||||
result.error.localizedMessage
|
||||
)
|
||||
result.error.localizedMessage,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -88,14 +88,16 @@ class TapWalletManager {
|
|||
|
||||
withMainContext {
|
||||
store.dispatch(WalletAction.ResetState(data.card.cardId))
|
||||
store.dispatch(WalletConnectAction.ResetState)
|
||||
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
|
||||
store.dispatch(WalletAction.SetIfTestnetCard(data.card.isTestCard))
|
||||
store.dispatch(WalletAction.MultiWallet.SetIsMultiwalletAllowed(data.card.isMultiwalletAllowed))
|
||||
store.dispatch(WalletConnectAction.RestoreSessions(data))
|
||||
store.dispatch(
|
||||
WalletAction.MultiWallet.ShowWalletBackupWarning(
|
||||
show = data.card.settings.isBackupAllowed
|
||||
&& data.card.backupStatus == Card.BackupStatus.NoBackup
|
||||
)
|
||||
&& data.card.backupStatus == Card.BackupStatus.NoBackup,
|
||||
),
|
||||
)
|
||||
loadData(data)
|
||||
}
|
||||
|
|
@ -132,27 +134,25 @@ class TapWalletManager {
|
|||
}
|
||||
|
||||
dispatchOnMain(WalletAction.LoadWallet())
|
||||
dispatchOnMain(WalletAction.LoadFiatRate())
|
||||
}
|
||||
|
||||
private suspend fun loadMultiWalletData(
|
||||
scanResponse: ScanResponse
|
||||
) {
|
||||
val savedCurrencies = currenciesRepository.loadSavedCurrencies(
|
||||
scanResponse.card.cardId, scanResponse.card.settings.isHDWalletAllowed
|
||||
)
|
||||
if (savedCurrencies.isEmpty()) return
|
||||
private suspend fun loadMultiWalletData(scanResponse: ScanResponse) {
|
||||
loadUserCurrencies(scanResponse, walletManagerFactory)
|
||||
}
|
||||
|
||||
val walletManagers =
|
||||
walletManagerFactory.makeWalletManagersForApp(scanResponse, savedCurrencies)
|
||||
dispatchOnMain(
|
||||
WalletAction.MultiWallet.AddBlockchains(savedCurrencies, walletManagers),
|
||||
)
|
||||
savedCurrencies.map {
|
||||
private fun checkIfDerivationsAreMissing(blockchainNetworks: List<BlockchainNetwork>, scanResponse: ScanResponse) {
|
||||
blockchainNetworks.map {
|
||||
if (it.tokens.isNotEmpty()) {
|
||||
dispatchOnMain(WalletAction.MultiWallet.AddTokens(it.tokens, it))
|
||||
WalletAction.MultiWallet.AddTokens(it.tokens, it, false)
|
||||
}
|
||||
}
|
||||
val missingDerivations = blockchainNetworks
|
||||
.filter {
|
||||
it.derivationPath != null && !scanResponse.hasDerivation(it.blockchain, it.derivationPath)
|
||||
}
|
||||
if (missingDerivations.isNotEmpty()) {
|
||||
store.dispatch(WalletAction.MultiWallet.AddMissingDerivations(missingDerivations))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadSingleWalletData(data: ScanResponse) {
|
||||
|
|
@ -169,14 +169,45 @@ class TapWalletManager {
|
|||
}
|
||||
dispatchOnMain(
|
||||
WalletAction.MultiWallet.AddBlockchains(
|
||||
listOf(BlockchainNetwork.fromWalletManager(primaryWalletManager)),
|
||||
listOf(primaryWalletManager)
|
||||
)
|
||||
blockchains = listOf(BlockchainNetwork.fromWalletManager(primaryWalletManager)),
|
||||
walletManagers = listOf(primaryWalletManager),
|
||||
save = false,
|
||||
),
|
||||
WalletAction.LoadFiatRate(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadUserCurrencies(scanResponse: ScanResponse, walletManagerFactory: WalletManagerFactory) {
|
||||
val userTokens = userTokensRepository.getUserTokens(scanResponse.card)
|
||||
withMainContext {
|
||||
val blockchainNetworks = userTokens.toBlockchainNetworks()
|
||||
val walletManagers = walletManagerFactory.makeWalletManagersForApp(scanResponse, userTokens)
|
||||
store.dispatch(
|
||||
WalletAction.MultiWallet.AddBlockchains(
|
||||
blockchains = blockchainNetworks,
|
||||
walletManagers = walletManagers,
|
||||
save = false,
|
||||
),
|
||||
)
|
||||
|
||||
blockchainNetworks.filter { it.tokens.isNotEmpty() }
|
||||
.map {
|
||||
store.dispatch(
|
||||
WalletAction.MultiWallet.AddTokens(
|
||||
tokens = it.tokens,
|
||||
blockchain = it,
|
||||
save = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
checkIfDerivationsAreMissing(blockchainNetworks, scanResponse)
|
||||
store.dispatch(WalletAction.LoadFiatRate(coinsList = userTokens))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun reloadData(data: ScanResponse) {
|
||||
loadUserCurrencies(data, walletManagerFactory)
|
||||
withContext(Dispatchers.Main) {
|
||||
getActionIfUnknownBlockchainOrEmptyWallet(data)?.let {
|
||||
store.dispatch(it)
|
||||
|
|
@ -188,7 +219,6 @@ class TapWalletManager {
|
|||
}
|
||||
|
||||
store.dispatch(WalletAction.LoadWallet())
|
||||
store.dispatch(WalletAction.LoadFiatRate())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
package com.tangem.tap.domain.configurable.config
|
||||
|
||||
import android.content.Context
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import com.google.firebase.remoteconfig.ktx.remoteConfig
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
|
||||
import com.tangem.tap.common.extensions.readAssetAsString
|
||||
import com.tangem.tap.common.AssetReader
|
||||
import com.tangem.tap.domain.configurable.Loader
|
||||
import timber.log.Timber
|
||||
|
||||
|
|
@ -14,8 +10,8 @@ import timber.log.Timber
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class FeaturesLocalLoader(
|
||||
private val context: Context,
|
||||
private val moshi: Moshi,
|
||||
private val assetReader: AssetReader,
|
||||
private val moshi: Moshi,
|
||||
) : Loader<ConfigModel> {
|
||||
|
||||
override fun load(onComplete: (ConfigModel) -> Unit) {
|
||||
|
|
@ -23,8 +19,8 @@ class FeaturesLocalLoader(
|
|||
val featureAdapter: JsonAdapter<FeatureModel> = moshi.adapter(FeatureModel::class.java)
|
||||
val valuesAdapter: JsonAdapter<ConfigValueModel> = moshi.adapter(ConfigValueModel::class.java)
|
||||
|
||||
val jsonFeatures = context.readAssetAsString(Loader.featuresName)
|
||||
val jsonConfigValues = context.readAssetAsString(Loader.configValuesName)
|
||||
val jsonFeatures = assetReader.readAssetAsString(Loader.featuresName)
|
||||
val jsonConfigValues = assetReader.readAssetAsString(Loader.configValuesName)
|
||||
|
||||
ConfigModel(featureAdapter.fromJson(jsonFeatures), valuesAdapter.fromJson(jsonConfigValues))
|
||||
} catch (ex: Exception) {
|
||||
|
|
@ -33,31 +29,4 @@ class FeaturesLocalLoader(
|
|||
}
|
||||
onComplete(config)
|
||||
}
|
||||
}
|
||||
|
||||
class FeaturesRemoteLoader(
|
||||
private val moshi: Moshi,
|
||||
) : Loader<ConfigModel> {
|
||||
|
||||
override fun load(onComplete: (ConfigModel) -> Unit) {
|
||||
val emptyConfig = ConfigModel.empty()
|
||||
val remoteConfig = Firebase.remoteConfig
|
||||
remoteConfig.fetchAndActivate().addOnCompleteListener {
|
||||
if (it.isSuccessful) {
|
||||
val config = remoteConfig.getValue(Loader.featuresName)
|
||||
val jsonConfig = config.asString()
|
||||
if (jsonConfig.isEmpty()) {
|
||||
onComplete(emptyConfig)
|
||||
return@addOnCompleteListener
|
||||
}
|
||||
val featureAdapter: JsonAdapter<FeatureModel> = moshi.adapter(FeatureModel::class.java)
|
||||
onComplete(ConfigModel(featureAdapter.fromJson(jsonConfig), null))
|
||||
} else {
|
||||
onComplete(emptyConfig)
|
||||
}
|
||||
}.addOnFailureListener {
|
||||
FirebaseAnalyticsHandler.logException("remote_config_error.features", it)
|
||||
onComplete(emptyConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.tangem.blockchain.common.BlockchainSdkConfig
|
|||
import com.tangem.tap.common.shop.shopify.ShopifyShop
|
||||
import com.tangem.tap.common.zendesk.ZendeskConfig
|
||||
import com.tangem.tap.domain.configurable.Loader
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -22,28 +23,22 @@ data class Config(
|
|||
val isCreatingTwinCardsAllowed: Boolean = false,
|
||||
val shopify: ShopifyShop? = null,
|
||||
val zendesk: ZendeskConfig? = null,
|
||||
val saltPayConfig: SaltPayConfig? = null,
|
||||
)
|
||||
|
||||
class ConfigManager(
|
||||
private val localLoader: Loader<ConfigModel>,
|
||||
private val remoteLoader: Loader<ConfigModel>
|
||||
) {
|
||||
class ConfigManager {
|
||||
|
||||
var config: Config = Config()
|
||||
private set
|
||||
|
||||
private var defaultConfig = Config()
|
||||
|
||||
fun load(onComplete: ((config: Config) -> Unit)? = null) {
|
||||
localLoader.load { configModel ->
|
||||
fun load(configLoader: Loader<ConfigModel>, onComplete: ((config: Config) -> Unit)? = null) {
|
||||
configLoader.load { configModel ->
|
||||
setupFeature(configModel.features)
|
||||
setupKey(configModel.configValues)
|
||||
onComplete?.invoke(config)
|
||||
}
|
||||
// Uncomment to enable remote config
|
||||
// remoteLoader.load { config ->
|
||||
// setupFeature(config.features)
|
||||
// }
|
||||
}
|
||||
|
||||
fun turnOff(name: String) {
|
||||
|
|
@ -97,6 +92,7 @@ class ConfigManager(
|
|||
appsFlyerDevKey = values.appsFlyerDevKey,
|
||||
shopify = values.shopifyShop,
|
||||
zendesk = values.zendesk,
|
||||
saltPayConfig = values.saltPay,
|
||||
)
|
||||
defaultConfig = defaultConfig.copy(
|
||||
coinMarketCapKey = values.coinMarketCapKey,
|
||||
|
|
@ -113,6 +109,7 @@ class ConfigManager(
|
|||
appsFlyerDevKey = values.appsFlyerDevKey,
|
||||
shopify = values.shopifyShop,
|
||||
zendesk = values.zendesk,
|
||||
saltPayConfig = values.saltPay,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,32 +2,34 @@ package com.tangem.tap.domain.configurable.config
|
|||
|
||||
import com.tangem.tap.common.shop.shopify.ShopifyShop
|
||||
import com.tangem.tap.common.zendesk.ZendeskConfig
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
|
||||
class FeatureModel(
|
||||
val isWalletPayIdEnabled: Boolean,
|
||||
val isTopUpEnabled: Boolean,
|
||||
val isSendingToPayIdEnabled: Boolean,
|
||||
val isCreatingTwinCardsAllowed: Boolean,
|
||||
val isWalletPayIdEnabled: Boolean,
|
||||
val isTopUpEnabled: Boolean,
|
||||
val isSendingToPayIdEnabled: Boolean,
|
||||
val isCreatingTwinCardsAllowed: Boolean,
|
||||
)
|
||||
|
||||
class ConfigValueModel(
|
||||
val coinMarketCapKey: String,
|
||||
val mercuryoWidgetId: String,
|
||||
val mercuryoSecret: String,
|
||||
val moonPayApiKey: String,
|
||||
val moonPayApiSecretKey: String,
|
||||
val blockchairApiKey: String?,
|
||||
val blockchairAuthorizationToken: String?,
|
||||
val blockcypherTokens: Set<String>?,
|
||||
val infuraProjectId: String?,
|
||||
val appsFlyerDevKey: String,
|
||||
val shopifyShop: ShopifyShop?,
|
||||
val zendesk: ZendeskConfig?,
|
||||
val tronGridApiKey: String,
|
||||
val coinMarketCapKey: String,
|
||||
val mercuryoWidgetId: String,
|
||||
val mercuryoSecret: String,
|
||||
val moonPayApiKey: String,
|
||||
val moonPayApiSecretKey: String,
|
||||
val blockchairApiKey: String?,
|
||||
val blockchairAuthorizationToken: String?,
|
||||
val blockcypherTokens: Set<String>?,
|
||||
val infuraProjectId: String?,
|
||||
val appsFlyerDevKey: String,
|
||||
val shopifyShop: ShopifyShop?,
|
||||
val zendesk: ZendeskConfig?,
|
||||
val saltPay: SaltPayConfig,
|
||||
val tronGridApiKey: String,
|
||||
)
|
||||
|
||||
class ConfigModel(val features: FeatureModel?, val configValues: ConfigValueModel?) {
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
package com.tangem.tap.domain.configurable.warningMessage
|
||||
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import com.google.firebase.remoteconfig.ktx.remoteConfig
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
|
||||
import com.tangem.tap.domain.configurable.Loader
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class RemoteWarningLoader(
|
||||
private val moshi: Moshi,
|
||||
) : Loader<List<WarningMessage>> {
|
||||
|
||||
override fun load(onComplete: (List<WarningMessage>) -> Unit) {
|
||||
val emptyConfig = listOf<WarningMessage>()
|
||||
val remoteConfig = Firebase.remoteConfig
|
||||
remoteConfig.fetchAndActivate().addOnCompleteListener {
|
||||
if (!it.isSuccessful) {
|
||||
onComplete(emptyConfig)
|
||||
return@addOnCompleteListener
|
||||
}
|
||||
|
||||
val config = remoteConfig.getValue(Loader.warnings)
|
||||
val jsonConfig = config.asString()
|
||||
if (jsonConfig.isEmpty()) {
|
||||
onComplete(emptyConfig)
|
||||
return@addOnCompleteListener
|
||||
}
|
||||
|
||||
val adapterType = Types.newParameterizedType(List::class.java, WarningMessage::class.java)
|
||||
val warningsAdapter: JsonAdapter<List<WarningMessage>> = moshi.adapter(adapterType)
|
||||
try {
|
||||
val warnings = warningsAdapter.fromJson(jsonConfig) ?: listOf()
|
||||
onComplete(warnings)
|
||||
} catch (ex: Exception) {
|
||||
handleError(ex)
|
||||
onComplete(emptyConfig)
|
||||
}
|
||||
}.addOnFailureListener {
|
||||
handleError(it)
|
||||
onComplete(emptyConfig)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleError(ex: Exception) {
|
||||
FirebaseAnalyticsHandler.logException("remote_config_error.warnings", ex)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +1,17 @@
|
|||
package com.tangem.tap.domain.configurable.warningMessage
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.tap.common.extensions.containsAny
|
||||
import com.tangem.tap.common.extensions.removeBy
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class WarningMessagesManager(
|
||||
private val warningLoader: RemoteWarningLoader,
|
||||
) {
|
||||
class WarningMessagesManager {
|
||||
|
||||
private val warningsList: MutableList<WarningMessage> = mutableListOf()
|
||||
|
||||
fun load(onComplete: VoidCallback? = null) {
|
||||
// exclude annoying remote debug warnings
|
||||
if (BuildConfig.DEBUG) {
|
||||
onComplete?.invoke()
|
||||
return
|
||||
}
|
||||
warningLoader.load { remoteList ->
|
||||
warningsList.clear()
|
||||
warningsList.addAll(remoteList)
|
||||
sortByPriority()
|
||||
onComplete?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
fun addWarning(warning: WarningMessage) {
|
||||
if (findWarning(warning) == null) {
|
||||
warningsList.add(warning)
|
||||
|
|
@ -37,7 +19,10 @@ class WarningMessagesManager(
|
|||
}
|
||||
}
|
||||
|
||||
fun getWarnings(location: WarningMessage.Location, forBlockchains: List<Blockchain> = emptyList()): List<WarningMessage> {
|
||||
fun getWarnings(
|
||||
location: WarningMessage.Location,
|
||||
forBlockchains: List<Blockchain> = emptyList(),
|
||||
): List<WarningMessage> {
|
||||
return warningsList
|
||||
.filter { !it.isHidden && it.location.contains(location) }
|
||||
.filter {
|
||||
|
|
@ -94,7 +79,7 @@ class WarningMessagesManager(
|
|||
null,
|
||||
R.string.alert_title,
|
||||
R.string.alert_developer_card,
|
||||
WarningMessage.Origin.Local
|
||||
WarningMessage.Origin.Local,
|
||||
)
|
||||
|
||||
fun alreadySignedHashesWarning(): WarningMessage = WarningMessage(
|
||||
|
|
@ -106,7 +91,7 @@ class WarningMessagesManager(
|
|||
null,
|
||||
R.string.alert_title,
|
||||
R.string.alert_card_signed_transactions,
|
||||
WarningMessage.Origin.Local
|
||||
WarningMessage.Origin.Local,
|
||||
)
|
||||
|
||||
fun signedHashesMultiWalletWarning(): WarningMessage = WarningMessage(
|
||||
|
|
@ -131,7 +116,7 @@ class WarningMessagesManager(
|
|||
null,
|
||||
R.string.warning_rate_app_title,
|
||||
R.string.warning_rate_app_message,
|
||||
WarningMessage.Origin.Local
|
||||
WarningMessage.Origin.Local,
|
||||
)
|
||||
|
||||
fun isAlreadySignedHashesWarning(warning: WarningMessage): Boolean {
|
||||
|
|
@ -147,7 +132,7 @@ class WarningMessagesManager(
|
|||
null,
|
||||
R.string.warning_failed_to_verify_card_title,
|
||||
R.string.warning_failed_to_verify_card_message,
|
||||
WarningMessage.Origin.Local
|
||||
WarningMessage.Origin.Local,
|
||||
)
|
||||
|
||||
fun remainingSignaturesNotEnough(remainingSignatures: Int): WarningMessage = WarningMessage(
|
||||
|
|
@ -160,7 +145,7 @@ class WarningMessagesManager(
|
|||
titleResId = R.string.alert_title,
|
||||
messageResId = R.string.warning_low_signatures_format,
|
||||
origin = WarningMessage.Origin.Local,
|
||||
messageFormatArg = remainingSignatures.toString()
|
||||
messageFormatArg = remainingSignatures.toString(),
|
||||
)
|
||||
|
||||
fun testCardWarning(): WarningMessage = WarningMessage(
|
||||
|
|
@ -172,7 +157,7 @@ class WarningMessagesManager(
|
|||
null,
|
||||
R.string.alert_title,
|
||||
R.string.warning_testnet_card_message,
|
||||
WarningMessage.Origin.Local
|
||||
WarningMessage.Origin.Local,
|
||||
)
|
||||
|
||||
fun demoCardWarning(): WarningMessage = WarningMessage(
|
||||
|
|
@ -184,7 +169,7 @@ class WarningMessagesManager(
|
|||
null,
|
||||
R.string.alert_title,
|
||||
R.string.alert_demo_message,
|
||||
WarningMessage.Origin.Local
|
||||
WarningMessage.Origin.Local,
|
||||
)
|
||||
|
||||
const val REMAINING_SIGNATURES_WARNING = 10
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.common.card.EllipticCurve
|
|||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.TapWorkarounds.isSaltPay
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.common.TapWorkarounds.isTangemNote
|
||||
import com.tangem.domain.common.TwinCardNumber
|
||||
|
|
@ -23,11 +24,14 @@ val Card.isWalletDataSupported: Boolean
|
|||
|
||||
val Card.isMultiwalletAllowed: Boolean
|
||||
get() {
|
||||
return !isTangemTwin() && !isStart2Coin && !isTangemNote()
|
||||
return !isTangemTwin() && !isStart2Coin && !isTangemNote && !isSaltPay
|
||||
&& (firmwareVersion >= FirmwareVersion.MultiWalletAvailable ||
|
||||
getSingleWallet()?.curve == EllipticCurve.Secp256k1)
|
||||
}
|
||||
|
||||
val Card.isHdWalletAllowedByApp: Boolean
|
||||
get() = settings.isHDWalletAllowed && !isSaltPay
|
||||
|
||||
fun Card.getSingleWallet(): CardWallet? {
|
||||
return wallets.firstOrNull()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,20 @@
|
|||
package com.tangem.tap.domain.extensions
|
||||
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.DerivationParams
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.common.WalletManagerFactory
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.card.CardWallet
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.common.hdWallet.DerivationPath
|
||||
import com.tangem.domain.common.SaltPayWorkaround
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
|
||||
|
|
@ -16,7 +24,7 @@ import com.tangem.tap.features.wallet.models.Currency
|
|||
fun WalletManagerFactory.makeWalletManagerForApp(
|
||||
scanResponse: ScanResponse,
|
||||
blockchain: Blockchain,
|
||||
derivationParams: DerivationParams?
|
||||
derivationParams: DerivationParams?,
|
||||
): WalletManager? {
|
||||
val card = scanResponse.card
|
||||
if (card.isTestCard && blockchain.getTestnetVersion() == null) return null
|
||||
|
|
@ -35,10 +43,10 @@ fun WalletManagerFactory.makeWalletManagerForApp(
|
|||
walletPublicKey = wallet.publicKey,
|
||||
pairPublicKey = scanResponse.secondTwinPublicKey!!.hexToBytes(),
|
||||
blockchain = environmentBlockchain,
|
||||
curve = wallet.curve
|
||||
curve = wallet.curve,
|
||||
)
|
||||
}
|
||||
seedKey != null && derivationParams != null -> {
|
||||
scanResponse.card.isHdWalletAllowedByApp && (seedKey != null && derivationParams != null) -> {
|
||||
val derivedKeys = scanResponse.derivedKeys[wallet.publicKey.toMapKey()]
|
||||
val derivationPath = when (derivationParams) {
|
||||
is DerivationParams.Default -> blockchain.derivationPath(derivationParams.style)
|
||||
|
|
@ -51,33 +59,33 @@ fun WalletManagerFactory.makeWalletManagerForApp(
|
|||
blockchain = environmentBlockchain,
|
||||
seedKey = wallet.publicKey,
|
||||
derivedKey = derivedKey,
|
||||
derivation = derivationParams
|
||||
derivation = derivationParams,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
makeWalletManager(
|
||||
blockchain = environmentBlockchain,
|
||||
walletPublicKey = wallet.publicKey,
|
||||
curve = wallet.curve
|
||||
curve = wallet.curve,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun WalletManagerFactory.makeWalletManagerForApp(
|
||||
scanResponse: ScanResponse, blockchainNetwork: BlockchainNetwork
|
||||
scanResponse: ScanResponse, blockchainNetwork: BlockchainNetwork,
|
||||
): WalletManager? {
|
||||
return makeWalletManagerForApp(
|
||||
scanResponse,
|
||||
blockchain = blockchainNetwork.blockchain,
|
||||
derivationParams = getDerivationParams(blockchainNetwork.derivationPath, scanResponse.card)
|
||||
derivationParams = getDerivationParams(blockchainNetwork.derivationPath, scanResponse.card),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDerivationParams(derivationPath: String?, card: Card): DerivationParams? {
|
||||
return derivationPath?.let {
|
||||
DerivationParams.Custom(
|
||||
DerivationPath(it)
|
||||
DerivationPath(it),
|
||||
)
|
||||
} ?: if (!card.settings.isHDWalletAllowed) {
|
||||
null
|
||||
|
|
@ -89,19 +97,23 @@ private fun getDerivationParams(derivationPath: String?, card: Card): Derivation
|
|||
}
|
||||
|
||||
fun WalletManagerFactory.makeWalletManagerForApp(
|
||||
scanResponse: ScanResponse, currency: Currency
|
||||
scanResponse: ScanResponse,
|
||||
currency: Currency,
|
||||
): WalletManager? {
|
||||
return makeWalletManagerForApp(
|
||||
scanResponse,
|
||||
blockchain = currency.blockchain,
|
||||
derivationParams = getDerivationParams(currency.derivationPath, scanResponse.card)
|
||||
derivationParams = getDerivationParams(currency.derivationPath, scanResponse.card),
|
||||
)
|
||||
}
|
||||
|
||||
fun WalletManagerFactory.makeWalletManagersForApp(
|
||||
scanResponse: ScanResponse, blockchains: List<BlockchainNetwork>,
|
||||
scanResponse: ScanResponse,
|
||||
blockchains: List<Currency>,
|
||||
): List<WalletManager> {
|
||||
return blockchains.mapNotNull { this.makeWalletManagerForApp(scanResponse, it) }
|
||||
return blockchains
|
||||
.filter { it.isBlockchain() }
|
||||
.mapNotNull { this.makeWalletManagerForApp(scanResponse, it) }
|
||||
}
|
||||
|
||||
fun WalletManagerFactory.makePrimaryWalletManager(
|
||||
|
|
@ -116,7 +128,7 @@ fun WalletManagerFactory.makePrimaryWalletManager(
|
|||
return makeWalletManagerForApp(
|
||||
scanResponse = scanResponse,
|
||||
blockchain = blockchain,
|
||||
derivationParams = derivationParams
|
||||
derivationParams = derivationParams,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -126,4 +138,23 @@ private fun selectWallet(wallets: List<CardWallet>): CardWallet? {
|
|||
1 -> wallets[0]
|
||||
else -> wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: wallets[0]
|
||||
}
|
||||
}
|
||||
|
||||
fun WalletManagerFactory.makeSaltPayWalletManager(
|
||||
scanResponse: ScanResponse,
|
||||
): EthereumWalletManager {
|
||||
val blockchain = scanResponse.getBlockchain()
|
||||
if (blockchain != Blockchain.SaltPay && blockchain != Blockchain.SaltPayTestnet)
|
||||
throw IllegalArgumentException()
|
||||
|
||||
val token = SaltPayWorkaround.tokenFrom(blockchain)
|
||||
val cardWallet = scanResponse.card.wallets.firstOrNull().guard {
|
||||
throw NullPointerException("SaltPay card must have one wallet at least")
|
||||
}
|
||||
|
||||
return makeWalletManager(
|
||||
blockchain = blockchain,
|
||||
publicKey = Wallet.PublicKey(cardWallet.publicKey, null, null),
|
||||
tokens = listOf(token),
|
||||
) as EthereumWalletManager
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.tap.domain.moduleMessage
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
* Base interface for conversion outputs from ModuleMessageConverter
|
||||
*/
|
||||
interface ConvertedMessage {
|
||||
val message: String
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog message used to construct a android.Dialog
|
||||
*/
|
||||
interface DialogMessage : ConvertedMessage {
|
||||
val title: String
|
||||
override val message: String
|
||||
val onPositive: String?
|
||||
val onNegative: String?
|
||||
val onNeutral: String?
|
||||
}
|
||||
|
||||
data class ConvertedStringMessage(
|
||||
override val message: String,
|
||||
) : ConvertedMessage
|
||||
|
||||
data class ConvertedDialogMessage(
|
||||
override val title: String,
|
||||
override val message: String,
|
||||
override val onPositive: String? = null,
|
||||
override val onNegative: String? = null,
|
||||
override val onNeutral: String? = null,
|
||||
) : DialogMessage
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.tap.domain.moduleMessage
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.common.module.ModuleMessage
|
||||
import com.tangem.common.module.ModuleMessageConverter
|
||||
import com.tangem.domain.DomainModuleMessage
|
||||
import com.tangem.tap.domain.moduleMessage.domain.DomainMessageConverter
|
||||
import com.tangem.tap.domain.moduleMessage.saltPay.SaltPayMessageConverter
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayModuleMessage
|
||||
|
||||
class ModuleMessageConverter(
|
||||
private val context: Context,
|
||||
) : ModuleMessageConverter<ModuleMessage, ConvertedMessage> {
|
||||
|
||||
override fun convert(message: ModuleMessage): ConvertedMessage {
|
||||
val convertedMessage = when (message) {
|
||||
is DomainModuleMessage -> DomainMessageConverter(context).convert(message)
|
||||
is SaltPayModuleMessage -> SaltPayMessageConverter(context).convert(message)
|
||||
else -> null
|
||||
}
|
||||
return convertedMessage ?: convertUnknownMessage(message)
|
||||
}
|
||||
|
||||
private fun convertUnknownMessage(message: ModuleMessage): ConvertedMessage {
|
||||
return ConvertedStringMessage("Unknown message: ${message::class.java.simpleName}")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.tap.domain.moduleMessage.domain
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.common.module.ModuleMessageConverter
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import com.tangem.domain.DomainModuleError
|
||||
import com.tangem.domain.DomainModuleMessage
|
||||
import com.tangem.tap.domain.moduleMessage.ConvertedMessage
|
||||
import com.tangem.tap.domain.moduleMessage.domain.converter.AddCustomTokenErrorConverter
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class DomainMessageConverter(
|
||||
private val context: Context,
|
||||
) : ModuleMessageConverter<DomainModuleMessage, ConvertedMessage?> {
|
||||
|
||||
override fun convert(message: DomainModuleMessage): ConvertedMessage? {
|
||||
return when (message) {
|
||||
is DomainModuleError -> DomainErrorConverter(context).convert(message)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DomainErrorConverter(
|
||||
private val context: Context,
|
||||
) : ModuleMessageConverter<DomainModuleError, ConvertedMessage?> {
|
||||
|
||||
override fun convert(message: DomainModuleError): ConvertedMessage? = when (message) {
|
||||
is AddCustomTokenError -> AddCustomTokenErrorConverter(context).convert(message)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +1,21 @@
|
|||
package com.tangem.tap.common.moduleMessage.domain
|
||||
package com.tangem.tap.domain.moduleMessage.domain.converter
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.common.module.ModuleMessageConverter
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import com.tangem.domain.DomainError
|
||||
import com.tangem.domain.DomainModuleError
|
||||
import com.tangem.tap.domain.moduleMessage.ConvertedMessage
|
||||
import com.tangem.tap.domain.moduleMessage.ConvertedStringMessage
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class DomainErrorConverter(
|
||||
private val context: Context
|
||||
) : ModuleMessageConverter<DomainError, String?> {
|
||||
override fun convert(message: DomainError): String? = when (message) {
|
||||
is AddCustomTokenError -> AddCustomTokenConverter(context).convert(message)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
internal class AddCustomTokenErrorConverter(
|
||||
private val context: Context,
|
||||
) : ModuleMessageConverter<DomainModuleError, ConvertedMessage?> {
|
||||
|
||||
private class AddCustomTokenConverter(
|
||||
private val context: Context
|
||||
) : ModuleMessageConverter<DomainError, String?> {
|
||||
|
||||
override fun convert(message: DomainError): String? {
|
||||
override fun convert(message: DomainModuleError): ConvertedMessage? {
|
||||
val customTokenError = (message as? AddCustomTokenError) ?: throw UnsupportedOperationException()
|
||||
|
||||
val rawMessage = when (customTokenError) {
|
||||
|
|
@ -40,8 +33,8 @@ private class AddCustomTokenConverter(
|
|||
}
|
||||
|
||||
return when (rawMessage) {
|
||||
is Int -> context.getString(rawMessage)
|
||||
is String -> rawMessage
|
||||
is Int -> ConvertedStringMessage(context.getString(rawMessage))
|
||||
is String -> ConvertedStringMessage(rawMessage)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.tap.domain.moduleMessage.saltPay
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.common.module.ModuleMessageConverter
|
||||
import com.tangem.tap.domain.moduleMessage.ConvertedMessage
|
||||
import com.tangem.tap.domain.moduleMessage.saltPay.converter.SaltPayRegistrationErrorConverter
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayModuleError
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayModuleMessage
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayRegistrationError
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class SaltPayMessageConverter(
|
||||
private val context: Context,
|
||||
) : ModuleMessageConverter<SaltPayModuleMessage, ConvertedMessage?> {
|
||||
|
||||
override fun convert(message: SaltPayModuleMessage): ConvertedMessage? {
|
||||
return when (message) {
|
||||
is SaltPayModuleError -> SaltPayErrorConverter(context).convert(message)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SaltPayErrorConverter(
|
||||
private val context: Context,
|
||||
) : ModuleMessageConverter<SaltPayModuleError, ConvertedMessage?> {
|
||||
|
||||
override fun convert(message: SaltPayModuleError): ConvertedMessage? = when (message) {
|
||||
is SaltPayRegistrationError -> SaltPayRegistrationErrorConverter(context).convert(message)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.tap.domain.moduleMessage.saltPay.converter
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.common.module.ModuleMessageConverter
|
||||
import com.tangem.tap.domain.moduleMessage.ConvertedDialogMessage
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayRegistrationError
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class SaltPayRegistrationErrorConverter(
|
||||
private val context: Context,
|
||||
) : ModuleMessageConverter<SaltPayRegistrationError, ConvertedDialogMessage?> {
|
||||
|
||||
override fun convert(message: SaltPayRegistrationError): ConvertedDialogMessage? {
|
||||
val saltPayError = (message as? SaltPayRegistrationError) ?: throw UnsupportedOperationException()
|
||||
|
||||
val dialogMessage = when (saltPayError) {
|
||||
SaltPayRegistrationError.NoGas -> {
|
||||
ConvertedDialogMessage(
|
||||
title = context.getString(R.string.saltpay_error_no_gas_title),
|
||||
message = context.getString(R.string.saltpay_error_no_gas_message),
|
||||
)
|
||||
}
|
||||
SaltPayRegistrationError.EmptyBackupCardScanned -> {
|
||||
ConvertedDialogMessage(
|
||||
title = context.getString(R.string.saltpay_error_empty_backup_title),
|
||||
message = context.getString(R.string.saltpay_error_empty_backup_message),
|
||||
)
|
||||
}
|
||||
SaltPayRegistrationError.WeakPin -> {
|
||||
ConvertedDialogMessage(
|
||||
title = context.getString(R.string.saltpay_error_pin_weak_title),
|
||||
message = context.getString(R.string.saltpay_error_pin_weak_message),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
val errorMessage = if (saltPayError.message == SaltPayRegistrationError.EMPTY_MESSAGE) {
|
||||
saltPayError::class.java.simpleName
|
||||
} else {
|
||||
saltPayError.message
|
||||
}
|
||||
ConvertedDialogMessage(
|
||||
title = context.getString(R.string.common_error),
|
||||
message = errorMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return dialogMessage
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package com.tangem.tap.domain.statePrinter
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.domain.redux.state.StringStateConverter
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStep
|
||||
import com.tangem.tap.store
|
||||
import timber.log.Timber
|
||||
import java.math.BigInteger
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class OnboardingWalletStateConverter : StringStateConverter<AppState> {
|
||||
private val converter = MoshiJsonConverter(
|
||||
MoshiJsonConverter.getTangemSdkAdapters() + internalAdapters(),
|
||||
MoshiJsonConverter.getTangemSdkTypedAdapters(),
|
||||
)
|
||||
|
||||
private fun internalAdapters(): List<Any> {
|
||||
return listOf(
|
||||
BackupStepAdapter(),
|
||||
BigIntegerAdapter(),
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
override fun convert(stateHolder: AppState): String {
|
||||
val model = stateHolder.onboardingWalletState
|
||||
val json = converter.prettyPrint(model)
|
||||
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
fun printOnboardingWalletState() {
|
||||
val stringState = OnboardingWalletStateConverter().convert(store.state)
|
||||
Timber.d(stringState)
|
||||
}
|
||||
|
||||
class BackupStepAdapter {
|
||||
@ToJson
|
||||
fun toJson(src: BackupStep): String {
|
||||
return when (src) {
|
||||
BackupStep.AddBackupCards -> "AddBackupCards"
|
||||
BackupStep.EnterAccessCode -> "EnterAccessCode"
|
||||
BackupStep.Finished -> "Finished"
|
||||
BackupStep.InitBackup -> "InitBackup"
|
||||
BackupStep.ReenterAccessCode -> "ReenterAccessCode"
|
||||
BackupStep.ScanOriginCard -> "ScanOriginCard"
|
||||
BackupStep.SetAccessCode -> "SetAccessCode"
|
||||
is BackupStep.WriteBackupCard -> "WriteBackupCard[${src.cardNumber}]"
|
||||
BackupStep.WritePrimaryCard -> "WritePrimaryCard"
|
||||
}
|
||||
}
|
||||
|
||||
@FromJson
|
||||
fun fromJson(json: String): BackupStep {
|
||||
return when (json) {
|
||||
"AddBackupCards" -> BackupStep.AddBackupCards
|
||||
"EnterAccessCode" -> BackupStep.EnterAccessCode
|
||||
"Finished" -> BackupStep.Finished
|
||||
"InitBackup" -> BackupStep.InitBackup
|
||||
"ReenterAccessCode" -> BackupStep.ReenterAccessCode
|
||||
"ScanOriginCard" -> BackupStep.ScanOriginCard
|
||||
"SetAccessCode" -> BackupStep.SetAccessCode
|
||||
"WriteBackupCard[1]" -> BackupStep.WriteBackupCard(1)
|
||||
"WriteBackupCard[2]" -> BackupStep.WriteBackupCard(2)
|
||||
"WriteBackupCard[3]" -> BackupStep.WriteBackupCard(3)
|
||||
"WritePrimaryCard" -> BackupStep.WritePrimaryCard
|
||||
else -> throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BigIntegerAdapter {
|
||||
@ToJson
|
||||
fun toJson(src: BigInteger): String {
|
||||
return src.toString()
|
||||
}
|
||||
|
||||
@FromJson
|
||||
fun fromJson(json: String): BigInteger {
|
||||
return BigInteger(json)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.common.core.CardSession
|
||||
import com.tangem.common.core.CardSessionRunnable
|
||||
import com.tangem.common.core.TangemError
|
||||
|
|
@ -18,10 +17,11 @@ import com.tangem.domain.common.ProductType
|
|||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TapWorkarounds.isExcluded
|
||||
import com.tangem.domain.common.TapWorkarounds.isNotSupportedInThatRelease
|
||||
import com.tangem.domain.common.TapWorkarounds.isSaltPay
|
||||
import com.tangem.domain.common.TapWorkarounds.isTangemNote
|
||||
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
|
||||
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
|
||||
import com.tangem.domain.common.TwinsHelper
|
||||
import com.tangem.domain.common.isTangemTwins
|
||||
import com.tangem.operations.PreflightReadMode
|
||||
import com.tangem.operations.PreflightReadTask
|
||||
import com.tangem.operations.ScanTask
|
||||
|
|
@ -32,7 +32,8 @@ import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
|
|||
import com.tangem.tap.domain.TapSdkError
|
||||
import com.tangem.tap.domain.extensions.getPrimaryCurve
|
||||
import com.tangem.tap.domain.extensions.getSingleWallet
|
||||
import com.tangem.tap.domain.tokens.CurrenciesRepository
|
||||
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
|
||||
import com.tangem.tap.domain.tokens.UserTokensRepository
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.scope
|
||||
|
|
@ -40,8 +41,8 @@ import kotlinx.coroutines.launch
|
|||
|
||||
class ScanProductTask(
|
||||
val card: Card? = null,
|
||||
private val currenciesRepository: CurrenciesRepository?,
|
||||
private val additionalBlockchainsToDerive: Collection<Blockchain>? = null
|
||||
private val userTokensRepository: UserTokensRepository?,
|
||||
private val additionalBlockchainsToDerive: Collection<Blockchain>? = null,
|
||||
) : CardSessionRunnable<ScanResponse> {
|
||||
|
||||
override fun run(
|
||||
|
|
@ -60,9 +61,9 @@ class ScanProductTask(
|
|||
}
|
||||
|
||||
val commandProcessor = when {
|
||||
card.isTangemNote() -> ScanNoteProcessor()
|
||||
card.isTangemTwins() -> ScanTwinProcessor()
|
||||
else -> ScanWalletProcessor(currenciesRepository, additionalBlockchainsToDerive)
|
||||
card.isTangemNote -> ScanNoteProcessor()
|
||||
card.isTangemTwins -> ScanTwinProcessor()
|
||||
else -> ScanWalletProcessor(userTokensRepository, additionalBlockchainsToDerive)
|
||||
}
|
||||
commandProcessor.proceed(card, session) { processorResult ->
|
||||
when (processorResult) {
|
||||
|
|
@ -85,9 +86,8 @@ class ScanProductTask(
|
|||
}
|
||||
|
||||
private fun getErrorIfExcludedCard(card: Card): TangemError? {
|
||||
if (card.isExcluded()) return TapSdkError.CardForDifferentApp
|
||||
if (card.isNotSupportedInThatRelease()) return TapSdkError.CardNotSupportedByRelease
|
||||
|
||||
if (card.isExcluded) return TapSdkError.CardForDifferentApp
|
||||
if (card.isNotSupportedInThatRelease) return TapSdkError.CardNotSupportedByRelease
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -111,8 +111,8 @@ private class ScanNoteProcessor : ProductCommandProcessor<ScanResponse> {
|
|||
}
|
||||
|
||||
private class ScanWalletProcessor(
|
||||
private val currenciesRepository: CurrenciesRepository?,
|
||||
private val additionalBlockchainsToDerive: Collection<Blockchain>? = null
|
||||
private val userTokensRepository: UserTokensRepository?,
|
||||
private val additionalBlockchainsToDerive: Collection<Blockchain>? = null,
|
||||
) : ProductCommandProcessor<ScanResponse> {
|
||||
|
||||
var primaryCard: PrimaryCard? = null
|
||||
|
|
@ -123,12 +123,13 @@ private class ScanWalletProcessor(
|
|||
) {
|
||||
createMissingWalletsIfNeeded(card, session, callback)
|
||||
}
|
||||
|
||||
private fun createMissingWalletsIfNeeded(
|
||||
card: Card,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> Unit,
|
||||
) {
|
||||
if (card.wallets.isEmpty() || card.firmwareVersion < FirmwareVersion.MultiWalletAvailable) {
|
||||
if (card.wallets.isEmpty() || !card.isMultiwalletAllowed) {
|
||||
startLinkingForBackupIfNeeded(card, session, callback)
|
||||
return
|
||||
}
|
||||
|
|
@ -184,6 +185,10 @@ private class ScanWalletProcessor(
|
|||
session: CardSession,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> Unit,
|
||||
) {
|
||||
val productType = when (card.isSaltPay) {
|
||||
true -> ProductType.SaltPay
|
||||
else -> ProductType.Wallet
|
||||
}
|
||||
scope.launch {
|
||||
val derivations = collectDerivations(card)
|
||||
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
|
||||
|
|
@ -191,11 +196,11 @@ private class ScanWalletProcessor(
|
|||
CompletionResult.Success(
|
||||
ScanResponse(
|
||||
card = card,
|
||||
productType = ProductType.Wallet,
|
||||
productType = productType,
|
||||
walletData = session.environment.walletData,
|
||||
primaryCard = primaryCard
|
||||
)
|
||||
)
|
||||
primaryCard = primaryCard,
|
||||
),
|
||||
),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
|
@ -205,10 +210,10 @@ private class ScanWalletProcessor(
|
|||
is CompletionResult.Success -> {
|
||||
val response = ScanResponse(
|
||||
card = card,
|
||||
productType = ProductType.Wallet,
|
||||
productType = productType,
|
||||
walletData = session.environment.walletData,
|
||||
derivedKeys = result.data.entries,
|
||||
primaryCard = primaryCard
|
||||
primaryCard = primaryCard,
|
||||
)
|
||||
callback(CompletionResult.Success(response))
|
||||
}
|
||||
|
|
@ -219,12 +224,8 @@ private class ScanWalletProcessor(
|
|||
}
|
||||
|
||||
private suspend fun getBlockchainsToDerive(card: Card): List<BlockchainNetwork> {
|
||||
val currenciesRepository = currenciesRepository ?: return emptyList()
|
||||
|
||||
val cardCurrencies = currenciesRepository
|
||||
.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed).toMutableList()
|
||||
|
||||
val blockchainsToDerive = cardCurrencies.ifEmpty {
|
||||
val userTokensRepository = userTokensRepository ?: return emptyList()
|
||||
val blockchainsToDerive = userTokensRepository.loadBlockchainsToDerive(card).toMutableList().ifEmpty {
|
||||
mutableListOf(
|
||||
BlockchainNetwork(Blockchain.Bitcoin, card),
|
||||
BlockchainNetwork(Blockchain.Ethereum, card),
|
||||
|
|
@ -236,7 +237,7 @@ private class ScanWalletProcessor(
|
|||
listOf(
|
||||
BlockchainNetwork(Blockchain.Ethereum, card),
|
||||
BlockchainNetwork(Blockchain.EthereumTestnet, card),
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
if (additionalBlockchainsToDerive != null) {
|
||||
|
|
@ -262,7 +263,6 @@ private class ScanWalletProcessor(
|
|||
|
||||
blockchains.forEach { blockchain ->
|
||||
val curve = blockchain.blockchain.getPrimaryCurve()
|
||||
|
||||
val wallet = card.wallets.firstOrNull { it.curve == curve } ?: return@forEach
|
||||
if (wallet.chainCode == null) return@forEach
|
||||
|
||||
|
|
|
|||
|
|
@ -1,247 +1,12 @@
|
|||
package com.tangem.tap.domain.tokens
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.extensions.getTokens
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.network.api.tangemTech.TangemTechService
|
||||
import com.tangem.network.common.MoshiConverter
|
||||
import com.tangem.tap.common.extensions.appendIf
|
||||
import com.tangem.tap.common.extensions.readJsonFileToString
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.domain.tokens.models.ObsoleteTokenDao
|
||||
import com.tangem.tap.domain.tokens.models.TokenDao
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
class CurrenciesRepository(
|
||||
private val context: Application,
|
||||
private val tangemNetworkService: TangemTechService
|
||||
) {
|
||||
|
||||
private val moshi = MoshiConverter.defaultMoshi()
|
||||
private val blockchainsAdapter: JsonAdapter<List<Blockchain>> = moshi.adapter(
|
||||
Types.newParameterizedType(List::class.java, Blockchain::class.java)
|
||||
)
|
||||
private val tokensAdapter: JsonAdapter<List<TokenDao>> = moshi.adapter(
|
||||
Types.newParameterizedType(List::class.java, TokenDao::class.java)
|
||||
)
|
||||
private val obsoleteTokensAdapter: JsonAdapter<List<ObsoleteTokenDao>> = moshi.adapter(
|
||||
Types.newParameterizedType(List::class.java, ObsoleteTokenDao::class.java)
|
||||
)
|
||||
private val currenciesAdapter: JsonAdapter<CurrenciesFromJson> =
|
||||
moshi.adapter(CurrenciesFromJson::class.java)
|
||||
|
||||
private val blockchainNetworkAdapter: JsonAdapter<List<BlockchainNetwork>> =
|
||||
moshi.adapter(Types.newParameterizedType(List::class.java, BlockchainNetwork::class.java))
|
||||
|
||||
fun saveUpdatedCurrency(cardId: String, blockchainNetwork: BlockchainNetwork) {
|
||||
var changed = false
|
||||
val currencies = loadSavedCurrenciesWithoutMigration(cardId).map {
|
||||
if (it == blockchainNetwork) {
|
||||
changed = true
|
||||
blockchainNetwork
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
val updatedCurrencies = if (changed) currencies else currencies + blockchainNetwork
|
||||
saveCurrencies(cardId, updatedCurrencies.distinct())
|
||||
}
|
||||
|
||||
fun removeToken(cardId: String, token: Token, blockchainNetwork: BlockchainNetwork) {
|
||||
val currencies = loadSavedCurrenciesWithoutMigration(cardId).map {
|
||||
if (it == blockchainNetwork) {
|
||||
it.copy(tokens = it.tokens.filterNot { it == token })
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
saveCurrencies(cardId, currencies)
|
||||
}
|
||||
|
||||
fun removeBlockchain(cardId: String, blockchainNetwork: BlockchainNetwork) {
|
||||
val currencies = loadSavedCurrenciesWithoutMigration(cardId)
|
||||
.filterNot { it == blockchainNetwork }
|
||||
saveCurrencies(cardId, currencies)
|
||||
}
|
||||
|
||||
fun removeCurrencies(cardId: String) {
|
||||
saveCurrencies(cardId, emptyList())
|
||||
}
|
||||
|
||||
@Deprecated("Use BlockchainNetwork instead")
|
||||
private fun loadSavedTokens(cardId: String): List<TokenDao> {
|
||||
val json = try {
|
||||
context.readFileText(getFileNameForTokens(cardId))
|
||||
} catch (exception: Exception) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
return try {
|
||||
tokensAdapter.fromJson(json) ?: emptyList()
|
||||
} catch (exception: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use BlockchainNetwork instead")
|
||||
private fun loadSavedBlockchains(cardId: String): List<Blockchain> {
|
||||
return try {
|
||||
val json = context.readFileText(getFileNameForBlockchains(cardId))
|
||||
blockchainsAdapter.fromJson(json)?.distinct() ?: emptyList()
|
||||
} catch (exception: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadSavedCurrencies(
|
||||
cardId: String,
|
||||
isHdWalletSupported: Boolean = false
|
||||
): List<BlockchainNetwork> {
|
||||
if (DemoHelper.isDemoCardId(cardId)) {
|
||||
return loadDemoCurrencies()
|
||||
}
|
||||
return try {
|
||||
val json = context.readFileText(getFileNameForBlockchains(cardId))
|
||||
blockchainNetworkAdapter.fromJson(json)?.distinct() ?: emptyList()
|
||||
} catch (exception: Exception) {
|
||||
tryToLoadPreviousFormatAndMigrate(cardId, isHdWalletSupported)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadSavedCurrenciesWithoutMigration(
|
||||
cardId: String,
|
||||
): List<BlockchainNetwork> {
|
||||
if (DemoHelper.isDemoCardId(cardId)) {
|
||||
return loadDemoCurrencies()
|
||||
}
|
||||
return try {
|
||||
val json = context.readFileText(getFileNameForBlockchains(cardId))
|
||||
blockchainNetworkAdapter.fromJson(json)?.distinct() ?: emptyList()
|
||||
} catch (exception: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadDemoCurrencies(): List<BlockchainNetwork> {
|
||||
return DemoHelper.config.demoBlockchains.map {
|
||||
BlockchainNetwork(
|
||||
blockchain = it,
|
||||
derivationPath = it.derivationPath(DerivationStyle.LEGACY)?.rawPath,
|
||||
tokens = emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun tryToLoadPreviousFormatAndMigrate(
|
||||
cardId: String,
|
||||
isHdWalletSupported: Boolean = false
|
||||
): List<BlockchainNetwork> {
|
||||
return try {
|
||||
loadSavedCurrenciesOldWay(
|
||||
cardId,
|
||||
isHdWalletSupported
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadSavedCurrenciesOldWay(
|
||||
cardId: String, isHdWalletSupported: Boolean = false
|
||||
): List<BlockchainNetwork> {
|
||||
val blockchains = loadSavedBlockchains(cardId)
|
||||
val tokens = loadSavedTokens(cardId)
|
||||
val ids = getTokensIds(tokens)
|
||||
val derivationStyle = if (isHdWalletSupported) DerivationStyle.LEGACY else null
|
||||
val blockchainNetworks = blockchains.map { blockchain ->
|
||||
BlockchainNetwork(
|
||||
blockchain = blockchain,
|
||||
derivationPath = blockchain.derivationPath(derivationStyle)?.rawPath,
|
||||
tokens = tokens
|
||||
.filter { it.blockchainDao.toBlockchain() == blockchain }
|
||||
.map {
|
||||
val token = it.toToken()
|
||||
token.copy(id = ids[token.contractAddress])
|
||||
}
|
||||
)
|
||||
}
|
||||
saveCurrencies(cardId, blockchainNetworks) // migrate saved currencies
|
||||
return blockchainNetworks
|
||||
}
|
||||
|
||||
private suspend fun getTokensIds(tokens: List<TokenDao>): Map<String, String> = coroutineScope {
|
||||
tokens.map {
|
||||
async {
|
||||
tangemNetworkService.getTokens(
|
||||
contractAddress = it.contractAddress,
|
||||
networkId = it.blockchainDao.toBlockchain().toNetworkId(),
|
||||
active = true,
|
||||
)
|
||||
}
|
||||
}.map { it.await() }
|
||||
.map { (it as? Result.Success)?.data?.coins?.firstOrNull()?.id }
|
||||
.mapIndexedNotNull { index, id ->
|
||||
if (id == null) null else tokens[index].contractAddress to id
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
fun saveCurrencies(cardId: String, currencies: List<BlockchainNetwork>) {
|
||||
val json = blockchainNetworkAdapter.toJson(currencies)
|
||||
context.rewriteFile(json, getFileNameForBlockchains(cardId))
|
||||
}
|
||||
|
||||
private fun Context.readFileText(fileName: String): String =
|
||||
this.openFileInput(fileName).bufferedReader().readText()
|
||||
|
||||
private fun Context.rewriteFile(content: String, fileName: String) {
|
||||
this.openFileOutput(fileName, Context.MODE_PRIVATE).use {
|
||||
it.write(content.toByteArray(), 0, content.length)
|
||||
}
|
||||
}
|
||||
|
||||
fun getTestnetCoins(): List<Currency> {
|
||||
val json = context.assets.readJsonFileToString(FILE_NAME_TESTNET_COINS)
|
||||
return currenciesAdapter.fromJson(json)!!.coins
|
||||
.map { Currency.fromJsonObject(it) }
|
||||
}
|
||||
|
||||
private fun loadTokensJson(blockchain: Blockchain): String? {
|
||||
val fileName = getFileName(blockchain)
|
||||
return try {
|
||||
context.assets.readJsonFileToString(fileName)
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Tokens with the file name %s not found", fileName)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFileName(blockchain: Blockchain): String {
|
||||
return StringBuilder().apply {
|
||||
append(blockchain.id.lowercase(Locale.getDefault()).replace("/test", ""))
|
||||
append("_tokens")
|
||||
appendIf("_testnet") { blockchain.isTestnet() }
|
||||
}.toString()
|
||||
}
|
||||
|
||||
private fun fromJsonToTokensDao(tokenJson: String, blockchain: Blockchain): List<TokenDao> {
|
||||
return obsoleteTokensAdapter.fromJson(tokenJson)!!.map { it.toTokenDao(blockchain) }
|
||||
}
|
||||
|
||||
object CurrenciesRepository {
|
||||
fun getBlockchains(
|
||||
cardFirmware: FirmwareVersion,
|
||||
isTestNet: Boolean = false
|
||||
isTestNet: Boolean = false,
|
||||
): List<Blockchain> {
|
||||
val blockchains = if (cardFirmware < FirmwareVersion.MultiWalletAvailable) {
|
||||
Blockchain.secp256k1Blockchains(isTestNet)
|
||||
|
|
@ -261,21 +26,6 @@ class CurrenciesRepository(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FILE_NAME_PREFIX_TOKENS = "tokens"
|
||||
private const val FILE_NAME_PREFIX_BLOCKCHAINS = "blockchains"
|
||||
private const val FILE_NAME_TESTNET_COINS = "testnet_tokens"
|
||||
|
||||
fun getFileNameForTokens(cardId: String): String = "${FILE_NAME_PREFIX_TOKENS}_$cardId"
|
||||
fun getFileNameForBlockchains(cardId: String): String =
|
||||
"${FILE_NAME_PREFIX_BLOCKCHAINS}_$cardId"
|
||||
}
|
||||
}
|
||||
|
||||
fun Blockchain.getTokensName(): String {
|
||||
return when (this) {
|
||||
Blockchain.Fantom -> "Fantom Opera"
|
||||
else -> this.fullName
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +1,36 @@
|
|||
package com.tangem.tap.domain.tokens
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.extensions.getListOfCoins
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.network.api.tangemTech.CoinsResponse
|
||||
import com.tangem.network.api.tangemTech.TangemTechService
|
||||
import com.tangem.network.common.MoshiConverter
|
||||
import com.tangem.tap.common.AssetReader
|
||||
|
||||
class LoadAvailableCoinsService(
|
||||
private val networkService: TangemTechService,
|
||||
private val currenciesRepository: CurrenciesRepository
|
||||
private val assetReader: AssetReader,
|
||||
) {
|
||||
private val moshi: Moshi by lazy { MoshiConverter.defaultMoshi() }
|
||||
private val currenciesAdapter: JsonAdapter<CurrenciesFromJson> =
|
||||
moshi.adapter(CurrenciesFromJson::class.java)
|
||||
|
||||
suspend fun getSupportedTokens(
|
||||
isTestNet: Boolean = false,
|
||||
supportedBlockchains: List<Blockchain>,
|
||||
page: Int,
|
||||
searchInput: String? = null
|
||||
searchInput: String? = null,
|
||||
): Result<LoadedCoins> {
|
||||
if (isTestNet) {
|
||||
return Result.Success(
|
||||
LoadedCoins(
|
||||
currencies = currenciesRepository.getTestnetCoins().filter(searchInput),
|
||||
currencies = getTestnetCoins().filter(searchInput),
|
||||
moreAvailable = false,
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
val offset = page * LOAD_PER_PAGE
|
||||
|
|
@ -56,21 +63,28 @@ class LoadAvailableCoinsService(
|
|||
active = true,
|
||||
offset = offset,
|
||||
limit = LOAD_PER_PAGE,
|
||||
searchText = searchInput
|
||||
searchText = searchInput,
|
||||
)
|
||||
}
|
||||
|
||||
fun getTestnetCoins(): List<Currency> {
|
||||
val json = assetReader.readAssetAsString(FILE_NAME_TESTNET_COINS)
|
||||
return currenciesAdapter.fromJson(json)!!.coins
|
||||
.map { Currency.fromJsonObject(it) }
|
||||
}
|
||||
|
||||
private fun List<Currency>.filter(searchInput: String?): List<Currency> {
|
||||
if (searchInput.isNullOrBlank()) return this
|
||||
|
||||
return filter{
|
||||
return filter {
|
||||
it.symbol.contains(searchInput, ignoreCase = true) ||
|
||||
it.name.contains(searchInput, ignoreCase = true)
|
||||
it.name.contains(searchInput, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LOAD_PER_PAGE = 100
|
||||
private const val FILE_NAME_TESTNET_COINS = "testnet_tokens"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
package com.tangem.tap.domain.tokens
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.extensions.getTokens
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.network.api.tangemTech.TangemTechService
|
||||
import com.tangem.network.common.MoshiConverter
|
||||
import com.tangem.tap.common.FileReader
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.domain.tokens.models.TokenDao
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
||||
@Deprecated("Use this only for migration")
|
||||
class OldUserTokensRepository(
|
||||
private val fileReader: FileReader,
|
||||
private val tangemNetworkService: TangemTechService,
|
||||
) {
|
||||
private val moshi = MoshiConverter.defaultMoshi()
|
||||
private val blockchainsAdapter: JsonAdapter<List<Blockchain>> = moshi.adapter(
|
||||
Types.newParameterizedType(List::class.java, Blockchain::class.java),
|
||||
)
|
||||
private val tokensAdapter: JsonAdapter<List<TokenDao>> = moshi.adapter(
|
||||
Types.newParameterizedType(List::class.java, TokenDao::class.java),
|
||||
)
|
||||
private val blockchainNetworkAdapter: JsonAdapter<List<BlockchainNetwork>> =
|
||||
moshi.adapter(Types.newParameterizedType(List::class.java, BlockchainNetwork::class.java))
|
||||
|
||||
@Deprecated("Use BlockchainNetwork instead")
|
||||
private fun loadSavedTokens(cardId: String): List<TokenDao> {
|
||||
val json = try {
|
||||
fileReader.readFile(getFileNameForTokens(cardId))
|
||||
} catch (exception: Exception) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
return try {
|
||||
tokensAdapter.fromJson(json) ?: emptyList()
|
||||
} catch (exception: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use BlockchainNetwork instead")
|
||||
private fun loadSavedBlockchains(cardId: String): List<Blockchain> {
|
||||
return try {
|
||||
val json = fileReader.readFile(getFileNameForBlockchains(cardId))
|
||||
blockchainsAdapter.fromJson(json)?.distinct() ?: emptyList()
|
||||
} catch (exception: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use TokensRepository instead")
|
||||
suspend fun loadSavedCurrencies(
|
||||
cardId: String,
|
||||
isHdWalletSupported: Boolean = false,
|
||||
): List<BlockchainNetwork> {
|
||||
return try {
|
||||
val json = fileReader.readFile(getFileNameForBlockchains(cardId))
|
||||
blockchainNetworkAdapter.fromJson(json)?.distinct() ?: emptyList()
|
||||
} catch (exception: Exception) {
|
||||
tryToLoadPreviousFormatAndMigrate(cardId, isHdWalletSupported)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun tryToLoadPreviousFormatAndMigrate(
|
||||
cardId: String,
|
||||
isHdWalletSupported: Boolean = false,
|
||||
): List<BlockchainNetwork> {
|
||||
return try {
|
||||
loadSavedCurrenciesOldWay(
|
||||
cardId,
|
||||
isHdWalletSupported,
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadSavedCurrenciesOldWay(
|
||||
cardId: String, isHdWalletSupported: Boolean = false,
|
||||
): List<BlockchainNetwork> {
|
||||
val blockchains = loadSavedBlockchains(cardId)
|
||||
val tokens = loadSavedTokens(cardId)
|
||||
val ids = getTokensIds(tokens)
|
||||
val derivationStyle = if (isHdWalletSupported) DerivationStyle.LEGACY else null
|
||||
val blockchainNetworks = blockchains.map { blockchain ->
|
||||
BlockchainNetwork(
|
||||
blockchain = blockchain,
|
||||
derivationPath = blockchain.derivationPath(derivationStyle)?.rawPath,
|
||||
tokens = tokens
|
||||
.filter { it.blockchainDao.toBlockchain() == blockchain }
|
||||
.map {
|
||||
val token = it.toToken()
|
||||
token.copy(id = ids[token.contractAddress])
|
||||
},
|
||||
)
|
||||
}
|
||||
return blockchainNetworks
|
||||
}
|
||||
|
||||
private suspend fun getTokensIds(tokens: List<TokenDao>): Map<String, String> = coroutineScope {
|
||||
tokens.map {
|
||||
async {
|
||||
tangemNetworkService.getTokens(
|
||||
contractAddress = it.contractAddress,
|
||||
networkId = it.blockchainDao.toBlockchain().toNetworkId(),
|
||||
active = true,
|
||||
)
|
||||
}
|
||||
}.map { it.await() }
|
||||
.map { (it as? Result.Success)?.data?.coins?.firstOrNull()?.id }
|
||||
.mapIndexedNotNull { index, id ->
|
||||
if (id == null) null else tokens[index].contractAddress to id
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FILE_NAME_PREFIX_TOKENS = "tokens"
|
||||
private const val FILE_NAME_PREFIX_BLOCKCHAINS = "blockchains"
|
||||
private fun getFileNameForTokens(cardId: String): String = "${FILE_NAME_PREFIX_TOKENS}_$cardId"
|
||||
private fun getFileNameForBlockchains(cardId: String): String =
|
||||
"${FILE_NAME_PREFIX_BLOCKCHAINS}_$cardId"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.tap.domain.tokens
|
||||
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.network.api.tangemTech.TangemTechService
|
||||
import com.tangem.network.api.tangemTech.UserTokensResponse
|
||||
import com.tangem.tap.domain.NoDataError
|
||||
|
||||
class UserTokensNetworkService(private val tangemTechService: TangemTechService) {
|
||||
suspend fun getUserTokens(userId: String): Result<UserTokensResponse> {
|
||||
return when (val result = tangemTechService.getUserTokens(userId)) {
|
||||
is Result.Success -> result
|
||||
is Result.Failure -> {
|
||||
val error = result.error
|
||||
if (error is TangemSdkError.NetworkError && error.customMessage.contains("404")) {
|
||||
return Result.Failure(NoDataError(error.customMessage))
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveUserTokens(userId: String, tokens: UserTokensResponse): Result<Unit> {
|
||||
return tangemTechService.putUserTokens(userId, tokens)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
package com.tangem.tap.domain.tokens
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.extensions.calculateSha256
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.extensions.calculateHmacSha256
|
||||
import com.tangem.network.api.tangemTech.TangemTechService
|
||||
import com.tangem.network.api.tangemTech.UserTokensResponse
|
||||
import com.tangem.tap.common.AndroidFileReader
|
||||
import com.tangem.tap.domain.NoDataError
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.models.toBlockchainNetworks
|
||||
import com.tangem.tap.features.wallet.models.toCurrencies
|
||||
import com.tangem.tap.network.NetworkConnectivity
|
||||
import com.tangem.tap.store
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class UserTokensRepository(
|
||||
private val storageService: UserTokensStorageService,
|
||||
private val networkService: UserTokensNetworkService,
|
||||
) {
|
||||
suspend fun getUserTokens(card: Card): List<Currency> {
|
||||
val userId = card.getUserId()
|
||||
if (DemoHelper.isDemoCardId(card.cardId)) {
|
||||
return loadTokensOffline(card, userId).ifEmpty { loadDemoCurrencies() }
|
||||
}
|
||||
|
||||
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
|
||||
return loadTokensOffline(card, userId)
|
||||
}
|
||||
|
||||
return when (val networkResult = networkService.getUserTokens(userId)) {
|
||||
is Result.Success -> {
|
||||
val tokens = networkResult.data.tokens.mapNotNull { Currency.fromTokenResponse(it) }
|
||||
storageService.saveUserTokens(card.getUserId(), tokens.toUserTokensResponse())
|
||||
tokens.distinct()
|
||||
}
|
||||
is Result.Failure -> {
|
||||
handleGetUserTokensFailure(card = card, userId = userId, error = networkResult.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveUserTokens(card: Card, tokens: List<Currency>) {
|
||||
val userTokens = tokens.toUserTokensResponse()
|
||||
networkService.saveUserTokens(card.getUserId(), userTokens)
|
||||
storageService.saveUserTokens(card.getUserId(), userTokens)
|
||||
}
|
||||
|
||||
suspend fun removeUserTokens(card: Card) {
|
||||
val userTokens = emptyList<Currency>().toUserTokensResponse()
|
||||
networkService.saveUserTokens(card.getUserId(), userTokens)
|
||||
storageService.saveUserTokens(card.getUserId(), userTokens)
|
||||
}
|
||||
|
||||
private fun List<Currency>.toUserTokensResponse(): UserTokensResponse {
|
||||
val tokensResponse = this.map { it.toTokenResponse() }
|
||||
return UserTokensResponse(
|
||||
tokens = tokensResponse,
|
||||
group = GROUP_DEFAULT_VALUE,
|
||||
sort = SORT_DEFAULT_VALUE,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun loadBlockchainsToDerive(card: Card): List<BlockchainNetwork> {
|
||||
val userId = card.getUserId()
|
||||
val blockchainNetworks = loadTokensOffline(card, userId).toBlockchainNetworks()
|
||||
|
||||
if (DemoHelper.isDemoCardId(card.cardId)) {
|
||||
return blockchainNetworks
|
||||
.ifEmpty { loadDemoCurrencies().toBlockchainNetworks() }
|
||||
}
|
||||
|
||||
return blockchainNetworks
|
||||
}
|
||||
|
||||
private fun loadDemoCurrencies(): List<Currency> {
|
||||
return DemoHelper.config.demoBlockchains.map {
|
||||
BlockchainNetwork(
|
||||
blockchain = it,
|
||||
derivationPath = it.derivationPath(DerivationStyle.LEGACY)?.rawPath,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
}.flatMap { it.toCurrencies() }
|
||||
}
|
||||
|
||||
private suspend fun handleGetUserTokensFailure(
|
||||
card: Card,
|
||||
userId: String,
|
||||
error: Throwable,
|
||||
): List<Currency> {
|
||||
return when (error) {
|
||||
is NoDataError -> {
|
||||
val tokens = storageService.getUserTokens(card)
|
||||
val userTokens = tokens.toUserTokensResponse()
|
||||
coroutineScope { launch { networkService.saveUserTokens(userId = userId, tokens = userTokens) } }
|
||||
tokens
|
||||
}
|
||||
else -> {
|
||||
val tokens = storageService.getUserTokens(userId) ?: storageService.getUserTokens(card)
|
||||
tokens.distinct()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadTokensOffline(card: Card, userId: String): List<Currency> {
|
||||
return storageService.getUserTokens(userId) ?: storageService.getUserTokens(card)
|
||||
}
|
||||
|
||||
private fun Card.getUserId(): String {
|
||||
val walletPublicKey = this.wallets.firstOrNull()?.publicKey ?: return ""
|
||||
return UserWalletId(walletPublicKey).stringValue
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SORT_DEFAULT_VALUE = "manual"
|
||||
const val GROUP_DEFAULT_VALUE = "none"
|
||||
fun init(context: Context, tangemTechService: TangemTechService): UserTokensRepository {
|
||||
val fileReader = AndroidFileReader(context)
|
||||
val oldUserTokensRepository = OldUserTokensRepository(
|
||||
fileReader, store.state.domainNetworks.tangemTechService,
|
||||
)
|
||||
val storageService = UserTokensStorageService(oldUserTokensRepository, fileReader)
|
||||
val networkService = UserTokensNetworkService(tangemTechService)
|
||||
return UserTokensRepository(storageService, networkService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class UserWalletId(
|
||||
val walletPublicKey: ByteArray,
|
||||
) {
|
||||
val stringValue: String = calculateUserId(walletPublicKey)
|
||||
|
||||
private fun calculateUserId(walletPublicKey: ByteArray): String {
|
||||
val message = "UserWalletID".toByteArray()
|
||||
val keyHash = walletPublicKey.calculateSha256()
|
||||
return message.calculateHmacSha256(keyHash).toHexString()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.tap.domain.tokens
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.tangem.Log
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.network.api.tangemTech.UserTokensResponse
|
||||
import com.tangem.network.common.MoshiConverter
|
||||
import com.tangem.tap.common.FileReader
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.models.toCurrencies
|
||||
|
||||
class UserTokensStorageService(
|
||||
private val oldUserTokensRepository: OldUserTokensRepository,
|
||||
private val fileReader: FileReader,
|
||||
) {
|
||||
private val moshi = MoshiConverter.defaultMoshi()
|
||||
private val userTokensAdapter: JsonAdapter<UserTokensResponse> =
|
||||
moshi.adapter(UserTokensResponse::class.java)
|
||||
|
||||
fun getUserTokens(userId: String): List<Currency>? {
|
||||
return try {
|
||||
val json = fileReader.readFile(getFileNameForUserTokens(userId))
|
||||
userTokensAdapter.fromJson(json)?.tokens?.mapNotNull { Currency.fromTokenResponse(it) }
|
||||
} catch (exception: Exception) {
|
||||
Log.error { exception.stackTraceToString() }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
suspend fun getUserTokens(card: Card): List<Currency> {
|
||||
val blockchainNetworks =
|
||||
oldUserTokensRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
|
||||
return blockchainNetworks.flatMap { it.toCurrencies() }
|
||||
}
|
||||
|
||||
fun saveUserTokens(userId: String, tokens: UserTokensResponse) {
|
||||
val json = userTokensAdapter.toJson(tokens)
|
||||
fileReader.rewriteFile(json, getFileNameForUserTokens(userId))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FILE_NAME_PREFIX_USER_TOKENS = "user_tokens"
|
||||
private fun getFileNameForUserTokens(userId: String): String = "${FILE_NAME_PREFIX_USER_TOKENS}_$userId"
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import com.tangem.common.extensions.toHexString
|
|||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.network.common.MoshiConverter
|
||||
import com.tangem.operations.wallet.CreateWalletResponse
|
||||
import com.tangem.tap.common.AssetReader
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.AnalyticsHandler
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
|
|
@ -119,10 +120,6 @@ class TwinCardsManager(
|
|||
}
|
||||
}
|
||||
|
||||
interface AssetReader {
|
||||
fun readAssetAsString(name: String): String
|
||||
}
|
||||
|
||||
private class Issuer(
|
||||
val id: String,
|
||||
val privateKey: String,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package com.tangem.tap.domain.walletconnect
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
|
|
@ -57,7 +59,15 @@ class WalletConnectManager {
|
|||
private var sessions: MutableMap<WCSession, WalletConnectActiveData> = mutableMapOf()
|
||||
|
||||
fun connect(wcUri: String) {
|
||||
val session = WCSession.from(wcUri) ?: return
|
||||
val session = WCSession.from(wcUri).guard {
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.FailureEstablishingSession(
|
||||
session = null,
|
||||
error = TapError.WalletConnect.UnsupportedLink,
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
if (sessions[session] != null) {
|
||||
store.dispatchOnMain(WalletConnectAction.RefuseOpeningSession)
|
||||
return
|
||||
|
|
@ -65,13 +75,25 @@ class WalletConnectManager {
|
|||
val client = WCClient(httpClient = okHttpClient)
|
||||
setListeners(client)
|
||||
val peerId = UUID.randomUUID().toString()
|
||||
client.connect(session, tangemPeerMeta, peerId)
|
||||
|
||||
try {
|
||||
client.connect(session, tangemPeerMeta, peerId)
|
||||
} catch (exception: IllegalArgumentException) {
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.FailureEstablishingSession(
|
||||
session = null,
|
||||
error = TapError.WalletConnect.UnsupportedLink,
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
sessions[session] = WalletConnectActiveData(
|
||||
peerId = peerId,
|
||||
remotePeerId = null,
|
||||
session = session,
|
||||
client = client,
|
||||
wallet = WalletForSession(cardId = "")
|
||||
wallet = WalletForSession(),
|
||||
)
|
||||
setupConnectionTimeoutCheck(session)
|
||||
}
|
||||
|
|
@ -112,8 +134,12 @@ class WalletConnectManager {
|
|||
}
|
||||
}
|
||||
|
||||
fun restoreSessions() {
|
||||
fun restoreSessions(scanResponse: ScanResponse) {
|
||||
val walletPublicKey = scanResponse.card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }?.publicKey
|
||||
?: return
|
||||
val sessions = walletConnectRepository.loadSavedSessions()
|
||||
// filter sessions for this particular card
|
||||
.filter { it.wallet.walletPublicKey.contentEquals(walletPublicKey) }
|
||||
this.sessions = sessions
|
||||
.map { session ->
|
||||
WalletConnectActiveData(
|
||||
|
|
@ -128,8 +154,8 @@ class WalletConnectManager {
|
|||
setListeners(it.client)
|
||||
it.client.connect(it.session, tangemPeerMeta, it.peerId, it.remotePeerId)
|
||||
}
|
||||
}
|
||||
.map { it.session to it }.toMap().toMutableMap()
|
||||
}.associateBy { it.session }.toMutableMap()
|
||||
|
||||
store.dispatchOnMain(WalletConnectAction.SetSessionsRestored(sessions))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ class WalletConnectNetworkUtils {
|
|||
peer: WCPeerMeta,
|
||||
): Blockchain? {
|
||||
return when {
|
||||
peer.url.contains("pancakeswap.finance") -> {
|
||||
Blockchain.BSC
|
||||
}
|
||||
chainId != null -> {
|
||||
Blockchain.fromChainId(chainId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,7 +92,6 @@ class WalletConnectSdkHelper {
|
|||
)
|
||||
)
|
||||
val dialogData = TransactionRequestDialogData(
|
||||
cardId = session.wallet.cardId,
|
||||
dAppName = session.peerMeta.name,
|
||||
dAppUrl = session.peerMeta.url,
|
||||
amount = value.toFormattedString(decimals),
|
||||
|
|
@ -235,7 +234,6 @@ class WalletConnectSdkHelper {
|
|||
|
||||
|
||||
val dialogData = PersonalSignDialogData(
|
||||
cardId = session.wallet.cardId,
|
||||
dAppName = session.peerMeta.name,
|
||||
message = messageString,
|
||||
session = session.session,
|
||||
|
|
@ -272,11 +270,11 @@ class WalletConnectSdkHelper {
|
|||
suspend fun signPersonalMessage(hashToSign: ByteArray, wallet: WalletForSession): String? {
|
||||
val key = wallet.derivedPublicKey ?: wallet.walletPublicKey
|
||||
val command = SignHashCommand(hashToSign, wallet.walletPublicKey!!, wallet.derivationPath)
|
||||
return when (val result = tangemSdkManager.runTaskAsync(command, wallet.cardId)) {
|
||||
return when (val result = tangemSdkManager.runTaskAsync(command)) {
|
||||
is CompletionResult.Success -> {
|
||||
val hash = result.data.signature
|
||||
return EthereumUtils.prepareSignedMessageData(
|
||||
hash, hashToSign, CryptoUtils.decompressPublicKey(key!!)
|
||||
hash, hashToSign, CryptoUtils.decompressPublicKey(key!!),
|
||||
)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
|
|
|
|||
|
|
@ -1,94 +0,0 @@
|
|||
package com.tangem.tap.domain.walletconnect
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.common.WalletManagerFactory
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.tap.domain.extensions.getPrimaryCurve
|
||||
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
|
||||
import com.tangem.tap.domain.tokens.CurrenciesRepository
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletForSession
|
||||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
|
||||
class WcWalletManagerFactory(
|
||||
private val factory: WalletManagerFactory,
|
||||
private val currenciesRepository: CurrenciesRepository,
|
||||
) {
|
||||
|
||||
suspend fun getWalletManager(
|
||||
wallet: WalletForSession, blockchain: Blockchain, walletState: WalletState
|
||||
): WalletManager? {
|
||||
val blockchainToMake = if (blockchain == Blockchain.Ethereum && wallet.isTestNet) {
|
||||
Blockchain.EthereumTestnet
|
||||
} else {
|
||||
blockchain
|
||||
}
|
||||
|
||||
val blockchainNetwork = BlockchainNetwork(
|
||||
blockchain = blockchainToMake,
|
||||
derivationPath = wallet.derivationPath?.rawPath,
|
||||
tokens = emptyList()
|
||||
)
|
||||
|
||||
return if (walletState.cardId == wallet.cardId) {
|
||||
walletState.getWalletManager(blockchainNetwork)
|
||||
} else {
|
||||
val blockchainNetworkWithTokens = currenciesRepository
|
||||
.loadSavedCurrencies(
|
||||
cardId = wallet.cardId,
|
||||
isHdWalletSupported = wallet.derivationPath != null
|
||||
).firstOrNull { it == blockchainNetwork }
|
||||
|
||||
if (blockchainNetworkWithTokens != null) {
|
||||
factory.makeWalletManager(
|
||||
blockchain = blockchainToMake,
|
||||
publicKey = Wallet.PublicKey(
|
||||
wallet.walletPublicKey!!,
|
||||
wallet.derivedPublicKey,
|
||||
wallet.derivationPath
|
||||
),
|
||||
tokens = blockchainNetworkWithTokens.tokens,
|
||||
curve = blockchainToMake.getPrimaryCurve() ?: EllipticCurve.Secp256k1
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getWalletManager(
|
||||
scanResponse: ScanResponse, blockchain: Blockchain, walletState: WalletState
|
||||
): WalletManager? {
|
||||
val card = scanResponse.card
|
||||
val blockchainToMake = if (blockchain == Blockchain.Ethereum && card.isTestCard) {
|
||||
Blockchain.EthereumTestnet
|
||||
} else {
|
||||
blockchain
|
||||
}
|
||||
|
||||
val blockchainNetwork = BlockchainNetwork(
|
||||
blockchain = blockchainToMake,
|
||||
card = card
|
||||
)
|
||||
|
||||
return if (walletState.cardId == card.cardId) {
|
||||
walletState.getWalletManager(blockchainNetwork)
|
||||
} else {
|
||||
if (currenciesRepository
|
||||
.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
|
||||
.contains(blockchainNetwork)
|
||||
) {
|
||||
factory.makeWalletManagerForApp(
|
||||
scanResponse = scanResponse,
|
||||
blockchainNetwork = blockchainNetwork
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ package com.tangem.tap.features.details.redux
|
|||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.domain.common.isTangemTwins
|
||||
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.AnalyticsParam
|
||||
import com.tangem.tap.common.extensions.dispatchNotification
|
||||
|
|
@ -11,7 +11,6 @@ import com.tangem.tap.common.redux.AppState
|
|||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.currenciesRepository
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
|
||||
|
|
@ -70,13 +69,8 @@ class DetailsMiddleware {
|
|||
}
|
||||
is DetailsAction.CreateBackup -> {
|
||||
store.state.detailsState.scanResponse?.let {
|
||||
store.dispatch(GlobalAction.Onboarding.Start(it, canSkipBackup = false))
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
|
||||
store.dispatch(
|
||||
GlobalAction.Onboarding.Start(
|
||||
it,
|
||||
fromHomeScreen = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
DetailsAction.ScanCard -> {
|
||||
|
|
@ -98,21 +92,21 @@ class DetailsMiddleware {
|
|||
fun handle(action: DetailsAction.ResetToFactory) {
|
||||
when (action) {
|
||||
is DetailsAction.ResetToFactory.Start -> {
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.ResetToFactory))
|
||||
val card = store.state.detailsState.cardSettingsState?.card ?: return
|
||||
if (card.isTangemTwins) {
|
||||
store.dispatch(DetailsAction.ReCreateTwinsWallet)
|
||||
return
|
||||
} else {
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.ResetToFactory))
|
||||
}
|
||||
}
|
||||
is DetailsAction.ResetToFactory.Proceed -> {
|
||||
val card = store.state.detailsState.cardSettingsState?.card ?: return
|
||||
if (card.isTangemTwins()) {
|
||||
store.dispatch(DetailsAction.ReCreateTwinsWallet)
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
val result = tangemSdkManager.resetToFactorySettings(card)
|
||||
withContext(Dispatchers.Main) {
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
currenciesRepository.removeCurrencies(card.cardId)
|
||||
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
(result.error as? TangemSdkError)?.let { error ->
|
||||
|
|
@ -123,7 +117,6 @@ class DetailsMiddleware {
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ private fun prepareSecurityOptions(card: Card): ManageSecurityState {
|
|||
}
|
||||
}
|
||||
val allowedSecurityOptions = when {
|
||||
card.isStart2Coin || card.isTangemNote() -> {
|
||||
card.isStart2Coin || card.isTangemNote || card.isSaltPay -> {
|
||||
EnumSet.of(SecurityOption.LongTap)
|
||||
}
|
||||
card.settings.isBackupAllowed -> {
|
||||
|
|
@ -97,7 +97,7 @@ private fun prepareSecurityOptions(card: Card): ManageSecurityState {
|
|||
private fun isResetToFactoryAllowedByCard(card: Card): Boolean {
|
||||
val notAllowedByAnyWallet = card.wallets.any { it.settings.isPermanent }
|
||||
val notAllowedByCard = notAllowedByAnyWallet ||
|
||||
(card.isWalletDataSupported && (!card.isTangemNote() && !card.settings.isBackupAllowed)) ||
|
||||
(card.isWalletDataSupported && (!card.isTangemNote && !card.settings.isBackupAllowed)) ||
|
||||
card.isSaltPay
|
||||
return !notAllowedByCard
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.features.details.redux.walletconnect
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.tap.common.redux.NotificationAction
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -12,11 +13,9 @@ import com.trustwallet.walletconnect.models.session.WCSession
|
|||
import org.rekotlin.Action
|
||||
|
||||
sealed class WalletConnectAction : Action {
|
||||
|
||||
object ResetState : WalletConnectAction()
|
||||
data class HandleDeepLink(val wcUri: String?) : WalletConnectAction()
|
||||
|
||||
object RestoreSessions : WalletConnectAction()
|
||||
|
||||
data class RestoreSessions(val scanResponse: ScanResponse) : WalletConnectAction()
|
||||
data class StartWalletConnect(
|
||||
val copiedUri: String?,
|
||||
) : WalletConnectAction()
|
||||
|
|
@ -105,6 +104,6 @@ sealed class WalletConnectAction : Action {
|
|||
|
||||
data class Sign(
|
||||
val id: Long, val data: ByteArray, val sessionData: WCSession,
|
||||
) : WalletConnectAction()
|
||||
) : WalletConnectAction()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.features.details.redux.walletconnect
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
|
|
@ -13,12 +14,11 @@ import com.tangem.tap.common.redux.AppState
|
|||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.currenciesRepository
|
||||
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.domain.walletconnect.BnbHelper
|
||||
import com.tangem.tap.domain.walletconnect.WalletConnectManager
|
||||
import com.tangem.tap.domain.walletconnect.WalletConnectNetworkUtils
|
||||
import com.tangem.tap.domain.walletconnect.WcWalletManagerFactory
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
import com.tangem.tap.scope
|
||||
|
|
@ -30,7 +30,7 @@ import org.rekotlin.Middleware
|
|||
import timber.log.Timber
|
||||
|
||||
class WalletConnectMiddleware {
|
||||
private val walletConnectManager = WalletConnectManager()
|
||||
private var walletConnectManager = WalletConnectManager()
|
||||
val walletConnectMiddleware: Middleware<AppState> = { dispatch, state ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
|
|
@ -44,8 +44,9 @@ class WalletConnectMiddleware {
|
|||
if (DemoHelper.tryHandle(state, action)) return
|
||||
|
||||
when (action) {
|
||||
is WalletConnectAction.ResetState -> walletConnectManager = WalletConnectManager()
|
||||
is WalletConnectAction.RestoreSessions -> {
|
||||
walletConnectManager.restoreSessions()
|
||||
walletConnectManager.restoreSessions(action.scanResponse)
|
||||
}
|
||||
is WalletConnectAction.HandleDeepLink -> {
|
||||
if (!action.wcUri.isNullOrBlank()) {
|
||||
|
|
@ -75,7 +76,13 @@ class WalletConnectMiddleware {
|
|||
is WalletConnectAction.ChooseNetwork -> {
|
||||
val data = state()?.walletConnectState?.newSessionData ?: return
|
||||
scope.launch {
|
||||
prepareWalletManager(data.scanResponse, store.state.walletState, action.blockchain, data.session)
|
||||
prepareWalletManager(
|
||||
scanResponse = data.scanResponse,
|
||||
walletState = store.state.walletState,
|
||||
blockchain = action.blockchain,
|
||||
session = data.session,
|
||||
walletConnectManager = walletConnectManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
is WalletConnectAction.ShowClipboardOrScanQrDialog -> {
|
||||
|
|
@ -121,7 +128,8 @@ class WalletConnectMiddleware {
|
|||
)
|
||||
}
|
||||
is WalletConnectAction.ScanCard -> {
|
||||
scanCard(action.session, action.chainId)
|
||||
val scanResponse = store.state.globalState.scanResponse ?: return
|
||||
scanCard(scanResponse, action.session, action.chainId)
|
||||
}
|
||||
is WalletConnectAction.ApproveSession -> {
|
||||
walletConnectManager.approve(action.session)
|
||||
|
|
@ -161,7 +169,6 @@ class WalletConnectMiddleware {
|
|||
data = messageData,
|
||||
session = action.sessionData.session,
|
||||
sessionId = action.id,
|
||||
cardId = action.sessionData.wallet.cardId,
|
||||
dAppName = action.sessionData.peerMeta.name,
|
||||
),
|
||||
),
|
||||
|
|
@ -175,7 +182,6 @@ class WalletConnectMiddleware {
|
|||
data = messageData,
|
||||
session = action.sessionData.session,
|
||||
sessionId = action.id,
|
||||
cardId = action.sessionData.wallet.cardId,
|
||||
dAppName = action.sessionData.peerMeta.name,
|
||||
),
|
||||
),
|
||||
|
|
@ -195,33 +201,26 @@ class WalletConnectMiddleware {
|
|||
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork))
|
||||
return
|
||||
}
|
||||
val factory = WcWalletManagerFactory(
|
||||
factory = store.state.globalState.tapWalletManager.walletManagerFactory,
|
||||
currenciesRepository = currenciesRepository,
|
||||
)
|
||||
val walletState = store.state.walletState
|
||||
scope.launch {
|
||||
val walletManager = factory.getWalletManager(
|
||||
wallet = action.session.wallet,
|
||||
blockchain = blockchain,
|
||||
walletState = walletState,
|
||||
).guard {
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.AddNetwork(blockchain.fullName),
|
||||
),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
val updatedWallet = action.session.wallet.copy(
|
||||
walletPublicKey = walletManager.wallet.publicKey.seedKey,
|
||||
derivedPublicKey = walletManager.wallet.publicKey.derivedKey,
|
||||
derivationPath = walletManager.wallet.publicKey.derivationPath,
|
||||
blockchain = action.blockchain,
|
||||
val walletManager = getWalletManager(
|
||||
wallet = action.session.wallet,
|
||||
blockchain = blockchain,
|
||||
walletState = store.state.walletState,
|
||||
).guard {
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.AddNetwork(blockchain.fullName),
|
||||
),
|
||||
)
|
||||
val updatedSession = action.session.copy(wallet = updatedWallet)
|
||||
store.dispatchOnMain(WalletConnectAction.UpdateBlockchain(updatedSession))
|
||||
return
|
||||
}
|
||||
val updatedWallet = action.session.wallet.copy(
|
||||
walletPublicKey = walletManager.wallet.publicKey.seedKey,
|
||||
derivedPublicKey = walletManager.wallet.publicKey.derivedKey,
|
||||
derivationPath = walletManager.wallet.publicKey.derivationPath,
|
||||
blockchain = action.blockchain,
|
||||
)
|
||||
val updatedSession = action.session.copy(wallet = updatedWallet)
|
||||
store.dispatchOnMain(WalletConnectAction.UpdateBlockchain(updatedSession))
|
||||
}
|
||||
is WalletConnectAction.UpdateBlockchain -> {
|
||||
walletConnectManager.updateBlockchain(action.updatedSession)
|
||||
|
|
@ -229,40 +228,22 @@ class WalletConnectMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun scanCard(session: WalletConnectSession, chainId: Int?) {
|
||||
private fun scanCard(scanResponse: ScanResponse, session: WalletConnectSession, chainId: Int?) {
|
||||
val blockchain = WalletConnectNetworkUtils.parseBlockchain(
|
||||
chainId = chainId,
|
||||
peer = session.peerMeta,
|
||||
) ?: Blockchain.Ethereum
|
||||
).guard {
|
||||
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork))
|
||||
return
|
||||
}
|
||||
|
||||
store.dispatch(
|
||||
GlobalAction.ScanCard(
|
||||
additionalBlockchainsToDerive = listOf(blockchain),
|
||||
onSuccess = { scanResponse ->
|
||||
handleScanResponse(scanResponse = scanResponse, session = session, blockchain = blockchain)
|
||||
},
|
||||
onFailure = {
|
||||
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(null))
|
||||
},
|
||||
R.string.wallet_connect_scan_card_message,
|
||||
),
|
||||
)
|
||||
handleScanResponse(scanResponse = scanResponse, session = session, blockchain = blockchain)
|
||||
}
|
||||
|
||||
private suspend fun getAvailableBlockchains(card: Card, walletState: WalletState): List<Blockchain> {
|
||||
return if (walletState.cardId == card.cardId) {
|
||||
walletState.currencies.filter {
|
||||
it.isBlockchain() && !it.isCustomCurrency(card.derivationStyle) && it.blockchain.isEvm()
|
||||
}.map { it.blockchain }
|
||||
} else {
|
||||
currenciesRepository
|
||||
.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
|
||||
.filter {
|
||||
it.blockchain.isEvm() &&
|
||||
it.derivationPath == it.blockchain.derivationPath(card.derivationStyle)?.rawPath
|
||||
}
|
||||
.map { it.blockchain }
|
||||
}
|
||||
private fun getAvailableBlockchains(card: Card, walletState: WalletState): List<Blockchain> {
|
||||
return walletState.currencies.filter {
|
||||
it.isBlockchain() && !it.isCustomCurrency(card.derivationStyle) && it.blockchain.isEvm()
|
||||
}.map { it.blockchain }
|
||||
}
|
||||
|
||||
private suspend fun prepareWalletManager(
|
||||
|
|
@ -270,12 +251,9 @@ class WalletConnectMiddleware {
|
|||
walletState: WalletState,
|
||||
blockchain: Blockchain,
|
||||
session: WalletConnectSession,
|
||||
walletConnectManager: WalletConnectManager,
|
||||
) {
|
||||
val factory = WcWalletManagerFactory(
|
||||
factory = store.state.globalState.tapWalletManager.walletManagerFactory,
|
||||
currenciesRepository = currenciesRepository,
|
||||
)
|
||||
val walletManager = factory.getWalletManager(scanResponse, blockchain, walletState).guard {
|
||||
val walletManager = getWalletManager(session.wallet, blockchain, walletState).guard {
|
||||
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(session.session))
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.ShowDialog(
|
||||
|
|
@ -292,7 +270,6 @@ class WalletConnectMiddleware {
|
|||
walletManager.wallet.publicKey.blockchainKey
|
||||
}
|
||||
val walletForSession = WalletForSession(
|
||||
cardId = scanResponse.card.cardId,
|
||||
walletPublicKey = wallet.publicKey.seedKey,
|
||||
derivedPublicKey = derivedKey,
|
||||
derivationPath = wallet.publicKey.derivationPath,
|
||||
|
|
@ -325,17 +302,29 @@ class WalletConnectMiddleware {
|
|||
NewWcSessionData(session = updatedSession, scanResponse = scanResponse, blockchain = blockchain),
|
||||
),
|
||||
)
|
||||
val blockchains = if (blockchain.isEvm()) getAvailableBlockchains(card, walletState) else emptyList()
|
||||
store.dispatch(
|
||||
GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.ApproveWcSession(session = updatedSession, networks = blockchains),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
val blockchains = if (blockchain.isEvm()) getAvailableBlockchains(card, walletState) else emptyList()
|
||||
|
||||
withMainContext {
|
||||
store.dispatch(
|
||||
GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.ApproveWcSession(session = updatedSession, networks = blockchains),
|
||||
),
|
||||
)
|
||||
}
|
||||
private fun getWalletManager(
|
||||
wallet: WalletForSession, blockchain: Blockchain, walletState: WalletState,
|
||||
): WalletManager? {
|
||||
val blockchainToMake = if (blockchain == Blockchain.Ethereum && wallet.isTestNet) {
|
||||
Blockchain.EthereumTestnet
|
||||
} else {
|
||||
blockchain
|
||||
}
|
||||
val derivation = blockchainToMake.derivationPath(store.state.globalState.scanResponse?.card?.derivationStyle)
|
||||
?.rawPath
|
||||
val blockchainNetwork = BlockchainNetwork(
|
||||
blockchain = blockchainToMake,
|
||||
derivationPath = derivation,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
return walletState.getWalletManager(blockchainNetwork)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ class WalletConnectReducer {
|
|||
if (action !is WalletConnectAction) return state
|
||||
|
||||
return when (action) {
|
||||
is WalletConnectAction.ResetState -> return WalletConnectState()
|
||||
is WalletConnectAction.ApproveSession.Success -> {
|
||||
state.copy(
|
||||
loading = false,
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ data class WalletConnectSession(
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class WalletForSession(
|
||||
val cardId: String,
|
||||
val walletPublicKey: ByteArray? = null,
|
||||
val derivedPublicKey: ByteArray? = null,
|
||||
val derivationPath: DerivationPath? = null,
|
||||
|
|
@ -59,7 +58,6 @@ data class WalletForSession(
|
|||
|
||||
other as WalletForSession
|
||||
|
||||
if (cardId != other.cardId) return false
|
||||
if (walletPublicKey != null) {
|
||||
if (other.walletPublicKey == null) return false
|
||||
if (!walletPublicKey.contentEquals(other.walletPublicKey)) return false
|
||||
|
|
@ -76,8 +74,7 @@ data class WalletForSession(
|
|||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = cardId.hashCode()
|
||||
result = 31 * result + (walletPublicKey?.contentHashCode() ?: 0)
|
||||
var result = (walletPublicKey?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (derivedPublicKey?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (derivationPath?.hashCode() ?: 0)
|
||||
result = 31 * result + isTestNet.hashCode()
|
||||
|
|
@ -112,7 +109,6 @@ sealed class WalletConnectDialog : StateDialog {
|
|||
val data: BinanceMessageData,
|
||||
val session: WCSession,
|
||||
val sessionId: Long,
|
||||
val cardId: String,
|
||||
val dAppName: String,
|
||||
) : WalletConnectDialog()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.tap.features.details.ui.cardsettings
|
||||
|
||||
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
|
||||
import com.tangem.domain.common.getTwinCardIdForUser
|
||||
import com.tangem.domain.common.isTangemTwins
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.details.redux.CardSettingsState
|
||||
import com.tangem.tap.features.details.redux.DetailsAction
|
||||
|
|
@ -20,7 +20,7 @@ class CardSettingsViewModel(private val store: Store<AppState>) {
|
|||
},
|
||||
)
|
||||
} else {
|
||||
val cardId = if (state.card.isTangemTwins()) {
|
||||
val cardId = if (state.card.isTangemTwins) {
|
||||
state.card.getTwinCardIdForUser()
|
||||
} else {
|
||||
state.cardInfo.cardId
|
||||
|
|
@ -30,7 +30,7 @@ class CardSettingsViewModel(private val store: Store<AppState>) {
|
|||
CardInfo.Issuer(state.cardInfo.issuer),
|
||||
)
|
||||
|
||||
if (!state.card.isTangemTwins()) {
|
||||
if (!state.card.isTangemTwins) {
|
||||
cardDetails.add(CardInfo.SignedHashes(state.cardInfo.signedHashes.toString()))
|
||||
}
|
||||
cardDetails.add(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
|
|
@ -86,7 +87,7 @@ fun WalletConnectDetailsItem(
|
|||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.height(84.dp)
|
||||
.defaultMinSize(minHeight = 84.dp)
|
||||
.fillMaxWidth()
|
||||
.clickable { onItemsClick(SettingsElement.WalletConnect) },
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
|
|
@ -99,7 +100,7 @@ fun WalletConnectDetailsItem(
|
|||
tint = colorResource(id = R.color.all_colors_azure),
|
||||
)
|
||||
Column(
|
||||
modifier = modifier.height(56.dp),
|
||||
modifier = modifier.defaultMinSize(minHeight = 56.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.features.details.ui.details
|
||||
|
||||
import com.tangem.domain.common.TapWorkarounds.isSaltPay
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.tap.common.feedback.FeedbackEmail
|
||||
import com.tangem.tap.common.feedback.SupportInfo
|
||||
|
|
@ -25,7 +26,9 @@ class DetailsViewModel(private val store: Store<AppState>) {
|
|||
if (state.scanResponse?.card?.isMultiwalletAllowed == true) it else null
|
||||
}
|
||||
SettingsElement.LinkMoreCards -> {
|
||||
if (state.createBackupAllowed) it else null
|
||||
// if (state.createBackupAllowed) it else null
|
||||
// TODO: SaltPay: temporary excluding backup process for Visa cards
|
||||
if (state.createBackupAllowed && state.scanResponse?.card?.isSaltPay != true) it else null
|
||||
}
|
||||
SettingsElement.PrivacyPolicy -> {
|
||||
if (state.privacyPolicyUrl != null) it else null
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.tap.features.details.ui.resetcard
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -89,11 +91,14 @@ fun ResetCardView(
|
|||
color = colorResource(id = R.color.text_secondary),
|
||||
)
|
||||
|
||||
Spacer(modifier = modifier.size(44.dp))
|
||||
Spacer(modifier = modifier.size(28.dp))
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(end = 20.dp),
|
||||
.clickable(
|
||||
onClick = { state.onAcceptWarningToggleClick(!state.accepted) },
|
||||
)
|
||||
.padding(top = 16.dp, bottom = 16.dp),
|
||||
) {
|
||||
IconToggleButton(
|
||||
checked = state.accepted,
|
||||
|
|
@ -120,13 +125,15 @@ fun ResetCardView(
|
|||
text = stringResource(id = R.string.reset_card_to_factory_warning_message),
|
||||
style = TangemTypography.body2,
|
||||
color = colorResource(id = R.color.text_secondary),
|
||||
modifier = modifier
|
||||
.padding(end = 20.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = modifier.size(32.dp))
|
||||
Spacer(modifier = modifier.size(16.dp))
|
||||
Box(
|
||||
modifier = modifier
|
||||
.padding(start = 16.dp, end = 16.dp),
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 32.dp),
|
||||
) {
|
||||
DetailsMainButton(
|
||||
title = stringResource(id = R.string.reset_card_to_factory_button_title),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.zxing.Result
|
||||
import com.otaliastudios.cameraview.CameraView
|
||||
|
|
@ -23,6 +24,7 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, true) }
|
||||
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
|
|
@ -54,7 +56,7 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
|
|||
|
||||
override fun handleResult(result: Result) {
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
|
||||
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, false) }
|
||||
if (!result.text.isNullOrBlank()) {
|
||||
store.dispatch(WalletConnectAction.OpenSession(result.text))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
package com.tangem.tap.features.details.ui.walletconnect
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
||||
class WalletConnectSessionsAdapter
|
||||
: ListAdapter<WalletConnectSession, WalletConnectSessionsAdapter.SessionsViewHolder>(
|
||||
DiffUtilCallback
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SessionsViewHolder {
|
||||
val layout = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_wallet_connect_session, parent, false)
|
||||
return SessionsViewHolder(layout)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SessionsViewHolder, position: Int) {
|
||||
holder.bind(currentList[position])
|
||||
}
|
||||
|
||||
object DiffUtilCallback : DiffUtil.ItemCallback<WalletConnectSession>() {
|
||||
override fun areContentsTheSame(
|
||||
oldItem: WalletConnectSession, newItem: WalletConnectSession,
|
||||
) = oldItem == newItem
|
||||
|
||||
override fun areItemsTheSame(
|
||||
oldItem: WalletConnectSession, newItem: WalletConnectSession,
|
||||
) = oldItem == newItem
|
||||
}
|
||||
|
||||
class SessionsViewHolder(val view: View) :
|
||||
RecyclerView.ViewHolder(view) {
|
||||
|
||||
fun bind(session: WalletConnectSession) {
|
||||
view.findViewById<TextView>(R.id.tv_card_id).text = view.context.getString(
|
||||
R.string.wallet_connect_card_number, session.wallet.cardId
|
||||
)
|
||||
view.findViewById<TextView>(R.id.tv_d_app_name).text = session.peerMeta.name
|
||||
|
||||
view.findViewById<Chip>(R.id.btn_disconnect).setOnClickListener {
|
||||
store.dispatch(WalletConnectAction.DisconnectSession(session.session))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -140,13 +140,12 @@ private fun WalletConnectSessions(
|
|||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Text(
|
||||
text = session.description,
|
||||
style = TangemTypography.subtitle1,
|
||||
color = colorResource(id = R.color.text_primary_1),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = session.description,
|
||||
style = TangemTypography.subtitle1,
|
||||
color = colorResource(id = R.color.text_primary_1),
|
||||
modifier = modifier.weight(1f),
|
||||
)
|
||||
IconButton(
|
||||
onClick = { state.onRemoveSession(session.sessionId) },
|
||||
) {
|
||||
|
|
@ -169,7 +168,6 @@ fun WalletConnectScreenPreview() {
|
|||
sessions = listOf(
|
||||
WcSessionForScreen(
|
||||
description = "session from some dApp",
|
||||
cardId = "12312312321",
|
||||
sessionId = "",
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -11,14 +11,12 @@ data class WalletConnectScreenState(
|
|||
|
||||
data class WcSessionForScreen(
|
||||
val description: String,
|
||||
val cardId: String,
|
||||
val sessionId: String,
|
||||
) {
|
||||
companion object {
|
||||
fun fromSession(session: WalletConnectSession): WcSessionForScreen {
|
||||
return WcSessionForScreen(
|
||||
description = session.peerMeta.name,
|
||||
cardId = session.wallet.cardId,
|
||||
sessionId = session.session.toUri(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ class ApproveWcSessionDialog {
|
|||
fun create(session: WalletConnectSession, networks: List<Blockchain>, context: Context): AlertDialog {
|
||||
val message = context.getString(
|
||||
R.string.wallet_connect_request_session_start,
|
||||
session.wallet.cardId,
|
||||
session.peerMeta.name,
|
||||
session.peerMeta.url,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ class BnbTransactionDialog {
|
|||
data: BinanceMessageData,
|
||||
session: WCSession,
|
||||
sessionId: Long,
|
||||
cardId: String,
|
||||
dAppName: String,
|
||||
context: Context,
|
||||
): AlertDialog {
|
||||
|
|
@ -40,7 +39,7 @@ class BnbTransactionDialog {
|
|||
|
||||
val fullMessage = context.getString(
|
||||
R.string.wallet_connect_bnb_sign_message,
|
||||
dAppName, cardId, message
|
||||
dAppName, message,
|
||||
)
|
||||
val positiveButtonTitle = context.getText(R.string.common_sign)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,10 +15,12 @@ class PersonalSignDialog {
|
|||
context: Context,
|
||||
): AlertDialog {
|
||||
val message =
|
||||
context.getString(R.string.wallet_connect_alert_sign_message, data.cardId) +
|
||||
context.getString(R.string.wallet_connect_personal_sign_message,
|
||||
data.dAppName,
|
||||
data.message)
|
||||
context.getString(R.string.wallet_connect_alert_sign_message) +
|
||||
context.getString(
|
||||
R.string.wallet_connect_personal_sign_message,
|
||||
data.dAppName,
|
||||
data.message,
|
||||
)
|
||||
return AlertDialog.Builder(context).apply {
|
||||
setTitle(context.getString(R.string.wallet_connect))
|
||||
setMessage(message)
|
||||
|
|
@ -37,7 +39,6 @@ class PersonalSignDialog {
|
|||
}
|
||||
|
||||
data class PersonalSignDialogData(
|
||||
val cardId: String,
|
||||
val dAppName: String,
|
||||
val message: String,
|
||||
val session: WCSession,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ class TransactionDialog {
|
|||
): AlertDialog {
|
||||
val message = context.getString(
|
||||
R.string.wallet_connect_create_tx_message,
|
||||
data.cardId,
|
||||
data.dAppName,
|
||||
data.dAppUrl,
|
||||
data.amount,
|
||||
|
|
@ -53,7 +52,6 @@ class TransactionDialog {
|
|||
}
|
||||
|
||||
data class TransactionRequestDialogData(
|
||||
val cardId: String,
|
||||
val dAppName: String,
|
||||
val dAppUrl: String,
|
||||
val amount: String,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ data class ButtonData(
|
|||
interface DialogMessageData
|
||||
|
||||
data class WcTransactionDialogMessageData(
|
||||
val cardId: String,
|
||||
val dAppName: String,
|
||||
val dAppUrl: String,
|
||||
val amount: String,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import androidx.activity.OnBackPressedCallback
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.transition.TransitionInflater
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.tangem.tap.common.extensions.configureSettings
|
||||
import com.tangem.tap.common.extensions.hide
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
|
||||
|
|
@ -16,17 +17,20 @@ import com.tangem.wallet.databinding.FragmentDisclaimerBinding
|
|||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer),
|
||||
StoreSubscriber<DisclaimerState> {
|
||||
StoreSubscriber<DisclaimerState> {
|
||||
|
||||
private val binding: FragmentDisclaimerBinding by viewBinding(FragmentDisclaimerBinding::bind)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
}
|
||||
})
|
||||
activity?.onBackPressedDispatcher?.addCallback(
|
||||
this,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
}
|
||||
},
|
||||
)
|
||||
val inflater = TransitionInflater.from(requireContext())
|
||||
enterTransition = inflater.inflateTransition(android.R.transition.slide_bottom)
|
||||
exitTransition = inflater.inflateTransition(android.R.transition.slide_top)
|
||||
|
|
@ -51,9 +55,15 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer),
|
|||
binding.toolbar.setNavigationOnClickListener {
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
}
|
||||
initAndLoadTOS()
|
||||
setOnClickListeners()
|
||||
}
|
||||
|
||||
private fun initAndLoadTOS() {
|
||||
binding.webView.configureSettings()
|
||||
binding.webView.loadUrl("file:///android_asset/tos.html")
|
||||
}
|
||||
|
||||
private fun setOnClickListeners() {
|
||||
binding.btnAccept.setOnClickListener { store.dispatch(DisclaimerAction.AcceptDisclaimer) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,16 +81,12 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
|
|||
StoriesScreen(
|
||||
homeState,
|
||||
onScanButtonClick = { store.dispatch(HomeAction.ReadCard) },
|
||||
onShopButtonClick = {
|
||||
store.dispatch(
|
||||
HomeAction.GoToShop(store.state.globalState.userCountryCode)
|
||||
)
|
||||
},
|
||||
onShopButtonClick = { store.dispatch(HomeAction.GoToShop(store.state.globalState.userCountryCode)) },
|
||||
onSearchTokensClick = {
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
|
||||
store.dispatch(TokensAction.AllowToAddTokens(false))
|
||||
store.dispatch(TokensAction.LoadCurrencies())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ import com.tangem.tap.features.home.compose.content.StoriesWeb3
|
|||
import com.tangem.tap.features.home.compose.views.HomeButtons
|
||||
import com.tangem.tap.features.home.compose.views.StoriesProgressBar
|
||||
import com.tangem.tap.features.home.redux.HomeState
|
||||
import com.tangem.tap.features.wallet.redux.ProgressState
|
||||
import com.tangem.wallet.R
|
||||
import kotlin.math.max
|
||||
|
||||
|
|
@ -74,7 +73,7 @@ fun StoriesScreen(
|
|||
}
|
||||
|
||||
val isPressed = remember { mutableStateOf(false) }
|
||||
val isPaused = isPressed.value || homeState.value.btnScanState.progressState == ProgressState.Loading
|
||||
val isPaused = isPressed.value || homeState.value.scanInProgress
|
||||
|
||||
val hideContent = remember { mutableStateOf(true) }
|
||||
|
||||
|
|
@ -88,10 +87,10 @@ fun StoriesScreen(
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFF090E13))
|
||||
.background(Color(0xFF090E13)),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
|
|
@ -109,7 +108,7 @@ fun StoriesScreen(
|
|||
isPressed.value = false
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
|
|
@ -127,7 +126,7 @@ fun StoriesScreen(
|
|||
isPressed.value = false
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
if (!isDarkBackground) {
|
||||
|
|
@ -135,7 +134,7 @@ fun StoriesScreen(
|
|||
modifier = Modifier.fillMaxSize(),
|
||||
painter = painterResource(id = R.drawable.ic_overlay),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillBounds
|
||||
contentScale = ContentScale.FillBounds,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -165,7 +164,7 @@ fun StoriesScreen(
|
|||
.height(17.dp)
|
||||
.alpha(if (hideContent.value) 0f else 1f)
|
||||
.align(Alignment.Start),
|
||||
colorFilter = if (isDarkBackground) null else ColorFilter.tint(Color.Black)
|
||||
colorFilter = if (isDarkBackground) null else ColorFilter.tint(Color.Black),
|
||||
)
|
||||
when (currentStep.value) {
|
||||
1 -> FirstStoriesContent(isPaused, currentStep.duration()) { hideContent.value = it }
|
||||
|
|
@ -190,27 +189,28 @@ fun StoriesScreen(
|
|||
.height(48.dp),
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
backgroundColor = Color.White,
|
||||
contentColor = Color(0xFF080C10)
|
||||
)
|
||||
contentColor = Color(0xFF080C10),
|
||||
),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_search),
|
||||
contentDescription = null
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.search_tokens_title),
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 16.sp,
|
||||
textAlign = TextAlign.Center
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
HomeButtons(
|
||||
isDarkBackground = isDarkBackground,
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, top = 0.dp, end = 16.dp, bottom = 37.dp)
|
||||
.fillMaxWidth(),
|
||||
isDarkBackground = isDarkBackground,
|
||||
btnScanStateInProgress = homeState.value.btnScanStateInProgress,
|
||||
onScanButtonClick = onScanButtonClick,
|
||||
onShopButtonClick = onShopButtonClick
|
||||
onShopButtonClick = onShopButtonClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ package com.tangem.tap.features.home.compose.views
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -19,53 +21,89 @@ import com.tangem.wallet.R
|
|||
|
||||
@Composable
|
||||
fun HomeButtons(
|
||||
isDarkBackground: Boolean, modifier: Modifier = Modifier,
|
||||
modifier: Modifier = Modifier,
|
||||
isDarkBackground: Boolean,
|
||||
btnScanStateInProgress: Boolean,
|
||||
onScanButtonClick: () -> Unit,
|
||||
onShopButtonClick: () -> Unit
|
||||
onShopButtonClick: () -> Unit,
|
||||
) {
|
||||
val darkColorBackground = Color(0xFF26292E)
|
||||
val darkColorButton = Color(0xFF080C10)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
modifier = modifier
|
||||
modifier = modifier,
|
||||
) {
|
||||
Button(
|
||||
ProgressButton(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(48.dp),
|
||||
inProgress = btnScanStateInProgress,
|
||||
backgroundColor = if (isDarkBackground) darkColorBackground else Color.White,
|
||||
contentColor = if (isDarkBackground) Color.White else darkColorButton,
|
||||
onClick = onScanButtonClick,
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
backgroundColor = if (isDarkBackground) darkColorBackground else Color.White,
|
||||
contentColor = if (isDarkBackground) Color.White else darkColorButton
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.home_button_scan),
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 16.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
progressContent = {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(30.dp),
|
||||
color = if (isDarkBackground) Color.White else darkColorBackground,
|
||||
strokeWidth = 3.dp,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.home_button_scan),
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 16.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
)
|
||||
SpacerS8()
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(48.dp),
|
||||
onClick = onShopButtonClick,
|
||||
enabled = !btnScanStateInProgress,
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
backgroundColor = if (isDarkBackground) Color.White else darkColorBackground,
|
||||
contentColor = if (isDarkBackground) darkColorButton else Color.White
|
||||
)
|
||||
contentColor = if (isDarkBackground) darkColorButton else Color.White,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.home_button_order),
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 16.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProgressButton(
|
||||
modifier: Modifier,
|
||||
backgroundColor: Color,
|
||||
contentColor: Color,
|
||||
onClick: () -> Unit,
|
||||
inProgress: Boolean,
|
||||
progressContent: @Composable (() -> Unit)? = null,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Button(
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
backgroundColor = backgroundColor,
|
||||
contentColor = contentColor,
|
||||
),
|
||||
) {
|
||||
if (progressContent != null && inProgress) {
|
||||
progressContent()
|
||||
} else {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import org.rekotlin.Action
|
|||
sealed class HomeAction : Action {
|
||||
// from ui
|
||||
object ReadCard : HomeAction()
|
||||
data class ScanInProgress(val scanInProgress: Boolean) : HomeAction()
|
||||
data class GoToShop(val userCountryCode: String?) : HomeAction()
|
||||
|
||||
// internal
|
||||
|
|
@ -14,5 +15,4 @@ sealed class HomeAction : Action {
|
|||
|
||||
data class SetTermsOfUseState(val isDisclaimerAccepted: Boolean) : HomeAction()
|
||||
data class ChangeScanCardButtonState(val state: IndeterminateProgressButton) : HomeAction()
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,21 @@
|
|||
package com.tangem.tap.features.home.redux
|
||||
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.extensions.withIOContext
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
|
||||
import com.tangem.tap.common.analytics.AnalyticsEvent
|
||||
import com.tangem.tap.common.analytics.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.GetCardSourceParams
|
||||
import com.tangem.tap.common.entities.IndeterminateProgressButton
|
||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchOpenUrl
|
||||
import com.tangem.tap.common.extensions.onCardScanned
|
||||
import com.tangem.tap.common.post
|
||||
import com.tangem.tap.common.postUi
|
||||
import com.tangem.tap.common.postUiDelayBg
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
|
|
@ -19,13 +25,22 @@ import com.tangem.tap.features.home.BELARUS_COUNTRY_CODE
|
|||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.features.home.redux.HomeMiddleware.Companion.BUY_WALLET_URL
|
||||
import com.tangem.tap.features.onboarding.OnboardingHelper
|
||||
import com.tangem.tap.features.onboarding.OnboardingSaltPayHelper
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayRegistrationError
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
|
||||
import com.tangem.tap.features.send.redux.states.ButtonState
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.DispatchFunction
|
||||
import org.rekotlin.Middleware
|
||||
|
||||
class HomeMiddleware {
|
||||
|
|
@ -36,82 +51,146 @@ class HomeMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private val homeMiddleware: Middleware<AppState> = { _, _ ->
|
||||
private val homeMiddleware: Middleware<AppState> = { dispatch, state ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
when (action) {
|
||||
is HomeAction.Init -> {
|
||||
store.dispatch(GlobalAction.RestoreAppCurrency)
|
||||
store.dispatch(GlobalAction.ExchangeManager.Init)
|
||||
store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted()))
|
||||
store.dispatch(GlobalAction.FetchUserCountry)
|
||||
}
|
||||
is HomeAction.ShouldScanCardOnResume -> {
|
||||
if (action.shouldScanCard) {
|
||||
store.dispatch(HomeAction.ShouldScanCardOnResume(false))
|
||||
postUi(700) { store.dispatch(HomeAction.ReadCard) }
|
||||
}
|
||||
}
|
||||
is HomeAction.ReadCard -> {
|
||||
handleReadCard()
|
||||
// store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomTokens))
|
||||
}
|
||||
is HomeAction.GoToShop -> {
|
||||
when (action.userCountryCode) {
|
||||
RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE ->
|
||||
store.dispatchOpenUrl(BUY_WALLET_URL)
|
||||
else -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop))
|
||||
}
|
||||
store.state.globalState.analyticsHandlers?.triggerEvent(
|
||||
event = AnalyticsEvent.GET_CARD,
|
||||
params = mapOf(AnalyticsParam.SOURCE.param to GetCardSourceParams.WELCOME.param)
|
||||
)
|
||||
}
|
||||
}
|
||||
handleHomeAction(state, action, dispatch)
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReadCard() {
|
||||
if (!preferencesStorage.wasDisclaimerAccepted()) {
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
|
||||
} else {
|
||||
changeButtonState(ButtonState.PROGRESS)
|
||||
store.dispatch(GlobalAction.ScanCard(onSuccess = { scanResponse ->
|
||||
store.state.globalState.tapWalletManager.updateConfigManager(scanResponse)
|
||||
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
|
||||
|
||||
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
|
||||
val navigateTo = OnboardingHelper.whereToNavigate(scanResponse)
|
||||
store.dispatch(GlobalAction.Onboarding.Start(scanResponse))
|
||||
navigateTo(navigateTo)
|
||||
private fun handleHomeAction(appState: () -> AppState?, action: Action, dispatch: DispatchFunction) {
|
||||
when (action) {
|
||||
is HomeAction.Init -> {
|
||||
store.dispatch(GlobalAction.RestoreAppCurrency)
|
||||
store.dispatch(GlobalAction.ExchangeManager.Init)
|
||||
store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted()))
|
||||
store.dispatch(GlobalAction.FetchUserCountry)
|
||||
}
|
||||
is HomeAction.ShouldScanCardOnResume -> {
|
||||
if (action.shouldScanCard) {
|
||||
store.dispatch(HomeAction.ShouldScanCardOnResume(false))
|
||||
postUiDelayBg(700) { store.dispatch(HomeAction.ReadCard) }
|
||||
}
|
||||
}
|
||||
is HomeAction.ReadCard -> {
|
||||
if (!preferencesStorage.wasDisclaimerAccepted()) {
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
|
||||
} else {
|
||||
scope.launch {
|
||||
store.onCardScanned(scanResponse)
|
||||
withMainContext {
|
||||
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
|
||||
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly))
|
||||
navigateTo(AppScreen.OnboardingTwins)
|
||||
} else {
|
||||
navigateTo(AppScreen.Wallet, null)
|
||||
changeButtonState(ButtonState.PROGRESS)
|
||||
val scanCardAction = GlobalAction.ScanCard(
|
||||
onSuccess = ::onScanSuccess,
|
||||
onFailure = ::onScanFailure,
|
||||
)
|
||||
store.dispatch(HomeAction.ScanInProgress(true))
|
||||
postUiDelayBg(300) { store.dispatch(scanCardAction) }
|
||||
}
|
||||
}
|
||||
is HomeAction.GoToShop -> {
|
||||
when (action.userCountryCode) {
|
||||
RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE -> store.dispatchOpenUrl(BUY_WALLET_URL)
|
||||
else -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop))
|
||||
}
|
||||
store.state.globalState.analyticsHandlers?.triggerEvent(
|
||||
event = AnalyticsEvent.GET_CARD,
|
||||
params = mapOf(AnalyticsParam.SOURCE.param to GetCardSourceParams.WELCOME.param),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onScanSuccess(scanResponse: ScanResponse) {
|
||||
val globalState = store.state.globalState
|
||||
val tapWalletManager = globalState.tapWalletManager
|
||||
tapWalletManager.updateConfigManager(scanResponse)
|
||||
|
||||
store.dispatch(HomeAction.ScanInProgress(false))
|
||||
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
|
||||
|
||||
if (scanResponse.isSaltPay()) {
|
||||
if (scanResponse.isSaltPayVisa()) {
|
||||
scope.launch {
|
||||
val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse)
|
||||
delay(500)
|
||||
withMainContext {
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
val isOnboardingCase = result.data
|
||||
if (isOnboardingCase) {
|
||||
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
|
||||
val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
|
||||
store.dispatch(OnboardingSaltPayAction.Init.SetDependencies(manager, config))
|
||||
store.dispatch(OnboardingSaltPayAction.Update)
|
||||
// store.dispatch(OnboardingSaltPayAction.Init.DiscardBackupSteps)
|
||||
navigateTo(AppScreen.OnboardingWallet)
|
||||
} else {
|
||||
navigateTo(AppScreen.Wallet)
|
||||
withIOContext { store.onCardScanned(scanResponse) }
|
||||
}
|
||||
}
|
||||
is Result.Failure -> {
|
||||
changeButtonState(ButtonState.ENABLED)
|
||||
when (val error = result.error) {
|
||||
is SaltPayRegistrationError -> {
|
||||
val dialog = when (error) {
|
||||
is SaltPayRegistrationError.NoGas -> SaltPayDialog.NoFundsForActivation
|
||||
else -> SaltPayDialog.RegistrationError(error)
|
||||
}
|
||||
store.dispatchDialogShow(dialog)
|
||||
}
|
||||
else -> {
|
||||
store.dispatchDebugErrorNotification(
|
||||
error.localizedMessage ?: "SaltPay: Unknown error",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, onFailure = {
|
||||
changeButtonState(ButtonState.ENABLED)
|
||||
}))
|
||||
} else {
|
||||
if (scanResponse.card.backupStatus?.isActive == false) {
|
||||
changeButtonState(ButtonState.ENABLED)
|
||||
store.dispatchDialogShow(
|
||||
AppDialog.SimpleOkDialogRes(
|
||||
headerId = R.string.saltpay_error_empty_backup_title,
|
||||
messageId = R.string.saltpay_error_empty_backup_message,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
navigateTo(AppScreen.Wallet, null)
|
||||
scope.launch { store.onCardScanned(scanResponse) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
|
||||
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = true))
|
||||
val appScreen = OnboardingHelper.whereToNavigate(scanResponse)
|
||||
navigateTo(appScreen)
|
||||
} else {
|
||||
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
|
||||
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly))
|
||||
navigateTo(AppScreen.OnboardingTwins)
|
||||
} else {
|
||||
navigateTo(AppScreen.Wallet, null)
|
||||
}
|
||||
scope.launch { store.onCardScanned(scanResponse) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onScanFailure(error: TangemError) {
|
||||
store.dispatch(HomeAction.ScanInProgress(false))
|
||||
changeButtonState(ButtonState.ENABLED)
|
||||
}
|
||||
|
||||
private fun changeButtonState(state: ButtonState) {
|
||||
val btnState = IndeterminateProgressButton(state)
|
||||
store.dispatch(HomeAction.ChangeScanCardButtonState(btnState))
|
||||
store.dispatch(HomeAction.ChangeScanCardButtonState(IndeterminateProgressButton(state)))
|
||||
}
|
||||
|
||||
private fun navigateTo(screen: AppScreen, transition: FragmentShareTransition? = null) {
|
||||
post(DELAY_SDK_DIALOG_CLOSE) {
|
||||
postUiDelayBg(DELAY_SDK_DIALOG_CLOSE) {
|
||||
changeButtonState(ButtonState.ENABLED)
|
||||
store.dispatch(NavigationAction.NavigateTo(screen, transition))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ private fun internalReduce(action: Action, state: AppState): HomeState {
|
|||
|
||||
var state = state.homeState
|
||||
when (action) {
|
||||
is HomeAction.ScanInProgress -> {
|
||||
state = state.copy(scanInProgress = action.scanInProgress)
|
||||
}
|
||||
is HomeAction.ShouldScanCardOnResume -> {
|
||||
state = state.copy(shouldScanCardOnResume = action.shouldScanCard)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,15 @@ package com.tangem.tap.features.home.redux
|
|||
|
||||
import com.tangem.tap.common.entities.IndeterminateProgressButton
|
||||
import com.tangem.tap.features.send.redux.states.ButtonState
|
||||
import com.tangem.tap.features.wallet.redux.ProgressState
|
||||
import org.rekotlin.StateType
|
||||
|
||||
data class HomeState(
|
||||
val shouldScanCardOnResume: Boolean = false,
|
||||
val scanInProgress: Boolean = false,
|
||||
val btnScanState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.ENABLED),
|
||||
) : StateType
|
||||
) : StateType {
|
||||
|
||||
val btnScanStateInProgress: Boolean
|
||||
get() = btnScanState.progressState == ProgressState.Loading
|
||||
}
|
||||
|
|
@ -15,16 +15,14 @@ class OnboardingHelper {
|
|||
fun isOnboardingCase(response: ScanResponse): Boolean {
|
||||
val cardInfoStorage = preferencesStorage.usedCardsPrefStorage
|
||||
return when {
|
||||
response.productType == ProductType.Twins -> {
|
||||
response.isTangemTwins() -> {
|
||||
if (!response.twinsIsTwinned()) {
|
||||
true
|
||||
} else {
|
||||
cardInfoStorage.activationIsStarted(response.card.cardId)
|
||||
}
|
||||
}
|
||||
response.card.hasWallets() -> {
|
||||
cardInfoStorage.activationIsStarted(response.card.cardId)
|
||||
}
|
||||
response.card.hasWallets() -> cardInfoStorage.activationIsStarted(response.card.cardId)
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
|
@ -38,6 +36,7 @@ class OnboardingHelper {
|
|||
AppScreen.OnboardingOther
|
||||
}
|
||||
ProductType.Twins -> AppScreen.OnboardingTwins
|
||||
ProductType.SaltPay -> AppScreen.OnboardingWallet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.tap.features.onboarding
|
||||
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.extensions.successOr
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayRegistrationError
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.SaltPayRegistrationStep
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.toSaltPayStep
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class OnboardingSaltPayHelper {
|
||||
companion object {
|
||||
|
||||
suspend fun isOnboardingCase(scanResponse: ScanResponse): Result<Boolean> {
|
||||
return try {
|
||||
val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
|
||||
val registrationResponseItem = manager.checkRegistration().successOr { return it }
|
||||
val registrationStep = registrationResponseItem.toSaltPayStep()
|
||||
manager.checkHasGas().successOr { return it }
|
||||
|
||||
val isRegistrationOnboardingCase = registrationStep != SaltPayRegistrationStep.Finished
|
||||
val isBackupOnboardingCase = scanResponse.card.backupStatus?.isActive == false
|
||||
Result.Success(isRegistrationOnboardingCase || isBackupOnboardingCase)
|
||||
} catch (ex: Exception) {
|
||||
Result.Failure(ex)
|
||||
} catch (error: SaltPayRegistrationError){
|
||||
Result.Failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
|
|||
startPostponedEnterTransition()
|
||||
|
||||
binding.toolbar.setTitle(R.string.onboarding_title)
|
||||
btnRefreshBalanceWidget = RefreshBalanceWidget(binding.onboardingTopContainer.onboardingMainContainer)
|
||||
btnRefreshBalanceWidget = RefreshBalanceWidget(binding.onboardingTopContainer.onboardingWalletContainer)
|
||||
|
||||
store.dispatch(OnboardingNoteAction.LoadCardArtwork)
|
||||
store.dispatch(OnboardingNoteAction.DetermineStepOfScreen)
|
||||
|
|
@ -207,10 +207,10 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
|
|||
with(binding.onboardingTopContainer) {
|
||||
val constraintSet = ConstraintSet()
|
||||
constraintSet.clone(requireContext(), layoutId)
|
||||
constraintSet.applyTo(onboardingMainContainer)
|
||||
constraintSet.applyTo(onboardingWalletContainer)
|
||||
val transition = InternalNoteLayoutTransition()
|
||||
transition.interpolator = OvershootInterpolator()
|
||||
TransitionManager.beginDelayedTransition(onboardingMainContainer, transition)
|
||||
TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,10 +116,10 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCards
|
|||
with(binding.onboardingTopContainer) {
|
||||
val constraintSet = ConstraintSet()
|
||||
constraintSet.clone(requireContext(), layoutId)
|
||||
constraintSet.applyTo(onboardingMainContainer)
|
||||
constraintSet.applyTo(onboardingWalletContainer)
|
||||
val transition = InternalNoteLayoutTransition()
|
||||
transition.interpolator = OvershootInterpolator()
|
||||
TransitionManager.beginDelayedTransition(onboardingMainContainer, transition)
|
||||
TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -79,17 +79,21 @@ private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) {
|
|||
val primaryBlockchain = updatedResponse.getBlockchain()
|
||||
val blockchainNetworks = if (primaryBlockchain != Blockchain.Unknown) {
|
||||
val primaryToken = updatedResponse.getPrimaryToken()
|
||||
val blockchainNetwork = BlockchainNetwork(primaryBlockchain, updatedResponse.card).updateTokens(
|
||||
listOfNotNull(primaryToken))
|
||||
val blockchainNetwork =
|
||||
BlockchainNetwork(primaryBlockchain, updatedResponse.card).updateTokens(
|
||||
listOfNotNull(primaryToken),
|
||||
)
|
||||
listOf(blockchainNetwork)
|
||||
} else {
|
||||
listOf(
|
||||
BlockchainNetwork(Blockchain.Bitcoin, updatedResponse.card),
|
||||
BlockchainNetwork(Blockchain.Ethereum, updatedResponse.card)
|
||||
BlockchainNetwork(Blockchain.Ethereum, updatedResponse.card),
|
||||
)
|
||||
}
|
||||
|
||||
store.dispatch(WalletAction.MultiWallet.SaveCurrencies(blockchainNetworks))
|
||||
store.dispatch(
|
||||
WalletAction.MultiWallet.SaveCurrencies(blockchainNetworks, updatedResponse.card),
|
||||
)
|
||||
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
store.dispatch(OnboardingOtherCardsAction.SetStepOfScreen(OnboardingOtherCardsStep.Done))
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import com.tangem.Message
|
|||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.tap.common.AssetReader
|
||||
import com.tangem.tap.common.redux.StateDialog
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.twins.AssetReader
|
||||
import com.tangem.tap.domain.twins.TwinCardsManager
|
||||
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
|
||||
import org.rekotlin.Action
|
||||
|
|
|
|||
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