Updated on 2026-08-14
This commit is contained in:
commit
36b7566a2a
184 changed files with 4396 additions and 1625 deletions
|
|
@ -52,6 +52,7 @@ dependencies {
|
|||
implementation(projects.domain.balanceHiding.models)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.analytics)
|
||||
implementation(projects.domain.visa)
|
||||
|
||||
implementation(projects.common)
|
||||
implementation(projects.core.analytics)
|
||||
|
|
@ -62,6 +63,7 @@ dependencies {
|
|||
implementation(projects.core.ui)
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.deepLinks)
|
||||
implementation(projects.libs.crypto)
|
||||
implementation(projects.libs.auth)
|
||||
|
||||
|
|
@ -77,6 +79,7 @@ dependencies {
|
|||
implementation(projects.data.wallets)
|
||||
implementation(projects.data.analytics)
|
||||
implementation(projects.data.transaction)
|
||||
implementation(projects.data.visa)
|
||||
|
||||
/** Features */
|
||||
implementation(projects.features.onboarding)
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ import androidx.lifecycle.flowWithLifecycle
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import arrow.core.getOrElse
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.tangem.core.deeplink.DeepLinksRegistry
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
|
|
@ -36,6 +36,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode
|
|||
import com.tangem.domain.card.ScanCardUseCase
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
import com.tangem.features.managetokens.navigation.ManageTokensRouter
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.tester.api.TesterRouter
|
||||
|
|
@ -57,8 +58,6 @@ import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsL
|
|||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
|
||||
import com.tangem.tap.features.intentHandler.IntentProcessor
|
||||
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
|
||||
import com.tangem.tap.features.intentHandler.handlers.BuyCurrencyIntentHandler
|
||||
import com.tangem.tap.features.intentHandler.handlers.SellCurrencyIntentHandler
|
||||
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
|
||||
import com.tangem.tap.features.main.MainViewModel
|
||||
import com.tangem.tap.features.main.model.Toast
|
||||
|
|
@ -138,6 +137,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
@Inject
|
||||
lateinit var qrScanningRouter: QrScanningRouter
|
||||
|
||||
@Inject
|
||||
lateinit var deepLinksRegistry: DeepLinksRegistry
|
||||
|
||||
internal val viewModel: MainViewModel by viewModels()
|
||||
|
||||
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode?>
|
||||
|
|
@ -167,6 +169,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
|
||||
checkForNotificationPermission()
|
||||
observeStateUpdates()
|
||||
|
||||
if (intent != null) {
|
||||
deepLinksRegistry.launch(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeStateUpdates() {
|
||||
|
|
@ -293,8 +299,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
val hasSavedWalletsProvider = { store.state.globalState.userWalletsListManager?.hasUserWallets == true }
|
||||
intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope))
|
||||
intentProcessor.addHandler(WalletConnectLinkIntentHandler())
|
||||
intentProcessor.addHandler(BuyCurrencyIntentHandler())
|
||||
intentProcessor.addHandler(SellCurrencyIntentHandler())
|
||||
}
|
||||
|
||||
private fun updateAppTheme(appThemeMode: AppThemeMode) {
|
||||
|
|
@ -332,6 +336,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
lifecycleScope.launch {
|
||||
intentProcessor.handleIntent(intent)
|
||||
}
|
||||
|
||||
if (intent != null) {
|
||||
deepLinksRegistry.launch(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun showSnackbar(
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
|
|||
)
|
||||
is OnboardingDialog.TwinningProcessNotCompleted -> TwinningProcessNotCompletedDialog.create(context)
|
||||
is OnboardingDialog.InterruptOnboarding -> InterruptOnboardingDialog.create(context, state.dialog)
|
||||
is OnboardingDialog.WalletActivationError -> WalletActivationErrorDialog.create(context, state.dialog)
|
||||
is WalletConnectDialog.UnsupportedCard ->
|
||||
SimpleAlertDialog.create(
|
||||
titleRes = R.string.wallet_connect_title,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ sealed class GlobalAction : Action {
|
|||
data class StartForUnfinishedBackup(val addedBackupCardsCount: Int) : Onboarding()
|
||||
|
||||
object Stop : Onboarding()
|
||||
|
||||
data class ShouldResetCardOnCreate(val shouldReset: Boolean) : Onboarding()
|
||||
}
|
||||
|
||||
object ScanFailsCounter {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,11 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
|
|||
is GlobalAction.Onboarding.Stop -> {
|
||||
globalState.copy(onboardingState = OnboardingState(false))
|
||||
}
|
||||
is GlobalAction.Onboarding.ShouldResetCardOnCreate -> {
|
||||
globalState.copy(
|
||||
onboardingState = globalState.onboardingState.copy(shouldResetOnCreate = action.shouldReset),
|
||||
)
|
||||
}
|
||||
is GlobalAction.ScanFailsCounter.Increment -> {
|
||||
globalState.copy(scanCardFailsCounter = globalState.scanCardFailsCounter + 1)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,4 +36,5 @@ typealias CryptoCurrencyName = String
|
|||
data class OnboardingState(
|
||||
val onboardingStarted: Boolean = false,
|
||||
val onboardingManager: OnboardingManager? = null,
|
||||
val shouldResetOnCreate: Boolean = false,
|
||||
)
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.common.redux.legacy
|
|||
|
||||
import com.tangem.domain.redux.LegacyAction
|
||||
import com.tangem.tap.common.feedback.RateCanBeBetterEmail
|
||||
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.store
|
||||
|
|
@ -20,6 +21,11 @@ internal object LegacyMiddleware {
|
|||
GlobalAction.Onboarding.Start(action.scanResponse, canSkipBackup = action.canSkipBackup),
|
||||
)
|
||||
}
|
||||
is LegacyAction.SendEmailTransactionFailed -> {
|
||||
store.state.globalState.feedbackManager?.sendEmail(
|
||||
SendTransactionFailedEmail(action.errorMessage),
|
||||
)
|
||||
}
|
||||
}
|
||||
next(action)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -310,4 +310,22 @@ internal object TokensDomainModule {
|
|||
): GetNetworksSupportedByWallet {
|
||||
return GetNetworksSupportedByWallet(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetBalanceNotEnoughForFeeWarningUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): GetBalanceNotEnoughForFeeWarningUseCase {
|
||||
return GetBalanceNotEnoughForFeeWarningUseCase(currenciesRepository, dispatchers)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideIsAmountSubtractAvailableUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): IsAmountSubtractAvailableUseCase {
|
||||
return IsAmountSubtractAvailableUseCase(currenciesRepository, dispatchers)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,13 @@ package com.tangem.tap.di.domain
|
|||
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.transaction.FeeRepository
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -20,24 +21,21 @@ internal object TransactionDomainModule {
|
|||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetFeeUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): GetFeeUseCase {
|
||||
return GetFeeUseCase(walletManagersFacade, dispatchers)
|
||||
fun provideGetFeeUseCase(walletManagersFacade: WalletManagersFacade): GetFeeUseCase {
|
||||
return GetFeeUseCase(walletManagersFacade)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideSendTransactionUseCase(
|
||||
isDemoCardUseCase: IsDemoCardUseCase,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
transactionRepository: TransactionRepository,
|
||||
): SendTransactionUseCase {
|
||||
return SendTransactionUseCase(
|
||||
isDemoCardUseCase = isDemoCardUseCase,
|
||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
transactionRepository = transactionRepository,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -46,4 +44,10 @@ internal object TransactionDomainModule {
|
|||
fun provideCreateTransactionUseCase(transactionRepository: TransactionRepository): CreateTransactionUseCase {
|
||||
return CreateTransactionUseCase(transactionRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideIsFeeApproximateUseCase(feeRepository: FeeRepository): IsFeeApproximateUseCase {
|
||||
return IsFeeApproximateUseCase(feeRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.visa.GetVisaCurrencyUseCase
|
||||
import com.tangem.domain.visa.repository.VisaRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
internal object VisaDomainModule {
|
||||
|
||||
@Provides
|
||||
fun provideVisaCurrencyUseCase(visaRepository: VisaRepository): GetVisaCurrencyUseCase {
|
||||
return GetVisaCurrencyUseCase(visaRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -85,11 +85,15 @@ class TangemSdkManager(
|
|||
).also { sendScanResultsToAnalytics(it) }
|
||||
}
|
||||
|
||||
suspend fun createProductWallet(scanResponse: ScanResponse): CompletionResult<CreateProductWalletTaskResponse> {
|
||||
suspend fun createProductWallet(
|
||||
scanResponse: ScanResponse,
|
||||
shouldReset: Boolean = false,
|
||||
): CompletionResult<CreateProductWalletTaskResponse> {
|
||||
return runTaskAsync(
|
||||
runnable = CreateProductWalletTask(
|
||||
cardTypesResolver = scanResponse.cardTypesResolver,
|
||||
derivationStyleProvider = scanResponse.derivationStyleProvider,
|
||||
shouldReset = shouldReset,
|
||||
),
|
||||
cardId = scanResponse.card.cardId,
|
||||
initialMessage = Message(resources.getString(R.string.initial_message_create_wallet_body)),
|
||||
|
|
@ -100,6 +104,7 @@ class TangemSdkManager(
|
|||
suspend fun importWallet(
|
||||
scanResponse: ScanResponse,
|
||||
mnemonic: String,
|
||||
shouldReset: Boolean,
|
||||
): CompletionResult<CreateProductWalletTaskResponse> {
|
||||
val defaultMnemonic = try {
|
||||
DefaultMnemonic(mnemonic, tangemSdk.wordlist)
|
||||
|
|
@ -108,9 +113,10 @@ class TangemSdkManager(
|
|||
}
|
||||
return runTaskAsync(
|
||||
CreateProductWalletTask(
|
||||
scanResponse.cardTypesResolver,
|
||||
cardTypesResolver = scanResponse.cardTypesResolver,
|
||||
derivationStyleProvider = scanResponse.derivationStyleProvider,
|
||||
defaultMnemonic,
|
||||
mnemonic = defaultMnemonic,
|
||||
shouldReset = shouldReset,
|
||||
),
|
||||
scanResponse.card.cardId,
|
||||
Message(resources.getString(R.string.initial_message_create_wallet_body)),
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
package com.tangem.tap.domain.tasks
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.common.core.CardSession
|
||||
import com.tangem.common.core.CardSessionRunnable
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.operations.PreflightReadMode
|
||||
import com.tangem.operations.PreflightReadTask
|
||||
import com.tangem.operations.wallet.CreateWalletTask
|
||||
|
||||
@Deprecated("Use CreateProductWalletAndRescanTask instead")
|
||||
class CreateWalletAndRescanTask : CardSessionRunnable<Card> {
|
||||
|
||||
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
|
||||
val card = session.environment.card.guard {
|
||||
callback(CompletionResult.Failure(TangemSdkError.CardError()))
|
||||
return
|
||||
}
|
||||
val firmwareVersion = card.firmwareVersion
|
||||
|
||||
val task = if (firmwareVersion < FirmwareVersion.MultiWalletAvailable) {
|
||||
CreateWalletTask(card.supportedCurves.first())
|
||||
} else {
|
||||
CreateWalletsTask()
|
||||
}
|
||||
|
||||
task.run(session) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success ->
|
||||
PreflightReadTask(PreflightReadMode.FullCardRead).run(session, callback)
|
||||
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
package com.tangem.tap.domain.tasks
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.core.CardSession
|
||||
import com.tangem.common.core.CardSessionRunnable
|
||||
import com.tangem.operations.PreflightReadMode
|
||||
import com.tangem.operations.PreflightReadTask
|
||||
import com.tangem.operations.wallet.CreateWalletTask
|
||||
|
||||
@Deprecated("Use CreateProductWalletTask instead")
|
||||
class CreateWalletsTask(curves: List<EllipticCurve>? = null) : CardSessionRunnable<Card> {
|
||||
|
||||
private val curves = curves ?: listOf(
|
||||
EllipticCurve.Secp256k1,
|
||||
EllipticCurve.Ed25519,
|
||||
EllipticCurve.Secp256r1,
|
||||
)
|
||||
|
||||
private var index = 0
|
||||
|
||||
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
|
||||
val curve = curves[index]
|
||||
createWallet(curve, session, callback)
|
||||
}
|
||||
|
||||
private fun createWallet(
|
||||
curve: EllipticCurve,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<Card>) -> Unit,
|
||||
) {
|
||||
CreateWalletTask(curve).run(session) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
if (index == curves.lastIndex) {
|
||||
PreflightReadTask(PreflightReadMode.FullCardRead).run(session, callback)
|
||||
return@run
|
||||
}
|
||||
index += 1
|
||||
createWallet(curves[index], session, callback)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
callback(CompletionResult.Failure(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.tap.domain.tasks.product
|
||||
|
||||
import com.tangem.common.card.CardWallet
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
|
||||
class CardInitializationValidator(private val expectedCurves: List<EllipticCurve>) {
|
||||
|
||||
fun validateWallets(wallets: List<CardWallet>): Boolean {
|
||||
val curves = wallets.map { it.curve }.toSet()
|
||||
return curves.size == expectedCurves.size &&
|
||||
curves.containsAll(expectedCurves)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.common.core.CardSession
|
||||
import com.tangem.common.core.CardSessionRunnable
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
|
|
@ -24,6 +25,7 @@ import com.tangem.operations.backup.PrimaryCard
|
|||
import com.tangem.operations.backup.StartPrimaryCardLinkingCommand
|
||||
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.operations.read.ReadWalletsListCommand
|
||||
import com.tangem.operations.wallet.CreateWalletTask
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.operations.wallet.CreateWalletResponse as SdkCreateWalletResponse
|
||||
|
|
@ -60,6 +62,7 @@ class CreateProductWalletTask(
|
|||
private val cardTypesResolver: CardTypesResolver,
|
||||
private val derivationStyleProvider: DerivationStyleProvider,
|
||||
private val mnemonic: Mnemonic? = null,
|
||||
private val shouldReset: Boolean,
|
||||
) : CardSessionRunnable<CreateProductWalletTaskResponse> {
|
||||
|
||||
override val allowsRequestAccessCodeFromRepository: Boolean = false
|
||||
|
|
@ -79,7 +82,7 @@ class CreateProductWalletTask(
|
|||
cardTypesResolver.isTangemTwins() ->
|
||||
throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet")
|
||||
|
||||
else -> CreateWalletTangemWallet(mnemonic, derivationStyleProvider)
|
||||
else -> CreateWalletTangemWallet(mnemonic, shouldReset, derivationStyleProvider, cardDto)
|
||||
}
|
||||
commandProcessor.proceed(cardDto, session) {
|
||||
when (it) {
|
||||
|
|
@ -139,38 +142,38 @@ private class CreateWalletTangemNote(private val cardTypesResolver: CardTypesRes
|
|||
*/
|
||||
private class CreateWalletTangemWallet(
|
||||
private val mnemonic: Mnemonic?,
|
||||
private val shouldReset: Boolean,
|
||||
private val derivationStyleProvider: DerivationStyleProvider,
|
||||
cardDTO: CardDTO,
|
||||
) : ProductCommandProcessor<CreateProductWalletTaskResponse> {
|
||||
|
||||
private var primaryCard: PrimaryCard? = null
|
||||
private val cardConfig = CardConfig.createConfig(cardDTO)
|
||||
|
||||
override fun proceed(
|
||||
card: CardDTO,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
|
||||
) {
|
||||
val config = CardConfig.createConfig(card)
|
||||
val walletsOnCard = card.wallets.map { it.curve }.toSet()
|
||||
val curves = config.mandatoryCurves.toSet()
|
||||
.intersect(card.supportedCurves.toSet())
|
||||
.subtract(walletsOnCard).toList()
|
||||
|
||||
if (curves.isEmpty()) {
|
||||
val createWalletResponses = card.wallets.map { wallet ->
|
||||
CreateWalletResponse(card.cardId, wallet)
|
||||
}
|
||||
proceedWithCreatedWallets(card, createWalletResponses, session, callback)
|
||||
return
|
||||
if (walletsOnCard.isEmpty()) {
|
||||
createMultiWallet(card, session, callback)
|
||||
} else if (shouldReset) {
|
||||
resetCard(card, session, callback)
|
||||
} else {
|
||||
callback(CompletionResult.Failure(TangemSdkError.WalletAlreadyCreated()))
|
||||
}
|
||||
CreateWalletsTask(curves, mnemonic).run(session) { result ->
|
||||
}
|
||||
|
||||
private fun createMultiWallet(
|
||||
card: CardDTO,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
|
||||
) {
|
||||
CreateWalletsTask(cardConfig.mandatoryCurves, mnemonic).run(session) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
proceedWithCreatedWallets(
|
||||
card = card,
|
||||
createWalletResponses = result.data.createWalletResponses.map { CreateWalletResponse(it) },
|
||||
session = session,
|
||||
callback = callback,
|
||||
)
|
||||
checkIfAllWalletsCreated(card, session, result.data, callback)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
callback(CompletionResult.Failure(result.error))
|
||||
|
|
@ -179,6 +182,61 @@ private class CreateWalletTangemWallet(
|
|||
}
|
||||
}
|
||||
|
||||
private fun checkIfAllWalletsCreated(
|
||||
card: CardDTO,
|
||||
session: CardSession,
|
||||
createResponse: CreateWalletsResponse,
|
||||
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
|
||||
) {
|
||||
if (card.firmwareVersion < FirmwareVersion.MultiWalletAvailable) {
|
||||
proceedWithCreatedWallets(
|
||||
card = card,
|
||||
createWalletResponses = createResponse.createWalletResponses.map { CreateWalletResponse(it) },
|
||||
session = session,
|
||||
callback = callback,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val command = ReadWalletsListCommand()
|
||||
command.run(session) { response ->
|
||||
when (response) {
|
||||
is CompletionResult.Success -> {
|
||||
val cardInitializationValidator = CardInitializationValidator(cardConfig.mandatoryCurves)
|
||||
if (cardInitializationValidator.validateWallets(response.data.wallets)) {
|
||||
proceedWithCreatedWallets(
|
||||
card = card,
|
||||
createWalletResponses = createResponse.createWalletResponses.map {
|
||||
CreateWalletResponse(it)
|
||||
},
|
||||
session = session,
|
||||
callback = callback,
|
||||
)
|
||||
} else {
|
||||
callback(CompletionResult.Failure(TangemSdkError.WalletAlreadyCreated()))
|
||||
}
|
||||
}
|
||||
is CompletionResult.Failure -> callback(CompletionResult.Failure(response.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetCard(
|
||||
card: CardDTO,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
|
||||
) {
|
||||
val resetCommand = ResetToFactorySettingsTask(allowsRequestAccessCodeFromRepository = false)
|
||||
resetCommand.run(session) {
|
||||
when (it) {
|
||||
is CompletionResult.Success -> {
|
||||
createMultiWallet(card, session, callback)
|
||||
}
|
||||
is CompletionResult.Failure -> callback(CompletionResult.Failure(it.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedWithCreatedWallets(
|
||||
card: CardDTO,
|
||||
createWalletResponses: List<CreateWalletResponse>,
|
||||
|
|
|
|||
|
|
@ -313,7 +313,12 @@ internal sealed class AddCustomTokenWarning(val description: TextReference) {
|
|||
/**
|
||||
* Floating button of add custom token screen
|
||||
*
|
||||
* @property isEnabled button availability
|
||||
* @property onClick lambda be invoked when button is been pressed
|
||||
* @property isEnabled button availability
|
||||
* @property showProgress whether circle progress indication is enabled
|
||||
* @property onClick lambda be invoked when button is been pressed
|
||||
*/
|
||||
internal data class AddCustomTokenFloatingButton(val isEnabled: Boolean, val onClick: () -> Unit)
|
||||
internal data class AddCustomTokenFloatingButton(
|
||||
val isEnabled: Boolean,
|
||||
val showProgress: Boolean,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -91,7 +91,11 @@ internal object AddCustomTokenPreviewData {
|
|||
),
|
||||
form = createDefaultForm(),
|
||||
warnings = createWarnings(),
|
||||
floatingButton = AddCustomTokenFloatingButton(isEnabled = false, onClick = {}),
|
||||
floatingButton = AddCustomTokenFloatingButton(
|
||||
isEnabled = false,
|
||||
showProgress = false,
|
||||
onClick = {},
|
||||
),
|
||||
testBlock = AddCustomTokenTestBlock(
|
||||
chooseTokenButtonText = "Choose token",
|
||||
clearButtonText = "Clear address",
|
||||
|
|
@ -112,7 +116,11 @@ internal object AddCustomTokenPreviewData {
|
|||
),
|
||||
form = createDefaultForm(),
|
||||
warnings = createWarnings(),
|
||||
floatingButton = AddCustomTokenFloatingButton(isEnabled = false, onClick = {}),
|
||||
floatingButton = AddCustomTokenFloatingButton(
|
||||
isEnabled = false,
|
||||
showProgress = false,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ internal fun AddCustomTokenFloatingButton(model: AddCustomTokenFloatingButton, m
|
|||
text = stringResource(id = R.string.custom_token_add_token),
|
||||
iconResId = R.drawable.ic_plus_24,
|
||||
enabled = model.isEnabled,
|
||||
showProgress = model.showProgress,
|
||||
onClick = model.onClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -48,7 +49,7 @@ private fun Preview_AddCustomTokenFloatingButton(
|
|||
|
||||
private class AddCustomTokenFloatingButtonProvider : CollectionPreviewParameterProvider<AddCustomTokenFloatingButton>(
|
||||
listOf(
|
||||
AddCustomTokenFloatingButton(isEnabled = true, onClick = {}),
|
||||
AddCustomTokenFloatingButton(isEnabled = false, onClick = {}),
|
||||
AddCustomTokenFloatingButton(isEnabled = true, showProgress = false, onClick = {}),
|
||||
AddCustomTokenFloatingButton(isEnabled = false, showProgress = false, onClick = {}),
|
||||
),
|
||||
)
|
||||
|
|
@ -142,7 +142,11 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun createFloatingButton(): AddCustomTokenFloatingButton {
|
||||
return AddCustomTokenFloatingButton(isEnabled = false, onClick = actionsHandler::onAddCustomTokenClick)
|
||||
return AddCustomTokenFloatingButton(
|
||||
isEnabled = true,
|
||||
showProgress = false,
|
||||
onClick = actionsHandler::onAddCustomTokenClick,
|
||||
)
|
||||
}
|
||||
|
||||
private inner class FormStateBuilder {
|
||||
|
|
@ -852,9 +856,14 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
analyticsSender.sendWhenAddTokenButtonClicked(currency)
|
||||
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val oldButtonState = uiState.floatingButton
|
||||
uiState = uiState.copySealed(
|
||||
floatingButton = uiState.floatingButton.copy(isEnabled = false, showProgress = true),
|
||||
)
|
||||
runCatching { featureInteractor.saveToken(currency) }
|
||||
.onSuccess { featureRouter.openWalletScreen() }
|
||||
.onFailure {
|
||||
uiState = uiState.copySealed(floatingButton = oldButtonState)
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,8 @@ internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier
|
|||
checked = item.isChecked,
|
||||
enabled = item.isEnabled,
|
||||
onCheckedChange = item.onCheckedChange,
|
||||
checkedColor = TangemTheme.colors.icon.accent,
|
||||
checkedColor = TangemTheme.colors.control.checked,
|
||||
uncheckedColor = TangemTheme.colors.icon.inactive,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ internal class ResetCardFragment : ComposeFragment(), StoreSubscriber<DetailsSta
|
|||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
ResetCardScreen(
|
||||
modifier = modifier,
|
||||
state = screenState.value,
|
||||
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,23 @@
|
|||
package com.tangem.tap.features.details.ui.resetcard
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconToggleButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.SpacerH24
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
|
||||
import com.tangem.tap.features.details.ui.common.DetailsMainButton
|
||||
import com.tangem.tap.features.details.ui.common.ScreenTitle
|
||||
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
|
||||
import com.tangem.wallet.R
|
||||
|
||||
|
|
@ -31,9 +27,7 @@ internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Uni
|
|||
modifier = modifier,
|
||||
content = {
|
||||
when (state) {
|
||||
is ResetCardScreenState.ResetCardScreenContent -> {
|
||||
ResetCardView(state = state)
|
||||
}
|
||||
is ResetCardScreenState.ResetCardScreenContent -> ResetCardView(state = state)
|
||||
ResetCardScreenState.InitialState -> {
|
||||
// do nothing for now, just white screen
|
||||
}
|
||||
|
|
@ -43,83 +37,86 @@ internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Uni
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "MagicNumber")
|
||||
@Composable
|
||||
private fun ResetCardView(state: ResetCardScreenState.ResetCardScreenContent) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
.verticalScroll(scrollState) // set scrollState after fillMaxSize
|
||||
.padding(horizontal = TangemTheme.dimens.spacing20),
|
||||
) {
|
||||
ScreenTitle(titleRes = R.string.card_settings_reset_card_to_factory)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 21.dp),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.img_alert),
|
||||
contentDescription = "",
|
||||
tint = Color.Unspecified,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.offset(y = (-32).dp),
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.common_attention),
|
||||
modifier = Modifier.padding(start = 20.dp, end = 20.dp),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Title()
|
||||
SpacerH24()
|
||||
AlertImage()
|
||||
SpacerH24()
|
||||
Subtitle()
|
||||
SpacerH16()
|
||||
Description(text = state.descriptionText)
|
||||
SpacerH12()
|
||||
Conditions(state)
|
||||
DynamicSpacer(scrollState = scrollState)
|
||||
SpacerH16()
|
||||
ResetButton(enabled = state.resetButtonEnabled, onResetButtonClick = state.onResetButtonClick)
|
||||
SpacerH16()
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
@Composable
|
||||
private fun Title() {
|
||||
Text(
|
||||
text = stringResource(id = R.string.card_settings_reset_card_to_factory),
|
||||
style = TangemTheme.typography.h1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = state.descriptionText.resolveReference(),
|
||||
modifier = Modifier.padding(start = 20.dp, end = 20.dp),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
@Composable
|
||||
private fun AlertImage() {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.img_alert_80),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(TangemTheme.dimens.size80),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(28.dp))
|
||||
@Composable
|
||||
private fun Subtitle() {
|
||||
Text(
|
||||
text = stringResource(id = R.string.common_attention),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
|
||||
state.warningsToShow.forEach {
|
||||
when (it) {
|
||||
ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS -> {
|
||||
ConditionCheckBox(
|
||||
checkedState = state.acceptCondition1Checked,
|
||||
onCheckedChange = state.onAcceptCondition1ToggleClick,
|
||||
description = TextReference.Res(R.string.reset_card_to_factory_condition_1),
|
||||
)
|
||||
}
|
||||
ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE -> {
|
||||
ConditionCheckBox(
|
||||
checkedState = state.acceptCondition2Checked,
|
||||
onCheckedChange = state.onAcceptCondition2ToggleClick,
|
||||
description = TextReference.Res(R.string.reset_card_to_factory_condition_2),
|
||||
)
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
private fun Description(text: TextReference) {
|
||||
Text(
|
||||
text = text.resolveReference(),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Conditions(state: ResetCardScreenState.ResetCardScreenContent) {
|
||||
state.warningsToShow.forEach {
|
||||
when (it) {
|
||||
ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS -> {
|
||||
ConditionCheckBox(
|
||||
checkedState = state.acceptCondition1Checked,
|
||||
onCheckedChange = state.onAcceptCondition1ToggleClick,
|
||||
description = TextReference.Res(R.string.reset_card_to_factory_condition_1),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
Box(
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 32.dp),
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = state.resetButtonEnabled,
|
||||
label = "Update checked state",
|
||||
) { buttonEnabled ->
|
||||
DetailsMainButton(
|
||||
title = stringResource(id = R.string.reset_card_to_factory_button_title),
|
||||
onClick = state.onResetButtonClick,
|
||||
enabled = buttonEnabled,
|
||||
)
|
||||
}
|
||||
ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE -> {
|
||||
ConditionCheckBox(
|
||||
checkedState = state.acceptCondition2Checked,
|
||||
onCheckedChange = state.onAcceptCondition2ToggleClick,
|
||||
description = TextReference.Res(R.string.reset_card_to_factory_condition_2),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -130,16 +127,11 @@ private fun ConditionCheckBox(checkedState: Boolean, onCheckedChange: (Boolean)
|
|||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
onClick = { onCheckedChange.invoke(!checkedState) },
|
||||
)
|
||||
.padding(top = TangemTheme.dimens.size16, bottom = TangemTheme.dimens.size16),
|
||||
.clickable(onClick = { onCheckedChange.invoke(!checkedState) })
|
||||
.padding(vertical = TangemTheme.dimens.size16),
|
||||
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
IconToggleButton(
|
||||
checked = checkedState,
|
||||
onCheckedChange = onCheckedChange,
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.size20, end = TangemTheme.dimens.size20),
|
||||
) {
|
||||
IconToggleButton(checked = checkedState, onCheckedChange = onCheckedChange) {
|
||||
AnimatedContent(targetState = checkedState, label = "Update checked state") { checked ->
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
|
|
@ -151,22 +143,44 @@ private fun ConditionCheckBox(checkedState: Boolean, onCheckedChange: (Boolean)
|
|||
),
|
||||
contentDescription = null,
|
||||
tint = if (checked) {
|
||||
TangemTheme.colors.icon.accent
|
||||
TangemTheme.colors.control.checked
|
||||
} else {
|
||||
TangemTheme.colors.icon.secondary
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = description.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
modifier = Modifier.padding(end = TangemTheme.dimens.size20),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It's helps to create an adaptive layout.
|
||||
* ResetButton will be attach to the bottom of large screen or will be inside scroll layout of small screen.
|
||||
*
|
||||
* @param scrollState flag determines if screen is small (has scroll) or large (hasn't scroll)
|
||||
*/
|
||||
@Composable
|
||||
private fun ColumnScope.DynamicSpacer(scrollState: ScrollState) {
|
||||
if (!scrollState.canScrollBackward && !scrollState.canScrollForward) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) {
|
||||
DetailsMainButton(
|
||||
title = stringResource(id = R.string.reset_card_to_factory_button_title),
|
||||
onClick = onResetButtonClick,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
private fun ResetCardScreenSample(modifier: Modifier = Modifier) {
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
package com.tangem.tap.features.intentHandler.handlers
|
||||
|
||||
import android.content.Intent
|
||||
import com.tangem.tap.features.intentHandler.IntentHandler
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class BuyCurrencyIntentHandler : IntentHandler {
|
||||
|
||||
override fun handleIntent(intent: Intent?): Boolean {
|
||||
// FIXME: [REDACTED_JIRA]
|
||||
// val data = intent?.data ?: return false
|
||||
// val currency = store.state.walletState.selectedCurrency ?: return false
|
||||
//
|
||||
// val successUri = Uri.parse(ExchangeUrlBuilder.SUCCESS_URL)
|
||||
// return if (data.host == successUri.host && data.authority == successUri.authority) {
|
||||
// val currencyType = AnalyticsParam.CurrencyType.Currency(currency)
|
||||
// Analytics.send(TokenScreenAnalyticsEvent.Bought(currencyType.value))
|
||||
// true
|
||||
// } else {
|
||||
// false
|
||||
// }
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
package com.tangem.tap.features.intentHandler.handlers
|
||||
|
||||
import android.content.Intent
|
||||
import com.tangem.tap.features.intentHandler.IntentHandler
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class SellCurrencyIntentHandler : IntentHandler {
|
||||
|
||||
override fun handleIntent(intent: Intent?): Boolean {
|
||||
// FIXME: [REDACTED_JIRA]
|
||||
// return try {
|
||||
// val intentData = intent?.data ?: return false
|
||||
// val transactionID = intentData.getQueryParameter(TRANSACTION_ID_PARAM) ?: return false
|
||||
// val currency = intentData.getQueryParameter(CURRENCY_CODE_PARAM) ?: return false
|
||||
// val amount = intentData.getQueryParameter(CURRENCY_AMOUNT_PARAM) ?: return false
|
||||
// val destinationAddress = intentData.getQueryParameter(DEPOSIT_WALLET_ADDRESS_PARAM) ?: return false
|
||||
//
|
||||
// Timber.d("MoonPay Sell: $amount $currency to $destinationAddress")
|
||||
// store.dispatchOnMain(
|
||||
// TradeCryptoAction.SendCrypto(
|
||||
// currencyId = currency,
|
||||
// amount = amount,
|
||||
// destinationAddress = destinationAddress,
|
||||
// transactionId = transactionID,
|
||||
// ),
|
||||
// )
|
||||
// true
|
||||
// } catch (exception: Exception) {
|
||||
// Timber.d("Not MoonPay URL")
|
||||
// false
|
||||
// }
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// private companion object {
|
||||
// private const val TRANSACTION_ID_PARAM = "transactionId"
|
||||
// private const val CURRENCY_CODE_PARAM = "baseCurrencyCode"
|
||||
// private const val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount"
|
||||
// private const val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress"
|
||||
// }
|
||||
}
|
||||
|
|
@ -9,4 +9,5 @@ import com.tangem.core.navigation.StateDialog
|
|||
sealed class OnboardingDialog : StateDialog {
|
||||
object TwinningProcessNotCompleted : OnboardingDialog()
|
||||
data class InterruptOnboarding(val onOk: VoidCallback) : OnboardingDialog()
|
||||
data class WalletActivationError(val onConfirm: () -> Unit) : OnboardingDialog()
|
||||
}
|
||||
|
|
@ -120,7 +120,10 @@ private fun handleWalletAction(action: Action) {
|
|||
is OnboardingWalletAction.CreateWallet -> {
|
||||
scanResponse ?: return
|
||||
scope.launch {
|
||||
val result = tangemSdkManager.createProductWallet(scanResponse)
|
||||
val result = tangemSdkManager.createProductWallet(
|
||||
scanResponse,
|
||||
globalState.onboardingState.shouldResetOnCreate,
|
||||
)
|
||||
store.dispatchOnMain(OnboardingWalletAction.WalletWasCreated(true, result))
|
||||
}
|
||||
}
|
||||
|
|
@ -139,10 +142,15 @@ private fun handleWalletAction(action: Action) {
|
|||
)
|
||||
onboardingManager.scanResponse = updatedResponse
|
||||
|
||||
store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false))
|
||||
startCardActivation(updatedResponse)
|
||||
store.dispatch(OnboardingWalletAction.ResumeBackup)
|
||||
}
|
||||
is CompletionResult.Failure -> Unit
|
||||
is CompletionResult.Failure -> {
|
||||
if (result.error is TangemSdkError.WalletAlreadyCreated) {
|
||||
handleActivationError()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is OnboardingWalletAction.FinishOnboarding -> {
|
||||
|
|
@ -218,9 +226,15 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) {
|
|||
is OnboardingWallet2Action.CreateWallet -> {
|
||||
scanResponse ?: return
|
||||
scope.launch {
|
||||
val mediateResult = when (val result = tangemSdkManager.createProductWallet(scanResponse)) {
|
||||
val mediateResult = when (
|
||||
val result = tangemSdkManager.createProductWallet(
|
||||
scanResponse,
|
||||
globalState.onboardingState.shouldResetOnCreate,
|
||||
)
|
||||
) {
|
||||
is CompletionResult.Success -> {
|
||||
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
|
||||
store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false))
|
||||
val response = CreateWalletResponse(
|
||||
card = result.data.card,
|
||||
derivedKeys = result.data.derivedKeys,
|
||||
|
|
@ -230,6 +244,9 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) {
|
|||
}
|
||||
|
||||
is CompletionResult.Failure -> {
|
||||
if (result.error is TangemSdkError.WalletAlreadyCreated) {
|
||||
handleActivationError()
|
||||
}
|
||||
CompletionResult.Failure(result.error)
|
||||
}
|
||||
}
|
||||
|
|
@ -246,6 +263,7 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) {
|
|||
val result = tangemSdkManager.importWallet(
|
||||
scanResponse = scanResponse,
|
||||
mnemonic = action.mnemonicComponents.joinToString(" "),
|
||||
shouldReset = globalState.onboardingState.shouldResetOnCreate,
|
||||
)
|
||||
) {
|
||||
is CompletionResult.Success -> {
|
||||
|
|
@ -259,6 +277,7 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) {
|
|||
seedPhraseLength = action.mnemonicComponents.size,
|
||||
),
|
||||
)
|
||||
store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false))
|
||||
val response = CreateWalletResponse(
|
||||
card = result.data.card,
|
||||
derivedKeys = result.data.derivedKeys,
|
||||
|
|
@ -268,6 +287,9 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) {
|
|||
}
|
||||
|
||||
is CompletionResult.Failure -> {
|
||||
if (result.error is TangemSdkError.WalletAlreadyCreated) {
|
||||
handleActivationError()
|
||||
}
|
||||
CompletionResult.Failure(result.error)
|
||||
}
|
||||
}
|
||||
|
|
@ -300,6 +322,16 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleActivationError() {
|
||||
store.dispatchDialogShow(
|
||||
OnboardingDialog.WalletActivationError(
|
||||
onConfirm = {
|
||||
store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(true))
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateScanResponseAfterBackup(scanResponse: ScanResponse, backupState: BackupState): ScanResponse {
|
||||
val card = if (backupState.backupCardsNumber > 0) {
|
||||
val cardsCount = backupState.backupCardsNumber
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.ui
|
||||
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import com.tangem.feature.onboarding.api.OnboardingSeedPhrase
|
||||
import com.tangem.feature.onboarding.api.OnboardingSeedPhraseScreen
|
||||
import com.tangem.feature.onboarding.api.OnboardingSeedPhraseApi
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseScreen
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseViewModel
|
||||
|
|
@ -15,7 +15,7 @@ import com.tangem.wallet.R
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class OnboardingSeedPhraseStateHandler(
|
||||
private val onboardingSeedPhraseApi: OnboardingSeedPhraseApi = OnboardingSeedPhrase(),
|
||||
private val onboardingSeedPhraseApi: OnboardingSeedPhraseApi = OnboardingSeedPhraseScreen(),
|
||||
) {
|
||||
|
||||
fun newState(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.tangem.tap.common.extensions.dispatchDialogHide
|
||||
import com.tangem.tap.common.feedback.SupportInfo
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.features.onboarding.OnboardingDialog
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
||||
object WalletActivationErrorDialog {
|
||||
|
||||
fun create(context: Context, dialog: OnboardingDialog.WalletActivationError): Dialog {
|
||||
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
|
||||
setTitle(context.getString(R.string.onboarding_activation_error_title))
|
||||
setMessage(context.getString(R.string.onboarding_activation_error_message))
|
||||
setPositiveButton(R.string.common_ok) { _, _ -> dialog.onConfirm() }
|
||||
setNegativeButton(R.string.chat_button_title) { _, _ ->
|
||||
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
|
||||
}
|
||||
setOnDismissListener { store.dispatchDialogHide() }
|
||||
setCancelable(false)
|
||||
}.create()
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ import com.tangem.tap.domain.TapError
|
|||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.features.send.redux.PrepareSendScreen
|
||||
import com.tangem.tap.features.send.redux.SendAction
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
|
|
@ -50,19 +51,18 @@ object TradeCryptoMiddleware {
|
|||
if (DemoHelper.tryHandle(state, action)) return
|
||||
|
||||
when (action) {
|
||||
is TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen()
|
||||
is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
|
||||
is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action)
|
||||
is TradeCryptoAction.New.Sell -> proceedNewSellAction(action)
|
||||
is TradeCryptoAction.New.Swap -> openSwap(
|
||||
is TradeCryptoAction.Buy -> proceedBuyAction(state, action)
|
||||
is TradeCryptoAction.Sell -> proceedSellAction(action)
|
||||
is TradeCryptoAction.Swap -> openSwap(
|
||||
currency = action.cryptoCurrency,
|
||||
)
|
||||
is TradeCryptoAction.New.SendToken -> handleNewSendToken(action = action)
|
||||
is TradeCryptoAction.New.SendCoin -> handleNewSendCoin(action = action)
|
||||
is TradeCryptoAction.SendToken -> handleSendToken(action = action)
|
||||
is TradeCryptoAction.SendCoin -> handleSendCoin(action = action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedNewBuyAction(state: () -> AppState?, action: TradeCryptoAction.New.Buy) {
|
||||
private fun proceedBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) {
|
||||
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress
|
||||
?.defaultAddress
|
||||
?.let(NetworkAddress.Address::value)
|
||||
|
|
@ -119,7 +119,7 @@ object TradeCryptoMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun proceedNewSellAction(action: TradeCryptoAction.New.Sell) {
|
||||
private fun proceedSellAction(action: TradeCryptoAction.Sell) {
|
||||
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress
|
||||
?.defaultAddress
|
||||
?.let(NetworkAddress.Address::value)
|
||||
|
|
@ -139,33 +139,6 @@ object TradeCryptoMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun preconfigureAndOpenSendScreen() = scope.launch {
|
||||
// FIXME: [REDACTED_JIRA]
|
||||
// val selectedWalletData = store.state.walletState.selectedWalletData ?: return
|
||||
//
|
||||
// Analytics.send(Token.ButtonSend(AnalyticsParam.CurrencyType.Currency(selectedWalletData.currency)))
|
||||
// val walletManager = store.state.walletState.getWalletManager(selectedWalletData.currency).guard {
|
||||
// FirebaseCrashlytics.getInstance().recordException(IllegalStateException("WalletManager is null"))
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// store.dispatchOnMain(
|
||||
// PrepareSendScreen(
|
||||
// walletManager = walletManager,
|
||||
// coinAmount = walletManager.wallet.amounts[AmountType.Coin],
|
||||
// coinRate = selectedWalletData.fiatRate,
|
||||
// ),
|
||||
// )
|
||||
// store.dispatchOnMain(
|
||||
// SendAction.SendSpecificTransaction(
|
||||
// sendAmount = action.amount,
|
||||
// destinationAddress = action.destinationAddress,
|
||||
// transactionId = action.transactionId,
|
||||
// ),
|
||||
// )
|
||||
// store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Send))
|
||||
}
|
||||
|
||||
private fun openReceiptUrl(transactionId: String) {
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
store.state.globalState.exchangeManager.getSellCryptoReceiptUrl(
|
||||
|
|
@ -182,7 +155,7 @@ object TradeCryptoMiddleware {
|
|||
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle))
|
||||
}
|
||||
|
||||
private fun handleNewSendToken(action: TradeCryptoAction.New.SendToken) {
|
||||
private fun handleSendToken(action: TradeCryptoAction.SendToken) {
|
||||
val currency = action.tokenCurrency
|
||||
val blockchain = Blockchain.fromId(currency.network.id.value)
|
||||
|
||||
|
|
@ -221,6 +194,17 @@ object TradeCryptoMiddleware {
|
|||
),
|
||||
)
|
||||
|
||||
val txInfo = action.transactionInfo
|
||||
if (txInfo != null) {
|
||||
store.dispatchOnMain(
|
||||
SendAction.SendSpecificTransaction(
|
||||
sendAmount = txInfo.amount,
|
||||
destinationAddress = txInfo.destinationAddress,
|
||||
transactionId = txInfo.transactionId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val bundle = bundleOf(
|
||||
SendRouter.CRYPTO_CURRENCY_KEY to currency,
|
||||
SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue,
|
||||
|
|
@ -229,7 +213,7 @@ object TradeCryptoMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleNewSendCoin(action: TradeCryptoAction.New.SendCoin) {
|
||||
private fun handleSendCoin(action: TradeCryptoAction.SendCoin) {
|
||||
val cryptoStatus = action.coinStatus
|
||||
val currency = cryptoStatus.currency
|
||||
val blockchain = Blockchain.fromId(currency.network.id.value)
|
||||
|
|
@ -278,6 +262,17 @@ object TradeCryptoMiddleware {
|
|||
is CryptoCurrency.Token -> error("Action.tokenStatus.currency is Token")
|
||||
}
|
||||
|
||||
val txInfo = action.transactionInfo
|
||||
if (txInfo != null) {
|
||||
store.dispatchOnMain(
|
||||
SendAction.SendSpecificTransaction(
|
||||
sendAmount = txInfo.amount,
|
||||
destinationAddress = txInfo.destinationAddress,
|
||||
transactionId = txInfo.transactionId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val bundle = bundleOf(
|
||||
SendRouter.CRYPTO_CURRENCY_KEY to currency,
|
||||
SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue,
|
||||
|
|
|
|||
|
|
@ -22,20 +22,16 @@ data class MercuryoCurrenciesResponse(
|
|||
)
|
||||
|
||||
data class Config(
|
||||
val base: Map<String, String>,
|
||||
@Json(name = "has_withdrawal_fee")
|
||||
val hasWithdrawalFee: Map<String, Boolean>,
|
||||
@Json(name = "display_options")
|
||||
val displayOptions: Map<String, DisplayOption>,
|
||||
val icons: Map<String, Any>,
|
||||
@Json(name = "crypto_currencies")
|
||||
val cryptoCurrencies: List<MercuryoCryptoCurrency>,
|
||||
)
|
||||
|
||||
data class DisplayOption(
|
||||
@Json(name = "fullname")
|
||||
val fullName: String,
|
||||
@Json(name = "total_digits")
|
||||
val totalDigits: Int,
|
||||
@Json(name = "display_digits")
|
||||
val displayDigits: Int,
|
||||
data class MercuryoCryptoCurrency(
|
||||
@Json(name = "currency")
|
||||
val currencySymbol: String,
|
||||
@Json(name = "network")
|
||||
val network: String,
|
||||
@Json(name = "contract")
|
||||
val contractAddress: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ import com.tangem.tap.domain.model.Currency
|
|||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.network.exchangeServices.ExchangeService
|
||||
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
/**
|
||||
|
|
@ -21,8 +20,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E
|
|||
|
||||
private val api: MercuryoApi = environment.mercuryoApi
|
||||
|
||||
private val blockchainsAvailableToBuy = CopyOnWriteArrayList<Blockchain>()
|
||||
private val tokensAvailableToBuy = ConcurrentHashMap<String, List<Blockchain>>()
|
||||
private val availableMercuryoCurrencies = CopyOnWriteArrayList<MercuryoCurrenciesResponse.MercuryoCryptoCurrency>()
|
||||
|
||||
override fun featureIsSwitchedOn(): Boolean = true
|
||||
|
||||
|
|
@ -33,29 +31,14 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E
|
|||
override fun availableForBuy(currency: Currency): Boolean {
|
||||
if (!isBuyAllowed()) return false
|
||||
|
||||
// blockchains which cant be defined by mercuryo service
|
||||
val unsupportedBlockchains = listOf(
|
||||
Blockchain.Unknown,
|
||||
Blockchain.Binance,
|
||||
Blockchain.Arbitrum,
|
||||
Blockchain.Optimism,
|
||||
)
|
||||
val blockchain = currency.blockchain
|
||||
|
||||
return when (currency) {
|
||||
is Currency.Blockchain -> {
|
||||
when {
|
||||
blockchain.isTestnet() -> blockchain.getTestnetTopUpUrl() != null
|
||||
unsupportedBlockchains.contains(blockchain) -> false
|
||||
else -> blockchainsAvailableToBuy.contains(blockchain)
|
||||
}
|
||||
}
|
||||
|
||||
is Currency.Token -> {
|
||||
val supportedInBlockchains = tokensAvailableToBuy[currency.currencySymbol] ?: return false
|
||||
supportedInBlockchains.contains(blockchain)
|
||||
}
|
||||
val mercuryoNetwork = currency.blockchain.mercuryoNetwork()
|
||||
val contractAddress = (currency as? Currency.Token)?.token?.contractAddress ?: ""
|
||||
val availableCurrency = availableMercuryoCurrencies.firstOrNull {
|
||||
it.currencySymbol == currency.currencySymbol &&
|
||||
it.network == mercuryoNetwork &&
|
||||
it.contractAddress == contractAddress
|
||||
}
|
||||
return availableCurrency != null
|
||||
}
|
||||
|
||||
override fun availableForSell(currency: Currency): Boolean = false
|
||||
|
|
@ -67,8 +50,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E
|
|||
handleSuccessfullyUpdatedData(data = result.data.data)
|
||||
}
|
||||
result is Result.Failure -> {
|
||||
blockchainsAvailableToBuy.clear()
|
||||
tokensAvailableToBuy.clear()
|
||||
availableMercuryoCurrencies.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -95,34 +77,48 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E
|
|||
.appendQueryParameter("return_url", ExchangeUrlBuilder.SUCCESS_URL)
|
||||
if (isDarkTheme) builder.appendQueryParameter("theme", "1inch")
|
||||
|
||||
blockchain.mercuryoNetwork()?.let {
|
||||
builder.appendQueryParameter("network", it)
|
||||
}
|
||||
|
||||
return builder.build().toString()
|
||||
}
|
||||
|
||||
override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? = null
|
||||
|
||||
private fun handleSuccessfullyUpdatedData(data: MercuryoCurrenciesResponse.Data) {
|
||||
data.crypto.forEach { currencyName ->
|
||||
val blockchain = blockchainFromCurrencyName(currencyName)
|
||||
if (blockchain == null) {
|
||||
val specificBlockchain = data.config.base[currencyName]?.let(::blockchainFromCurrencyName)
|
||||
if (specificBlockchain != null) {
|
||||
tokensAvailableToBuy.set(
|
||||
key = currencyName,
|
||||
value = tokensAvailableToBuy[currencyName].orEmpty() + specificBlockchain,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
blockchainsAvailableToBuy.add(blockchain)
|
||||
}
|
||||
}
|
||||
availableMercuryoCurrencies.clear()
|
||||
availableMercuryoCurrencies.addAll(data.config.cryptoCurrencies)
|
||||
}
|
||||
|
||||
private fun blockchainFromCurrencyName(currencyName: String): Blockchain? {
|
||||
return when (currencyName) {
|
||||
"BNB" -> Blockchain.BSC
|
||||
"ETH" -> Blockchain.Ethereum
|
||||
"ADA" -> Blockchain.Cardano
|
||||
else -> Blockchain.values().find { it.currency.lowercase() == currencyName.lowercase() }
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun Blockchain.mercuryoNetwork(): String? {
|
||||
return when (this) {
|
||||
// Blockchain.Algorand -> "ALGORAND" //TODO: Uncomment with algo support
|
||||
Blockchain.Arbitrum -> "ARBITRUM"
|
||||
Blockchain.Avalanche -> "AVALANCHE"
|
||||
Blockchain.BSC -> "BINANCESMARTCHAIN"
|
||||
Blockchain.Bitcoin -> "BITCOIN"
|
||||
Blockchain.BitcoinCash -> "BITCOINCASH"
|
||||
Blockchain.Cardano -> "CARDANO"
|
||||
Blockchain.Cosmos -> "COSMOS"
|
||||
Blockchain.Dash -> "DASH"
|
||||
Blockchain.Dogecoin -> "DOGECOIN"
|
||||
Blockchain.Ethereum -> "ETHEREUM"
|
||||
Blockchain.Fantom -> "FANTOM"
|
||||
Blockchain.Kusama -> "KUSAMA"
|
||||
Blockchain.Litecoin -> "LITECOIN"
|
||||
Blockchain.Near -> "NEAR_PROTOCOL"
|
||||
Blockchain.TON -> "NEWTON"
|
||||
Blockchain.Optimism -> "OPTIMISM"
|
||||
Blockchain.Polkadot -> "POLKADOT"
|
||||
Blockchain.Polygon -> "POLYGON"
|
||||
Blockchain.XRP -> "RIPPLE"
|
||||
Blockchain.Solana -> "SOLANA"
|
||||
Blockchain.Stellar -> "STELLAR"
|
||||
Blockchain.Tezos -> "TEZOS"
|
||||
Blockchain.Tron -> "TRON"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.tap.proxy.redux
|
||||
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||
|
|
@ -13,6 +12,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository
|
|||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles
|
||||
import com.tangem.features.managetokens.navigation.ManageTokensRouter
|
||||
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ data class ExpressErrorValue(
|
|||
@Json(name = "minAmount")
|
||||
val minAmount: String?,
|
||||
|
||||
@Json(name = "maxAmount")
|
||||
val maxAmount: String?,
|
||||
|
||||
@Json(name = "decimals")
|
||||
val decimals: Int?,
|
||||
|
||||
|
|
|
|||
1
core/deep-links/.gitignore
vendored
Normal file
1
core/deep-links/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
23
core/deep-links/build.gradle.kts
Normal file
23
core/deep-links/build.gradle.kts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.core.deeplink"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/* Libs - AndroidX */
|
||||
implementation(deps.lifecycle.runtime.ktx)
|
||||
|
||||
/* Libs - Other */
|
||||
implementation(deps.timber)
|
||||
|
||||
/* DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
1
core/deep-links/global/.gitignore
vendored
Normal file
1
core/deep-links/global/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
15
core/deep-links/global/build.gradle.kts
Normal file
15
core/deep-links/global/build.gradle.kts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.core.deeplink.global"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/* Project */
|
||||
implementation(projects.core.deepLinks)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.core.deeplink.global
|
||||
|
||||
import com.tangem.core.deeplink.DeepLink
|
||||
|
||||
class BuyCurrencyDeepLink(val onReceive: () -> Unit) : DeepLink {
|
||||
|
||||
override val uri: String = "tangem://success.tangem.com"
|
||||
|
||||
override fun onReceive(params: Map<String, String>) {
|
||||
onReceive()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.core.deeplink.global
|
||||
|
||||
import com.tangem.core.deeplink.DeepLink
|
||||
|
||||
class SellCurrencyDeepLink(val onReceive: (data: Data) -> Unit) : DeepLink {
|
||||
|
||||
override val uri: String = "tangem://sell-request.tangem.com"
|
||||
|
||||
override fun onReceive(params: Map<String, String>) {
|
||||
val data = Data(
|
||||
transactionId = params["transactionId"] ?: return,
|
||||
baseCurrencyAmount = params["baseCurrencyAmount"] ?: return,
|
||||
depositWalletAddress = params["depositWalletAddress"] ?: return,
|
||||
)
|
||||
|
||||
onReceive(data)
|
||||
}
|
||||
|
||||
data class Data(
|
||||
val transactionId: String,
|
||||
val baseCurrencyAmount: String,
|
||||
val depositWalletAddress: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.core.deeplink
|
||||
|
||||
/**
|
||||
* Represents a deep link.
|
||||
*/
|
||||
interface DeepLink {
|
||||
|
||||
/**
|
||||
* ID of the deep link.
|
||||
*
|
||||
* By default, it is the same as the [uri].
|
||||
* */
|
||||
val id: String get() = uri
|
||||
|
||||
/**
|
||||
* URI of the deep link.
|
||||
*
|
||||
* **Note: Remember to add the URI in the AndroidManifest.xml file in the `app` module.**
|
||||
*
|
||||
* Query parameters will be received automatically.
|
||||
*
|
||||
* Path parameters can be added using the following syntax:
|
||||
* ```kotlin
|
||||
* "tangem://link" // Without parameters
|
||||
* "tangem://link/{param1}/{param2}" // With path parameters
|
||||
* ```
|
||||
* */
|
||||
val uri: String
|
||||
|
||||
/**
|
||||
* Method to be called when this deep link is received.
|
||||
*
|
||||
* @param params Map of parameters received from the deep link.
|
||||
* */
|
||||
fun onReceive(params: Map<String, String>)
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.tangem.core.deeplink
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
// TODO: Add tests
|
||||
/**
|
||||
* Provides functionality to handle deep links.
|
||||
*
|
||||
* Allows deep links to be launched, registered, or unregistered.
|
||||
*/
|
||||
interface DeepLinksRegistry {
|
||||
|
||||
/**
|
||||
* Finds matches registered deep links for the given [intent] and launches them.
|
||||
*
|
||||
* @return `true` if any deep link was received, `false` otherwise.
|
||||
*/
|
||||
fun launch(intent: Intent): Boolean
|
||||
|
||||
/**
|
||||
* Registers the given [deepLink].
|
||||
*
|
||||
* @see registerWithLifecycle
|
||||
* @see registerWithViewModel
|
||||
*/
|
||||
fun register(deepLink: DeepLink)
|
||||
|
||||
/**
|
||||
* Registers the given [deepLinks].
|
||||
*
|
||||
* @see registerWithLifecycle
|
||||
* @see registerWithViewModel
|
||||
*/
|
||||
fun register(deepLinks: Collection<DeepLink>)
|
||||
|
||||
/**
|
||||
* Unregisters the given [deepLinks].
|
||||
*/
|
||||
fun unregister(deepLinks: Collection<DeepLink>)
|
||||
|
||||
/**
|
||||
* Unregisters the given [deepLink].
|
||||
*/
|
||||
fun unregister(deepLink: DeepLink)
|
||||
|
||||
/**
|
||||
* Unregisters deep links with the given [ids].
|
||||
* */
|
||||
fun unregisterByIds(ids: Collection<String>)
|
||||
|
||||
/**
|
||||
* Registers the [deepLinks] when the [owner] is resumed and ensures that they are unregistered when the [owner] is
|
||||
* stopped.
|
||||
*/
|
||||
fun registerWithLifecycle(owner: LifecycleOwner, deepLinks: Collection<DeepLink>)
|
||||
|
||||
/**
|
||||
* Registers the [deepLinks] and ensures that they are unregistered when the [ViewModel] is closed.
|
||||
*/
|
||||
fun registerWithViewModel(viewModel: ViewModel, deepLinks: Collection<DeepLink>)
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.core.deeplink.di
|
||||
|
||||
import com.tangem.core.deeplink.DeepLinksRegistry
|
||||
import com.tangem.core.deeplink.impl.DefaultDeepLinksRegistry
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object DeepLinksModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDeepLinksRegistry(): DeepLinksRegistry {
|
||||
return DefaultDeepLinksRegistry()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
package com.tangem.core.deeplink.impl
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.tangem.core.deeplink.DeepLink
|
||||
import com.tangem.core.deeplink.DeepLinksRegistry
|
||||
import com.tangem.core.deeplink.utils.DeepLinksLifecycleObserver
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultDeepLinksRegistry : DeepLinksRegistry {
|
||||
|
||||
private var registries: List<DeepLink> = emptyList()
|
||||
|
||||
override fun launch(intent: Intent): Boolean {
|
||||
val received = intent.data ?: return false
|
||||
var hasMatch = false
|
||||
|
||||
Timber.d(
|
||||
"""
|
||||
Received deep link intent
|
||||
|- Received URI: $received
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
registries.forEach { deepLink ->
|
||||
val expected = deepLink.uri.toUri()
|
||||
|
||||
if (!isMatches(expected, received)) return@forEach
|
||||
hasMatch = true
|
||||
|
||||
val params = getParams(expected, received)
|
||||
|
||||
Timber.d(
|
||||
"""
|
||||
Matched deep link
|
||||
|- Expected URI: $expected
|
||||
|- Received URI: $received
|
||||
|- Params: $params
|
||||
""".trimIndent(),
|
||||
)
|
||||
deepLink.onReceive(params)
|
||||
}
|
||||
|
||||
if (!hasMatch) {
|
||||
Timber.d(
|
||||
"""
|
||||
No match found for deep link
|
||||
|- Received URI: $received
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
return hasMatch
|
||||
}
|
||||
|
||||
override fun register(deepLinks: Collection<DeepLink>) {
|
||||
registries = (registries + deepLinks).distinctBy(DeepLink::id)
|
||||
|
||||
Timber.d(
|
||||
"""
|
||||
Registered deep links
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun register(deepLink: DeepLink) {
|
||||
registries = (registries + deepLink).distinctBy(DeepLink::id)
|
||||
|
||||
Timber.d(
|
||||
"""
|
||||
Registered deep link
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun unregister(deepLinks: Collection<DeepLink>) {
|
||||
registries = registries.filter { it !in deepLinks }
|
||||
|
||||
Timber.d(
|
||||
"""
|
||||
Unregistered deep links
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun unregister(deepLink: DeepLink) {
|
||||
registries = registries.filter { it.id != deepLink.id }
|
||||
|
||||
Timber.d(
|
||||
"""
|
||||
Unregistered deep link
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun unregisterByIds(ids: Collection<String>) {
|
||||
registries = registries.filter { it.id !in ids }
|
||||
|
||||
Timber.d(
|
||||
"""
|
||||
Unregistered deep links
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun registerWithLifecycle(owner: LifecycleOwner, deepLinks: Collection<DeepLink>) {
|
||||
val observer = DeepLinksLifecycleObserver(deepLinksRegistry = this, deepLinks)
|
||||
owner.lifecycle.addObserver(observer)
|
||||
}
|
||||
|
||||
override fun registerWithViewModel(viewModel: ViewModel, deepLinks: Collection<DeepLink>) {
|
||||
viewModel.addCloseable {
|
||||
unregister(deepLinks)
|
||||
}
|
||||
|
||||
register(deepLinks)
|
||||
}
|
||||
|
||||
private fun isMatches(received: Uri, expected: Uri): Boolean {
|
||||
if (received == expected) return true
|
||||
if (received.authority != expected.authority ||
|
||||
received.pathSegments.size != expected.pathSegments.size
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
received.pathSegments.forEachIndexed { index, receivedSegment ->
|
||||
val expectedSegment = expected.pathSegments[index]
|
||||
if (receivedSegment != expectedSegment &&
|
||||
!(receivedSegment.startsWith(prefix = "{") && receivedSegment.endsWith(suffix = "}"))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getParams(received: Uri, expected: Uri): Map<String, String> {
|
||||
val params = mutableMapOf<String, String>()
|
||||
|
||||
received.pathSegments.forEachIndexed { index, receivedSegment ->
|
||||
val expectedSegment = expected.pathSegments[index]
|
||||
if (receivedSegment != expectedSegment &&
|
||||
receivedSegment.startsWith(prefix = "{") &&
|
||||
receivedSegment.endsWith(suffix = "}")
|
||||
) {
|
||||
val path = receivedSegment
|
||||
.replace(oldValue = "{", newValue = "")
|
||||
.replace(oldValue = "}", newValue = "")
|
||||
|
||||
params[path] = expectedSegment
|
||||
}
|
||||
}
|
||||
|
||||
expected.queryParameterNames.forEach { paramName ->
|
||||
expected.getQueryParameter(paramName)?.let { param ->
|
||||
params[paramName] = param
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.core.deeplink.utils
|
||||
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.tangem.core.deeplink.DeepLink
|
||||
import com.tangem.core.deeplink.DeepLinksRegistry
|
||||
|
||||
internal class DeepLinksLifecycleObserver(
|
||||
private val deepLinksRegistry: DeepLinksRegistry,
|
||||
private val deepLinks: Collection<DeepLink>,
|
||||
) : DefaultLifecycleObserver {
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
deepLinksRegistry.register(deepLinks)
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
deepLinksRegistry.unregister(deepLinks)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="common_fee_error">Erhalt der Gebühr fehlgeschlagen</string>
|
||||
<string name="no_account_bnb">Senden Sie Geld an diese Andresse um ein Konto zu erstellen</string>
|
||||
<string name="send_error_dust_amount_format">Minimaler Betrag ist %s</string>
|
||||
<string name="send_error_dust_change">Restbestand zu klein</string>
|
||||
<string name="send_error_invalid_fee_value">Falsche Gebühr</string>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="common_fee_error">Échec de réception des commissions</string>
|
||||
<string name="no_account_bnb">Pour créer un compte, envoyez des fonds monétaires à cette adresse</string>
|
||||
<string name="send_error_dust_amount_format">Le montant minimal est de %s</string>
|
||||
<string name="send_error_dust_change">Le reste est trop petit</string>
|
||||
<string name="send_error_invalid_fee_value">Commission non valide</string>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="common_fee_error">Impossibile ottenere la commissione</string>
|
||||
<string name="no_account_bnb">Per creare un account, invia fondi a questo indirizzo</string>
|
||||
<string name="send_error_dust_amount_format">L\'importo minimo è di %s</string>
|
||||
<string name="send_error_dust_change">L\'importo residuo è molto basso</string>
|
||||
<string name="send_error_invalid_fee_value">Commissione non valida</string>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
<string name="common_utxo_validate_withdrawal_message_warning">Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму.</string>
|
||||
<string name="eth_gas_required_exceeds_allowance">Недостаточно средств для совершения транзакции. Пожалуйста, пополните свой аккаунт.</string>
|
||||
<string name="generic_error_code">Произошла ошибка. Код: %s.</string>
|
||||
<string name="kaspa_withdrawal_message_warning">Из-за ограничений Kaspa в одну транзакцию может поместиться только %1$d UTXO. Это означает, что вы можете отправить только %2$s или меньше. Вам нужно уменьшить сумму.</string>
|
||||
<string name="no_account_generic">Чтобы использовать сеть %1$s, вы должны оплатить резерв аккаунта (%2$s %3$s), который блокируется и не используется в вашем балансе.</string>
|
||||
<string name="no_account_bnb">Для создания аккаунта отправьте средства на этот адрес</string>
|
||||
<string name="no_account_generic">Чтобы использовать сеть %1$s, вы должны оплатить резерв аккаунта (%2$s %3$s), который блокируется и не используется в вашем балансе</string>
|
||||
<string name="no_account_polkadot">Аккаунт получателя не активирован. Отправьте %s или более для активации аккаунта.</string>
|
||||
<string name="send_error_dust_amount_format">Минимальная сумма: %s</string>
|
||||
<string name="send_error_dust_change">Сдача слишком мала</string>
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@
|
|||
<string name="common_biometric_authentication">биометрическую аутентификацию</string>
|
||||
<string name="common_biometrics">биометрией</string>
|
||||
<string name="common_buy">Купить</string>
|
||||
<string name="common_buy_currency">Купить %1$s</string>
|
||||
<string name="common_buy_currency">Перейти на %1$s</string>
|
||||
<string name="common_camera_denied_alert_message">Вы не предоставили доступ к камере, пожалуйста, измените настройки конфиденциальности.</string>
|
||||
<string name="common_cancel">Отмена</string>
|
||||
<string name="common_close">Закрыть</string>
|
||||
|
|
@ -136,6 +136,7 @@
|
|||
<string name="common_transfer">Перевод</string>
|
||||
<string name="common_understand">Я понял</string>
|
||||
<string name="common_unreachable">Недоступно</string>
|
||||
<string name="common_unknown_error">Произошла ошибка. Пожалуйста, попробуйте снова.</string>
|
||||
<string name="common_yes">Да</string>
|
||||
<string name="contract_address_copied_message">Адрес контракта скопирован!</string>
|
||||
<string name="currency_subtitle_expanded">Доступные сети</string>
|
||||
|
|
@ -231,6 +232,7 @@
|
|||
<string name="express_provider">Провайдер</string>
|
||||
<string name="express_provider_best_rate">Лучший курс</string>
|
||||
<string name="express_provider_min_amount">Доступно с %s</string>
|
||||
<string name="express_provider_max_amount">Доступно до %s</string>
|
||||
<string name="express_provider_not_available">Недоступно для этой пары</string>
|
||||
<string name="express_provider_permission_needed">Требуется разрешение</string>
|
||||
<string name="express_terms_of_use">Условиями использования</string>
|
||||
|
|
@ -306,6 +308,8 @@
|
|||
<string name="onboarding_access_code_repeat_code_title">Повторно введите код доступа</string>
|
||||
<string name="onboarding_access_code_too_short">Код доступа должен состоять не менее чем из 4 символов.</string>
|
||||
<string name="onboarding_access_codes_doesnt_match">Введенные коды доступа не совпадают</string>
|
||||
<string name="onboarding_activation_error_message">Необходимо повторить операцию, при этом карта будет сброшена к заводским настройкам</string>
|
||||
<string name="onboarding_activation_error_title">Ошибка активации</string>
|
||||
<string name="onboarding_alert_message_not_max_backup_cards_added">Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить?</string>
|
||||
<string name="onboarding_backup_exit_warning">Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас.</string>
|
||||
<string name="onboarding_button_add_backup_card">Добавить резервную карту</string>
|
||||
|
|
@ -448,8 +452,9 @@
|
|||
<string name="send_amount_label">Сумма</string>
|
||||
<string name="send_amount_substract">Вычесть из суммы отправки</string>
|
||||
<string name="send_amount_substract_footer">Сумма к получению %s</string>
|
||||
<string name="send_alert_button_request_support">Поддержка</string>
|
||||
<string name="send_alert_transaction_failed_title">Транзакция не выполнена</string>
|
||||
<string name="send_alert_transaction_failed_text">Причина: %1$s\Код:%2$s</string>
|
||||
<string name="send_alert_transaction_failed_text">Причина: %1$s\nКод: %2$s</string>
|
||||
<string name="send_date_format">%1$s в %2$s</string>
|
||||
<string name="send_destination_hint_address">Адрес</string>
|
||||
<string name="send_destination_tag_field">Код назначения</string>
|
||||
|
|
@ -475,10 +480,10 @@
|
|||
<string name="send_network_fee_warning_title">Покрытие сетевой комиссии</string>
|
||||
<string name="send_notification_exceed_balance_text">Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса</string>
|
||||
<string name="send_notification_exceed_balance_title">Недостаточно средств</string>
|
||||
<string name="send_notification_exceed_fee_text">Размер комиссии превышает баланс сети. Для продолжения необходимо пополнить баланс сети.</string>
|
||||
<string name="send_notification_exceed_fee_title">Комиссия превышает баланс</string>
|
||||
<string name="send_notification_high_fee_text">Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01.</string>
|
||||
<string name="send_notification_high_fee_title">Увеличение комиссии</string>
|
||||
<string name="send_notification_high_fee_text">Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01.</string>
|
||||
<string name="send_notification_fee_too_high_accept">Оставить %s XTZ</string>
|
||||
<string name="send_notification_fee_too_high_ignore">Отправить все</string>
|
||||
<string name="send_notification_fee_too_high_title">Установлена высокая комиссия</string>
|
||||
<string name="send_notification_fee_too_high_text">Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.</string>
|
||||
<string name="send_notification_invalid_amount_text">Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению</string>
|
||||
|
|
@ -489,7 +494,6 @@
|
|||
<string name="send_notification_transaction_delay_text">Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции</string>
|
||||
<string name="send_notification_transaction_delay_title">Возможны задержки по транзакции</string>
|
||||
<string name="send_optional_field">Необязательное</string>
|
||||
<string name="send_qrcode_scan_amount_alert_title">QR код содержит информацию о сумме отправки равной %s</string>
|
||||
<string name="send_recent_transactions">Последние</string>
|
||||
<string name="send_recipient">Получатель</string>
|
||||
<string name="send_recipient_address_footer">Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов</string>
|
||||
|
|
@ -528,7 +532,6 @@
|
|||
<string name="swapping_approve_information_title">Подтвердить</string>
|
||||
<string name="swapping_error_wrapper">Ошибка: %s</string>
|
||||
<string name="swapping_from_title">Вы отправляете</string>
|
||||
<string name="swapping_generic_error">Произошла ошибка. Пожалуйста, попробуйте еще раз.</string>
|
||||
<string name="swapping_give_permission">Дать разрешение</string>
|
||||
<string name="swapping_high_price_impact_description">Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму.</string>
|
||||
<string name="swapping_insufficient_funds">Недостаточно средств</string>
|
||||
|
|
@ -651,7 +654,7 @@
|
|||
<string name="wallet_connect_service_no_chain_id">Dapp не предоставил необходимые данные для открытия сессии WalletConnect</string>
|
||||
<string name="wallet_connect_session_not_found">Не удалось найти сессию для обработки запроса</string>
|
||||
<string name="wallet_connect_sessions_title">Сессии WalletConnect</string>
|
||||
<string name="wallet_connect_subtitle">Подключение к Dapps</string>
|
||||
<string name="wallet_connect_subtitle">Подключение к dApps</string>
|
||||
<string name="wallet_connect_title">WalletConnect</string>
|
||||
<string name="wallet_connect_transaction_signed">Транзакция успешно подписана и отправлена в Dapp</string>
|
||||
<string name="wallet_connect_transaction_signed_and_send">Транзакция успешно подписана и отправлена в блокчейн</string>
|
||||
|
|
@ -690,8 +693,9 @@
|
|||
<string name="warning_express_not_enough_fee_for_token_tx_description">Чтобы совершить транзакцию, вам необходимо внести немного %1$s %2$s</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_title">Невозможно покрыть комиссию %s</string>
|
||||
<string name="warning_express_refresh_required_title">Cервис временно недоступен</string>
|
||||
<string name="warning_express_too_minimal_amount_description">Пожалуйста, измените сумму для обмена</string>
|
||||
<string name="warning_express_wrong_amount_description">Пожалуйста, измените сумму для обмена</string>
|
||||
<string name="warning_express_too_minimal_amount_title">Сумма для обмена должна быть не менее %s</string>
|
||||
<string name="warning_express_too_maximum_amount_title">Сумма для обмена должна быть не более %s</string>
|
||||
<string name="warning_failed_to_verify_card_message">Возможно, данная карта - образец или подделка</string>
|
||||
<string name="warning_failed_to_verify_card_title">Ошибка проверки подлинности</string>
|
||||
<string name="warning_low_signatures_message">На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства.</string>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<string name="address_type_legacy">遺留資產</string>
|
||||
<string name="common_fee_error">獲取費用失敗</string>
|
||||
<string name="kaspa_withdrawal_message_warning">由於 Kaspa 的限制,只有%1$d UTXO 可以放入單次交易中。這意味著您只能發送%2$s或更少數量。您需要減少數量。</string>
|
||||
<string name="no_account_bnb">要創建帳戶,請將資金發送到此地址</string>
|
||||
<string name="no_account_polkadot">目標帳戶未激活。發送 %s 或更多以激活帳戶</string>
|
||||
<string name="send_error_dust_amount_format">最小數量是 %s</string>
|
||||
<string name="send_error_dust_change">更動太小</string>
|
||||
|
|
|
|||
|
|
@ -327,7 +327,6 @@
|
|||
<string name="swapping_approve_information_text">批准被視為所有去中心化交易所的行業標準,並保護您的錢包在未經您許可的情況下不被智能合約訪問。按照設計,智能合約無法訪問您的代幣,除非您從您的終端批准訪問。通過“解鎖”您的代幣,您將獲得 1inch 智能合約使用您的資產的權限。網絡的礦工將獲得Gas Fee(由您支付)作為補償,以在區塊鏈上記錄此操作。一旦獲得許可,您就可以交易您的代幣。</string>
|
||||
<string name="swapping_approve_information_title">批准</string>
|
||||
<string name="swapping_error_wrapper">錯誤: %s</string>
|
||||
<string name="swapping_generic_error">有錯誤。請再試一遍</string>
|
||||
<string name="swapping_give_permission">賦予權限</string>
|
||||
<string name="swapping_high_price_impact_description">在此代幣交換的數量將對價格產生重大影響,並降低您收到的數量</string>
|
||||
<string name="swapping_insufficient_funds">餘額不足</string>
|
||||
|
|
@ -426,7 +425,7 @@
|
|||
<string name="wallet_connect_service_no_chain_id">Dapp 沒有提供必要的數據來建立 WalletConnect 連接</string>
|
||||
<string name="wallet_connect_session_not_found">找不到請求的連接</string>
|
||||
<string name="wallet_connect_sessions_title">WalletConnect 連接</string>
|
||||
<string name="wallet_connect_subtitle">連結到Dapps</string>
|
||||
<string name="wallet_connect_subtitle">連結到dApps</string>
|
||||
<string name="wallet_connect_title">WalletConnect</string>
|
||||
<string name="wallet_connect_transaction_signed">交易已成功簽署並發送至 Dapp</string>
|
||||
<string name="wallet_connect_transaction_signed_and_send">交易已成功簽署並發送至區塊鏈</string>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
<string name="eth_gas_required_exceeds_allowance">Not enough funds for the transaction. Please top up your account.</string>
|
||||
<string name="generic_error_code">An error occurred. Code: %s.</string>
|
||||
<string name="kaspa_withdrawal_message_warning">Due to Kaspa limitations only %1$d UTXOs can fit in a single transaction. This means you can only send %2$s or less. You need to reduce the amount.</string>
|
||||
<string name="no_account_generic">To use the %1$s network, you must pay the account reserve (%2$s %3$s), which locks up and hides that amount indefinitely.</string>
|
||||
<string name="no_account_bnb">To create account send funds to this address</string>
|
||||
<string name="no_account_generic">To use the %1$s network, you must pay the account reserve (%2$s %3$s), which locks up and hides that amount indefinitely</string>
|
||||
<string name="no_account_polkadot">Destination account is not active. Send %s or more to activate the account.</string>
|
||||
<string name="send_error_dust_amount_format">Minimum amount is %s</string>
|
||||
<string name="send_error_dust_change">Change is too small</string>
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@
|
|||
<string name="common_biometric_authentication">biometric authentication</string>
|
||||
<string name="common_biometrics">biometrics</string>
|
||||
<string name="common_buy">Buy</string>
|
||||
<string name="common_buy_currency">Buy %1$s</string>
|
||||
<string name="common_buy_currency">Go to %1$s</string>
|
||||
<string name="common_camera_denied_alert_message">You have not given access to your camera, please adjust your privacy settings</string>
|
||||
<string name="common_cancel">Cancel</string>
|
||||
<string name="common_close">Close</string>
|
||||
|
|
@ -135,6 +135,7 @@
|
|||
<string name="common_transfer">Transfer</string>
|
||||
<string name="common_understand">I understand</string>
|
||||
<string name="common_unreachable">Unreachable</string>
|
||||
<string name="common_unknown_error">There was an error. Please try again.</string>
|
||||
<string name="common_yes">Yes</string>
|
||||
<string name="contract_address_copied_message">Contract address copied!</string>
|
||||
<string name="currency_subtitle_expanded">Available networks</string>
|
||||
|
|
@ -234,6 +235,7 @@
|
|||
<string name="express_provider">Provider</string>
|
||||
<string name="express_provider_best_rate">Best rate</string>
|
||||
<string name="express_provider_min_amount">Available from %s</string>
|
||||
<string name="express_provider_max_amount">Available up to %s</string>
|
||||
<string name="express_provider_not_available">Unavailable for this pair</string>
|
||||
<string name="express_provider_permission_needed">Permission Required</string>
|
||||
<string name="express_terms_of_use">Terms of Use</string>
|
||||
|
|
@ -309,6 +311,8 @@
|
|||
<string name="onboarding_access_code_repeat_code_title">Re-enter your Access Code</string>
|
||||
<string name="onboarding_access_code_too_short">Access code must be at least 4 characters long</string>
|
||||
<string name="onboarding_access_codes_doesnt_match">Entered access code didn\'t match the initial access code</string>
|
||||
<string name="onboarding_activation_error_message">Please repeat the operation. The card will be reset to factory settings.</string>
|
||||
<string name="onboarding_activation_error_title">Activation error</string>
|
||||
<string name="onboarding_alert_message_not_max_backup_cards_added">You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process?</string>
|
||||
<string name="onboarding_backup_exit_warning">The backup process is partly complete. You can\'t exit it now.</string>
|
||||
<string name="onboarding_button_add_backup_card">Add a backup card</string>
|
||||
|
|
@ -447,8 +451,9 @@
|
|||
<string name="send_amount_label">Amount</string>
|
||||
<string name="send_amount_substract">Subtract from send amount</string>
|
||||
<string name="send_amount_substract_footer">The recipient will receive %s</string>
|
||||
<string name="send_alert_button_request_support">Support</string>
|
||||
<string name="send_alert_transaction_failed_title">The transaction is not completed</string>
|
||||
<string name="send_alert_transaction_failed_text">Reason: %1$s\nCode:%2$s</string>
|
||||
<string name="send_alert_transaction_failed_text">Reason: %1$s\nCode: %2$s</string>
|
||||
<string name="send_confirm_label">Confirm</string>
|
||||
<string name="send_date_format">%1$s at %2$s</string>
|
||||
<string name="send_destination_hint_address">Address</string>
|
||||
|
|
@ -480,10 +485,10 @@
|
|||
<string name="send_network_fee_warning_title">Network fee coverage</string>
|
||||
<string name="send_notification_exceed_balance_text">Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance</string>
|
||||
<string name="send_notification_exceed_balance_title">Total exceeds balance</string>
|
||||
<string name="send_notification_exceed_fee_text">The commission fee exceeds the network balance. To continue, it is necessary to replenish the network balance.</string>
|
||||
<string name="send_notification_exceed_fee_title">Fee exceeds balance</string>
|
||||
<string name="send_notification_high_fee_text">The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01.</string>
|
||||
<string name="send_notification_high_fee_title">Fee is increased</string>
|
||||
<string name="send_notification_high_fee_text">The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01.</string>
|
||||
<string name="send_notification_fee_too_high_accept">Reduce by %s XTZ</string>
|
||||
<string name="send_notification_fee_too_high_ignore">No, send all</string>
|
||||
<string name="send_notification_fee_too_high_title">Custom fee is high</string>
|
||||
<string name="send_notification_fee_too_high_text">The commission amount is %s times the recommended amount. Make sure that the custom settings are correct.</string>
|
||||
<string name="send_notification_invalid_amount_text">The included commission exceeds the transfer amount, leading to a negative value</string>
|
||||
|
|
@ -498,11 +503,6 @@
|
|||
<string name="send_notification_existential_deposit_title">Existential deposit</string>
|
||||
<string name="send_notification_existential_deposit_text">The account will be wiped from the blockchain if a balance goes below the existential deposit. Please ensure that the remaining balance after sending will not be less than %s.</string>
|
||||
<string name="send_optional_field">Optional</string>
|
||||
<string name="send_qrcode_alert_change">Change</string>
|
||||
<string name="send_qrcode_alert_decline">Decline</string>
|
||||
<string name="send_qrcode_scan_address_success">Recipient’s address scanned</string>
|
||||
<string name="send_qrcode_scan_amount_alert_text">Change the entered amount?</string>
|
||||
<string name="send_qrcode_scan_amount_alert_title">QR code contains information about the sending amount equal to %s</string>
|
||||
<string name="send_qrcode_scan_info">Please align your QR code with the square to scan it. Ensure you scan %s network address.</string>
|
||||
<string name="send_recent_transactions">Recent</string>
|
||||
<string name="send_recipient">Recipient</string>
|
||||
|
|
@ -519,6 +519,7 @@
|
|||
<string name="send_transaction_success">Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while</string>
|
||||
<string name="send_validation_invalid_address">Invalid address</string>
|
||||
<string name="sent_transaction_sent_title">Transaction sent</string>
|
||||
<string name="send_wallet_balance_format">%s (%s)</string>
|
||||
<string name="shop_buy_now">Buy now</string>
|
||||
<string name="shop_i_have_a_promo_code">I have a promo code…</string>
|
||||
<string name="shop_one_wallet">Tangem Wallet</string>
|
||||
|
|
@ -545,7 +546,6 @@
|
|||
<string name="swapping_approve_information_title">Approve</string>
|
||||
<string name="swapping_error_wrapper">Error: %s</string>
|
||||
<string name="swapping_from_title">You swap</string>
|
||||
<string name="swapping_generic_error">There was an error. Please try again.</string>
|
||||
<string name="swapping_give_permission">Give Permission</string>
|
||||
<string name="swapping_high_price_impact_description">Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome.</string>
|
||||
<string name="swapping_insufficient_funds">Insufficient funds</string>
|
||||
|
|
@ -666,7 +666,7 @@
|
|||
<string name="wallet_connect_service_no_chain_id">Dapp didn\'t provide essential data to establish WalletConnect session</string>
|
||||
<string name="wallet_connect_session_not_found">Failed to find session for request</string>
|
||||
<string name="wallet_connect_sessions_title">WalletConnect Sessions</string>
|
||||
<string name="wallet_connect_subtitle">Connect to Dapps</string>
|
||||
<string name="wallet_connect_subtitle">Connect to dApps</string>
|
||||
<string name="wallet_connect_title">WalletConnect</string>
|
||||
<string name="wallet_connect_transaction_signed">The transaction has been successfully signed and sent to the Dapp</string>
|
||||
<string name="wallet_connect_transaction_signed_and_send">The transaction has been succesfully signed and sent to the blockchain</string>
|
||||
|
|
@ -705,8 +705,9 @@
|
|||
<string name="warning_express_not_enough_fee_for_token_tx_description">To make a transaction you need to deposit some %1$s %2$s</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_title">Unable to cover %s fee</string>
|
||||
<string name="warning_express_refresh_required_title">Service temporarily unavailable</string>
|
||||
<string name="warning_express_too_minimal_amount_description">Please change the amount to swap</string>
|
||||
<string name="warning_express_wrong_amount_description">Please change the amount to swap</string>
|
||||
<string name="warning_express_too_minimal_amount_title">The amount to swap must be at least %s</string>
|
||||
<string name="warning_express_too_maximum_amount_title">The amount of tokens to be swapped must not exceed %s</string>
|
||||
<string name="warning_failed_to_verify_card_message">This card might be a production sample or counterfeit</string>
|
||||
<string name="warning_failed_to_verify_card_title">Authenticity check failed</string>
|
||||
<string name="warning_low_signatures_message">Only %s signatures are left on this card. You must withdraw all of your funds.</string>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.RadioButton
|
||||
import androidx.compose.material.RadioButtonDefaults
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -210,7 +212,7 @@ private fun TangemDialog(
|
|||
modifier = Modifier
|
||||
.background(
|
||||
shape = TangemTheme.shapes.roundedCornersLarge,
|
||||
color = TangemTheme.colors.background.plain,
|
||||
color = TangemTheme.colors.background.primary,
|
||||
)
|
||||
.padding(vertical = TangemTheme.dimens.spacing24),
|
||||
) {
|
||||
|
|
@ -386,7 +388,7 @@ private fun SelectorDialogContent(
|
|||
selected = index == selectedItemIndex,
|
||||
onClick = onClick,
|
||||
colors = RadioButtonDefaults.colors(
|
||||
selectedColor = TangemTheme.colors.icon.accent,
|
||||
selectedColor = TangemTheme.colors.control.checked,
|
||||
unselectedColor = TangemTheme.colors.icon.secondary,
|
||||
),
|
||||
interactionSource = interactionSource,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,172 @@
|
|||
package com.tangem.core.ui.components.fields
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.TopCenter
|
||||
import androidx.compose.ui.Alignment.Companion.TopStart
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.*
|
||||
import java.text.DecimalFormat
|
||||
|
||||
/**
|
||||
* Simple text field for amount input.
|
||||
* Validates and trims input text using [DecimalFormat]. Formats visual output using [AmountVisualTransformation].
|
||||
* Can display aligned placeholder and currency symbol [symbol].
|
||||
*
|
||||
* @param value initial text
|
||||
* @param decimals number of decimal places
|
||||
* @param onValueChange callback
|
||||
* @param textStyle text and placeholder styles
|
||||
* @param modifier modifier
|
||||
* @param symbol currency symbol
|
||||
* @param color text color
|
||||
* @param placeholderAlignment alignment of placeholder
|
||||
* @param showPlaceholder show placeholder
|
||||
* @param keyboardOptions keyboard options
|
||||
*
|
||||
* @see [SimpleTextField] for standard text field
|
||||
*/
|
||||
@Composable
|
||||
fun AmountTextField(
|
||||
value: String,
|
||||
decimals: Int,
|
||||
onValueChange: (String) -> Unit,
|
||||
textStyle: TextStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
symbol: String? = null,
|
||||
color: Color = TangemTheme.colors.text.primary1,
|
||||
placeholderAlignment: Alignment = TopStart,
|
||||
showPlaceholder: Boolean = true,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
) {
|
||||
val decimalFormat = rememberDecimalFormat()
|
||||
|
||||
val placeholderTextAlign = if (placeholderAlignment == TopCenter) {
|
||||
TextAlign.Center
|
||||
} else {
|
||||
TextAlign.Start
|
||||
}
|
||||
SimpleTextField(
|
||||
value = value,
|
||||
onValueChange = { newText ->
|
||||
if (decimalFormat.isValidSymbols(newText)) {
|
||||
val trimmed = decimalFormat.getValidatedNumberWithFixedDecimals(newText, decimals)
|
||||
onValueChange(trimmed)
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.action),
|
||||
textStyle = textStyle,
|
||||
color = color,
|
||||
keyboardOptions = keyboardOptions,
|
||||
singleLine = true,
|
||||
visualTransformation = AmountVisualTransformation(decimals, symbol, decimalFormat),
|
||||
decorationBox = { innerTextField ->
|
||||
Box {
|
||||
if (value.isBlank() && showPlaceholder) {
|
||||
val placeholder = if (symbol != null) {
|
||||
decimalFormat.defaultFormat().plus(" $symbol")
|
||||
} else {
|
||||
decimalFormat.defaultFormat()
|
||||
}
|
||||
Text(
|
||||
text = placeholder,
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
textAlign = placeholderTextAlign,
|
||||
modifier = Modifier
|
||||
.align(placeholderAlignment),
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun DecimalFormat.isValidSymbols(text: String): Boolean {
|
||||
return checkDecimalSeparatorDuplicate(text) && checkGroupingSeparator(text)
|
||||
}
|
||||
|
||||
// region preview
|
||||
@Preview(locale = "en", showBackground = true, name = "English")
|
||||
@Preview(locale = "ru", showBackground = true, name = "Russian")
|
||||
@Composable
|
||||
private fun AmountTextFieldPreview(
|
||||
@PreviewParameter(AmountTextFieldPreviewProvider::class) amount: AmountTextFieldPreviewData,
|
||||
) {
|
||||
var text by remember { mutableStateOf(amount.value.orEmpty()) }
|
||||
TangemTheme {
|
||||
AmountTextField(
|
||||
value = text,
|
||||
decimals = amount.decimals,
|
||||
symbol = amount.symbol,
|
||||
placeholderAlignment = amount.placeholderAlignment,
|
||||
showPlaceholder = amount.showPlaceholder,
|
||||
onValueChange = { text = it },
|
||||
textStyle = TangemTheme.typography.h2.copy(
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class AmountTextFieldPreviewProvider : PreviewParameterProvider<AmountTextFieldPreviewData> {
|
||||
override val values = sequenceOf(
|
||||
AmountTextFieldPreviewData(
|
||||
symbol = "USD",
|
||||
value = "1000000,123123",
|
||||
decimals = 3,
|
||||
placeholderAlignment = TopStart,
|
||||
showPlaceholder = true,
|
||||
),
|
||||
AmountTextFieldPreviewData(
|
||||
symbol = null,
|
||||
value = "1000000.123123",
|
||||
decimals = 6,
|
||||
placeholderAlignment = TopStart,
|
||||
showPlaceholder = false,
|
||||
),
|
||||
AmountTextFieldPreviewData(
|
||||
symbol = "$",
|
||||
value = null,
|
||||
decimals = 2,
|
||||
showPlaceholder = true,
|
||||
placeholderAlignment = TopCenter,
|
||||
),
|
||||
AmountTextFieldPreviewData(
|
||||
symbol = null,
|
||||
value = null,
|
||||
decimals = 2,
|
||||
showPlaceholder = true,
|
||||
placeholderAlignment = TopStart,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private data class AmountTextFieldPreviewData(
|
||||
val symbol: String? = "$",
|
||||
val value: String? = null,
|
||||
val decimals: Int = 2,
|
||||
val showPlaceholder: Boolean,
|
||||
val placeholderAlignment: Alignment,
|
||||
)
|
||||
// endregion
|
||||
|
|
@ -3,14 +3,18 @@ package com.tangem.core.ui.components.fields
|
|||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||
import androidx.compose.foundation.text.selection.TextSelectionColors
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
|
|
@ -19,6 +23,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
/**
|
||||
* Simple text field with placeholder
|
||||
*/
|
||||
@Suppress("ReusedModifierInstance")
|
||||
@Composable
|
||||
fun SimpleTextField(
|
||||
value: String,
|
||||
|
|
@ -29,32 +34,72 @@ fun SimpleTextField(
|
|||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
color: Color = TangemTheme.colors.text.primary1,
|
||||
textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color),
|
||||
readOnly: Boolean = false,
|
||||
decorationBox: (@Composable (innerTextField: @Composable () -> Unit) -> Unit)? = null,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
textStyle = TangemTheme.typography.body2.copy(color = color),
|
||||
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
|
||||
singleLine = singleLine,
|
||||
readOnly = readOnly,
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
decorationBox = { textValue ->
|
||||
Box {
|
||||
if (value.isBlank() && placeholder != null) {
|
||||
Text(
|
||||
text = placeholder.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
textValue()
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
.focusRequester(focusRequester),
|
||||
var textFieldValueState by remember {
|
||||
mutableStateOf(
|
||||
TextFieldValue(
|
||||
text = value,
|
||||
selection = when {
|
||||
value.isEmpty() -> TextRange.Zero
|
||||
else -> TextRange(value.length, value.length)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
val focusRequester = remember { FocusRequester.Default }
|
||||
val customTextSelectionColors = TextSelectionColors(
|
||||
handleColor = TangemTheme.colors.text.secondary,
|
||||
backgroundColor = TangemTheme.colors.text.secondary.copy(alpha = 0.4f),
|
||||
)
|
||||
|
||||
val textFieldValue = textFieldValueState.copy(text = value)
|
||||
|
||||
SideEffect {
|
||||
if (textFieldValue.selection != textFieldValueState.selection ||
|
||||
textFieldValue.composition != textFieldValueState.composition
|
||||
) {
|
||||
textFieldValueState = textFieldValue
|
||||
}
|
||||
}
|
||||
|
||||
var lastTextValue by remember(value) { mutableStateOf(value) }
|
||||
|
||||
CompositionLocalProvider(LocalTextSelectionColors provides customTextSelectionColors) {
|
||||
BasicTextField(
|
||||
value = textFieldValue,
|
||||
onValueChange = { newTextFieldValueState ->
|
||||
textFieldValueState = newTextFieldValueState
|
||||
|
||||
val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
|
||||
lastTextValue = newTextFieldValueState.text
|
||||
|
||||
if (stringChangedSinceLastInvocation) {
|
||||
onValueChange(newTextFieldValueState.text)
|
||||
}
|
||||
},
|
||||
textStyle = textStyle.copy(color = color),
|
||||
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
|
||||
singleLine = singleLine,
|
||||
readOnly = readOnly,
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
decorationBox = decorationBox ?: { textValue ->
|
||||
Box {
|
||||
if (value.isBlank() && placeholder != null) {
|
||||
Text(
|
||||
text = placeholder.resolveReference(),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
)
|
||||
}
|
||||
textValue()
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,28 +5,49 @@ import androidx.compose.ui.text.buildAnnotatedString
|
|||
import androidx.compose.ui.text.input.OffsetMapping
|
||||
import androidx.compose.ui.text.input.TransformedText
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import com.tangem.core.ui.utils.formatWithThousands
|
||||
import java.text.DecimalFormat
|
||||
|
||||
class AmountVisualTransformation(
|
||||
private val symbol: String,
|
||||
private val decimals: Int,
|
||||
private val symbol: String? = null,
|
||||
private val decimalFormat: DecimalFormat = DecimalFormat(),
|
||||
) : VisualTransformation {
|
||||
override fun filter(text: AnnotatedString): TransformedText {
|
||||
return TransformedText(
|
||||
buildAnnotatedString {
|
||||
append(text)
|
||||
if (text.isNotBlank()) {
|
||||
append(" ")
|
||||
append(symbol)
|
||||
}
|
||||
},
|
||||
object : OffsetMapping {
|
||||
override fun originalToTransformed(offset: Int): Int {
|
||||
return text.length
|
||||
}
|
||||
|
||||
override fun transformedToOriginal(offset: Int): Int {
|
||||
return text.length
|
||||
override fun filter(text: AnnotatedString): TransformedText {
|
||||
val formattedText = decimalFormat.formatWithThousands(
|
||||
text.text,
|
||||
decimals,
|
||||
)
|
||||
val groupingSymbol = decimalFormat.decimalFormatSymbols.groupingSeparator
|
||||
return TransformedText(
|
||||
text = buildAnnotatedString {
|
||||
append(formattedText)
|
||||
if (formattedText.isNotEmpty() && symbol != null) {
|
||||
append(" $symbol")
|
||||
}
|
||||
},
|
||||
offsetMapping = OffsetMappingImpl(text.text, formattedText, groupingSymbol),
|
||||
)
|
||||
}
|
||||
|
||||
private class OffsetMappingImpl(
|
||||
private val text: String,
|
||||
private val formattedText: String,
|
||||
private val gropingSymbol: Char,
|
||||
) : OffsetMapping {
|
||||
override fun originalToTransformed(offset: Int): Int {
|
||||
var noneDigitCount = 0
|
||||
var i = 0
|
||||
while (i < offset + noneDigitCount) {
|
||||
if (formattedText.getOrNull(i++) == gropingSymbol) noneDigitCount++
|
||||
}
|
||||
return (offset + noneDigitCount).coerceIn(0, formattedText.length)
|
||||
}
|
||||
|
||||
override fun transformedToOriginal(offset: Int): Int {
|
||||
val noneDigitCount = formattedText.take(offset).count { it == gropingSymbol }
|
||||
return (offset - noneDigitCount).coerceIn(0, text.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package com.tangem.core.ui.components.inputrow
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
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.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.tangem.core.ui.components.fields.AmountTextField
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Input row for entering amount. Manages correct amount format and validation
|
||||
*
|
||||
* @param title title reference
|
||||
* @param text primary text reference
|
||||
* @param onValueChange text change callback
|
||||
* @param modifier modifier
|
||||
* @param titleColor title color
|
||||
* @param textColor text color
|
||||
* @param keyboardOptions keyboard options for field
|
||||
* @param iconRes action icon
|
||||
* @param iconTint action icon tint
|
||||
* @param onIconClick click on action icon
|
||||
* @param showDivider show divider
|
||||
*
|
||||
* @see [InputRowDefault] for read only version
|
||||
* @see <a href=https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-799&mode=design&
|
||||
* t=IQ5lBJEkFGU4WSvi-4>InputRowEnter</a>
|
||||
*/
|
||||
@Composable
|
||||
fun InputRowEnterAmount(
|
||||
title: TextReference,
|
||||
text: String,
|
||||
decimals: Int,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
symbol: String? = null,
|
||||
titleColor: Color = TangemTheme.colors.text.secondary,
|
||||
textColor: Color = TangemTheme.colors.text.primary1,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
iconRes: Int? = null,
|
||||
iconTint: Color = TangemTheme.colors.icon.informative,
|
||||
onIconClick: (() -> Unit)? = null,
|
||||
showDivider: Boolean = false,
|
||||
) {
|
||||
DividerContainer(
|
||||
modifier = modifier,
|
||||
showDivider = showDivider,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = titleColor,
|
||||
)
|
||||
AmountTextField(
|
||||
value = text,
|
||||
decimals = decimals,
|
||||
symbol = symbol,
|
||||
onValueChange = onValueChange,
|
||||
color = textColor,
|
||||
textStyle = TangemTheme.typography.body2,
|
||||
keyboardOptions = keyboardOptions,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing8),
|
||||
)
|
||||
}
|
||||
iconRes?.let {
|
||||
Icon(
|
||||
painter = painterResource(id = iconRes),
|
||||
contentDescription = null,
|
||||
tint = iconTint,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing10,
|
||||
bottom = TangemTheme.dimens.spacing10,
|
||||
)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false),
|
||||
) { onIconClick?.invoke() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package com.tangem.core.ui.components.inputrow
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.components.fields.AmountTextField
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* `Input Row Enter Info` for entering amount. Manages correct amount format and validation
|
||||
|
||||
* @param title title reference
|
||||
* @param text primary text reference
|
||||
* @param onValueChange text change callback
|
||||
* @param modifier modifier
|
||||
* @param titleColor title color
|
||||
* @param textColor text color
|
||||
* @param isSingleLine text
|
||||
* @param visualTransformation applied transformation to text
|
||||
* @param keyboardOptions keyboard options for field
|
||||
* @param showDivider show divider
|
||||
*
|
||||
* @see [InputRowEnterInfo]
|
||||
* @see <a href=https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-799&mode
|
||||
* =design&t=IQ5lBJEkFGU4WSvi-4>Input Row Enter</a>
|
||||
* @see <a href=https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node
|
||||
* * -id=7854-33577&mode=design&t=6o23sqF8fDQdn4C5-4>Input Row Enter Info</a>
|
||||
*/
|
||||
@Composable
|
||||
fun InputRowEnterInfoAmount(
|
||||
title: TextReference,
|
||||
text: String,
|
||||
decimals: Int,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
symbol: String? = null,
|
||||
info: TextReference? = null,
|
||||
titleColor: Color = TangemTheme.colors.text.secondary,
|
||||
textColor: Color = TangemTheme.colors.text.primary1,
|
||||
infoColor: Color = TangemTheme.colors.text.tertiary,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
showDivider: Boolean = false,
|
||||
) {
|
||||
DividerContainer(
|
||||
modifier = modifier,
|
||||
showDivider = showDivider,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = titleColor,
|
||||
)
|
||||
Row {
|
||||
AmountTextField(
|
||||
value = text,
|
||||
decimals = decimals,
|
||||
symbol = symbol,
|
||||
onValueChange = onValueChange,
|
||||
color = textColor,
|
||||
textStyle = TangemTheme.typography.body2,
|
||||
keyboardOptions = keyboardOptions,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing8)
|
||||
.weight(1f),
|
||||
)
|
||||
info?.let {
|
||||
Text(
|
||||
text = it.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = infoColor,
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing8)
|
||||
.align(Alignment.Bottom),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -48,6 +48,7 @@ fun getActiveIconRes(blockchainId: String): Int {
|
|||
"decimal", "decimal/test" -> R.drawable.img_decimal_22
|
||||
"xdc", "xdc/test" -> R.drawable.img_xdc_22
|
||||
"vechain", "vechain/test" -> R.drawable.img_vechain_22
|
||||
"aptos", "aptos/test" -> R.drawable.img_aptos_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -97,6 +98,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int {
|
|||
"decimal", "decimal/test" -> R.drawable.img_decimal_22
|
||||
"xdc-network", "xdc-network/test" -> R.drawable.img_xdc_22
|
||||
"vechain", "vechain/test" -> R.drawable.img_vechain_22
|
||||
"aptos", "aptos/test" -> R.drawable.img_aptos_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -143,6 +145,7 @@ fun getActiveIconResByCoinId(coinId: String): Int {
|
|||
"decimal" -> R.drawable.img_decimal_22
|
||||
"xdce-crowd-sale" -> R.drawable.img_xdc_22
|
||||
"vechain" -> R.drawable.img_vechain_22
|
||||
"aptos" -> R.drawable.img_aptos_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -192,6 +195,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
|
|||
"decimal", "decimal/test" -> R.drawable.ic_decimal_22
|
||||
"xdc", "xdc/test" -> R.drawable.ic_xdc_22
|
||||
"vechain", "vechain/test" -> R.drawable.ic_vechain_22
|
||||
"aptos", "aptos/test" -> R.drawable.ic_aptos_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -241,6 +245,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int {
|
|||
"decimal", "decimal/test" -> R.drawable.ic_decimal_22
|
||||
"xdc-network", "xdc-network/test" -> R.drawable.ic_xdc_22
|
||||
"vechain", "vechain/test" -> R.drawable.ic_vechain_22
|
||||
"aptos", "aptos/test" -> R.drawable.ic_aptos_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ class TangemColors internal constructor(
|
|||
warning: Color,
|
||||
attention: Color,
|
||||
accent: Color = TangemColorPalette.Azure,
|
||||
constantWhite: Color = TangemColorPalette.White,
|
||||
constant: Color = TangemColorPalette.White,
|
||||
) {
|
||||
var primary1 by mutableStateOf(primary1)
|
||||
private set
|
||||
|
|
@ -60,7 +60,7 @@ class TangemColors internal constructor(
|
|||
private set
|
||||
var attention by mutableStateOf(attention)
|
||||
private set
|
||||
var constantWhite by mutableStateOf(constantWhite)
|
||||
var constantWhite by mutableStateOf(constant)
|
||||
private set
|
||||
|
||||
fun update(other: Text) {
|
||||
|
|
@ -83,6 +83,7 @@ class TangemColors internal constructor(
|
|||
warning: Color,
|
||||
attention: Color,
|
||||
accent: Color = TangemColorPalette.Azure,
|
||||
constant: Color = TangemColorPalette.White,
|
||||
) {
|
||||
var primary1 by mutableStateOf(primary1)
|
||||
private set
|
||||
|
|
@ -118,8 +119,7 @@ class TangemColors internal constructor(
|
|||
primary: Color,
|
||||
secondary: Color,
|
||||
disabled: Color,
|
||||
positive: Color = TangemColorPalette.Meadow,
|
||||
positiveDisabled: Color,
|
||||
positive: Color = TangemColorPalette.Azure,
|
||||
) {
|
||||
var primary by mutableStateOf(primary)
|
||||
private set
|
||||
|
|
@ -129,15 +129,12 @@ class TangemColors internal constructor(
|
|||
private set
|
||||
var positive by mutableStateOf(positive)
|
||||
private set
|
||||
var positiveDisabled by mutableStateOf(positiveDisabled)
|
||||
private set
|
||||
|
||||
fun update(other: Button) {
|
||||
primary = other.primary
|
||||
secondary = other.secondary
|
||||
disabled = other.disabled
|
||||
positive = other.positive
|
||||
positiveDisabled = other.positiveDisabled
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -146,9 +143,7 @@ class TangemColors internal constructor(
|
|||
primary: Color,
|
||||
secondary: Color,
|
||||
tertiary: Color,
|
||||
plain: Color,
|
||||
action: Color,
|
||||
fade: Color,
|
||||
) {
|
||||
var primary by mutableStateOf(primary)
|
||||
private set
|
||||
|
|
@ -156,20 +151,14 @@ class TangemColors internal constructor(
|
|||
private set
|
||||
var tertiary by mutableStateOf(tertiary)
|
||||
private set
|
||||
var plain by mutableStateOf(plain)
|
||||
private set
|
||||
var action by mutableStateOf(action)
|
||||
private set
|
||||
var fade by mutableStateOf(fade)
|
||||
private set
|
||||
|
||||
fun update(other: Background) {
|
||||
primary = other.primary
|
||||
secondary = other.secondary
|
||||
tertiary = other.tertiary
|
||||
plain = other.plain
|
||||
action = other.action
|
||||
fade = other.fade
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ private fun materialThemeColors(colors: TangemColors, isDark: Boolean): Colors {
|
|||
secondary = colors.button.primary,
|
||||
secondaryVariant = colors.text.accent,
|
||||
background = colors.background.primary,
|
||||
surface = colors.background.plain,
|
||||
surface = colors.background.secondary,
|
||||
error = colors.text.warning,
|
||||
onPrimary = colors.text.primary1,
|
||||
onSecondary = colors.text.primary1,
|
||||
|
|
@ -94,10 +94,10 @@ private fun lightThemeColors(): TangemColors {
|
|||
attention = TangemColorPalette.Tangerine,
|
||||
),
|
||||
icon = TangemColors.Icon(
|
||||
primary1 = TangemColorPalette.Black,
|
||||
primary1 = TangemColorPalette.Dark6,
|
||||
primary2 = TangemColorPalette.White,
|
||||
secondary = TangemColorPalette.Dark2,
|
||||
informative = TangemColorPalette.Light5,
|
||||
informative = TangemColorPalette.Dark1,
|
||||
inactive = TangemColorPalette.Light4,
|
||||
warning = TangemColorPalette.Amaranth,
|
||||
attention = TangemColorPalette.Tangerine,
|
||||
|
|
@ -106,15 +106,12 @@ private fun lightThemeColors(): TangemColors {
|
|||
primary = TangemColorPalette.Dark6,
|
||||
secondary = TangemColorPalette.Light2,
|
||||
disabled = TangemColorPalette.Light2,
|
||||
positiveDisabled = TangemColorPalette.MagicMint,
|
||||
),
|
||||
background = TangemColors.Background(
|
||||
primary = TangemColorPalette.White,
|
||||
secondary = TangemColorPalette.Light1,
|
||||
tertiary = TangemColorPalette.Light1,
|
||||
plain = TangemColorPalette.White,
|
||||
action = TangemColorPalette.White,
|
||||
fade = TangemColorPalette.White,
|
||||
),
|
||||
control = TangemColors.Control(
|
||||
checked = TangemColorPalette.Dark6,
|
||||
|
|
@ -123,7 +120,7 @@ private fun lightThemeColors(): TangemColors {
|
|||
),
|
||||
stroke = TangemColors.Stroke(
|
||||
primary = TangemColorPalette.Light2,
|
||||
secondary = TangemColorPalette.Dark4,
|
||||
secondary = TangemColorPalette.Dark5,
|
||||
transparency = TangemColorPalette.White,
|
||||
),
|
||||
field = TangemColors.Field(
|
||||
|
|
@ -149,25 +146,22 @@ private fun darkThemeColors(): TangemColors {
|
|||
icon = TangemColors.Icon(
|
||||
primary1 = TangemColorPalette.White,
|
||||
primary2 = TangemColorPalette.Dark6,
|
||||
secondary = TangemColorPalette.Dark1,
|
||||
informative = TangemColorPalette.Dark2,
|
||||
inactive = TangemColorPalette.Dark4,
|
||||
secondary = TangemColorPalette.Light5,
|
||||
informative = TangemColorPalette.Dark1,
|
||||
inactive = TangemColorPalette.Dark3,
|
||||
warning = TangemColorPalette.Flamingo,
|
||||
attention = TangemColorPalette.Mustard,
|
||||
),
|
||||
button = TangemColors.Button(
|
||||
primary = TangemColorPalette.Light4,
|
||||
primary = TangemColorPalette.Light2,
|
||||
secondary = TangemColorPalette.Dark4,
|
||||
disabled = TangemColorPalette.Dark5,
|
||||
positiveDisabled = TangemColorPalette.DarkGreen,
|
||||
),
|
||||
background = TangemColors.Background(
|
||||
primary = TangemColorPalette.Dark6,
|
||||
secondary = TangemColorPalette.Black,
|
||||
tertiary = TangemColorPalette.Dark6,
|
||||
plain = TangemColorPalette.Black,
|
||||
action = TangemColorPalette.Dark5,
|
||||
fade = TangemColorPalette.Black,
|
||||
),
|
||||
control = TangemColors.Control(
|
||||
checked = TangemColorPalette.Azure,
|
||||
|
|
@ -176,7 +170,7 @@ private fun darkThemeColors(): TangemColors {
|
|||
),
|
||||
stroke = TangemColors.Stroke(
|
||||
primary = TangemColorPalette.Dark4,
|
||||
secondary = TangemColorPalette.Dark1,
|
||||
secondary = TangemColorPalette.Dark4,
|
||||
transparency = TangemColorPalette.Dark6,
|
||||
),
|
||||
field = TangemColors.Field(
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ data class TangemTypography internal constructor(
|
|||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
letterSpacing = TextUnit(value = 0.5f, type = TextUnitType.Sp),
|
||||
lineHeight = TextUnit(value = 24f, type = TextUnitType.Sp),
|
||||
lineHeight = TextUnit(value = 20f, type = TextUnitType.Sp),
|
||||
),
|
||||
val body1: TextStyle = TextStyle(
|
||||
fontFamily = RobotoFamily,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ abstract class ComposeBottomSheetFragment : BottomSheetDialogFragment(), Compose
|
|||
if (expandedHeightFraction != null) it.fillMaxHeight(expandedHeightFraction!!) else it
|
||||
}
|
||||
.background(
|
||||
color = TangemTheme.colors.background.plain,
|
||||
color = TangemTheme.colors.background.primary,
|
||||
shape = TangemTheme.shapes.bottomSheet,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import java.util.Locale
|
|||
object BigDecimalFormatter {
|
||||
|
||||
const val EMPTY_BALANCE_SIGN = "—"
|
||||
const val CAN_BE_LOWER_SIGN = "<"
|
||||
|
||||
private const val TEMP_CURRENCY_CODE = "USD"
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.util.Locale
|
||||
|
||||
private const val TEXT_CHUNK_THOUSAND = 3
|
||||
private const val POINT_SEPARATOR = '.'
|
||||
|
||||
@Composable
|
||||
fun rememberDecimalFormat(): DecimalFormat {
|
||||
val locale = LocalConfiguration.current.locale
|
||||
val decimalSymbols = remember { DecimalFormatSymbols.getInstance(locale) }
|
||||
|
||||
return remember {
|
||||
DecimalFormat().apply {
|
||||
decimalFormatSymbols = decimalSymbols
|
||||
isParseBigDecimal = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats input [String] for InputField, to remove wrong symbols, letters etc
|
||||
* Use [decimals] for cut this number symbols after floating point
|
||||
*
|
||||
* Example (with 8 decimals):
|
||||
* input string - ab123.46377372ab53
|
||||
* result string 123.46377372
|
||||
*/
|
||||
fun DecimalFormat.getValidatedNumberWithFixedDecimals(text: String, decimals: Int): String {
|
||||
val thousandsSeparator = decimalFormatSymbols.groupingSeparator
|
||||
val decimalSeparator = decimalFormatSymbols.decimalSeparator
|
||||
|
||||
val lastChar = text.lastOrNull()
|
||||
val trimmedText = if (text.isNotEmpty() && (lastChar == thousandsSeparator || lastChar == POINT_SEPARATOR)) {
|
||||
text.dropLast(1) + decimalSeparator
|
||||
} else {
|
||||
text
|
||||
}
|
||||
|
||||
if (trimmedText.startsWith("0") && trimmedText.length > 1 && trimmedText[1] != decimalSeparator) {
|
||||
return "0"
|
||||
}
|
||||
|
||||
val filteredChars = trimmedText.replace(thousandsSeparator.toString(), "").filterIndexed { index, c ->
|
||||
val isOneOrZeroPoint =
|
||||
c == decimalSeparator && index != 0 && trimmedText.count { it == decimalSeparator } <= 1
|
||||
val isIndexPointIndex =
|
||||
c == decimalSeparator && index != 0 && trimmedText.indexOf(decimalSeparator) == index
|
||||
c.isDigit() || isIndexPointIndex || isOneOrZeroPoint
|
||||
}
|
||||
// If dot is present, take first digits before decimal and first decimals digits after decimal
|
||||
return if (filteredChars.count { it == decimalSeparator } == 1) {
|
||||
val beforeDecimal = filteredChars.substringBefore(decimalSeparator)
|
||||
val afterDecimal = filteredChars.substringAfter(decimalSeparator)
|
||||
beforeDecimal + decimalSeparator + afterDecimal.take(decimals)
|
||||
}
|
||||
// If there is no dot, just take all digits
|
||||
else {
|
||||
filteredChars
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats input [text] with grouping and decimal separators.
|
||||
* Takes into account [decimals] number of digits after floating point.
|
||||
*/
|
||||
fun DecimalFormat.formatWithThousands(text: String, decimals: Int): String {
|
||||
val thousandsSeparator = decimalFormatSymbols.groupingSeparator
|
||||
val decimalSeparator = decimalFormatSymbols.decimalSeparator
|
||||
val localizedText = text.replace("[,.]".toRegex(), decimalSeparator.toString())
|
||||
return if (localizedText.count { it == decimalSeparator } == 1) {
|
||||
val beforeDecimal = localizedText.substringBefore(decimalSeparator)
|
||||
.reversed()
|
||||
.chunked(TEXT_CHUNK_THOUSAND)
|
||||
.joinToString(thousandsSeparator.toString())
|
||||
.reversed()
|
||||
val afterDecimal = localizedText.substringAfter(decimalSeparator)
|
||||
beforeDecimal + decimalSeparator + afterDecimal.take(decimals)
|
||||
}
|
||||
// If there is no dot, just take all digits
|
||||
else {
|
||||
localizedText.reversed()
|
||||
.chunked(TEXT_CHUNK_THOUSAND)
|
||||
.joinToString(thousandsSeparator.toString())
|
||||
.reversed()
|
||||
}
|
||||
}
|
||||
|
||||
fun DecimalFormat.defaultFormat(): String {
|
||||
return "0${decimalFormatSymbols.decimalSeparator}00"
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if text input contains extra decimal separators.
|
||||
* If so, it will return false, otherwise true.
|
||||
*
|
||||
* Note: number can contain only one decimal separator.
|
||||
*/
|
||||
fun DecimalFormat.checkDecimalSeparatorDuplicate(text: String): Boolean {
|
||||
val regex = "[${decimalFormatSymbols.decimalSeparator}]".toRegex()
|
||||
val decimalSeparatorCount = regex.findAll(text).count()
|
||||
return decimalSeparatorCount <= 1 // only one decimal separator
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if text input contains grouping separators.
|
||||
* If so, it will return false, otherwise true.
|
||||
*
|
||||
* Note: grouping separators are used only for VisualTransformations.
|
||||
*/
|
||||
fun DecimalFormat.checkGroupingSeparator(text: String): Boolean {
|
||||
val regex = "[${decimalFormatSymbols.groupingSeparator}]".toRegex()
|
||||
val decimalSeparatorCount = regex.findAll(text).count()
|
||||
return decimalSeparatorCount == 0 // no grouping separator
|
||||
}
|
||||
|
||||
fun String.parseToBigDecimal(decimals: Int): BigDecimal {
|
||||
val decimalFormat = DecimalFormat().apply {
|
||||
decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault())
|
||||
isParseBigDecimal = true
|
||||
maximumFractionDigits = decimals
|
||||
minimumFractionDigits = decimals
|
||||
}
|
||||
return try {
|
||||
decimalFormat.parse(this) as? BigDecimal ?: BigDecimal.ZERO
|
||||
} catch (e: Exception) {
|
||||
BigDecimal.ZERO
|
||||
}
|
||||
}
|
||||
|
||||
fun BigDecimal.parseBigDecimal(decimals: Int, roundingMode: RoundingMode = RoundingMode.DOWN): String {
|
||||
val decimalFormat = DecimalFormat().apply {
|
||||
decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault())
|
||||
isParseBigDecimal = true
|
||||
isGroupingUsed = false
|
||||
maximumFractionDigits = decimals
|
||||
minimumFractionDigits = 0
|
||||
this.roundingMode = roundingMode
|
||||
}
|
||||
|
||||
return try {
|
||||
decimalFormat.format(this)
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.core.ui.utils
|
|||
|
||||
import java.text.DecimalFormat
|
||||
|
||||
@Deprecated("Deprecated due to unnecessary abstraction. Use methods from DecimalFormatterExt")
|
||||
class InputNumberFormatter(
|
||||
numberFormat: DecimalFormat,
|
||||
) {
|
||||
|
|
|
|||
10
core/ui/src/main/res/drawable/ic_aptos_22.xml
Normal file
10
core/ui/src/main/res/drawable/ic_aptos_22.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="22dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="22">
|
||||
<path
|
||||
android:pathData="M12.624,7.396C12.72,7.396 12.812,7.357 12.879,7.288L13.39,6.757C13.453,6.691 13.542,6.653 13.634,6.653H13.655C13.752,6.653 13.844,6.694 13.908,6.767L14.338,7.251C14.42,7.343 14.538,7.396 14.662,7.396H15.815C14.717,5.941 12.969,5 11,5C9.032,5 7.285,5.941 6.186,7.396H12.624ZM13.232,9.014H14.294V9.015H16.684C16.849,9.485 16.957,9.981 17,10.496H12.523C12.399,10.496 12.282,10.443 12.2,10.351L11.769,9.866C11.705,9.794 11.613,9.753 11.516,9.753H11.495C11.403,9.753 11.315,9.79 11.251,9.857L10.74,10.387C10.674,10.457 10.582,10.496 10.485,10.496H5C5.043,9.981 5.151,9.485 5.316,9.015H11.126C11.308,9.015 11.482,8.937 11.603,8.801L11.972,8.385C12.036,8.312 12.129,8.271 12.225,8.271C12.322,8.271 12.415,8.313 12.479,8.385L12.909,8.869C12.991,8.961 13.109,9.014 13.232,9.014ZM8.575,13.5C8.508,13.57 8.416,13.609 8.32,13.609V13.609H5.576C5.349,13.142 5.182,12.64 5.083,12.114H8.988C9.171,12.114 9.344,12.036 9.465,11.9L9.835,11.484C9.899,11.411 9.991,11.37 10.088,11.37C10.184,11.37 10.277,11.412 10.341,11.484L10.771,11.968C10.853,12.06 10.971,12.113 11.094,12.113H16.918C16.819,12.639 16.652,13.141 16.425,13.609H10.358C10.234,13.609 10.117,13.556 10.034,13.464L9.604,12.98C9.54,12.907 9.448,12.866 9.351,12.866H9.33C9.238,12.866 9.149,12.903 9.086,12.97L8.575,13.5ZM8.728,15.092H10.306V15.092H15.403C14.303,16.266 12.738,17 11,17C9.262,17 7.697,16.266 6.597,15.092H6.622C6.805,15.092 6.978,15.014 7.099,14.878L7.468,14.462C7.532,14.39 7.625,14.349 7.722,14.349C7.818,14.349 7.911,14.39 7.975,14.462L8.405,14.947C8.487,15.039 8.605,15.092 8.728,15.092Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd" />
|
||||
</vector>
|
||||
16
core/ui/src/main/res/drawable/img_aptos_22.xml
Normal file
16
core/ui/src/main/res/drawable/img_aptos_22.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="22dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="22">
|
||||
<group>
|
||||
<clip-path android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z" />
|
||||
<path
|
||||
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
|
||||
android:fillColor="#000000" />
|
||||
<path
|
||||
android:pathData="M12.624,7.396C12.72,7.396 12.812,7.357 12.879,7.288L13.39,6.757C13.453,6.691 13.542,6.653 13.634,6.653H13.655C13.752,6.653 13.844,6.694 13.908,6.767L14.338,7.251C14.42,7.343 14.538,7.396 14.662,7.396H15.815C14.717,5.941 12.969,5 11,5C9.032,5 7.285,5.941 6.186,7.396H12.624ZM13.232,9.014H14.294V9.015H16.684C16.849,9.485 16.957,9.981 17,10.496H12.523C12.399,10.496 12.282,10.443 12.2,10.351L11.769,9.866C11.705,9.794 11.613,9.753 11.516,9.753H11.495C11.403,9.753 11.315,9.79 11.251,9.857L10.74,10.387C10.674,10.457 10.582,10.496 10.485,10.496H5C5.043,9.981 5.151,9.485 5.316,9.015H11.126C11.308,9.015 11.482,8.937 11.603,8.801L11.972,8.385C12.036,8.312 12.129,8.271 12.225,8.271C12.322,8.271 12.415,8.313 12.479,8.385L12.909,8.869C12.991,8.961 13.109,9.014 13.232,9.014ZM8.575,13.5C8.508,13.57 8.416,13.609 8.32,13.609V13.609H5.576C5.349,13.142 5.182,12.64 5.083,12.114H8.988C9.171,12.114 9.344,12.036 9.465,11.9L9.835,11.484C9.899,11.411 9.991,11.37 10.088,11.37C10.184,11.37 10.277,11.412 10.341,11.484L10.771,11.968C10.853,12.06 10.971,12.113 11.094,12.113H16.918C16.819,12.639 16.652,13.141 16.425,13.609H10.358C10.234,13.609 10.117,13.556 10.034,13.464L9.604,12.98C9.54,12.907 9.448,12.866 9.351,12.866H9.33C9.238,12.866 9.149,12.903 9.086,12.97L8.575,13.5ZM8.728,15.092H10.306V15.092H15.403C14.303,16.266 12.738,17 11,17C9.262,17 7.697,16.266 6.597,15.092H6.622C6.805,15.092 6.978,15.014 7.099,14.878L7.468,14.462C7.532,14.39 7.625,14.349 7.722,14.349C7.818,14.349 7.911,14.39 7.975,14.462L8.405,14.947C8.487,15.039 8.605,15.092 8.728,15.092Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd" />
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -10,12 +10,16 @@ class PeriodicTask<T>(
|
|||
private val task: suspend () -> Result<T>,
|
||||
private val onSuccess: (T) -> Unit,
|
||||
private val onError: (Throwable) -> Unit,
|
||||
private val isDelayFirst: Boolean = false,
|
||||
) {
|
||||
|
||||
private var isActive: AtomicBoolean = AtomicBoolean(false)
|
||||
|
||||
suspend fun runTaskWithDelay() {
|
||||
isActive.set(true)
|
||||
if (isDelayFirst) {
|
||||
delay(delay)
|
||||
}
|
||||
while (isActive.get()) {
|
||||
task.invoke()
|
||||
.onSuccess {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory
|
||||
import com.tangem.data.tokens.utils.NetworkStatusFactory
|
||||
|
|
@ -7,6 +8,7 @@ import com.tangem.data.tokens.utils.ResponseCryptoCurrenciesFactory
|
|||
import com.tangem.datasource.local.network.NetworksStatusesStore
|
||||
import com.tangem.datasource.local.token.UserTokensStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
|
@ -68,6 +70,11 @@ internal class DefaultNetworksRepository(
|
|||
networksStatusesStore.getSyncOrNull(userWalletId).orEmpty()
|
||||
}
|
||||
|
||||
override fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean {
|
||||
val blockchain = Blockchain.fromNetworkId(network.id.value)
|
||||
return blockchain == Blockchain.Aptos
|
||||
}
|
||||
|
||||
private suspend fun fetchNetworksStatusesIfCacheExpired(
|
||||
userWalletId: UserWalletId,
|
||||
networks: Set<Network>,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.data.transaction
|
||||
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.transaction.FeeRepository
|
||||
|
||||
internal class DefaultFeeRepository : FeeRepository {
|
||||
override fun isFeeApproximate(networkId: Network.ID, amountType: AmountType): Boolean {
|
||||
val blockchain = Blockchain.fromId(networkId.value)
|
||||
return blockchain.isFeeApproximate(amountType)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,10 +7,7 @@ import com.tangem.blockchain.blockchains.stellar.StellarMemo
|
|||
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.ton.TonTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.TransactionExtras
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
|
|
@ -44,6 +41,21 @@ internal class DefaultTransactionRepository(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun sendTransaction(
|
||||
txData: TransactionData,
|
||||
signer: CommonSigner,
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
) = withContext(coroutineDispatcherProvider.io) {
|
||||
val blockchain = Blockchain.fromId(network.id.value)
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
)
|
||||
(walletManager as TransactionSender).send(txData, signer)
|
||||
}
|
||||
|
||||
private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? {
|
||||
val blockchain = Blockchain.fromId(networkId)
|
||||
if (memo == null) return null
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.data.transaction.di
|
||||
|
||||
import com.tangem.data.transaction.DefaultFeeRepository
|
||||
import com.tangem.data.transaction.DefaultTransactionRepository
|
||||
import com.tangem.domain.transaction.FeeRepository
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -25,4 +27,10 @@ internal object TransactionDataModule {
|
|||
coroutineDispatcherProvider = coroutineDispatcherProvider,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesFeeRepository(): FeeRepository {
|
||||
return DefaultFeeRepository()
|
||||
}
|
||||
}
|
||||
1
data/visa/.gitignore
vendored
Normal file
1
data/visa/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
21
data/visa/build.gradle.kts
Normal file
21
data/visa/build.gradle.kts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.data.visa"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Project - Domain */
|
||||
implementation(projects.domain.visa)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.core)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.data.visa
|
||||
|
||||
import com.tangem.domain.visa.model.VisaCurrency
|
||||
import com.tangem.domain.visa.repository.VisaRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
internal class DummyVisaRepository : VisaRepository {
|
||||
|
||||
override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency {
|
||||
TODO(reason = "Implement in [REDACTED_JIRA]")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.data.visa.di
|
||||
|
||||
import com.tangem.data.visa.DummyVisaRepository
|
||||
import com.tangem.domain.visa.repository.VisaRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object VisaDataModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVisaRepository(): VisaRepository {
|
||||
return DummyVisaRepository()
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,12 @@ interface CardTypesResolver {
|
|||
|
||||
fun isBadWallet(): Boolean
|
||||
|
||||
fun isJrWallet(): Boolean
|
||||
|
||||
fun isGrimWallet(): Boolean
|
||||
|
||||
fun isSatoshiFriendsWallet(): Boolean
|
||||
|
||||
fun isWhiteWallet(): Boolean
|
||||
|
||||
fun isWallet2(): Boolean
|
||||
|
|
|
|||
|
|
@ -42,6 +42,12 @@ internal class TangemCardTypesResolver(
|
|||
|
||||
override fun isBadWallet(): Boolean = card.batchId == BAD_WALLET_BATCH_ID
|
||||
|
||||
override fun isJrWallet(): Boolean = card.batchId == JR_WALLET_BATCH_ID
|
||||
|
||||
override fun isGrimWallet(): Boolean = card.batchId == GRIM_WALLET_BATCH_ID
|
||||
|
||||
override fun isSatoshiFriendsWallet(): Boolean = card.batchId == SATOSHI_WALLET_BATCH_ID
|
||||
|
||||
override fun isWhiteWallet(): Boolean {
|
||||
return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable
|
||||
}
|
||||
|
|
@ -66,7 +72,10 @@ internal class TangemCardTypesResolver(
|
|||
override fun isSingleWalletWithToken(): Boolean = walletData?.token != null && !isMultiwalletAllowed()
|
||||
|
||||
override fun isMultiwalletAllowed(): Boolean {
|
||||
return !isTangemTwins() && !card.isStart2Coin && !isTangemNote() &&
|
||||
return !isTangemTwins() &&
|
||||
!card.isStart2Coin &&
|
||||
!isTangemNote() &&
|
||||
!isVisaWallet() &&
|
||||
(multiWalletAvailable() || card.wallets.firstOrNull()?.curve == EllipticCurve.Secp256k1)
|
||||
}
|
||||
|
||||
|
|
@ -75,6 +84,7 @@ internal class TangemCardTypesResolver(
|
|||
override fun getBlockchain(): Blockchain {
|
||||
return when (productType) {
|
||||
ProductType.Start2Coin -> if (card.isTestCard) Blockchain.BitcoinTestnet else Blockchain.Bitcoin
|
||||
ProductType.Visa -> Blockchain.PolygonTestnet
|
||||
else -> {
|
||||
val blockchainName: String = walletData?.blockchain
|
||||
?: if (productType == ProductType.Note) {
|
||||
|
|
@ -138,6 +148,9 @@ internal class TangemCardTypesResolver(
|
|||
const val TRON_WALLET_BATCH_ID = "AF07"
|
||||
const val KASPA_WALLET_BATCH_ID = "AF08"
|
||||
const val BAD_WALLET_BATCH_ID = "AF09"
|
||||
const val JR_WALLET_BATCH_ID = "AF14"
|
||||
const val GRIM_WALLET_BATCH_ID = "AF13"
|
||||
const val SATOSHI_WALLET_BATCH_ID = "AF19"
|
||||
const val WHITE_WALLET2_BATCH_ID = "AF15"
|
||||
const val TRILLIANT_WALLET_BATCH_ID = "AF16"
|
||||
const val AVRORA_WALLET_BATCH_ID = "AF18"
|
||||
|
|
|
|||
|
|
@ -225,6 +225,7 @@ fun Blockchain.amountToCreateAccount(token: Token? = null): BigDecimal? {
|
|||
Blockchain.Stellar -> if (token?.symbol == NODL) BigDecimal(NODL_AMOUNT_TO_CREATE_ACCOUNT) else BigDecimal.ONE
|
||||
Blockchain.XRP -> BigDecimal.TEN
|
||||
Blockchain.Near, Blockchain.NearTestnet -> 0.00182.toBigDecimal()
|
||||
Blockchain.Aptos, Blockchain.AptosTestnet -> BigDecimal.ZERO
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,4 +13,9 @@ sealed interface LegacyAction : Action {
|
|||
* BackupAction.CheckForUnfinishedBackup, GlobalAction.Onboarding.StartForUnfinishedBackup
|
||||
*/
|
||||
data class StartOnboardingProcess(val scanResponse: ScanResponse, val canSkipBackup: Boolean = true) : LegacyAction
|
||||
|
||||
/**
|
||||
* Sending an email to support when sending transaction failed
|
||||
*/
|
||||
data class SendEmailTransactionFailed(val errorMessage: String) : LegacyAction
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.domain.tokens.model
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class Amount(
|
||||
val currencySymbol: String,
|
||||
val value: BigDecimal? = null,
|
||||
val decimals: Int,
|
||||
val type: AmountType = AmountType.CoinType,
|
||||
)
|
||||
|
||||
sealed class AmountType {
|
||||
object CoinType : AmountType()
|
||||
object ReserveType : AmountType()
|
||||
data class TokenType(val token: CryptoCurrency.Token) : AmountType()
|
||||
data class FiatType(val code: String) : AmountType()
|
||||
}
|
||||
|
||||
/** Converts `BigDecimal` [cryptoCurrency] to [Amount] */
|
||||
fun BigDecimal.convertToAmount(cryptoCurrency: CryptoCurrency) = Amount(
|
||||
currencySymbol = cryptoCurrency.symbol,
|
||||
value = this,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
type = when (cryptoCurrency) {
|
||||
is CryptoCurrency.Coin -> AmountType.CoinType
|
||||
is CryptoCurrency.Token -> AmountType.TokenType(
|
||||
token = cryptoCurrency,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -30,6 +30,8 @@ sealed class CryptoCurrencyWarning {
|
|||
val amountCurrency: CryptoCurrency,
|
||||
) : CryptoCurrencyWarning()
|
||||
|
||||
object TopUpWithoutReserve : CryptoCurrencyWarning()
|
||||
|
||||
/**
|
||||
* Represents wallet blockchain rent
|
||||
* @param rent Amount that will be charged in overtime if the blockchain does not have an amount greater than
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.FeePaidCurrency
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Use case for getting balance not enough warning to cover fee.
|
||||
*
|
||||
* This warning is shown when current currency is not paying fee and paying fee currency balance is not enough
|
||||
*
|
||||
* Current | Paying fee | Warning
|
||||
* Coin | Coin | -
|
||||
* Token | Coin | +
|
||||
* Coin | PToken | + (VTO - VTHO)
|
||||
* Token | PToken | + (Other VeChainToken - VTHO)
|
||||
* PToken | PToken | - (VTHO - VTHO or TerraToken - TerraToken)
|
||||
*/
|
||||
class GetBalanceNotEnoughForFeeWarningUseCase(
|
||||
private val currenciesRepository: CurrenciesRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
fee: BigDecimal,
|
||||
userWalletId: UserWalletId,
|
||||
tokenStatus: CryptoCurrencyStatus,
|
||||
coinStatus: CryptoCurrencyStatus,
|
||||
): Either<Throwable, CryptoCurrencyWarning?> = Either.catch {
|
||||
withContext(dispatchers.io) {
|
||||
val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, tokenStatus.currency)
|
||||
val coinBalance = coinStatus.value.amount ?: BigDecimal.ZERO
|
||||
|
||||
val isFeePaidByCoin = tokenStatus.currency is CryptoCurrency.Token
|
||||
val isFeePaidByToken =
|
||||
feePaidCurrency is FeePaidCurrency.Token && tokenStatus.currency.id != feePaidCurrency.tokenId
|
||||
|
||||
val warning = when {
|
||||
feePaidCurrency is FeePaidCurrency.Coin && isFeePaidByCoin && fee > coinBalance -> {
|
||||
CryptoCurrencyWarning.BalanceNotEnoughForFee(
|
||||
tokenCurrency = tokenStatus.currency,
|
||||
coinCurrency = coinStatus.currency,
|
||||
)
|
||||
}
|
||||
feePaidCurrency is FeePaidCurrency.Token && isFeePaidByToken && fee > feePaidCurrency.balance -> {
|
||||
constructTokenBalanceNotEnoughWarning(
|
||||
userWalletId = userWalletId,
|
||||
tokenStatus = tokenStatus,
|
||||
feePaidToken = feePaidCurrency,
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
warning
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if fee paying token [feePaidToken] is added to wallet [userWalletId]
|
||||
*/
|
||||
private suspend fun constructTokenBalanceNotEnoughWarning(
|
||||
userWalletId: UserWalletId,
|
||||
tokenStatus: CryptoCurrencyStatus,
|
||||
feePaidToken: FeePaidCurrency.Token,
|
||||
): CryptoCurrencyWarning {
|
||||
val token = currenciesRepository
|
||||
.getMultiCurrencyWalletCurrenciesSync(userWalletId)
|
||||
.find {
|
||||
it is CryptoCurrency.Token &&
|
||||
it.contractAddress.equals(feePaidToken.contractAddress, ignoreCase = true) &&
|
||||
it.network.derivationPath == tokenStatus.currency.network.derivationPath
|
||||
}
|
||||
return if (token != null) {
|
||||
CryptoCurrencyWarning.CustomTokenNotEnoughForFee(
|
||||
currency = tokenStatus.currency,
|
||||
feeCurrency = token,
|
||||
networkName = token.network.name,
|
||||
feeCurrencyName = feePaidToken.name,
|
||||
feeCurrencySymbol = feePaidToken.symbol,
|
||||
)
|
||||
} else {
|
||||
CryptoCurrencyWarning.CustomTokenNotEnoughForFee(
|
||||
currency = tokenStatus.currency,
|
||||
feeCurrency = null,
|
||||
networkName = tokenStatus.currency.network.name,
|
||||
feeCurrencyName = feePaidToken.name,
|
||||
feeCurrencySymbol = feePaidToken.symbol,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.tokens.error.TokenListError
|
||||
import com.tangem.domain.tokens.error.mapper.mapToTokenListError
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.error.mapper.mapToCurrencyError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
|
||||
|
|
@ -22,7 +22,7 @@ class GetCryptoCurrencyStatusSyncUseCase(
|
|||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencyId: CryptoCurrency.ID,
|
||||
): Either<TokenListError, CryptoCurrencyStatus> {
|
||||
): Either<CurrencyStatusError, CryptoCurrencyStatus> {
|
||||
val operations = CurrenciesStatusesOperations(
|
||||
userWalletId = userWalletId,
|
||||
currenciesRepository = currenciesRepository,
|
||||
|
|
@ -31,6 +31,18 @@ class GetCryptoCurrencyStatusSyncUseCase(
|
|||
)
|
||||
|
||||
return operations.getCurrencyStatusSync(cryptoCurrencyId)
|
||||
.mapLeft { error -> error.mapToTokenListError() }
|
||||
.mapLeft { error -> error.mapToCurrencyError() }
|
||||
}
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId): Either<CurrencyStatusError, CryptoCurrencyStatus> {
|
||||
val operations = CurrenciesStatusesOperations(
|
||||
userWalletId = userWalletId,
|
||||
currenciesRepository = currenciesRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
networksRepository = networksRepository,
|
||||
)
|
||||
|
||||
return operations.getPrimaryCurrencyStatusSync()
|
||||
.mapLeft { error -> error.mapToCurrencyError() }
|
||||
}
|
||||
}
|
||||
|
|
@ -254,10 +254,14 @@ class GetCurrencyWarningsUseCase(
|
|||
|
||||
private fun getNetworkNoAccountWarning(currencyStatus: CryptoCurrencyStatus): CryptoCurrencyWarning? {
|
||||
return (currencyStatus.value as? CryptoCurrencyStatus.NoAccount)?.let {
|
||||
CryptoCurrencyWarning.SomeNetworksNoAccount(
|
||||
amountToCreateAccount = it.amountToCreateAccount,
|
||||
amountCurrency = currencyStatus.currency,
|
||||
)
|
||||
if (networksRepository.isNeedToCreateAccountWithoutReserve(network = currencyStatus.currency.network)) {
|
||||
CryptoCurrencyWarning.TopUpWithoutReserve
|
||||
} else {
|
||||
CryptoCurrencyWarning.SomeNetworksNoAccount(
|
||||
amountToCreateAccount = it.amountToCreateAccount,
|
||||
amountCurrency = currencyStatus.currency,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.FeePaidCurrency
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Use case for checking if currency amount can be subtracted.
|
||||
* Amount can be subtracted if only it is paying fee
|
||||
*/
|
||||
class IsAmountSubtractAvailableUseCase(
|
||||
private val currenciesRepository: CurrenciesRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either<Throwable, Boolean> =
|
||||
Either.catch {
|
||||
withContext(dispatchers.io) {
|
||||
when (val feeCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, currency)) {
|
||||
is FeePaidCurrency.Coin -> currency is CryptoCurrency.Coin
|
||||
is FeePaidCurrency.SameCurrency -> true
|
||||
is FeePaidCurrency.Token -> currency.id == feeCurrency.tokenId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,43 +8,41 @@ import java.math.BigDecimal
|
|||
|
||||
sealed class TradeCryptoAction : Action {
|
||||
|
||||
data class SendCrypto(
|
||||
val currencyId: String,
|
||||
data class FinishSelling(val transactionId: String) : TradeCryptoAction()
|
||||
|
||||
data class Buy(
|
||||
val userWallet: UserWallet,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val appCurrencyCode: String,
|
||||
val checkUserLocation: Boolean = true,
|
||||
) : TradeCryptoAction()
|
||||
|
||||
data class Sell(
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val appCurrencyCode: String,
|
||||
) : TradeCryptoAction()
|
||||
|
||||
data class SendToken(
|
||||
val userWallet: UserWallet,
|
||||
val tokenCurrency: CryptoCurrency.Token,
|
||||
val tokenFiatRate: BigDecimal?,
|
||||
val coinFiatRate: BigDecimal?,
|
||||
val feeCurrencyStatus: CryptoCurrencyStatus?,
|
||||
val transactionInfo: TransactionInfo? = null,
|
||||
) : TradeCryptoAction()
|
||||
|
||||
data class SendCoin(
|
||||
val userWallet: UserWallet,
|
||||
val coinStatus: CryptoCurrencyStatus,
|
||||
val feeCurrencyStatus: CryptoCurrencyStatus?,
|
||||
val transactionInfo: TransactionInfo? = null,
|
||||
) : TradeCryptoAction()
|
||||
|
||||
data class Swap(val cryptoCurrency: CryptoCurrency) : TradeCryptoAction()
|
||||
|
||||
data class TransactionInfo(
|
||||
val amount: String,
|
||||
val destinationAddress: String,
|
||||
val transactionId: String,
|
||||
) : TradeCryptoAction()
|
||||
|
||||
data class FinishSelling(val transactionId: String) : TradeCryptoAction()
|
||||
|
||||
sealed class New : TradeCryptoAction() {
|
||||
|
||||
data class Buy(
|
||||
val userWallet: UserWallet,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val appCurrencyCode: String,
|
||||
val checkUserLocation: Boolean = true,
|
||||
) : New()
|
||||
|
||||
data class Sell(
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val appCurrencyCode: String,
|
||||
) : New()
|
||||
|
||||
data class SendToken(
|
||||
val userWallet: UserWallet,
|
||||
val tokenCurrency: CryptoCurrency.Token,
|
||||
val tokenFiatRate: BigDecimal?,
|
||||
val coinFiatRate: BigDecimal?,
|
||||
val feeCurrencyStatus: CryptoCurrencyStatus?,
|
||||
) : New()
|
||||
|
||||
data class SendCoin(
|
||||
val userWallet: UserWallet,
|
||||
val coinStatus: CryptoCurrencyStatus,
|
||||
val feeCurrencyStatus: CryptoCurrencyStatus?,
|
||||
) : New()
|
||||
|
||||
data class Swap(val cryptoCurrency: CryptoCurrency) : New()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -11,6 +11,8 @@ import com.tangem.domain.wallets.models.UserWalletId
|
|||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
// FIXME: Refactor - [REDACTED_JIRA]
|
||||
@Suppress("LargeClass")
|
||||
internal class CurrenciesStatusesOperations(
|
||||
private val currenciesRepository: CurrenciesRepository,
|
||||
private val quotesRepository: QuotesRepository,
|
||||
|
|
@ -107,6 +109,27 @@ internal class CurrenciesStatusesOperations(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun getPrimaryCurrencyStatusSync(): Either<Error, CryptoCurrencyStatus> = either {
|
||||
val currency = catch(
|
||||
block = { currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) },
|
||||
catch = { raise(Error.DataError(it)) },
|
||||
)
|
||||
val quotes = catch(
|
||||
block = { quotesRepository.getQuoteSync(currency.id).right() },
|
||||
catch = { Error.DataError(it).left() },
|
||||
)
|
||||
val networkStatus = catch(
|
||||
block = {
|
||||
networksRepository.getNetworkStatusesSync(userWalletId, setOf(currency.network))
|
||||
.firstOrNull { it.network == currency.network }
|
||||
.right()
|
||||
},
|
||||
catch = { Error.DataError(it).left() },
|
||||
)
|
||||
|
||||
return createCurrencyStatus(currency, quotes, networkStatus)
|
||||
}
|
||||
|
||||
fun getCardCurrenciesStatusesFlow(): Flow<Either<Error, List<CryptoCurrencyStatus>>> {
|
||||
return flow {
|
||||
val nonEmptyCurrencies = recover(
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ interface NetworksRepository {
|
|||
suspend fun getNetworkStatusesSync(
|
||||
userWalletId: UserWalletId,
|
||||
networks: Set<Network>,
|
||||
refresh: Boolean,
|
||||
refresh: Boolean = false,
|
||||
): Set<NetworkStatus>
|
||||
|
||||
fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean
|
||||
}
|
||||
|
|
@ -32,4 +32,6 @@ internal class MockNetworksRepository(
|
|||
): Set<NetworkStatus> {
|
||||
return getNetworkStatusesUpdates(userWalletId, networks).first()
|
||||
}
|
||||
|
||||
override fun isNeedToCreateAccountWithoutReserve(network: Network) = false
|
||||
}
|
||||
|
|
@ -13,9 +13,13 @@ dependencies {
|
|||
implementation(deps.arrow.core)
|
||||
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/** Tangem SDKs */
|
||||
implementation(deps.tangem.card.core)
|
||||
implementation(deps.tangem.card.android) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
implementation(deps.tangem.blockchain)
|
||||
|
||||
implementation(projects.domain.models)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.domain.transaction
|
||||
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
|
||||
interface FeeRepository {
|
||||
|
||||
/** Returns if fee is approximate for current [networkId] */
|
||||
fun isFeeApproximate(networkId: Network.ID, amountType: AmountType): Boolean
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.domain.transaction
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.CommonSigner
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
|
|
@ -17,4 +19,11 @@ interface TransactionRepository {
|
|||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): TransactionData?
|
||||
|
||||
suspend fun sendTransaction(
|
||||
txData: TransactionData,
|
||||
signer: CommonSigner,
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): SimpleResult
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.domain.transaction.error
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
sealed class SendTransactionError {
|
||||
|
||||
object DemoCardError : SendTransactionError()
|
||||
|
|
@ -8,9 +10,12 @@ sealed class SendTransactionError {
|
|||
|
||||
data class NetworkError(val message: String?) : SendTransactionError()
|
||||
|
||||
data class BlockchainSdkError(val code: Int, val cause: Throwable?) : SendTransactionError()
|
||||
data class BlockchainSdkError(val code: Int, val message: String?) : SendTransactionError()
|
||||
|
||||
object UserCancelledError : SendTransactionError()
|
||||
data class TangemSdkError(val code: Int, val cause: Throwable?) : SendTransactionError()
|
||||
|
||||
data class TangemSdkError(val code: Int, val messageReference: TextReference) : SendTransactionError()
|
||||
|
||||
data class UnknownError(val ex: Exception? = null) : SendTransactionError()
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -1,21 +1,15 @@
|
|||
package com.tangem.domain.transaction.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
|
|
@ -23,16 +17,15 @@ import java.math.BigDecimal
|
|||
*/
|
||||
class GetFeeUseCase(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val dispatcher: CoroutineDispatcherProvider,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
amount: BigDecimal,
|
||||
destination: String,
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): Flow<Either<GetFeeError, TransactionFee>> {
|
||||
return flow {
|
||||
try {
|
||||
) = either {
|
||||
catch(
|
||||
block = {
|
||||
val result = requireNotNull(
|
||||
walletManagersFacade.getFee(
|
||||
amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount),
|
||||
|
|
@ -43,14 +36,15 @@ class GetFeeUseCase(
|
|||
) { "Fee is null" }
|
||||
|
||||
val maybeFee = when (result) {
|
||||
is Result.Success -> result.data.right()
|
||||
is Result.Failure -> GetFeeError.DataError(result.error).left()
|
||||
is Result.Success -> result.data
|
||||
is Result.Failure -> raise(GetFeeError.DataError(result.error))
|
||||
}
|
||||
emit(maybeFee)
|
||||
} catch (e: Exception) {
|
||||
emit(GetFeeError.DataError(e.cause).left())
|
||||
}
|
||||
}.flowOn(dispatcher.io)
|
||||
maybeFee
|
||||
},
|
||||
catch = {
|
||||
raise(GetFeeError.DataError(it))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.domain.transaction.usecase
|
||||
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.transaction.FeeRepository
|
||||
|
||||
/**
|
||||
* Use case to check if fee is approximate
|
||||
*
|
||||
* @param feeRepository [FeeRepository]
|
||||
*/
|
||||
class IsFeeApproximateUseCase(
|
||||
private val feeRepository: FeeRepository,
|
||||
) {
|
||||
|
||||
operator fun invoke(networkId: Network.ID, amountType: AmountType): Boolean {
|
||||
return feeRepository.isFeeApproximate(networkId, amountType)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,19 +8,23 @@ import com.tangem.blockchain.common.TransactionData
|
|||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.blockchain.network.ResultChecker
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.transaction.R
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.domain.transaction.error.SendTransactionError.Companion.USER_CANCELLED_ERROR_CODE
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.sdk.extensions.localizedDescriptionRes
|
||||
|
||||
class SendTransactionUseCase(
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val transactionRepository: TransactionRepository,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
txData: TransactionData,
|
||||
|
|
@ -37,7 +41,7 @@ class SendTransactionUseCase(
|
|||
if (isDemoCardUseCase(cardId = userWallet.cardId)) {
|
||||
SendTransactionError.DemoCardError.left()
|
||||
} else {
|
||||
walletManagersFacade.sendTransaction(
|
||||
transactionRepository.sendTransaction(
|
||||
txData = txData,
|
||||
signer = signer,
|
||||
userWalletId = userWallet.walletId,
|
||||
|
|
@ -64,29 +68,28 @@ class SendTransactionUseCase(
|
|||
private fun handleError(result: SimpleResult.Failure): SendTransactionError {
|
||||
if (ResultChecker.isNetworkError(result)) return SendTransactionError.NetworkError(result.error.message)
|
||||
val error = result.error as? BlockchainSdkError ?: return SendTransactionError.UnknownError()
|
||||
when (error) {
|
||||
return when (error) {
|
||||
is BlockchainSdkError.WrappedTangemError -> {
|
||||
val errorByCode = mapErrorByCode(error)
|
||||
if (errorByCode != null) {
|
||||
return errorByCode
|
||||
if (error.code == USER_CANCELLED_ERROR_CODE) {
|
||||
SendTransactionError.UserCancelledError
|
||||
} else {
|
||||
val tangemError = error.tangemError
|
||||
if (tangemError is TangemSdkError) {
|
||||
val resource = tangemError.localizedDescriptionRes()
|
||||
val resId = resource.resId ?: R.string.common_unknown_error
|
||||
val resArgs = resource.args.map { it.value }
|
||||
val textReference = resourceReference(resId, wrappedList(resArgs))
|
||||
SendTransactionError.TangemSdkError(tangemError.code, textReference)
|
||||
} else {
|
||||
SendTransactionError.BlockchainSdkError(error.code, tangemError.customMessage)
|
||||
}
|
||||
}
|
||||
val tangemSdkError = error.tangemError as? TangemSdkError ?: return SendTransactionError.UnknownError()
|
||||
if (tangemSdkError is TangemSdkError.UserCancelled) return SendTransactionError.UserCancelledError
|
||||
return SendTransactionError.TangemSdkError(tangemSdkError.code, tangemSdkError.cause)
|
||||
}
|
||||
else -> {
|
||||
return SendTransactionError.TangemSdkError(error.code, error.cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapErrorByCode(error: BlockchainSdkError.WrappedTangemError): SendTransactionError? {
|
||||
return when (error.code) {
|
||||
USER_CANCELLED_ERROR_CODE -> {
|
||||
return SendTransactionError.UserCancelledError
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
SendTransactionError.BlockchainSdkError(
|
||||
code = error.code,
|
||||
message = error.customMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue