Updated on 2026-08-14

This commit is contained in:
Tangem 2020-12-19 22:06:30 +03:00
commit f2a766d0a1
88 changed files with 2600 additions and 310 deletions

View file

@ -3,7 +3,6 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.google.firebase.crashlytics'
//apply plugin: 'com.google.firebase.firebase-perf'
apply plugin: 'com.google.gms.google-services'
android {
@ -25,6 +24,7 @@ android {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"prod\"'
}
debug {
debuggable true
@ -34,6 +34,7 @@ android {
firebaseCrashlytics {
mappingFileUploadEnabled false
}
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"dev\"'
}
debug_beta {
initWith debug
@ -64,21 +65,21 @@ repositories {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin"
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.google.android.material:material:1.2.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
implementation 'com.tangem:blockchain:1.108.0'
implementation 'com.tangem:core:1.80.0'
implementation 'com.tangem:sdk:1.80.0'
implementation 'com.tangem:blockchain:1.123.0'
implementation 'com.tangem:core:1.90.0'
implementation 'com.tangem:sdk:1.90.0'
// WebView
implementation "androidx.browser:browser:1.2.0"
//lifecycle
implementation "androidx.lifecycle:lifecycle-runtime:2.2.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
@ -98,6 +99,7 @@ dependencies {
// Firebase
implementation platform('com.google.firebase:firebase-bom:26.0.0')
implementation 'com.google.firebase:firebase-crashlytics'
implementation 'com.google.firebase:firebase-config-ktx'
implementation 'com.google.firebase:firebase-analytics-ktx'
testImplementation 'junit:junit:4.13'

View file

@ -0,0 +1,18 @@
[
{
"name": "isWalletPayIdEnabled",
"value": true
},
{
"name": "isSendingToPayIdEnabled",
"value": true
},
{
"name": "isTopUpEnabled",
"value": true
},
{
"name": "isCreatingTwinCardsAllowed",
"value": true
}
]

View file

@ -0,0 +1,18 @@
[
{
"name": "isWalletPayIdEnabled",
"value": true
},
{
"name": "isSendingToPayIdEnabled",
"value": true
},
{
"name": "isTopUpEnabled",
"value": true
},
{
"name": "isCreatingTwinCardsAllowed",
"value": true
}
]

View file

@ -10,7 +10,7 @@ import com.tangem.CardFilter
import com.tangem.Config
import com.tangem.Log
import com.tangem.TangemSdk
import com.tangem.common.extensions.CardType
import com.tangem.commands.common.card.CardType
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.common.redux.navigation.AppScreen
@ -87,7 +87,8 @@ class MainActivity : AppCompatActivity() {
val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
if (tag != null) {
intent.action = null
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home, false))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatch(HomeAction.ReadCard)
}
}

View file

@ -1,10 +1,18 @@
package com.tangem.tap
import android.app.Application
import com.google.firebase.ktx.Firebase
import com.google.firebase.remoteconfig.ktx.remoteConfig
import com.google.firebase.remoteconfig.ktx.remoteConfigSettings
import com.tangem.tap.common.images.PicassoHelper
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.config.ConfigManager
import com.tangem.tap.domain.config.LocalLoader
import com.tangem.tap.domain.config.RemoteLoader
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.network.createMoshi
import com.tangem.tap.persistence.PreferencesStorage
import com.tangem.wallet.BuildConfig
import org.rekotlin.Store
@ -20,11 +28,31 @@ lateinit var preferencesStorage: PreferencesStorage
class TapApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
Firebase.remoteConfig.setConfigSettingsAsync(remoteConfigSettings {
this.minimumFetchIntervalInSeconds = 60
})
} else {
Firebase.remoteConfig.setConfigSettingsAsync(remoteConfigSettings {
this.minimumFetchIntervalInSeconds = 3600
})
}
NetworkConnectivity.createInstance(store, this)
preferencesStorage = PreferencesStorage(this)
PicassoHelper.initPicassoWithCaching(this)
loadConfigs()
}
private fun loadConfigs() {
val moshi = createMoshi()
val localLoader = LocalLoader(this, moshi)
val remoteLoader = RemoteLoader(moshi)
val configManager = ConfigManager(localLoader, remoteLoader)
configManager.load { store.dispatch(GlobalAction.SetConfigManager(configManager)) }
}
}

View file

@ -1,9 +0,0 @@
package com.tangem.tap
object TapConfig {
const val usePayId: Boolean = true
const val useTopUp: Boolean = true
const val coinMarketCapKey = "f6622117-c043-47a0-8975-9d673ce484de"
const val moonPayApiKey = "pk_live_YTDl4Uei4411FXLAFe2rn8R2qWSiQcm"
const val moonPayApiSecretKey = "sk_live_7wMjoyGTGAB3i1suQHszCWRRqJBRPsQF"
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.common.analytics
import com.tangem.commands.Card
import com.tangem.commands.common.card.Card
interface AnalyticsHandler {
fun triggerEvent(event: AnalyticsEvent, card: Card?)

View file

@ -4,7 +4,7 @@ import android.os.Bundle
import androidx.core.os.bundleOf
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import com.tangem.commands.Card
import com.tangem.commands.common.card.Card
object FirebaseAnalyticsHandler: AnalyticsHandler {
override fun triggerEvent(event: AnalyticsEvent, card: Card?) {
@ -17,7 +17,7 @@ object FirebaseAnalyticsHandler: AnalyticsHandler {
return bundleOf(
AnalyticsParam.BLOCKCHAIN.param to card.cardData?.blockchainName,
AnalyticsParam.BATCH_ID.param to card.cardData?.batchId,
AnalyticsParam.FIRMWARE.param to card.firmwareVersion
AnalyticsParam.FIRMWARE.param to card.firmwareVersion.version
)
}

View file

@ -7,21 +7,20 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.features.details.ui.DetailsConfirmFragment
import com.tangem.tap.features.details.ui.DetailsFragment
import com.tangem.tap.features.details.ui.DetailsSecurityFragment
import com.tangem.tap.features.details.ui.twins.CreateTwinWalletFragment
import com.tangem.tap.features.details.ui.twins.TwinWalletWarningFragment
import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment
import com.tangem.tap.features.home.HomeFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.wallet.ui.dialogs.TwinsOnboardingFragment
import com.tangem.wallet.R
fun FragmentActivity.openFragment(screen: AppScreen, addToBackStack: Boolean = true) {
val transaction = this.supportFragmentManager.beginTransaction()
.replace(
R.id.fragment_container,
fragmentFactory(screen),
screen.name
)
transaction.replace(R.id.fragment_container, fragmentFactory(screen), screen.name)
if (addToBackStack && screen != AppScreen.Home) transaction.addToBackStack(null)
transaction.commit();
transaction.commitAllowingStateLoss()
}
fun FragmentActivity.popBackTo(screen: AppScreen?, inclusive: Boolean = false) {
@ -44,5 +43,8 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.DetailsConfirm -> DetailsConfirmFragment()
AppScreen.DetailsSecurity -> DetailsSecurityFragment()
AppScreen.Disclaimer -> DisclaimerFragment()
AppScreen.CreateTwinWalletWarning -> TwinWalletWarningFragment()
AppScreen.CreateTwinWallet -> CreateTwinWalletFragment()
AppScreen.TwinsOnboarding -> TwinsOnboardingFragment()
}
}

View file

@ -13,6 +13,8 @@ import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
@ -28,6 +30,11 @@ fun Context.getDrawableCompat(@DrawableRes drawableResId: Int): Drawable? {
return ContextCompat.getDrawable(this, drawableResId)
}
@ColorInt
fun Fragment.getColor(@ColorRes colorRes: Int): Int {
return ContextCompat.getColor(requireContext(), colorRes)
}
fun View.getString(@StringRes id: Int): String {
return context.getString(id)
}
@ -57,22 +64,22 @@ fun View.makeInvisible() {
}
fun Context.dpToPixels(dp: Int): Int =
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics
).toInt()
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics
).toInt()
fun MaterialCardView.setMargins(
marginLeftDp: Int = 16,
marginTopDp: Int = 8,
marginRightDp: Int = 16,
marginBottomDp: Int = 8
marginLeftDp: Int = 16,
marginTopDp: Int = 8,
marginRightDp: Int = 16,
marginBottomDp: Int = 8
) {
val params = this.layoutParams
(params as ViewGroup.MarginLayoutParams).setMargins(
context.dpToPixels(marginLeftDp),
context.dpToPixels(marginTopDp),
context.dpToPixels(marginRightDp),
context.dpToPixels(marginBottomDp)
context.dpToPixels(marginLeftDp),
context.dpToPixels(marginTopDp),
context.dpToPixels(marginRightDp),
context.dpToPixels(marginBottomDp)
)
this.layoutParams = params
}
@ -82,31 +89,30 @@ fun Activity.setSystemBarTextColor(setTextDark: Boolean) {
val flags = this.window.decorView.systemUiVisibility
// Update the SystemUiVisibility dependening on whether we want a Light or Dark theme.
this.window.decorView.systemUiVisibility =
if (setTextDark) {
flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
} else {
flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
if (setTextDark) {
flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
} else {
flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
}
}
fun String.colorSegment(
context: Context,
color: Int,
startIndex: Int = 0,
endIndex: Int = this.length
context: Context,
color: Int,
startIndex: Int = 0,
endIndex: Int = this.length
): Spannable {
return this.toSpannable()
.also { spannable ->
spannable.setSpan(
ForegroundColorSpan(ContextCompat.getColor(context, color)),
startIndex,
endIndex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
.also { spannable ->
spannable.setSpan(
ForegroundColorSpan(ContextCompat.getColor(context, color)),
startIndex,
endIndex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
fun View.hideKeyboard() {
@ -122,7 +128,8 @@ fun Context.copyToClipboard(value: Any, label: String = "") {
}
fun Context.getFromClipboard(default: CharSequence? = null): CharSequence? {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager ?: return default
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
?: return default
val clipData = clipboard.primaryClip ?: return default
if (clipData.itemCount == 0) return default

View file

@ -1,5 +1,6 @@
package com.tangem.tap.common.redux.global
import com.tangem.tap.domain.config.ConfigManager
import com.tangem.tap.domain.tasks.ScanNoteResponse
import org.rekotlin.Action
import java.math.BigDecimal
@ -15,4 +16,5 @@ sealed class GlobalAction : Action {
data class Success(val appCurrency: FiatCurrencyName) : GlobalAction()
}
data class UpdateWalletSignedHashes(val walletSignedHashes: Int?) : GlobalAction()
data class SetConfigManager(val configManager: ConfigManager) : GlobalAction()
}

View file

@ -33,6 +33,9 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
globalState
}
}
is GlobalAction.SetConfigManager -> {
globalState.copy(configManager = action.configManager)
}
else -> globalState
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.commands.common.network.TangemService
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.domain.config.ConfigManager
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import org.rekotlin.StateType
@ -16,6 +17,7 @@ data class GlobalState(
val coinMarketCapService: CoinMarketCapService = CoinMarketCapService(),
val tangemService: TangemService = TangemService(),
val conversionRates: ConversionRates = ConversionRates(emptyMap()),
val configManager: ConfigManager? = null,
val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY
) : StateType

View file

@ -9,4 +9,7 @@ data class NavigationState(
val activity: WeakReference<FragmentActivity>? = null
) : StateType
enum class AppScreen { Home, Wallet, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer }
enum class AppScreen {
Home, Wallet, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer,
CreateTwinWalletWarning, CreateTwinWallet, TwinsOnboarding
}

View file

@ -3,8 +3,9 @@ package com.tangem.tap.domain
import androidx.activity.ComponentActivity
import com.tangem.*
import com.tangem.commands.*
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardType
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.CardType
import com.tangem.common.extensions.calculateSha256
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tap.common.analytics.AnalyticsEvent
@ -12,6 +13,7 @@ import com.tangem.tap.common.analytics.AnalyticsHandler
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.ScanNoteTask
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -64,8 +66,8 @@ class TangemSdkManager(val activity: ComponentActivity) {
), cardId, initialMessage = Message(activity.getString(R.string.initial_message_tap_header)))
}
private suspend fun <T : CommandResponse> runTaskAsync(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null
suspend fun <T : CommandResponse> runTaskAsync(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
): CompletionResult<T> =
withContext(Dispatchers.IO) {
suspendCoroutine { continuation ->
@ -76,9 +78,13 @@ class TangemSdkManager(val activity: ComponentActivity) {
}
private suspend fun <T : CommandResponse> runTaskAsyncReturnOnMain(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
): CompletionResult<T> {
val result = runTaskAsync(runnable, cardId, initialMessage)
return withContext(Dispatchers.Main) { result }
}
fun changeDisplayedCardIdNumbersCount(card: Card) {
tangemSdk.config.cardIdDisplayedNumbersCount = if (card.isTwinCard()) 4 else null
}
}

View file

@ -1,19 +1,23 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.commands.CardStatus
import com.tangem.commands.common.card.CardStatus
import com.tangem.commands.common.network.Result
import com.tangem.common.extensions.toHexString
import com.tangem.tap.TapConfig
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.config.ConfigManager
import com.tangem.tap.domain.extensions.amountToCreateAccount
import com.tangem.tap.domain.extensions.isNoAccountError
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.twins.TwinsHelper
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
@ -65,17 +69,13 @@ class TapWalletManager {
}
suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName) {
val wallet = store.state.globalState.scanNoteResponse?.walletManager?.wallet
val blockchainCurrency = wallet?.blockchain?.currency
val tokenCurrency = wallet?.token?.symbol
val wallet = store.state.globalState.scanNoteResponse?.walletManager?.wallet ?: return
val blockchainRate = blockchainCurrency?.let { coinMarketCapService.getRate(it, fiatCurrency) }
val tokenRate = tokenCurrency?.let { coinMarketCapService.getRate(it, fiatCurrency) }
val currencyList = wallet.getTokens().map { it.symbol }.toMutableList()
currencyList.add(wallet.blockchain.currency)
val results = mutableListOf<Pair<CryptoCurrencyName, Result<BigDecimal>?>>()
if (blockchainCurrency != null) results.add(blockchainCurrency to blockchainRate)
if (tokenCurrency != null) results.add(tokenCurrency to tokenRate)
currencyList.forEach { results.add(it to coinMarketCapService.getRate(it, fiatCurrency)) }
handleFiatRatesResult(results)
}
@ -84,9 +84,33 @@ class TapWalletManager {
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.CARD_IS_SCANNED, data.card)
}
TapWorkarounds.updateCard(data.card)
val configManager = store.state.globalState.configManager
if (TapWorkarounds.isStart2Coin) {
configManager?.turnOff(ConfigManager.isWalletPayIdEnabled)
configManager?.turnOff(ConfigManager.isSendingToPayIdEnabled)
configManager?.turnOff(ConfigManager.isTopUpEnabled)
} else if (data.walletManager?.wallet?.blockchain == Blockchain.Bitcoin
|| data.card.cardData?.blockchainName == Blockchain.Bitcoin.id){
configManager?.turnOff(ConfigManager.isWalletPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isTopUpEnabled)
} else {
configManager?.resetToDefault(ConfigManager.isWalletPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isTopUpEnabled)
}
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.ResetState)
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
if (data.card.isTwinCard()) {
val secondCardId = TwinsHelper.getTwinsCardId(data.card.cardId)
val cardNumber = TwinsHelper.getTwinCardNumber(data.card.cardId)
if (secondCardId != null && cardNumber != null) {
store.dispatch(WalletAction.TwinsAction.SetTwinCard(
secondCardId, cardNumber, isCreatingTwinCardsAllowed = true
))
}
}
loadData(data)
}
}
@ -100,14 +124,14 @@ class TapWalletManager {
store.dispatch(WalletAction.LoadData.Failure(TapError.NoInternetConnection))
return@withContext
}
val config = store.state.globalState.configManager?.config ?: return@withContext
store.dispatch(WalletAction.LoadWallet(
data.walletManager.wallet, data.verifyResponse?.artworkInfo?.id,
TapWorkarounds.isStart2Coin != true && TapConfig.useTopUp
))
data.walletManager.wallet, data.verifyResponse?.artworkInfo?.id, config.isTopUpEnabled))
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
store.dispatch(WalletAction.LoadFiatRate)
store.dispatch(WalletAction.LoadPayId)
} else if (data.card.status == CardStatus.Empty) {
} else if (data.card.status == CardStatus.Empty || data.card.isTwinCard()) {
store.dispatch(WalletAction.EmptyWallet)
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
} else {
@ -135,7 +159,8 @@ class TapWalletManager {
val error = result.error
val blockchain = walletManager.wallet.blockchain
if (error != null && blockchain.isNoAccountError(error)) {
val amountToCreateAccount = blockchain.amountToCreateAccount(walletManager.wallet.token)
val token = walletManager.wallet.getFirstToken()
val amountToCreateAccount = blockchain.amountToCreateAccount(token)
if (amountToCreateAccount != null) {
store.dispatch(WalletAction.LoadWallet.NoAccount(amountToCreateAccount.toString()))
return@withContext
@ -150,8 +175,8 @@ class TapWalletManager {
private suspend fun loadPayIdIfNeeded(): Result<String?>? {
val scanNoteResponse = store.state.globalState.scanNoteResponse
if (!TapConfig.usePayId ||
scanNoteResponse?.walletManager?.wallet?.blockchain?.isPayIdSupported() == false) {
if (store.state.globalState.configManager?.config?.isWalletPayIdEnabled == false
|| scanNoteResponse?.walletManager?.wallet?.blockchain?.isPayIdSupported() == false) {
return null
}
val cardId = scanNoteResponse?.card?.cardId
@ -166,11 +191,12 @@ class TapWalletManager {
withContext(Dispatchers.Main) {
when (result) {
is Result.Success -> {
val payId = result.data
if (TapWorkarounds.isPayIdEnabled() == false) {
val config = store.state.globalState.configManager?.config
if (config?.isWalletPayIdEnabled == false) {
store.dispatch(WalletAction.DisablePayId)
return@withContext
}
val payId = result.data
if (payId == null) {
store.dispatch(WalletAction.LoadPayId.NotCreated)
} else {
@ -198,4 +224,8 @@ class TapWalletManager {
}
}
}
}
fun Wallet.getFirstToken(): Token? {
return getTokens().toList().getOrNull(0)
}

View file

@ -1,6 +1,6 @@
package com.tangem.tap.domain
import com.tangem.commands.Card
import com.tangem.commands.common.card.Card
import java.util.*
object TapWorkarounds {
@ -12,10 +12,5 @@ object TapWorkarounds {
isStart2Coin = card.cardData?.issuerName?.toLowerCase(Locale.US) == START_2_COIN_ISSUER
}
fun isPayIdEnabled(): Boolean {
if (isStart2Coin) return false
return true
}
const val START_2_COIN_ISSUER = "start2coin"
}

View file

@ -1,7 +1,6 @@
package com.tangem.tap.domain
import android.net.Uri
import com.tangem.tap.TapConfig
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import org.spongycastle.util.encoders.Base64
import javax.crypto.Mac
@ -19,12 +18,12 @@ class TopUpHelper {
// private const val REDIRECT_URL_PATH = "&redirectUrl="
private const val SIGNATURE_PATH = "&signature="
fun getUrl(cryptoCurrencyName: CryptoCurrencyName, walletAddress: String): String {
val originalQuery = API_KEY_PATH + TapConfig.moonPayApiKey.urlEncode() +
fun getUrl(cryptoCurrencyName: CryptoCurrencyName, walletAddress: String, apiKey: String, secretKey: String): String {
val originalQuery = API_KEY_PATH + apiKey.urlEncode() +
CURRENCY_PATH + cryptoCurrencyName.urlEncode() +
WALLET_ADDRESS_PATH + walletAddress.urlEncode()
// REDIRECT_URL_PATH + REDIRECT_URL.urlEncode()
val signature = createSignature(originalQuery, TapConfig.moonPayApiSecretKey)
val signature = createSignature(originalQuery, secretKey)
return BASE_URL + originalQuery + SIGNATURE_PATH + signature.urlEncode()
}

View file

@ -0,0 +1,81 @@
package com.tangem.tap.domain.config
import android.content.Context
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.ktx.Firebase
import com.google.firebase.remoteconfig.ktx.remoteConfig
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.wallet.BuildConfig
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
interface ConfigLoader {
fun loadConfig(onComplete: (ConfigModel) -> Unit)
companion object {
const val featuresName = "features_${BuildConfig.CONFIG_ENVIRONMENT}"
const val configValuesName = "config_${BuildConfig.CONFIG_ENVIRONMENT}"
}
}
class LocalLoader(
private val context: Context,
private val moshi: Moshi
) : ConfigLoader {
override fun loadConfig(onComplete: (ConfigModel) -> Unit) {
val config = try {
val featureType = Types.newParameterizedType(List::class.java, FeatureModel::class.java)
val featureAdapter: JsonAdapter<List<FeatureModel>> = moshi.adapter(featureType)
val valuesType = Types.newParameterizedType(List::class.java, ConfigValueModel::class.java)
val valuesAdapter: JsonAdapter<List<ConfigValueModel>> = moshi.adapter(valuesType)
val jsonFeatures = readAssetAsString(ConfigLoader.featuresName)
val jsonConfigValues = readAssetAsString(ConfigLoader.configValuesName)
ConfigModel(featureAdapter.fromJson(jsonFeatures) ?: listOf(),
valuesAdapter.fromJson(jsonConfigValues) ?: listOf())
} catch (ex: Exception) {
Timber.e(ex)
ConfigModel.empty()
}
onComplete(config)
}
private fun readAssetAsString(fileName: String): String {
return context.assets.open("$fileName.json").bufferedReader().readText()
}
}
class RemoteLoader(
private val moshi: Moshi
) : ConfigLoader {
override fun loadConfig(onComplete: (ConfigModel) -> Unit) {
val emptyConfig = ConfigModel.empty()
val remoteConfig = Firebase.remoteConfig
remoteConfig.fetchAndActivate().addOnCompleteListener {
if (it.isSuccessful) {
val config = remoteConfig.getValue(ConfigLoader.featuresName)
val jsonConfig = config.asString()
if (jsonConfig.isEmpty()) {
onComplete(emptyConfig)
return@addOnCompleteListener
}
val featureType = Types.newParameterizedType(List::class.java, FeatureModel::class.java)
val featureAdapter: JsonAdapter<List<FeatureModel>> = moshi.adapter(featureType)
onComplete(ConfigModel(featureAdapter.fromJson(jsonConfig) ?: listOf(), listOf()))
} else {
onComplete(emptyConfig)
}
}.addOnFailureListener {
FirebaseCrashlytics.getInstance().recordException(it)
onComplete(emptyConfig)
}
}
}

View file

@ -0,0 +1,108 @@
package com.tangem.tap.domain.config
import com.tangem.tangem_sdk_new.ui.animation.VoidCallback
/**
[REDACTED_AUTHOR]
*/
data class Config(
val coinMarketCapKey: String = "f6622117-c043-47a0-8975-9d673ce484de",
val moonPayApiKey: String = "pk_test_kc90oYTANy7UQdBavDKGfL4K9l6VEPE",
val moonPayApiSecretKey: String = "sk_test_V8w4M19LbDjjYOt170s0tGuvXAgyEb1C",
val isWalletPayIdEnabled: Boolean = true,
val isSendingToPayIdEnabled: Boolean = true,
val isTopUpEnabled: Boolean = false,
val isCreatingTwinCardsAllowed: Boolean = false
)
class ConfigManager(
private val localLoader: ConfigLoader,
private val remoteLoader: ConfigLoader
) {
var config: Config = Config()
private set
private var defaultConfig = Config()
fun load(onComplete: VoidCallback? = null) {
localLoader.loadConfig { config ->
config.features?.forEach { setupFeature(it.name, it.value) }
config.configValues?.forEach { setupKey(it.name, it.value) }
onComplete?.invoke()
}
// remoteLoader.loadConfig { config ->
// config.features?.forEach { setupFeature(it.name, it.value) }
// onComplete?.invoke()
// }
}
fun turnOff(name: String) {
when (name) {
isWalletPayIdEnabled -> config = config.copy(isWalletPayIdEnabled = false)
isSendingToPayIdEnabled -> config = config.copy(isSendingToPayIdEnabled = false)
isTopUpEnabled -> config = config.copy(isTopUpEnabled = false)
isCreatingTwinCardsAllowed -> config = config.copy(isCreatingTwinCardsAllowed = false)
}
}
fun resetToDefault(name: String) {
when (name) {
isWalletPayIdEnabled -> config = config.copy(isWalletPayIdEnabled = defaultConfig.isWalletPayIdEnabled)
isSendingToPayIdEnabled -> config = config.copy(isSendingToPayIdEnabled = defaultConfig.isSendingToPayIdEnabled)
isTopUpEnabled -> config = config.copy(isTopUpEnabled = defaultConfig.isTopUpEnabled)
isCreatingTwinCardsAllowed -> config =
config.copy(isCreatingTwinCardsAllowed = defaultConfig.isCreatingTwinCardsAllowed)
}
}
private fun setupFeature(name: String, value: Boolean) {
val newValue = value ?: return
when (name) {
isWalletPayIdEnabled -> {
config = config.copy(isWalletPayIdEnabled = newValue)
defaultConfig = defaultConfig.copy(isWalletPayIdEnabled = newValue)
}
isSendingToPayIdEnabled -> {
config = config.copy(isSendingToPayIdEnabled = newValue)
defaultConfig = defaultConfig.copy(isSendingToPayIdEnabled = newValue)
}
isTopUpEnabled -> {
config = config.copy(isTopUpEnabled = newValue)
defaultConfig = defaultConfig.copy(isTopUpEnabled = newValue)
}
isCreatingTwinCardsAllowed -> {
config = config.copy(isCreatingTwinCardsAllowed = newValue)
defaultConfig = defaultConfig.copy(isCreatingTwinCardsAllowed = newValue)
}
}
}
private fun setupKey(name: String, value: String) {
when (name) {
coinMarketCapKey -> {
config = config.copy(coinMarketCapKey = value)
defaultConfig = defaultConfig.copy(coinMarketCapKey = value)
}
moonPayApiKey -> {
config = config.copy(moonPayApiKey = value)
defaultConfig = defaultConfig.copy(moonPayApiKey = value)
}
moonPayApiSecretKey -> {
config = config.copy(moonPayApiSecretKey = value)
defaultConfig = defaultConfig.copy(moonPayApiSecretKey = value)
}
}
}
companion object {
const val isWalletPayIdEnabled = "isWalletPayIdEnabled"
const val isSendingToPayIdEnabled = "isSendingToPayIdEnabled"
const val isCreatingTwinCardsAllowed = "isCreatingTwinCardsAllowed"
const val isTopUpEnabled = "isTopUpEnabled"
const val coinMarketCapKey = "coinMarketCapKey"
const val moonPayApiKey = "moonPayApiKey"
const val moonPayApiSecretKey = "moonPayApiSecretKey"
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.tap.domain.config
/**
[REDACTED_AUTHOR]
*/
interface BaseConfigModel<V> {
val name: String
val value: V?
}
class FeatureModel(
override val name: String,
override val value: Boolean
) : BaseConfigModel<Boolean>
class ConfigValueModel(
override val name: String,
override val value: String
) : BaseConfigModel<String>
class ConfigModel(val features: List<FeatureModel>?, val configValues: List<ConfigValueModel>?) {
companion object {
fun empty(): ConfigModel = ConfigModel(listOf(), listOf())
}
}

View file

@ -6,20 +6,25 @@ import com.tangem.TangemError
import com.tangem.TangemSdkError
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.commands.Card
import com.tangem.commands.CardStatus
import com.tangem.commands.CommandResponse
import com.tangem.commands.Product
import com.tangem.commands.ReadIssuerDataCommand
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardStatus
import com.tangem.commands.common.card.masks.Product
import com.tangem.commands.verifycard.VerifyCardCommand
import com.tangem.commands.verifycard.VerifyCardResponse
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.toHexString
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tasks.ScanTask
data class ScanNoteResponse(
val walletManager: WalletManager?,
val card: Card,
val verifyResponse: VerifyCardResponse? = null
val verifyResponse: VerifyCardResponse? = null,
val secondTwinPublicKey: String? = null
) : CommandResponse
class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteResponse> {
@ -42,31 +47,75 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
return@run
}
if (card.isTwinCard()) {
dealWithTwinCard(card, session, callback)
return@run
}
val walletManager = try {
WalletManagerFactory.makeWalletManager(card)
} catch (exception: Exception) {
return@run callback(CompletionResult.Success(ScanNoteResponse(null, card)))
}
VerifyCardCommand(true).run(session) { verifyResult ->
when (verifyResult) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(ScanNoteResponse(
walletManager, card, verifyResult.data
)))
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
}
}
}
verifyCard(walletManager, card, null, session, callback)
}
}
}
}
private fun verifyCard(
walletManager: WalletManager?, card: Card, publicKey: String? = null,
session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> Unit
) {
VerifyCardCommand(true).run(session) { verifyResult ->
when (verifyResult) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(ScanNoteResponse(
walletManager, card, verifyResult.data, publicKey
)))
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
}
}
}
}
private fun dealWithTwinCard(
card: Card, session: CardSession,
callback: (result: CompletionResult<ScanNoteResponse>) -> Unit
) {
ReadIssuerDataCommand().run(session) { readDataResult ->
when (readDataResult) {
is CompletionResult.Success -> {
val verified = TwinCardsManager.verifyTwinPublicKey(
readDataResult.data.issuerData, card.walletPublicKey
)
if (verified) {
val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65)
val walletManager = try {
WalletManagerFactory.makeMultisigWalletManager(card, twinPublicKey)
} catch (exception: Exception) {
callback(CompletionResult.Success(ScanNoteResponse(null, card)))
return@run
}
verifyCard(walletManager, card, twinPublicKey.toHexString(), session, callback)
return@run
} else {
callback(CompletionResult.Success(ScanNoteResponse(null, card)))
}
}
is CompletionResult.Failure ->
callback(CompletionResult.Success(ScanNoteResponse(null, card)))
}
}
}
private fun getErrorIfExcludedCard(card: Card): TangemError? {
if (card.cardData?.productMask != null &&
card.cardData?.productMask?.contains(Product.Note) != true) {
val productMask = card.cardData?.productMask
if (productMask != null && // product mask is on cards v2.30 and later
!productMask.contains(Product.Note) && !productMask.contains(Product.TwinCard)) {
return TapSdkError.CardForDifferentApp
}
if (excludedBatches.contains(card.cardData?.batchId)) {

View file

@ -0,0 +1,29 @@
package com.tangem.tap.domain.twins
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.commands.CreateWalletResponse
import com.tangem.commands.PurgeWalletCommand
import com.tangem.commands.common.card.CardStatus
import com.tangem.common.CompletionResult
import com.tangem.tasks.CreateWalletTask
class CreateFirstTwinWalletTask : CardSessionRunnable<CreateWalletResponse> {
override val requiresPin2 = false
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
if (session.environment.card?.walletPublicKey != null) {
PurgeWalletCommand().run(session) { response ->
when (response) {
is CompletionResult.Success -> {
session.environment.card = session.environment.card?.copy(status = CardStatus.Empty)
CreateWalletTask().run(session) { callback(it) }
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(response.error))
}
}
} else {
CreateWalletTask().run(session) { callback(it) }
}
}
}

View file

@ -0,0 +1,59 @@
package com.tangem.tap.domain.twins
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.Message
import com.tangem.commands.CreateWalletResponse
import com.tangem.commands.PurgeWalletCommand
import com.tangem.commands.common.card.CardStatus
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.hexToBytes
import com.tangem.tasks.CreateWalletTask
class CreateSecondTwinWalletTask(
private val firstPublicKey: String,
private val preparingMessage: Message,
private val creatingWalletMessage: Message
) : CardSessionRunnable<CreateWalletResponse> {
override val requiresPin2 = true
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
if (session.environment.card?.walletPublicKey != null) {
session.setInitialMessage(preparingMessage)
PurgeWalletCommand().run(session) { response ->
when (response) {
is CompletionResult.Success -> {
session.environment.card =
session.environment.card?.copy(status = CardStatus.Empty)
finishTask(session, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(response.error))
}
}
} else {
finishTask(session, callback)
}
}
private fun finishTask(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
session.setInitialMessage(creatingWalletMessage)
CreateWalletTask().run(session) { result ->
when (result) {
is CompletionResult.Success -> {
session.environment.card =
session.environment.card?.copy(status = CardStatus.Loaded)
WriteProtectedIssuerDataTask(
firstPublicKey.hexToBytes(), TwinCardsManager.issuerKeys
).run(session) { writeResult ->
when (writeResult) {
is CompletionResult.Success -> callback(result)
is CompletionResult.Failure ->
callback(CompletionResult.Failure(writeResult.error))
}
}
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.tap.domain.twins
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.KeyPair
import com.tangem.commands.ReadCommand
import com.tangem.common.CompletionResult
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.ScanNoteTask
class FinalizeTwinTask(
private val twinPublicKey: ByteArray, private val issuerKeys: KeyPair
) : CardSessionRunnable<ScanNoteResponse> {
override val requiresPin2 = true
override fun run(session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> Unit) {
WriteProtectedIssuerDataTask(twinPublicKey, issuerKeys).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
ReadCommand().run(session) { readResult ->
when (readResult) {
is CompletionResult.Success -> ScanNoteTask(readResult.data).run(session, callback)
is CompletionResult.Failure -> callback(CompletionResult.Failure(readResult.error))
}
}
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}

View file

@ -0,0 +1,83 @@
package com.tangem.tap.domain.twins
import com.tangem.KeyPair
import com.tangem.Message
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.crypto.CryptoUtils
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.tangemSdkManager
class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse) {
private val currentCardId: String = scanNoteResponse.card.cardId
private val secondCardId: String? = TwinsHelper.getTwinsCardId(currentCardId)
private var currentCardPublicKey: String? = null
private var secondCardPublicKey: String? = null
suspend fun createFirstWallet(message: Message): SimpleResult {
val response = tangemSdkManager.runTaskAsync(
CreateFirstTwinWalletTask(), currentCardId, message
)
when (response) {
is CompletionResult.Success -> {
currentCardPublicKey = response.data.walletPublicKey.toHexString()
return SimpleResult.Success
}
is CompletionResult.Failure -> return SimpleResult.failure(response.error)
}
}
suspend fun createSecondWallet(
initialMessage: Message,
preparingMessage: Message,
creatingWalletMessage: Message
): SimpleResult {
val response = tangemSdkManager.runTaskAsync(
CreateSecondTwinWalletTask(currentCardPublicKey!!, preparingMessage, creatingWalletMessage),
secondCardId, initialMessage
)
when (response) {
is CompletionResult.Success -> {
secondCardPublicKey = response.data.walletPublicKey.toHexString()
return SimpleResult.Success
}
is CompletionResult.Failure -> return SimpleResult.failure(response.error)
}
}
suspend fun complete(message: Message): Result<ScanNoteResponse> {
val response = tangemSdkManager.runTaskAsync(
FinalizeTwinTask(secondCardPublicKey!!.hexToBytes(), issuerKeys),
currentCardId, message
)
return when (response) {
is CompletionResult.Success -> Result.Success(response.data)
is CompletionResult.Failure -> Result.failure(response.error)
}
}
companion object {
val issuerKeys = KeyPair(
privateKey = "F9F4C50636C9E6FC65F92655BD5C21C85A5F6A34DCD0F1E75FCEA1980FE242F5".hexToBytes(),
publicKey = ("048196AA4B410AC44A3B9CCE18E7BE226AEA070ACC83A9CF67540F" +
"AC49AF25129F6A538A28AD6341358E3C4F9963064F" +
"7E365372A651D374E5C23CDD37FD099BF2").hexToBytes()
)
fun verifyTwinPublicKey(issuerData: ByteArray, cardWalletPublicKey: ByteArray?): Boolean {
if (issuerData.size < 65) return false
val publicKey = issuerData.sliceArray(0 until 65)
val signedKey = issuerData.sliceArray(65 until issuerData.size)
return (cardWalletPublicKey != null &&
CryptoUtils.verify(cardWalletPublicKey, publicKey, signedKey))
}
}
}

View file

@ -0,0 +1,84 @@
package com.tangem.tap.domain.twins
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.masks.Product
import com.tangem.tap.common.extensions.isEven
class TwinsHelper {
companion object {
const val TWIN_FILE_NAME = "TwinPublicKey"
fun getTwinCardNumber(cardId: String): TwinCardNumber? {
return when {
firstCardSeries.map { cardId.startsWith(it) }.contains(true) -> {
TwinCardNumber.First
}
secondCardSeries.map { cardId.startsWith(it) }.contains(true) -> {
TwinCardNumber.Second
}
else -> {
null
}
}
}
fun getTwinsCardId(cardId: String): String? {
val cardIdWithNewSeries = when (getTwinCardNumber(cardId) ?: return null) {
TwinCardNumber.First -> {
val index = if (cardId.startsWith(firstCardSeries[0])) 0 else 1
cardId.replace(firstCardSeries[index], secondCardSeries[index])
}
TwinCardNumber.Second -> {
val index = if (cardId.startsWith(secondCardSeries[0])) 0 else 1
cardId.replace(secondCardSeries[index], firstCardSeries[index])
}
}
val cardIdWithoutChecksum = cardIdWithNewSeries.dropLast(1)
val checkSum = cardIdWithoutChecksum.calculateLuhn()
return cardIdWithoutChecksum + checkSum
}
fun getTwinCardIdForUser(cardId: String): String {
if (cardId.length < 16) return cardId
val twinCardId = cardId.substring(11..14)
val twinCardNumber = getTwinCardNumber(cardId)?.number ?: 1
return "$twinCardId #$twinCardNumber"
}
private val firstCardSeries = listOf("CB61", "CB64")
private val secondCardSeries = listOf("CB62", "CB65")
}
}
private fun String.calculateLuhn(): Int {
val checksum = this.reversed()
.mapIndexed { index, c ->
val digit = if (c in '0'..'9') c - '0' else c - 'A'
if (!index.isEven()) {
digit
} else {
val newDigit = digit * 2
if (newDigit >= 10) newDigit - 9 else newDigit
}
}.sum()
.rem(10)
return (10 - checksum) % 10
}
enum class TwinCardNumber(val number: Int) {
First(1), Second(2);
fun pairNumber(): TwinCardNumber = when (this) {
First -> Second
Second -> First
}
}
fun Card.isTwinCard(): Boolean {
return this.cardData?.productMask?.contains(Product.TwinCard) == true
}
fun Card.getTwinCardIdForUser(): String {
return TwinsHelper.getTwinCardIdForUser(this.cardId)
}

View file

@ -0,0 +1,59 @@
package com.tangem.tap.domain.twins
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.KeyPair
import com.tangem.TangemSdkError
import com.tangem.commands.*
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.guard
import com.tangem.common.files.FileHashHelper
class WriteProtectedIssuerDataTask(
private val twinPublicKey: ByteArray, private val issuerKeys: KeyPair
) : CardSessionRunnable<WriteIssuerDataResponse> {
override val requiresPin2 = true
override fun run(session: CardSession, callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit) {
val hashes = arrayOf(twinPublicKey.calculateSha256())
SignCommand(hashes).run(session) { signResult ->
when (signResult) {
is CompletionResult.Success -> {
ReadIssuerDataCommand().run(session) { readResult ->
when (readResult) {
is CompletionResult.Success -> {
writeIssuerData(
twinPublicKey, issuerKeys, signResult.data.signature,
readResult.data, session, callback
)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(readResult.error))
}
}
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(signResult.error))
}
}
}
private fun writeIssuerData(
twinPublicKey: ByteArray, issuerKeys: KeyPair, cardSignature: ByteArray,
readResponse: ReadIssuerDataResponse,
session: CardSession, callback: (result: CompletionResult<WriteIssuerDataResponse>
) -> Unit) {
val cardId = session.environment.card?.cardId.guard {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
val counter = (readResponse.issuerDataCounter ?: 0) + 1
val data = twinPublicKey + cardSignature
val signedByIssuer = FileHashHelper.prepareHashes(
cardId, data, counter, issuerKeys.privateKey
)
WriteIssuerDataCommand(
data, signedByIssuer.finalizingSignature!!,
counter, issuerKeys.publicKey
).run(session, callback)
}
}

View file

@ -1,9 +1,13 @@
package com.tangem.tap.features.details.redux
import com.tangem.Message
import com.tangem.blockchain.common.Wallet
import com.tangem.commands.Card
import com.tangem.commands.common.card.Card
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import com.tangem.wallet.R
import org.rekotlin.Action
@ -12,10 +16,12 @@ sealed class DetailsAction : Action {
data class PrepareScreen(
val card: Card,
val scanNoteResponse: ScanNoteResponse,
val wallet: Wallet?,
val isCreatingTwinWalletAllowed: Boolean?,
val fiatCurrencyName: FiatCurrencyName,
val fiatCurrencies: List<FiatCurrencyName>? = null,
): DetailsAction()
) : DetailsAction()
object ShowDisclaimer : DetailsAction()
@ -23,24 +29,62 @@ sealed class DetailsAction : Action {
sealed class EraseWallet : DetailsAction() {
object Check : EraseWallet()
object Proceed : EraseWallet() {
object NotAllowedByCard: EraseWallet(), NotificationAction {
object NotAllowedByCard : EraseWallet(), NotificationAction {
override val messageResource = R.string.error_purge_prohibited
}
object NotEmpty: EraseWallet(), NotificationAction {
object NotEmpty : EraseWallet(), NotificationAction {
override val messageResource = R.string.details_notification_erase_wallet_not_possible
}
}
object Confirm : EraseWallet()
object Cancel : EraseWallet()
object Failure : EraseWallet()
object Success : EraseWallet()
}
sealed class CreateTwinWalletAction : DetailsAction() {
data class ShowWarning(
val twinCardNumber: TwinCardNumber?,
val createTwinWallet: CreateTwinWallet = CreateTwinWallet.RecreateWallet
) : CreateTwinWalletAction()
object NotEmpty : CreateTwinWalletAction(), NotificationAction {
override val messageResource = R.string.details_notification_erase_wallet_not_possible
}
object ShowAlert : CreateTwinWalletAction()
object HideAlert : CreateTwinWalletAction()
object Proceed: CreateTwinWalletAction()
object Cancel : CreateTwinWalletAction() {
object Confirm : CreateTwinWalletAction()
}
data class LaunchFirstStep(val message: Message) : CreateTwinWalletAction() {
object Success : CreateTwinWalletAction()
object Failure : CreateTwinWalletAction()
}
data class LaunchSecondStep(
val initialMessage: Message,
val preparingMessage: Message,
val creatingWalletMessage: Message,
) : CreateTwinWalletAction() {
object Success : CreateTwinWalletAction()
object Failure : CreateTwinWalletAction()
}
data class LaunchThirdStep(val message: Message) : CreateTwinWalletAction() {
data class Success(val scanNoteResponse: ScanNoteResponse) : CreateTwinWalletAction()
object Failure : CreateTwinWalletAction()
}
}
sealed class AppCurrencyAction : DetailsAction() {
data class SetCurrencies(val currencies: List<FiatCurrency>) : AppCurrencyAction()
object ChooseAppCurrency : AppCurrencyAction()
object Cancel: AppCurrencyAction()
data class SelectAppCurrency(val fiatCurrencyName: FiatCurrencyName): AppCurrencyAction()
object Cancel : AppCurrencyAction()
data class SelectAppCurrency(val fiatCurrencyName: FiatCurrencyName) : AppCurrencyAction()
}
sealed class ManageSecurity : DetailsAction() {
@ -50,8 +94,9 @@ sealed class DetailsAction : Action {
object Success : ManageSecurity()
object Failure : ManageSecurity()
}
data class ConfirmSelection(val option: SecurityOption) : ManageSecurity() {
object AlreadySet: ManageSecurity(), NotificationAction {
object AlreadySet : ManageSecurity(), NotificationAction {
override val messageResource = R.string.details_notification_security_option_already_active
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.twins.CreateTwinWalletMiddleware
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
@ -22,6 +23,7 @@ class DetailsMiddleware {
private val eraseWalletMiddleware = EraseWalletMiddleware()
private val appCurrencyMiddleware = AppCurrencyMiddleware()
private val manageSecurityMiddleware = ManageSecurityMiddleware()
private val twinWalletMiddleware = CreateTwinWalletMiddleware()
val detailsMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
@ -30,7 +32,8 @@ class DetailsMiddleware {
is DetailsAction.EraseWallet -> eraseWalletMiddleware.handle(action)
is DetailsAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action)
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action)
is DetailsAction.ShowDisclaimer -> {
is DetailsAction.CreateTwinWalletAction -> twinWalletMiddleware.handle(action)
is DetailsAction.ShowDisclaimer -> {
store.dispatch(DisclaimerAction.ShowAcceptedDisclaimer)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
}

View file

@ -1,10 +1,15 @@
package com.tangem.tap.features.details.redux
import com.tangem.commands.Card
import com.tangem.commands.Settings
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.masks.Settings
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TapWorkarounds
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.twins.TwinsHelper
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.features.details.redux.twins.CreateTwinWalletReducer
import com.tangem.tap.features.details.redux.twins.CreateTwinWalletState
import org.rekotlin.Action
import java.util.*
@ -31,6 +36,9 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
is DetailsAction.ManageSecurity -> {
handleSecurityAction(action, detailsState)
}
is DetailsAction.CreateTwinWalletAction -> {
CreateTwinWalletReducer.handle(action, detailsState)
}
else -> detailsState
}
}
@ -47,13 +55,25 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: Deta
SecurityOption.LongTap
}
}
val twinsState = if (action.card.isTwinCard()) {
CreateTwinWalletState(
scanResponse = action.scanNoteResponse,
twinCardNumber = TwinsHelper.getTwinCardNumber(action.card.cardId),
createTwinWallet = null,
showAlert = false,
allowRecreatingWallet = action.isCreatingTwinWalletAllowed
)
} else {
null
}
return DetailsState(
card = action.card, wallet = action.wallet,
cardInfo = action.card.toCardInfo(),
appCurrencyState = AppCurrencyState(
action.fiatCurrencyName
),
securityScreenState = SecurityScreenState(currentOption = securityOption)
securityScreenState = SecurityScreenState(currentOption = securityOption),
createTwinWalletState = twinsState
)
}
@ -169,6 +189,7 @@ private fun handleSecurityAction(
}
}
private fun Card.toCardInfo(): CardInfo? {
val cardId = this.cardId.chunked(4).joinToString(separator = " ")
val issuer = this.cardData?.issuerName ?: return null

View file

@ -1,10 +1,11 @@
package com.tangem.tap.features.details.redux
import com.tangem.blockchain.common.Wallet
import com.tangem.commands.Card
import com.tangem.commands.common.card.Card
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.features.details.redux.twins.CreateTwinWalletState
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import org.rekotlin.StateType
import java.util.*
@ -17,6 +18,7 @@ data class DetailsState(
val eraseWalletState: EraseWalletState? = null,
val confirmScreenState: ConfirmScreenState? = null,
val securityScreenState: SecurityScreenState? = null,
val createTwinWalletState: CreateTwinWalletState? = null
) : StateType
data class CardInfo(
@ -39,4 +41,4 @@ data class AppCurrencyState(
val fiatCurrencyName: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val showAppCurrencyDialog: Boolean = false,
val fiatCurrencies: List<FiatCurrency>? = null,
)
)

View file

@ -0,0 +1,119 @@
package com.tangem.tap.features.details.redux.twins
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class CreateTwinWalletMiddleware {
var twinsManager: TwinCardsManager? = null
fun handle(action: DetailsAction.CreateTwinWalletAction) {
when (action) {
is DetailsAction.CreateTwinWalletAction.ShowWarning -> {
val wallet = store.state.detailsState.wallet
if (wallet == null) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.CreateTwinWalletWarning))
return
}
val notEmpty = wallet.recentTransactions.isNotEmpty() || wallet.amounts.toSendableAmounts().isNotEmpty()
if (notEmpty) {
store.dispatch(DetailsAction.CreateTwinWalletAction.NotEmpty)
} else {
store.dispatch(NavigationAction.NavigateTo(AppScreen.CreateTwinWalletWarning))
}
}
is DetailsAction.CreateTwinWalletAction.Proceed ->
store.dispatch(NavigationAction.NavigateTo(AppScreen.CreateTwinWallet))
is DetailsAction.CreateTwinWalletAction.Cancel -> {
val step = store.state.detailsState.createTwinWalletState?.step
if (step != null && step != CreateTwinWalletStep.FirstStep
) {
store.dispatch(DetailsAction.CreateTwinWalletAction.ShowAlert)
} else {
twinsManager = null
store.dispatch(NavigationAction.PopBackTo())
}
}
is DetailsAction.CreateTwinWalletAction.Cancel.Confirm -> {
twinsManager = null
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
is DetailsAction.CreateTwinWalletAction.LaunchFirstStep -> {
store.state.globalState.scanNoteResponse?.let {
twinsManager = TwinCardsManager(it)
}
scope.launch {
val result = twinsManager?.createFirstWallet(action.message)
withContext(Dispatchers.Main) {
when (result) {
SimpleResult.Success ->
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchFirstStep.Success)
is SimpleResult.Failure ->
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchFirstStep.Failure)
}
}
}
}
DetailsAction.CreateTwinWalletAction.LaunchFirstStep.Success -> {
}
DetailsAction.CreateTwinWalletAction.LaunchFirstStep.Failure -> {
}
is DetailsAction.CreateTwinWalletAction.LaunchSecondStep ->
scope.launch {
val result = twinsManager?.createSecondWallet(action.initialMessage,
action.preparingMessage, action.creatingWalletMessage)
withContext(Dispatchers.Main) {
when (result) {
SimpleResult.Success ->
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchSecondStep.Success)
is SimpleResult.Failure ->
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchSecondStep.Failure)
}
}
}
DetailsAction.CreateTwinWalletAction.LaunchSecondStep.Success -> {
}
DetailsAction.CreateTwinWalletAction.LaunchSecondStep.Failure -> {
}
is DetailsAction.CreateTwinWalletAction.LaunchThirdStep -> {
scope.launch {
val result = twinsManager?.complete(action.message)
withContext(Dispatchers.Main) {
when (result) {
is Result.Success ->
store.dispatch(
DetailsAction.CreateTwinWalletAction
.LaunchThirdStep.Success(result.data)
)
is Result.Failure ->
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchThirdStep.Failure)
}
}
}
}
is DetailsAction.CreateTwinWalletAction.LaunchThirdStep.Success -> {
scope.launch {
store.state.globalState.tapWalletManager.onCardScanned(action.scanNoteResponse)
}
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
DetailsAction.CreateTwinWalletAction.LaunchThirdStep.Failure -> {
}
}
}
}

View file

@ -0,0 +1,68 @@
package com.tangem.tap.features.details.redux.twins
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
class CreateTwinWalletReducer {
companion object {
fun handle(
action: DetailsAction.CreateTwinWalletAction, state: DetailsState
): DetailsState {
return when (action) {
is DetailsAction.CreateTwinWalletAction.ShowWarning -> {
state.copy(createTwinWalletState = CreateTwinWalletState(
scanResponse = null,
twinCardNumber = action.twinCardNumber
?: state.createTwinWalletState?.twinCardNumber,
createTwinWallet = action.createTwinWallet,
showAlert = false,
allowRecreatingWallet = state.createTwinWalletState?.allowRecreatingWallet
))
}
DetailsAction.CreateTwinWalletAction.NotEmpty -> state
DetailsAction.CreateTwinWalletAction.ShowAlert -> {
state.copy(createTwinWalletState = state.createTwinWalletState?.copy(
showAlert = true
))
}
DetailsAction.CreateTwinWalletAction.HideAlert -> {
state.copy(createTwinWalletState = state.createTwinWalletState?.copy(
showAlert = false
))
}
is DetailsAction.CreateTwinWalletAction.Proceed -> {
state
}
DetailsAction.CreateTwinWalletAction.Cancel -> state
DetailsAction.CreateTwinWalletAction.Cancel.Confirm -> state
is DetailsAction.CreateTwinWalletAction.LaunchFirstStep -> state
DetailsAction.CreateTwinWalletAction.LaunchFirstStep.Success -> {
state.copy(createTwinWalletState = state.createTwinWalletState?.copy(
step = CreateTwinWalletStep.SecondStep
))
}
DetailsAction.CreateTwinWalletAction.LaunchFirstStep.Failure -> state
is DetailsAction.CreateTwinWalletAction.LaunchSecondStep -> state
DetailsAction.CreateTwinWalletAction.LaunchSecondStep.Success ->
state.copy(createTwinWalletState = state.createTwinWalletState?.copy(
step = CreateTwinWalletStep.ThirdStep
))
DetailsAction.CreateTwinWalletAction.LaunchSecondStep.Failure -> {
state
}
is DetailsAction.CreateTwinWalletAction.LaunchThirdStep -> {
state
}
is DetailsAction.CreateTwinWalletAction.LaunchThirdStep.Success -> {
state
}
DetailsAction.CreateTwinWalletAction.LaunchThirdStep.Failure -> {
state
}
}
}
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.tap.features.details.redux.twins
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.twins.TwinCardNumber
data class CreateTwinWalletState(
val scanResponse: ScanNoteResponse?,
val step: CreateTwinWalletStep = CreateTwinWalletStep.FirstStep,
val twinCardNumber: TwinCardNumber?,
val createTwinWallet: CreateTwinWallet?,
val showAlert: Boolean,
val allowRecreatingWallet: Boolean? = null
)
enum class CreateTwinWalletStep { FirstStep, SecondStep, ThirdStep }
enum class CreateTwinWallet { CreateWallet, RecreateWallet }

View file

@ -5,10 +5,15 @@ import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.twins.getTwinCardIdForUser
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details.*
@ -59,18 +64,44 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
if (state.cardInfo != null) {
tv_card_id.text = state.cardInfo.cardId
val cardId = if (state.card?.isTwinCard() == true) {
state.card.getTwinCardIdForUser()
} else {
state.cardInfo.cardId
}
tv_card_id.text = cardId
tv_issuer.text = state.cardInfo.issuer
tv_signed_hashes.text = state.cardInfo.signedHashes.toString()
}
tv_signed_hashes.show(state.card?.isTwinCard() != true)
tv_signed_hashes_title.show(state.card?.isTwinCard() != true)
tv_disclaimer.setOnClickListener { store.dispatch(DetailsAction.ShowDisclaimer) }
tv_erase_wallet.setOnClickListener {
store.dispatch(DetailsAction.EraseWallet.Check)
store.dispatch(DetailsAction.EraseWallet.Proceed)
if (state.createTwinWalletState != null) {
if (state.createTwinWalletState.allowRecreatingWallet == true) {
tv_erase_wallet.show()
tv_erase_wallet.text = getText(R.string.details_row_title_twins_recreate)
tv_erase_wallet.setOnClickListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.ShowWarning(
null, CreateTwinWallet.RecreateWallet
))
}
} else {
tv_erase_wallet.hide()
}
} else {
tv_erase_wallet.show()
tv_erase_wallet.text = getText(R.string.details_row_title_erase_wallet)
tv_erase_wallet.setOnClickListener {
store.dispatch(DetailsAction.EraseWallet.Check)
store.dispatch(DetailsAction.EraseWallet.Proceed)
}
}
tv_app_currency.text = state.appCurrencyState.fiatCurrencyName
tv_app_currency_title.setOnClickListener {

View file

@ -0,0 +1,171 @@
package com.tangem.tap.features.details.ui.twins
import android.app.Dialog
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.core.content.ContextCompat.getColor
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.squareup.picasso.Picasso
import com.tangem.Message
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.features.details.redux.twins.CreateTwinWalletStep
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details_twin_cards.*
import org.rekotlin.StoreSubscriber
class CreateTwinWalletFragment : Fragment(R.layout.fragment_details_twin_cards),
StoreSubscriber<DetailsState> {
private var dialog: Dialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(DetailsAction.CreateTwinWalletAction.Cancel)
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setNavigationOnClickListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.Cancel)
}
Picasso.get()
.load(Artwork.TWIN_CARD_1)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_card_1)
Picasso.get()
.load(Artwork.TWIN_CARD_2)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_card_2)
}
override fun newState(state: DetailsState) {
if (activity == null) return
toolbar.title = when (state.createTwinWalletState?.createTwinWallet) {
CreateTwinWallet.CreateWallet -> getText(R.string.wallet_button_create_wallet)
CreateTwinWallet.RecreateWallet, null -> getText(R.string.details_twins_recreate_toolbar)
}
val selectedColor = getColor(requireContext(), R.color.colorSecondary)
val defaultColor = getColor(requireContext(), R.color.blue_pale)
val twinCardNumber = state.createTwinWalletState?.twinCardNumber ?: TwinCardNumber.First
val cardNumber = when (state.createTwinWalletState?.step) {
CreateTwinWalletStep.FirstStep -> {
val twinCardNumberString = twinCardNumber.number.toString()
tv_step_number.text =
getString(R.string.details_twins_recreate_step_format, "1")
v_step_1.setBackgroundColor(selectedColor)
v_step_2.setBackgroundColor(defaultColor)
v_step_3.setBackgroundColor(defaultColor)
btn_tap.setOnClickListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchFirstStep(
Message(getString(
R.string.details_twins_recreate_title_format, twinCardNumber)
)))
}
btn_tap.text = getString(R.string.details_twins_recreate_button_format,
twinCardNumber.number.toString())
twinCardNumberString
}
CreateTwinWalletStep.SecondStep -> {
val twinCardNumberString = twinCardNumber.pairNumber().number.toString()
tv_step_number.text =
getString(R.string.details_twins_recreate_step_format, "2")
v_step_1.setBackgroundColor(selectedColor)
v_step_2.setBackgroundColor(selectedColor)
v_step_3.setBackgroundColor(defaultColor)
btn_tap.setOnClickListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchSecondStep(
Message(getString(R.string.details_twins_recreate_title_format, twinCardNumberString)),
Message(getString(R.string.details_twins_recreate_title_preparing)),
Message(getString(R.string.details_twins_recreate_title_creating_wallet)),
))
}
btn_tap.text = getString(R.string.details_twins_recreate_button_format, "2")
twinCardNumberString
}
CreateTwinWalletStep.ThirdStep -> {
val twinCardNumberString = twinCardNumber.number.toString()
tv_step_number.text =
getString(R.string.details_twins_recreate_step_format, "3")
v_step_1.setBackgroundColor(selectedColor)
v_step_2.setBackgroundColor(selectedColor)
v_step_3.setBackgroundColor(selectedColor)
btn_tap.setOnClickListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchThirdStep(
Message(getString(
R.string.details_twins_recreate_title_format, twinCardNumberString)
)
))
}
btn_tap.text = getString(R.string.details_twins_recreate_button_format, twinCardNumberString)
twinCardNumberString
}
else -> null
}
btn_tap.text = getString(R.string.details_twins_recreate_button_format, cardNumber)
tv_twin_title.text = getString(R.string.details_twins_recreate_title_format, cardNumber)
if (state.createTwinWalletState?.showAlert == true) {
if (dialog == null) {
dialog = MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.details_twins_recreate_alert)
.setPositiveButton(R.string.common_ok) { _, _ ->
store.dispatch(DetailsAction.CreateTwinWalletAction.Cancel.Confirm)
}
.setNegativeButton(R.string.common_cancel) { _, _ ->
dialog?.cancel()
}
.setOnCancelListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.HideAlert)
}
.create()
dialog?.show()
}
} else {
dialog?.cancel()
dialog = null
}
}
}

