Updated on 2026-08-14
This commit is contained in:
commit
c8b755729f
61 changed files with 931 additions and 415 deletions
|
|
@ -55,6 +55,7 @@
|
|||
|
||||
<activity
|
||||
android:name="com.tangem.tap.MainActivity"
|
||||
android:configChanges="uiMode"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="portrait"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import androidx.annotation.StringRes
|
|||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
|
|
@ -37,7 +39,8 @@ import com.tangem.core.navigation.email.EmailSender
|
|||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.data.card.sdk.CardSdkLifecycleObserver
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.data.card.sdk.CardSdkOwner
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.card.ScanCardUseCase
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
|
|
@ -108,7 +111,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
lateinit var testerRouter: TesterRouter
|
||||
|
||||
@Inject
|
||||
lateinit var cardSdkLifecycleObserver: CardSdkLifecycleObserver
|
||||
lateinit var cardSdkOwner: CardSdkOwner
|
||||
|
||||
@Inject
|
||||
lateinit var cardSdkConfigRepository: CardSdkConfigRepository
|
||||
|
|
@ -160,7 +163,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
|
||||
internal val viewModel: MainViewModel by viewModels()
|
||||
|
||||
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode?>
|
||||
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
|
||||
|
||||
// TODO: fixme: inject through DI
|
||||
private val intentProcessor: IntentProcessor = IntentProcessor()
|
||||
|
|
@ -172,9 +175,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val splashScreen = installSplashScreen()
|
||||
// We need to call it before onCreate to prevent unnecessary activity recreation
|
||||
installAppTheme()
|
||||
|
||||
installAppTheme() // We need to call it before onCreate to prevent unnecessary activity recreation
|
||||
val splashScreen = installSplashScreen()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
|
@ -218,7 +222,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
private fun installActivityDependencies() {
|
||||
store.dispatch(NavigationAction.ActivityCreated(WeakReference(this)))
|
||||
|
||||
cardSdkLifecycleObserver.onCreate(context = this)
|
||||
cardSdkOwner.register(activity = this)
|
||||
tangemSdkManager = injectedTangemSdkManager
|
||||
appStateHolder.tangemSdkManager = tangemSdkManager
|
||||
backupService = BackupService.init(cardSdkConfigRepository.sdk, this)
|
||||
|
|
@ -248,14 +252,13 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
|
||||
private fun installAppTheme() {
|
||||
appThemeModeFlow = createAppThemeModeFlow()
|
||||
val mode = runBlocking { appThemeModeFlow.filterNotNull().first() }
|
||||
val mode = runBlocking { appThemeModeFlow.first() }
|
||||
|
||||
updateAppTheme(mode)
|
||||
}
|
||||
|
||||
private fun observeAppThemeModeUpdates() {
|
||||
appThemeModeFlow
|
||||
.filterNotNull()
|
||||
.flowWithLifecycle(lifecycle)
|
||||
.onEach(::updateAppTheme)
|
||||
.launchIn(lifecycleScope)
|
||||
|
|
@ -273,15 +276,17 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
|
||||
private fun createAppThemeModeFlow(): SharedFlow<AppThemeMode?> {
|
||||
private fun createAppThemeModeFlow(): SharedFlow<AppThemeMode> {
|
||||
val tangemApplication = application as TangemApplication
|
||||
|
||||
return tangemApplication.getAppThemeModeUseCase()
|
||||
.filterNotNull()
|
||||
.distinctUntilChanged()
|
||||
.map { maybeMode ->
|
||||
maybeMode.getOrElse { AppThemeMode.DEFAULT }
|
||||
}
|
||||
.shareIn(
|
||||
scope = lifecycleScope + Dispatchers.IO,
|
||||
scope = lifecycleScope,
|
||||
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000),
|
||||
)
|
||||
}
|
||||
|
|
@ -327,7 +332,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
override fun onDestroy() {
|
||||
store.dispatch(NavigationAction.ActivityDestroyed(WeakReference(this)))
|
||||
intentProcessor.removeAll()
|
||||
cardSdkLifecycleObserver.onDestroy(this)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
|
@ -338,9 +342,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
}
|
||||
|
||||
private fun updateAppTheme(appThemeMode: AppThemeMode) {
|
||||
MutableAppThemeModeHolder.value = appThemeMode
|
||||
MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme()
|
||||
|
||||
val mode = when (appThemeMode) {
|
||||
AppThemeMode.FORCE_DARK -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
AppThemeMode.FORCE_LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
|
|
@ -348,7 +349,32 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
}
|
||||
|
||||
setDefaultNightMode(mode)
|
||||
delegate.localNightMode = mode
|
||||
|
||||
MutableAppThemeModeHolder.value = appThemeMode
|
||||
MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
|
||||
/*
|
||||
* We need to manually change the background color of the activity when the UI mode changes to prevent
|
||||
* flickering when navigating between fragments.
|
||||
*
|
||||
|
||||
* `android:configChanges="uiMode"` is set in the manifest.
|
||||
* */
|
||||
updateAppBackground()
|
||||
}
|
||||
|
||||
private fun updateAppBackground() {
|
||||
val backgroundColor = if (isDarkTheme()) {
|
||||
TangemColorPalette.Dark6
|
||||
} else {
|
||||
TangemColorPalette.White
|
||||
}
|
||||
|
||||
findViewById<CoordinatorLayout>(R.id.fragment_container).setBackgroundColor(backgroundColor.toArgb())
|
||||
}
|
||||
|
||||
private fun isDarkTheme(): Boolean {
|
||||
|
|
@ -363,12 +389,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
/*
|
||||
* FIXME: Test functionality. TangemSdk is null on some devices when HomeAction.Read is called
|
||||
* inside IntentHandler.
|
||||
*/
|
||||
cardSdkLifecycleObserver.onCreate(context = this)
|
||||
|
||||
lifecycleScope.launch {
|
||||
intentProcessor.handleIntent(intent, true)
|
||||
}
|
||||
|
|
@ -450,15 +470,16 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
}
|
||||
|
||||
private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) {
|
||||
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
|
||||
val backStack = store.state.navigationState.backStack
|
||||
val isOnInitialScreen = backStack.all { it == AppScreen.Welcome || it == AppScreen.Home }
|
||||
val isNotScannedBefore = store.state.globalState.scanResponse == null
|
||||
val isOnboardingServiceNotActive = !store.state.globalState.onboardingState.onboardingStarted
|
||||
|
||||
when {
|
||||
!backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive -> {
|
||||
!isOnInitialScreen && isNotScannedBefore && isOnboardingServiceNotActive -> {
|
||||
navigateToInitialScreen(intentWhichStartedActivity)
|
||||
}
|
||||
backStackIsEmpty -> {
|
||||
backStack.isEmpty() -> {
|
||||
navigateToInitialScreen(intentWhichStartedActivity)
|
||||
}
|
||||
else -> Unit
|
||||
|
|
|
|||
|
|
@ -353,6 +353,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
|
|||
Log.Level.Network,
|
||||
Log.Level.Error,
|
||||
Log.Level.Biometric,
|
||||
Log.Level.Info,
|
||||
)
|
||||
return TangemLogCollector(logLevels, LogFormat.StairsFormatter())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ internal object CardSDKLoggerModule {
|
|||
Log.Level.Network,
|
||||
Log.Level.Error,
|
||||
Log.Level.Biometric,
|
||||
Log.Level.Info,
|
||||
)
|
||||
|
||||
return TangemCardSDKLogger(
|
||||
|
|
|
|||
|
|
@ -14,15 +14,15 @@ import com.tangem.tap.domain.sdk.TangemSdkManager
|
|||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object CardDomainModule {
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
@Singleton
|
||||
fun provideGetBiometricsStatusUseCase(
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
): GetBiometricsStatusUseCase {
|
||||
|
|
@ -30,7 +30,7 @@ internal object CardDomainModule {
|
|||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
@Singleton
|
||||
fun provideSetAccessCodeRequestPolicyUseCase(
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
): SetAccessCodeRequestPolicyUseCase {
|
||||
|
|
@ -38,35 +38,35 @@ internal object CardDomainModule {
|
|||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
@Singleton
|
||||
fun provideWasWalletAlreadySignedHashesConfirmedUseCase(cardRepository: CardRepository): WasCardScannedUseCase {
|
||||
return WasCardScannedUseCase(cardRepository = cardRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
@Singleton
|
||||
fun provideSetCardWasScannedUseCase(cardRepository: CardRepository): SetCardWasScannedUseCase {
|
||||
return SetCardWasScannedUseCase(cardRepository = cardRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
@Singleton
|
||||
fun provideIsDemoCardUseCase(): IsDemoCardUseCase = IsDemoCardUseCase(config = DemoConfig())
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
@Singleton
|
||||
fun provideDerivePublicKeysUseCase(derivationsRepository: DerivationsRepository): DerivePublicKeysUseCase {
|
||||
return DerivePublicKeysUseCase(derivationsRepository = derivationsRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
@Singleton
|
||||
fun provideIsNeedToBackupUseCase(userWalletsListManager: UserWalletsListManager): IsNeedToBackupUseCase {
|
||||
return IsNeedToBackupUseCase(userWalletsListManager = userWalletsListManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
@Singleton
|
||||
fun provideGetExtendedPublicKeyForCurrencyUseCase(
|
||||
derivationsRepository: DerivationsRepository,
|
||||
): GetExtendedPublicKeyForCurrencyUseCase {
|
||||
|
|
@ -74,13 +74,13 @@ internal object CardDomainModule {
|
|||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
@Singleton
|
||||
fun provideDeleteSavedAccessCodesUseCase(tangemSdkManager: TangemSdkManager): DeleteSavedAccessCodesUseCase {
|
||||
return DefaultDeleteSavedAccessCodesUseCase(tangemSdkManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
@Singleton
|
||||
fun provideResetCardUseCase(tangemSdkManager: TangemSdkManager): ResetCardUseCase {
|
||||
return DefaultResetCardUseCase(tangemSdkManager)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class UserWalletIdPreflightReadFilter(private val expectedUserWalletId: UserWall
|
|||
override fun onCardRead(card: Card, environment: SessionEnvironment) = Unit
|
||||
|
||||
override fun onFullCardRead(card: Card, environment: SessionEnvironment) {
|
||||
val actualUserWalletId = UserWalletIdBuilder.card(card = CardDTO(card)).build()
|
||||
val actualUserWalletId = UserWalletIdBuilder.card(card = CardDTO(card)).build() ?: return
|
||||
|
||||
if (expectedUserWalletId != actualUserWalletId) throw TangemSdkError.WalletNotFound()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,19 +152,29 @@ internal class BiometricUserWalletsListManager(
|
|||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
changeSelectedUserWalletIdIfNeeded(idsToRemove)
|
||||
if (idsToRemove.size == state.value.userWallets.size) {
|
||||
return clear()
|
||||
}
|
||||
|
||||
return sensitiveInformationRepository.delete(idsToRemove)
|
||||
.flatMap { publicInformationRepository.delete(idsToRemove) }
|
||||
.map { keysRepository.delete(idsToRemove) }
|
||||
.map {
|
||||
state.update { prevState ->
|
||||
val newUserWallets = prevState.userWallets.filter { it.walletId !in idsToRemove }
|
||||
val remainingWallets = prevState.userWallets.filter { it.walletId !in idsToRemove }
|
||||
|
||||
val isSelectedWalletDeleted = prevState.selectedUserWalletId in idsToRemove
|
||||
val newSelectedUserWallet = findOrSetSelectedWallet(
|
||||
prevSelectedWalletId = prevState.selectedUserWalletId,
|
||||
userWallets = remainingWallets,
|
||||
ignorePrevSelectedWallet = isSelectedWalletDeleted,
|
||||
)
|
||||
|
||||
prevState.copy(
|
||||
encryptionKeys = prevState.encryptionKeys.filter { it.walletId !in idsToRemove },
|
||||
userWallets = newUserWallets,
|
||||
isLocked = newUserWallets.any { it.isLocked },
|
||||
userWallets = remainingWallets,
|
||||
isLocked = remainingWallets.any { it.isLocked },
|
||||
selectedUserWalletId = newSelectedUserWallet?.walletId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -173,10 +183,10 @@ internal class BiometricUserWalletsListManager(
|
|||
override suspend fun clear(): CompletionResult<Unit> {
|
||||
return sensitiveInformationRepository.clear()
|
||||
.flatMap { publicInformationRepository.clear() }
|
||||
.map { keysRepository.clear() }
|
||||
.map {
|
||||
keysRepository.clear()
|
||||
selectedUserWalletRepository.set(null)
|
||||
lock()
|
||||
state.value = State()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -301,47 +311,24 @@ internal class BiometricUserWalletsListManager(
|
|||
private fun findOrSetSelectedWallet(
|
||||
prevSelectedWalletId: UserWalletId?,
|
||||
userWallets: List<UserWallet>,
|
||||
ignorePrevSelectedWallet: Boolean = false,
|
||||
): UserWallet? {
|
||||
val selectedWalletId = prevSelectedWalletId ?: selectedUserWalletRepository.get()
|
||||
var possibleSelectedUserWallet = findSelectedUserWallet(userWallets, selectedWalletId)
|
||||
var possibleSelectedUserWallet: UserWallet? = null
|
||||
|
||||
if (!ignorePrevSelectedWallet) {
|
||||
val selectedWalletId = prevSelectedWalletId ?: selectedUserWalletRepository.get()
|
||||
possibleSelectedUserWallet = findSelectedUserWallet(userWallets, selectedWalletId)
|
||||
}
|
||||
|
||||
if (possibleSelectedUserWallet == null || possibleSelectedUserWallet.isLocked) {
|
||||
possibleSelectedUserWallet = userWallets.firstOrNull { !it.isLocked } ?: userWallets.firstOrNull()
|
||||
|
||||
if (possibleSelectedUserWallet != null) {
|
||||
selectedUserWalletRepository.set(possibleSelectedUserWallet.walletId)
|
||||
}
|
||||
}
|
||||
|
||||
selectedUserWalletRepository.set(possibleSelectedUserWallet?.walletId)
|
||||
|
||||
return possibleSelectedUserWallet
|
||||
}
|
||||
|
||||
private fun changeSelectedUserWalletIdIfNeeded(walletsIdsToRemove: List<UserWalletId>) {
|
||||
val remainingWallets = state.value.userWallets.filter {
|
||||
it.walletId !in walletsIdsToRemove
|
||||
}
|
||||
val selectedWallet = findSelectedUserWallet()
|
||||
when {
|
||||
remainingWallets.isEmpty() -> {
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
selectedUserWalletId = null,
|
||||
)
|
||||
}
|
||||
selectedUserWalletRepository.set(null)
|
||||
}
|
||||
!remainingWallets.contains(selectedWallet) -> {
|
||||
val newSelectedWallet = remainingWallets.firstOrNull { !it.isLocked }
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
selectedUserWalletId = newSelectedWallet?.walletId,
|
||||
)
|
||||
}
|
||||
selectedUserWalletRepository.set(newSelectedWallet?.walletId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun findSelectedUserWallet(
|
||||
userWallets: List<UserWallet> = state.value.userWallets,
|
||||
selectedUserWalletId: UserWalletId? = state.value.selectedUserWalletId,
|
||||
|
|
|
|||
|
|
@ -214,9 +214,10 @@ internal class ResetCardViewModel @Inject constructor(
|
|||
|
||||
return when (val status = card.backupStatus) {
|
||||
is CardDTO.BackupStatus.Active -> status.cardCount
|
||||
is CardDTO.BackupStatus.CardLinked -> status.cardCount
|
||||
is CardDTO.BackupStatus.NoBackup -> 0
|
||||
null -> 0 // Multi-currency wallet without backup function. Example, 4.12
|
||||
is CardDTO.BackupStatus.CardLinked,
|
||||
is CardDTO.BackupStatus.NoBackup,
|
||||
null,
|
||||
-> 0
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ 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.res.TangemThemePreview
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Suppress("LongMethod")
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ sealed class AnalyticsParam {
|
|||
data object Send : ScreensSources("Send")
|
||||
data object Intro : ScreensSources("Introduction")
|
||||
data object MyWallets : ScreensSources("My Wallets")
|
||||
data object Token : ScreensSources("Token")
|
||||
}
|
||||
|
||||
sealed class TxSentFrom(val value: String) {
|
||||
|
|
|
|||
|
|
@ -31,4 +31,7 @@ enum class ExchangeProviderType {
|
|||
|
||||
@Json(name = "cex")
|
||||
CEX,
|
||||
|
||||
@Json(name = "dex-bridge")
|
||||
DEX_BRIDGE,
|
||||
}
|
||||
|
|
@ -23,34 +23,43 @@ data class ExchangeStatusResponse(
|
|||
enum class ExchangeStatus {
|
||||
|
||||
@Json(name = "new")
|
||||
NEW,
|
||||
New,
|
||||
|
||||
@Json(name = "waiting")
|
||||
WAITING,
|
||||
Waiting,
|
||||
|
||||
@Json(name = "confirming")
|
||||
CONFIRMING,
|
||||
Confirming,
|
||||
|
||||
@Json(name = "exchanging")
|
||||
EXCHANGING,
|
||||
Exchanging,
|
||||
|
||||
@Json(name = "sending")
|
||||
SENDING,
|
||||
Sending,
|
||||
|
||||
@Json(name = "finished")
|
||||
FINISHED,
|
||||
Finished,
|
||||
|
||||
@Json(name = "failed")
|
||||
FAILED,
|
||||
Failed,
|
||||
|
||||
@Json(name = "refunded")
|
||||
REFUNDED,
|
||||
Refunded,
|
||||
|
||||
@Json(name = "verifying")
|
||||
VERIFYING,
|
||||
Verifying,
|
||||
|
||||
@Json(name = "expired")
|
||||
CANCELLED,
|
||||
Cancelled,
|
||||
|
||||
@Json(name = "waiting-tx-hash")
|
||||
WaitingTxHash,
|
||||
|
||||
@Json(name = "tx-failed")
|
||||
TxFailed,
|
||||
|
||||
@Json(name = "unknown")
|
||||
Unknown,
|
||||
}
|
||||
|
||||
data class ExchangeStatusError(
|
||||
|
|
|
|||
|
|
@ -53,14 +53,6 @@ object PreferencesKeys {
|
|||
|
||||
val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") }
|
||||
|
||||
val IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY by lazy {
|
||||
booleanPreferencesKey(name = "isWalletSwapPromoChangellyShown")
|
||||
}
|
||||
|
||||
val IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY by lazy {
|
||||
booleanPreferencesKey(name = "isTokenSwapPromoChangellyShown")
|
||||
}
|
||||
|
||||
val IS_WALLET_TRAVALA_PROMO_SHOWN_KEY by lazy {
|
||||
booleanPreferencesKey(name = "isWalletTravalaPromoShown")
|
||||
}
|
||||
|
|
@ -93,6 +85,14 @@ object PreferencesKeys {
|
|||
|
||||
val IS_WALLET_NAMES_MIGRATION_DONE_KEY by lazy { booleanPreferencesKey(name = "isWalletNamesMigrationDone") }
|
||||
|
||||
val IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY by lazy {
|
||||
booleanPreferencesKey(name = "isWalletSwapPromoOkxShown")
|
||||
}
|
||||
|
||||
val IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY by lazy {
|
||||
booleanPreferencesKey(name = "isTokenSwapPromoOkxShown")
|
||||
}
|
||||
|
||||
fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -225,6 +225,7 @@
|
|||
<string name="express_exchange_status_subtitle">Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий.</string>
|
||||
<string name="express_exchange_status_title">Статус обмена</string>
|
||||
<string name="express_exchange_status_verifying">Требуется верификация</string>
|
||||
<string name="express_exchange_status_waiting_tx_hash">Ожидание хеша транзакции</string>
|
||||
<string name="express_exchange_token_list_subtitle">Список токенов в вашем кошельке</string>
|
||||
<string name="express_fetch_best_rates">Получение наилучших курсов...</string>
|
||||
<string name="express_floating_rate">Плавающая ставка</string>
|
||||
|
|
@ -549,6 +550,8 @@
|
|||
<string name="story_meet_title">Встречайте Tangem</string>
|
||||
<string name="story_web3_description">Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах</string>
|
||||
<string name="story_web3_title">Поддержка Web 3.0</string>
|
||||
<string name="swap_promo_text">Обменивайте больше токенов по лучшим курсам прямо в вашем кошельке.</string>
|
||||
<string name="swap_promo_title">Новый провайдер обмена!</string>
|
||||
<string name="swapping_alert_cex_description">В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя</string>
|
||||
<string name="swapping_alert_dex_description">В сумму включена комиссия провайдера сервиса.</string>
|
||||
<string name="swapping_alert_title">Комиссии</string>
|
||||
|
|
|
|||
|
|
@ -224,6 +224,7 @@
|
|||
<string name="express_exchange_status_subtitle">Provider-sourced data. Estimated amount subject to change due to market conditions.</string>
|
||||
<string name="express_exchange_status_title">Exchange status</string>
|
||||
<string name="express_exchange_status_verifying">Verification required</string>
|
||||
<string name="express_exchange_status_waiting_tx_hash">Awaiting transaction hash</string>
|
||||
<string name="express_exchange_token_list_subtitle">List of all tokens added to your wallet</string>
|
||||
<string name="express_fetch_best_rates">Fetching best rates...</string>
|
||||
<string name="express_floating_rate">Floating rate</string>
|
||||
|
|
@ -542,6 +543,8 @@
|
|||
<string name="story_meet_title">Meet Tangem</string>
|
||||
<string name="story_web3_description">Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services</string>
|
||||
<string name="story_web3_title">Web 3.0 Compatible</string>
|
||||
<string name="swap_promo_text">Exchange more tokens at better rates directly in your wallet.</string>
|
||||
<string name="swap_promo_title">New Swap Provider Available!</string>
|
||||
<string name="swapping_alert_cex_description">The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address.</string>
|
||||
<string name="swapping_alert_dex_description">The amount includes the service provider\'s fee.</string>
|
||||
<string name="swapping_alert_title">Fees</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
package com.tangem.core.ui.components.notifications
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.core.ui.R
|
||||
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.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.LocalIsInDarkTheme
|
||||
import com.tangem.core.ui.res.TangemColorPalette.Dark6
|
||||
import com.tangem.core.ui.res.TangemColorPalette.Light4
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
private val OxkPromoColor = Color(0xFFBCFF2F)
|
||||
|
||||
@Composable
|
||||
fun OkxPromoNotification(config: NotificationConfig, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(Dark6),
|
||||
) {
|
||||
Content(config = config)
|
||||
Button(config = config)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(config: NotificationConfig) {
|
||||
Row {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = ImageVector.vectorResource(id = R.drawable.img_okx_dex_logo)),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.constant,
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing12)
|
||||
.align(Alignment.CenterVertically),
|
||||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Text(
|
||||
text = config.title.resolveReference(),
|
||||
style = TangemTheme.typography.button,
|
||||
color = OxkPromoColor,
|
||||
)
|
||||
Text(
|
||||
text = config.subtitle.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.constantWhite,
|
||||
)
|
||||
}
|
||||
config.onCloseClick?.let {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(
|
||||
image = ImageVector.vectorResource(R.drawable.ic_close_24),
|
||||
),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.constant,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing8,
|
||||
end = TangemTheme.dimens.spacing8,
|
||||
)
|
||||
.size(TangemTheme.dimens.size20)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(radius = TangemTheme.dimens.radius10),
|
||||
onClick = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Button(config: NotificationConfig) {
|
||||
val button = config.buttonsState as? NotificationConfig.ButtonsState.SecondaryButtonConfig
|
||||
|
||||
button?.let {
|
||||
val isDarkMode = LocalIsInDarkTheme.current
|
||||
TangemButton(
|
||||
text = button.text.resolveReference(),
|
||||
icon = TangemButtonIconPosition.Start(button.iconResId ?: R.drawable.ic_exchange_vertical_24),
|
||||
onClick = button.onClick,
|
||||
colors = TangemButtonColors(
|
||||
backgroundColor = if (isDarkMode) Light4 else TangemTheme.colors.button.secondary,
|
||||
contentColor = Dark6,
|
||||
disabledBackgroundColor = TangemTheme.colors.button.disabled,
|
||||
disabledContentColor = TangemTheme.colors.text.disabled,
|
||||
),
|
||||
enabled = true,
|
||||
showProgress = false,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(widthDp = 360)
|
||||
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun OkxPromoNotification_Preview(
|
||||
@PreviewParameter(OkxPromoNotificationPreviewProvider::class) data: NotificationConfig,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
OkxPromoNotification(data)
|
||||
}
|
||||
}
|
||||
|
||||
private class OkxPromoNotificationPreviewProvider : PreviewParameterProvider<NotificationConfig> {
|
||||
override val values: Sequence<NotificationConfig>
|
||||
get() = sequenceOf(
|
||||
NotificationConfig(
|
||||
title = resourceReference(R.string.swap_promo_title),
|
||||
subtitle = resourceReference(R.string.swap_promo_text),
|
||||
iconResId = R.drawable.img_okx_dex_logo,
|
||||
onCloseClick = {},
|
||||
),
|
||||
NotificationConfig(
|
||||
title = resourceReference(R.string.swap_promo_title),
|
||||
subtitle = resourceReference(R.string.swap_promo_text),
|
||||
iconResId = R.drawable.img_okx_dex_logo,
|
||||
onCloseClick = {},
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = resourceReference(R.string.token_swap_promotion_button),
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -101,6 +101,7 @@ class TangemColors internal constructor(
|
|||
private set
|
||||
var attention by mutableStateOf(attention)
|
||||
private set
|
||||
var constant by mutableStateOf(constant)
|
||||
|
||||
fun update(other: Icon) {
|
||||
primary1 = other.primary1
|
||||
|
|
@ -111,6 +112,7 @@ class TangemColors internal constructor(
|
|||
accent = other.accent
|
||||
warning = other.warning
|
||||
attention = other.attention
|
||||
constant = other.constant
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.core.ui.screen
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
|
@ -22,6 +23,18 @@ abstract class ComposeFragment : Fragment(), ComposeScreen {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
|
||||
/*
|
||||
* We need to manually dispatch configuration changes to the Compose view.
|
||||
*
|
||||
|
||||
* `android:configChanges="uiMode"` is set in the manifest.
|
||||
* */
|
||||
view?.dispatchConfigurationChanged(newConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates transitions for the fragment. Override this method to customize
|
||||
* enter and exit transitions for the fragment.
|
||||
|
|
|
|||
29
core/ui/src/main/res/drawable/img_okx_dex_logo.xml
Normal file
29
core/ui/src/main/res/drawable/img_okx_dex_logo.xml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="1232.1" android:viewportWidth="2516" android:width="49.00901dp">
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="m740.2,0L23.5,0c-4.4,0 -8.7,1.6 -11.8,4.6 -3.1,2.9 -4.9,6.9 -4.9,11v670.6c0,4.1 1.8,8.1 4.9,11 3.1,2.9 7.4,4.6 11.8,4.6h716.7c4.4,0 8.7,-1.6 11.8,-4.6 3.1,-2.9 4.9,-6.9 4.9,-11L756.9,15.6c0,-4.1 -1.8,-8.1 -4.9,-11 -3.1,-2.9 -7.4,-4.6 -11.8,-4.6ZM506.9,452.3c0,4.1 -1.8,8.1 -4.9,11 -3.1,2.9 -7.4,4.6 -11.8,4.6h-216.7c-4.4,0 -8.7,-1.6 -11.8,-4.6 -3.1,-2.9 -4.9,-6.9 -4.9,-11v-202.8c0,-4.1 1.8,-8.1 4.9,-11 3.1,-2.9 7.4,-4.6 11.8,-4.6h216.7c4.4,0 8.7,1.6 11.8,4.6 3.1,2.9 4.9,6.9 4.9,11v202.8Z"/>
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="m2240.6,234h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6v-202.8c0,-8.6 -7.5,-15.6 -16.7,-15.6h0Z"/>
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="m1990.7,0h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6V15.6c0,-8.6 -7.5,-15.6 -16.7,-15.6Z"/>
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="m2490.8,0h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6V15.6c0,-8.6 -7.5,-15.6 -16.7,-15.6Z"/>
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="m1990.7,467.9h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6v-202.8c0,-8.6 -7.5,-15.6 -16.7,-15.6Z"/>
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="m2490.8,467.9h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6v-202.8c0,-8.6 -7.5,-15.6 -16.7,-15.6Z"/>
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="m1615.3,0h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6V15.6c0,-8.6 -7.5,-15.6 -16.7,-15.6Z"/>
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="m1615.3,467.9h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6v-202.8c0,-8.6 -7.5,-15.6 -16.7,-15.6Z"/>
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="m1381.9,249.4c0,-4.1 -1.8,-8.1 -4.9,-11 -3.1,-2.9 -7.4,-4.6 -11.8,-4.6h-233.4V15.6c0,-4.1 -1.8,-8.1 -4.9,-11 -3.1,-2.9 -7.4,-4.6 -11.8,-4.6h-216.7c-4.4,0 -8.7,1.6 -11.8,4.6 -3.1,2.9 -4.9,6.9 -4.9,11v670.3c0,4.1 1.8,8.1 4.9,11 3.1,2.9 7.4,4.6 11.8,4.6h216.7c4.4,0 8.7,-1.6 11.8,-4.6 3.1,-2.9 4.9,-6.9 4.9,-11v-218.2h233.4c4.4,0 8.7,-1.6 11.8,-4.6 3.1,-2.9 4.9,-6.9 4.9,-11v-202.8Z"/>
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="m1675.2,885c33.8,0 64.5,6.8 92,20.4 27.5,13.6 49.2,33.4 65.2,59.5 16,26.1 24,57.2 24,93.2s-8,67.2 -24,93.4c-16,26.3 -37.6,46.2 -65,60 -27.4,13.8 -58.1,20.6 -92.2,20.6h-125.2v-347.1h125.2ZM1666.2,1163.8c34.5,0 60.5,-9.2 78.3,-27.5 17.7,-18.3 26.6,-44.4 26.6,-78.3s-8.9,-59.4 -26.8,-77.5c-17.9,-18.2 -43.9,-27.3 -78,-27.3h-35.1v210.6h35.1Z"/>
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="m2125.7,1092.2h-135.6v69.7h157v70.2h-238.1v-347.1h232.4v69.7h-151.3v68.3h135.6v69.2Z"/>
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="m2509.8,885l-121.4,168.4 127.6,178.8h-100.1l-77.3,-112.9 -78.7,112.9h-92.9l127.6,-176.9 -121.4,-170.3h99.1l71.6,104.3 72.6,-104.3h93.4Z"/>
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="M0,1033.9h1381.6v49h-1381.6z"/>
|
||||
|
||||
</vector>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.data.card.di
|
||||
|
||||
import com.tangem.data.card.sdk.CardSdkLifecycleObserver
|
||||
import com.tangem.data.card.sdk.CardSdkOwner
|
||||
import com.tangem.data.card.sdk.CardSdkProvider
|
||||
import com.tangem.data.card.sdk.DefaultCardSdkProvider
|
||||
import dagger.Binds
|
||||
|
|
@ -19,5 +19,5 @@ internal interface CardSdkModule {
|
|||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun providerCardSdkLifecycleObserver(defaultCardSdkProvider: DefaultCardSdkProvider): CardSdkLifecycleObserver
|
||||
fun providerCardSdkLifecycleObserver(defaultCardSdkProvider: DefaultCardSdkProvider): CardSdkOwner
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
package com.tangem.data.card.sdk
|
||||
|
||||
import android.content.Context
|
||||
|
||||
/**
|
||||
* Lifecycle observer for creating Card SDK instance
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface CardSdkLifecycleObserver {
|
||||
|
||||
/** Callback of creating activity [context] */
|
||||
fun onCreate(context: Context)
|
||||
|
||||
/** Callback of destroying activity [context] */
|
||||
fun onDestroy(context: Context)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.data.card.sdk
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
|
||||
/**
|
||||
* Lifecycle observer for creating Card SDK instance
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface CardSdkOwner {
|
||||
|
||||
fun register(activity: FragmentActivity)
|
||||
}
|
||||
|
|
@ -1,12 +1,21 @@
|
|||
package com.tangem.data.card.sdk
|
||||
|
||||
import android.content.Context
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.tangem.Log
|
||||
import com.tangem.TangemSdk
|
||||
import com.tangem.common.CardFilter
|
||||
import com.tangem.common.authentication.AuthenticationManager
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.common.core.Config
|
||||
import com.tangem.sdk.extensions.initWithBiometrics
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.crypto.bip39.Wordlist
|
||||
import com.tangem.sdk.DefaultSessionViewDelegate
|
||||
import com.tangem.sdk.extensions.*
|
||||
import com.tangem.sdk.nfc.NfcManager
|
||||
import com.tangem.sdk.storage.create
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -16,31 +25,94 @@ import javax.inject.Singleton
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Singleton
|
||||
internal class DefaultCardSdkProvider @Inject constructor() : CardSdkProvider, CardSdkLifecycleObserver {
|
||||
internal class DefaultCardSdkProvider @Inject constructor() : CardSdkProvider, CardSdkOwner {
|
||||
|
||||
override val sdk: TangemSdk
|
||||
get() = requireNotNull(value = _sdk) { "Impossible to get the TangemSdk when activity is destroyed" }
|
||||
get() = requireNotNull(value = holder?.sdk) {
|
||||
"Impossible to get the TangemSdk when activity is destroyed"
|
||||
}
|
||||
|
||||
private var _sdk: TangemSdk? = null
|
||||
private val observer: LifecycleObserver = Observer()
|
||||
|
||||
override fun onCreate(context: Context) {
|
||||
_sdk = TangemSdk.initWithBiometrics(activity = context as FragmentActivity, config = config)
|
||||
private var holder: Holder? = null
|
||||
|
||||
override fun register(activity: FragmentActivity) {
|
||||
Log.info { "Tangem SDK owner registered" }
|
||||
|
||||
if (holder != null) {
|
||||
unsubscribeAndCleanup()
|
||||
}
|
||||
|
||||
initialize(activity)
|
||||
|
||||
activity.lifecycle.addObserver(observer)
|
||||
}
|
||||
|
||||
override fun onDestroy(context: Context) {
|
||||
// Commented out to prevent crash on getting sdk when it's null.
|
||||
// FIXME: We still should find the real cause and fix it properly.
|
||||
// idea: pass everywhere DefaultCardSdkProvider instead sdk to reach lazy access to sdk property
|
||||
// _sdk = null
|
||||
private fun initialize(activity: FragmentActivity) {
|
||||
val secureStorage = SecureStorage.create(activity)
|
||||
val nfcManager = TangemSdk.initNfcManager(activity)
|
||||
val authenticationManager = TangemSdk.initAuthenticationManager(activity)
|
||||
val keystoreManager = TangemSdk.initKeystoreManager(authenticationManager, secureStorage)
|
||||
|
||||
val viewDelegate = DefaultSessionViewDelegate(nfcManager, activity)
|
||||
viewDelegate.sdkConfig = config
|
||||
|
||||
val sdk = TangemSdk(
|
||||
reader = nfcManager.reader,
|
||||
viewDelegate = viewDelegate,
|
||||
secureStorage = secureStorage,
|
||||
authenticationManager = authenticationManager,
|
||||
keystoreManager = keystoreManager,
|
||||
wordlist = Wordlist.getWordlist(activity),
|
||||
config = config,
|
||||
)
|
||||
|
||||
holder = Holder(
|
||||
activity = activity,
|
||||
nfcManager = nfcManager,
|
||||
authenticationManager = authenticationManager,
|
||||
sdk = sdk,
|
||||
)
|
||||
|
||||
Log.info { "Tangem SDK initialized" }
|
||||
}
|
||||
|
||||
private fun unsubscribeAndCleanup() {
|
||||
with(receiver = holder ?: return) {
|
||||
nfcManager.unsubscribe(activity)
|
||||
authenticationManager.unsubscribe(activity)
|
||||
|
||||
activity.lifecycle.removeObserver(observer)
|
||||
}
|
||||
|
||||
holder = null
|
||||
|
||||
Log.info { "Tangem SDK unsubscribed and cleaned up" }
|
||||
}
|
||||
|
||||
inner class Observer : DefaultLifecycleObserver {
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
Log.info { "Tangem SDK owner destroyed" }
|
||||
|
||||
unsubscribeAndCleanup()
|
||||
}
|
||||
}
|
||||
|
||||
data class Holder(
|
||||
val activity: FragmentActivity,
|
||||
val sdk: TangemSdk,
|
||||
val nfcManager: NfcManager,
|
||||
val authenticationManager: AuthenticationManager,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
|
||||
val config = Config(
|
||||
linkedTerminal = true,
|
||||
allowUntrustedCards = true,
|
||||
filter = CardFilter(
|
||||
allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(),
|
||||
allowedCardTypes = FirmwareVersion.FirmwareType.entries.toList(),
|
||||
maxFirmwareVersion = FirmwareVersion(major = 6, minor = 33),
|
||||
batchIdFilter = CardFilter.Companion.ItemFilter.Deny(
|
||||
items = setOf("0027", "0030", "0031", "0035"),
|
||||
|
|
|
|||
|
|
@ -32,8 +32,18 @@ internal class DefaultPromoRepository(
|
|||
}.getOrNull()
|
||||
}
|
||||
|
||||
override suspend fun getOkxPromoBanner(): PromoBanner? {
|
||||
return runCatching(dispatchers.io) {
|
||||
promoResponseConverter.convert(
|
||||
tangemApi.getPromotionInfo(OKX)
|
||||
.getOrThrow(),
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val CHANGELLY_NAME = "changelly"
|
||||
private const val TRAVALA = "travala"
|
||||
private const val OKX = "okx"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.data.settings
|
||||
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_TRAVALA_PROMO_SHOWN_KEY
|
||||
import com.tangem.datasource.local.preferences.utils.get
|
||||
import com.tangem.datasource.local.preferences.utils.store
|
||||
|
|
@ -16,23 +16,23 @@ class DefaultPromoSettingsRepository(
|
|||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : PromoSettingsRepository {
|
||||
override fun isReadyToShowWalletSwapPromo(): Flow<Boolean> {
|
||||
return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY, true)
|
||||
return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY, true)
|
||||
}
|
||||
|
||||
override fun isReadyToShowTokenSwapPromo(): Flow<Boolean> {
|
||||
return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY, true)
|
||||
return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY, true)
|
||||
}
|
||||
|
||||
override suspend fun setNeverToShowWalletSwapPromo() {
|
||||
appPreferencesStore.store(
|
||||
key = IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY,
|
||||
key = IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY,
|
||||
value = false,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun setNeverToShowTokenSwapPromo() {
|
||||
appPreferencesStore.store(
|
||||
key = IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY,
|
||||
key = IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY,
|
||||
value = false,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,8 +87,11 @@ internal object TokensDataModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDefaultMarketCoinsRepository(assetsStore: AssetsStore): MarketCryptoCurrencyRepository {
|
||||
return DefaultMarketCryptoCurrencyRepository(assetsStore)
|
||||
fun provideDefaultMarketCoinsRepository(
|
||||
assetsStore: AssetsStore,
|
||||
coroutineDispatcherProvider: CoroutineDispatcherProvider,
|
||||
): MarketCryptoCurrencyRepository {
|
||||
return DefaultMarketCryptoCurrencyRepository(assetsStore, coroutineDispatcherProvider)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -302,26 +302,28 @@ internal class DefaultCurrenciesRepository(
|
|||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): CryptoCurrency.Coin {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true)
|
||||
return withContext(dispatchers.io) {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true)
|
||||
|
||||
fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false)
|
||||
fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false)
|
||||
|
||||
val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) {
|
||||
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
|
||||
val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) {
|
||||
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
|
||||
}
|
||||
val blockchain = Blockchain.fromId(networkId.value)
|
||||
val blockchainNetworkId = blockchain.toNetworkId()
|
||||
val coinId = blockchain.toCoinId()
|
||||
|
||||
val storedCoin = storedTokens.tokens
|
||||
.find {
|
||||
it.networkId == blockchainNetworkId && it.id == coinId && it.derivationPath == derivationPath.value
|
||||
} ?: error("Coin in this network $networkId not found")
|
||||
|
||||
val coin = responseCurrenciesFactory.createCurrency(storedCoin, userWallet.scanResponse)
|
||||
|
||||
coin as? CryptoCurrency.Coin ?: error("Unable to create currency")
|
||||
}
|
||||
val blockchain = Blockchain.fromId(networkId.value)
|
||||
val blockchainNetworkId = blockchain.toNetworkId()
|
||||
val coinId = blockchain.toCoinId()
|
||||
|
||||
val storedCoin = storedTokens.tokens
|
||||
.find {
|
||||
it.networkId == blockchainNetworkId && it.id == coinId && it.derivationPath == derivationPath.value
|
||||
} ?: error("Coin in this network $networkId not found")
|
||||
|
||||
val coin = responseCurrenciesFactory.createCurrency(storedCoin, userWallet.scanResponse)
|
||||
|
||||
return coin as? CryptoCurrency.Coin ?: error("Unable to create currency")
|
||||
}
|
||||
|
||||
override fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean> {
|
||||
|
|
@ -382,29 +384,31 @@ internal class DefaultCurrenciesRepository(
|
|||
}
|
||||
|
||||
override suspend fun getFeePaidCurrency(userWalletId: UserWalletId, currency: CryptoCurrency): FeePaidCurrency {
|
||||
val blockchain = Blockchain.fromId(currency.network.id.value)
|
||||
return when (val feePaidCurrency = blockchain.feePaidCurrency()) {
|
||||
FeePaidSdkCurrency.Coin -> FeePaidCurrency.Coin
|
||||
FeePaidSdkCurrency.SameCurrency -> FeePaidCurrency.SameCurrency
|
||||
is FeePaidSdkCurrency.Token -> {
|
||||
val balance = walletManagersFacade.tokenBalance(
|
||||
userWalletId = userWalletId,
|
||||
network = currency.network,
|
||||
name = feePaidCurrency.token.name,
|
||||
symbol = feePaidCurrency.token.symbol,
|
||||
contractAddress = feePaidCurrency.token.contractAddress,
|
||||
decimals = feePaidCurrency.token.decimals,
|
||||
id = feePaidCurrency.token.id,
|
||||
)
|
||||
FeePaidCurrency.Token(
|
||||
tokenId = getTokenId(network = currency.network, sdkToken = feePaidCurrency.token),
|
||||
name = feePaidCurrency.token.name,
|
||||
symbol = feePaidCurrency.token.symbol,
|
||||
contractAddress = feePaidCurrency.token.contractAddress,
|
||||
balance = balance,
|
||||
)
|
||||
return withContext(dispatchers.io) {
|
||||
val blockchain = Blockchain.fromId(currency.network.id.value)
|
||||
when (val feePaidCurrency = blockchain.feePaidCurrency()) {
|
||||
FeePaidSdkCurrency.Coin -> FeePaidCurrency.Coin
|
||||
FeePaidSdkCurrency.SameCurrency -> FeePaidCurrency.SameCurrency
|
||||
is FeePaidSdkCurrency.Token -> {
|
||||
val balance = walletManagersFacade.tokenBalance(
|
||||
userWalletId = userWalletId,
|
||||
network = currency.network,
|
||||
name = feePaidCurrency.token.name,
|
||||
symbol = feePaidCurrency.token.symbol,
|
||||
contractAddress = feePaidCurrency.token.contractAddress,
|
||||
decimals = feePaidCurrency.token.decimals,
|
||||
id = feePaidCurrency.token.id,
|
||||
)
|
||||
FeePaidCurrency.Token(
|
||||
tokenId = getTokenId(network = currency.network, sdkToken = feePaidCurrency.token),
|
||||
name = feePaidCurrency.token.name,
|
||||
symbol = feePaidCurrency.token.symbol,
|
||||
contractAddress = feePaidCurrency.token.contractAddress,
|
||||
balance = balance,
|
||||
)
|
||||
}
|
||||
is FeePaidSdkCurrency.FeeResource -> FeePaidCurrency.FeeResource(currency = feePaidCurrency.currency)
|
||||
}
|
||||
is FeePaidSdkCurrency.FeeResource -> FeePaidCurrency.FeeResource(currency = feePaidCurrency.currency)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@ import com.tangem.datasource.local.token.AssetsStore
|
|||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class DefaultMarketCryptoCurrencyRepository(
|
||||
private val assetsStore: AssetsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MarketCryptoCurrencyRepository {
|
||||
|
||||
override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
|
||||
|
|
@ -15,11 +18,16 @@ class DefaultMarketCryptoCurrencyRepository(
|
|||
}
|
||||
|
||||
private suspend fun getExchangeableFlag(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
|
||||
val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE
|
||||
return withContext(dispatchers.io) {
|
||||
val contractAddress =
|
||||
(cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE
|
||||
|
||||
return assetsStore.getSyncOrNull(userWalletId)?.find {
|
||||
it.network == cryptoCurrency.network.backendId &&
|
||||
it.contractAddress.equals(contractAddress, ignoreCase = true)
|
||||
}?.exchangeAvailable ?: false
|
||||
val asset = assetsStore.getSyncOrNull(userWalletId)?.find {
|
||||
it.network == cryptoCurrency.network.backendId &&
|
||||
it.contractAddress.equals(contractAddress, ignoreCase = true)
|
||||
}
|
||||
|
||||
asset?.exchangeAvailable ?: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -35,16 +35,19 @@ class DefaultTxHistoryRepository(
|
|||
private val sdkPageConverter by lazy { SdkPageConverter() }
|
||||
|
||||
override suspend fun getTxHistoryItemsCount(userWalletId: UserWalletId, currency: CryptoCurrency): Int {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val state = walletManagersFacade.getTxHistoryState(
|
||||
userWalletId = userWallet.walletId,
|
||||
currency = currency,
|
||||
)
|
||||
return when (state) {
|
||||
is TxHistoryState.Failed.FetchError -> throw TxHistoryStateError.DataError(state.exception)
|
||||
is TxHistoryState.NotImplemented -> throw TxHistoryStateError.TxHistoryNotImplemented
|
||||
is TxHistoryState.Success.Empty -> throw TxHistoryStateError.EmptyTxHistories
|
||||
is TxHistoryState.Success.HasTransactions -> state.txCount
|
||||
return withContext(dispatchers.io) {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val state = walletManagersFacade.getTxHistoryState(
|
||||
userWalletId = userWallet.walletId,
|
||||
currency = currency,
|
||||
)
|
||||
|
||||
when (state) {
|
||||
is TxHistoryState.Failed.FetchError -> throw TxHistoryStateError.DataError(state.exception)
|
||||
is TxHistoryState.NotImplemented -> throw TxHistoryStateError.TxHistoryNotImplemented
|
||||
is TxHistoryState.Success.Empty -> throw TxHistoryStateError.EmptyTxHistories
|
||||
is TxHistoryState.Success.HasTransactions -> state.txCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import com.tangem.domain.core.utils.lceContent
|
|||
import com.tangem.domain.core.utils.lceError
|
||||
import com.tangem.domain.core.utils.lceLoading
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.channels.ProducerScope
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.experimental.ExperimentalTypeInference
|
||||
|
||||
/**
|
||||
|
|
@ -25,35 +25,43 @@ typealias LceFlow<E, C> = Flow<Lce<E, C>>
|
|||
* It provides methods to handle [Lce] instances and raise errors within a [Flow].
|
||||
*
|
||||
* @property raise The [LceRaise] instance that this class wraps.
|
||||
* @property scope The [ProducerScope] that this class operates within.
|
||||
* @property producerScope The [ProducerScope] instance that this class wraps.
|
||||
* @property ifLoading The function to call if a loading state is raised.
|
||||
*/
|
||||
class LceFlowScope<E : Any, C : Any> @PublishedApi internal constructor(
|
||||
private val raise: LceRaise<E>,
|
||||
private val scope: ProducerScope<Lce<E, C>>,
|
||||
private val producerScope: ProducerScope<Lce<E, C>>,
|
||||
private val ifLoading: suspend LceFlowScope<E, C>.(C?) -> Unit,
|
||||
) : Raise<E>, CoroutineScope by scope {
|
||||
) : Raise<E>, CoroutineScope by producerScope {
|
||||
|
||||
/**
|
||||
* Raises an [Lce] instance within the [ProducerScope].
|
||||
* It closes the [ProducerScope] after raise.
|
||||
* Sends a error of type [E] within the [ProducerScope] and then closes it for send.
|
||||
* All subsequent sends will be ignored.
|
||||
*
|
||||
* @param r The [Lce] instance to raise.
|
||||
* This method blocks the coroutine until a error is handled by the receiver.
|
||||
*
|
||||
* If the [ProducerScope] is already closed for send (e.g. after rising another error), it just raises [r]
|
||||
* without closing.
|
||||
*
|
||||
* @param r Error to raise.
|
||||
*/
|
||||
override fun raise(r: E): Nothing {
|
||||
scope.launch(NonCancellable) {
|
||||
scope.send(r.lceError())
|
||||
scope.close()
|
||||
}
|
||||
producerScope.trySendBlocking(r.lceError())
|
||||
producerScope.close()
|
||||
|
||||
raise.raise(r.lceError())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a content value within the [ProducerScope].
|
||||
* Sends a [content] value within the [ProducerScope].
|
||||
*
|
||||
* If the content is still loading, it calls [ifLoading] lambda to retrieve a state.
|
||||
* Otherwise, it wraps the content in a [Lce.Content] state.
|
||||
*
|
||||
* This method suspends until the [Lce] instance is handled by the receiver.
|
||||
*
|
||||
* If the [ProducerScope] is closed for send (e.g. after rising a error), it does nothing.
|
||||
*
|
||||
* @param content The content value to send.
|
||||
* @param isStillLoading A flag indicating whether the content is still loading.
|
||||
*/
|
||||
|
|
@ -65,11 +73,23 @@ class LceFlowScope<E : Any, C : Any> @PublishedApi internal constructor(
|
|||
content.lceContent()
|
||||
}
|
||||
|
||||
scope.send(value)
|
||||
send(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an [Lce] instance within the [ProducerScope].
|
||||
*
|
||||
* This method suspends until the [Lce] instance is handled by the receiver.
|
||||
*
|
||||
* If the [ProducerScope] is closed for send (e.g. after rising a error), it does nothing.
|
||||
*
|
||||
* @param value The [Lce] instance to send.
|
||||
*/
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
suspend fun send(value: Lce<E, C>) {
|
||||
scope.send(value)
|
||||
if (producerScope.isClosedForSend) return
|
||||
|
||||
producerScope.send(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,7 +114,7 @@ fun <E : Any, C : Any> lceFlow(
|
|||
lce {
|
||||
val scope = LceFlowScope(
|
||||
raise = this@lce,
|
||||
scope = this@channelFlow,
|
||||
producerScope = this@channelFlow,
|
||||
ifLoading = ifLoading,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -87,11 +87,13 @@ class DefaultWalletManagersFacade(
|
|||
Blockchain.fromId(it.id.value) to it.derivationPath.value
|
||||
}
|
||||
|
||||
walletManagersStore.remove(userWalletId) { walletManager ->
|
||||
val wallet = walletManager.wallet
|
||||
val blockchainToDerivationPath = wallet.blockchain to wallet.publicKey.derivationPath?.rawPath
|
||||
withContext(dispatchers.io) {
|
||||
walletManagersStore.remove(userWalletId) { walletManager ->
|
||||
val wallet = walletManager.wallet
|
||||
val blockchainToDerivationPath = wallet.blockchain to wallet.publicKey.derivationPath?.rawPath
|
||||
|
||||
blockchainToDerivationPath in blockchainsToDerivationPaths
|
||||
blockchainToDerivationPath in blockchainsToDerivationPaths
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,18 +112,20 @@ class DefaultWalletManagersFacade(
|
|||
network: Network,
|
||||
networkTokens: List<CryptoCurrency.Token>,
|
||||
) {
|
||||
val walletManager = walletManagersStore.getSyncOrNull(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = Blockchain.fromId(network.id.value),
|
||||
derivationPath = network.derivationPath.value,
|
||||
) ?: return
|
||||
val tokensToRemove = sdkTokenConverter.convertList(networkTokens)
|
||||
withContext(dispatchers.io) {
|
||||
val walletManager = walletManagersStore.getSyncOrNull(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = Blockchain.fromId(network.id.value),
|
||||
derivationPath = network.derivationPath.value,
|
||||
) ?: return@withContext
|
||||
val tokensToRemove = sdkTokenConverter.convertList(networkTokens)
|
||||
|
||||
tokensToRemove.forEach { token ->
|
||||
walletManager.removeToken(token)
|
||||
tokensToRemove.forEach { token ->
|
||||
walletManager.removeToken(token)
|
||||
}
|
||||
|
||||
walletManagersStore.store(userWalletId, walletManager)
|
||||
}
|
||||
|
||||
walletManagersStore.store(userWalletId, walletManager)
|
||||
}
|
||||
|
||||
override suspend fun updatePendingTransactions(
|
||||
|
|
@ -576,12 +580,16 @@ class DefaultWalletManagersFacade(
|
|||
userWalletId: UserWalletId,
|
||||
currency: CryptoCurrency,
|
||||
): AssetRequirementsCondition? {
|
||||
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
|
||||
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
|
||||
if (walletManager !is AssetRequirementsManager || !walletManager.hasRequirements(currencyType)) return null
|
||||
return withContext(dispatchers.io) {
|
||||
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
|
||||
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
|
||||
if (walletManager !is AssetRequirementsManager || !walletManager.hasRequirements(currencyType)) {
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
val condition = walletManager.requirementsCondition(currencyType) ?: return null
|
||||
return requirementsConditionConverter.convert(condition)
|
||||
val condition = walletManager.requirementsCondition(currencyType) ?: return@withContext null
|
||||
requirementsConditionConverter.convert(condition)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun associateAsset(
|
||||
|
|
@ -589,15 +597,18 @@ class DefaultWalletManagersFacade(
|
|||
currency: CryptoCurrency,
|
||||
signer: CommonSigner,
|
||||
): SimpleResult {
|
||||
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
|
||||
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
|
||||
return withContext(dispatchers.io) {
|
||||
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
|
||||
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
|
||||
|
||||
if (walletManager !is AssetRequirementsManager) {
|
||||
return SimpleResult.Failure(
|
||||
BlockchainSdkError.CustomError("WalletManager is not implemented AssetRequirementsManager"),
|
||||
)
|
||||
if (walletManager !is AssetRequirementsManager) {
|
||||
return@withContext SimpleResult.Failure(
|
||||
BlockchainSdkError.CustomError("WalletManager is not implemented AssetRequirementsManager"),
|
||||
)
|
||||
}
|
||||
|
||||
walletManager.fulfillRequirements(currencyType, signer)
|
||||
}
|
||||
return walletManager.fulfillRequirements(currencyType, signer)
|
||||
}
|
||||
|
||||
override suspend fun checkUtxoConsolidationAvailability(userWalletId: UserWalletId, network: Network): Boolean {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,43 @@
|
|||
package com.tangem.domain.tokens.models.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
||||
sealed class TokenSwapPromoAnalyticsEvent(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent("Swap Promo", event, params, null) {
|
||||
|
||||
object Close : TokenSwapPromoAnalyticsEvent(event = "Button - Close")
|
||||
|
||||
class Exchange(
|
||||
token: String,
|
||||
) : AnalyticsEvent(category = "Promotion", event = event, params = params) {
|
||||
class NoticePromotionBanner(
|
||||
source: AnalyticsParam.ScreensSources,
|
||||
programName: ProgramName,
|
||||
) : TokenSwapPromoAnalyticsEvent(
|
||||
event = "Button - Exchange Now",
|
||||
params = mapOf("Token" to token),
|
||||
event = "Notice - Promotion Banner",
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to source.value,
|
||||
"Program Name" to programName.name,
|
||||
),
|
||||
)
|
||||
|
||||
class PromotionBannerClicked(
|
||||
source: AnalyticsParam.ScreensSources,
|
||||
programName: ProgramName,
|
||||
action: BannerAction,
|
||||
) : TokenSwapPromoAnalyticsEvent(
|
||||
event = "Promo Banner Clicked",
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to source.value,
|
||||
"Program Name" to programName.name,
|
||||
"Action" to action.action,
|
||||
),
|
||||
) {
|
||||
sealed class BannerAction(val action: String) {
|
||||
data object Clicked : BannerAction(action = "Clicked")
|
||||
data object Closed : BannerAction(action = "Closed")
|
||||
}
|
||||
}
|
||||
|
||||
enum class ProgramName {
|
||||
Travala,
|
||||
OKX,
|
||||
}
|
||||
}
|
||||
|
|
@ -84,7 +84,7 @@ class GetCurrencyWarningsUseCase(
|
|||
): Flow<CryptoCurrencyWarning?> {
|
||||
val currency = currencyStatus.currency
|
||||
val cryptoStatuses = operations.getCurrenciesStatusesSync()
|
||||
val promoBanner = promoRepository.getChangellyPromoBanner()
|
||||
val promoBanner = promoRepository.getOkxPromoBanner()
|
||||
return combine(
|
||||
showSwapPromoTokenUseCase().conflate(),
|
||||
flowOf(marketCryptoCurrencyRepository.isExchangeable(userWalletId, currency)).conflate(),
|
||||
|
|
|
|||
|
|
@ -7,4 +7,6 @@ interface PromoRepository {
|
|||
suspend fun getChangellyPromoBanner(): PromoBanner?
|
||||
|
||||
suspend fun getTravalaPromoBanner(): PromoBanner?
|
||||
|
||||
suspend fun getOkxPromoBanner(): PromoBanner?
|
||||
}
|
||||
|
|
@ -68,6 +68,7 @@ class SwapPairInfoConverter : Converter<SwapPairsWithProviders, PairsWithProvide
|
|||
return when (type) {
|
||||
ExchangeProviderType.DEX -> ExchangeProviderTypeDomain.DEX
|
||||
ExchangeProviderType.CEX -> ExchangeProviderTypeDomain.CEX
|
||||
ExchangeProviderType.DEX_BRIDGE -> ExchangeProviderTypeDomain.DEX_BRIDGE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ data class ExchangeStatusModel(
|
|||
enum class ExchangeStatus {
|
||||
New,
|
||||
Waiting,
|
||||
WaitingTxHash,
|
||||
Confirming,
|
||||
Verifying,
|
||||
Exchanging,
|
||||
|
|
@ -19,4 +20,5 @@ enum class ExchangeStatus {
|
|||
Finished,
|
||||
Refunded,
|
||||
Cancelled,
|
||||
Unknown,
|
||||
}
|
||||
|
|
@ -36,9 +36,10 @@ data class SwapProvider(
|
|||
val privacyPolicy: String?,
|
||||
)
|
||||
|
||||
enum class ExchangeProviderType {
|
||||
DEX,
|
||||
CEX,
|
||||
enum class ExchangeProviderType(val providerName: String) {
|
||||
DEX("DEX"),
|
||||
CEX("CEX"),
|
||||
DEX_BRIDGE("DEX/Bridge"),
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import com.tangem.core.ui.utils.parseBigDecimal
|
|||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.extenstions.unwrap
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
|
|
@ -65,6 +65,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
private val createTransactionUseCase: CreateTransactionUseCase,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val quotesRepository: QuotesRepository,
|
||||
private val dispatcher: CoroutineDispatcherProvider,
|
||||
private val swapTransactionRepository: SwapTransactionRepository,
|
||||
|
|
@ -72,7 +73,6 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
private val appCurrencyRepository: AppCurrencyRepository,
|
||||
private val currenciesRepository: CurrenciesRepository,
|
||||
private val initialToCurrencyResolver: InitialToCurrencyResolver,
|
||||
private val demoConfig: DemoConfig,
|
||||
private val transactionRepository: TransactionRepository,
|
||||
) : SwapInteractor {
|
||||
|
||||
|
|
@ -268,7 +268,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
val isBalanceWithoutFeeEnough = isBalanceEnough(fromToken, amount, null)
|
||||
val networkId = fromToken.currency.network.backendId
|
||||
when (provider.type) {
|
||||
ExchangeProviderType.DEX -> {
|
||||
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
|
||||
manageDex(
|
||||
networkId = networkId,
|
||||
fromToken = fromToken,
|
||||
|
|
@ -342,7 +342,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
)
|
||||
} else {
|
||||
provider to getQuotesState(
|
||||
exchangeProviderType = ExchangeProviderType.DEX,
|
||||
exchangeProviderType = provider.type,
|
||||
quoteDataModel = quotes,
|
||||
amount = amount,
|
||||
fromToken = fromToken,
|
||||
|
|
@ -568,7 +568,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
fee: TxFee,
|
||||
): SwapTransactionState {
|
||||
val cardId = getSelectedWallet()?.scanResponse?.card?.cardId ?: return SwapTransactionState.UnknownError
|
||||
if (demoConfig.isDemoCardId(cardId)) return SwapTransactionState.DemoMode
|
||||
if (isDemoCardUseCase(cardId)) return SwapTransactionState.DemoMode
|
||||
|
||||
return when (swapProvider.type) {
|
||||
ExchangeProviderType.CEX -> {
|
||||
|
|
@ -588,8 +588,9 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
userWalletId = requireNotNull(getSelectedWallet()).walletId,
|
||||
)
|
||||
}
|
||||
ExchangeProviderType.DEX -> {
|
||||
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
|
||||
onSwapDex(
|
||||
provider = swapProvider,
|
||||
networkId = currencyToSend.currency.network.backendId,
|
||||
swapData = requireNotNull(swapData),
|
||||
currencyToSendStatus = currencyToSend,
|
||||
|
|
@ -641,6 +642,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun onSwapDex(
|
||||
provider: SwapProvider,
|
||||
networkId: String,
|
||||
swapData: SwapDataModel,
|
||||
currencyToSendStatus: CryptoCurrencyStatus,
|
||||
|
|
@ -652,7 +654,8 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" }
|
||||
val amount = SwapAmount(amountDecimal, currencyToSendStatus.currency.decimals)
|
||||
val derivationPath = currencyToSendStatus.currency.network.derivationPath.value
|
||||
val dataToSign = (swapData.transaction as ExpressTransactionModel.DEX).txData
|
||||
val dexTransaction = swapData.transaction as ExpressTransactionModel.DEX
|
||||
val dataToSign = dexTransaction.txData
|
||||
val txData = createTransactionUseCase(
|
||||
amount = amount.value.convertToAmount(currencyToSendStatus.currency),
|
||||
fee = getFeeForTransaction(
|
||||
|
|
@ -686,6 +689,17 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
txHash = txHash,
|
||||
payInExtraId = swapData.transaction.txExtraId,
|
||||
)
|
||||
if (provider.type == ExchangeProviderType.DEX_BRIDGE) {
|
||||
val timestamp = System.currentTimeMillis()
|
||||
storeSwapTransaction(
|
||||
currencyToSend = currencyToSendStatus,
|
||||
currencyToGet = currencyToGetStatus,
|
||||
amount = amount,
|
||||
swapProvider = provider,
|
||||
swapDataModel = swapData,
|
||||
timestamp = timestamp,
|
||||
)
|
||||
}
|
||||
storeLastCryptoCurrencyId(currencyToGetStatus.currency)
|
||||
SwapTransactionState.TxSent(
|
||||
fromAmount = amountFormatter.formatSwapAmountToUI(
|
||||
|
|
@ -702,16 +716,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
timestamp = System.currentTimeMillis(),
|
||||
)
|
||||
},
|
||||
ifLeft = {
|
||||
when (it) {
|
||||
SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled
|
||||
is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError
|
||||
is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError
|
||||
is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError
|
||||
is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode
|
||||
else -> SwapTransactionState.UnknownError
|
||||
}
|
||||
},
|
||||
ifLeft = { handleSendTxError(it) },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -783,14 +788,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
val derivationPath = currencyToSend.currency.network.derivationPath.value
|
||||
return result.fold(
|
||||
ifLeft = {
|
||||
when (it) {
|
||||
SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled
|
||||
is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError
|
||||
is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError
|
||||
is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError
|
||||
is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode
|
||||
else -> SwapTransactionState.UnknownError
|
||||
}
|
||||
handleSendTxError(it)
|
||||
},
|
||||
ifRight = { txHash ->
|
||||
repository.exchangeSent(
|
||||
|
|
@ -836,6 +834,17 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun handleSendTxError(txError: SendTransactionError?): SwapTransactionState {
|
||||
return when (txError) {
|
||||
SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled
|
||||
is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError
|
||||
is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError
|
||||
is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError
|
||||
is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode
|
||||
else -> SwapTransactionState.UnknownError
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFeeForTransaction(fee: TxFee, blockchain: Blockchain): Fee {
|
||||
val feeAmountValue = fee.feeValue
|
||||
val feeAmount = Amount(
|
||||
|
|
@ -884,8 +893,8 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
swapProvider: SwapProvider,
|
||||
swapDataModel: SwapDataModel,
|
||||
timestamp: Long,
|
||||
txExternalUrl: String,
|
||||
txExternalId: String,
|
||||
txExternalUrl: String? = null,
|
||||
txExternalId: String? = null,
|
||||
) {
|
||||
swapTransactionRepository.storeTransaction(
|
||||
userWalletId = UserWalletId(userWalletManager.getWalletId()),
|
||||
|
|
@ -1074,7 +1083,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
)
|
||||
|
||||
when (exchangeProviderType) {
|
||||
ExchangeProviderType.DEX -> {
|
||||
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
|
||||
val state = updatePermissionState(
|
||||
networkId = networkId,
|
||||
fromTokenStatus = fromToken,
|
||||
|
|
@ -1409,19 +1418,25 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
swapAmount = swapAmount,
|
||||
spenderAddress = requireNotNull(spenderAddress) { "Spender address is null" },
|
||||
)
|
||||
val feeData = try {
|
||||
transactionManager.getFee(
|
||||
networkId = networkId,
|
||||
amountToSend = BigDecimal.ZERO,
|
||||
currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)),
|
||||
destinationAddress = fromToken.getContractAddress(),
|
||||
increaseBy = INCREASE_GAS_LIMIT_BY,
|
||||
data = transactionData,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to get fee")
|
||||
null
|
||||
val userWallet = getSelectedWallet()
|
||||
val cardId = userWallet?.scanResponse?.card?.cardId
|
||||
val feeData = if (cardId != null && isDemoCardUseCase(cardId)) {
|
||||
getDemoFees(fromTokenStatus.currency)
|
||||
} else {
|
||||
try {
|
||||
transactionManager.getFee(
|
||||
networkId = networkId,
|
||||
amountToSend = BigDecimal.ZERO,
|
||||
currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)),
|
||||
destinationAddress = fromToken.getContractAddress(),
|
||||
increaseBy = INCREASE_GAS_LIMIT_BY,
|
||||
data = transactionData,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to get fee")
|
||||
null
|
||||
}
|
||||
}
|
||||
val feeState = feeData?.let {
|
||||
when (feeData) {
|
||||
|
|
@ -1805,10 +1820,37 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
.toMap()
|
||||
}
|
||||
|
||||
private fun getDemoFees(cryptoCurrency: CryptoCurrency): ProxyFees.MultipleFees {
|
||||
val demoFee = ProxyAmount(
|
||||
currencySymbol = cryptoCurrency.symbol,
|
||||
value = minDemoFee,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
)
|
||||
return ProxyFees.MultipleFees(
|
||||
minFee = ProxyFee.Common(
|
||||
gasLimit = 1.toBigInteger(),
|
||||
fee = demoFee,
|
||||
),
|
||||
normalFee = ProxyFee.Common(
|
||||
gasLimit = 1.toBigInteger(),
|
||||
fee = demoFee.copy(value = normalDemoFee),
|
||||
|
||||
),
|
||||
priorityFee = ProxyFee.Common(
|
||||
gasLimit = 1.toBigInteger(),
|
||||
fee = demoFee.copy(value = priorityDemoFee),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private const val INCREASE_GAS_LIMIT_BY = 112 // 12%
|
||||
private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5%
|
||||
private const val INFINITY_SYMBOL = "∞"
|
||||
|
||||
private val minDemoFee = "0.0001".toBigDecimal()
|
||||
private val normalDemoFee = "0.0002".toBigDecimal()
|
||||
private val priorityDemoFee = "0.0003".toBigDecimal()
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ class SwapDomainModule {
|
|||
getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
|
||||
@SwapScope sendTransactionUseCase: SendTransactionUseCase,
|
||||
@SwapScope createTransactionUseCase: CreateTransactionUseCase,
|
||||
isDemoCardUseCase: IsDemoCardUseCase,
|
||||
quotesRepository: QuotesRepository,
|
||||
swapTransactionRepository: SwapTransactionRepository,
|
||||
appCurrencyRepository: AppCurrencyRepository,
|
||||
|
|
@ -61,6 +62,7 @@ class SwapDomainModule {
|
|||
getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase,
|
||||
sendTransactionUseCase = sendTransactionUseCase,
|
||||
createTransactionUseCase = createTransactionUseCase,
|
||||
isDemoCardUseCase = isDemoCardUseCase,
|
||||
quotesRepository = quotesRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dispatcher = coroutineDispatcherProvider,
|
||||
|
|
@ -69,7 +71,6 @@ class SwapDomainModule {
|
|||
currencyChecksRepository = currencyChecksRepository,
|
||||
currenciesRepository = currenciesRepository,
|
||||
initialToCurrencyResolver = initialToCurrencyResolver,
|
||||
demoConfig = DemoConfig(),
|
||||
transactionRepository = transactionRepository,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1004,13 +1004,15 @@ internal class StateBuilder(
|
|||
val fromFiatAmount = getFormattedFiatAmount(fromCryptoCurrency.value.fiatRate?.multiply(fromAmount))
|
||||
val toFiatAmount = getFormattedFiatAmount(toCryptoCurrency.value.fiatRate?.multiply(toAmount))
|
||||
|
||||
val shouldShowStatus = providerState.type == ExchangeProviderType.CEX.providerName ||
|
||||
providerState.type == ExchangeProviderType.DEX_BRIDGE.providerName
|
||||
return uiState.copy(
|
||||
successState = SwapSuccessStateHolder(
|
||||
timestamp = swapTransactionState.timestamp,
|
||||
txUrl = txUrl,
|
||||
providerName = stringReference(providerState.name),
|
||||
providerType = stringReference(providerState.type),
|
||||
showStatusButton = providerState.type == ExchangeProviderType.CEX.name,
|
||||
showStatusButton = shouldShowStatus,
|
||||
providerIcon = providerState.iconUrl,
|
||||
rate = providerState.subtitle,
|
||||
fee = stringReference("${fee.feeCryptoFormatted} (${fee.feeFiatFormatted})"),
|
||||
|
|
@ -1092,7 +1094,7 @@ internal class StateBuilder(
|
|||
): SwapStateHolder {
|
||||
val message = when (providerType) {
|
||||
ExchangeProviderType.CEX -> resourceReference(R.string.swapping_alert_cex_description, wrappedList(token))
|
||||
ExchangeProviderType.DEX -> {
|
||||
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
|
||||
val refs = buildList {
|
||||
if (isPriceImpact) {
|
||||
add(resourceReference(R.string.swapping_high_price_impact_description))
|
||||
|
|
@ -1548,7 +1550,7 @@ internal class StateBuilder(
|
|||
id = this.providerId,
|
||||
name = this.name,
|
||||
iconUrl = this.imageLarge,
|
||||
type = this.type.toString(),
|
||||
type = this.type.providerName,
|
||||
subtitle = stringReference(rateString),
|
||||
additionalBadge = badge,
|
||||
selectionType = selectionType,
|
||||
|
|
@ -1575,7 +1577,7 @@ internal class StateBuilder(
|
|||
id = this.providerId,
|
||||
name = this.name,
|
||||
iconUrl = this.imageLarge,
|
||||
type = this.type.toString(),
|
||||
type = this.type.providerName,
|
||||
subtitle = stringReference(rateString),
|
||||
additionalBadge = additionalBadge,
|
||||
selectionType = selectionType,
|
||||
|
|
@ -1612,7 +1614,7 @@ internal class StateBuilder(
|
|||
id = this.providerId,
|
||||
name = this.name,
|
||||
iconUrl = this.imageLarge,
|
||||
type = this.type.toString(),
|
||||
type = this.type.providerName,
|
||||
selectionType = selectionType,
|
||||
subtitle = alertText,
|
||||
additionalBadge = ProviderState.AdditionalBadge.Empty,
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ private val state = SwapSuccessStateHolder(
|
|||
txUrl = "https://www.google.com/#q=nam",
|
||||
fee = TextReference.Str("1 000 DAI ~ 1 000 MATIC"),
|
||||
providerName = TextReference.Str("1inch"),
|
||||
providerType = TextReference.Str(ExchangeProviderType.DEX.name),
|
||||
providerType = TextReference.Str(ExchangeProviderType.DEX.providerName),
|
||||
showStatusButton = false,
|
||||
providerIcon = "",
|
||||
fromTokenAmount = TextReference.Str("1 000 DAI"),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.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.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
|
||||
|
||||
|
|
@ -35,12 +37,15 @@ internal class TokenDetailsNotificationsAnalyticsSender(
|
|||
-> TokenDetailsAnalyticsEvent.Notice.NotEnoughFee(
|
||||
currency = cryptoCurrency,
|
||||
)
|
||||
is TokenDetailsNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner(
|
||||
programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX,
|
||||
source = AnalyticsParam.ScreensSources.Token,
|
||||
)
|
||||
is TokenDetailsNotification.NetworksUnreachable,
|
||||
is TokenDetailsNotification.ExistentialDeposit,
|
||||
is TokenDetailsNotification.NetworksNoAccount,
|
||||
is TokenDetailsNotification.TopUpWithoutReserve,
|
||||
is TokenDetailsNotification.RentInfo,
|
||||
is TokenDetailsNotification.SwapPromo,
|
||||
is TokenDetailsNotification.NetworkShutdown,
|
||||
is TokenDetailsNotification.HederaAssociateWarning,
|
||||
is TokenDetailsNotification.KoinosMana,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.componen
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.networkIconResId
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
|
|
@ -45,16 +48,9 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) {
|
|||
val onCloseClick: () -> Unit,
|
||||
) : TokenDetailsNotification(
|
||||
config = NotificationConfig(
|
||||
title = resourceReference(
|
||||
id = R.string.token_swap_changelly_promotion_title,
|
||||
formatArgs = wrappedList("0%"),
|
||||
),
|
||||
subtitle = resourceReference(
|
||||
id = R.string.token_swap_changelly_promotion_message,
|
||||
formatArgs = wrappedList("0%", startDateTime.dayOfMonth, endDateTime.dayOfMonth),
|
||||
),
|
||||
iconResId = R.drawable.img_swap_promo,
|
||||
backgroundResId = R.drawable.img_swap_promo_green_banner_background,
|
||||
title = resourceReference(id = R.string.swap_promo_title),
|
||||
subtitle = resourceReference(id = R.string.swap_promo_text),
|
||||
iconResId = R.drawable.img_okx_dex_logo,
|
||||
onCloseClick = onCloseClick,
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = resourceReference(id = com.tangem.core.ui.R.string.token_swap_promotion_button),
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ internal class TokenDetailsSwapTransactionsStateConverter(
|
|||
private fun getStatuses(status: ExchangeStatus?, hasFailed: Boolean = false): ImmutableList<ExchangeStatusState> {
|
||||
if (status == null) return persistentListOf()
|
||||
val isWaiting = status == ExchangeStatus.New || status == ExchangeStatus.Waiting
|
||||
val isWaitingTxHash = status == ExchangeStatus.WaitingTxHash
|
||||
val isConfirming = status == ExchangeStatus.Confirming
|
||||
val isVerifying = status == ExchangeStatus.Verifying
|
||||
val isExchanging = status == ExchangeStatus.Exchanging
|
||||
|
|
@ -174,27 +175,35 @@ internal class TokenDetailsSwapTransactionsStateConverter(
|
|||
val isExchangingDone = !isExchanging && isConfirmingDone
|
||||
val isSendingDone = !isSending && !isVerifying && !isFailed && isExchangingDone
|
||||
|
||||
return if (status == ExchangeStatus.Cancelled) {
|
||||
listOf(cancelledStep())
|
||||
} else {
|
||||
listOf(
|
||||
waitStep(isWaiting, isWaitingDone),
|
||||
confirmStep(isConfirming, isConfirmingDone),
|
||||
exchangeStep(
|
||||
isExchanging = isExchanging,
|
||||
isExchangingDone = isExchangingDone,
|
||||
isRefunded = isRefunded,
|
||||
hasFailed = hasFailed,
|
||||
isVerifying = isVerifying,
|
||||
isFailed = isFailed,
|
||||
),
|
||||
sendStep(
|
||||
isSending = isSending,
|
||||
isSendingDone = isSendingDone,
|
||||
isRefunded = isRefunded,
|
||||
hasFailed = hasFailed,
|
||||
),
|
||||
)
|
||||
return buildList {
|
||||
when {
|
||||
status == ExchangeStatus.Cancelled -> add(cancelledStep())
|
||||
isWaitingTxHash -> add(waitTxStep())
|
||||
// ExchangeStatus.Unknown is temporary added for 1inch
|
||||
status == ExchangeStatus.Unknown -> add(unknownStateStep())
|
||||
else -> {
|
||||
add(waitStep(isWaiting, isWaitingDone))
|
||||
add(confirmStep(isConfirming, isConfirmingDone))
|
||||
add(
|
||||
exchangeStep(
|
||||
isExchanging = isExchanging,
|
||||
isExchangingDone = isExchangingDone,
|
||||
isRefunded = isRefunded,
|
||||
hasFailed = hasFailed,
|
||||
isVerifying = isVerifying,
|
||||
isFailed = isFailed,
|
||||
),
|
||||
)
|
||||
add(
|
||||
sendStep(
|
||||
isSending = isSending,
|
||||
isSendingDone = isSendingDone,
|
||||
isRefunded = isRefunded,
|
||||
hasFailed = hasFailed,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}.toPersistentList()
|
||||
}
|
||||
|
||||
|
|
@ -216,6 +225,20 @@ internal class TokenDetailsSwapTransactionsStateConverter(
|
|||
isDone = isNewDone,
|
||||
)
|
||||
|
||||
private fun waitTxStep() = ExchangeStatusState(
|
||||
status = ExchangeStatus.Verifying,
|
||||
text = TextReference.Res(R.string.express_exchange_status_waiting_tx_hash),
|
||||
isActive = false,
|
||||
isDone = false,
|
||||
)
|
||||
|
||||
private fun unknownStateStep() = ExchangeStatusState(
|
||||
status = ExchangeStatus.Failed,
|
||||
text = TextReference.Res(R.string.express_exchange_status_failed),
|
||||
isActive = false,
|
||||
isDone = true,
|
||||
)
|
||||
|
||||
private fun confirmStep(isConfirming: Boolean, isConfirmingDone: Boolean) = ExchangeStatusState(
|
||||
status = ExchangeStatus.Confirming,
|
||||
text = when {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBotto
|
|||
import com.tangem.core.ui.components.marketprice.MarketPriceBlock
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.components.notifications.NotificationWithBackground
|
||||
import com.tangem.core.ui.components.notifications.OkxPromoNotification
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.components.transactions.txHistoryItems
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
|
|
@ -111,7 +111,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
|
|||
contentType = { it.config::class.java },
|
||||
itemContent = {
|
||||
if (it is TokenDetailsNotification.SwapPromo) {
|
||||
NotificationWithBackground(
|
||||
OkxPromoNotification(
|
||||
config = it.config,
|
||||
modifier = itemModifier.animateItemPlacement(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC
|
|||
SpacerH12()
|
||||
ExchangeProvider(
|
||||
providerName = TextReference.Str(config.provider.name),
|
||||
providerType = TextReference.Str(config.provider.type.name),
|
||||
providerType = TextReference.Str(config.provider.type.providerName),
|
||||
providerTxId = config.txExternalId,
|
||||
imageUrl = config.provider.imageLarge,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@ internal class ExchangeStatusFactory(
|
|||
return when (status) {
|
||||
ExchangeStatus.New,
|
||||
ExchangeStatus.Waiting,
|
||||
ExchangeStatus.WaitingTxHash,
|
||||
ExchangeStatus.Sending,
|
||||
ExchangeStatus.Confirming,
|
||||
ExchangeStatus.Exchanging,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.paging.cachedIn
|
|||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.deeplink.DeepLinksRegistry
|
||||
import com.tangem.core.deeplink.global.BuyCurrencyDeepLink
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
|
|
@ -709,14 +710,26 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
override fun onSwapPromoDismiss() {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
shouldShowSwapPromoTokenUseCase.neverToShow()
|
||||
analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Close)
|
||||
analyticsEventsHandler.send(
|
||||
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
|
||||
source = AnalyticsParam.ScreensSources.Token,
|
||||
programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX,
|
||||
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSwapPromoClick() {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
shouldShowSwapPromoTokenUseCase.neverToShow()
|
||||
analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Exchange(cryptoCurrency.symbol))
|
||||
analyticsEventsHandler.send(
|
||||
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
|
||||
source = AnalyticsParam.ScreensSources.Token,
|
||||
programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX,
|
||||
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Clicked,
|
||||
),
|
||||
)
|
||||
}
|
||||
onSwapClick(ScenarioUnavailabilityReason.None)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,38 +121,4 @@ sealed class WalletScreenAnalyticsEvent {
|
|||
|
||||
data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped")
|
||||
}
|
||||
|
||||
sealed class Promotion(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent(category = "Promotion", event = event, params = params) {
|
||||
class NoticePromotionBanner(
|
||||
source: AnalyticsParam.ScreensSources,
|
||||
programName: String,
|
||||
) : Promotion(
|
||||
event = "Notice - Promotion Banner",
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to source.value,
|
||||
"Program Name" to programName,
|
||||
),
|
||||
)
|
||||
|
||||
class PromotionBannerClicked(
|
||||
source: AnalyticsParam.ScreensSources,
|
||||
programName: String,
|
||||
action: BannerAction,
|
||||
) : Promotion(
|
||||
event = "Promo Banner Clicked",
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to source.value,
|
||||
"Program Name" to programName,
|
||||
"Action" to action.action,
|
||||
),
|
||||
) {
|
||||
sealed class BannerAction(val action: String) {
|
||||
data object Clicked : BannerAction(action = "Clicked")
|
||||
data object Closed : BannerAction(action = "Closed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils
|
|||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
|
||||
import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
||||
|
|
@ -46,16 +46,19 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
|
|||
is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses
|
||||
is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem
|
||||
is WalletNotification.Critical.BackupError -> MainScreen.BackupError
|
||||
is WalletNotification.TravalaPromo -> WalletScreenAnalyticsEvent.Promotion.NoticePromotionBanner(
|
||||
is WalletNotification.TravalaPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner(
|
||||
source = AnalyticsParam.ScreensSources.Main,
|
||||
programName = "Travala",
|
||||
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Travala,
|
||||
)
|
||||
is WalletNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner(
|
||||
source = AnalyticsParam.ScreensSources.Main,
|
||||
programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX,
|
||||
)
|
||||
is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender]
|
||||
is WalletNotification.Informational.NoAccount,
|
||||
is WalletNotification.Warning.LowSignatures,
|
||||
is WalletNotification.Warning.SomeNetworksUnreachable,
|
||||
is WalletNotification.Warning.NetworksUnreachable,
|
||||
is WalletNotification.SwapPromo,
|
||||
-> null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import com.tangem.domain.common.util.cardTypesResolver
|
|||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.promo.PromoBanner
|
||||
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
|
||||
import com.tangem.domain.settings.ShouldShowTravalaPromoWalletUseCase
|
||||
import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase
|
||||
import com.tangem.domain.tokens.GetTokenListUseCase
|
||||
import com.tangem.domain.tokens.error.TokenListError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
|
@ -34,7 +34,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
private val getTokenListUseCase: GetTokenListUseCase,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
|
||||
private val shouldShowTravalaPromoWalletUseCase: ShouldShowTravalaPromoWalletUseCase,
|
||||
private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase,
|
||||
private val promoRepository: PromoRepository,
|
||||
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
|
||||
private val backupValidator: BackupValidator,
|
||||
|
|
@ -45,18 +45,18 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
|
||||
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
||||
|
||||
val travalaPromoFlow = flow { emit(promoRepository.getTravalaPromoBanner()) }
|
||||
val promoFlow = flow { emit(promoRepository.getOkxPromoBanner()) }
|
||||
return combine(
|
||||
flow = getTokenListUseCase.launch(userWallet.walletId).conflate(),
|
||||
flow2 = isReadyToShowRateAppUseCase().conflate(),
|
||||
flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(),
|
||||
flow4 = shouldShowTravalaPromoWalletUseCase().conflate(),
|
||||
flow5 = travalaPromoFlow.conflate(),
|
||||
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowTravalaPromo, promoBanner ->
|
||||
flow4 = shouldShowSwapPromoWalletUseCase().conflate(),
|
||||
flow5 = promoFlow.conflate(),
|
||||
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowPromo, promoBanner ->
|
||||
|
||||
readyForRateAppNotification = true
|
||||
buildList {
|
||||
addTravalaPromoNotification(shouldShowTravalaPromo, promoBanner, clickIntents)
|
||||
addSwapPromoNotification(shouldShowPromo, promoBanner, clickIntents)
|
||||
|
||||
addCriticalNotifications(userWallet)
|
||||
|
||||
|
|
@ -69,18 +69,16 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addTravalaPromoNotification(
|
||||
private fun MutableList<WalletNotification>.addSwapPromoNotification(
|
||||
shouldShowPromo: Boolean,
|
||||
promoBanner: PromoBanner?,
|
||||
clickIntents: WalletClickIntents,
|
||||
) {
|
||||
promoBanner ?: return
|
||||
val promoNotification = WalletNotification.TravalaPromo(
|
||||
val promoNotification = WalletNotification.SwapPromo(
|
||||
startDateTime = promoBanner.bannerState.timeline.start,
|
||||
endDateTime = promoBanner.bannerState.timeline.end,
|
||||
bannerLink = promoBanner.bannerState.link,
|
||||
onBookNowButtonClick = clickIntents::onTravalaPromoClick,
|
||||
onCloseClick = clickIntents::onCloseTravalaPromoClick,
|
||||
onCloseClick = clickIntents::onCloseSwapPromoClick,
|
||||
)
|
||||
addIf(
|
||||
element = promoNotification,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.stringReference
|
|||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo
|
||||
|
|
@ -107,4 +108,18 @@ internal object WalletAdditionalInfoFactory {
|
|||
WalletAdditionalInfo(hideable = true, content = TextReference.Str(value = amount.orEmpty()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun UserWallet.getCardsCount(): Int? {
|
||||
return if (isMultiCurrency) {
|
||||
when (val status = scanResponse.card.backupStatus) {
|
||||
is CardDTO.BackupStatus.Active -> status.cardCount + 1
|
||||
is CardDTO.BackupStatus.CardLinked,
|
||||
is CardDTO.BackupStatus.NoBackup,
|
||||
null,
|
||||
-> 1
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -41,6 +42,7 @@ internal class WalletStateController @Inject constructor(
|
|||
}
|
||||
|
||||
fun update(transformer: WalletScreenStateTransformer) {
|
||||
Timber.d("Applying: ${transformer::class.simpleName}")
|
||||
mutableUiState.update(function = transformer::transform)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.model
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.pluralReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import org.joda.time.DateTime
|
||||
|
|
@ -205,16 +208,9 @@ sealed class WalletNotification(val config: NotificationConfig) {
|
|||
val onCloseClick: () -> Unit,
|
||||
) : WalletNotification(
|
||||
config = NotificationConfig(
|
||||
title = resourceReference(
|
||||
id = R.string.main_swap_changelly_promotion_title,
|
||||
formatArgs = wrappedList("0%"),
|
||||
),
|
||||
subtitle = resourceReference(
|
||||
id = R.string.main_swap_changelly_promotion_message,
|
||||
formatArgs = wrappedList("0%", startDateTime.dayOfMonth, endDateTime.dayOfMonth),
|
||||
),
|
||||
iconResId = R.drawable.img_swap_promo,
|
||||
backgroundResId = R.drawable.img_swap_promo_green_banner_background,
|
||||
title = resourceReference(id = R.string.swap_promo_title),
|
||||
subtitle = resourceReference(id = R.string.swap_promo_text),
|
||||
iconResId = R.drawable.img_okx_dex_logo,
|
||||
onCloseClick = onCloseClick,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,22 +1,32 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.transformers
|
||||
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.error.TokenListError
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class SetTokenListErrorTransformer(
|
||||
userWalletId: UserWalletId,
|
||||
private val selectedWallet: UserWallet,
|
||||
private val error: TokenListError,
|
||||
) : WalletStateTransformer(userWalletId) {
|
||||
private val appCurrency: AppCurrency,
|
||||
) : WalletStateTransformer(selectedWallet.walletId) {
|
||||
|
||||
override fun transform(prevState: WalletState): WalletState {
|
||||
return when (error) {
|
||||
is TokenListError.EmptyTokens -> {
|
||||
when (prevState) {
|
||||
is WalletState.MultiCurrency.Content -> {
|
||||
prevState.copy(tokensListState = WalletTokensListState.Empty)
|
||||
prevState.copy(
|
||||
walletCardState = prevState.walletCardState.toLoadedState(),
|
||||
tokensListState = WalletTokensListState.Empty,
|
||||
)
|
||||
}
|
||||
is WalletState.MultiCurrency.Locked -> {
|
||||
Timber.w("Impossible to load tokens list for locked wallet")
|
||||
|
|
@ -37,4 +47,21 @@ internal class SetTokenListErrorTransformer(
|
|||
-> prevState
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletCardState.toLoadedState(): WalletCardState {
|
||||
return WalletCardState.Content(
|
||||
id = id,
|
||||
title = title,
|
||||
additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = selectedWallet),
|
||||
imageResId = imageResId,
|
||||
onRenameClick = onRenameClick,
|
||||
onDeleteClick = onDeleteClick,
|
||||
balance = BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = BigDecimal.ZERO,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
),
|
||||
cardCount = selectedWallet.getCardsCount(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -61,6 +61,11 @@ internal abstract class BasicTokenListSubscriber(
|
|||
},
|
||||
flow2 = getSelectedAppCurrencyUseCase().distinctUntilChanged(),
|
||||
transform = { maybeTokenList, maybeAppCurrency ->
|
||||
val appCurrency = maybeAppCurrency.getOrElse { e ->
|
||||
Timber.e("Failed to load app currency: $e")
|
||||
AppCurrency.Default
|
||||
}
|
||||
|
||||
val tokenList = maybeTokenList.getOrElse(
|
||||
ifLoading = { maybeContent ->
|
||||
val isRefreshing = stateHolder.getWalletState(userWallet.walletId)
|
||||
|
|
@ -74,16 +79,17 @@ internal abstract class BasicTokenListSubscriber(
|
|||
},
|
||||
ifError = { e ->
|
||||
Timber.e("Failed to load token list: $e")
|
||||
SetTokenListErrorTransformer(userWallet.walletId, e)
|
||||
stateHolder.update(
|
||||
SetTokenListErrorTransformer(
|
||||
selectedWallet = userWallet,
|
||||
error = e,
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
)
|
||||
return@combine
|
||||
},
|
||||
)
|
||||
|
||||
val appCurrency = maybeAppCurrency.getOrElse { e ->
|
||||
Timber.e("Failed to load app currency: $e")
|
||||
AppCurrency.Default
|
||||
}
|
||||
|
||||
updateContent(tokenList, appCurrency)
|
||||
walletWithFundsChecker.check(tokenList)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import androidx.compose.foundation.lazy.LazyListScope
|
|||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.components.notifications.NotificationWithBackground
|
||||
import com.tangem.core.ui.components.notifications.OkxPromoNotification
|
||||
import com.tangem.core.ui.components.notifications.TravalaNotificationWithBackground
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
||||
|
|
@ -29,7 +29,7 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
|
|||
// TODO develop promo banner general component
|
||||
when (it) {
|
||||
is WalletNotification.SwapPromo -> {
|
||||
NotificationWithBackground(
|
||||
OkxPromoNotification(
|
||||
config = it.config,
|
||||
modifier = modifier.animateItemPlacement(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -216,6 +216,14 @@ internal class WalletViewModel @Inject constructor(
|
|||
is WalletsUpdateActionResolver.Action.DeleteWallet -> deleteWallet(action)
|
||||
is WalletsUpdateActionResolver.Action.UnlockWallet -> unlockWallet(action)
|
||||
is WalletsUpdateActionResolver.Action.UpdateWalletCardCount -> {
|
||||
// refresh loader to use actual user wallet
|
||||
walletScreenContentLoader.load(
|
||||
userWallet = action.selectedWallet,
|
||||
clickIntents = clickIntents,
|
||||
isRefresh = true,
|
||||
coroutineScope = viewModelScope,
|
||||
)
|
||||
|
||||
stateHolder.update(transformer = UpdateWalletCardsCountTransformer(action.selectedWallet))
|
||||
}
|
||||
is WalletsUpdateActionResolver.Action.UpdateWalletName -> {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.extenstions.unwrap
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.settings.NeverToShowWalletsScrollPreview
|
||||
import com.tangem.domain.tokens.FetchCardTokenListUseCase
|
||||
|
|
@ -37,6 +39,7 @@ internal class WalletClickIntents @Inject constructor(
|
|||
private val fetchTokenListUseCase: FetchTokenListUseCase,
|
||||
private val fetchCardTokenListUseCase: FetchCardTokenListUseCase,
|
||||
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val neverToShowWalletsScrollPreview: NeverToShowWalletsScrollPreview,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -115,7 +118,13 @@ internal class WalletClickIntents @Inject constructor(
|
|||
}
|
||||
|
||||
maybeFetchResult.onLeft {
|
||||
stateHolder.update(SetTokenListErrorTransformer(userWalletId = userWallet.walletId, error = it))
|
||||
stateHolder.update(
|
||||
SetTokenListErrorTransformer(
|
||||
selectedWallet = userWallet,
|
||||
error = it,
|
||||
appCurrency = getSelectedAppCurrencyUseCase.unwrap(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
stateHolder.update(
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@ import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase
|
|||
import com.tangem.domain.settings.ShouldShowTravalaPromoWalletUseCase
|
||||
import com.tangem.domain.tokens.FetchTokenListUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
|
||||
import com.tangem.domain.wallets.models.UnlockWalletsError
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletClickHandler
|
||||
|
|
@ -214,6 +214,13 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onCloseSwapPromoClick() {
|
||||
analyticsEventHandler.send(
|
||||
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
|
||||
source = AnalyticsParam.ScreensSources.Main,
|
||||
programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX,
|
||||
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed,
|
||||
),
|
||||
)
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
shouldShowSwapPromoWalletUseCase.neverToShow()
|
||||
}
|
||||
|
|
@ -221,10 +228,10 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
|||
|
||||
override fun onTravalaPromoClick(link: String?) {
|
||||
analyticsEventHandler.send(
|
||||
WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked(
|
||||
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
|
||||
source = AnalyticsParam.ScreensSources.Main,
|
||||
programName = "Travala",
|
||||
action = WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked.BannerAction.Clicked,
|
||||
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Travala,
|
||||
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Clicked,
|
||||
),
|
||||
)
|
||||
link?.let {
|
||||
|
|
@ -236,10 +243,10 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
|||
|
||||
override fun onCloseTravalaPromoClick() {
|
||||
analyticsEventHandler.send(
|
||||
WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked(
|
||||
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
|
||||
source = AnalyticsParam.ScreensSources.Main,
|
||||
programName = "Travala",
|
||||
action = WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked.BannerAction.Closed,
|
||||
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Travala,
|
||||
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed,
|
||||
),
|
||||
)
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
|
|
|
|||
|
|
@ -87,9 +87,9 @@ markdown = "0.7.2"
|
|||
# endregion Other libraries
|
||||
|
||||
# region Tangem
|
||||
tangemBlockchainSdk = "release-app_5.12-691"
|
||||
tangemBlockchainSdk = "release-app_5.12-694"
|
||||
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
||||
tangemCardSdk = "release-app_5.12-367"
|
||||
tangemCardSdk = "release-app_5.12-369"
|
||||
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||
# endregion Tangem
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue