diff --git a/app/build.gradle b/app/build.gradle index de5564ec62..e35b177dba 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/assets/features_dev.json b/app/src/main/assets/features_dev.json new file mode 100644 index 0000000000..e416a046a7 --- /dev/null +++ b/app/src/main/assets/features_dev.json @@ -0,0 +1,18 @@ +[ + { + "name": "isWalletPayIdEnabled", + "value": true + }, + { + "name": "isSendingToPayIdEnabled", + "value": true + }, + { + "name": "isTopUpEnabled", + "value": true + }, + { + "name": "isCreatingTwinCardsAllowed", + "value": true + } +] \ No newline at end of file diff --git a/app/src/main/assets/features_prod.json b/app/src/main/assets/features_prod.json new file mode 100644 index 0000000000..e416a046a7 --- /dev/null +++ b/app/src/main/assets/features_prod.json @@ -0,0 +1,18 @@ +[ + { + "name": "isWalletPayIdEnabled", + "value": true + }, + { + "name": "isSendingToPayIdEnabled", + "value": true + }, + { + "name": "isTopUpEnabled", + "value": true + }, + { + "name": "isCreatingTwinCardsAllowed", + "value": true + } +] \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index d8e89fe868..2a20c88628 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -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(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) } } diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index b19bfc730e..5e43828af0 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -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)) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TapConfig.kt b/app/src/main/java/com/tangem/tap/TapConfig.kt deleted file mode 100644 index 219dbfb94a..0000000000 --- a/app/src/main/java/com/tangem/tap/TapConfig.kt +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt index 8057bcae9e..9dddeee4f2 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt @@ -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?) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt index 6e7f460773..5b468233ea 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt @@ -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 ) } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index bb69887342..56f0d70061 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -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() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt index 11d5aa6ca4..9491bc253a 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index c0e641bffe..9fb73bdfcb 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index 66fbb80140..2b27751177 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -33,6 +33,9 @@ fun globalReducer(action: Action, state: AppState): GlobalState { globalState } } + is GlobalAction.SetConfigManager -> { + globalState.copy(configManager = action.configManager) + } else -> globalState } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 9da6906d9c..f9e05cdeac 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt index 5edfad540b..393a4e7e77 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt @@ -9,4 +9,7 @@ data class NavigationState( val activity: WeakReference? = null ) : StateType -enum class AppScreen { Home, Wallet, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer } \ No newline at end of file +enum class AppScreen { + Home, Wallet, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer, + CreateTwinWalletWarning, CreateTwinWallet, TwinsOnboarding +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index a1971bad03..77d26702ab 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -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 runTaskAsync( - runnable: CardSessionRunnable, cardId: String? = null, initialMessage: Message? = null + suspend fun runTaskAsync( + runnable: CardSessionRunnable, cardId: String? = null, initialMessage: Message? = null, ): CompletionResult = withContext(Dispatchers.IO) { suspendCoroutine { continuation -> @@ -76,9 +78,13 @@ class TangemSdkManager(val activity: ComponentActivity) { } private suspend fun runTaskAsyncReturnOnMain( - runnable: CardSessionRunnable, cardId: String? = null, initialMessage: Message? = null + runnable: CardSessionRunnable, cardId: String? = null, initialMessage: Message? = null, ): CompletionResult { val result = runTaskAsync(runnable, cardId, initialMessage) return withContext(Dispatchers.Main) { result } } + + fun changeDisplayedCardIdNumbersCount(card: Card) { + tangemSdk.config.cardIdDisplayedNumbersCount = if (card.isTwinCard()) 4 else null + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 622c29ef40..36dd3c238c 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -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?>>() - 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? { 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) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt b/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt index de1640fd9c..1e18167a52 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt @@ -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" } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TopUpHelper.kt b/app/src/main/java/com/tangem/tap/domain/TopUpHelper.kt index 7494073c72..5f5ae80096 100644 --- a/app/src/main/java/com/tangem/tap/domain/TopUpHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/TopUpHelper.kt @@ -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() } diff --git a/app/src/main/java/com/tangem/tap/domain/config/ConfigLoader.kt b/app/src/main/java/com/tangem/tap/domain/config/ConfigLoader.kt new file mode 100644 index 0000000000..60bb343e02 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/config/ConfigLoader.kt @@ -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> = moshi.adapter(featureType) + val valuesType = Types.newParameterizedType(List::class.java, ConfigValueModel::class.java) + val valuesAdapter: JsonAdapter> = 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> = moshi.adapter(featureType) + onComplete(ConfigModel(featureAdapter.fromJson(jsonConfig) ?: listOf(), listOf())) + } else { + onComplete(emptyConfig) + } + }.addOnFailureListener { + FirebaseCrashlytics.getInstance().recordException(it) + onComplete(emptyConfig) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/config/ConfigManager.kt b/app/src/main/java/com/tangem/tap/domain/config/ConfigManager.kt new file mode 100644 index 0000000000..c9b0d20151 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/config/ConfigManager.kt @@ -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" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/config/JsonModels.kt b/app/src/main/java/com/tangem/tap/domain/config/JsonModels.kt new file mode 100644 index 0000000000..1b7272765b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/config/JsonModels.kt @@ -0,0 +1,26 @@ +package com.tangem.tap.domain.config + +/** +[REDACTED_AUTHOR] + */ + +interface BaseConfigModel { + val name: String + val value: V? +} + +class FeatureModel( + override val name: String, + override val value: Boolean +) : BaseConfigModel + +class ConfigValueModel( + override val name: String, + override val value: String +) : BaseConfigModel + +class ConfigModel(val features: List?, val configValues: List?) { + companion object { + fun empty(): ConfigModel = ConfigModel(listOf(), listOf()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt index 93a2617258..dfa1752929 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt @@ -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 { @@ -42,31 +47,75 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable - 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) -> 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) -> 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)) { diff --git a/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt new file mode 100644 index 0000000000..7296c72a11 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt @@ -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 { + override val requiresPin2 = false + + override fun run(session: CardSession, callback: (result: CompletionResult) -> 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) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt new file mode 100644 index 0000000000..5587faa1ab --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt @@ -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 { + override val requiresPin2 = true + + override fun run(session: CardSession, callback: (result: CompletionResult) -> 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) -> 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)) + } + } + } +} diff --git a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt new file mode 100644 index 0000000000..073c47b344 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt @@ -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 { + override val requiresPin2 = true + + override fun run(session: CardSession, callback: (result: CompletionResult) -> 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)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt new file mode 100644 index 0000000000..43d313f8a6 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt @@ -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 { + 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)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinsHelper.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinsHelper.kt new file mode 100644 index 0000000000..a1ca7dcc53 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinsHelper.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/twins/WriteProtectedIssuerDataTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/WriteProtectedIssuerDataTask.kt new file mode 100644 index 0000000000..30d3b688dd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/twins/WriteProtectedIssuerDataTask.kt @@ -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 { + override val requiresPin2 = true + + override fun run(session: CardSession, callback: (result: CompletionResult) -> 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 + ) -> 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index 448e0a48ad..2ec73c0f00 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -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? = 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) : 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 } } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index e3a4f73b18..d603c66c39 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -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 = { 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)) } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index 5f4641aed7..593f31d21f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index 7c152c57ee..4730507ebc 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -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? = null, -) +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/twins/CreateTwinWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/twins/CreateTwinWalletMiddleware.kt new file mode 100644 index 0000000000..38cfac39bc --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/redux/twins/CreateTwinWalletMiddleware.kt @@ -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 -> { + + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/twins/CreateTwinWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/twins/CreateTwinWalletReducer.kt new file mode 100644 index 0000000000..7de266d076 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/redux/twins/CreateTwinWalletReducer.kt @@ -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 + } + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/twins/CreateTwinWalletState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/twins/CreateTwinWalletState.kt new file mode 100644 index 0000000000..488ac431bc --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/redux/twins/CreateTwinWalletState.kt @@ -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 } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt index df604f266d..262482b360 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt @@ -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 { + + 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 + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/twins/TwinWalletWarningFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/twins/TwinWalletWarningFragment.kt new file mode 100644 index 0000000000..678dddf2bb --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/twins/TwinWalletWarningFragment.kt @@ -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) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt index a9e8b493d8..066b2b83db 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 987e3c1b3b..aaf52f3564 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -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 { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt index 911a51af79..8a22ed35b0 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt @@ -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 { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressPayIdMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressPayIdMiddleware.kt index 680654633b..4380bdbf49 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressPayIdMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressPayIdMiddleware.kt @@ -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 + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index 845373fe34..ecc52b09f3 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -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 = { 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, 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)) +} + diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AddressPayIdReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AddressPayIdReducer.kt index e7c726241f..bca0259bee 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AddressPayIdReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AddressPayIdReducer.kt @@ -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) } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt index c6f6511773..1ecf9ea6bb 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt @@ -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 } } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/AddressPayIdState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/AddressPayIdState.kt index dfcc6ca4c5..0ba27e4466 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/AddressPayIdState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/AddressPayIdState.kt @@ -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 { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt index 68954619a6..2e3a6e4955 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt @@ -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 } } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index b9d542836f..9714886b7b 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt index bdd2e9f3c4..711493fb25 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt @@ -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) { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt index e129bf08b3..0656a9583f 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt @@ -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() + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletMiddleware.kt index d1ea1faa17..2f2c041924 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletMiddleware.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt index 10fc1610b8..533121bbe8 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt @@ -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() + // 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 } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt index d6c916d79e..29b239d41e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt @@ -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 = 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?) : 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 +) + 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 ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt index cc65bff46d..ce7fec6959 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt @@ -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() diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index 2be04ad3c9..464764cf68 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt @@ -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 + 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 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 + 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 { - 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 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 + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/TwinsOnboardingFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/TwinsOnboardingFragment.kt new file mode 100644 index 0000000000..a20b06aa72 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/TwinsOnboardingFragment.kt @@ -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)) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/coinmarketcap/CoinMarketCapApi.kt b/app/src/main/java/com/tangem/tap/network/coinmarketcap/CoinMarketCapApi.kt index 47889ff06d..5d78846280 100644 --- a/app/src/main/java/com/tangem/tap/network/coinmarketcap/CoinMarketCapApi.kt +++ b/app/src/main/java/com/tangem/tap/network/coinmarketcap/CoinMarketCapApi.kt @@ -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()) } } diff --git a/app/src/main/java/com/tangem/tap/network/coinmarketcap/CoinMarketCapService.kt b/app/src/main/java/com/tangem/tap/network/coinmarketcap/CoinMarketCapService.kt index 4dc7931623..fffc439820 100644 --- a/app/src/main/java/com/tangem/tap/network/coinmarketcap/CoinMarketCapService.kt +++ b/app/src/main/java/com/tangem/tap/network/coinmarketcap/CoinMarketCapService.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt index 47f832dbac..08346630f3 100644 --- a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt +++ b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt @@ -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> 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" } } \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_payid.png b/app/src/main/res/drawable-hdpi/ic_payid.png deleted file mode 100644 index 79c7f1ec4f..0000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_payid.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_payid.png b/app/src/main/res/drawable-mdpi/ic_payid.png deleted file mode 100644 index 1af851efc8..0000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_payid.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_payid.png b/app/src/main/res/drawable-xhdpi/ic_payid.png deleted file mode 100644 index 914e7f67b9..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_payid.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_payid.png b/app/src/main/res/drawable-xxhdpi/ic_payid.png deleted file mode 100644 index f7dbee2905..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_payid.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_payid.png b/app/src/main/res/drawable-xxxhdpi/ic_payid.png deleted file mode 100644 index bb99030d50..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_payid.png and /dev/null differ diff --git a/app/src/main/res/drawable/ic_group_1258.xml b/app/src/main/res/drawable/ic_group_1258.xml new file mode 100644 index 0000000000..b15674c9de --- /dev/null +++ b/app/src/main/res/drawable/ic_group_1258.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_payid.xml b/app/src/main/res/drawable/ic_payid.xml new file mode 100644 index 0000000000..822e97afc9 --- /dev/null +++ b/app/src/main/res/drawable/ic_payid.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_twin_cards_background_warning.xml b/app/src/main/res/drawable/ic_twin_cards_background_warning.xml new file mode 100644 index 0000000000..54cccb6fa8 --- /dev/null +++ b/app/src/main/res/drawable/ic_twin_cards_background_warning.xml @@ -0,0 +1,25 @@ + + + + + + diff --git a/app/src/main/res/drawable/rectangle_twin_background.xml b/app/src/main/res/drawable/rectangle_twin_background.xml new file mode 100644 index 0000000000..73ca08b417 --- /dev/null +++ b/app/src/main/res/drawable/rectangle_twin_background.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_chip.xml b/app/src/main/res/drawable/shape_chip.xml new file mode 100644 index 0000000000..9544d9916d --- /dev/null +++ b/app/src/main/res/drawable/shape_chip.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml index 65db1c16db..7a99598875 100644 --- a/app/src/main/res/layout/fragment_details.xml +++ b/app/src/main/res/layout/fragment_details.xml @@ -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" diff --git a/app/src/main/res/layout/fragment_details_twin_cards.xml b/app/src/main/res/layout/fragment_details_twin_cards.xml new file mode 100644 index 0000000000..7549f32998 --- /dev/null +++ b/app/src/main/res/layout/fragment_details_twin_cards.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_details_twin_cards_warning.xml b/app/src/main/res/layout/fragment_details_twin_cards_warning.xml new file mode 100644 index 0000000000..1343fbe116 --- /dev/null +++ b/app/src/main/res/layout/fragment_details_twin_cards_warning.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_disclaimer.xml b/app/src/main/res/layout/fragment_disclaimer.xml index 3efa0e31fa..0c771b9995 100644 --- a/app/src/main/res/layout/fragment_disclaimer.xml +++ b/app/src/main/res/layout/fragment_disclaimer.xml @@ -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" diff --git a/app/src/main/res/layout/fragment_twin_cards.xml b/app/src/main/res/layout/fragment_twin_cards.xml new file mode 100644 index 0000000000..0bd670bcc3 --- /dev/null +++ b/app/src/main/res/layout/fragment_twin_cards.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_wallet.xml b/app/src/main/res/layout/fragment_wallet.xml index 1f139adb02..9ed6acedb3 100644 --- a/app/src/main/res/layout/fragment_wallet.xml +++ b/app/src/main/res/layout/fragment_wallet.xml @@ -1,6 +1,7 @@ + + + + + + + + app:layout_constraintTop_toBottomOf="@id/barrier" /> + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/chip_group_segwit"> + app:layout_constraintTop_toBottomOf="@id/chip_group_segwit"> + app:layout_constraintTop_toBottomOf="@id/v_payid_divider" + app:layout_constraintBottom_toBottomOf="parent"/> + tools:text="romafdffdfdfn$payid.tangem.com" /> diff --git a/app/src/main/res/layout/layout_twin_cards.xml b/app/src/main/res/layout/layout_twin_cards.xml new file mode 100644 index 0000000000..cb2808a482 --- /dev/null +++ b/app/src/main/res/layout/layout_twin_cards.xml @@ -0,0 +1,38 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_twin_cards_orange.xml b/app/src/main/res/layout/layout_twin_cards_orange.xml new file mode 100644 index 0000000000..f524c5dd49 --- /dev/null +++ b/app/src/main/res/layout/layout_twin_cards_orange.xml @@ -0,0 +1,38 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d2b3bb460e..ae4aa7218f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -40,18 +40,18 @@ Erhalten\u0020 von %s Adresse erkunden - PayID erstellen + PayString erstellen %s Wallet - PayID erstellen + PayString erstellen Karte: %s - PayID Name + PayString Name $payid.tangem.com Erstellen - PayID sind Ihre einzigartigen Identifikationsdaten wie Telefonnummer, E-Mail-Adresse oder ABN. - Ihre PayID wurde erfolgreich erstellt und zum Clipboard kopiert - Fehlerantwort beim Erstellen der ZahlungsID - Diese PayID existiert bereits. Versuchen Sie mit einer anderen erneut. - Adresse oder PayID + PayString sind Ihre einzigartigen Identifikationsdaten wie Telefonnummer, E-Mail-Adresse oder ABN. + Ihre PayString wurde erfolgreich erstellt und zum Clipboard kopiert + Fehlerantwort beim Erstellen der PayString + Diese PayString existiert bereits. Versuchen Sie mit einer anderen erneut. + Adresse oder PayString Adresse Absenden Netzgebühr @@ -105,7 +105,7 @@ Tippen Sie um ein Wallet zu erstellen Tippen Sie um den Zugangscode zu ändern Tippen Sie um den Passcode zu ändern - Ungültige ZahlungsID + Ungültige PayString @@ -167,10 +167,10 @@ RECHTLICHER HAFTUNGSAUSSCHLUSS Erhalt der Gebühr fehlgeschlagen Unbekannter Fehler - Verifikation der PayID ist fehlgeschlagen - PayID wird von der Blockchain nicht unterstützt - PayID ist nicht registriert - PayID-Anfrage ist fehlgeschlagen + Verifikation der PayString ist fehlgeschlagen + PayString wird von der Blockchain nicht unterstützt + PayString ist nicht registriert + PayString-Anfrage ist fehlgeschlagen Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein Nicht genug Geld @@ -182,7 +182,7 @@ RECHTLICHER HAFTUNGSAUSSCHLUSS Die Adresse wurde erfolgreich kopiert - Zuerst geben Sie die gewünschte PayID ein + Zuerst geben Sie die gewünschte PayString ein Legen Sie die Karte zum Scannen an Legen Sie die Karte an diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 6c2934f23c..043edee6a3 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -41,18 +41,18 @@ Réception \u0020 de %s Explorer l\'adresse - Créer PayID + Créer PayString Portefeuille %s - Créer PayID + Créer PayString Carte: %s - Dénomination PayID + Dénomination PayString $payid.tangem.com Créer - 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. - PayID a été créé avec succès et copié dans le presse-papiers - Mauvaise réponse lors de la création de PayID - Ce PayID existe déjà. Essayez une autre variante. - Adresse ou PayID + 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. + PayString a été créé avec succès et copié dans le presse-papiers + Mauvaise réponse lors de la création de PayString + Ce PayString existe déjà. Essayez une autre variante. + Adresse ou PayString Adresse Envoyer Commissions du réseau @@ -105,7 +105,7 @@ Touchez, pour créer un portefeuille Touchez, pour modifier le code d’accès Touchez, pour modifier le mot de passe - PayID incorrect + PayString incorrect @@ -165,10 +165,10 @@ Avertissement Échec de réception des commissions Erreur inconnue - La vérification de PayID a échoué - PayID non pris en charge par la blockchain - PayID non enregistré - La demande de PayID a échoué + La vérification de PayString a échoué + PayString non pris en charge par la blockchain + PayString non enregistré + La demande de PayString a échoué L’adresse est la même que celle de votre portefeuille Solde insuffisant Erreur interne de la blockchain @@ -176,7 +176,7 @@ Avertissement Le montant minimal est de %ы Le reste est trop petit L’adresse a été copiée avec succès - Saisissez d’abord le PayID requis + Saisissez d’abord le PayString requis Posez pour scanner Posez la carte Votre solde sur ce portefeuille n\'est pas nul, ou vous avez des transactions non confirmées. Impossible de supprimer la fonction du portefeuille diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 909b654b6b..08315d8822 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -39,18 +39,18 @@ Ricevi\u0020 da %s Cerca indirizzo - Crea PayID + Crea PayString portafoglio %s - Crea PayID + Crea PayString Scheda: %s - Nome PayID + Nome PayString $payid.tangem.com Crea - Il tuo PayID – informazioni per te uniche, come il tuo numero di telefono, indirizzo email o ABN. - PayID creato con successo e copiato negli appunti - Errore di risposta durante la creazione del PayID - Questo PayID esiste già. Scegline un altro. - Indirizzo o PayID + Il tuo PayString – informazioni per te uniche, come il tuo numero di telefono, indirizzo email o ABN. + PayString creato con successo e copiato negli appunti + Errore di risposta durante la creazione del PayString + Questo PayString esiste già. Scegline un altro. + Indirizzo o PayString Indirizzo Invia Rete libera @@ -102,7 +102,7 @@ Avvicina per creare il portafoglio Avvicina per modificare il codice di accesso Avvicina per modificare la password - PayID non valido + PayString non valido @@ -166,10 +166,10 @@ Questa nota legale è stata modificata l\'ultima volta 01.10.2020. Nessuna connessione a Internet Errore sconosciuto - Verifica del PayID fallita - PayID non supportato dalla blockchain - PayID non registrato - Richiesta PayID fallita + Verifica del PayString fallita + PayString non supportato dalla blockchain + PayString non registrato + Richiesta PayString fallita L\'indirizzo corrisponde all\'indirizzo del tuo portafoglio Saldo insufficiente @@ -181,7 +181,7 @@ Questa nota legale è stata modificata l\'ultima volta 01.10.2020. L\'indirizzo è stato copiato con successo - Inserire prima il PayID desiderato + Inserire prima il PayString desiderato Avvicina per scansionare Avvicina la scheda diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 1883882991..2b8a0270b6 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -26,6 +26,8 @@ #F4F5F6 #DE000000 + #14181D + #F8F8FB #C7C7CC @@ -39,5 +41,7 @@ #0029FF #1F50FF #CBE4FF + #E0E6FA + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 49798d893b..fd2c18a380 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -33,5 +33,6 @@ 44dp 32sp + 16dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 80f2e89d5f..05ea986b66 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -49,20 +49,20 @@ Sending\u0020 Explore address - Create PayID + Create PayString %s wallet - Create PayID + Create PayString Card: %s - PayID name + PayString name $payid.tangem.com Create - Your PayID is information unique to you, like your phone number, email or ABN. - PayID created successfully and copied to clipboard - Error response while creating PayID - This PayID already exists. Try a different one. + Your PayString is information unique to you, like your phone number, email or ABN. + PayString created successfully and copied to clipboard + Error response while creating PayString + This PayString already exists. Try a different one. - Address or PayID + Address or PayString Address Send Network fee @@ -119,7 +119,7 @@ Tap to change the access code Tap to change the passcode - Invalid PayID + Invalid PayString Legal Disclaimer \n @@ -177,10 +177,10 @@ No internet connection Unknown error - PayID verification failed - PayID unsupported by blockchain - PayID not registered - PayID request failed + PayString verification failed + PayString unsupported by blockchain + PayString not registered + PayString request failed Address is the same as wallet address Insufficient balance @@ -192,20 +192,16 @@ Address was copied to clipboard - Enter desired PayID first + Enter desired PayString first Tap to scan Tap the card - You balance on this wallet is not zero, or you have unconfirmed transactions + Your balance on this wallet is not zero, or you have unconfirmed transactions This is the currently active option To avoid paying an increased commission the next time you top up your wallet, reduce the amount by %s XTZ Reduce by %s XTZ No, send all - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings_untranslated.xml b/app/src/main/res/values/strings_untranslated.xml index 834037ecad..2f0501ac9f 100644 --- a/app/src/main/res/values/strings_untranslated.xml +++ b/app/src/main/res/values/strings_untranslated.xml @@ -1,3 +1,36 @@ Top Up + Default + Compatibility + Legacy + + Create twin wallet + Create twin wallet + Generate wallet keys on both cards to start using your Twins + + Tangem Twin + One wallet. Two cards. + 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. + + + Card %s of 2 + + Re-create twin wallet + 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. + Re-create wallet + Step %s + Tap the #%s twin card + Preparing card + Creating wallet + The wallet creation procedure consists of three steps. You must complete it to the end, otherwise you will have to start from the beginning. + Tap the card #%s + 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. + + The twin address was successfully created + + Start + Back + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 5c6ee06378..ae260a7854 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -51,7 +51,7 @@ @color/selector_btn_black -