View file

@ -0,0 +1,60 @@
package com.tangem.tap.features.details.ui.twins
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.squareup.picasso.Picasso
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details_twin_cards_warning.*
import kotlinx.android.synthetic.main.layout_twin_cards_orange.*
class TwinWalletWarningFragment : Fragment(R.layout.fragment_details_twin_cards_warning) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val createTwinWallet = store.state.detailsState.createTwinWalletState?.createTwinWallet
if (createTwinWallet == CreateTwinWallet.CreateWallet) {
tv_twin_cards_description.text = getText(R.string.details_twins_recreate_subtitle)
} else if (createTwinWallet == CreateTwinWallet.RecreateWallet) {
tv_twin_cards_description.text = getText(R.string.details_twins_recreate_warning)
}
btn_cancel.setOnClickListener { store.dispatch(NavigationAction.PopBackTo()) }
btn_start.setOnClickListener {
store.dispatch(
DetailsAction.CreateTwinWalletAction.Proceed)
}
Picasso.get()
.load(Artwork.TWIN_CARD_1)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_twin_card_1)
Picasso.get()
.load(Artwork.TWIN_CARD_2)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_twin_card_2)
}
}

View file

@ -14,7 +14,16 @@ class DisclaimerMiddleware {
when (action) {
is DisclaimerAction.AcceptDisclaimer -> {
preferencesStorage.saveDisclaimerAccepted()
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
if (store.state.walletState.twinCardsState != null) {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding))
} else {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
} else {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
}
next(action)

View file

@ -25,7 +25,7 @@ class HomeMiddleware {
when (action) {
is HomeAction.CheckIfFirstLaunch -> {
store.dispatch(
HomeAction.CheckIfFirstLaunch.Result(preferencesStorage.isFirstLaunch())
HomeAction.CheckIfFirstLaunch.Result(preferencesStorage.getCountOfLaunches() == 1)
)
}
is HomeAction.ReadCard -> {
@ -34,6 +34,7 @@ class HomeMiddleware {
withContext(Dispatchers.Main) {
when (result) {
is CompletionResult.Success -> {
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card)
store.dispatch(GlobalAction.RestoreAppCurrency)
store.state.globalState.tapWalletManager.onCardScanned(result.data)
showDisclaimerOrNavigateToWallet()
@ -54,11 +55,18 @@ class HomeMiddleware {
}
private fun showDisclaimerOrNavigateToWallet() {
if (preferencesStorage.wasDisclaimerAccepted()) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
} else {
if (!preferencesStorage.wasDisclaimerAccepted()) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
return
}
if (store.state.walletState.twinCardsState != null) {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding))
return
}
}
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
companion object {

View file

@ -33,6 +33,7 @@ sealed class AddressPayIdActionUi : SendScreenActionUi {
object CheckAddressPayId : AddressPayIdActionUi()
data class SetTruncateHandler(val handler: (String) -> String) : AddressPayIdActionUi()
data class TruncateOrRestore(val truncate: Boolean) : AddressPayIdActionUi()
data class ChangePayIdState(val sendingToPayIdEnabled: Boolean): AddressPayIdActionUi()
}
sealed class AddressPayIdVerifyAction : SendScreenAction {

View file

@ -4,7 +4,6 @@ import com.tangem.blockchain.common.Wallet
import com.tangem.commands.common.network.Result
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.TapWorkarounds
import com.tangem.tap.domain.isPayIdSupported
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
@ -15,6 +14,7 @@ import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerifica
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification.SetPayIdWalletAddress
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -49,7 +49,7 @@ internal class AddressPayIdMiddleware {
private fun setAddressAndCheck(data: String, isUserInput: Boolean, dispatch: (Action) -> Unit) {
val potentialPayId = data.toLowerCase()
if (PayIdManager.isPayId(potentialPayId) && TapWorkarounds.isPayIdEnabled()) {
if (PayIdManager.isPayId(potentialPayId) && isPayIdEnabled()) {
dispatch(SetPayIdWalletAddress(potentialPayId, "", isUserInput))
} else {
dispatch(SetWalletAddress(data, isUserInput))
@ -63,7 +63,7 @@ internal class AddressPayIdMiddleware {
val addressPayId = sendState.addressPayIdState.normalFieldValue ?: return
val isUserInput = sendState.addressPayIdState.viewFieldValue.isFromUserInput
if (PayIdManager.isPayId(addressPayId) && TapWorkarounds.isPayIdEnabled()) {
if (PayIdManager.isPayId(addressPayId) && isPayIdEnabled()) {
verifyPayId(addressPayId, wallet, isUserInput, dispatch)
} else {
verifyAddress(addressPayId, wallet, isUserInput, dispatch)
@ -117,7 +117,7 @@ internal class AddressPayIdMiddleware {
private fun isValidBlockchainAddressAndNotTheSameAsWallet(wallet: Wallet, address: String): Error? {
return if (wallet.blockchain.validateAddress(address)) {
if (wallet.address != address) {
if (wallet.addresses.all { it.value != address } ) {
null
} else {
Error.ADDRESS_SAME_AS_WALLET
@ -152,10 +152,14 @@ internal class AddressPayIdMiddleware {
}
}
if (PayIdManager.isPayId(addressPayId) && TapWorkarounds.isPayIdEnabled()) {
if (PayIdManager.isPayId(addressPayId) && isPayIdEnabled()) {
verifyPayId(addressPayId, wallet, false, internalDispatcher)
} else {
verifyAddress(addressPayId, wallet, false, internalDispatcher)
}
}
private fun isPayIdEnabled(): Boolean {
return store.state.globalState.configManager?.config?.isSendingToPayIdEnabled ?: false
}
}

View file

@ -4,7 +4,7 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.Signer
import com.tangem.commands.Card
import com.tangem.commands.common.card.Card
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.stripZeroPlainString
@ -39,6 +39,7 @@ val sendMiddleware: Middleware<AppState> = { dispatch, appState ->
is RequestFee -> RequestFeeMiddleware().handle(appState(), dispatch)
is SendActionUi.SendAmountToRecipient ->
verifyAndSendTransaction(action, appState(), dispatch)
is PrepareSendScreen -> setIfSendingToPayIdEnabled(appState(), dispatch)
}
nextDispatch(action)
}
@ -185,3 +186,9 @@ fun createValidateTransactionError(errorList: EnumSet<TransactionError>, walletM
return TapError.ValidateTransactionErrors(tapErrors) { it.joinToString("\r\n") }
}
private fun setIfSendingToPayIdEnabled(appState: AppState?, dispatch: (Action) -> Unit) {
val isSendingToPayIdEnabled =
appState?.globalState?.configManager?.config?.isSendingToPayIdEnabled ?: false
dispatch(AddressPayIdActionUi.ChangePayIdState(isSendingToPayIdEnabled))
}

View file

@ -30,6 +30,7 @@ class AddressPayIdReducer : SendInternalReducer {
is AddressPayIdActionUi.PasteAddressPayId -> return sendState
is AddressPayIdActionUi.CheckClipboard -> return sendState
is AddressPayIdActionUi.CheckAddressPayId -> return sendState
is AddressPayIdActionUi.ChangePayIdState -> state.copy(sendingToPayIdEnabled = action.sendingToPayIdEnabled)
}
return updateLastState(sendState.copy(addressPayIdState = result), result)
}

View file

@ -4,6 +4,7 @@ import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Wallet
import com.tangem.tap.common.extensions.scaleToFiat
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.features.send.redux.ReceiptAction.RefreshReceipt
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.*
@ -179,7 +180,7 @@ class ReceiptReducer : SendInternalReducer {
return ReceiptSymbols(
fiat = store.state.globalState.appCurrency,
crypto = wallet.blockchain.currency,
token = wallet.amounts[AmountType.Token]?.currencySymbol
token = wallet.getFirstToken()?.symbol
)
}
@ -187,12 +188,12 @@ class ReceiptReducer : SendInternalReducer {
return when (mainCurrencyType) {
MainCurrencyType.FIAT -> when (amountType) {
AmountType.Coin -> ReceiptLayoutType.FIAT
AmountType.Token -> ReceiptLayoutType.TOKEN_FIAT
is AmountType.Token -> ReceiptLayoutType.TOKEN_FIAT
AmountType.Reserve -> ReceiptLayoutType.UNKNOWN
}
MainCurrencyType.CRYPTO -> when (amountType) {
AmountType.Coin -> ReceiptLayoutType.CRYPTO
AmountType.Token -> ReceiptLayoutType.TOKEN_CRYPTO
is AmountType.Token -> ReceiptLayoutType.TOKEN_CRYPTO
AmountType.Reserve -> ReceiptLayoutType.UNKNOWN
}
}

View file

@ -9,6 +9,7 @@ data class AddressPayIdState(
val recipientWalletAddress: String? = null,
val error: AddressPayIdVerifyAction.Error? = null,
val truncateHandler: ((String) -> String)? = null,
val sendingToPayIdEnabled: Boolean = false,
val pasteIsEnabled: Boolean = false
) : SendScreenState {

View file

@ -78,14 +78,14 @@ data class SendState(
fun convertFiatToExtractCrypto(fiatValue: BigDecimal): BigDecimal = when (amountState.typeOfAmount) {
AmountType.Coin -> convertFiatToCoin(fiatValue)
AmountType.Token -> convertFiatToToken(fiatValue)
is AmountType.Token -> convertFiatToToken(fiatValue)
AmountType.Reserve -> fiatValue
}
fun convertExtractCryptoToFiat(cryptoValue: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
return when (amountState.typeOfAmount) {
AmountType.Coin -> convertCoinToFiat(cryptoValue, scaleWithPrecision)
AmountType.Token -> convertTokenToFiat(cryptoValue, scaleWithPrecision)
is AmountType.Token -> convertTokenToFiat(cryptoValue, scaleWithPrecision)
AmountType.Reserve -> cryptoValue
}
}
@ -103,7 +103,7 @@ data class SendState(
fun mainCurrencyCanBeSwitched(): Boolean {
return when (amountState.typeOfAmount) {
AmountType.Coin -> coinIsConvertible()
AmountType.Token -> tokenIsConvertible()
is AmountType.Token -> tokenIsConvertible()
AmountType.Reserve -> false
}
}

View file

@ -240,7 +240,7 @@ class FeeUiHelper {
companion object {
fun feeToId(fee: FeeType): Int {
return when (fee) {
FeeType.SINGLE -> 0
FeeType.SINGLE -> View.NO_ID
FeeType.LOW -> R.id.chipLow
FeeType.NORMAL -> R.id.chipNormal
FeeType.PRIORITY -> R.id.chipPriority

View file

@ -3,6 +3,7 @@ package com.tangem.tap.features.send.ui.stateSubscribers
import android.app.Dialog
import android.content.Context
import android.text.SpannableStringBuilder
import android.view.View
import android.view.ViewGroup
import androidx.core.text.bold
import com.tangem.tap.common.extensions.*
@ -10,7 +11,6 @@ import com.tangem.tap.common.redux.getMessageString
import com.tangem.tap.common.text.DecimalDigitsInputFilter
import com.tangem.tap.common.toggleWidget.ProgressState
import com.tangem.tap.domain.MultiMessageError
import com.tangem.tap.domain.TapWorkarounds
import com.tangem.tap.domain.assembleErrors
import com.tangem.tap.features.send.BaseStoreFragment
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.Error
@ -104,7 +104,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
val til = fg.tilAddressOrPayId
val parsedError = parseError(til.context, state.error)
val hintResId = if (TapWorkarounds.isPayIdEnabled()) {
val hintResId = if (state.sendingToPayIdEnabled) {
R.string.send_destination_hint_address_payid
}else {
R.string.send_destination_hint_address
@ -201,7 +201,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
}
val chipId = FeeUiHelper.feeToId(state.selectedFeeType)
if (fg.chipGroup.checkedChipId != chipId && chipId != 0) fg.chipGroup.check(chipId)
if (fg.chipGroup.checkedChipId != chipId && chipId != View.NO_ID) fg.chipGroup.check(chipId)
}
private fun handleReceiptState(fg: BaseStoreFragment, state: ReceiptState) {

View file

@ -3,11 +3,13 @@ package com.tangem.tap.features.wallet.redux
import android.content.Context
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Wallet
import com.tangem.commands.Card
import com.tangem.blockchain.common.address.AddressType
import com.tangem.commands.common.card.Card
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.wallet.R
import org.rekotlin.Action
import java.math.BigDecimal
@ -91,4 +93,15 @@ sealed class WalletAction : Action {
sealed class TopUpAction : WalletAction() {
data class TopUp(val context: Context, val toolbarColor: Int) : TopUpAction()
}
data class ChangeSelectedAddress(val type: AddressType) : WalletAction()
sealed class TwinsAction : WalletAction() {
object ShowOnboarding : TwinsAction()
object SetOnboardingShown : TwinsAction()
data class SetTwinCard(
val secondCardId: String, val number: TwinCardNumber,
val isCreatingTwinCardsAllowed: Boolean
) : TwinsAction()
}
}

View file

@ -6,10 +6,10 @@ import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.commands.Card
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardType
import com.tangem.commands.common.network.Result
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.CardType
import com.tangem.common.extensions.getType
import com.tangem.common.extensions.toHexString
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
@ -21,6 +21,10 @@ import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.TopUpHelper
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.twins.TwinsHelper
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.network.NetworkConnectivity
@ -60,15 +64,25 @@ class WalletMiddleware {
}
}
is WalletAction.CreateWallet -> {
scope.launch {
val result = tangemSdkManager.createWallet(
store.state.globalState.scanNoteResponse?.card?.cardId
)
when (result) {
is CompletionResult.Success -> {
store.state.globalState.tapWalletManager.onCardScanned(result.data)
}
if (store.state.walletState.twinCardsState != null) {
store.dispatch(DetailsAction.CreateTwinWalletAction.ShowWarning(
store.state.globalState.scanNoteResponse?.card?.cardId?.let {
TwinsHelper.getTwinCardNumber(it)
},
CreateTwinWallet.CreateWallet
))
} else {
scope.launch {
val result = tangemSdkManager.createWallet(
store.state.globalState.scanNoteResponse?.card?.cardId
)
when (result) {
is CompletionResult.Success -> {
store.state.globalState.tapWalletManager
.onCardScanned(result.data)
}
}
}
}
}
@ -111,8 +125,15 @@ class WalletMiddleware {
val result = tangemSdkManager.scanNote(FirebaseAnalyticsHandler)
when (result) {
is CompletionResult.Success -> {
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card)
store.state.globalState.tapWalletManager
.onCardScanned(result.data, true)
if (store.state.walletState.twinCardsState != null) {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding))
}
}
}
}
}
@ -133,13 +154,13 @@ class WalletMiddleware {
}
}
is WalletAction.CopyAddress -> {
store.state.walletState.addressData?.address?.let {
store.state.walletState.walletAddresses?.selectedAddress?.address?.let {
action.context.copyToClipboard(it)
store.dispatch(WalletAction.CopyAddress.Success)
}
}
is WalletAction.ExploreAddress -> {
val uri = Uri.parse(store.state.walletState.addressData?.exploreUrl)
val uri = Uri.parse(store.state.walletState.walletAddresses?.selectedAddress?.exploreUrl)
val intent = Intent(Intent.ACTION_VIEW, uri)
ContextCompat.startActivity(action.context, intent, null)
}
@ -164,6 +185,13 @@ class WalletMiddleware {
val cardId = store.state.globalState.scanNoteResponse?.card?.cardId
cardId?.let { preferencesStorage.saveScannedCardId(it) }
}
is WalletAction.TwinsAction.SetTwinCard -> {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) store.dispatch(WalletAction.TwinsAction.ShowOnboarding)
}
is WalletAction.TwinsAction.SetOnboardingShown -> {
preferencesStorage.saveTwinsOnboardingShown()
}
}
next(action)
}
@ -185,7 +213,7 @@ class WalletMiddleware {
private fun prepareSendAction(amount: Amount?): Action {
return if (amount != null) {
if (amount.type == AmountType.Token) {
if (amount.type is AmountType.Token) {
PrepareSendScreen(store.state.walletState.wallet?.amounts?.get(AmountType.Coin), amount)
} else {
PrepareSendScreen(amount)
@ -208,6 +236,7 @@ class WalletMiddleware {
if (card.getType() != CardType.Release) {
return WarningType.DevCard
}
if (card.isTwinCard()) return null
return if (signatureCountValidator == null) {
if (card.walletSignedHashes ?: 0 > 0) {
@ -229,6 +258,8 @@ class WalletMiddleware {
val card = store.state.globalState.scanNoteResponse?.card
if (card == null || preferencesStorage.wasCardScannedBefore(card.cardId)) return
if (card.isTwinCard()) return
val validator = store.state.globalState.scanNoteResponse?.walletManager
as? SignatureCountValidator
scope.launch {
@ -255,9 +286,13 @@ private class TopUpMiddleware {
fun handle(action: WalletAction.TopUpAction) {
when (action) {
is WalletAction.TopUpAction.TopUp -> {
val config = store.state.globalState.configManager?.config ?: return
val defaultAddress = store.state.walletState.walletAddresses!!.list[0].address
val url = TopUpHelper.getUrl(
store.state.walletState.currencyData.currencySymbol!!,
store.state.walletState.addressData!!.address
defaultAddress,
config.moonPayApiKey,
config.moonPayApiSecretKey
)
val customTabsIntent = CustomTabsIntent.Builder()
.setToolbarColor(action.toolbarColor)

View file

@ -11,6 +11,8 @@ import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.common.extensions.toQrCode
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.wallet.models.removeUnknownTransactions
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.ui.BalanceStatus
@ -33,29 +35,29 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
when (action) {
is WalletAction.ResetState -> newState = WalletState()
is WalletAction.EmptyWallet -> newState = newState.copy(
state = ProgressState.Done,
currencyData = BalanceWidgetData(BalanceStatus.EmptyCard),
mainButton = WalletMainButton.CreateWalletButton(true),
topUpState = TopUpState(false)
)
is WalletAction.EmptyWallet -> {
val creatingWalletAllowed = !(newState.twinCardsState != null &&
newState.twinCardsState?.isCreatingTwinCardsAllowed != true)
newState = newState.copy(
state = ProgressState.Done,
currencyData = BalanceWidgetData(BalanceStatus.EmptyCard),
mainButton = WalletMainButton.CreateWalletButton(creatingWalletAllowed),
topUpState = TopUpState(false)
)
}
is WalletAction.LoadData.Failure -> {
when (action.error) {
is TapError.NoInternetConnection -> {
val wallet = state.globalState.scanNoteResponse?.walletManager?.wallet
val addressData = if (wallet == null) {
null
} else {
AddressData(wallet.address, wallet.shareUrl, wallet.exploreUrl)
}
newState = newState.copy(
state = ProgressState.Error,
error = ErrorType.NoInternetConnection,
addressData = addressData,
walletAddresses = createAddressList(wallet, newState.walletAddresses),
currencyData = BalanceWidgetData(
status = BalanceStatus.Unreachable,
currency = wallet?.blockchain?.fullName,
token = wallet?.token?.symbol?.let {
token = wallet?.getFirstToken()?.symbol?.let {
TokenData("", tokenSymbol = it)
}),
mainButton = WalletMainButton.SendButton(false),
@ -86,11 +88,11 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
BalanceStatus.Loading,
wallet.blockchain.fullName,
currencySymbol = wallet.blockchain.currency,
token = wallet.token?.symbol?.let {
token = wallet.getFirstToken()?.symbol?.let {
TokenData("", tokenSymbol = it)
}
),
addressData = AddressData(wallet.address, wallet.shareUrl, wallet.exploreUrl),
walletAddresses = createAddressList(wallet, newState.walletAddresses),
mainButton = WalletMainButton.SendButton(false),
topUpState = TopUpState(allowed = action.allowTopUp)
)
@ -143,9 +145,9 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
} else {
newState.currencyData.fiatAmount
}
val tokenFiatAmount = if (currency == newState.wallet?.token?.symbol) {
newState.wallet?.amounts?.get(AmountType.Token)?.value
?.toFiatString(rate, state.globalState.appCurrency)
val token = newState.wallet?.getFirstToken()
val tokenFiatAmount = if (currency == token?.symbol) {
newState.wallet?.getTokenAmount(token)?.value?.toFiatString(rate, state.globalState.appCurrency)
} else {
newState.currencyData.token?.fiatAmount
}
@ -163,6 +165,12 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
Artwork.SERGIO_CARD_URL
} else if (action.card.cardId.startsWith(Artwork.MARTA_CARD_ID)) {
Artwork.MARTA_CARD_URL
} else if (newState.twinCardsState?.cardNumber != null) {
when (newState.twinCardsState?.cardNumber) {
TwinCardNumber.First -> Artwork.TWIN_CARD_1
TwinCardNumber.Second -> Artwork.TWIN_CARD_2
null -> Artwork.DEFAULT_IMG_URL
}
} else {
Artwork.DEFAULT_IMG_URL
}
@ -171,8 +179,8 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
is WalletAction.ShowQrCode -> {
newState = newState.copy(
walletDialog = WalletDialog.QrDialog(
newState.addressData?.shareUrl?.toQrCode(),
newState.addressData?.shareUrl,
newState.walletAddresses?.selectedAddress?.shareUrl?.toQrCode(),
newState.walletAddresses?.selectedAddress?.shareUrl,
newState.currencyData.currency
)
)
@ -216,10 +224,64 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
is WalletAction.TopUpAction -> {
newState = newState.copy(topUpState = handleTopUpActions(action, newState.topUpState))
}
is WalletAction.ChangeSelectedAddress -> {
val walletAddresses = newState.walletAddresses ?: return newState
val address = walletAddresses.list.firstOrNull { it.type == action.type }
?: return newState
newState = newState.copy(walletAddresses = WalletAddresses(address, walletAddresses.list))
}
is WalletAction.TwinsAction.SetTwinCard -> {
newState = newState.copy(
twinCardsState = TwinCardsState(
secondCardId = action.secondCardId,
cardNumber = action.number,
showTwinOnboarding = newState.twinCardsState?.showTwinOnboarding
?: false,
isCreatingTwinCardsAllowed = action.isCreatingTwinCardsAllowed
)
)
}
is WalletAction.TwinsAction.ShowOnboarding -> {
newState = newState.copy(
twinCardsState = newState.twinCardsState?.copy(showTwinOnboarding = true)
?: TwinCardsState(null, null,
showTwinOnboarding = true,
isCreatingTwinCardsAllowed = false)
)
}
is WalletAction.TwinsAction.SetOnboardingShown -> {
newState = newState.copy(
twinCardsState = newState.twinCardsState?.copy(showTwinOnboarding = false)
)
}
}
return newState
}
fun createAddressList(wallet: Wallet?, walletAddresses: WalletAddresses? = null): WalletAddresses? {
if (wallet == null) return null
val listOfAddressData = mutableListOf<AddressData>()
// put a defaultAddress at the first place
wallet.addresses.forEach {
val addressData = AddressData(it.value, it.type, wallet.getShareUri(it.value), wallet.getExploreUrl(it.value))
if (it.type == wallet.blockchain.defaultAddressType()) {
listOfAddressData.add(0, addressData)
} else {
listOfAddressData.add(addressData)
}
}
// restore a selected wallet address
var indexOfSelectedWallet = 0
walletAddresses?.let {
val index = listOfAddressData.indexOfFirst { it.address == walletAddresses.selectedAddress.address }
if (index != -1) indexOfSelectedWallet = index
}
return WalletAddresses(listOfAddressData[indexOfSelectedWallet], listOfAddressData)
}
private fun handleTopUpActions(action: WalletAction.TopUpAction, state: TopUpState): TopUpState {
return when (action) {
is WalletAction.TopUpAction.TopUp -> state
@ -230,13 +292,17 @@ private fun onWalletLoaded(
wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null
): WalletState {
val fiatCurrencySymbol = store.state.globalState.appCurrency
val token = wallet.amounts[AmountType.Token]
val token = wallet.getFirstToken()
val tokenData = if (token != null) {
val tokenFiatRate = store.state.globalState.conversionRates.getRate(token.currencySymbol)
val tokenFiatAmount = tokenFiatRate?.let { token.value?.toFiatString(it, fiatCurrencySymbol) }
TokenData(
token.value?.toFormattedString(token.decimals) ?: "",
token.currencySymbol, tokenFiatAmount)
val tokenAmount = wallet.getTokenAmount(token)
if (tokenAmount != null) {
val tokenFiatRate = store.state.globalState.conversionRates.getRate(tokenAmount.currencySymbol)
val tokenFiatAmount = tokenFiatRate?.let { tokenAmount.value?.toFiatString(it, fiatCurrencySymbol) }
TokenData(tokenAmount.value?.toFormattedString(tokenAmount.decimals) ?: "",
tokenAmount.currencySymbol, tokenFiatAmount)
} else {
null
}
} else {
null
}

View file

@ -2,9 +2,12 @@ package com.tangem.tap.features.wallet.redux
import android.graphics.Bitmap
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.address.AddressType
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import org.rekotlin.StateType
@ -16,17 +19,24 @@ data class WalletState(
val wallet: Wallet? = null,
val pendingTransactions: List<PendingTransaction> = emptyList(),
val hashesCountVerified: Boolean? = null,
val addressData: AddressData? = null,
val walletAddresses: WalletAddresses? = null,
val currencyData: BalanceWidgetData = BalanceWidgetData(),
val payIdData: PayIdData = PayIdData(),
val walletDialog: WalletDialog? = null,
val updatingWallet: Boolean = false,
val mainButton: WalletMainButton = WalletMainButton.SendButton(false),
val topUpState: TopUpState = TopUpState()
val topUpState: TopUpState = TopUpState(),
val twinCardsState: TwinCardsState? = null,
) : StateType {
val showDetails: Boolean =
currencyData.status != com.tangem.tap.features.wallet.ui.BalanceStatus.EmptyCard &&
currencyData.status != com.tangem.tap.features.wallet.ui.BalanceStatus.UnknownBlockchain
val showSegwitAddress: Boolean
get() {
val listOfAddresses = walletAddresses?.list ?: return false
return wallet?.blockchain == Blockchain.Bitcoin && listOfAddresses.size > 1
}
}
sealed class WalletDialog {
@ -37,6 +47,7 @@ sealed class WalletDialog {
data class CreatePayIdDialog(val creatingPayIdState: CreatingPayIdState?) : WalletDialog()
data class SelectAmountToSendDialog(val amounts: List<Amount>?) : WalletDialog()
data class WarningDialog(val type: WarningType) : WalletDialog()
data class TwinsOnboardingFragment(val secondCardId: String): WalletDialog()
}
enum class WarningType { CardSignedHashesBefore, DevCard }
@ -60,8 +71,14 @@ sealed class WalletMainButton(enabled: Boolean) : Button(enabled) {
class CreateWalletButton(enabled: Boolean) : WalletMainButton(enabled)
}
data class WalletAddresses(
val selectedAddress: AddressData,
val list: List<AddressData>
)
data class AddressData(
val address: String,
val type: AddressType,
val shareUrl: String,
val exploreUrl: String
)
@ -76,6 +93,8 @@ data class Artwork(
const val MARTA_CARD_URL = "https://app.tangem.com/cards/card_tg083.png"
const val SERGIO_CARD_ID = "BC01"
const val MARTA_CARD_ID = "BC02"
const val TWIN_CARD_1 = "https://app.tangem.com/cards/card_tg085.png"
const val TWIN_CARD_2 = "https://app.tangem.com/cards/card_tg086.png"
}
}
@ -83,4 +102,11 @@ data class TopUpState(
val allowed: Boolean = true,
val url: String? = null,
val redirectUrl: String? = null
)
data class TwinCardsState(
val secondCardId: String?,
val cardNumber: TwinCardNumber?,
val showTwinOnboarding: Boolean,
val isCreatingTwinCardsAllowed: Boolean
)

View file

@ -40,6 +40,7 @@ data class TokenData(
class BalanceWidget(
val fragment: Fragment,
val data: BalanceWidgetData,
val isTwinCard: Boolean,
) {
fun setup() {
@ -100,8 +101,13 @@ class BalanceWidget(
BalanceStatus.EmptyCard -> {
fragment.l_balance.hide()
fragment.l_balance_error.show()
fragment.tv_error_title.text = fragment.getText(R.string.wallet_error_empty_card)
fragment.tv_error_descriptions.text = fragment.getText(R.string.wallet_error_empty_card_subtitle)
if (isTwinCard) {
fragment.tv_error_title.text = fragment.getText(R.string.wallet_error_empty_twin_card)
fragment.tv_error_descriptions.text = fragment.getText(R.string.wallet_error_empty_twin_card_subtitle)
} else {
fragment.tv_error_title.text = fragment.getText(R.string.wallet_error_empty_card)
fragment.tv_error_descriptions.text = fragment.getText(R.string.wallet_error_empty_card_subtitle)
}
}
BalanceStatus.NoAccount -> {
fragment.l_balance.hide()

View file

@ -2,11 +2,8 @@ package com.tangem.tap.features.wallet.ui
import android.app.Dialog
import android.os.Bundle
import android.view.Menu
import android.view.*
import android.view.Menu.NONE
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
@ -14,10 +11,14 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import com.google.android.material.snackbar.Snackbar
import com.squareup.picasso.Picasso
import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressType
import com.tangem.blockchain.common.address.AddressType
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog
@ -29,6 +30,7 @@ import com.tangem.wallet.R
import kotlinx.android.synthetic.main.card_balance.*
import kotlinx.android.synthetic.main.fragment_wallet.*
import kotlinx.android.synthetic.main.layout_address.*
import kotlinx.android.synthetic.main.layout_send_fee.*
import kotlinx.android.synthetic.main.layout_wallet_long_buttons.*
import kotlinx.android.synthetic.main.layout_wallet_short_buttons.*
import org.rekotlin.StoreSubscriber
@ -90,6 +92,20 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
override fun newState(state: WalletState) {
if (activity == null) return
state.twinCardsState?.cardNumber?.let { cardNumber ->
tv_twin_card_number.show()
iv_twin_card.show()
val number = when (cardNumber) {
TwinCardNumber.First -> "1"
TwinCardNumber.Second -> "2"
}
tv_twin_card_number.text = getString(R.string.wallet_twins_chip_format, number)
}
if (state.twinCardsState?.cardNumber == null) {
tv_twin_card_number.hide()
iv_twin_card.hide()
}
if (!state.showDetails) {
toolbar.menu.removeItem(R.id.details_menu)
} else if (toolbar.menu.findItem(R.id.details_menu) == null) {
@ -116,7 +132,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
handleDialogs(state.walletDialog)
l_balance.show()
BalanceWidget(this, state.currencyData).setup()
BalanceWidget(this, state.currencyData, state.twinCardsState != null).setup()
if (state.state == ProgressState.Done) {
srl_wallet.isRefreshing = false
@ -187,7 +203,13 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
val buttonTitle = when (state.mainButton) {
is WalletMainButton.SendButton -> R.string.wallet_button_send
is WalletMainButton.CreateWalletButton -> R.string.wallet_button_create_wallet
is WalletMainButton.CreateWalletButton -> {
if (state.twinCardsState == null) {
R.string.wallet_button_create_wallet
} else {
R.string.wallet_button_create_twin_wallet
}
}
}
btnConfirm.text = getString(buttonTitle)
btnConfirm.isEnabled = state.mainButton.enabled
@ -206,9 +228,30 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
private fun setupAddressCard(state: WalletState) {
if (state.addressData != null) {
if (state.walletAddresses != null) {
l_address?.show()
tv_address.text = state.addressData.address
val tvAddressPaddingTop = tv_address.resources.getDimension(R.dimen.dimen16).toInt()
if (state.showSegwitAddress) {
(l_address as? ViewGroup)?.beginDelayedTransition()
tv_address.setPadding(tv_address.paddingStart, tvAddressPaddingTop / 2,
tv_address.paddingEnd, tv_address.paddingBottom)
chip_group_segwit.show()
val checkedId = SegwitUiHelper.typeToId(state.walletAddresses.selectedAddress.type)
if (checkedId != View.NO_ID) chip_group_segwit.check(checkedId)
chip_group_segwit.setOnCheckedChangeListener { group, checkedId ->
if (checkedId == -1) return@setOnCheckedChangeListener
SegwitUiHelper.idToType(checkedId)?.let {
store.dispatch(WalletAction.ChangeSelectedAddress(it))
}
}
} else {
tv_address.setPadding(tv_address.paddingStart, tvAddressPaddingTop,
tv_address.paddingEnd, tv_address.paddingBottom)
chip_group_segwit.hide()
}
tv_address.text = state.walletAddresses.selectedAddress.address
tv_explore?.setOnClickListener {
store.dispatch(WalletAction.ExploreAddress(requireContext()))
}
@ -267,9 +310,11 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.details_menu -> {
store.state.globalState.scanNoteResponse?.card?.let { card ->
store.state.globalState.scanNoteResponse?.let { scanNoteResponse ->
store.dispatch(DetailsAction.PrepareScreen(
card, store.state.walletState.wallet,
scanNoteResponse.card, scanNoteResponse,
store.state.walletState.wallet,
store.state.globalState.configManager?.config?.isCreatingTwinCardsAllowed,
store.state.globalState.appCurrency
))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Details))
@ -285,4 +330,24 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
if (store.state.walletState.showDetails) inflater.inflate(R.menu.wallet, menu)
}
}
class SegwitUiHelper {
companion object {
fun typeToId(type: AddressType): Int {
return when (type) {
is BitcoinAddressType.Legacy -> R.id.chip_legacy
is BitcoinAddressType.Segwit -> R.id.chip_default
else -> View.NO_ID
}
}
fun idToType(id: Int): AddressType? {
return when (id) {
R.id.chip_default -> BitcoinAddressType.Segwit
R.id.chip_legacy -> BitcoinAddressType.Legacy
else -> null
}
}
}
}

View file

@ -0,0 +1,64 @@
package com.tangem.tap.features.wallet.ui.dialogs
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.squareup.picasso.Picasso
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.twins.TwinsHelper
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_twin_cards.*
import kotlinx.android.synthetic.main.layout_twin_cards.*
class TwinsOnboardingFragment : Fragment(R.layout.fragment_twin_cards) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
store.dispatch(WalletAction.TwinsAction.SetOnboardingShown)
val secondCardId = store.state.walletState.twinCardsState?.secondCardId ?: ""
val secondTwinCardId = TwinsHelper.getTwinCardIdForUser(secondCardId)
val text = getString(R.string.twins_onboarding_description_format, secondTwinCardId)
tv_twin_cards_description_1.text = text
setOnClickListeners()
Picasso.get()
.load(Artwork.TWIN_CARD_1)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_twin_card_1)
Picasso.get()
.load(Artwork.TWIN_CARD_2)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_twin_card_2)
}
private fun setOnClickListeners() {
btn_continue.setOnClickListener {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.tap.network.coinmarketcap
import com.tangem.tap.TapConfig
import com.tangem.tap.network.createRetrofitInstance
import okhttp3.Interceptor
import okhttp3.Response
@ -23,20 +22,20 @@ interface CoinMarketCapApi {
companion object {
private const val baseUrl = "https://pro-api.coinmarketcap.com/"
fun create(): CoinMarketCapApi {
fun create(apiKey: String): CoinMarketCapApi {
return createRetrofitInstance(
baseUrl,
listOf(createCoinMarketRequestInterceptor()),
listOf(createCoinMarketRequestInterceptor(apiKey)),
).create(CoinMarketCapApi::class.java)
}
}
}
private fun createCoinMarketRequestInterceptor(): Interceptor {
private fun createCoinMarketRequestInterceptor(apiKey: String): Interceptor {
return object : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val requestBuilder = chain.request().newBuilder()
requestBuilder.addHeader("X-CMC_PRO_API_KEY", TapConfig.coinMarketCapKey)
requestBuilder.addHeader("X-CMC_PRO_API_KEY", apiKey)
return chain.proceed(requestBuilder.build())
}
}

View file

@ -3,12 +3,17 @@ package com.tangem.tap.network.coinmarketcap
import com.tangem.commands.common.network.Result
import com.tangem.commands.common.network.performRequest
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.math.BigDecimal
class CoinMarketCapService {
private val api: CoinMarketCapApi by lazy { CoinMarketCapApi.create() }
class CoinMarketCapService() {
private val api: CoinMarketCapApi by lazy { CoinMarketCapApi.create(getApiKey()) }
private fun getApiKey(): String {
return store.state.globalState.configManager?.config?.coinMarketCapKey ?: ""
}
suspend fun getRate(
currency: String, fiatCurrency: FiatCurrencyName? = null

View file

@ -3,6 +3,7 @@ package com.tangem.tap.persistence
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
@ -18,6 +19,10 @@ class PreferencesStorage(applicationContext: Application) {
applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
}
init {
incrementLaunchCounter()
}
private val fiatCurrenciesAdapter: JsonAdapter<List<FiatCurrency>> by lazy {
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
@ -45,11 +50,7 @@ class PreferencesStorage(applicationContext: Application) {
return preferences.edit().putString(FIAT_CURRENCIES_KEY, json).apply()
}
fun isFirstLaunch(): Boolean {
val isFirst = !preferences.contains(FIRST_LAUNCH_CHECK_KEY)
if (isFirst) preferences.edit().putInt(FIRST_LAUNCH_CHECK_KEY, System.currentTimeMillis().toInt()).apply()
return isFirst
}
fun getCountOfLaunches(): Int = preferences.getInt(APP_LAUNCH_COUNT_KEY, 1)
fun saveScannedCardId(cardId: String) {
val scannedCardsIds: String = restoreScannedCardIds()
@ -65,7 +66,6 @@ class PreferencesStorage(applicationContext: Application) {
private fun restoreScannedCardIds(): String =
preferences.getString(SCANNED_CARDS_IDS_KEY, "") ?: ""
fun saveDisclaimerAccepted() {
preferences.edit().putBoolean(DISCLAIMER_ACCEPTED_KEY, true).apply()
}
@ -74,13 +74,27 @@ class PreferencesStorage(applicationContext: Application) {
return preferences.getBoolean(DISCLAIMER_ACCEPTED_KEY, false)
}
fun saveTwinsOnboardingShown() {
preferences.edit().putBoolean(TWINS_ONBOARDING_SHOWN_KEY, true).apply()
}
fun wasTwinsOnboardingShown(): Boolean {
return preferences.getBoolean(TWINS_ONBOARDING_SHOWN_KEY, false)
}
private fun incrementLaunchCounter() {
var count = preferences.getInt(APP_LAUNCH_COUNT_KEY, 0)
preferences.edit { putInt(APP_LAUNCH_COUNT_KEY, ++count) }
}
companion object {
private const val PREFERENCES_NAME = "tapPrefs"
private const val APP_CURRENCY_KEY = "appCurrency"
private const val FIAT_CURRENCIES_KEY = "fiatCurrencies"
private const val FIRST_LAUNCH_CHECK_KEY = "firstLaunchCheck"
private const val SCANNED_CARDS_IDS_KEY = "scannedCardIds"
private const val DISCLAIMER_ACCEPTED_KEY = "disclaimerAccepted"
private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown"
private const val APP_LAUNCH_COUNT_KEY = "launchCount"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

View file

@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="375dp"
android:height="471dp"
android:viewportWidth="375"
android:viewportHeight="471">
<path
android:pathData="M-65.241,418.887l452.126,-172.218l26.339,66.399l-452.126,172.218z"
android:strokeAlpha="0.35"
android:fillColor="#D1D1D6"
android:fillAlpha="0.35"/>
<path
android:pathData="M-91.81,315l474.237,-180.641l39.37,99.249l-474.237,180.641z"
android:strokeAlpha="0.2"
android:fillColor="#636366"
android:fillAlpha="0.2"/>
<path
android:pathData="M-114,31.889l452.126,-172.218l70.731,178.307l-452.126,172.218z"
android:strokeAlpha="0.22"
android:fillColor="#1C1C1E"
android:fillAlpha="0.22"/>
<path
android:pathData="M-61.336,216.984l452.126,-172.218l29.793,75.107l-452.126,172.218z"
android:strokeAlpha="0.2"
android:fillColor="#3A3A3C"
android:fillAlpha="0.2"/>
</vector>

View file

@ -0,0 +1,101 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="97dp"
android:height="19dp"
android:viewportWidth="97"
android:viewportHeight="19">
<group>
<clip-path
android:pathData="M0,0h96.5903v19h-96.5903z"/>
<path
android:pathData="M29.8167,8.4838C29.5066,9.0359 29.0376,9.482 28.4707,9.7641C27.8598,10.0888 27.0902,10.2507 26.1619,10.2496H24.2677V14.5925H21.9986V3.2826H26.1629C27.038,3.2826 27.7836,3.4337 28.3997,3.7358C28.9765,4.003 29.4594,4.4378 29.7853,4.9836C30.0983,5.5309 30.2581,6.1524 30.2479,6.7828C30.2488,7.377 30.1006,7.9619 29.8167,8.4838ZM27.4588,7.9899C27.7606,7.7038 27.9117,7.3014 27.912,6.7828C27.912,5.6812 27.2963,5.1302 26.0648,5.1298H24.2677V8.4191H26.0669C26.6924,8.4191 27.1563,8.276 27.4588,7.9899Z"
android:fillColor="#000E33"/>
<path
android:pathData="M31.2345,7.6391C31.5649,6.9717 32.0757,6.4102 32.709,6.0185C33.3364,5.6386 34.0577,5.4422 34.7911,5.4515C35.4019,5.438 36.0064,5.5772 36.5496,5.8566C37.0231,6.1024 37.4355,6.451 37.7567,6.8768V5.5977H40.0415V14.5738H37.7567V13.2612C37.4438,13.7012 37.0299,14.0597 36.5496,14.3064C36.002,14.5902 35.3921,14.7324 34.7755,14.72C34.0454,14.7274 33.3286,14.5252 32.71,14.1373C32.0765,13.7349 31.5661,13.1659 31.2345,12.4926C30.861,11.7351 30.6748,10.8989 30.6915,10.0544C30.6747,9.2172 30.861,8.3885 31.2345,7.6391ZM37.432,8.6687C37.2309,8.2903 36.9278,7.9759 36.5569,7.7613C36.1977,7.5525 35.7893,7.4433 35.3738,7.4449C34.9569,7.4408 34.5469,7.5512 34.1884,7.7641C33.83,7.9769 33.5367,8.284 33.3407,8.652C33.1106,9.0825 32.9961,9.5653 33.0087,10.0533C32.9974,10.5465 33.1116,11.0345 33.3407,11.4714C33.5423,11.8556 33.8449,12.1776 34.2158,12.4028C34.565,12.6153 34.9661,12.7273 35.3749,12.7265C35.79,12.7281 36.1981,12.6189 36.5569,12.4101C36.9275,12.1952 37.2306,11.8809 37.432,11.5027C37.6582,11.0651 37.7696,10.5771 37.7557,10.0847C37.7688,9.5929 37.6575,9.1059 37.432,8.6687Z"
android:fillColor="#000E33"/>
<path
android:pathData="M53.7502,14.3074C53.1525,14.0645 52.6338,13.6608 52.2517,13.141C51.8784,12.6157 51.68,11.9862 51.6847,11.3418H54.1157C54.1212,11.549 54.1678,11.7531 54.2528,11.9422C54.3378,12.1313 54.4594,12.3016 54.6106,12.4434C54.9082,12.7128 55.3162,12.8479 55.8344,12.8486C56.3639,12.8486 56.7798,12.7215 57.0824,12.4675C57.2294,12.3474 57.3467,12.1951 57.4253,12.0223C57.5039,11.8495 57.5416,11.661 57.5355,11.4713C57.5409,11.3201 57.5165,11.1693 57.4637,11.0276C57.4108,10.8859 57.3305,10.756 57.2274,10.6453C57.0105,10.4219 56.748,10.2477 56.4578,10.1347C56.0412,9.9752 55.6167,9.8372 55.186,9.7212C54.575,9.5494 53.9766,9.3355 53.3951,9.081C52.9285,8.8646 52.5225,8.5365 52.2131,8.1256C51.8831,7.6985 51.7181,7.1287 51.7181,6.4162C51.7043,5.7954 51.8795,5.1851 52.2204,4.6661C52.5705,4.1571 53.0604,3.7603 53.6311,3.5237C54.2873,3.2483 54.9936,3.1128 55.7051,3.1259C56.8715,3.1259 57.8196,3.4095 58.5494,3.9769C59.2783,4.5439 59.6807,5.3351 59.7565,6.3504H57.2608C57.2529,6.1635 57.2046,5.9806 57.1191,5.8142C57.0337,5.6478 56.9132,5.5019 56.7659,5.3866C56.4578,5.1339 56.0507,5.0054 55.5422,5.0054C55.0994,5.0054 54.7457,5.1189 54.4811,5.3459C54.2159,5.5725 54.0833,5.9017 54.0833,6.3337C54.0794,6.4731 54.104,6.6119 54.1555,6.7415C54.207,6.8712 54.2844,6.9889 54.383,7.0876C54.5969,7.2973 54.8503,7.4623 55.1286,7.5732C55.4262,7.6975 55.8438,7.8405 56.3847,8.0034C56.9991,8.1762 57.6003,8.3926 58.1839,8.6508C58.6562,8.8733 59.0675,9.2069 59.3827,9.623C59.7179,10.0553 59.8853,10.6227 59.885,11.3251C59.8879,11.9196 59.7252,12.5032 59.4151,13.0104C59.0826,13.5461 58.6045,13.976 58.0367,14.2499C57.4311,14.558 56.7127,14.7122 55.8815,14.7125C55.151,14.7234 54.4258,14.5856 53.7502,14.3074Z"
android:fillColor="#000E33"/>
<path
android:pathData="M64.0253,7.4426V11.7856C64.0253,12.0873 64.098,12.3063 64.2435,12.4424C64.3896,12.5781 64.635,12.6449 64.9807,12.6449H66.0333V14.5569H64.6069C62.695,14.5569 61.7388,13.6279 61.7385,11.7699V7.4426H60.6692V5.5798H61.7385V3.3597H64.0232V5.5798H66.0323V7.4437L64.0253,7.4426Z"
android:fillColor="#000E33"/>
<path
android:pathData="M70.9747,5.8565C71.4667,5.5809 72.0231,5.4411 72.5869,5.4513V7.8332H71.9877C71.2745,7.8332 70.737,8.0006 70.3754,8.3355C70.013,8.6696 69.8323,9.2544 69.8323,10.0856V14.558H67.5633V5.5819H69.8323V6.9759C70.1126,6.5124 70.5056,6.1273 70.9747,5.8565Z"
android:fillColor="#000E33"/>
<path
android:pathData="M74.0811,4.1648C73.9516,4.0437 73.8492,3.8967 73.7807,3.7333C73.7121,3.5699 73.6789,3.3938 73.6832,3.2166C73.6792,3.0395 73.7125,2.8635 73.7811,2.7001C73.8496,2.5368 73.9519,2.3897 74.0811,2.2685C74.3549,2.0231 74.7096,1.8873 75.0773,1.8873C75.4449,1.8873 75.7997,2.0231 76.0735,2.2685C76.2028,2.3896 76.305,2.5367 76.3736,2.7001C76.4421,2.8635 76.4754,3.0395 76.4713,3.2166C76.4753,3.3937 76.442,3.5697 76.3734,3.7331C76.3049,3.8965 76.2027,4.0436 76.0735,4.1648C75.7997,4.4102 75.4449,4.5459 75.0773,4.5459C74.7096,4.5459 74.3549,4.4102 74.0811,4.1648ZM76.1956,5.6152V14.5913H73.9265V5.6152H76.1956Z"
android:fillColor="#000E33"/>
<path
android:pathData="M85.3568,6.4643C86.0157,7.1399 86.345,8.0825 86.3446,9.2921V14.5581H84.0755V9.6001C84.0755,8.8869 83.8973,8.3387 83.5409,7.9555C83.1848,7.5722 82.6989,7.3804 82.0831,7.3801C81.4566,7.3801 80.9623,7.5719 80.6004,7.9555C80.238,8.3387 80.057,8.8869 80.0574,9.6001V14.5591H77.7883V5.5809H80.0574V6.6982C80.3655,6.3053 80.7629,5.9914 81.2164,5.7825C81.7013,5.5583 82.2299,5.4449 82.764,5.4504C83.8343,5.4514 84.6986,5.7894 85.3568,6.4643Z"
android:fillColor="#000E33"/>
<path
android:pathData="M93.1069,5.8503C93.5848,6.0896 93.9965,6.4429 94.3057,6.8788V5.5986H96.5904V14.6405C96.6038,15.4125 96.4315,16.1763 96.0882,16.8678C95.7505,17.5238 95.2262,18.0652 94.5814,18.4237C93.911,18.808 93.1006,18.9998 92.1504,18.9991C90.8754,18.9991 89.8302,18.7022 89.0146,18.1084C88.6198,17.8293 88.2911,17.4669 88.0516,17.0469C87.8122,16.6268 87.6679,16.1593 87.6289,15.6774H89.8813C90.0024,16.091 90.278,16.4421 90.6509,16.6579C91.0852,16.9115 91.582,17.0377 92.0846,17.0224C92.7435,17.0224 93.2785,16.825 93.6896,16.4303C94.1,16.0356 94.305,15.439 94.3046,14.6405V13.2465C93.9909,13.6877 93.5773,14.0484 93.0975,14.2991C92.5566,14.5865 91.9514,14.7313 91.3391,14.7199C90.6065,14.7266 89.8871,14.5245 89.2652,14.1372C88.6284,13.7367 88.1151,13.1674 87.7824,12.4926C87.4089,11.735 87.2227,10.8988 87.2394,10.0543C87.2231,9.2175 87.4094,8.3892 87.7824,7.6401C88.1128,6.9724 88.6241,6.4109 89.2579,6.0194C89.8854,5.6396 90.6067,5.4432 91.3401,5.4524C91.9526,5.4391 92.5592,5.5757 93.1069,5.8503ZM93.982,8.6697C93.7812,8.2911 93.478,7.9766 93.1069,7.7622C92.7477,7.5534 92.3393,7.4442 91.9238,7.4458C91.5069,7.4418 91.0969,7.5522 90.7384,7.765C90.3799,7.9779 90.0867,8.285 89.8907,8.653C89.6606,9.0834 89.5461,9.5663 89.5587,10.0543C89.5473,10.5475 89.6616,11.0355 89.8907,11.4724C90.0923,11.8566 90.3949,12.1786 90.7658,12.4038C91.115,12.6163 91.5161,12.7283 91.9249,12.7275C92.3403,12.729 92.7487,12.6198 93.108,12.4111C93.4788,12.1965 93.782,11.8821 93.983,11.5037C94.2092,11.066 94.3206,10.5781 94.3067,10.0856C94.3197,9.5938 94.208,9.1067 93.982,8.6697Z"
android:fillColor="#000E33"/>
<path
android:pathData="M47.8324,10.708H47.8378V10.6212C47.8367,10.6512 47.8356,10.6802 47.8324,10.708Z"
android:fillColor="#000E33"/>
<path
android:pathData="M50.1111,5.5996V14.6426C50.1242,15.4145 49.9519,16.1783 49.6088,16.8699C49.2713,17.526 48.7465,18.0672 48.101,18.4247C47.4316,18.809 46.6217,19.0008 45.6711,19.0001C45.1433,19.0051 44.617,18.9455 44.1037,18.8226C43.5408,18.6893 43.0091,18.4478 42.5384,18.1115C42.5374,18.1104 42.5363,18.1104 42.5353,18.1094C42.1406,17.8301 41.812,17.4677 41.5726,17.0477C41.3332,16.6277 41.1888,16.1603 41.1496,15.6784H43.402C43.4252,15.7605 43.4552,15.8405 43.4918,15.9175C43.6381,16.2278 43.8751,16.4864 44.1716,16.6589C44.5451,16.8805 44.9686,17.004 45.4027,17.0182C45.4685,17.0213 45.5374,17.0234 45.6063,17.0234C46.2652,17.0234 46.7999,16.8264 47.2103,16.4323C47.6206,16.0376 47.826,15.441 47.8263,14.6426V13.2485C47.573,13.6081 47.2507,13.9137 46.8782,14.1476C46.7957,14.2019 46.708,14.252 46.6192,14.3011C46.5879,14.3188 46.5555,14.3356 46.5232,14.3523H46.5221C46.127,14.5484 45.6982,14.6678 45.2586,14.7042C45.2189,14.7052 45.1772,14.7063 45.1364,14.7063C44.0661,14.7063 43.2018,14.369 42.5436,13.6944C42.0247,13.1598 41.7103,12.4601 41.6007,11.5955C41.5696,11.3535 41.5542,11.1097 41.5548,10.8656V5.5996H43.8228V10.5586C43.8228,11.2708 44.0014,11.82 44.3585,12.2022C44.6057,12.4686 44.9298,12.6511 45.2858,12.7243C45.4601,12.7612 45.638,12.7794 45.8162,12.7786C46.4438,12.7786 46.9381,12.5865 47.299,12.2022C47.6175,11.866 47.7954,11.402 47.8326,10.8103H47.8378V5.5996H50.1111Z"
android:fillColor="#000E33"/>
<path
android:pathData="M10.0944,15.0026C11.369,14.8333 12.5495,14.2395 13.4453,13.3172L18.6633,7.9687C18.3313,7.9145 17.9952,7.8893 17.6588,7.8935C16.9109,7.8953 16.171,8.0465 15.4824,8.3382C14.7938,8.63 14.1704,9.0564 13.649,9.5925L11.7245,11.564C11.713,11.5765 11.7004,11.589 11.6879,11.6026C11.0831,12.223 10.2581,12.5798 9.3918,12.5957C8.5256,12.6115 7.688,12.285 7.061,11.6872L9.0711,13.6482C9.4784,14.0456 9.8234,14.5022 10.0944,15.0026Z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="6.23136"
android:startX="19.5904"
android:endY="12.3056"
android:endX="10.6475"
android:type="linear">
<item android:offset="0" android:color="#FFC906F9"/>
<item android:offset="0.4483" android:color="#FFA803FC"/>
<item android:offset="0.8625" android:color="#FF8700FF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M5.3465,13.4445L10.695,18.6624C10.8987,17.4088 10.6875,16.123 10.0935,15.0004C9.8231,14.4999 9.478,14.0436 9.0702,13.647L7.0601,11.686C6.7446,11.3857 6.492,11.0258 6.3167,10.6271C6.1414,10.2284 6.047,9.7989 6.0389,9.3635C6.0308,8.9281 6.1093,8.4953 6.2697,8.0905C6.4301,7.6856 6.6693,7.3165 6.9734,7.0048L5.0144,9.0149C4.6106,9.4301 4.145,9.7802 3.634,10.0528C3.8012,11.3456 4.4055,12.5423 5.3465,13.4445Z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="10.1089"
android:startX="8.29132"
android:endY="18.3078"
android:endX="6.11687"
android:type="linear">
<item android:offset="0" android:color="#FF0044FF"/>
<item android:offset="0.4896" android:color="#FF0087FF"/>
<item android:offset="1" android:color="#FF00CAFF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M0,10.6417C0.332,10.6961 0.6682,10.7209 1.0045,10.7158C1.9213,10.7097 2.8232,10.483 3.6339,10.0548C4.145,9.7825 4.6108,9.4323 5.0143,9.0169L6.9764,7.0068C7.278,6.6968 7.6377,6.4494 8.035,6.2787C8.4322,6.108 8.8593,6.0172 9.2917,6.0117C9.724,6.0062 10.1533,6.0859 10.5548,6.2465C10.9563,6.407 11.3222,6.6451 11.6316,6.9472L9.6475,5.0123C9.2244,4.6011 8.8689,4.1257 8.5939,3.6036C7.3104,3.7688 6.1208,4.364 5.219,5.2921L0,10.6417Z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="6.24054"
android:startX="1.39411"
android:endY="7.59302"
android:endX="5.84485"
android:type="linear">
<item android:offset="0" android:color="#FFB0FF33"/>
<item android:offset="0.4948" android:color="#FF55DF20"/>
<item android:offset="1" android:color="#FF0AC222"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M11.7245,11.5658L13.649,9.5943C14.0528,9.1791 14.5185,8.829 15.0295,8.5563C14.8655,7.29 14.2821,6.115 13.3723,5.219L8.0239,0C7.9696,0.332 7.9445,0.6681 7.9487,1.0045C7.9549,1.9106 8.1764,2.8022 8.5951,3.6057C8.87,4.1277 9.2256,4.6031 9.6487,5.0143L11.6317,6.9493C12.2506,7.5522 12.6073,8.3746 12.6247,9.2385C12.6421,10.1024 12.3187,10.9384 11.7246,11.5658H11.7245Z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="6.51626"
android:startX="6.78645"
android:endY="4.72844"
android:endX="14.3139"
android:type="linear">
<item android:offset="0" android:color="#FFFF8100"/>
<item android:offset="0.5" android:color="#FFFF5D00"/>
<item android:offset="1" android:color="#FFFF3600"/>
</gradient>
</aapt:attr>
</path>
</group>
</vector>

View file

@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="375dp"
android:height="471dp"
android:viewportWidth="375"
android:viewportHeight="471">
<path
android:pathData="M-65.241,418.887l452.126,-172.218l26.339,66.399l-452.126,172.218z"
android:fillColor="@color/warning"
android:fillAlpha="0.25"/>
<path
android:pathData="M-91.81,315l474.237,-180.641l39.37,99.249l-474.237,180.641z"
android:strokeAlpha="0.2"
android:fillColor="@color/warning"
android:fillAlpha="0.35"/>
<path
android:pathData="M-114,31.889l452.126,-172.218l70.731,178.307l-452.126,172.218z"
android:strokeAlpha="0.22"
android:fillColor="@color/warning"
android:fillAlpha="0.55"/>
<path
android:pathData="M-61.336,216.984l452.126,-172.218l29.793,75.107l-452.126,172.218z"
android:strokeAlpha="0.2"
android:fillColor="@color/warning"
android:fillAlpha="0.45"/>
</vector>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"
>
<size
android:width="484dp"
android:height="192dp" />
<gradient
android:endColor="@color/darkGray6"
android:startColor="@color/darkGray6" />
</shape>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape android:shape="rectangle"
xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/twins_dark" />
<corners
android:bottomLeftRadius="30dp"
android:bottomRightRadius="30dp"
android:topLeftRadius="30dp"
android:topRightRadius="30dp" />
</shape>

View file

@ -47,7 +47,7 @@
android:id="@+id/tv_card_id_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="14dp"
android:paddingBottom="7dp"
android:text="@string/details_row_title_cid"
android:textColor="@color/darkGray6"
android:textSize="16sp"
@ -59,7 +59,7 @@
android:id="@+id/tv_card_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="14dp"
android:paddingBottom="7dp"
android:textColor="@color/darkGray1"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
@ -70,7 +70,8 @@
android:id="@+id/tv_issuer_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="14dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:text="@string/details_row_title_issuer"
android:textColor="@color/darkGray6"
android:textSize="16sp"
@ -81,7 +82,8 @@
android:id="@+id/tv_issuer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="14dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:textColor="@color/darkGray1"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
@ -92,6 +94,7 @@
android:id="@+id/tv_signed_hashes_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:text="@string/details_row_title_signed_hashes"
android:textColor="@color/darkGray6"

View file

@ -0,0 +1,160 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinator_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/backgroundLightGray"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
style="@style/Widget.MaterialComponents.Toolbar.Surface"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/backgroundLightGray"
android:fitsSystemWindows="true"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
app:title="Re-create wallet" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="16dp"
android:layout_marginBottom="33dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<TextView
android:id="@+id/tv_step_number"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="start"
android:text="@string/details_twins_recreate_step_format"
android:textColor="@color/colorSecondary"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/v_step_1"
app:layout_constraintStart_toStartOf="@id/v_step_1"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/v_step_1"
android:layout_width="0dp"
android:layout_height="3dp"
android:layout_marginStart="24dp"
android:layout_marginTop="14dp"
android:layout_marginEnd="4dp"
android:background="@color/colorSecondary"
app:layout_constraintEnd_toStartOf="@+id/v_step_2"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_step_number" />
<View
android:id="@+id/v_step_2"
android:layout_width="0dp"
android:layout_height="3dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:background="@color/colorSecondary"
app:layout_constraintEnd_toStartOf="@+id/v_step_3"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/v_step_1"
app:layout_constraintTop_toTopOf="@id/v_step_1" />
<View
android:id="@+id/v_step_3"
android:layout_width="0dp"
android:layout_height="3dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="24dp"
android:background="@color/colorSecondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/v_step_2"
app:layout_constraintTop_toTopOf="@id/v_step_1" />
<TextView
android:id="@+id/tv_twin_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:layout_marginBottom="8dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/details_twins_recreate_title_format"
android:textColor="@color/darkGray6"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/v_step_1" />
<TextView
android:id="@+id/tv_twin_cards_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/details_twins_recreate_subtitle"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_twin_title" />
<ImageView
android:id="@+id/iv_card_1"
android:layout_width="130dp"
android:layout_height="70dp"
android:layout_marginStart="47dp"
android:layout_marginTop="16dp"
android:src="@drawable/card_placeholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_twin_cards_description" />
<ImageView
android:id="@+id/iv_card_2"
android:layout_width="130dp"
android:layout_height="70dp"
android:layout_marginStart="24dp"
android:layout_marginTop="20dp"
android:src="@drawable/card_placeholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/iv_card_1" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintGuide_percent="0.37" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_tap"
style="@style/TapBlackButton"
android:layout_width="0dp"
android:layout_marginStart="7dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="16dp"
app:icon="@drawable/ic_scan"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/guideline"
tools:text="@string/details_twins_recreate_button_format" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinator_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/backgroundLightGray"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="33dp">
<View
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/l_twin_cards"
android:alpha="0.55"
android:background="@color/warning"/>
<include
android:id="@+id/l_twin_cards"
layout="@layout/layout_twin_cards_orange"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="-40dp"
android:layout_marginBottom="24dp"
app:layout_constraintBottom_toTopOf="@id/tv_twin_title"
app:layout_constraintVertical_bias="0" />
<TextView
android:id="@+id/tv_twin_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:drawablePadding="4dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/common_warning"
android:textColor="@color/darkGray6"
android:textSize="20sp"
android:textStyle="bold"
app:drawableStartCompat="@drawable/ic_warning"
app:drawableTint="@color/darkGray6"
app:layout_constraintBottom_toTopOf="@id/tv_twin_cards_description"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/tv_twin_cards_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/details_twins_recreate_warning"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/btn_start"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_cancel"
style="@style/TapBlackButton"
android:layout_width="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="30dp"
android:gravity="center"
android:text="@string/common_back"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintVertical_bias="1" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintGuide_percent="0.37" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_start"
style="@style/TapButtonWithIcon"
android:layout_width="0dp"
android:layout_marginStart="7dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="16dp"
android:text="@string/common_start"
app:icon="@drawable/ic_send"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/guideline" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -28,7 +28,6 @@
android:id="@+id/cl_details_confirm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="33dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
@ -36,7 +35,7 @@
android:id="@+id/sv_wallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginBottom="8dp"
android:fillViewport="true"
android:overScrollMode="never"
app:layout_constraintBottom_toTopOf="@id/btn_accept">
@ -67,7 +66,6 @@
android:id="@+id/btn_accept"
style="@style/TapButtonWithIcon"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginStart="7dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="16dp"

View file

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/backgroundLightGray"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="33dp">
<View
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/l_twin_cards"
android:alpha="0.22"
android:background="@color/darkGray6"/>
<include
android:id="@+id/l_twin_cards"
layout="@layout/layout_twin_cards"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="-40dp"
android:layout_marginBottom="24dp"
android:gravity="top"
app:layout_constraintBottom_toTopOf="@id/tv_twin_title"
app:layout_constraintVertical_bias="0" />
<TextView
android:id="@+id/tv_twin_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24sp"
android:layout_marginBottom="8dp"
android:text="@string/twins_onboarding_title"
android:textColor="@color/darkGray6"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/tv_twin_subtitle"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/tv_twin_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginBottom="16dp"
android:text="@string/twins_onboarding_subtitle"
android:textColor="@color/darkGray6"
android:textSize="17sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/tv_twin_cards_description_1"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/tv_twin_cards_description_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/twins_onboarding_description_format"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/btn_continue"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintGuide_percent="0.37" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_continue"
style="@style/TapBlackButton"
android:layout_width="0dp"
android:layout_marginStart="7dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="16dp"
android:text="@string/common_continue"
app:icon="@drawable/ic_send"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/guideline" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinator_wallet"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -61,6 +62,40 @@
android:src="@drawable/card_placeholder"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_twin_card"
android:layout_width="100dp"
android:layout_height="30dp"
android:src="@drawable/shape_chip"
android:elevation="3dp"
android:layout_marginStart="25dp"
app:layout_constraintStart_toStartOf="@id/iv_card"
app:layout_constraintTop_toBottomOf="@id/iv_card"
app:layout_constraintBottom_toBottomOf="@id/iv_card"/>
<TextView
android:id="@+id/tv_twin_card_number"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="@id/iv_twin_card"
app:layout_constraintEnd_toEndOf="@id/iv_twin_card"
app:layout_constraintTop_toTopOf="@id/iv_twin_card"
app:layout_constraintBottom_toBottomOf="@id/iv_twin_card"
tools:text="Card 2 of 2"
android:elevation="3dp"
android:gravity="center"
android:textColor="@color/buttonGray"
android:textStyle="bold"
android:textSize="14sp"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
app:barrierDirection="bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="iv_card,iv_twin_card"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_pending_transaction"
android:layout_width="match_parent"
@ -68,7 +103,7 @@
android:layout_marginTop="12dp"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
app:layout_constraintTop_toBottomOf="@id/iv_card" />
app:layout_constraintTop_toBottomOf="@id/barrier" />
<include
android:id="@+id/l_card_balance"

View file

@ -23,21 +23,52 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.chip.ChipGroup
android:id="@+id/chip_group_segwit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:selectionRequired="true"
app:singleLine="true"
app:singleSelection="true"
tools:visibility="visible">
<com.google.android.material.chip.Chip
android:id="@+id/chip_default"
style="@style/TapChip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/wallet_address_chip_default" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_legacy"
style="@style/TapChip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/wallet_address_chip_legacy" />
</com.google.android.material.chip.ChipGroup>
<TextView
android:id="@+id/tv_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:paddingStart="@dimen/dimen16"
android:paddingTop="@dimen/dimen16"
android:paddingEnd="@dimen/dimen16"
android:singleLine="true"
android:textColor="@color/darkGray1"
android:textSize="13sp"
app:layout_constraintEnd_toStartOf="@id/btn_copy"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toBottomOf="@id/chip_group_segwit"
tools:text="139mrsJgyWnJ**** **** y9BV" />
<TextView
@ -66,10 +97,10 @@
android:layout_marginEnd="16dp"
android:background="@drawable/shape_ellipse"
android:clickable="true"
android:focusable="true"
android:elevation="3dp"
android:focusable="true"
app:layout_constraintEnd_toStartOf="@id/btn_show_qr"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toBottomOf="@id/chip_group_segwit">
<ImageView
android:id="@+id/iv_copy"
@ -90,10 +121,10 @@
android:layout_marginEnd="16dp"
android:background="@drawable/shape_ellipse"
android:clickable="true"
android:focusable="true"
android:elevation="3dp"
android:focusable="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toBottomOf="@id/chip_group_segwit">
<ImageView
android:id="@+id/iv_qr"
@ -117,12 +148,13 @@
<ImageView
android:id="@+id/iv_payid_icon"
android:layout_width="68dp"
android:layout_height="22dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/ic_payid"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/v_payid_divider" />
app:layout_constraintTop_toBottomOf="@id/v_payid_divider"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
android:id="@+id/tv_create_payid"
@ -142,18 +174,16 @@
android:id="@+id/tv_payid_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:padding="16dp"
android:singleLine="true"
android:textAlignment="textEnd"
android:textColor="@color/darkGray1"
android:textSize="13sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toEndOf="@id/iv_payid_icon"
android:textAlignment="textEnd"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/v_payid_divider"
tools:text="roman$payid.tangem.com"
android:visibility="gone"/>
tools:text="romafdffdfdfn$payid.tangem.com" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_payid"

View file

@ -48,21 +48,21 @@
<com.google.android.material.chip.Chip
android:id="@+id/chipLow"
style="@style/ChipNetworkFee"
style="@style/TapChip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/send_fee_picker_low" />
<com.google.android.material.chip.Chip
android:id="@+id/chipNormal"
style="@style/ChipNetworkFee"
style="@style/TapChip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/send_fee_picker_normal" />
<com.google.android.material.chip.Chip
android:id="@+id/chipPriority"
style="@style/ChipNetworkFee"
style="@style/TapChip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/send_fee_picker_priority" />

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="-40dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:layout_marginTop="60dp"
android:scaleType="fitXY"
android:src="@drawable/ic_group_1258"
app:layout_constraintBottom_toBottomOf="@id/iv_twin_card_2"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_twin_card_1"
android:layout_width="wrap_content"
android:layout_height="170dp"
android:layout_marginTop="50dp"
android:rotation="-19.5"
android:src="@drawable/card_placeholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_twin_card_2"
android:layout_width="wrap_content"
android:layout_height="170dp"
android:layout_marginTop="20dp"
android:rotation="-19.5"
android:src="@drawable/card_placeholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_twin_card_1" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="-40dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:layout_marginTop="60dp"
android:scaleType="fitXY"
android:src="@drawable/ic_twin_cards_background_warning"
app:layout_constraintBottom_toBottomOf="@id/iv_twin_card_2"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_twin_card_1"
android:layout_width="wrap_content"
android:layout_height="170dp"
android:layout_marginTop="50dp"
android:rotation="-19.5"
android:src="@drawable/card_placeholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_twin_card_2"
android:layout_width="wrap_content"
android:layout_height="170dp"
android:layout_marginTop="20dp"
android:rotation="-19.5"
android:src="@drawable/card_placeholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_twin_card_1" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -40,18 +40,18 @@
<string name="wallet_pending_tx_receiving">Erhalten\u0020</string>
<string name="wallet_pending_tx_receiving_address_format">von %s </string>
<string name="wallet_address_button_explore">Adresse erkunden</string>
<string name="wallet_address_button_create_payid">PayID erstellen</string>
<string name="wallet_address_button_create_payid">PayString erstellen</string>
<string name="wallet_qr_title_format">%s Wallet</string>
<string name="wallet_create_payid">PayID erstellen</string>
<string name="wallet_create_payid">PayString erstellen</string>
<string name="wallet_create_payid_card_format">Karte: %s</string>
<string name="wallet_create_payid_hint">PayID Name</string>
<string name="wallet_create_payid_hint">PayString Name</string>
<string name="wallet_create_payid_domain">$payid.tangem.com</string>
<string name="wallet_create_payid_button_title">Erstellen</string>
<string name="wallet_create_payid_info">PayID sind Ihre einzigartigen Identifikationsdaten wie Telefonnummer, E-Mail-Adresse oder ABN.</string>
<string name="wallet_create_payid_success_message">Ihre PayID wurde erfolgreich erstellt und zum Clipboard kopiert</string>
<string name="wallet_create_payid_error_message">Fehlerantwort beim Erstellen der ZahlungsID</string>
<string name="wallet_create_payid_error_already_created">Diese PayID existiert bereits. Versuchen Sie mit einer anderen erneut.</string>
<string name="send_destination_hint_address_payid">Adresse oder PayID</string>
<string name="wallet_create_payid_info">PayString sind Ihre einzigartigen Identifikationsdaten wie Telefonnummer, E-Mail-Adresse oder ABN.</string>
<string name="wallet_create_payid_success_message">Ihre PayString wurde erfolgreich erstellt und zum Clipboard kopiert</string>
<string name="wallet_create_payid_error_message">Fehlerantwort beim Erstellen der PayString</string>
<string name="wallet_create_payid_error_already_created">Diese PayString existiert bereits. Versuchen Sie mit einer anderen erneut.</string>
<string name="send_destination_hint_address_payid">Adresse oder PayString</string>
<string name="send_destination_hint_address">Adresse</string>
<string name="send_title">Absenden</string>
<string name="send_network_fee_title">Netzgebühr</string>
@ -105,7 +105,7 @@
<string name="initial_message_create_wallet_body">Tippen Sie um ein Wallet zu erstellen</string>
<string name="initial_message_change_access_code_body">Tippen Sie um den Zugangscode zu ändern</string>
<string name="initial_message_change_passcode_body">Tippen Sie um den Passcode zu ändern</string>
<string name="payid_service_error_loading">Ungültige ZahlungsID</string>
<string name="payid_service_error_loading">Ungültige PayString</string>
<string name="disclaimer_text">
@ -167,10 +167,10 @@ RECHTLICHER HAFTUNGSAUSSCHLUSS
<string name="send_error_fee_request_failed">Erhalt der Gebühr fehlgeschlagen</string>
<string name="send_error_unknown">Unbekannter Fehler</string>
<string name="send_error_payid_verification_failed">Verifikation der PayID ist fehlgeschlagen</string>
<string name="send_error_payid_unsupported_by_blockchain">PayID wird von der Blockchain nicht unterstützt</string>
<string name="send_error_payid_not_registered">PayID ist nicht registriert</string>
<string name="send_error_payid_request_failed">PayID-Anfrage ist fehlgeschlagen</string>
<string name="send_error_payid_verification_failed">Verifikation der PayString ist fehlgeschlagen</string>
<string name="send_error_payid_unsupported_by_blockchain">PayString wird von der Blockchain nicht unterstützt</string>
<string name="send_error_payid_not_registered">PayString ist nicht registriert</string>
<string name="send_error_payid_request_failed">PayString-Anfrage ist fehlgeschlagen</string>
<string name="send_error_address_same_as_wallet">Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein</string>
<string name="send_error_insufficient_balance">Nicht genug Geld</string>
@ -182,7 +182,7 @@ RECHTLICHER HAFTUNGSAUSSCHLUSS
<string name="wallet_notification_address_copied">Die Adresse wurde erfolgreich kopiert</string>
<string name="wallet_create_payid_empty">Zuerst geben Sie die gewünschte PayID ein</string>
<string name="wallet_create_payid_empty">Zuerst geben Sie die gewünschte PayString ein</string>
<string name="initial_message_scan_header">Legen Sie die Karte zum Scannen an</string>
<string name="initial_message_tap_header">Legen Sie die Karte an</string>

View file

@ -41,18 +41,18 @@
<string name="wallet_pending_tx_receiving">Réception \u0020</string>
<string name="wallet_pending_tx_receiving_address_format">de %s </string>
<string name="wallet_address_button_explore">Explorer l\'adresse </string>
<string name="wallet_address_button_create_payid">Créer PayID</string>
<string name="wallet_address_button_create_payid">Créer PayString</string>
<string name="wallet_qr_title_format">Portefeuille %s</string>
<string name="wallet_create_payid">Créer PayID</string>
<string name="wallet_create_payid">Créer PayString</string>
<string name="wallet_create_payid_card_format">Carte: %s</string>
<string name="wallet_create_payid_hint">Dénomination PayID</string>
<string name="wallet_create_payid_hint">Dénomination PayString</string>
<string name="wallet_create_payid_domain">$payid.tangem.com</string>
<string name="wallet_create_payid_button_title">Créer </string>
<string name="wallet_create_payid_info">Votre PayID est une information qui vous est propre, comme un numéro de téléphone, une adresse du courrier électronique ou un ABN.</string>
<string name="wallet_create_payid_success_message">PayID a été créé avec succès et copié dans le presse-papiers </string>
<string name="wallet_create_payid_error_message">Mauvaise réponse lors de la création de PayID </string>
<string name="wallet_create_payid_error_already_created">Ce PayID existe déjà. Essayez une autre variante.</string>
<string name="send_destination_hint_address_payid">Adresse ou PayID</string>
<string name="wallet_create_payid_info">Votre PayString est une information qui vous est propre, comme un numéro de téléphone, une adresse du courrier électronique ou un ABN.</string>
<string name="wallet_create_payid_success_message">PayString a été créé avec succès et copié dans le presse-papiers </string>
<string name="wallet_create_payid_error_message">Mauvaise réponse lors de la création de PayString </string>
<string name="wallet_create_payid_error_already_created">Ce PayString existe déjà. Essayez une autre variante.</string>
<string name="send_destination_hint_address_payid">Adresse ou PayString</string>
<string name="send_destination_hint_address">Adresse</string>
<string name="send_title">Envoyer </string>
<string name="send_network_fee_title">Commissions du réseau </string>
@ -105,7 +105,7 @@
<string name="initial_message_create_wallet_body">Touchez, pour créer un portefeuille </string>
<string name="initial_message_change_access_code_body">Touchez, pour modifier le code daccès </string>
<string name="initial_message_change_passcode_body">Touchez, pour modifier le mot de passe </string>
<string name="payid_service_error_loading">PayID incorrect </string>
<string name="payid_service_error_loading">PayString incorrect </string>
<string name="disclaimer_text">
@ -165,10 +165,10 @@ Avertissement
<string name="send_error_fee_request_failed">Échec de réception des commissions</string>
<string name="send_error_unknown">Erreur inconnue</string>
<string name="send_error_payid_verification_failed">La vérification de PayID a échoué</string>
<string name="send_error_payid_unsupported_by_blockchain">PayID non pris en charge par la blockchain</string>
<string name="send_error_payid_not_registered">PayID non enregistré</string>
<string name="send_error_payid_request_failed">La demande de PayID a échoué</string>
<string name="send_error_payid_verification_failed">La vérification de PayString a échoué</string>
<string name="send_error_payid_unsupported_by_blockchain">PayString non pris en charge par la blockchain</string>
<string name="send_error_payid_not_registered">PayString non enregistré</string>
<string name="send_error_payid_request_failed">La demande de PayString a échoué</string>
<string name="send_error_address_same_as_wallet">Ladresse est la même que celle de votre portefeuille</string>
<string name="send_error_insufficient_balance">Solde insuffisant</string>
<string name="send_error_blockchain_internal">Erreur interne de la blockchain</string>
@ -176,7 +176,7 @@ Avertissement
<string name="send_error_dust_amount_format">Le montant minimal est de %ы</string>
<string name="send_error_dust_change">Le reste est trop petit</string>
<string name="wallet_notification_address_copied">Ladresse a été copiée avec succès</string>
<string name="wallet_create_payid_empty">Saisissez dabord le PayID requis</string>
<string name="wallet_create_payid_empty">Saisissez dabord le PayString requis</string>
<string name="initial_message_scan_header">Posez pour scanner</string>
<string name="initial_message_tap_header">Posez la carte</string>
<string name="details_notification_erase_wallet_not_possible">Votre solde sur ce portefeuille n\'est pas nul, ou vous avez des transactions non confirmées. Impossible de supprimer la fonction du portefeuille</string>

View file

@ -39,18 +39,18 @@
<string name="wallet_pending_tx_receiving">Ricevi\u0020</string>
<string name="wallet_pending_tx_receiving_address_format"> da %s </string>
<string name="wallet_address_button_explore">Cerca indirizzo</string>
<string name="wallet_address_button_create_payid">Crea PayID</string>
<string name="wallet_address_button_create_payid">Crea PayString</string>
<string name="wallet_qr_title_format">portafoglio %s</string>
<string name="wallet_create_payid">Crea PayID</string>
<string name="wallet_create_payid">Crea PayString</string>
<string name="wallet_create_payid_card_format">Scheda: %s</string>
<string name="wallet_create_payid_hint">Nome PayID</string>
<string name="wallet_create_payid_hint">Nome PayString</string>
<string name="wallet_create_payid_domain">$payid.tangem.com</string>
<string name="wallet_create_payid_button_title">Crea</string>
<string name="wallet_create_payid_info">Il tuo PayID informazioni per te uniche, come il tuo numero di telefono, indirizzo email o ABN.</string>
<string name="wallet_create_payid_success_message">PayID creato con successo e copiato negli appunti</string>
<string name="wallet_create_payid_error_message">Errore di risposta durante la creazione del PayID</string>
<string name="wallet_create_payid_error_already_created">Questo PayID esiste già. Scegline un altro.</string>
<string name="send_destination_hint_address_payid">Indirizzo o PayID</string>
<string name="wallet_create_payid_info">Il tuo PayString informazioni per te uniche, come il tuo numero di telefono, indirizzo email o ABN.</string>
<string name="wallet_create_payid_success_message">PayString creato con successo e copiato negli appunti</string>
<string name="wallet_create_payid_error_message">Errore di risposta durante la creazione del PayString</string>
<string name="wallet_create_payid_error_already_created">Questo PayString esiste già. Scegline un altro.</string>
<string name="send_destination_hint_address_payid">Indirizzo o PayString</string>
<string name="send_destination_hint_address">Indirizzo</string>
<string name="send_title">Invia</string>
<string name="send_network_fee_title">Rete libera</string>
@ -102,7 +102,7 @@
<string name="initial_message_create_wallet_body">Avvicina per creare il portafoglio</string>
<string name="initial_message_change_access_code_body">Avvicina per modificare il codice di accesso</string>
<string name="initial_message_change_passcode_body">Avvicina per modificare la password</string>
<string name="payid_service_error_loading">PayID non valido</string>
<string name="payid_service_error_loading">PayString non valido</string>
<string name="disclaimer_text">
@ -166,10 +166,10 @@ Questa nota legale è stata modificata l\'ultima volta 01.10.2020.
<string name="wallet_notification_no_internet">Nessuna connessione a Internet</string>
<string name="send_error_unknown">Errore sconosciuto</string>
<string name="send_error_payid_verification_failed">Verifica del PayID fallita</string>
<string name="send_error_payid_unsupported_by_blockchain">PayID non supportato dalla blockchain</string>
<string name="send_error_payid_not_registered">PayID non registrato</string>
<string name="send_error_payid_request_failed">Richiesta PayID fallita</string>
<string name="send_error_payid_verification_failed">Verifica del PayString fallita</string>
<string name="send_error_payid_unsupported_by_blockchain">PayString non supportato dalla blockchain</string>
<string name="send_error_payid_not_registered">PayString non registrato</string>
<string name="send_error_payid_request_failed">Richiesta PayString fallita</string>
<string name="send_error_address_same_as_wallet">L\'indirizzo corrisponde all\'indirizzo del tuo portafoglio</string>
<string name="send_error_insufficient_balance">Saldo insufficiente</string>
@ -181,7 +181,7 @@ Questa nota legale è stata modificata l\'ultima volta 01.10.2020.
<string name="wallet_notification_address_copied">L\'indirizzo è stato copiato con successo</string>
<string name="wallet_create_payid_empty">Inserire prima il PayID desiderato</string>
<string name="wallet_create_payid_empty">Inserire prima il PayString desiderato</string>
<string name="initial_message_scan_header">Avvicina per scansionare</string>
<string name="initial_message_tap_header">Avvicina la scheda</string>

View file

@ -26,6 +26,8 @@
<color name="buttonGray">#F4F5F6</color>
<color name="textGray">#DE000000</color>
<color name="twins_dark">#14181D</color>
<color name="lightGray1">#F8F8FB</color>
<color name="lightGray4">#C7C7CC</color>
@ -39,5 +41,7 @@
<color name="blue1">#0029FF</color>
<color name="blue2">#1F50FF</color>
<color name="blue3">#CBE4FF</color>
<color name="blue_pale">#E0E6FA</color>
</resources>

View file

@ -33,5 +33,6 @@
<dimen name="btn_rounded_size">44dp</dimen>
<dimen name="text_size_amount_to_send">32sp</dimen>
<dimen name="dimen16">16dp</dimen>
</resources>

View file

@ -49,20 +49,20 @@
<string name="wallet_pending_tx_sending">Sending\u0020</string>
<string name="wallet_address_button_explore">Explore address</string>
<string name="wallet_address_button_create_payid">Create PayID</string>
<string name="wallet_address_button_create_payid">Create PayString</string>
<string name="wallet_qr_title_format">%s wallet</string>
<string name="wallet_create_payid">Create PayID</string>
<string name="wallet_create_payid">Create PayString</string>
<string name="wallet_create_payid_card_format">Card: %s</string>
<string name="wallet_create_payid_hint">PayID name</string>
<string name="wallet_create_payid_hint">PayString name</string>
<string name="wallet_create_payid_domain">$payid.tangem.com</string>
<string name="wallet_create_payid_button_title">Create</string>
<string name="wallet_create_payid_info">Your PayID is information unique to you, like your phone number, email or ABN.</string>
<string name="wallet_create_payid_success_message">PayID created successfully and copied to clipboard</string>
<string name="wallet_create_payid_error_message">Error response while creating PayID</string>
<string name="wallet_create_payid_error_already_created">This PayID already exists. Try a different one.</string>
<string name="wallet_create_payid_info">Your PayString is information unique to you, like your phone number, email or ABN.</string>
<string name="wallet_create_payid_success_message">PayString created successfully and copied to clipboard</string>
<string name="wallet_create_payid_error_message">Error response while creating PayString</string>
<string name="wallet_create_payid_error_already_created">This PayString already exists. Try a different one.</string>
<string name="send_destination_hint_address_payid">Address or PayID</string>
<string name="send_destination_hint_address_payid">Address or PayString</string>
<string name="send_destination_hint_address">Address</string>
<string name="send_title">Send</string>
<string name="send_network_fee_title">Network fee</string>
@ -119,7 +119,7 @@
<string name="initial_message_change_access_code_body">Tap to change the access code</string>
<string name="initial_message_change_passcode_body">Tap to change the passcode</string>
<string name="payid_service_error_loading">Invalid PayID</string>
<string name="payid_service_error_loading">Invalid PayString</string>
<string name="disclaimer_text">Legal Disclaimer
\n
@ -177,10 +177,10 @@
<string name="wallet_notification_no_internet">No internet connection</string>
<string name="send_error_unknown">Unknown error</string>
<string name="send_error_payid_verification_failed">PayID verification failed</string>
<string name="send_error_payid_unsupported_by_blockchain">PayID unsupported by blockchain</string>
<string name="send_error_payid_not_registered">PayID not registered</string>
<string name="send_error_payid_request_failed">PayID request failed</string>
<string name="send_error_payid_verification_failed">PayString verification failed</string>
<string name="send_error_payid_unsupported_by_blockchain">PayString unsupported by blockchain</string>
<string name="send_error_payid_not_registered">PayString not registered</string>
<string name="send_error_payid_request_failed">PayString request failed</string>
<string name="send_error_address_same_as_wallet">Address is the same as wallet address</string>
<string name="send_error_insufficient_balance">Insufficient balance</string>
@ -192,20 +192,16 @@
<string name="wallet_notification_address_copied">Address was copied to clipboard</string>
<string name="wallet_create_payid_empty">Enter desired PayID first</string>
<string name="wallet_create_payid_empty">Enter desired PayString first</string>
<string name="initial_message_scan_header">Tap to scan</string>
<string name="initial_message_tap_header">Tap the card</string>
<string name="details_notification_erase_wallet_not_possible">You balance on this wallet is not zero, or you have unconfirmed transactions</string>
<string name="details_notification_erase_wallet_not_possible">Your balance on this wallet is not zero, or you have unconfirmed transactions</string>
<string name="details_notification_security_option_already_active">This is the currently active option</string>
<string name="xtz_withdrawal_message_warning">To avoid paying an increased commission the next time you top up your wallet, reduce the amount by %s XTZ</string>
<string name="xtz_withdrawal_message_reduce">Reduce by %s XTZ</string>
<string name="xtz_withdrawal_message_ignore">No, send all</string>
</resources>

View file

@ -1,3 +1,36 @@
<resources>
<string name="wallet_button_topup" translatable="false">Top Up</string>
<string name="wallet_address_chip_default" translatable="false">Default</string>
<string name="wallet_address_chip_compatibility" translatable="false">Compatibility</string>
<string name="wallet_address_chip_legacy" translatable="false">Legacy</string>
<string name="wallet_button_create_twin_wallet" translatable="false">Create twin wallet</string>
<string name="wallet_error_empty_twin_card" translatable="false">Create twin wallet</string>
<string name="wallet_error_empty_twin_card_subtitle" translatable="false"> Generate wallet keys on both cards to start using your Twins</string>
<string name="twins_onboarding_title" translatable="false">Tangem Twin</string>
<string name="twins_onboarding_subtitle" translatable="false">One wallet. Two cards.</string>
<string name="twins_onboarding_description_format" translatable="false">This one that you are holding in your hands
and the other one with number %s.\n\nBoth cards can be used to extract funds from
this wallet.
</string>
<string name="wallet_twins_chip_format" translatable="false">Card %s of 2</string>
<string name="details_row_title_twins_recreate" translatable="false">Re-create twin wallet</string>
<string name="details_twins_recreate_warning" translatable="false">This action is irreversible. You will not have access to the old wallet.\nThe wallet re-creation procedure consists of three steps. You must complete it to the end, otherwise you will have to start from the beginning.</string>
<string name="details_twins_recreate_toolbar" translatable="false">Re-create wallet</string>
<string name="details_twins_recreate_step_format" translatable="false">Step %s</string>
<string name="details_twins_recreate_title_format" translatable="false">Tap the #%s twin card</string>
<string name="details_twins_recreate_title_preparing" translatable="false">Preparing card</string>
<string name="details_twins_recreate_title_creating_wallet" translatable="false">Creating wallet</string>
<string name="details_twins_recreate_subtitle" translatable="false">The wallet creation procedure consists of three steps. You must complete it to the end, otherwise you will have to start from the beginning.</string>
<string name="details_twins_recreate_button_format" translatable="false">Tap the card #%s</string>
<string name="details_twins_recreate_alert" translatable="false">You have already started the process of recreating twin wallet. If you interrupt it, you won\'t be able to use your twin cards until you start it again and complete recreating the wallet.</string>
<string name="notification_twins_recreate_success" translatable="false">The twin address was successfully created</string>
<string name="common_start" translatable="false">Start</string>
<string name="common_back" translatable="false">Back</string>
</resources>

View file

@ -51,7 +51,7 @@
<item name="android:backgroundTint">@color/selector_btn_black</item>
</style>
<style name="ChipNetworkFee" parent="@style/Widget.MaterialComponents.Chip.Choice">
<style name="TapChip" parent="@style/Widget.MaterialComponents.Chip.Choice">
<item name="chipBackgroundColor">@color/selector_chip_background</item>
<item name="chipStrokeColor">@color/selector_chip_stroke</item>
<item name="chipStrokeWidth">1dp</item>

View file

@ -1,4 +1,4 @@
ext.versions = [
kotlin : '1.4.0',
build_gradle: '4.1.0',
kotlin : '1.4.10',
build_gradle: '4.1.1',
]