Updated on 2026-08-14
This commit is contained in:
commit
636fbfc6ab
607 changed files with 10302 additions and 5310 deletions
44
Dockerfile
Normal file
44
Dockerfile
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
FROM ubuntu:24.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV ANDROID_HOME=/opt/android-sdk
|
||||
ENV BUNDLE_PATH=vendor/bundle
|
||||
ENV PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/build-tools/34.0.0:$BUNDLE_PATH/bin
|
||||
ENV LC_ALL=en_US.UTF-8
|
||||
ENV LANG=en_US.UTF-8
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
openjdk-17-jdk \
|
||||
wget \
|
||||
unzip \
|
||||
curl \
|
||||
git \
|
||||
ruby \
|
||||
ruby-dev \
|
||||
build-essential \
|
||||
&& apt-get clean
|
||||
|
||||
RUN mkdir -p $ANDROID_HOME/cmdline-tools/latest && \
|
||||
wget https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip -O /tmp/cmdline-tools.zip && \
|
||||
unzip /tmp/cmdline-tools.zip -d $ANDROID_HOME/cmdline-tools/latest && \
|
||||
mv $ANDROID_HOME/cmdline-tools/latest/cmdline-tools/* $ANDROID_HOME/cmdline-tools/latest/ && \
|
||||
rm -rf $ANDROID_HOME/cmdline-tools/latest/cmdline-tools && \
|
||||
rm -f /tmp/cmdline-tools.zip && \
|
||||
yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses && \
|
||||
$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "platform-tools" "platforms;android-31" "platforms;android-34" "build-tools;34.0.0"
|
||||
|
||||
RUN wget https://github.com/lzhiyong/android-sdk-tools/releases/download/34.0.3/android-sdk-tools-static-aarch64.zip -O /tmp/android-sdk-tools-static-aarch64.zip && \
|
||||
unzip /tmp/android-sdk-tools-static-aarch64.zip -d /tmp/android-sdk-tools-static-arm && \
|
||||
cp -r /tmp/android-sdk-tools-static-arm/build-tools/* $ANDROID_HOME/build-tools/34.0.0/ && \
|
||||
rm -rf /tmp/android-sdk-tools-static-arm /tmp/android-sdk-tools-static-aarch64.zip
|
||||
|
||||
RUN gem install bundler:2.5.23
|
||||
RUN gem install fastlane -v 2.211.0 -N -V
|
||||
RUN gem install fastlane-plugin-firebase_app_distribution -v 0.9.1 -N -V
|
||||
|
||||
COPY Gemfile Gemfile.lock ./
|
||||
RUN bundle install --jobs=4 --retry=3 --verbose
|
||||
|
||||
RUN bundle exec fastlane -v
|
||||
|
||||
CMD ["bash"]
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(deps.plugins.android.application)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
|
|
@ -25,6 +27,23 @@ android {
|
|||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
signingConfigs {
|
||||
getByName("debug") {
|
||||
val keystorePath = "src/main/assets/tangem-app-config/android/keystore/debug_keystore"
|
||||
val propertiesPath = "src/main/assets/tangem-app-config/android/keystore/debug_keystore.properties"
|
||||
|
||||
val keystoreProperties = Properties()
|
||||
file(propertiesPath)
|
||||
.inputStream()
|
||||
.use { keystoreProperties.load(it) }
|
||||
|
||||
storeFile = file(keystorePath)
|
||||
storePassword = keystoreProperties["store_password"] as String
|
||||
keyAlias = keystoreProperties["key_alias"] as String
|
||||
keyPassword = keystoreProperties["key_password"] as String
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
|
|
@ -75,6 +94,7 @@ dependencies {
|
|||
implementation(projects.domain.markets)
|
||||
implementation(projects.domain.manageTokens)
|
||||
implementation(projects.domain.onramp)
|
||||
implementation(projects.domain.promo)
|
||||
|
||||
implementation(projects.common)
|
||||
implementation(projects.common.routing)
|
||||
|
|
@ -166,6 +186,7 @@ dependencies {
|
|||
implementation(deps.androidx.browser)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
implementation(deps.androidx.swipeRefreshLayout)
|
||||
implementation(deps.androidx.fragment.compose)
|
||||
implementation(deps.lifecycle.runtime.ktx)
|
||||
implementation(deps.lifecycle.common.java8)
|
||||
implementation(deps.lifecycle.viewModel.ktx)
|
||||
|
|
@ -226,11 +247,12 @@ dependencies {
|
|||
implementation(deps.xmlShimmer)
|
||||
implementation(deps.viewBindingDelegate)
|
||||
implementation(deps.armadillo)
|
||||
implementation(deps.mviCore.watcher)
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.reownCore)
|
||||
implementation(deps.reownWeb3)
|
||||
implementation(deps.prettyLogger)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.moshi.adapters)
|
||||
|
||||
/** Testing libraries */
|
||||
testImplementation(deps.test.coroutine)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!--<uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />-->
|
||||
<uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
|
@ -10,11 +10,6 @@
|
|||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit f1aa2f2cbf374ecba5da753d9f51c9df81749ef2
|
||||
Subproject commit 72d9d4b3c3f8e6604078798c4b5567327ed9b48a
|
||||
|
|
@ -759,6 +759,16 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "vanar-chain",
|
||||
"name": "Vanar Chain",
|
||||
"symbol": "VANRY",
|
||||
"networks": [
|
||||
{
|
||||
"networkId": "vanar-chain/test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "casper-network",
|
||||
"name": "Casper",
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
package com.tangem.tap
|
||||
|
||||
import android.content.Intent
|
||||
import android.hardware.biometrics.BiometricManager
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
|
||||
interface ActivityResultCaller {
|
||||
val activityResultLauncher: ActivityResultLauncher<Intent>?
|
||||
}
|
||||
|
||||
internal fun ActivityResultCaller.openSystemBiometrySettings() {
|
||||
val settingsAction = when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
||||
Settings.ACTION_BIOMETRIC_ENROLL
|
||||
}
|
||||
else -> {
|
||||
Settings.ACTION_SECURITY_SETTINGS
|
||||
}
|
||||
}
|
||||
val intent = Intent(settingsAction).apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
putExtra(
|
||||
Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
activityResultLauncher?.launch(intent)
|
||||
}
|
||||
|
|
@ -8,8 +8,10 @@ import com.tangem.common.routing.AppRouter
|
|||
import com.tangem.core.analytics.filter.OneTimeEventFilter
|
||||
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.core.navigation.settings.SettingsManager
|
||||
import com.tangem.core.navigation.share.ShareManager
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
||||
|
|
@ -34,9 +36,9 @@ import com.tangem.domain.wallets.repository.WalletsRepository
|
|||
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
|
||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||
import com.tangem.features.onramp.OnrampFeatureToggles
|
||||
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
|
||||
import com.tangem.tap.common.log.TangemAppLoggerInitializer
|
||||
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
|
||||
import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.EntryPoint
|
||||
|
|
@ -113,8 +115,6 @@ interface ApplicationEntryPoint {
|
|||
|
||||
fun getTransactionSignerFactory(): TransactionSignerFactory
|
||||
|
||||
fun getHomeFeatureToggles(): HomeFeatureToggles
|
||||
|
||||
fun getGetUserCountryCodeUseCase(): GetUserCountryUseCase
|
||||
|
||||
fun getOnrampFeatureToggles(): OnrampFeatureToggles
|
||||
|
|
@ -128,4 +128,10 @@ interface ApplicationEntryPoint {
|
|||
fun getExcludedBlockchains(): ExcludedBlockchains
|
||||
|
||||
fun getAppLogsStore(): AppLogsStore
|
||||
|
||||
fun getClipboardManager(): ClipboardManager
|
||||
|
||||
fun getSettingsManager(): SettingsManager
|
||||
|
||||
fun getBlockchainExceptionHandler(): BlockchainExceptionHandler
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ import com.tangem.core.decompose.context.AppComponentContext
|
|||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.message.EventMessageEffect
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
import com.tangem.utils.Provider
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
|
@ -33,11 +32,6 @@ internal class DecomposeFragment : ComposeFragment() {
|
|||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
component.Content(modifier)
|
||||
|
||||
EventMessageEffect(
|
||||
messageHandler = uiDependencies.eventMessageHandler,
|
||||
snackbarHostState = uiDependencies.globalSnackbarHostState,
|
||||
)
|
||||
}
|
||||
|
||||
private class ComponentBuilder<C : ComposableContentComponent, P : Any, F : ComponentFactory<P, C>>(
|
||||
|
|
|
|||
|
|
@ -2,17 +2,12 @@ package com.tangem.tap
|
|||
|
||||
import android.app.Activity
|
||||
import android.app.Application.ActivityLifecycleCallbacks
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import java.util.WeakHashMap
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class ForegroundActivityObserver : ActivityResultCaller {
|
||||
override var activityResultLauncher: ActivityResultLauncher<Intent>? = null
|
||||
private set
|
||||
class ForegroundActivityObserver {
|
||||
|
||||
private val activities = WeakHashMap<KClass<out Activity>, AppCompatActivity>()
|
||||
|
||||
|
|
@ -26,11 +21,6 @@ class ForegroundActivityObserver : ActivityResultCaller {
|
|||
|
||||
internal inner class Callbacks : ActivityLifecycleCallbacks {
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
activityResultLauncher = (activity as? AppCompatActivity)?.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
) {
|
||||
/* no-op */
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
|
|
@ -39,9 +29,6 @@ class ForegroundActivityObserver : ActivityResultCaller {
|
|||
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
activities.remove(activity::class)
|
||||
if (activities.isEmpty()) {
|
||||
activityResultLauncher = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {
|
||||
|
|
|
|||
|
|
@ -7,17 +7,20 @@ import android.content.res.Configuration
|
|||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.Lifecycle
|
||||
|
|
@ -36,10 +39,13 @@ import com.tangem.core.decompose.context.AppComponentContext
|
|||
import com.tangem.core.decompose.di.RootAppComponentContext
|
||||
import com.tangem.core.deeplink.DeepLinksRegistry
|
||||
import com.tangem.core.navigation.email.EmailSender
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.message.EventMessageEffect
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.data.card.sdk.CardSdkOwner
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.card.ScanCardUseCase
|
||||
|
|
@ -81,12 +87,12 @@ import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandle
|
|||
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
|
||||
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
|
||||
import com.tangem.tap.features.main.MainViewModel
|
||||
import com.tangem.tap.features.main.model.Toast
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphAction
|
||||
import com.tangem.tap.routing.RoutingComponent
|
||||
import com.tangem.tap.routing.component.RoutingComponent
|
||||
import com.tangem.tap.routing.configurator.AppRouterConfig
|
||||
import com.tangem.tap.routing.toggle.RoutingFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -209,6 +215,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
@Inject
|
||||
lateinit var dispatchers: CoroutineDispatcherProvider
|
||||
|
||||
@Inject
|
||||
internal lateinit var routingFeatureToggles: RoutingFeatureToggles
|
||||
|
||||
@Inject
|
||||
internal lateinit var uiDependencies: UiDependencies
|
||||
|
||||
internal val viewModel: MainViewModel by viewModels()
|
||||
|
||||
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
|
||||
|
|
@ -246,20 +258,25 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
|
||||
}
|
||||
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
// window.setHideOverlayWindows(true)
|
||||
// }
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
window.setHideOverlayWindows(true)
|
||||
}
|
||||
|
||||
splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenShown }
|
||||
|
||||
installActivityDependencies()
|
||||
observeAppThemeModeUpdates()
|
||||
|
||||
setContentView(R.layout.activity_main)
|
||||
installRouting()
|
||||
if (routingFeatureToggles.isNavigationRefactoringEnabled) {
|
||||
setRootContent()
|
||||
} else {
|
||||
setContentView(R.layout.activity_main)
|
||||
installRouting()
|
||||
installEventMessageEffect()
|
||||
}
|
||||
|
||||
initContent()
|
||||
|
||||
observeStateUpdates()
|
||||
observePolkadotAccountHealthCheck()
|
||||
sendStakingUnsubmittedHashes()
|
||||
checkGoogleServicesAvailability()
|
||||
|
|
@ -271,6 +288,43 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
lifecycle.addObserver(WindowObscurationObserver)
|
||||
}
|
||||
|
||||
private fun installEventMessageEffect() {
|
||||
binding.composeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
binding.composeView.setContent {
|
||||
TangemTheme(
|
||||
activity = this,
|
||||
uiDependencies = uiDependencies,
|
||||
overrideSystemBarColors = false,
|
||||
) {
|
||||
EventMessageEffect(
|
||||
onShowSnackbar = { message, _ ->
|
||||
showSnackbar(
|
||||
text = message.message.resolveReference(resources),
|
||||
length = when (message.duration) {
|
||||
SnackbarMessage.Duration.Short -> Snackbar.LENGTH_SHORT
|
||||
SnackbarMessage.Duration.Long -> Snackbar.LENGTH_LONG
|
||||
SnackbarMessage.Duration.Indefinite -> Snackbar.LENGTH_INDEFINITE
|
||||
},
|
||||
buttonTitle = message.actionLabel?.resolveReference(resources),
|
||||
action = message.action,
|
||||
onDismiss = message.onDismissRequest,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setRootContent() {
|
||||
val routingComponent = routingComponentFactory.create(
|
||||
context = rootComponentContext,
|
||||
)
|
||||
|
||||
setContent {
|
||||
routingComponent.Content(Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
|
||||
private fun installRouting() {
|
||||
val routingComponent = routingComponentFactory.create(
|
||||
context = rootComponentContext,
|
||||
|
|
@ -297,30 +351,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
is RoutingComponent.Child.LegacyIntent -> {
|
||||
startActivity(child.intent)
|
||||
}
|
||||
is RoutingComponent.Child.ComposableComponent -> error("Unsupported child: $child")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeStateUpdates() {
|
||||
viewModel.state
|
||||
.flowWithLifecycle(lifecycle)
|
||||
.onEach { state ->
|
||||
if (state.toast is StateEvent.Triggered) {
|
||||
showToast(state.toast.data)
|
||||
state.toast.onConsume()
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private fun showToast(toast: Toast) {
|
||||
dismissSnackbar()
|
||||
showSnackbar(toast.message, Snackbar.LENGTH_LONG, toast.action.text) {
|
||||
toast.action.onClick()
|
||||
dismissSnackbar()
|
||||
}
|
||||
}
|
||||
|
||||
private fun installActivityDependencies() {
|
||||
cardSdkOwner.register(activity = this)
|
||||
tangemSdkManager = injectedTangemSdkManager
|
||||
|
|
@ -400,8 +435,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// TODO: RESEARCH! NotificationsHandler is created in onResume and destroyed in onStop
|
||||
notificationsHandler = NotificationsHandler(binding.fragmentContainer)
|
||||
|
||||
if (!routingFeatureToggles.isNavigationRefactoringEnabled) {
|
||||
// TODO: RESEARCH! NotificationsHandler is created in onResume and destroyed in onStop
|
||||
notificationsHandler = NotificationsHandler(binding.fragmentContainer)
|
||||
}
|
||||
|
||||
navigateToInitialScreenIfNeeded(intent)
|
||||
}
|
||||
|
|
@ -473,7 +511,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
super.onNewIntent(intent)
|
||||
|
||||
lifecycleScope.launch {
|
||||
intentProcessor.handleIntent(intent, true)
|
||||
intentProcessor.handleIntent(intent = intent, isFromForeground = true)
|
||||
}
|
||||
|
||||
if (intent != null) {
|
||||
|
|
@ -481,22 +519,22 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
}
|
||||
}
|
||||
|
||||
override fun showSnackbar(
|
||||
@StringRes text: Int,
|
||||
length: Int,
|
||||
@StringRes buttonTitle: Int?,
|
||||
action: View.OnClickListener?,
|
||||
) {
|
||||
showSnackbar(getString(text), length, buttonTitle?.let(::getString), action)
|
||||
override fun showSnackbar(@StringRes text: Int, length: Int, @StringRes buttonTitle: Int?, action: (() -> Unit)?) {
|
||||
showSnackbar(
|
||||
getString(text),
|
||||
length,
|
||||
buttonTitle?.let(::getString),
|
||||
action = { action?.invoke() },
|
||||
)
|
||||
}
|
||||
|
||||
override fun showSnackbar(
|
||||
text: TextReference,
|
||||
length: Int,
|
||||
buttonTitle: TextReference?,
|
||||
action: View.OnClickListener?,
|
||||
) {
|
||||
showSnackbar(text.resolveReference(resources), length, buttonTitle?.resolveReference(resources), action)
|
||||
override fun showSnackbar(text: TextReference, length: Int, buttonTitle: TextReference?, action: (() -> Unit)?) {
|
||||
showSnackbar(
|
||||
text = text.resolveReference(resources),
|
||||
length = length,
|
||||
buttonTitle = buttonTitle?.resolveReference(resources),
|
||||
action = { action?.invoke() },
|
||||
)
|
||||
}
|
||||
|
||||
override fun dismissSnackbar() {
|
||||
|
|
@ -531,7 +569,13 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
return if (result) super.dispatchTouchEvent(event) else false
|
||||
}
|
||||
|
||||
private fun showSnackbar(text: String, length: Int, buttonTitle: String?, action: View.OnClickListener?) {
|
||||
private fun showSnackbar(
|
||||
text: String,
|
||||
length: Int,
|
||||
buttonTitle: String?,
|
||||
action: (() -> Unit)? = null,
|
||||
onDismiss: () -> Unit = {},
|
||||
) {
|
||||
if (snackbar != null) return
|
||||
|
||||
snackbar = Snackbar.make(binding.fragmentContainer, text, length).apply {
|
||||
|
|
@ -542,12 +586,13 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
setTextColor(textColor)
|
||||
|
||||
if (buttonTitle != null && action != null) {
|
||||
setAction(buttonTitle, action)
|
||||
setAction(buttonTitle, { action() })
|
||||
}
|
||||
|
||||
addCallback(
|
||||
object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
|
||||
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
|
||||
onDismiss()
|
||||
snackbar = null
|
||||
removeCallback(this)
|
||||
}
|
||||
|
|
@ -583,6 +628,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
store.dispatchNavigationAction {
|
||||
replaceAll(AppRoute.Welcome(intentWhichStartedActivity?.let(::SerializableIntent)))
|
||||
}
|
||||
intentProcessor.handleIntent(
|
||||
intent = intentWhichStartedActivity,
|
||||
isFromForeground = false,
|
||||
skipNavigationHandlers = true,
|
||||
)
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
val shouldShowTos = !cardRepository.isTangemTOSAccepted()
|
||||
|
|
@ -595,7 +645,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
}
|
||||
|
||||
store.dispatchNavigationAction { replaceAll(route) }
|
||||
intentProcessor.handleIntent(intentWhichStartedActivity, false)
|
||||
intentProcessor.handleIntent(
|
||||
intent = intentWhichStartedActivity,
|
||||
isFromForeground = false,
|
||||
skipNavigationHandlers = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import coil.ImageLoaderFactory
|
|||
import com.chuckerteam.chucker.api.ChuckerInterceptor
|
||||
import com.tangem.Log
|
||||
import com.tangem.TangemSdkLogger
|
||||
import com.tangem.blockchain.common.ExceptionHandler
|
||||
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
|
||||
import com.tangem.blockchainsdk.BlockchainSDKFactory
|
||||
import com.tangem.blockchainsdk.signer.TransactionSignerFactory
|
||||
|
|
@ -20,6 +21,8 @@ import com.tangem.core.analytics.models.Basic
|
|||
import com.tangem.core.analytics.models.Basic.TransactionSent.WalletForm
|
||||
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.core.navigation.settings.SettingsManager
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import com.tangem.datasource.api.common.MoshiConverter
|
||||
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
|
||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
|
|
@ -50,6 +53,7 @@ import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
|||
import com.tangem.features.onramp.OnrampFeatureToggles
|
||||
import com.tangem.tap.common.analytics.AnalyticsFactory
|
||||
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
|
||||
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
|
||||
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
|
||||
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
|
||||
import com.tangem.tap.common.images.createCoilImageLoader
|
||||
|
|
@ -58,7 +62,6 @@ import com.tangem.tap.common.redux.AppState
|
|||
import com.tangem.tap.common.redux.appReducer
|
||||
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
|
||||
import com.tangem.tap.domain.tasks.product.DerivationsFinder
|
||||
import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -74,7 +77,6 @@ import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
|
|||
lateinit var store: Store<AppState>
|
||||
|
||||
lateinit var foregroundActivityObserver: ForegroundActivityObserver
|
||||
lateinit var activityResultCaller: ActivityResultCaller
|
||||
internal lateinit var derivationsFinder: DerivationsFinder
|
||||
|
||||
abstract class TangemApplication : Application(), ImageLoaderFactory {
|
||||
|
|
@ -179,9 +181,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
|
|||
private val transactionSignerFactory: TransactionSignerFactory
|
||||
get() = entryPoint.getTransactionSignerFactory()
|
||||
|
||||
private val homeFeatureToggles: HomeFeatureToggles
|
||||
get() = entryPoint.getHomeFeatureToggles()
|
||||
|
||||
private val getUserCountryUseCase: GetUserCountryUseCase
|
||||
get() = entryPoint.getGetUserCountryCodeUseCase()
|
||||
|
||||
|
|
@ -202,6 +201,15 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
|
|||
|
||||
private val appLogsStore: AppLogsStore
|
||||
get() = entryPoint.getAppLogsStore()
|
||||
|
||||
private val clipboardManager: ClipboardManager
|
||||
get() = entryPoint.getClipboardManager()
|
||||
|
||||
private val settingsManager: SettingsManager
|
||||
get() = entryPoint.getSettingsManager()
|
||||
|
||||
private val blockchainExceptionHandler: BlockchainExceptionHandler
|
||||
get() = entryPoint.getBlockchainExceptionHandler()
|
||||
// endregion
|
||||
|
||||
override fun onCreate() {
|
||||
|
|
@ -231,7 +239,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
|
|||
tangemAppLoggerInitializer.initialize()
|
||||
|
||||
foregroundActivityObserver = ForegroundActivityObserver()
|
||||
activityResultCaller = foregroundActivityObserver
|
||||
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
|
||||
|
||||
// TODO: Try to performance and user experience.
|
||||
|
|
@ -245,7 +252,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
|
|||
}
|
||||
|
||||
loadNativeLibraries()
|
||||
|
||||
ExceptionHandler.append(blockchainExceptionHandler)
|
||||
if (LogConfig.network.blockchainSdkNetwork) {
|
||||
BlockchainSdkRetrofitBuilder.interceptors = listOf(
|
||||
createNetworkLoggingInterceptor(),
|
||||
|
|
@ -292,7 +299,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
|
|||
shareManager = shareManager,
|
||||
appRouter = appRouter,
|
||||
transactionSignerFactory = transactionSignerFactory,
|
||||
homeFeatureToggles = homeFeatureToggles,
|
||||
getUserCountryUseCase = getUserCountryUseCase,
|
||||
onrampFeatureToggles = onrampFeatureToggles,
|
||||
environmentConfigStorage = environmentConfigStorage,
|
||||
|
|
@ -300,6 +306,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
|
|||
onboardingRepository = onboardingRepository,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
clipboardManager = clipboardManager,
|
||||
settingsManager = settingsManager,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -355,6 +363,5 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
|
|||
)
|
||||
|
||||
factory.build(Analytics, buildData)
|
||||
// ExceptionHandler.append(blockchainExceptionHandler) TODO: [REDACTED_JIRA]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.tap.common
|
||||
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
|
@ -11,14 +10,14 @@ interface SnackbarHandler {
|
|||
@StringRes text: Int,
|
||||
length: Int = Snackbar.LENGTH_INDEFINITE,
|
||||
@StringRes buttonTitle: Int? = null,
|
||||
action: View.OnClickListener? = null,
|
||||
action: (() -> Unit)? = null,
|
||||
)
|
||||
|
||||
fun showSnackbar(
|
||||
text: TextReference,
|
||||
length: Int = Snackbar.LENGTH_INDEFINITE,
|
||||
buttonTitle: TextReference? = null,
|
||||
action: View.OnClickListener? = null,
|
||||
action: (() -> Unit)? = null,
|
||||
)
|
||||
|
||||
fun dismissSnackbar()
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ sealed class Onboarding(
|
|||
|
||||
class Started : Onboarding("Onboarding", "Onboarding Started")
|
||||
class Finished : Onboarding("Onboarding", "Onboarding Finished")
|
||||
data object OfflineAttestationFailed : Onboarding(category = "Onboarding", event = "Offline Attestation Failed")
|
||||
|
||||
sealed class CreateWallet(
|
||||
event: String,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.tap.common.analytics.handlers.firebase
|
||||
|
||||
internal class FirebaseAnalyticsEventConverter {
|
||||
|
||||
fun convertEventName(event: String): String {
|
||||
return convertString(event, FIREBASE_EVENT_NAME_MAX_LENGTH)
|
||||
}
|
||||
|
||||
fun convertEventParams(params: Map<String, String>): Map<String, String> {
|
||||
return params.map { (key, value) ->
|
||||
convertString(key, FIREBASE_EVENT_NAME_MAX_LENGTH) to convertString(value, FIREBASE_EVENT_VALUE_MAX_LENGTH)
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
private fun convertString(string: String, maxLength: Int): String {
|
||||
return string
|
||||
.replace(REPLACING_PATTERN.toRegex(), WORD_SEPARATOR)
|
||||
.trim { it in TRIMMING_CHARACTERS }
|
||||
.trimToLength(maxLength)
|
||||
}
|
||||
|
||||
private fun String.trimToLength(length: Int): String {
|
||||
return if (this.length > length) this.substring(0, length) else this
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val REPLACING_PATTERN = "[^\\w]+"
|
||||
const val WORD_SEPARATOR = "_"
|
||||
const val TRIMMING_CHARACTERS = WORD_SEPARATOR
|
||||
const val FIREBASE_EVENT_NAME_MAX_LENGTH = 40
|
||||
const val FIREBASE_EVENT_VALUE_MAX_LENGTH = 100
|
||||
}
|
||||
}
|
||||
|
|
@ -18,12 +18,19 @@ internal class FirebaseClient : FirebaseAnalyticsClient {
|
|||
private val fbAnalytics = Firebase.analytics
|
||||
private val fbCrashlytics = Firebase.crashlytics
|
||||
|
||||
private val eventConverter = FirebaseAnalyticsEventConverter()
|
||||
|
||||
override fun logEvent(event: String, params: Map<String, String>) {
|
||||
fbAnalytics.logEvent(event, params.toBundle())
|
||||
fbAnalytics.logEvent(
|
||||
eventConverter.convertEventName(event),
|
||||
eventConverter.convertEventParams(params).toBundle(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun logErrorEvent(error: Throwable, params: Map<String, String>) {
|
||||
params.forEach { fbCrashlytics.setCustomKey(it.key, it.value) }
|
||||
eventConverter.convertEventParams(params)
|
||||
.forEach { fbCrashlytics.setCustomKey(it.key, it.value) }
|
||||
|
||||
fbCrashlytics.recordException(error)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,52 @@
|
|||
package com.tangem.tap.common.clipboard
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipDescription
|
||||
import android.content.ClipDescription.MIMETYPE_TEXT_PLAIN
|
||||
import android.os.Build
|
||||
import android.os.PersistableBundle
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import timber.log.Timber
|
||||
import android.content.ClipboardManager as AndroidClipboardManager
|
||||
|
||||
internal class DefaultClipboardManager(private val clipboardManager: AndroidClipboardManager) : ClipboardManager {
|
||||
|
||||
override fun setText(label: String, text: String) {
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText(label, text))
|
||||
override fun setText(text: String, isSensitive: Boolean, label: String) {
|
||||
val clip = ClipData.newPlainText(label, text).apply {
|
||||
if (isSensitive) description.setAsSensitive()
|
||||
}
|
||||
|
||||
clipboardManager.setPrimaryClip(clip)
|
||||
}
|
||||
|
||||
override fun getText(default: String?): String? {
|
||||
val clip = clipboardManager.primaryClip
|
||||
|
||||
if (clip == null || clip.itemCount == 0) {
|
||||
Timber.d("Clipboard is empty")
|
||||
return default
|
||||
}
|
||||
|
||||
val clipDescription = clipboardManager.primaryClipDescription
|
||||
if (clipDescription?.hasMimeType(MIMETYPE_TEXT_PLAIN) == false) {
|
||||
Timber.d("Clipboard doesn't contain text")
|
||||
return default
|
||||
}
|
||||
|
||||
return clip.getItemAt(0).text?.toString()
|
||||
}
|
||||
|
||||
private fun ClipDescription.setAsSensitive() {
|
||||
extras = PersistableBundle().apply {
|
||||
putBoolean(getExtraIsSensitiveFlag(), true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getExtraIsSensitiveFlag(): String {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ClipDescription.EXTRA_IS_SENSITIVE
|
||||
} else {
|
||||
"android.content.extra.IS_SENSITIVE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,10 @@ import com.tangem.core.ui.clipboard.ClipboardManager
|
|||
import timber.log.Timber
|
||||
|
||||
internal object MockClipboardManager : ClipboardManager {
|
||||
override fun setText(label: String, text: String) {
|
||||
/** Intentionnaly do nothing */
|
||||
|
||||
override fun setText(text: String, isSensitive: Boolean, label: String) {
|
||||
Timber.w("Clipboard Manager not available")
|
||||
}
|
||||
|
||||
override fun getText(default: String?): String? = null
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* required since targetAndroid=30
|
||||
* <queries>
|
||||
* <intent>
|
||||
* <action android:name="android.intent.action.SENDTO" />
|
||||
* <data android:scheme="mailto" />
|
||||
* </intent>
|
||||
* </queries>
|
||||
*/
|
||||
@Deprecated("Use EmailSender instead")
|
||||
fun Activity.sendEmail(
|
||||
email: String,
|
||||
subject: String,
|
||||
message: String,
|
||||
file: File? = null,
|
||||
onFail: ((Exception) -> Unit)? = null,
|
||||
) {
|
||||
fun createEmailShareIntent(recipient: String, subject: String, text: String, file: File? = null): Intent {
|
||||
val builder = ShareCompat.IntentBuilder.from(this)
|
||||
.setType("message/rfc822")
|
||||
.setEmailTo(arrayOf(recipient))
|
||||
.setSubject(subject)
|
||||
.setText(text)
|
||||
file?.let { builder.setStream(FileProvider.getUriForFile(this, "$packageName.provider", it)) }
|
||||
return builder.intent
|
||||
}
|
||||
|
||||
val originalIntent = createEmailShareIntent(email, subject, message, file)
|
||||
val emailFilterIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
|
||||
|
||||
val originalIntentResults = packageManager.queryIntentActivities(originalIntent, 0)
|
||||
val emailFilterIntentResults = packageManager.queryIntentActivities(emailFilterIntent, 0)
|
||||
|
||||
val targetedIntents = originalIntentResults
|
||||
.filter { originalResult ->
|
||||
emailFilterIntentResults.any {
|
||||
originalResult.activityInfo.packageName == it.activityInfo.packageName
|
||||
}
|
||||
}
|
||||
.map {
|
||||
createEmailShareIntent(email, subject, message, file).apply {
|
||||
setPackage(it.activityInfo.packageName)
|
||||
}
|
||||
}
|
||||
.toMutableList()
|
||||
try {
|
||||
val chooserIntent = Intent.createChooser(targetedIntents.removeAt(0), "Send mail...")
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toTypedArray())
|
||||
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
ContextCompat.startActivity(this, chooserIntent, null)
|
||||
} catch (ex: Exception) {
|
||||
onFail?.invoke(ex)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
fun StringBuilder.breakLine(count: Int = 1): StringBuilder = append("\n".repeat(count))
|
||||
|
|
@ -2,26 +2,19 @@
|
|||
|
||||
package com.tangem.tap.common.extensions
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import androidx.annotation.*
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
fun Context.getDrawableCompat(@DrawableRes drawableResId: Int): Drawable? {
|
||||
return ContextCompat.getDrawable(this, drawableResId)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun Fragment.getColor(@ColorRes colorRes: Int): Int {
|
||||
return ContextCompat.getColor(requireContext(), colorRes)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun Context.getColorCompat(@ColorRes colorRes: Int): Int {
|
||||
return ContextCompat.getColor(this, colorRes)
|
||||
|
|
@ -40,10 +33,6 @@ fun View.getString(@StringRes id: Int, vararg formatArgs: String): String {
|
|||
return context.getString(id, *formatArgs)
|
||||
}
|
||||
|
||||
fun View.getQuantityString(@PluralsRes id: Int, quantity: Int): String {
|
||||
return context.resources.getQuantityString(id, quantity, quantity)
|
||||
}
|
||||
|
||||
fun View.show(show: Boolean, invokeBeforeStateChanged: (() -> Unit)? = null) {
|
||||
return if (show) this.show(invokeBeforeStateChanged) else this.hide(invokeBeforeStateChanged)
|
||||
}
|
||||
|
|
@ -62,25 +51,6 @@ fun View.hide(invokeBeforeStateChanged: (() -> Unit)? = null) {
|
|||
this.visibility = View.GONE
|
||||
}
|
||||
|
||||
tailrec fun Context?.getActivity(): Activity? = this as? Activity
|
||||
?: (this as? ContextWrapper)?.baseContext?.getActivity()
|
||||
|
||||
fun Context.copyToClipboard(value: Any, label: String = "") {
|
||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager ?: return
|
||||
|
||||
val clip: ClipData = ClipData.newPlainText(label, value.toString())
|
||||
clipboard.setPrimaryClip(clip)
|
||||
}
|
||||
|
||||
fun Context.getFromClipboard(default: CharSequence? = null): CharSequence? {
|
||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
||||
?: return default
|
||||
val clipData = clipboard.primaryClip ?: return default
|
||||
if (clipData.itemCount == 0) return default
|
||||
|
||||
return clipData.getItemAt(0).text
|
||||
}
|
||||
|
||||
fun View.getString(resId: Int, vararg formatArgs: Any?): String {
|
||||
return context.getString(resId, *formatArgs)
|
||||
}
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
package com.tangem.tap.common.redux
|
||||
|
||||
import com.tangem.domain.redux.DomainState
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.domain.redux.global.NetworkServices
|
||||
import com.tangem.tap.common.redux.global.GlobalMiddleware
|
||||
import com.tangem.tap.common.redux.global.GlobalState
|
||||
import com.tangem.tap.common.redux.legacy.LegacyMiddleware
|
||||
|
|
@ -28,7 +25,6 @@ import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
|
|||
import com.tangem.tap.features.welcome.redux.WelcomeState
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphMiddleware
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
|
||||
import org.rekotlin.Middleware
|
||||
import org.rekotlin.StateType
|
||||
|
||||
|
|
@ -46,18 +42,6 @@ data class AppState(
|
|||
val daggerGraphState: DaggerGraphState = DaggerGraphState(),
|
||||
) : StateType {
|
||||
|
||||
private val domainState: DomainState
|
||||
get() = domainStore.state
|
||||
|
||||
private val domainNetworks: NetworkServices
|
||||
get() = domainState.globalState.networkServices
|
||||
|
||||
val featureRepositoryProvider: FeatureRepositoryProvider
|
||||
get() = FeatureRepositoryProvider(
|
||||
tangemTechApi = domainNetworks.tangemTechService.api,
|
||||
dispatchers = AppCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun getMiddleware(): List<Middleware<AppState>> {
|
||||
return listOf(
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
package com.tangem.tap.common.redux
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.tap.features.home.data.HomeRepositoryImpl
|
||||
import com.tangem.tap.features.home.domain.HomeRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
||||
class FeatureRepositoryProvider(tangemTechApi: TangemTechApi, dispatchers: CoroutineDispatcherProvider) {
|
||||
|
||||
val homeRepository: HomeRepository = HomeRepositoryImpl(tangemTechApi, dispatchers)
|
||||
}
|
||||
|
|
@ -82,8 +82,4 @@ sealed class GlobalAction : Action {
|
|||
|
||||
object Update : GlobalAction()
|
||||
}
|
||||
|
||||
object FetchUserCountry : GlobalAction() {
|
||||
data class Success(val countryCode: String) : GlobalAction()
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.firstOrNull
|
|||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Middleware
|
||||
import java.util.Locale
|
||||
|
||||
object GlobalMiddleware {
|
||||
val handler = globalMiddlewareHandler
|
||||
|
|
@ -87,27 +86,6 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
|
|||
}
|
||||
scope.launch { exchangeManager.update() }
|
||||
}
|
||||
is GlobalAction.FetchUserCountry -> {
|
||||
val homeFeatureToggles = store.inject(DaggerGraphState::homeFeatureToggles)
|
||||
|
||||
if (!homeFeatureToggles.isMigrateUserCountryCodeEnabled) {
|
||||
scope.launch {
|
||||
runCatching { store.state.featureRepositoryProvider.homeRepository.getUserCountryCode() }
|
||||
.onSuccess {
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.FetchUserCountry.Success(countryCode = it.code.lowercase()),
|
||||
)
|
||||
}
|
||||
.onFailure {
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.FetchUserCountry.Success(
|
||||
countryCode = Locale.getDefault().country.lowercase(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
package com.tangem.tap.common.redux.global
|
||||
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.domain.redux.global.DomainGlobalAction
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.onboarding.OnboardingManager
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
|
|
@ -38,7 +36,6 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
|
|||
}
|
||||
is GlobalAction.SaveScanResponse -> {
|
||||
appStateHolder.scanResponse = action.scanResponse
|
||||
domainStore.dispatch(DomainGlobalAction.SaveScanNoteResponse(action.scanResponse))
|
||||
globalState.copy(scanResponse = action.scanResponse)
|
||||
}
|
||||
is GlobalAction.ChangeAppCurrency -> {
|
||||
|
|
@ -77,9 +74,6 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
|
|||
}
|
||||
is GlobalAction.SetIfCardVerifiedOnline ->
|
||||
globalState.copy(cardVerifiedOnline = action.verified)
|
||||
is GlobalAction.FetchUserCountry.Success -> {
|
||||
globalState.copy(userCountryCode = action.countryCode)
|
||||
}
|
||||
else -> globalState
|
||||
}
|
||||
}
|
||||
|
|
@ -3,18 +3,46 @@ package com.tangem.tap.common.settings
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.hardware.biometrics.BiometricManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import com.tangem.core.navigation.settings.SettingsManager
|
||||
|
||||
internal class IntentSettingsManager(val context: Context) : SettingsManager {
|
||||
override fun openSettings() {
|
||||
val openSettingsIntent = Intent(
|
||||
|
||||
override fun openAppSettings() {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||
Uri.fromParts("package", context.packageName, null),
|
||||
).apply {
|
||||
flags = FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
)
|
||||
|
||||
open(intent = intent)
|
||||
}
|
||||
|
||||
override fun openBiometricSettings() {
|
||||
val settingsAction = when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Settings.ACTION_BIOMETRIC_ENROLL
|
||||
else -> Settings.ACTION_SECURITY_SETTINGS
|
||||
}
|
||||
context.startActivity(openSettingsIntent)
|
||||
|
||||
val intent = Intent(settingsAction).apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
putExtra(
|
||||
Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
open(intent = intent)
|
||||
}
|
||||
|
||||
private fun open(intent: Intent) {
|
||||
context.startActivity(
|
||||
intent.apply {
|
||||
flags = FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.tap.common.ui
|
|||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.Toast
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.tangem.core.analytics.Analytics
|
||||
|
|
@ -10,6 +11,7 @@ import com.tangem.tap.common.analytics.events.Token
|
|||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.domain.model.WalletAddressData
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.DialogOnboardingAddressInfoBinding
|
||||
|
|
@ -51,7 +53,12 @@ internal class AddressInfoBottomSheetDialog(
|
|||
tvAddress.text = data.address
|
||||
btnFlCopyAddress.setOnClickListener {
|
||||
Analytics.send(Token.Receive.ButtonCopyAddress())
|
||||
context.copyToClipboard(data.address)
|
||||
|
||||
Toast
|
||||
.makeText(context, R.string.wallet_notification_address_copied, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
|
||||
store.inject(DaggerGraphState::clipboardManager).setText(text = data.address, isSensitive = true)
|
||||
}
|
||||
btnFlShare.setOnClickListener {
|
||||
Analytics.send(Token.Receive.ButtonShareAddress())
|
||||
|
|
|
|||
|
|
@ -68,5 +68,6 @@ internal class AndroidEmailSender : EmailSender {
|
|||
}
|
||||
|
||||
return builder.intent
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.tap.data
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.sdk.storage.AndroidSecureStorage
|
||||
import com.tangem.sdk.storage.createEncryptedSharedPreferences
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class DefaultVisaAuthTokenStorage @Inject constructor(
|
||||
@ApplicationContext applicationContext: Context,
|
||||
private val dispatcherProvider: CoroutineDispatcherProvider,
|
||||
) : VisaAuthTokenStorage {
|
||||
|
||||
private val secureStorage = AndroidSecureStorage(
|
||||
preferences = SecureStorage.createEncryptedSharedPreferences(
|
||||
context = applicationContext,
|
||||
storageName = "visa_auth_storage",
|
||||
),
|
||||
)
|
||||
|
||||
private val moshi = Moshi.Builder()
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
|
||||
private val tokensAdapter = moshi.adapter(VisaAuthTokens::class.java)
|
||||
|
||||
override suspend fun store(tokens: VisaAuthTokens) = withContext(dispatcherProvider.io) {
|
||||
val json = tokensAdapter.toJson(tokens)
|
||||
|
||||
secureStorage.store(
|
||||
json.encodeToByteArray(throwOnInvalidSequence = true),
|
||||
VISA_AUTH_TOKENS_KEY,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun get(): VisaAuthTokens? = withContext(dispatcherProvider.io) {
|
||||
secureStorage.get(VISA_AUTH_TOKENS_KEY)
|
||||
?.decodeToString(throwOnInvalidSequence = true)
|
||||
?.let(tokensAdapter::fromJson)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val VISA_AUTH_TOKENS_KEY = "visa_auth_tokens"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.tap.data
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.datasource.local.visa.VisaOTPStorage
|
||||
import com.tangem.sdk.storage.AndroidSecureStorage
|
||||
import com.tangem.sdk.storage.createEncryptedSharedPreferences
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class DefaultVisaOTPStorage @Inject constructor(
|
||||
@ApplicationContext applicationContext: Context,
|
||||
private val dispatcherProvider: CoroutineDispatcherProvider,
|
||||
) : VisaOTPStorage {
|
||||
|
||||
private val secureStorage = AndroidSecureStorage(
|
||||
preferences = SecureStorage.createEncryptedSharedPreferences(
|
||||
context = applicationContext,
|
||||
storageName = "visa_otp_storage",
|
||||
),
|
||||
)
|
||||
|
||||
override suspend fun saveOTP(cardId: String, otp: ByteArray) = withContext(dispatcherProvider.io) {
|
||||
secureStorage.store(otp, VISA_OTP_KEY_PREFIX + cardId)
|
||||
}
|
||||
|
||||
override suspend fun getOTP(cardId: String): ByteArray? = withContext(dispatcherProvider.io) {
|
||||
secureStorage.get(VISA_OTP_KEY_PREFIX + cardId)
|
||||
}
|
||||
|
||||
override suspend fun removeOTP(cardId: String) = withContext(dispatcherProvider.io) {
|
||||
secureStorage.delete(VISA_OTP_KEY_PREFIX + cardId)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val VISA_OTP_KEY_PREFIX = "visa_otp_"
|
||||
}
|
||||
}
|
||||
|
|
@ -6,8 +6,9 @@ import com.arkivanov.decompose.defaultComponentContext
|
|||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.DefaultAppComponentContext
|
||||
import com.tangem.core.decompose.di.DecomposeComponent
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.di.RootAppComponentContext
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -27,13 +28,13 @@ internal object RootAppComponentContextModule {
|
|||
@ActivityContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
componentBuilder: DecomposeComponent.Builder,
|
||||
uiDependencies: UiDependencies,
|
||||
@GlobalUiMessageSender messageSender: UiMessageSender,
|
||||
): AppComponentContext {
|
||||
return DefaultAppComponentContext(
|
||||
componentContext = (context as AppCompatActivity).defaultComponentContext(),
|
||||
messageHandler = uiDependencies.eventMessageHandler,
|
||||
dispatchers = dispatchers,
|
||||
hiltComponentBuilder = componentBuilder,
|
||||
messageSender = messageSender,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,12 @@ package com.tangem.tap.di
|
|||
import android.content.Context
|
||||
import com.tangem.domain.card.BuildConfig
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
|
||||
import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager
|
||||
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -15,18 +18,27 @@ import javax.inject.Singleton
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class TangemSdkManagerModule {
|
||||
internal class TangemSdkManagerModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemSdkManager(
|
||||
@ApplicationContext context: Context,
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
visaCardScanHandler: VisaCardScanHandler,
|
||||
visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
|
||||
onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
||||
): TangemSdkManager {
|
||||
return if (BuildConfig.MOCK_DATA_SOURCE) {
|
||||
MockTangemSdkManager(resources = context.resources)
|
||||
} else {
|
||||
DefaultTangemSdkManager(cardSdkConfigRepository = cardSdkConfigRepository, resources = context.resources)
|
||||
DefaultTangemSdkManager(
|
||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||
resources = context.resources,
|
||||
visaCardScanHandler = visaCardScanHandler,
|
||||
visaCardActivationTaskFactory = visaCardActivationTaskFactory,
|
||||
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
package com.tangem.tap.di
|
||||
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.ui.DefaultUiMessageSender
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.haptic.VibratorHapticManager
|
||||
import com.tangem.core.ui.message.EventMessageHandler
|
||||
|
|
@ -28,4 +31,11 @@ internal object UiDependenciesModule {
|
|||
override val eventMessageHandler: EventMessageHandler = EventMessageHandler()
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@GlobalUiMessageSender
|
||||
fun provideUiMessageSender(uiDependencies: UiDependencies): UiMessageSender {
|
||||
return DefaultUiMessageSender(uiDependencies.eventMessageHandler)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.tap.di.data
|
||||
|
||||
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
|
||||
import com.tangem.datasource.local.visa.VisaOTPStorage
|
||||
import com.tangem.tap.data.DefaultVisaAuthTokenStorage
|
||||
import com.tangem.tap.data.DefaultVisaOTPStorage
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface VisaStorageModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindVisaStorage(impl: DefaultVisaAuthTokenStorage): VisaAuthTokenStorage
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindVisaOTPStorage(impl: DefaultVisaOTPStorage): VisaOTPStorage
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|||
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
|
||||
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
|
||||
import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor
|
||||
import com.tangem.tap.domain.scanCard.LegacyScanProcessor
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -24,7 +25,9 @@ internal object CardLegacyDomainModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideScanCardProcessor(): ScanCardProcessor = DefaultScanCardProcessor()
|
||||
fun provideScanCardProcessor(legacyScanProcessor: LegacyScanProcessor): ScanCardProcessor {
|
||||
return DefaultScanCardProcessor(legacyScanProcessor = legacyScanProcessor)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.promo.*
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object PromoDomainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideShouldShowSwapPromoWalletUseCase(
|
||||
promoSettingsRepository: PromoRepository,
|
||||
): ShouldShowSwapPromoWalletUseCase {
|
||||
return ShouldShowSwapPromoWalletUseCase(promoSettingsRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideShouldShowRingPromoUseCase(promoRepository: PromoRepository): ShouldShowRingPromoUseCase {
|
||||
return ShouldShowRingPromoUseCase(promoRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideShouldShowSwapPromoTokenUseCase(promoRepository: PromoRepository): ShouldShowSwapPromoTokenUseCase {
|
||||
return ShouldShowSwapPromoTokenUseCase(promoRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideShouldShowSwapStoriesUseCase(promoRepository: PromoRepository): ShouldShowSwapStoriesUseCase {
|
||||
return ShouldShowSwapStoriesUseCase(promoRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
|
|||
import com.tangem.domain.settings.*
|
||||
import com.tangem.domain.settings.repositories.AppRatingRepository
|
||||
import com.tangem.domain.settings.repositories.PermissionRepository
|
||||
import com.tangem.domain.settings.repositories.PromoSettingsRepository
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
|
||||
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
|
||||
|
|
@ -117,30 +116,6 @@ internal object SettingsDomainModule {
|
|||
return IsWalletsScrollPreviewEnabled(settingsRepository = settingsRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideShouldShowSwapPromoWalletUseCase(
|
||||
promoSettingsRepository: PromoSettingsRepository,
|
||||
): ShouldShowSwapPromoWalletUseCase {
|
||||
return ShouldShowSwapPromoWalletUseCase(promoSettingsRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideShouldShowRingPromoUseCase(
|
||||
promoSettingsRepository: PromoSettingsRepository,
|
||||
): ShouldShowRingPromoUseCase {
|
||||
return ShouldShowRingPromoUseCase(promoSettingsRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideShouldShowSwapPromoTokenUseCase(
|
||||
promoSettingsRepository: PromoSettingsRepository,
|
||||
): ShouldShowSwapPromoTokenUseCase {
|
||||
return ShouldShowSwapPromoTokenUseCase(promoSettingsRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDeleteDeprecatedLogsUseCase(settingsRepository: SettingsRepository): DeleteDeprecatedLogsUseCase {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
|
||||
import com.tangem.domain.staking.repositories.StakingRepository
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.repository.*
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.feature.swap.domain.api.SwapRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -118,13 +116,9 @@ internal object TokensDomainModule {
|
|||
currenciesRepository: CurrenciesRepository,
|
||||
quotesRepository: QuotesRepository,
|
||||
networksRepository: NetworksRepository,
|
||||
swapRepository: SwapRepository,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
|
||||
promoRepository: PromoRepository,
|
||||
stakingRepository: StakingRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
rampStateManager: RampStateManager,
|
||||
): GetCurrencyWarningsUseCase {
|
||||
return GetCurrencyWarningsUseCase(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
|
|
@ -132,12 +126,8 @@ internal object TokensDomainModule {
|
|||
quotesRepository = quotesRepository,
|
||||
networksRepository = networksRepository,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
swapRepository = swapRepository,
|
||||
showSwapPromoTokenUseCase = showSwapPromoTokenUseCase,
|
||||
promoRepository = promoRepository,
|
||||
stakingRepository = stakingRepository,
|
||||
dispatchers = dispatchers,
|
||||
rampStateManager = rampStateManager,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.tap.di.routing
|
||||
|
||||
import com.tangem.tap.routing.RoutingComponent
|
||||
import com.tangem.tap.routing.impl.DefaultRoutingComponent
|
||||
import com.tangem.tap.routing.component.RoutingComponent
|
||||
import com.tangem.tap.routing.component.impl.DefaultRoutingComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ import com.tangem.tap.proxy.redux.DaggerGraphState
|
|||
import com.tangem.tap.store
|
||||
|
||||
// TODO: Remove this object after feature toggle was removed and use ScanCardUseCase instead
|
||||
internal class DefaultScanCardProcessor : ScanCardProcessor {
|
||||
internal class DefaultScanCardProcessor(
|
||||
private val legacyScanProcessor: LegacyScanProcessor,
|
||||
) : ScanCardProcessor {
|
||||
private val isNewCardScanningEnabled: Boolean
|
||||
get() = store.inject(DaggerGraphState::cardScanningFeatureToggles).isNewCardScanningEnabled
|
||||
|
||||
|
|
@ -21,7 +23,10 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
|
|||
return if (isNewCardScanningEnabled) {
|
||||
UseCaseScanProcessor.scan(cardId, allowsRequestAccessCodeFromRepository)
|
||||
} else {
|
||||
LegacyScanProcessor.scan(cardId, allowsRequestAccessCodeFromRepository)
|
||||
legacyScanProcessor.scan(
|
||||
cardId = cardId,
|
||||
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -38,24 +43,24 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
|
|||
) {
|
||||
if (isNewCardScanningEnabled) {
|
||||
UseCaseScanProcessor.scan(
|
||||
analyticsSource,
|
||||
cardId,
|
||||
onProgressStateChange,
|
||||
onWalletNotCreated,
|
||||
disclaimerWillShow,
|
||||
onFailure,
|
||||
onSuccess,
|
||||
analyticsSource = analyticsSource,
|
||||
cardId = cardId,
|
||||
onProgressStateChange = onProgressStateChange,
|
||||
onWalletNotCreated = onWalletNotCreated,
|
||||
disclaimerWillShow = disclaimerWillShow,
|
||||
onFailure = onFailure,
|
||||
onSuccess = onSuccess,
|
||||
)
|
||||
} else {
|
||||
LegacyScanProcessor.scan(
|
||||
analyticsSource,
|
||||
cardId,
|
||||
onProgressStateChange,
|
||||
onWalletNotCreated,
|
||||
disclaimerWillShow,
|
||||
onCancel,
|
||||
onFailure,
|
||||
onSuccess,
|
||||
legacyScanProcessor.scan(
|
||||
analyticsSource = analyticsSource,
|
||||
cardId = cardId,
|
||||
onProgressStateChange = onProgressStateChange,
|
||||
onWalletNotCreated = onWalletNotCreated,
|
||||
disclaimerWillShow = disclaimerWillShow,
|
||||
onCancel = onCancel,
|
||||
onFailure = onFailure,
|
||||
onSuccess = onSuccess,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,22 @@ package com.tangem.tap.domain.scanCard
|
|||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.toWrappedList
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
|
|
@ -18,6 +27,8 @@ import com.tangem.domain.common.util.twinsIsTwinned
|
|||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.sdk.extensions.localizedDescriptionRes
|
||||
import com.tangem.tap.common.analytics.events.Onboarding
|
||||
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
|
|
@ -37,8 +48,14 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
internal object LegacyScanProcessor {
|
||||
@Singleton
|
||||
internal class LegacyScanProcessor @Inject constructor(
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) {
|
||||
|
||||
suspend fun scan(
|
||||
cardId: String? = null,
|
||||
|
|
@ -48,6 +65,9 @@ internal object LegacyScanProcessor {
|
|||
cardId = cardId,
|
||||
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
|
||||
)
|
||||
.doOnFailure { error ->
|
||||
onScanFailure(error = error, onFailure = {}, onCancel = {})
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -72,7 +92,16 @@ internal object LegacyScanProcessor {
|
|||
|
||||
result
|
||||
.doOnFailure { error ->
|
||||
onFailure(error)
|
||||
onScanFailure(
|
||||
error = error,
|
||||
onFailure = onFailure,
|
||||
onCancel = {
|
||||
mainScope.launch {
|
||||
onProgressStateChange.invoke(false)
|
||||
onCancel()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
.doOnSuccess { scanResponse ->
|
||||
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
|
||||
|
|
@ -82,7 +111,6 @@ internal object LegacyScanProcessor {
|
|||
showDisclaimerIfNeed(
|
||||
scanResponse = scanResponse,
|
||||
disclaimerWillShow = disclaimerWillShow,
|
||||
onFailure = onFailure,
|
||||
nextHandler = { scanResponse2 ->
|
||||
onScanSuccess(
|
||||
scanResponse = scanResponse2,
|
||||
|
|
@ -114,7 +142,6 @@ internal object LegacyScanProcessor {
|
|||
scanResponse: ScanResponse,
|
||||
crossinline disclaimerWillShow: () -> Unit = {},
|
||||
crossinline nextHandler: suspend (ScanResponse) -> Unit,
|
||||
crossinline onFailure: suspend (error: TangemError) -> Unit,
|
||||
) {
|
||||
val disclaimer = scanResponse.card.createDisclaimer()
|
||||
|
||||
|
|
@ -135,6 +162,49 @@ internal object LegacyScanProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend inline fun onScanFailure(
|
||||
error: TangemError,
|
||||
crossinline onFailure: suspend (TangemError) -> Unit,
|
||||
crossinline onCancel: () -> Unit,
|
||||
) {
|
||||
if (error is TangemSdkError.CardVerificationFailed) {
|
||||
analyticsEventHandler.send(event = Onboarding.OfflineAttestationFailed)
|
||||
|
||||
val resource = error.localizedDescriptionRes()
|
||||
val resId = resource.resId ?: R.string.common_unknown_error
|
||||
val resArgs = resource.args.map { it.value }
|
||||
|
||||
uiMessageSender.send(
|
||||
message = DialogMessage(
|
||||
message = resourceReference(id = resId, resArgs.toWrappedList()),
|
||||
title = resourceReference(id = R.string.security_alert_title),
|
||||
isDismissable = false,
|
||||
firstActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = resourceReference(id = R.string.alert_button_request_support),
|
||||
onClick = {
|
||||
mainScope.launch {
|
||||
onCancel()
|
||||
|
||||
store.inject(DaggerGraphState::sendFeedbackEmailUseCase).invoke(
|
||||
type = FeedbackEmailType.CardAttestationFailed,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
secondActionBuilder = {
|
||||
cancelAction {
|
||||
mainScope.launch { onCancel() }
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
} else {
|
||||
onFailure(error)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "MagicNumber")
|
||||
private suspend inline fun onScanSuccess(
|
||||
scanResponse: ScanResponse,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.tangem.common.services.secure.SecureStorage
|
|||
import com.tangem.common.usersCode.UserCodeRepository
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.res.getStringSafe
|
||||
import com.tangem.crypto.bip39.DefaultMnemonic
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
|
|
@ -23,7 +24,11 @@ import com.tangem.domain.common.util.cardTypesResolver
|
|||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.visa.model.VisaActivationInput
|
||||
import com.tangem.domain.visa.model.VisaAuthChallenge
|
||||
import com.tangem.domain.visa.model.VisaCardActivationResponse
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||
import com.tangem.operations.ScanTask
|
||||
import com.tangem.operations.derivation.DerivationTaskResponse
|
||||
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
|
||||
|
|
@ -35,23 +40,28 @@ import com.tangem.operations.wallet.CreateWalletResponse
|
|||
import com.tangem.sdk.api.CreateProductWalletTaskResponse
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.tap.derivationsFinder
|
||||
import com.tangem.tap.domain.tasks.product.*
|
||||
import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
|
||||
import com.tangem.tap.domain.tasks.product.ResetBackupCardTask
|
||||
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
|
||||
import com.tangem.tap.domain.tasks.product.ScanProductTask
|
||||
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
|
||||
import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
|
||||
import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
|
||||
import com.tangem.tap.domain.twins.FinalizeTwinTask
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@Suppress("TooManyFunctions", "LargeClass")
|
||||
class DefaultTangemSdkManager(
|
||||
internal class DefaultTangemSdkManager(
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val resources: Resources,
|
||||
private val visaCardScanHandler: VisaCardScanHandler,
|
||||
private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
|
||||
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
||||
) : TangemSdkManager {
|
||||
|
||||
private val awaitInitializationMutex = Mutex()
|
||||
|
|
@ -122,16 +132,21 @@ class DefaultTangemSdkManager(
|
|||
messageRes: Int?,
|
||||
allowsRequestAccessCodeFromRepository: Boolean,
|
||||
): CompletionResult<ScanResponse> {
|
||||
val message = Message(resources.getString(messageRes ?: R.string.initial_message_scan_header))
|
||||
return runTaskAsyncReturnOnMain(
|
||||
runnable = ScanProductTask(
|
||||
card = null,
|
||||
derivationsFinder = derivationsFinder,
|
||||
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
|
||||
),
|
||||
cardId = cardId,
|
||||
initialMessage = message,
|
||||
).also { sendScanResultsToAnalytics(it) }
|
||||
val message = Message(resources.getStringSafe(messageRes ?: R.string.initial_message_scan_header))
|
||||
return coroutineScope {
|
||||
runTaskAsyncReturnOnMain(
|
||||
runnable = ScanProductTask(
|
||||
card = null,
|
||||
derivationsFinder = derivationsFinder,
|
||||
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
|
||||
visaCardScanHandler = visaCardScanHandler,
|
||||
visaCoroutineScope = this,
|
||||
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
||||
),
|
||||
cardId = cardId,
|
||||
initialMessage = message,
|
||||
).also { sendScanResultsToAnalytics(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createProductWallet(
|
||||
|
|
@ -154,9 +169,9 @@ class DefaultTangemSdkManager(
|
|||
),
|
||||
cardId = scanResponse.card.cardId,
|
||||
initialMessage = if (scanResponse.cardTypesResolver.isRing()) {
|
||||
Message(resources.getString(R.string.initial_message_create_wallet_body_ring))
|
||||
Message(resources.getStringSafe(R.string.initial_message_create_wallet_body_ring))
|
||||
} else {
|
||||
Message(resources.getString(R.string.initial_message_create_wallet_body))
|
||||
Message(resources.getStringSafe(R.string.initial_message_create_wallet_body))
|
||||
},
|
||||
iconScanRes = if (scanResponse.cardTypesResolver.isRing()) R.drawable.img_hand_scan_ring else null,
|
||||
preflightReadFilter = null,
|
||||
|
|
@ -194,9 +209,9 @@ class DefaultTangemSdkManager(
|
|||
),
|
||||
cardId = scanResponse.card.cardId,
|
||||
initialMessage = if (scanResponse.cardTypesResolver.isRing()) {
|
||||
Message(resources.getString(R.string.initial_message_create_wallet_body_ring))
|
||||
Message(resources.getStringSafe(R.string.initial_message_create_wallet_body_ring))
|
||||
} else {
|
||||
Message(resources.getString(R.string.initial_message_create_wallet_body))
|
||||
Message(resources.getStringSafe(R.string.initial_message_create_wallet_body))
|
||||
},
|
||||
preflightReadFilter = null,
|
||||
)
|
||||
|
|
@ -243,7 +258,7 @@ class DefaultTangemSdkManager(
|
|||
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
|
||||
),
|
||||
cardId = cardId,
|
||||
initialMessage = Message(resources.getString(R.string.card_settings_reset_card_to_factory)),
|
||||
initialMessage = Message(resources.getStringSafe(R.string.card_settings_reset_card_to_factory)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -251,7 +266,7 @@ class DefaultTangemSdkManager(
|
|||
return runTaskAsyncReturnOnMain(
|
||||
runnable = ResetBackupCardTask(userWalletId),
|
||||
initialMessage = Message(
|
||||
resources.getString(
|
||||
resources.getStringSafe(
|
||||
R.string.initial_message_reset_backup_card_header,
|
||||
cardNumber.toString(),
|
||||
),
|
||||
|
|
@ -281,7 +296,7 @@ class DefaultTangemSdkManager(
|
|||
return runTaskAsyncReturnOnMain(
|
||||
SetUserCodeCommand.changePasscode(null),
|
||||
cardId,
|
||||
initialMessage = Message(resources.getString(R.string.initial_message_change_passcode_body)),
|
||||
initialMessage = Message(resources.getStringSafe(R.string.initial_message_change_passcode_body)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -289,7 +304,7 @@ class DefaultTangemSdkManager(
|
|||
return runTaskAsyncReturnOnMain(
|
||||
SetUserCodeCommand.changeAccessCode(null),
|
||||
cardId,
|
||||
initialMessage = Message(resources.getString(R.string.initial_message_change_access_code_body)),
|
||||
initialMessage = Message(resources.getStringSafe(R.string.initial_message_change_access_code_body)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -297,7 +312,7 @@ class DefaultTangemSdkManager(
|
|||
return runTaskAsyncReturnOnMain(
|
||||
SetUserCodeCommand.resetUserCodes(),
|
||||
cardId,
|
||||
initialMessage = Message(resources.getString(R.string.initial_message_tap_header)),
|
||||
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -308,7 +323,7 @@ class DefaultTangemSdkManager(
|
|||
return runTaskAsyncReturnOnMain(
|
||||
SetUserCodeRecoveryAllowedTask(enabled),
|
||||
cardId,
|
||||
initialMessage = Message(resources.getString(R.string.initial_message_tap_header)),
|
||||
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -319,7 +334,7 @@ class DefaultTangemSdkManager(
|
|||
return runTaskAsyncReturnOnMain(
|
||||
runnable = ScanTask(allowRequestAccessCodeFromRepository),
|
||||
cardId = cardId,
|
||||
initialMessage = Message(resources.getString(R.string.initial_message_tap_header)),
|
||||
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),
|
||||
)
|
||||
.map { CardDTO(it) }
|
||||
}
|
||||
|
|
@ -372,7 +387,9 @@ class DefaultTangemSdkManager(
|
|||
|
||||
@Deprecated("TangemSdkManager shouldn't returns a string from resources")
|
||||
override fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String {
|
||||
return resources.getString(stringResId, *formatArgs)
|
||||
val args = formatArgs.toSet().filterNotNull().toTypedArray()
|
||||
|
||||
return resources.getStringSafe(stringResId, *args)
|
||||
}
|
||||
|
||||
override fun setUserCodeRequestPolicy(policy: UserCodeRequestPolicy) {
|
||||
|
|
@ -457,6 +474,28 @@ class DefaultTangemSdkManager(
|
|||
|
||||
// endregion
|
||||
|
||||
// region Visa-specific
|
||||
|
||||
override suspend fun activateVisaCard(
|
||||
accessCode: String,
|
||||
challengeToSign: VisaAuthChallenge.Card?,
|
||||
activationInput: VisaActivationInput,
|
||||
): CompletionResult<VisaCardActivationResponse> {
|
||||
return coroutineScope {
|
||||
runTaskAsyncReturnOnMain(
|
||||
runnable = visaCardActivationTaskFactory.create(
|
||||
accessCode = accessCode,
|
||||
challengeToSign = challengeToSign,
|
||||
activationInput = activationInput,
|
||||
coroutineScope = this,
|
||||
),
|
||||
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
companion object {
|
||||
private const val MAX_INITIALIZE_ATTEMPTS = 10
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,14 @@ import com.tangem.common.core.CardSessionRunnable
|
|||
import com.tangem.common.core.UserCodeRequestPolicy
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.services.InMemoryStorage
|
||||
import com.tangem.core.res.getStringSafe
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.visa.model.VisaActivationInput
|
||||
import com.tangem.domain.visa.model.VisaAuthChallenge
|
||||
import com.tangem.domain.visa.model.VisaCardActivationResponse
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.operations.derivation.DerivationTaskResponse
|
||||
import com.tangem.operations.preflightread.PreflightReadFilter
|
||||
|
|
@ -150,7 +154,9 @@ class MockTangemSdkManager(
|
|||
|
||||
@Deprecated("TangemSdkManager shouldn't returns a string from resources")
|
||||
override fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String {
|
||||
return resources.getString(stringResId, *formatArgs)
|
||||
val args = formatArgs.toSet().filterNotNull().toTypedArray()
|
||||
|
||||
return resources.getStringSafe(stringResId, *args)
|
||||
}
|
||||
|
||||
override fun setUserCodeRequestPolicy(policy: UserCodeRequestPolicy) {
|
||||
|
|
@ -191,4 +197,14 @@ class MockTangemSdkManager(
|
|||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Visa-specific
|
||||
|
||||
override suspend fun activateVisaCard(
|
||||
accessCode: String,
|
||||
challengeToSign: VisaAuthChallenge.Card?,
|
||||
activationInput: VisaActivationInput,
|
||||
): CompletionResult<VisaCardActivationResponse> {
|
||||
error("Not implemented")
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import com.tangem.common.core.TangemError
|
|||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.deserialization.WalletDataDeserializer
|
||||
import com.tangem.common.extensions.*
|
||||
import com.tangem.common.map
|
||||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.common.tlv.TlvDecoder
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
|
|
@ -22,11 +23,13 @@ import com.tangem.domain.common.TapWorkarounds.isVisa
|
|||
import com.tangem.domain.common.TwinsHelper
|
||||
import com.tangem.domain.common.configs.CardConfig
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.common.visa.VisaUtilities
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS
|
||||
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||
import com.tangem.operations.ScanTask
|
||||
import com.tangem.operations.backup.PrimaryCard
|
||||
import com.tangem.operations.backup.StartPrimaryCardLinkingTask
|
||||
|
|
@ -35,16 +38,21 @@ import com.tangem.operations.files.ReadFilesTask
|
|||
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.domain.TapSdkError
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import com.tangem.tap.mainScope
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.collections.set
|
||||
|
||||
internal class ScanProductTask(
|
||||
private val card: Card?,
|
||||
private val derivationsFinder: DerivationsFinder?,
|
||||
private val visaCardScanHandler: VisaCardScanHandler?,
|
||||
private val visaCoroutineScope: CoroutineScope?,
|
||||
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?,
|
||||
override val allowsRequestAccessCodeFromRepository: Boolean = false,
|
||||
) : CardSessionRunnable<ScanResponse> {
|
||||
|
||||
|
|
@ -61,6 +69,23 @@ internal class ScanProductTask(
|
|||
return
|
||||
}
|
||||
|
||||
if (VisaUtilities.isVisaCard(cardDto)) {
|
||||
if (onboardingV2FeatureToggles == null ||
|
||||
onboardingV2FeatureToggles.isVisaOnboardingEnabled.not()
|
||||
) {
|
||||
callback(CompletionResult.Failure(TangemSdkError.NotSupportedFirmwareVersion()))
|
||||
return
|
||||
}
|
||||
|
||||
readVisaCard(
|
||||
session = session,
|
||||
cardDto = cardDto,
|
||||
scanWalletProcessor = ScanWalletProcessor(derivationsFinder),
|
||||
callback = callback,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val commandProcessor = when {
|
||||
cardDto.isTangemTwins -> ScanTwinProcessor()
|
||||
else -> ScanWalletProcessor(derivationsFinder)
|
||||
|
|
@ -96,6 +121,43 @@ internal class ScanProductTask(
|
|||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun readVisaCard(
|
||||
session: CardSession,
|
||||
cardDto: CardDTO,
|
||||
scanWalletProcessor: ScanWalletProcessor,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> Unit,
|
||||
) {
|
||||
visaCardScanHandler ?: run {
|
||||
callback(CompletionResult.Failure(TangemSdkError.InsNotSupported()))
|
||||
return
|
||||
}
|
||||
|
||||
visaCoroutineScope ?: run {
|
||||
callback(CompletionResult.Failure(TangemSdkError.InsNotSupported()))
|
||||
return
|
||||
}
|
||||
|
||||
visaCoroutineScope.launch {
|
||||
when (val result = visaCardScanHandler.handleVisaCardScan(session = session)) {
|
||||
is CompletionResult.Success -> {
|
||||
scanWalletProcessor.proceed(
|
||||
card = cardDto,
|
||||
session = session,
|
||||
) { scanResponseResult ->
|
||||
callback(
|
||||
scanResponseResult.map { scanResponse ->
|
||||
scanResponse.copy(visaCardActivationStatus = result.data)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
callback(CompletionResult.Failure(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ScanWalletProcessor(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,268 @@
|
|||
package com.tangem.tap.domain.tasks.visa
|
||||
|
||||
import com.reown.util.hexToBytes
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.CardSession
|
||||
import com.tangem.common.core.CardSessionRunnable
|
||||
import com.tangem.common.core.CompletionCallback
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.map
|
||||
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
|
||||
import com.tangem.datasource.local.visa.VisaOTPStorage
|
||||
import com.tangem.domain.common.visa.VisaUtilities
|
||||
import com.tangem.domain.visa.model.*
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.domain.visa.repository.VisaAuthRepository
|
||||
import com.tangem.operations.GenerateOTPCommand
|
||||
import com.tangem.operations.attestation.AttestCardKeyCommand
|
||||
import com.tangem.operations.pins.SetUserCodeCommand
|
||||
import com.tangem.operations.sign.SignHashCommand
|
||||
import com.tangem.operations.sign.SignHashResponse
|
||||
import com.tangem.operations.wallet.CreateWalletTask
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
class VisaCardActivationTask @AssistedInject constructor(
|
||||
@Assisted private val accessCode: String,
|
||||
@Assisted private val challengeToSign: VisaAuthChallenge.Card?,
|
||||
@Assisted private val activationInput: VisaActivationInput,
|
||||
@Assisted private val coroutineScope: CoroutineScope,
|
||||
private val otpStorage: VisaOTPStorage,
|
||||
private val visaAuthTokenStorage: VisaAuthTokenStorage,
|
||||
private val visaAuthRepository: VisaAuthRepository,
|
||||
private val visaActivationRepository: VisaActivationRepository,
|
||||
) : CardSessionRunnable<VisaCardActivationResponse> {
|
||||
|
||||
override fun run(session: CardSession, callback: CompletionCallback<VisaCardActivationResponse>) {
|
||||
coroutineScope.launch {
|
||||
callback(runSuspend(session))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun runSuspend(session: CardSession): CompletionResult<VisaCardActivationResponse> {
|
||||
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
||||
if (card.cardId != activationInput.cardId) {
|
||||
return CompletionResult.Failure(TangemSdkError.Underlying(VisaActivationError.WrongCard.message))
|
||||
}
|
||||
|
||||
return if (challengeToSign != null) {
|
||||
signAuthorizationChallenge(session, challengeToSign)
|
||||
} else {
|
||||
val activationOrder = runCatching { visaActivationRepository.getActivationOrderToSign() }
|
||||
.getOrElse {
|
||||
return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: ""))
|
||||
}
|
||||
|
||||
signOrder(session, activationOrder)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun signAuthorizationChallenge(
|
||||
session: CardSession,
|
||||
challengeToSign: VisaAuthChallenge.Card,
|
||||
): CompletionResult<VisaCardActivationResponse> {
|
||||
val attestationCommand = AttestCardKeyCommand(challenge = challengeToSign.challenge.hexToBytes())
|
||||
val result = suspendCancellableCoroutine { continuation ->
|
||||
attestationCommand.run(session = session) { attestationResponse ->
|
||||
continuation.resume(attestationResponse)
|
||||
}
|
||||
}
|
||||
|
||||
return when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
processSignedAuthorizationChallenge(
|
||||
session = session,
|
||||
signedChallenge = challengeToSign.toSignedChallenge(
|
||||
signedChallenge = result.data.cardSignature.toHexString(),
|
||||
salt = result.data.salt.toHexString(),
|
||||
),
|
||||
)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
CompletionResult.Failure(result.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun processSignedAuthorizationChallenge(
|
||||
session: CardSession,
|
||||
signedChallenge: VisaAuthSignedChallenge,
|
||||
): CompletionResult<VisaCardActivationResponse> {
|
||||
return coroutineScope {
|
||||
val activationOrderDeferred = async { getActivationOrderToSign(signedChallenge) }
|
||||
val otpTaskDeferred = async { createWallet(session) }
|
||||
|
||||
val order = runCatching { activationOrderDeferred.await() }
|
||||
.getOrElse {
|
||||
otpTaskDeferred.cancel()
|
||||
return@coroutineScope CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: ""))
|
||||
}
|
||||
|
||||
otpTaskDeferred.await()
|
||||
|
||||
signOrder(session, order)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(TangemSdkError::class)
|
||||
private suspend fun getActivationOrderToSign(signedChallenge: VisaAuthSignedChallenge): ActivationOrder {
|
||||
val tokens = runCatching {
|
||||
visaAuthRepository.getAccessTokens(signedChallenge)
|
||||
}.getOrElse {
|
||||
throw TangemSdkError.Underlying(
|
||||
"Underlying network error: ${it.message ?: ""}",
|
||||
)
|
||||
}
|
||||
|
||||
visaAuthTokenStorage.store(tokens)
|
||||
|
||||
return visaActivationRepository.getActivationOrderToSign()
|
||||
}
|
||||
|
||||
private suspend fun createWallet(session: CardSession): CompletionResult<Unit> {
|
||||
coroutineScope { ensureActive() }
|
||||
|
||||
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
||||
return if (card.wallets.any { it.curve == VisaUtilities.mandatoryCurve }) {
|
||||
createOTP(session)
|
||||
} else {
|
||||
val createWalletTask = CreateWalletTask(VisaUtilities.mandatoryCurve)
|
||||
val result = suspendCancellableCoroutine { continuation ->
|
||||
createWalletTask.run(session) { createWalletResult ->
|
||||
continuation.resume(createWalletResult)
|
||||
}
|
||||
}
|
||||
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
createOTP(session)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
CompletionResult.Failure(result.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createOTP(session: CardSession): CompletionResult<Unit> {
|
||||
coroutineScope { ensureActive() }
|
||||
|
||||
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
||||
val otp = otpStorage.getOTP(card.cardId)
|
||||
return if (otp != null) {
|
||||
CompletionResult.Success(Unit)
|
||||
} else {
|
||||
val otpCommand = GenerateOTPCommand()
|
||||
val result = suspendCancellableCoroutine { continuation ->
|
||||
otpCommand.run(session) { otpResult ->
|
||||
continuation.resume(otpResult)
|
||||
}
|
||||
}
|
||||
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
otpStorage.saveOTP(card.cardId, result.data.rootOTP)
|
||||
CompletionResult.Success(Unit)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
CompletionResult.Failure(result.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun signOrder(
|
||||
session: CardSession,
|
||||
order: ActivationOrder,
|
||||
): CompletionResult<VisaCardActivationResponse> {
|
||||
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
val wallet =
|
||||
card.wallets.firstOrNull() ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
val task = SignHashCommand(order.hash.hexToBytes(), wallet.publicKey)
|
||||
val result = suspendCancellableCoroutine { continuation ->
|
||||
task.run(session) { signResult ->
|
||||
continuation.resume(signResult)
|
||||
}
|
||||
}
|
||||
|
||||
return when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
handleSignedOrder(
|
||||
session = session,
|
||||
activationOrder = order,
|
||||
response = result.data,
|
||||
)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
CompletionResult.Failure(result.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleSignedOrder(
|
||||
session: CardSession,
|
||||
activationOrder: ActivationOrder,
|
||||
response: SignHashResponse,
|
||||
): CompletionResult<VisaCardActivationResponse> {
|
||||
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
||||
val signedOrder = SignedActivationOrder(
|
||||
activationOrder = activationOrder,
|
||||
signature = response.signature.toHexString(),
|
||||
)
|
||||
|
||||
val otp = otpStorage.getOTP(card.cardId) ?: return CompletionResult.Failure(
|
||||
TangemSdkError.Underlying(VisaActivationError.MissingRootOTP.message),
|
||||
)
|
||||
|
||||
val activationResponse = VisaCardActivationResponse(
|
||||
signedActivationOrder = signedOrder,
|
||||
rootOTP = VisaRootOTP(otp.toHexString()),
|
||||
)
|
||||
|
||||
return setupAccessCode(session).map { activationResponse }
|
||||
}
|
||||
|
||||
private suspend fun setupAccessCode(session: CardSession): CompletionResult<Unit> {
|
||||
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
||||
if (card.isAccessCodeSet) {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
val task = SetUserCodeCommand.changeAccessCode(accessCode)
|
||||
val result = suspendCancellableCoroutine { continuation ->
|
||||
task.run(session) { setAccessCodeResult ->
|
||||
continuation.resume(setAccessCodeResult)
|
||||
}
|
||||
}
|
||||
|
||||
return when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
CompletionResult.Success(Unit)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
CompletionResult.Failure(result.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
accessCode: String,
|
||||
challengeToSign: VisaAuthChallenge.Card?,
|
||||
activationInput: VisaActivationInput,
|
||||
coroutineScope: CoroutineScope,
|
||||
): VisaCardActivationTask
|
||||
}
|
||||
}
|
||||
|
|
@ -23,8 +23,13 @@ class FinalizeTwinTask(
|
|||
PreflightReadTask(PreflightReadMode.FullCardRead).run(session) { readResult ->
|
||||
when (readResult) {
|
||||
is CompletionResult.Success ->
|
||||
ScanProductTask(readResult.data, derivationsFinder = null)
|
||||
.run(session, callback)
|
||||
ScanProductTask(
|
||||
readResult.data,
|
||||
derivationsFinder = null,
|
||||
visaCardScanHandler = null,
|
||||
visaCoroutineScope = null,
|
||||
onboardingV2FeatureToggles = null,
|
||||
).run(session, callback)
|
||||
is CompletionResult.Failure ->
|
||||
callback(CompletionResult.Failure(readResult.error))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.common.json.TangemSdkAdapter
|
|||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.models.scan.serialization.*
|
||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.sdk.storage.AndroidSecureStorage
|
||||
import com.tangem.sdk.storage.createEncryptedSharedPreferences
|
||||
|
|
@ -60,6 +61,7 @@ internal object UserWalletsListManagerModule {
|
|||
.add(TangemSdkAdapter.DateAdapter())
|
||||
.add(TangemSdkAdapter.DerivationNodeAdapter())
|
||||
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
|
||||
.add(VisaCardActivationStatus.jsonAdapter)
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,285 @@
|
|||
package com.tangem.tap.domain.visa
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.CardWallet
|
||||
import com.tangem.common.core.CardSession
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
|
||||
import com.tangem.domain.common.visa.VisaUtilities
|
||||
import com.tangem.domain.visa.model.*
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.domain.visa.repository.VisaAuthRepository
|
||||
import com.tangem.operations.attestation.AttestCardKeyCommand
|
||||
import com.tangem.operations.attestation.AttestCardKeyResponse
|
||||
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
|
||||
import com.tangem.operations.sign.SignHashCommand
|
||||
import com.tangem.operations.sign.SignHashResponse
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
internal class VisaCardScanHandler @Inject constructor(
|
||||
private val visaAuthRepository: VisaAuthRepository,
|
||||
private val visaActivationRepository: VisaActivationRepository,
|
||||
private val visaAuthTokenStorage: VisaAuthTokenStorage,
|
||||
) {
|
||||
|
||||
suspend fun handleVisaCardScan(session: CardSession): CompletionResult<VisaCardActivationStatus> {
|
||||
Timber.i("Attempting to handle Visa card scan")
|
||||
|
||||
val card = session.environment.card ?: run {
|
||||
Timber.e("Card is null")
|
||||
return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
}
|
||||
|
||||
val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run {
|
||||
val activationInput =
|
||||
VisaActivationInput(card.cardId, card.cardPublicKey, card.isAccessCodeSet)
|
||||
val activationStatus = VisaCardActivationStatus.NotStartedActivation(activationInput)
|
||||
return CompletionResult.Success(activationStatus)
|
||||
}
|
||||
|
||||
return deriveKey(wallet, session)
|
||||
}
|
||||
|
||||
private suspend fun deriveKey(
|
||||
wallet: CardWallet,
|
||||
session: CardSession,
|
||||
): CompletionResult<VisaCardActivationStatus> {
|
||||
val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run {
|
||||
Timber.e("Failed to create derivation path while first scan")
|
||||
|
||||
return CompletionResult.Failure(
|
||||
TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToCreateDerivationPath.errorDescription),
|
||||
)
|
||||
}
|
||||
|
||||
val derivationTask = DeriveWalletPublicKeyTask(wallet.publicKey, derivationPath)
|
||||
val derivationTaskResult = suspendCancellableCoroutine { continuation ->
|
||||
derivationTask.run(session) { result ->
|
||||
continuation.resume(result)
|
||||
}
|
||||
}
|
||||
return handleDerivationResponse(derivationTaskResult, session)
|
||||
}
|
||||
|
||||
private suspend fun handleDerivationResponse(
|
||||
result: CompletionResult<ExtendedPublicKey>,
|
||||
session: CardSession,
|
||||
): CompletionResult<VisaCardActivationStatus> {
|
||||
return when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
Timber.i("Start task for loading challenge for Visa wallet")
|
||||
handleWalletAuthorization(session)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
CompletionResult.Failure(result.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleWalletAuthorization(session: CardSession): CompletionResult<VisaCardActivationStatus> {
|
||||
Timber.i("Started handling authorization using Visa wallet")
|
||||
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
||||
val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run {
|
||||
Timber.e("Failed to create derivation path while handling wallet authorization")
|
||||
return CompletionResult.Failure(
|
||||
TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToCreateDerivationPath.errorDescription),
|
||||
)
|
||||
}
|
||||
|
||||
val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run {
|
||||
Timber.e("Failed to find extended public key while handling wallet authorization")
|
||||
return CompletionResult.Failure(
|
||||
TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToFindDerivedWalletKey.errorDescription),
|
||||
)
|
||||
}
|
||||
|
||||
val extendedPublicKey = wallet.derivedKeys[derivationPath] ?: run {
|
||||
Timber.e("Failed to find extended public key while handling wallet authorization")
|
||||
return CompletionResult.Failure(
|
||||
TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToFindDerivedWalletKey.errorDescription),
|
||||
)
|
||||
}
|
||||
|
||||
Timber.i("Requesting challenge for wallet authorization")
|
||||
// Will be changed later after backend implementation
|
||||
val challengeResponse = runCatching {
|
||||
visaAuthRepository.getCustomerWalletAuthChallenge(
|
||||
cardId = card.cardId,
|
||||
walletPublicKey = extendedPublicKey.publicKey.toHexString(),
|
||||
)
|
||||
}.getOrElse {
|
||||
return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: "Unknown error"))
|
||||
}
|
||||
|
||||
val signChallengeResult = signChallengeWithWallet(
|
||||
publicKey = wallet.publicKey,
|
||||
derivationPath = derivationPath,
|
||||
nonce = challengeResponse.challenge,
|
||||
session = session,
|
||||
)
|
||||
|
||||
return when (signChallengeResult) {
|
||||
is CompletionResult.Success -> {
|
||||
Timber.i("Challenge signed with Wallet public key")
|
||||
handleWalletAuthorizationTokens(
|
||||
session = session,
|
||||
signedChallenge = challengeResponse
|
||||
.toSignedChallenge(signChallengeResult.data.signature.toHexString()),
|
||||
)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
Timber.e("Error during Wallet authorization process. Tangem Sdk Error: ${signChallengeResult.error}")
|
||||
CompletionResult.Failure(signChallengeResult.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleWalletAuthorizationTokens(
|
||||
session: CardSession,
|
||||
signedChallenge: VisaAuthSignedChallenge,
|
||||
): CompletionResult<VisaCardActivationStatus> {
|
||||
val authorizationTokensResponse = runCatching {
|
||||
visaAuthRepository.getAccessTokens(signedChallenge = signedChallenge)
|
||||
}.getOrElse {
|
||||
Timber.i(
|
||||
"Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key",
|
||||
)
|
||||
return handleCardAuthorization(session)
|
||||
}
|
||||
|
||||
visaAuthTokenStorage.store(authorizationTokensResponse)
|
||||
|
||||
Timber.i("Authorized using Wallet public key successfully")
|
||||
|
||||
return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse))
|
||||
}
|
||||
|
||||
private suspend fun handleCardAuthorization(session: CardSession): CompletionResult<VisaCardActivationStatus> {
|
||||
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
||||
Timber.i("Requesting authorization challenge to sign")
|
||||
|
||||
val challengeResponse = runCatching {
|
||||
visaAuthRepository.getCardAuthChallenge(
|
||||
cardId = card.cardId,
|
||||
cardPublicKey = card.cardPublicKey.toHexString(),
|
||||
)
|
||||
}.getOrElse {
|
||||
Timber.e("Failed to get challenge for Card authorization. Plain error: ${it.message}")
|
||||
return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: "Unknown error"))
|
||||
}
|
||||
|
||||
Timber.i("Received challenge to sign: ${challengeResponse.challenge}")
|
||||
|
||||
val signChallengeResult = signChallengeWithCard(session = session, challenge = challengeResponse.challenge)
|
||||
|
||||
val attestCardKeyResponse = when (signChallengeResult) {
|
||||
is CompletionResult.Success -> {
|
||||
Timber.i("Challenged signed.")
|
||||
signChallengeResult.data
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
Timber.e(
|
||||
"Failed to sign challenge with Card public key. Tangem Sdk Error: ${signChallengeResult.error}",
|
||||
)
|
||||
return CompletionResult.Failure(signChallengeResult.error)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
val authorizationTokensResponse = runCatching {
|
||||
visaAuthRepository.getAccessTokens(
|
||||
signedChallenge = challengeResponse.toSignedChallenge(
|
||||
signedChallenge = attestCardKeyResponse.cardSignature.toHexString(),
|
||||
salt = attestCardKeyResponse.salt.toHexString(),
|
||||
),
|
||||
)
|
||||
}.getOrElse {
|
||||
Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.message}")
|
||||
return CompletionResult.Failure(
|
||||
TangemSdkError.Underlying(
|
||||
customMessage = it.message ?: "Unknown error",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
visaAuthTokenStorage.store(authorizationTokensResponse)
|
||||
|
||||
val activationRemoteState = visaActivationRepository.getActivationRemoteState()
|
||||
|
||||
val error = when (activationRemoteState) {
|
||||
VisaActivationRemoteState.BlockedForActivation -> VisaActivationError.BlockedForActivation
|
||||
VisaActivationRemoteState.Activated -> VisaActivationError.InvalidActivationState
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (error != null) {
|
||||
return CompletionResult.Failure(TangemSdkError.Underlying(error.message))
|
||||
}
|
||||
|
||||
val activationInput = VisaActivationInput(
|
||||
cardId = card.cardId,
|
||||
cardPublicKey = card.cardPublicKey,
|
||||
isAccessCodeSet = card.isAccessCodeSet,
|
||||
)
|
||||
|
||||
return CompletionResult.Success(
|
||||
VisaCardActivationStatus.ActivationStarted(
|
||||
activationInput = activationInput,
|
||||
authTokens = authorizationTokensResponse,
|
||||
remoteState = activationRemoteState,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun signChallengeWithWallet(
|
||||
publicKey: ByteArray,
|
||||
derivationPath: DerivationPath,
|
||||
nonce: String,
|
||||
session: CardSession,
|
||||
): CompletionResult<SignHashResponse> {
|
||||
val signHashCommand = SignHashCommand(publicKey, nonce.toByteArray(), derivationPath)
|
||||
val result = suspendCancellableCoroutine {
|
||||
signHashCommand.run(session) { result ->
|
||||
it.resume(result)
|
||||
}
|
||||
}
|
||||
|
||||
return when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
CompletionResult.Success(result.data)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
CompletionResult.Failure(result.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun signChallengeWithCard(
|
||||
session: CardSession,
|
||||
challenge: String,
|
||||
): CompletionResult<AttestCardKeyResponse> {
|
||||
val signHashCommand = AttestCardKeyCommand(challenge = challenge.toByteArray())
|
||||
val result = suspendCancellableCoroutine { continuation ->
|
||||
signHashCommand.run(session) { result ->
|
||||
continuation.resume(result)
|
||||
}
|
||||
}
|
||||
|
||||
return when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
CompletionResult.Success(result.data)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
CompletionResult.Failure(result.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.tap.domain.visa
|
||||
|
||||
private const val COMMON_DESCRIPTION = "Error occurred. Please contact support."
|
||||
|
||||
internal enum class VisaCardScanHandlerError(val errorDescription: String) {
|
||||
FailedToCreateDerivationPath(COMMON_DESCRIPTION),
|
||||
FailedToFindWallet(COMMON_DESCRIPTION),
|
||||
FailedToFindDerivedWalletKey(COMMON_DESCRIPTION),
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ import com.tangem.common.routing.AppRoute
|
|||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Settings
|
||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||
|
|
@ -18,6 +17,9 @@ import com.tangem.tap.common.redux.AppState
|
|||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -112,7 +114,7 @@ class DetailsMiddleware {
|
|||
|
||||
private fun enrollBiometrics() {
|
||||
Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication)
|
||||
activityResultCaller.openSystemBiometrySettings()
|
||||
store.inject(DaggerGraphState::settingsManager).openBiometricSettings()
|
||||
}
|
||||
|
||||
private fun changeAppThemeMode(appThemeMode: AppThemeMode) {
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ class WalletConnectMiddleware {
|
|||
is WalletConnectAction.StartWalletConnect -> {
|
||||
val uri = action.copiedUri
|
||||
if (uri != null && isWalletConnectUri(uri)) {
|
||||
// TODO check
|
||||
store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri))
|
||||
} else {
|
||||
store.dispatchNavigationAction { push(AppRoute.QrScanning(SourceType.WALLET_CONNECT)) }
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import androidx.compose.ui.focus.focusRequester
|
|||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
|
|
@ -27,8 +26,9 @@ import com.tangem.core.ui.components.RectangleShimmer
|
|||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorState.Currency
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -114,7 +114,7 @@ private fun TopBar(
|
|||
is AppCurrencySelectorState.Loading,
|
||||
is AppCurrencySelectorState.Default,
|
||||
-> Text(
|
||||
text = stringResource(id = R.string.details_row_title_currency),
|
||||
text = stringResourceSafe(id = R.string.details_row_title_currency),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
)
|
||||
}
|
||||
|
|
@ -163,7 +163,7 @@ private fun SearchBar(onInputChange: (String) -> Unit, modifier: Modifier = Modi
|
|||
singleLine = true,
|
||||
textStyle = TangemTheme.typography.subtitle2,
|
||||
placeholder = {
|
||||
Text(text = stringResource(id = R.string.common_search))
|
||||
Text(text = stringResourceSafe(id = R.string.common_search))
|
||||
},
|
||||
colors = SearchBarColors,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ package com.tangem.tap.features.details.ui.appsettings.components
|
|||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.BasicDialog
|
||||
import com.tangem.core.ui.components.DialogButtonUM
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
|
||||
|
|
@ -26,7 +26,7 @@ internal fun SettingsAlertDialog(dialog: Dialog.Alert) {
|
|||
onClick = dialog.onConfirm,
|
||||
),
|
||||
dismissButton = DialogButtonUM(
|
||||
title = stringResource(id = R.string.common_cancel),
|
||||
title = stringResourceSafe(id = R.string.common_cancel),
|
||||
onClick = dialog.onDismiss,
|
||||
),
|
||||
onDismissDialog = dialog.onDismiss,
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ package com.tangem.tap.features.details.ui.appsettings.components
|
|||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.DialogButtonUM
|
||||
import com.tangem.core.ui.components.SelectorDialog
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
|
||||
|
|
@ -22,7 +22,7 @@ internal fun SettingsSelectorDialog(dialog: Dialog.Selector) {
|
|||
selectedItemIndex = dialog.selectedItemIndex,
|
||||
items = dialog.items.map { it.resolveReference() }.toImmutableList(),
|
||||
confirmButton = DialogButtonUM(
|
||||
title = stringResource(R.string.common_cancel),
|
||||
title = stringResourceSafe(R.string.common_cancel),
|
||||
onClick = dialog.onDismiss,
|
||||
),
|
||||
onSelect = dialog.onSelect,
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.tap.features.details.ui.common.DetailsMainButton
|
||||
|
|
@ -88,13 +88,13 @@ private fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
|
|||
),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.scan_card_settings_title),
|
||||
text = stringResourceSafe(id = R.string.scan_card_settings_title),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.h3,
|
||||
)
|
||||
Spacer(modifier = Modifier.size(TangemTheme.dimens.size20))
|
||||
Text(
|
||||
text = stringResource(id = R.string.scan_card_settings_message),
|
||||
text = stringResourceSafe(id = R.string.scan_card_settings_message),
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
style = TangemTheme.typography.body1,
|
||||
modifier = Modifier
|
||||
|
|
@ -103,7 +103,7 @@ private fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
|
|||
)
|
||||
Spacer(modifier = Modifier.size(TangemTheme.dimens.size32))
|
||||
DetailsMainButton(
|
||||
title = stringResource(id = R.string.scan_card_settings_button),
|
||||
title = stringResourceSafe(id = R.string.scan_card_settings_button),
|
||||
onClick = onScanCardClick,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.tap.features.details.ui.cardsettings
|
|||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.tap.features.details.redux.SecurityOption
|
||||
import com.tangem.tap.features.details.ui.securitymode.toTitleRes
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -77,7 +77,7 @@ internal sealed interface TextReference {
|
|||
@ReadOnlyComposable
|
||||
internal fun TextReference.resolveReference(): String {
|
||||
return when (this) {
|
||||
is TextReference.Res -> stringResource(this.id, *this.formatArgs.toTypedArray())
|
||||
is TextReference.Res -> stringResourceSafe(this.id, *this.formatArgs.toTypedArray())
|
||||
is TextReference.Str -> this.value
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import androidx.compose.foundation.rememberScrollState
|
|||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.details.ui.common.DetailsMainButton
|
||||
import com.tangem.tap.features.details.ui.common.DetailsRadioButtonElement
|
||||
|
|
@ -36,14 +36,14 @@ fun AccessCodeRecoveryOptions(state: AccessCodeRecoveryScreenState) {
|
|||
)
|
||||
|
||||
DetailsRadioButtonElement(
|
||||
title = stringResource(id = R.string.common_enabled),
|
||||
subtitle = stringResource(id = R.string.card_settings_access_code_recovery_enabled_description),
|
||||
title = stringResourceSafe(id = R.string.common_enabled),
|
||||
subtitle = stringResourceSafe(id = R.string.card_settings_access_code_recovery_enabled_description),
|
||||
selected = state.enabledSelection,
|
||||
onClick = { state.onOptionClick(true) },
|
||||
)
|
||||
DetailsRadioButtonElement(
|
||||
title = stringResource(id = R.string.common_disabled),
|
||||
subtitle = stringResource(id = R.string.card_settings_access_code_recovery_disabled_description),
|
||||
title = stringResourceSafe(id = R.string.common_disabled),
|
||||
subtitle = stringResourceSafe(id = R.string.card_settings_access_code_recovery_disabled_description),
|
||||
selected = !state.enabledSelection,
|
||||
onClick = { state.onOptionClick(false) },
|
||||
)
|
||||
|
|
@ -51,7 +51,7 @@ fun AccessCodeRecoveryOptions(state: AccessCodeRecoveryScreenState) {
|
|||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
DetailsMainButton(
|
||||
title = stringResource(id = R.string.common_save_changes),
|
||||
title = stringResourceSafe(id = R.string.common_save_changes),
|
||||
enabled = state.isSaveChangesEnabled,
|
||||
onClick = { state.onSaveChangesClick() },
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing20),
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -64,7 +64,7 @@ internal fun SettingsScreensScaffold(
|
|||
) {
|
||||
if (titleRes != null) {
|
||||
Text(
|
||||
text = stringResource(id = titleRes),
|
||||
text = stringResourceSafe(id = titleRes),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing20)
|
||||
.padding(bottom = TangemTheme.dimens.spacing36),
|
||||
|
|
@ -82,7 +82,7 @@ internal fun SettingsScreensScaffold(
|
|||
@Composable
|
||||
internal fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = stringResource(id = titleRes),
|
||||
text = stringResourceSafe(id = titleRes),
|
||||
modifier = modifier.padding(start = 20.dp, end = 20.dp),
|
||||
style = TangemTheme.typography.h1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
|
|
@ -71,7 +71,7 @@ private fun ResetCardView(state: ResetCardScreenState) {
|
|||
@Composable
|
||||
private fun Title() {
|
||||
Text(
|
||||
text = stringResource(id = R.string.card_settings_reset_card_to_factory),
|
||||
text = stringResourceSafe(id = R.string.card_settings_reset_card_to_factory),
|
||||
style = TangemTheme.typography.h1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
|
@ -89,7 +89,7 @@ private fun AlertImage() {
|
|||
@Composable
|
||||
private fun Subtitle() {
|
||||
Text(
|
||||
text = stringResource(id = R.string.common_attention),
|
||||
text = stringResourceSafe(id = R.string.common_attention),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
|
@ -179,7 +179,7 @@ private fun ColumnScope.DynamicSpacer(scrollState: ScrollState) {
|
|||
@Composable
|
||||
private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) {
|
||||
DetailsMainButton(
|
||||
title = stringResource(id = R.string.reset_card_to_factory_button_title),
|
||||
title = stringResourceSafe(id = R.string.reset_card_to_factory_button_title),
|
||||
onClick = onResetButtonClick,
|
||||
enabled = enabled,
|
||||
)
|
||||
|
|
@ -188,14 +188,14 @@ private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) {
|
|||
@Composable
|
||||
private fun CommonResetDialog(dialog: ResetCardScreenState.Dialog) {
|
||||
BasicDialog(
|
||||
title = stringResource(dialog.titleResId),
|
||||
message = stringResource(dialog.messageResId),
|
||||
title = stringResourceSafe(dialog.titleResId),
|
||||
message = stringResourceSafe(dialog.messageResId),
|
||||
dismissButton = DialogButtonUM(
|
||||
title = stringResource(id = R.string.common_cancel),
|
||||
title = stringResourceSafe(id = R.string.common_cancel),
|
||||
onClick = dialog.onDismiss,
|
||||
),
|
||||
confirmButton = DialogButtonUM(
|
||||
title = stringResource(id = R.string.card_settings_action_sheet_reset),
|
||||
title = stringResourceSafe(id = R.string.card_settings_action_sheet_reset),
|
||||
warning = true,
|
||||
onClick = dialog.onConfirmClick,
|
||||
),
|
||||
|
|
@ -206,10 +206,10 @@ private fun CommonResetDialog(dialog: ResetCardScreenState.Dialog) {
|
|||
@Composable
|
||||
private fun CompletedResetDialog(dialog: ResetCardDialog) {
|
||||
BasicDialog(
|
||||
title = stringResource(id = dialog.titleResId),
|
||||
message = stringResource(id = dialog.messageResId),
|
||||
title = stringResourceSafe(id = dialog.titleResId),
|
||||
message = stringResourceSafe(id = dialog.messageResId),
|
||||
confirmButton = DialogButtonUM(
|
||||
title = stringResource(id = R.string.common_ok),
|
||||
title = stringResourceSafe(id = R.string.common_ok),
|
||||
onClick = dialog.onConfirmClick,
|
||||
),
|
||||
onDismissDialog = {},
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import androidx.compose.foundation.rememberScrollState
|
|||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.tap.features.details.redux.SecurityOption
|
||||
import com.tangem.tap.features.details.ui.common.DetailsMainButton
|
||||
import com.tangem.tap.features.details.ui.common.DetailsRadioButtonElement
|
||||
|
|
@ -47,7 +47,7 @@ private fun SecurityModeOptions(state: SecurityModeScreenState) {
|
|||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
DetailsMainButton(
|
||||
title = stringResource(id = R.string.common_save_changes),
|
||||
title = stringResourceSafe(id = R.string.common_save_changes),
|
||||
enabled = state.isSaveChangesEnabled,
|
||||
onClick = state.onSaveChangesClicked,
|
||||
modifier = Modifier.padding(start = 20.dp, end = 20.dp),
|
||||
|
|
@ -68,8 +68,8 @@ private fun SecurityOption(option: SecurityOption, state: SecurityModeScreenStat
|
|||
}
|
||||
|
||||
DetailsRadioButtonElement(
|
||||
title = stringResource(id = title),
|
||||
subtitle = stringResource(id = subtitle),
|
||||
title = stringResourceSafe(id = title),
|
||||
subtitle = stringResourceSafe(id = subtitle),
|
||||
selected = selected,
|
||||
onClick = { state.onNewModeSelected(option) },
|
||||
)
|
||||
|
|
@ -80,7 +80,7 @@ private fun SecurityOption(option: SecurityOption, state: SecurityModeScreenStat
|
|||
private fun SecurityModeScreenPreview() {
|
||||
SecurityModeScreen(
|
||||
state = SecurityModeScreenState(
|
||||
availableOptions = SecurityOption.values().toList(),
|
||||
availableOptions = SecurityOption.entries,
|
||||
selectedSecurityMode = SecurityOption.LongTap,
|
||||
isSaveChangesEnabled = false,
|
||||
onNewModeSelected = {},
|
||||
|
|
|
|||
|
|
@ -5,21 +5,19 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.common.analytics.events.Settings
|
||||
import com.tangem.tap.common.extensions.getFromClipboard
|
||||
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -30,8 +28,6 @@ internal fun WalletConnectScreen(
|
|||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
SettingsScreensScaffold(
|
||||
modifier = modifier,
|
||||
content = {
|
||||
|
|
@ -46,7 +42,7 @@ internal fun WalletConnectScreen(
|
|||
AddSessionFab(
|
||||
onAddSession = {
|
||||
Analytics.send(Settings.ButtonStartWalletConnectSession())
|
||||
state.onAddSession(context.getFromClipboard()?.toString())
|
||||
state.onAddSession()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -60,7 +56,7 @@ internal fun WalletConnectScreen(
|
|||
private fun AddSessionFab(onAddSession: () -> Unit, modifier: Modifier = Modifier) {
|
||||
FloatingActionButton(
|
||||
onClick = onAddSession,
|
||||
backgroundColor = TangemTheme.colors.button.primary,
|
||||
containerColor = TangemTheme.colors.button.primary,
|
||||
contentColor = TangemTheme.colors.icon.primary2,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
modifier = modifier,
|
||||
|
|
@ -96,7 +92,7 @@ private fun EmptyScreen(state: WalletConnectScreenState) {
|
|||
)
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.wallet_connect_subtitle),
|
||||
text = stringResourceSafe(id = R.string.wallet_connect_subtitle),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ internal data class WalletConnectScreenState(
|
|||
val sessions: ImmutableList<WcSessionForScreen>,
|
||||
val isLoading: Boolean = false,
|
||||
val onRemoveSession: (String) -> Unit = {},
|
||||
val onAddSession: (String?) -> Unit = {},
|
||||
val onAddSession: () -> Unit = {},
|
||||
)
|
||||
|
||||
data class WcSessionForScreen(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.walletconnect
|
|||
|
||||
import androidx.lifecycle.*
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
|
|
@ -17,6 +18,7 @@ import javax.inject.Inject
|
|||
@HiltViewModel
|
||||
internal class WalletConnectViewModel @Inject constructor(
|
||||
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
|
||||
private val clipboardManager: ClipboardManager,
|
||||
) : ViewModel(), DefaultLifecycleObserver {
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
|
|
@ -35,7 +37,11 @@ internal class WalletConnectViewModel @Inject constructor(
|
|||
sessions.toImmutableList(),
|
||||
isLoading = state.loading,
|
||||
onRemoveSession = { sessionUri -> onRemoveSession(sessionUri, sessions) },
|
||||
onAddSession = { copiedUri -> store.dispatch(WalletConnectAction.StartWalletConnect(copiedUri)) },
|
||||
onAddSession = {
|
||||
store.dispatch(
|
||||
WalletConnectAction.StartWalletConnect(copiedUri = clipboardManager.getText()),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ internal fun StoriesScreen(
|
|||
}
|
||||
}
|
||||
|
||||
// todo refactor [REDACTED_TASK_KEY]
|
||||
StoriesScreenContent(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -75,6 +76,7 @@ internal fun StoriesScreen(
|
|||
)
|
||||
}
|
||||
|
||||
@Deprecated("Use StoriesContainer from core/ui")
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: Modifier = Modifier) {
|
||||
|
|
|
|||
|
|
@ -2,20 +2,23 @@ package com.tangem.tap.features.home.compose.content
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.SpacerH32
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.home.compose.StoriesBottomImageAnimation
|
||||
|
|
@ -27,8 +30,8 @@ fun StoriesRevolutionaryWallet() {
|
|||
SplitContent(
|
||||
topContent = {
|
||||
TopContent(
|
||||
titleText = stringResource(id = R.string.story_awe_title),
|
||||
subtitleText = stringResource(id = R.string.story_awe_description),
|
||||
titleText = stringResourceSafe(id = R.string.story_awe_title),
|
||||
subtitleText = stringResourceSafe(id = R.string.story_awe_description),
|
||||
)
|
||||
},
|
||||
bottomContent = {
|
||||
|
|
@ -46,8 +49,8 @@ fun StoriesUltraSecureBackup(isPaused: Boolean, stepDuration: Int) {
|
|||
SplitContent(
|
||||
topContent = {
|
||||
TopContent(
|
||||
titleText = stringResource(id = R.string.story_backup_title),
|
||||
subtitleText = stringResource(id = R.string.story_backup_description),
|
||||
titleText = stringResourceSafe(id = R.string.story_backup_title),
|
||||
subtitleText = stringResourceSafe(id = R.string.story_backup_description),
|
||||
)
|
||||
},
|
||||
bottomContent = {
|
||||
|
|
@ -65,8 +68,8 @@ fun StoriesCurrencies(isPaused: Boolean, stepDuration: Int) {
|
|||
SplitContent(
|
||||
topContent = {
|
||||
TopContent(
|
||||
titleText = stringResource(id = R.string.story_currencies_title),
|
||||
subtitleText = stringResource(id = R.string.story_currencies_description),
|
||||
titleText = stringResourceSafe(id = R.string.story_currencies_title),
|
||||
subtitleText = stringResourceSafe(id = R.string.story_currencies_description),
|
||||
)
|
||||
},
|
||||
bottomContent = {
|
||||
|
|
@ -81,8 +84,8 @@ fun StoriesWeb3(isPaused: Boolean, stepDuration: Int) {
|
|||
SplitContent(
|
||||
topContent = {
|
||||
TopContent(
|
||||
titleText = stringResource(id = R.string.story_web3_title),
|
||||
subtitleText = stringResource(id = R.string.story_web3_description),
|
||||
titleText = stringResourceSafe(id = R.string.story_web3_title),
|
||||
subtitleText = stringResourceSafe(id = R.string.story_web3_description),
|
||||
)
|
||||
},
|
||||
bottomContent = {
|
||||
|
|
@ -97,8 +100,8 @@ fun StoriesWalletForEveryone(stepDuration: Int) {
|
|||
SplitContent(
|
||||
topContent = {
|
||||
TopContent(
|
||||
titleText = stringResource(id = R.string.story_finish_title),
|
||||
subtitleText = stringResource(id = R.string.story_finish_description),
|
||||
titleText = stringResourceSafe(id = R.string.story_finish_title),
|
||||
subtitleText = stringResourceSafe(id = R.string.story_finish_description),
|
||||
)
|
||||
},
|
||||
bottomContent = {
|
||||
|
|
|
|||
|
|
@ -16,13 +16,13 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.home.compose.StoriesTextAnimation
|
||||
|
|
@ -66,7 +66,7 @@ fun FirstStoriesContent(isPaused: Boolean, duration: Int) {
|
|||
) { modifier ->
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = stringResource(R.string.story_meet_title),
|
||||
text = stringResourceSafe(R.string.story_meet_title),
|
||||
style = style,
|
||||
color = TangemColorPalette.White,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.SpacerW12
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.TestTags
|
||||
|
|
@ -52,7 +52,7 @@ internal fun HomeButtons(
|
|||
private fun ScanCardButton(showProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResource(id = R.string.home_button_scan),
|
||||
text = stringResourceSafe(id = R.string.home_button_scan),
|
||||
useDarkerColors = false,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24),
|
||||
onClick = onClick,
|
||||
|
|
@ -64,7 +64,7 @@ private fun ScanCardButton(showProgress: Boolean, onClick: () -> Unit, modifier:
|
|||
private fun OrderCardButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResource(id = R.string.home_button_order),
|
||||
text = stringResourceSafe(id = R.string.home_button_order),
|
||||
useDarkerColors = true,
|
||||
onClick = onClick,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -18,7 +18,7 @@ import com.tangem.wallet.R
|
|||
internal fun SearchCurrenciesButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResource(id = R.string.common_search_tokens),
|
||||
text = stringResourceSafe(id = R.string.common_search_tokens),
|
||||
icon = TangemButtonIconPosition.Start(R.drawable.ic_search_24),
|
||||
showProgress = false,
|
||||
useDarkerColors = true,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
package com.tangem.tap.features.home.compose.views
|
||||
|
||||
import androidx.compose.material.ButtonColors
|
||||
import androidx.compose.material3.ButtonColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonColors
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -37,16 +36,16 @@ internal fun StoriesButton(
|
|||
)
|
||||
}
|
||||
|
||||
private val LighterButtonColors: ButtonColors = TangemButtonColors(
|
||||
backgroundColor = TangemColorPalette.Light4,
|
||||
private val LighterButtonColors: ButtonColors = ButtonColors(
|
||||
containerColor = TangemColorPalette.Light4,
|
||||
contentColor = TangemColorPalette.Dark6,
|
||||
disabledBackgroundColor = TangemColorPalette.Dark5,
|
||||
disabledContainerColor = TangemColorPalette.Dark5,
|
||||
disabledContentColor = TangemColorPalette.Dark6,
|
||||
)
|
||||
|
||||
private val DarkerButtonColors: ButtonColors = TangemButtonColors(
|
||||
backgroundColor = TangemColorPalette.Dark4,
|
||||
private val DarkerButtonColors: ButtonColors = ButtonColors(
|
||||
containerColor = TangemColorPalette.Dark4,
|
||||
contentColor = TangemColorPalette.White,
|
||||
disabledBackgroundColor = TangemColorPalette.Dark4,
|
||||
disabledContainerColor = TangemColorPalette.Dark4,
|
||||
disabledContentColor = TangemColorPalette.White,
|
||||
)
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
package com.tangem.tap.features.home.data
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.GeoResponse
|
||||
import com.tangem.tap.features.home.domain.HomeRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Implementation of repository for Home feature
|
||||
*
|
||||
* @property tangemTechApi API for server requests
|
||||
* @property dispatchers coroutine dispatcher provider
|
||||
*/
|
||||
class HomeRepositoryImpl(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : HomeRepository {
|
||||
|
||||
override suspend fun getUserCountryCode(): GeoResponse = withContext(dispatchers.io) {
|
||||
tangemTechApi.getUserCountryCode()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package com.tangem.tap.features.home.domain
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.GeoResponse
|
||||
|
||||
/** Repository for Home feature */
|
||||
interface HomeRepository {
|
||||
|
||||
/** Get user's country code */
|
||||
suspend fun getUserCountryCode(): GeoResponse
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
package com.tangem.tap.features.home.featuretoggles
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import javax.inject.Inject
|
||||
|
||||
class HomeFeatureToggles @Inject constructor(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) {
|
||||
|
||||
val isMigrateUserCountryCodeEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "MIGRATE_USER_COUNTRY_CODE_ENABLED")
|
||||
}
|
||||
|
|
@ -53,7 +53,6 @@ private fun handleHomeAction(action: Action) {
|
|||
|
||||
store.dispatch(GlobalAction.RestoreAppCurrency)
|
||||
store.dispatch(GlobalAction.ExchangeManager.Init)
|
||||
store.dispatch(GlobalAction.FetchUserCountry)
|
||||
}
|
||||
is HomeAction.ReadCard -> {
|
||||
action.scope.launch {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.rekotlin.StateType
|
||||
|
||||
// todo refactor [REDACTED_TASK_KEY]
|
||||
data class HomeState(
|
||||
val scanInProgress: Boolean = false,
|
||||
val stories: ImmutableList<Stories> = Stories.entries.toImmutableList(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
package com.tangem.tap.features.intentHandler
|
||||
|
||||
interface AffectsNavigation
|
||||
|
|
@ -19,9 +19,9 @@ class IntentProcessor {
|
|||
intentHandlers.clear()
|
||||
}
|
||||
|
||||
fun handleIntent(intent: Intent?, isFromForeground: Boolean) {
|
||||
intentHandlers.forEach {
|
||||
it.handleIntent(intent, isFromForeground)
|
||||
}
|
||||
fun handleIntent(intent: Intent?, isFromForeground: Boolean, skipNavigationHandlers: Boolean = false) {
|
||||
intentHandlers
|
||||
.filterNot { handler -> skipNavigationHandlers && handler is AffectsNavigation }
|
||||
.forEach { handler -> handler.handleIntent(intent, isFromForeground) }
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import android.os.Build
|
|||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.features.home.redux.HomeAction
|
||||
import com.tangem.tap.features.intentHandler.IntentHandler
|
||||
import com.tangem.tap.features.intentHandler.AffectsNavigation
|
||||
import com.tangem.tap.features.welcome.redux.WelcomeAction
|
||||
import com.tangem.tap.store
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -17,7 +18,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||
class BackgroundScanIntentHandler(
|
||||
private val hasSavedUserWalletsProvider: () -> Boolean,
|
||||
private val scope: CoroutineScope,
|
||||
) : IntentHandler {
|
||||
) : IntentHandler, AffectsNavigation {
|
||||
|
||||
private val nfcActions = arrayOf(
|
||||
NfcAdapter.ACTION_NDEF_DISCOVERED,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.tap.common.extensions.dispatchOnMain
|
|||
import com.tangem.tap.common.extensions.removePrefixOrNull
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
import com.tangem.tap.features.intentHandler.IntentHandler
|
||||
import com.tangem.tap.features.intentHandler.AffectsNavigation
|
||||
import com.tangem.tap.store
|
||||
import timber.log.Timber
|
||||
import java.net.URLDecoder
|
||||
|
|
@ -12,7 +13,7 @@ import java.net.URLDecoder
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class WalletConnectLinkIntentHandler : IntentHandler {
|
||||
class WalletConnectLinkIntentHandler : IntentHandler, AffectsNavigation {
|
||||
|
||||
override fun handleIntent(intent: Intent?, isFromForeground: Boolean): Boolean {
|
||||
val intentData = intent?.data ?: return false
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
package com.tangem.tap.features.main
|
||||
|
||||
internal interface MainIntents {
|
||||
|
||||
fun onHiddenBalanceToastAction()
|
||||
|
||||
fun onShownBalanceToastAction()
|
||||
|
||||
fun onHiddenBalanceNotificationAction(isPermanent: Boolean)
|
||||
|
||||
fun onDismissBottomSheet()
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
package com.tangem.tap.features.main
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.tap.features.main.model.MainScreenState
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
internal class MainScreenStateHolder(private val intents: MainIntents) {
|
||||
|
||||
private val notificationsFactory = NotificationsFactory(intents)
|
||||
|
||||
private val stateFlowInternal: MutableStateFlow<MainScreenState> = MutableStateFlow(getInitialState())
|
||||
|
||||
val stateFlow: StateFlow<MainScreenState> = stateFlowInternal
|
||||
|
||||
fun updateWithHiddenBalancesToast(isBalanceHidden: Boolean) {
|
||||
stateFlowInternal.update { state ->
|
||||
state.copy(
|
||||
toast = triggeredEvent(
|
||||
data = if (isBalanceHidden) {
|
||||
notificationsFactory.createBalancesAreHiddenToast()
|
||||
} else {
|
||||
notificationsFactory.createBalancesAreShownToast()
|
||||
},
|
||||
onConsume = ::consumeToastEvent,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateWithHiddenBalancesNotification() {
|
||||
stateFlowInternal.update { state ->
|
||||
state.copy(
|
||||
modalNotification = TangemBottomSheetConfig(
|
||||
isShow = true,
|
||||
onDismissRequest = intents::onDismissBottomSheet,
|
||||
content = notificationsFactory.createBalancesAreHiddenModalNotification(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateWithoutModalNotification() {
|
||||
stateFlowInternal.update { state ->
|
||||
state.copy(modalNotification = state.modalNotification?.copy(isShow = false))
|
||||
}
|
||||
}
|
||||
|
||||
private fun consumeToastEvent() {
|
||||
stateFlowInternal.update { state ->
|
||||
state.copy(toast = consumedEvent())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialState() = MainScreenState(
|
||||
toast = consumedEvent(),
|
||||
modalNotification = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -3,9 +3,17 @@ package com.tangem.tap.features.main
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tangem.blockchainsdk.BlockchainSDKFactory
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.keyboard.KeyboardValidator
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.event.TechAnalyticsEvent
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.BottomSheetMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase
|
||||
import com.tangem.domain.balancehiding.BalanceHidingSettings
|
||||
|
|
@ -18,8 +26,6 @@ import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
|
|||
import com.tangem.domain.staking.FetchStakingTokensUseCase
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.tap.common.extensions.setContext
|
||||
import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles
|
||||
import com.tangem.tap.features.main.model.MainScreenState
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
|
@ -32,7 +38,6 @@ import javax.inject.Inject
|
|||
internal class MainViewModel @Inject constructor(
|
||||
private val updateBalanceHidingSettingsUseCase: UpdateBalanceHidingSettingsUseCase,
|
||||
private val listenToFlipsUseCase: ListenToFlipsUseCase,
|
||||
private val router: AppRouter,
|
||||
private val fetchAppCurrenciesUseCase: FetchAppCurrenciesUseCase,
|
||||
deleteDeprecatedLogsUseCase: DeleteDeprecatedLogsUseCase,
|
||||
private val incrementAppLaunchCounterUseCase: IncrementAppLaunchCounterUseCase,
|
||||
|
|
@ -41,20 +46,16 @@ internal class MainViewModel @Inject constructor(
|
|||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val fetchStakingTokensUseCase: FetchStakingTokensUseCase,
|
||||
private val apiConfigsManager: ApiConfigsManager,
|
||||
homeFeatureToggles: HomeFeatureToggles,
|
||||
private val fetchUserCountryUseCase: FetchUserCountryUseCase,
|
||||
@GlobalUiMessageSender private val messageSender: UiMessageSender,
|
||||
private val keyboardValidator: KeyboardValidator,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
) : ViewModel(), MainIntents {
|
||||
|
||||
private val stateHolder = MainScreenStateHolder(
|
||||
intents = this,
|
||||
)
|
||||
) : ViewModel() {
|
||||
|
||||
private val balanceHidingSettingsFlow: SharedFlow<BalanceHidingSettings> = getBalanceHidingSettingsUseCase()
|
||||
.shareIn(viewModelScope, SharingStarted.WhileSubscribed())
|
||||
|
||||
val state: StateFlow<MainScreenState> = stateHolder.stateFlow
|
||||
|
||||
var isSplashScreenShown: Boolean = true
|
||||
private set
|
||||
|
||||
|
|
@ -63,11 +64,9 @@ internal class MainViewModel @Inject constructor(
|
|||
|
||||
viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() }
|
||||
|
||||
if (homeFeatureToggles.isMigrateUserCountryCodeEnabled) {
|
||||
viewModelScope.launch {
|
||||
fetchUserCountryUseCase().onLeft {
|
||||
Timber.e("Unable to fetch the user country code $it")
|
||||
}
|
||||
viewModelScope.launch {
|
||||
fetchUserCountryUseCase().onLeft {
|
||||
Timber.e("Unable to fetch the user country code $it")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -79,6 +78,8 @@ internal class MainViewModel @Inject constructor(
|
|||
fetchStakingTokens()
|
||||
|
||||
deleteDeprecatedLogsUseCase()
|
||||
|
||||
sendKeyboardIdentifierEvent()
|
||||
}
|
||||
|
||||
/** Loading the resources needed to run the application */
|
||||
|
|
@ -129,10 +130,34 @@ internal class MainViewModel @Inject constructor(
|
|||
it.isBalanceHidingNotificationEnabled && it.isBalanceHidden
|
||||
}
|
||||
.onEach {
|
||||
if (state.value.modalNotification?.isShow != true && !it.isUpdateFromToast) {
|
||||
if (!it.isUpdateFromToast) {
|
||||
listenToFlipsUseCase.changeUpdateEnabled(false)
|
||||
stateHolder.updateWithHiddenBalancesNotification()
|
||||
router.push(AppRoute.ModalNotification)
|
||||
|
||||
val message = BottomSheetMessage.invoke(
|
||||
iconResId = R.drawable.ic_eye_off_outline_24,
|
||||
title = resourceReference(R.string.balance_hidden_title),
|
||||
message = resourceReference(R.string.balance_hidden_description),
|
||||
onDismissRequest = ::onBottomSheetDismissed,
|
||||
firstActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = resourceReference(R.string.balance_hidden_got_it_button),
|
||||
onClick = {
|
||||
onHiddenBalanceNotificationAction(isPermanent = false)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
},
|
||||
secondActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = resourceReference(R.string.balance_hidden_do_not_show_button),
|
||||
onClick = {
|
||||
onHiddenBalanceNotificationAction(isPermanent = true)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
messageSender.send(message)
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
|
@ -171,11 +196,27 @@ internal class MainViewModel @Inject constructor(
|
|||
private fun displayBalancesHiddenStatusToast(settings: BalanceHidingSettings) {
|
||||
// If modal notification is enabled and balances are hidden, the toast will not show
|
||||
if (!settings.isBalanceHidingNotificationEnabled || !settings.isBalanceHidden) {
|
||||
stateHolder.updateWithHiddenBalancesToast(settings.isBalanceHidden)
|
||||
val message = SnackbarMessage(
|
||||
message = if (settings.isBalanceHidden) {
|
||||
resourceReference(R.string.toast_balances_hidden)
|
||||
} else {
|
||||
resourceReference(R.string.toast_balances_shown)
|
||||
},
|
||||
actionLabel = resourceReference(R.string.toast_undo),
|
||||
action = {
|
||||
if (settings.isBalanceHidden) {
|
||||
onHiddenBalanceToastAction()
|
||||
} else {
|
||||
onShownBalanceToastAction()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
messageSender.send(message)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onHiddenBalanceToastAction() {
|
||||
private fun onHiddenBalanceToastAction() {
|
||||
viewModelScope.launch {
|
||||
updateBalanceHidingSettingsUseCase.invoke {
|
||||
copy(
|
||||
|
|
@ -186,7 +227,7 @@ internal class MainViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onShownBalanceToastAction() {
|
||||
private fun onShownBalanceToastAction() {
|
||||
viewModelScope.launch {
|
||||
updateBalanceHidingSettingsUseCase.invoke {
|
||||
copy(
|
||||
|
|
@ -197,10 +238,14 @@ internal class MainViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onHiddenBalanceNotificationAction(isPermanent: Boolean) {
|
||||
onDismissBottomSheet()
|
||||
private fun onHiddenBalanceNotificationAction(isPermanent: Boolean) {
|
||||
val message = SnackbarMessage(
|
||||
message = resourceReference(R.string.toast_balances_hidden),
|
||||
actionLabel = resourceReference(R.string.toast_undo),
|
||||
action = ::onHiddenBalanceToastAction,
|
||||
)
|
||||
messageSender.send(message)
|
||||
|
||||
stateHolder.updateWithHiddenBalancesToast(true)
|
||||
if (isPermanent) {
|
||||
viewModelScope.launch {
|
||||
updateBalanceHidingSettingsUseCase.invoke {
|
||||
|
|
@ -210,10 +255,27 @@ internal class MainViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onDismissBottomSheet() {
|
||||
listenToFlipsUseCase.changeUpdateEnabled(true)
|
||||
router.pop()
|
||||
stateHolder.updateWithoutModalNotification()
|
||||
stateHolder.updateWithHiddenBalancesToast(true)
|
||||
private fun onBottomSheetDismissed() {
|
||||
listenToFlipsUseCase.changeUpdateEnabled(isUpdateEnabled = true)
|
||||
}
|
||||
|
||||
private fun sendKeyboardIdentifierEvent() {
|
||||
viewModelScope.launch {
|
||||
val keyboardId = keyboardValidator.getKeyboardId()
|
||||
|
||||
if (keyboardId != null) {
|
||||
Timber.d("Keyboard ID: https://play.google.com/store/apps/details?id=${keyboardId.getPackageName()}")
|
||||
|
||||
analyticsEventHandler.send(
|
||||
event = TechAnalyticsEvent.KeyboardIdentifier(
|
||||
id = keyboardId.value,
|
||||
packageName = keyboardId.getPackageName(),
|
||||
isTrusted = keyboardValidator.validate(id = keyboardId),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Timber.e("Unable to get keyboard identifier")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
package com.tangem.tap.features.main
|
||||
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.tap.features.main.model.ActionConfig
|
||||
import com.tangem.tap.features.main.model.ModalNotification
|
||||
import com.tangem.tap.features.main.model.Toast
|
||||
import com.tangem.wallet.R
|
||||
|
||||
internal class NotificationsFactory(
|
||||
private val intents: MainIntents,
|
||||
) {
|
||||
|
||||
fun createBalancesAreHiddenToast(): Toast {
|
||||
return Toast(
|
||||
message = resourceReference(R.string.toast_balances_hidden),
|
||||
action = ActionConfig(
|
||||
text = resourceReference(R.string.toast_undo),
|
||||
onClick = intents::onHiddenBalanceToastAction,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createBalancesAreShownToast(): Toast {
|
||||
return Toast(
|
||||
message = resourceReference(R.string.toast_balances_shown),
|
||||
action = ActionConfig(
|
||||
text = resourceReference(R.string.toast_undo),
|
||||
onClick = intents::onShownBalanceToastAction,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createBalancesAreHiddenModalNotification(): ModalNotification {
|
||||
return ModalNotification(
|
||||
iconResId = R.drawable.ic_eye_off_outline_24,
|
||||
title = resourceReference(R.string.balance_hidden_title),
|
||||
message = resourceReference(R.string.balance_hidden_description),
|
||||
primaryAction = ActionConfig(
|
||||
text = resourceReference(R.string.balance_hidden_got_it_button),
|
||||
onClick = { intents.onHiddenBalanceNotificationAction(isPermanent = false) },
|
||||
),
|
||||
secondaryAction = ActionConfig(
|
||||
text = resourceReference(R.string.balance_hidden_do_not_show_button),
|
||||
onClick = { intents.onHiddenBalanceNotificationAction(isPermanent = true) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package com.tangem.tap.features.main.model
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class ActionConfig(
|
||||
val text: TextReference,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package com.tangem.tap.features.main.model
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
|
||||
internal data class MainScreenState(
|
||||
val toast: StateEvent<Toast>,
|
||||
val modalNotification: TangemBottomSheetConfig?,
|
||||
)
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
package com.tangem.tap.features.main.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class ModalNotification(
|
||||
@DrawableRes val iconResId: Int,
|
||||
val title: TextReference,
|
||||
val message: TextReference,
|
||||
val primaryAction: ActionConfig,
|
||||
val secondaryAction: ActionConfig?,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package com.tangem.tap.features.main.model
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class Toast(
|
||||
val message: TextReference,
|
||||
val action: ActionConfig,
|
||||
)
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
package com.tangem.tap.features.main.ui
|
||||
|
||||
import android.content.DialogInterface
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.screen.ComposeBottomSheetFragment
|
||||
import com.tangem.tap.features.main.MainViewModel
|
||||
import com.tangem.tap.features.main.model.ModalNotification
|
||||
import com.tangem.tap.features.main.ui.components.ModalNotificationContent
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
internal class ModalNotificationBottomSheetFragment : ComposeBottomSheetFragment() {
|
||||
|
||||
@Inject
|
||||
override lateinit var uiDependencies: UiDependencies
|
||||
|
||||
private val viewModel: MainViewModel by activityViewModels()
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val notification = state.modalNotification
|
||||
if (notification == null) {
|
||||
dismiss()
|
||||
return
|
||||
}
|
||||
|
||||
BackHandler(onBack = notification.onDismissRequest)
|
||||
|
||||
when (val content = notification.content) {
|
||||
is ModalNotification -> ModalNotificationContent(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.primary,
|
||||
shape = TangemTheme.shapes.bottomSheet,
|
||||
),
|
||||
notification = content,
|
||||
)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
LaunchedEffect(notification) {
|
||||
if (!notification.isShow) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
viewModel.state.value.modalNotification?.onDismissRequest?.invoke()
|
||||
super.onDismiss(dialog)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
package com.tangem.tap.features.main.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.atoms.Hand
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.main.model.ActionConfig
|
||||
import com.tangem.tap.features.main.model.ModalNotification
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Composable
|
||||
internal fun ModalNotificationContent(notification: ModalNotification, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing40),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Hand()
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size48),
|
||||
painter = painterResource(notification.iconResId),
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
contentDescription = null,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = notification.title.resolveReference(),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
text = notification.message.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(bottom = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = notification.primaryAction.text.resolveReference(),
|
||||
onClick = notification.primaryAction.onClick,
|
||||
)
|
||||
when (val secondaryAction = notification.secondaryAction) {
|
||||
null -> Unit
|
||||
else -> SecondaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = secondaryAction.text.resolveReference(),
|
||||
onClick = secondaryAction.onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(widthDp = 360, heightDp = 404)
|
||||
@Preview(widthDp = 360, heightDp = 404, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ModalNotificationContentPreview(
|
||||
@PreviewParameter(GlobalNotificationProvider::class) param: ModalNotification,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
ModalNotificationContent(
|
||||
notification = param,
|
||||
modifier = Modifier.background(
|
||||
color = TangemTheme.colors.background.primary,
|
||||
shape = TangemTheme.shapes.bottomSheet,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class GlobalNotificationProvider : CollectionPreviewParameterProvider<ModalNotification>(
|
||||
collection = listOf(
|
||||
ModalNotification(
|
||||
iconResId = R.drawable.ic_eye_off_outline_24,
|
||||
title = resourceReference(R.string.balance_hidden_title),
|
||||
message = resourceReference(R.string.balance_hidden_description),
|
||||
primaryAction = ActionConfig(
|
||||
text = resourceReference(R.string.balance_hidden_got_it_button),
|
||||
onClick = {},
|
||||
),
|
||||
secondaryAction = ActionConfig(
|
||||
text = resourceReference(R.string.balance_hidden_do_not_show_button),
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -22,11 +22,9 @@ import com.tangem.tap.common.analytics.events.AnalyticsParam
|
|||
import com.tangem.tap.common.analytics.events.Onboarding
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.common.redux.global.GlobalState
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
|
||||
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
|
||||
import com.tangem.tap.mainScope
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
|
|
@ -50,7 +48,7 @@ object OnboardingHelper {
|
|||
if (!response.twinsIsTwinned()) {
|
||||
true
|
||||
} else {
|
||||
onboardingManager.isActivationInProgress(cardId) ?: false
|
||||
onboardingManager.isActivationInProgress(cardId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +60,7 @@ object OnboardingHelper {
|
|||
emptyWallets || activationInProgress || isNoBackup
|
||||
}
|
||||
|
||||
response.card.wallets.isNotEmpty() -> onboardingManager.isActivationInProgress(cardId) ?: false
|
||||
response.card.wallets.isNotEmpty() -> onboardingManager.isActivationInProgress(cardId)
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
|
@ -70,7 +68,7 @@ object OnboardingHelper {
|
|||
fun whereToNavigate(scanResponse: ScanResponse): AppRoute {
|
||||
val newOnboardingSupportTypes = scanResponse.productType == ProductType.Wallet2 ||
|
||||
scanResponse.productType == ProductType.Ring ||
|
||||
scanResponse.productType == ProductType.Wallet
|
||||
scanResponse.productType == ProductType.Wallet // AppRoute.OnboardingOther is also supported
|
||||
if (store.inject(DaggerGraphState::onboardingV2FeatureToggles).isOnboardingV2Enabled &&
|
||||
newOnboardingSupportTypes
|
||||
) {
|
||||
|
|
@ -222,7 +220,7 @@ object OnboardingHelper {
|
|||
}
|
||||
}
|
||||
|
||||
fun handleTopUpAction(walletManager: WalletManager, scanResponse: ScanResponse, globalState: GlobalState) {
|
||||
fun handleTopUpAction(walletManager: WalletManager, scanResponse: ScanResponse) {
|
||||
val blockchain = walletManager.wallet.blockchain
|
||||
val excludedBlockchains = store.inject(DaggerGraphState::excludedBlockchains)
|
||||
|
||||
|
|
@ -238,17 +236,10 @@ object OnboardingHelper {
|
|||
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
|
||||
|
||||
scope.launch {
|
||||
val homeFeatureToggles = store.inject(DaggerGraphState::homeFeatureToggles)
|
||||
val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase)
|
||||
val isRussia = getUserCountryCodeUseCase.invokeSync().isRight { it is UserCountry.Russia }
|
||||
|
||||
val onrampFeatureToggles = store.inject(DaggerGraphState::onrampFeatureToggles)
|
||||
|
||||
val isRussia = if (homeFeatureToggles.isMigrateUserCountryCodeEnabled) {
|
||||
val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase)
|
||||
|
||||
getUserCountryCodeUseCase().isRight { it is UserCountry.Russia }
|
||||
} else {
|
||||
globalState.userCountryCode == RUSSIA_COUNTRY_CODE
|
||||
}
|
||||
|
||||
if (isRussia && !onrampFeatureToggles.isFeatureEnabled) {
|
||||
val dialogData = AppDialog.RussianCardholdersWarningDialog.Data(topUpUrl)
|
||||
store.dispatchDialogShow(AppDialog.RussianCardholdersWarningDialog(dialogData))
|
||||
|
|
|
|||
|
|
@ -176,7 +176,6 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
|
|||
OnboardingHelper.handleTopUpAction(
|
||||
walletManager = walletManager,
|
||||
scanResponse = scanResponse,
|
||||
globalState = globalState,
|
||||
)
|
||||
}
|
||||
is OnboardingNoteAction.Done -> {
|
||||
|
|
|
|||
|
|
@ -299,7 +299,6 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
|
|||
OnboardingHelper.handleTopUpAction(
|
||||
walletManager = walletManager,
|
||||
scanResponse = onboardingManager?.scanResponse ?: return,
|
||||
globalState = globalState,
|
||||
)
|
||||
}
|
||||
TwinCardsAction.Done -> {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ import com.tangem.core.decompose.context.childByContext
|
|||
import com.tangem.core.decompose.di.RootAppComponentContext
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.extensions.setStatusBarColor
|
||||
import com.tangem.core.ui.message.EventMessageEffect
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.windowsize.rememberWindowSize
|
||||
import com.tangem.datasource.utils.isNullOrEmpty
|
||||
|
|
@ -301,10 +300,6 @@ class OnboardingWalletFragment :
|
|||
windowSize = rememberWindowSize(activity = requireActivity()),
|
||||
) {
|
||||
onboardingManageTokensComponent?.Content(modifier = Modifier.fillMaxSize())
|
||||
EventMessageEffect(
|
||||
messageHandler = uiDependencies.eventMessageHandler,
|
||||
snackbarHostState = uiDependencies.globalSnackbarHostState,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import com.tangem.core.analytics.Analytics
|
|||
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
|
||||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Onboarding
|
||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||
|
|
@ -17,7 +16,11 @@ import com.tangem.tap.common.extensions.inject
|
|||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
|
||||
import com.tangem.tap.mainScope
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -97,7 +100,7 @@ internal class SaveWalletMiddleware {
|
|||
}
|
||||
|
||||
private fun enrollBiometrics() {
|
||||
activityResultCaller.openSystemBiometrySettings()
|
||||
store.inject(DaggerGraphState::settingsManager).openBiometricSettings()
|
||||
}
|
||||
|
||||
private fun allowToUseBiometrics(state: SaveWalletState) = scope.launch {
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.BasicDialog
|
||||
import com.tangem.core.ui.components.DialogButtonUM
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog
|
||||
|
|
@ -17,10 +17,10 @@ import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog
|
|||
@Composable
|
||||
fun EnrollBiometricsDialogContent(dialog: EnrollBiometricsDialog) {
|
||||
BasicDialog(
|
||||
title = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_title),
|
||||
message = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_description),
|
||||
title = stringResourceSafe(R.string.save_user_wallet_agreement_enroll_biometrics_title),
|
||||
message = stringResourceSafe(R.string.save_user_wallet_agreement_enroll_biometrics_description),
|
||||
confirmButton = DialogButtonUM(
|
||||
title = stringResource(R.string.common_enable),
|
||||
title = stringResourceSafe(R.string.common_enable),
|
||||
onClick = dialog.onEnroll,
|
||||
),
|
||||
dismissButton = DialogButtonUM(
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.atoms.Hand
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -63,7 +63,7 @@ private fun Header(onCloseClick: () -> Unit) {
|
|||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
painter = painterResource(id = R.drawable.ic_close_24),
|
||||
tint = TangemTheme.colors.icon.secondary,
|
||||
contentDescription = stringResource(id = R.string.common_cancel),
|
||||
contentDescription = stringResourceSafe(id = R.string.common_cancel),
|
||||
)
|
||||
}
|
||||
SpacerW8()
|
||||
|
|
@ -85,7 +85,7 @@ private fun Title(modifier: Modifier = Modifier) {
|
|||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.save_user_wallet_agreement_header_biometrics),
|
||||
text = stringResourceSafe(id = R.string.save_user_wallet_agreement_header_biometrics),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
@ -102,13 +102,13 @@ private fun Description(modifier: Modifier = Modifier) {
|
|||
) {
|
||||
DescriptionItem(
|
||||
iconPainter = painterResource(id = R.drawable.ic_face_recognition_24),
|
||||
title = stringResource(id = R.string.save_user_wallet_agreement_access_title),
|
||||
description = stringResource(id = R.string.save_user_wallet_agreement_access_description),
|
||||
title = stringResourceSafe(id = R.string.save_user_wallet_agreement_access_title),
|
||||
description = stringResourceSafe(id = R.string.save_user_wallet_agreement_access_description),
|
||||
)
|
||||
DescriptionItem(
|
||||
iconPainter = painterResource(id = R.drawable.ic_lock_24),
|
||||
title = stringResource(id = R.string.save_user_wallet_agreement_code_title),
|
||||
description = stringResource(id = R.string.save_user_wallet_agreement_code_description_biometrics),
|
||||
title = stringResourceSafe(id = R.string.save_user_wallet_agreement_code_title),
|
||||
description = stringResourceSafe(id = R.string.save_user_wallet_agreement_code_description_biometrics),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -125,12 +125,12 @@ private fun Footer(showProgress: Boolean, onSaveWalletClick: () -> Unit) {
|
|||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
showProgress = showProgress,
|
||||
text = stringResource(id = R.string.save_user_wallet_agreement_allow_biometrics),
|
||||
text = stringResourceSafe(id = R.string.save_user_wallet_agreement_allow_biometrics),
|
||||
onClick = onSaveWalletClick,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(fraction = .7f),
|
||||
text = stringResource(R.string.save_user_wallet_agreement_notice),
|
||||
text = stringResourceSafe(R.string.save_user_wallet_agreement_notice),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import com.tangem.tap.common.extensions.*
|
|||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
|
|
@ -26,7 +25,6 @@ import com.tangem.tap.store
|
|||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Middleware
|
||||
|
||||
@Suppress("LargeClass")
|
||||
@Deprecated("Will be removed soon")
|
||||
object TradeCryptoMiddleware {
|
||||
|
||||
|
|
@ -41,23 +39,22 @@ object TradeCryptoMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun handle(state: () -> AppState?, action: TradeCryptoAction) {
|
||||
if (DemoHelper.tryHandle(state, action)) return
|
||||
|
||||
when (action) {
|
||||
is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
|
||||
is TradeCryptoAction.Buy -> proceedBuyAction(state, action)
|
||||
is TradeCryptoAction.Buy -> proceedBuyAction(action)
|
||||
is TradeCryptoAction.Sell -> proceedSellAction(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) {
|
||||
private fun proceedBuyAction(action: TradeCryptoAction.Buy) {
|
||||
val isOnrampEnabled = store.inject(DaggerGraphState::onrampFeatureToggles).isFeatureEnabled
|
||||
if (isOnrampEnabled) {
|
||||
proceedWithOnramp(action.userWallet.walletId, action.cryptoCurrencyStatus.currency, action.source)
|
||||
} else {
|
||||
proceedWithLegacyBuyAction(state, action)
|
||||
proceedWithLegacyBuyAction(action)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -73,7 +70,7 @@ object TradeCryptoMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun proceedWithLegacyBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) {
|
||||
private fun proceedWithLegacyBuyAction(action: TradeCryptoAction.Buy) {
|
||||
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress
|
||||
?.defaultAddress
|
||||
?.let(NetworkAddress.Address::value)
|
||||
|
|
@ -92,15 +89,10 @@ object TradeCryptoMiddleware {
|
|||
)
|
||||
|
||||
scope.launch {
|
||||
val homeFeatureToggles = store.inject(DaggerGraphState::homeFeatureToggles)
|
||||
val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase)
|
||||
val onrampFeatureToggles = store.inject(DaggerGraphState::onrampFeatureToggles)
|
||||
val isRussia = if (homeFeatureToggles.isMigrateUserCountryCodeEnabled) {
|
||||
val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase)
|
||||
|
||||
getUserCountryCodeUseCase().isRight { it is UserCountry.Russia }
|
||||
} else {
|
||||
state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE
|
||||
}
|
||||
val isRussia = getUserCountryCodeUseCase.invokeSync().isRight { it is UserCountry.Russia }
|
||||
|
||||
if (action.checkUserLocation && isRussia && !onrampFeatureToggles.isFeatureEnabled) {
|
||||
val dialogData = topUrl?.let {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.tap.features.welcome.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.tap.features.welcome.model.WelcomeModel
|
||||
import com.tangem.tap.features.welcome.ui.components.WelcomeScreen
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultWelcomeComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted params: WelcomeComponent.Params,
|
||||
) : WelcomeComponent, AppComponentContext by context {
|
||||
|
||||
private val model: WelcomeModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
|
||||
WelcomeScreen(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : WelcomeComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: WelcomeComponent.Params): DefaultWelcomeComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.tap.features.welcome.component
|
||||
|
||||
import com.tangem.common.routing.entity.SerializableIntent
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
|
||||
interface WelcomeComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val intent: SerializableIntent?,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, WelcomeComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.tap.features.welcome.component.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.tap.features.welcome.component.WelcomeComponent
|
||||
import com.tangem.tap.features.welcome.ui.WelcomeScreenState
|
||||
import com.tangem.tap.features.welcome.ui.components.WelcomeScreen
|
||||
|
||||
internal class PreviewWelcomeComponent(
|
||||
private val initialState: WelcomeScreenState = WelcomeScreenState(),
|
||||
) : WelcomeComponent {
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
WelcomeScreen(
|
||||
modifier = modifier,
|
||||
state = initialState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.tap.features.welcome.di
|
||||
|
||||
import com.tangem.tap.features.welcome.component.DefaultWelcomeComponent
|
||||
import com.tangem.tap.features.welcome.component.WelcomeComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindWelcomeComponentFactory(factory: DefaultWelcomeComponent.Factory): WelcomeComponent.Factory
|
||||
}
|
||||
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