diff --git a/app/build.gradle b/app/build.gradle index 2c09a36f9c..73344a3d04 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,6 +40,7 @@ android { initWith debug versionNameSuffix "-beta" applicationIdSuffix ".debug" + buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"prod\"' } } kotlinOptions { @@ -69,8 +70,11 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'com.google.android.material:material:1.3.0' - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' implementation "androidx.core:core-ktx:1.3.2" + implementation 'com.google.android.play:core:1.9.1' + implementation 'com.google.android.play:core-ktx:1.8.1' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' + // implementation 'com.tangem:blockchain:1.141.0' implementation 'blockchain-sdk-kotlin:blockchain:tangem-20210302.084810-3' diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml index 4cc2b19474..bf9ef4d842 100644 --- a/app/src/debug/res/values/strings.xml +++ b/app/src/debug/res/values/strings.xml @@ -1,6 +1,6 @@ - DEBUG + DebugTangem \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2b7b764bfc..ee1acb1fe8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -81,6 +81,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/assets/features_dev.json b/app/src/main/assets/features_dev.json index 4866065376..71561c4c69 100644 --- a/app/src/main/assets/features_dev.json +++ b/app/src/main/assets/features_dev.json @@ -1,6 +1,6 @@ { "isWalletPayIdEnabled": true, - "isTopUpEnabled": true, "isSendingToPayIdEnabled": true, + "isTopUpEnabled": true, "isCreatingTwinCardsAllowed": false } \ No newline at end of file diff --git a/app/src/main/assets/features_prod.json b/app/src/main/assets/features_prod.json index 7c4ebbbb72..d3b1375fae 100644 --- a/app/src/main/assets/features_prod.json +++ b/app/src/main/assets/features_prod.json @@ -1,6 +1,6 @@ { - "isWalletPayIdEnabled": true, - "isTopUpEnabled": true, + "isWalletPayIdEnabled": false, "isSendingToPayIdEnabled": true, + "isTopUpEnabled": true, "isCreatingTwinCardsAllowed": true } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index d5f031080b..0f609b7328 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -4,6 +4,7 @@ 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.Log import com.tangem.tap.common.images.PicassoHelper import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.appReducer @@ -14,6 +15,9 @@ import com.tangem.tap.domain.configurable.config.FeaturesRemoteLoader import com.tangem.tap.domain.configurable.warningMessage.RemoteWarningLoader import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.domain.tokens.CurrenciesRepository +import com.tangem.tap.features.feedback.AdditionalEmailInfo +import com.tangem.tap.features.feedback.FeedbackManager +import com.tangem.tap.features.feedback.TangemLogCollector import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.network.createMoshi import com.tangem.tap.persistence.PreferencesStorage @@ -49,10 +53,10 @@ class TapApplication : Application() { PicassoHelper.initPicassoWithCaching(this) currenciesRepository = CurrenciesRepository(this) + initFeedbackManager() loadConfigs() } - private fun loadConfigs() { val moshi = createMoshi() val localLoader = FeaturesLocalLoader(this, moshi) @@ -62,4 +66,15 @@ class TapApplication : Application() { val warningsManager = WarningMessagesManager(RemoteWarningLoader(moshi)) warningsManager.load { store.dispatch(GlobalAction.SetWarningManager(warningsManager)) } } + + private fun initFeedbackManager() { + val infoHolder = AdditionalEmailInfo() + infoHolder.updateAppVersion(this) + + val logWriter = TangemLogCollector() + Log.addLogger(logWriter) + + val feedbackManager = FeedbackManager(infoHolder, this, logWriter) + store.dispatch(GlobalAction.SetFeedbackManager(feedbackManager)) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEvent.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEvent.kt index 932897e204..4395bd0922 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEvent.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEvent.kt @@ -4,4 +4,8 @@ enum class AnalyticsEvent(val event: String) { CARD_IS_SCANNED("card_is_scanned"), TRANSACTION_IS_SENT("transaction_is_sent"), READY_TO_SCAN("ready_to_scan"), + APP_RATING_DISPLAYED("rate_app_warning_displayed"), + APP_RATING_DISMISS("dismiss_rate_app_warning"), + APP_RATING_NEGATIVE("negative_rate_app_feedback"), + APP_RATING_POSITIVE("positive_rate_app_feedback"), } \ 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 9dddeee4f2..1dc3586808 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 @@ -4,5 +4,5 @@ import com.tangem.commands.common.card.Card interface AnalyticsHandler { - fun triggerEvent(event: AnalyticsEvent, card: Card?) + fun triggerEvent(event: AnalyticsEvent, card: Card? = null) } \ No newline at end of file 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 0d49728dd6..d1d3ce7a38 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 @@ -8,8 +8,7 @@ import com.tangem.commands.common.card.Card object FirebaseAnalyticsHandler : AnalyticsHandler { override fun triggerEvent(event: AnalyticsEvent, card: Card?) { - Firebase.analytics - .logEvent(event.event, setCardData(card)) + Firebase.analytics.logEvent(event.event, setCardData(card)) } fun logException(name: String, throwable: Throwable) { 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 49bc412a5d..45bc0c2707 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 @@ -5,6 +5,8 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.domain.tasks.ScanNoteResponse import com.tangem.tap.features.details.redux.SecurityOption +import com.tangem.tap.features.feedback.EmailData +import com.tangem.tap.features.feedback.FeedbackManager import org.rekotlin.Action sealed class GlobalAction : Action { @@ -15,8 +17,12 @@ 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() - data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction() data class HideWarningMessage(val warning: WarningMessage) : GlobalAction() data class UpdateSecurityOptions(val securityOption: SecurityOption) : GlobalAction() + + data class SetConfigManager(val configManager: ConfigManager) : GlobalAction() + data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction() + data class SetFeedbackManager(val feedbackManager: FeedbackManager): GlobalAction() + + data class SendFeedback(val emailData: EmailData): GlobalAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt index 16892e5475..baec3d76f4 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt @@ -26,11 +26,17 @@ val globalMiddleware: Middleware = { dispatch, appState -> store.dispatch(WalletAction.CheckSignedHashes.SaveCardId) } - store.dispatch(WalletAction.SetWarnings(it.getWarnings(WarningMessage.Location.MainScreen))) - store.dispatch(SendAction.SetWarnings(it.getWarnings(WarningMessage.Location.SendScreen))) + store.dispatch(WalletAction.Warnings.SetWarnings( + it.getWarnings(WarningMessage.Location.MainScreen))) + store.dispatch(SendAction.SetWarnings( + it.getWarnings(WarningMessage.Location.SendScreen))) } } } + + is GlobalAction.SendFeedback -> { + store.state.globalState.feedbackManager?.send(action.emailData) + } } nextDispatch(action) } 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 fcd42c9395..8df35e1064 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 @@ -52,6 +52,9 @@ fun globalReducer(action: Action, state: AppState): GlobalState { globalState } } + is GlobalAction.SetFeedbackManager -> { + globalState.copy(feedbackManager = action.feedbackManager) + } 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 4d96b02a3c..52340df26d 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 @@ -7,6 +7,7 @@ import com.tangem.tap.domain.TapWalletManager import com.tangem.tap.domain.configurable.config.ConfigManager import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.domain.tasks.ScanNoteResponse +import com.tangem.tap.features.feedback.FeedbackManager import com.tangem.tap.network.coinmarketcap.CoinMarketCapService import org.rekotlin.StateType @@ -18,6 +19,7 @@ data class GlobalState( val tangemService: TangemService = TangemService(), val configManager: ConfigManager? = null, val warningManager: WarningMessagesManager? = null, + val feedbackManager: FeedbackManager? = null, val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY ) : StateType diff --git a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt index 37f159fdb8..521ed462b4 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt @@ -49,7 +49,7 @@ sealed class TapError( } sealed class TapSdkError(override val messageResId: Int?) : Throwable(), TangemError { - final override val code: Int = 1 + final override val code: Int = 50100 override var customMessage: String = code.toString() object CardForDifferentApp : TapSdkError(R.string.alert_unsupported_card) 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 d62759f6f4..51883d2e9a 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -85,20 +85,10 @@ class TapWalletManager { FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.CARD_IS_SCANNED, data.card) } TapWorkarounds.updateCard(data.card) - store.state.globalState.warningManager?.setBlockchain(data.walletManager?.wallet?.blockchain) - val configManager = store.state.globalState.configManager - if (TapWorkarounds.isStart2Coin) { - configManager?.turnOff(ConfigManager.isSendingToPayIdEnabled) - configManager?.turnOff(ConfigManager.isTopUpEnabled) - } else if (data.walletManager?.wallet?.blockchain == Blockchain.Bitcoin - || data.card.cardData?.blockchainName == Blockchain.Bitcoin.id) { - configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled) - configManager?.resetToDefault(ConfigManager.isTopUpEnabled) - } else { - configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled) - configManager?.resetToDefault(ConfigManager.isTopUpEnabled) - } + updateConfigManager(data) + updateFeedbackManager(data) + withContext(Dispatchers.Main) { store.dispatch(WalletAction.ResetState) store.dispatch(GlobalAction.SaveScanNoteResponse(data)) @@ -120,9 +110,38 @@ class TapWalletManager { } } + private fun updateConfigManager(data: ScanNoteResponse) { + val configManager = store.state.globalState.configManager + if (TapWorkarounds.isStart2Coin) { + configManager?.turnOff(ConfigManager.isSendingToPayIdEnabled) + configManager?.turnOff(ConfigManager.isTopUpEnabled) + } else if (data.walletManager?.wallet?.blockchain == Blockchain.Bitcoin + || data.card.cardData?.blockchainName == Blockchain.Bitcoin.id) { + configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled) + configManager?.resetToDefault(ConfigManager.isTopUpEnabled) + } else { + configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled) + configManager?.resetToDefault(ConfigManager.isTopUpEnabled) + } + } + + private fun updateFeedbackManager(data: ScanNoteResponse) { + val card = data.card + val wallet = data.walletManager?.wallet ?: return + val infoHolder = store.state.globalState.feedbackManager?.infoHolder ?: return + + infoHolder.cardId = card.cardId + infoHolder.cardFirmwareVersion = card.firmwareVersion.version + infoHolder.signedHashesCount = card.walletSignedHashes?.toString() ?: "0" + infoHolder.sourceAddress = wallet.address + infoHolder.explorerLink = wallet.getExploreUrl(wallet.address) + infoHolder.blockchain = wallet.blockchain + } + suspend fun loadData(data: ScanNoteResponse) { withContext(Dispatchers.Main) { - store.dispatch(WalletAction.CheckSignedHashes.CheckIfWarningNeeded) + //TODO: I made it to WalletAction.CheckSignedHashes.CheckIfWarningNeeded + store.dispatch(WalletAction.Warnings.CheckIfNeeded) val artworkId = data.verifyResponse?.artworkInfo?.id if (data.walletManager != null) { if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { 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 1e18167a52..d0cc8b64b2 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,7 @@ package com.tangem.tap.domain import com.tangem.commands.common.card.Card +import com.tangem.commands.common.card.masks.Product import java.util.* object TapWorkarounds { @@ -12,5 +13,26 @@ object TapWorkarounds { isStart2Coin = card.cardData?.issuerName?.toLowerCase(Locale.US) == START_2_COIN_ISSUER } - const val START_2_COIN_ISSUER = "start2coin" + fun Card.isExcluded(): Boolean { + val cardData = this.cardData ?: return false + val productMask = cardData.productMask + val excludedBatch = excludedBatches.contains(cardData.batchId) + val excludedIssuerName = excludedIssuers.contains(cardData.issuerName?.capitalize(Locale.US)) + val excludedProductMask = (productMask != null && // product mask is on cards v2.30 and later + !productMask.contains(Product.Note) && !productMask.contains(Product.TwinCard)) + return excludedBatch || excludedIssuerName || excludedProductMask + + } + + private const val START_2_COIN_ISSUER = "start2coin" + + private val excludedBatches = listOf( + "0027", + "0030", + "0031", + ) // Tangem tags + + private val excludedIssuers = listOf( + "TTM BANK" + ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessage.kt b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessage.kt index 0a8f66677e..ab94c0d563 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessage.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessage.kt @@ -40,7 +40,9 @@ data class WarningMessage( Permanent, // нельзя скрыть @Json(name = "temporary") - Temporary // можно скрыть (кнопка ОК) + Temporary, // можно скрыть (кнопка ОК) + + AppRating } enum class Location { diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt index 4855b20499..1ee151033f 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt @@ -51,7 +51,8 @@ class WarningMessagesManager( val foundWarning = findWarning(warning) return when { foundWarning == null -> false - foundWarning.type == WarningMessage.Type.Temporary -> { + foundWarning.type == WarningMessage.Type.Temporary + || foundWarning.type == WarningMessage.Type.AppRating -> { if (foundWarning.isHidden) { false } else { @@ -101,7 +102,19 @@ class WarningMessagesManager( WarningMessage.Origin.Local ) - fun isAlreadySignedHashesWarning(warning: WarningMessage):Boolean { + fun appRatingWarning(): WarningMessage = WarningMessage( + "", + "", + WarningMessage.Type.AppRating, + WarningMessage.Priority.Info, + listOf(WarningMessage.Location.MainScreen), + null, + R.string.warning_rate_app_title, + R.string.warning_rate_app_message, + WarningMessage.Origin.Local + ) + + fun isAlreadySignedHashesWarning(warning: WarningMessage): Boolean { return warning.messageResId == R.string.alert_card_signed_transactions } } 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 3ceeaadda6..5fd45114d2 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 @@ -19,6 +19,7 @@ import com.tangem.common.CompletionResult import com.tangem.common.extensions.toHexString import com.tangem.tap.domain.TapSdkError import com.tangem.tap.domain.TapWorkarounds +import com.tangem.tap.domain.TapWorkarounds.isExcluded import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.domain.twins.isTwinCard import com.tangem.tap.store @@ -123,23 +124,13 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable "Start2Coin-fr-ch-tangem.pdf" + languageCode == "de" && regionCode == "ch" -> "Start2Coin-de-ch-tangem.pdf" + languageCode == "en" && regionCode == "ch" -> "Start2Coin-en-ch-tangem.pdf" + languageCode == "it" && regionCode == "ch" -> "Start2Coin-it-ch-tangem.pdf" + languageCode == "fr" && regionCode == "fr" -> "Start2Coin-fr-fr-atangem.pdf" + languageCode == "de" && regionCode == "at" -> "Start2Coin-de-at-tangem.pdf" + regionCode == "fr" -> "Start2Coin-fr-fr-atangem.pdf" + regionCode == "ch" -> "Start2Coin-en-ch-tangem.pdf" + regionCode == "at" -> "Start2Coin-de-at-tangem.pdf" + else -> "Start2Coin-fr-fr-atangem.pdf" + } + } + + private fun regionCode(cardId: String): String? = when (cardId[1]) { + '0' -> "fr" + '1' -> "ch" + '2' -> "at" + else -> null + } +} \ 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 5bb753ff4c..f37ef1c696 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 @@ -6,6 +6,7 @@ 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.termsOfUse.CardTou import com.tangem.tap.domain.twins.TwinCardNumber import com.tangem.tap.features.details.redux.twins.CreateTwinWallet import com.tangem.tap.network.coinmarketcap.FiatCurrency @@ -19,6 +20,7 @@ sealed class DetailsAction : Action { val scanNoteResponse: ScanNoteResponse, val wallets: List, val isCreatingTwinWalletAllowed: Boolean?, + val cardTou: CardTou, val fiatCurrencyName: FiatCurrencyName, val fiatCurrencies: List? = null, ) : DetailsAction() 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 9c90579d69..ee44e36c9d 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 @@ -75,6 +75,7 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: Deta ), securityScreenState = SecurityScreenState(currentOption = securityOption), createTwinWalletState = twinsState, + cardTermsOfUseUrl = action.cardTou.getUrl(action.card) ) } 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 f4d58c3000..5ef1033e2f 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,5 +1,6 @@ package com.tangem.tap.features.details.redux +import android.net.Uri import com.tangem.blockchain.common.Wallet import com.tangem.commands.common.card.Card import com.tangem.tap.common.entities.Button @@ -18,13 +19,14 @@ data class DetailsState( val eraseWalletState: EraseWalletState? = null, val confirmScreenState: ConfirmScreenState? = null, val securityScreenState: SecurityScreenState? = null, - val createTwinWalletState: CreateTwinWalletState? = null + val createTwinWalletState: CreateTwinWalletState? = null, + val cardTermsOfUseUrl: Uri? = null, ) : StateType data class CardInfo( val cardId: String, val issuer: String, - val signedHashes: Int + val signedHashes: Int, ) enum class EraseWalletState { Allowed, NotAllowedByCard, NotEmpty } @@ -33,7 +35,7 @@ data class SecurityScreenState( val currentOption: SecurityOption = SecurityOption.LongTap, val selectedOption: SecurityOption = currentOption, val allowedOptions: EnumSet = EnumSet.allOf(SecurityOption::class.java), - val buttonProceed: Button = Button(true) + val buttonProceed: Button = Button(true), ) enum class SecurityOption { LongTap, PassCode, AccessCode } 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 index d40bb904d7..669ab9be91 100644 --- 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 @@ -18,7 +18,7 @@ class CreateTwinWalletMiddleware { fun handle(action: DetailsAction.CreateTwinWalletAction) { when (action) { is DetailsAction.CreateTwinWalletAction.ShowWarning -> { - val wallet = store.state.detailsState.wallets.getOrNull(0) + val wallet = store.state.globalState.scanNoteResponse?.walletManager?.wallet if (wallet == null) { store.dispatch(NavigationAction.NavigateTo(AppScreen.CreateTwinWalletWarning)) return 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 50de2e5535..895f379b09 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 @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.ui +import android.content.Intent import android.os.Bundle import android.view.View import androidx.activity.OnBackPressedCallback @@ -7,6 +8,7 @@ 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.global.GlobalAction import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.twins.getTwinCardIdForUser import com.tangem.tap.domain.twins.isTwinCard @@ -14,6 +16,7 @@ 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.features.feedback.FeedbackEmail import com.tangem.tap.store import com.tangem.wallet.R import kotlinx.android.synthetic.main.fragment_details.* @@ -79,6 +82,13 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber() + + override fun e(logTag: String, message: String) {} + override fun i(logTag: String, message: String) {} + override fun v(logTag: String, message: String) {} + + override fun write(message: LogMessage) { + logs.add(message.message) + } + + fun getLogs(): List = logs.toList() + + fun clearLogs() { + logs.clear() + } +} + +class AdditionalEmailInfo { + var cardId: String = "" + var cardFirmwareVersion: String = "" + var blockchain: Blockchain = Blockchain.Unknown + + var phoneModel: String = Build.MODEL + var osVersion: String = Build.VERSION.SDK_INT.toString() + var appVersion: String = "" + + var token: String = "" + var sourceAddress: String = "" + var destinationAddress: String = "" + var amount: String = "" + var fee: String = "" + + // var transactionHex: String = "" + var signedHashesCount: String = "" + var explorerLink: String = "" +// var outputsCount: String = "" + + fun updateAppVersion(context: Context) { + try { + val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) + appVersion = pInfo.versionName + } catch (e: PackageManager.NameNotFoundException) { + e.printStackTrace() + } + } +} + +interface EmailData { + val subject: String + val mainMessage: String + fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String + + fun joinTogether(infoHolder: AdditionalEmailInfo): String { + return "$mainMessage\n\n\n\n\n" + + "Following information is optional. You can erase it if you don’t want to share it.\n" + + createOptionalMessage(infoHolder) + } +} + +class RateCanBeBetterEmail : EmailData { + override val subject: String = "My suggestions" + override val mainMessage: String = "Tell us what functions you are missing, and we will try to help you." + + override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String { + return StringBuilder().apply { + appendKeyValue("Card ID", infoHolder.cardId) + appendKeyValue("Blockchain", infoHolder.blockchain.fullName) + appendKeyValue("Phone model", infoHolder.phoneModel) + appendKeyValue("OS version", infoHolder.osVersion) + appendKeyValue("App version", infoHolder.appVersion) + }.toString() + } +} + +class ScanFailsEmail : EmailData { + override val subject: String = "Can’t scan a card" + override val mainMessage: String = "Please tell us what card do you have?" + override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String { + return StringBuilder().apply { + appendKeyValue("Phone model", infoHolder.phoneModel) + appendKeyValue("OS version", infoHolder.osVersion) + appendKeyValue("App version", infoHolder.appVersion) + }.toString() + } +} + +class SendTransactionFailedEmail(private val error: String) : EmailData { + override val subject: String = "Can’t send a transaction" + override val mainMessage: String = "Please tell us more about your issue. Every small detail can help." + override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String { + return StringBuilder().apply { + appendKeyValue("Error", error) + appendKeyValue("Card ID", infoHolder.cardId) + appendKeyValue("Blockchain", infoHolder.blockchain.fullName) + appendKeyValue("Token", infoHolder.token) + appendKeyValue("Source address", infoHolder.sourceAddress) + appendKeyValue("Destination address", infoHolder.destinationAddress) + appendKeyValue("Amount", infoHolder.amount) + appendKeyValue("Fee", infoHolder.fee) + appendKeyValue("Phone model", infoHolder.phoneModel) + appendKeyValue("OS version", infoHolder.osVersion) + appendKeyValue("App version", infoHolder.appVersion) + appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion) +// appendKeyValue("Transaction HEX", infoHolder.transactionHex) + }.toString() + } +} + +class FeedbackEmail : EmailData { + override val subject: String = "Tangem Tap feedback" + override val mainMessage: String = "Hi Tangem," + override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String { + return StringBuilder().apply { + appendKeyValue("Card ID", infoHolder.cardId) + appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion) + appendKeyValue("Signed hashes", infoHolder.signedHashesCount) + appendKeyValue("Blockchain", infoHolder.blockchain.fullName) + appendKeyValue("Wallet address", infoHolder.sourceAddress) + appendKeyValue("Explorer link", infoHolder.explorerLink) +// appendKeyValue("Outputs count", infoHolder.outputsCount) + appendKeyValue("Phone model", infoHolder.phoneModel) + appendKeyValue("OS version", infoHolder.osVersion) + }.toString() + } +} + +fun StringBuilder.appendKeyValue(key: String, value: String): StringBuilder { + return this.append("$key: $value\n") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/BaseStoreFragment.kt b/app/src/main/java/com/tangem/tap/features/send/BaseStoreFragment.kt index 3e2bc8cdf0..fb46ed08da 100644 --- a/app/src/main/java/com/tangem/tap/features/send/BaseStoreFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/BaseStoreFragment.kt @@ -6,6 +6,7 @@ import android.view.View import android.view.ViewGroup import androidx.activity.OnBackPressedCallback import androidx.fragment.app.Fragment +import androidx.transition.TransitionInflater import com.google.android.material.snackbar.Snackbar import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.store @@ -30,6 +31,9 @@ abstract class BaseStoreFragment(layoutId: Int) : Fragment(layoutId) { store.dispatch(NavigationAction.PopBackTo()) } }) + val inflater = TransitionInflater.from(requireContext()) + enterTransition = inflater.inflateTransition(R.transition.slide_right) + exitTransition = inflater.inflateTransition(R.transition.fade) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 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 6888e3b462..8a87b6108f 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 @@ -141,12 +141,12 @@ sealed class SendAction : SendScreenAction { data class SendError(override val error: TapError) : SendAction(), ErrorAction sealed class Dialog : SendAction() { - data class ShowTezosWarningDialog( + data class TezosWarningDialog( val reduceCallback: () -> Unit, val sendAllCallback: () -> Unit, val reduceAmount: BigDecimal, ) : Dialog() - + data class SendTransactionFails(val errorMessage: String): Dialog() object Hide : Dialog() } data class SetWarnings(val warningList: List) : SendAction() 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 ded3984926..a713ed3163 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 @@ -1,19 +1,17 @@ package com.tangem.tap.features.send.redux.middlewares +import com.tangem.blockchain.common.Blockchain 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.isPayIdSupported -import com.tangem.tap.features.send.redux.AddressPayIdActionUi -import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction +import com.tangem.tap.features.send.redux.* import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.AddressVerification.SetAddressError import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.AddressVerification.SetWalletAddress import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.Error import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification.SetPayIdError import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification.SetPayIdWalletAddress -import com.tangem.tap.features.send.redux.FeeAction -import com.tangem.tap.features.send.redux.TransactionExtrasAction import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.Dispatchers @@ -50,7 +48,7 @@ internal class AddressPayIdMiddleware { private fun setAddressAndCheck(data: String, isUserInput: Boolean, dispatch: (Action) -> Unit) { val potentialPayId = data.toLowerCase() - if (PayIdManager.isPayId(potentialPayId) && isPayIdEnabled()) { + if (isPayIdEnabled() && PayIdManager.isPayId(potentialPayId)) { dispatch(SetPayIdWalletAddress(potentialPayId, "", isUserInput)) } else { dispatch(SetWalletAddress(data, isUserInput)) @@ -64,7 +62,7 @@ internal class AddressPayIdMiddleware { val addressPayId = sendState.addressPayIdState.normalFieldValue ?: return val isUserInput = sendState.addressPayIdState.viewFieldValue.isFromUserInput - if (PayIdManager.isPayId(addressPayId) && isPayIdEnabled()) { + if (isPayIdEnabled() && PayIdManager.isPayId(addressPayId)) { verifyPayId(addressPayId, wallet, isUserInput, dispatch) } else { verifyAddress(addressPayId, wallet, isUserInput, dispatch) @@ -110,7 +108,11 @@ internal class AddressPayIdMiddleware { } private fun verifyAddress(address: String, wallet: Wallet, isUserInput: Boolean, dispatch: (Action) -> Unit) { - val addressSchemeSplit = address.split(":") + val addressSchemeSplit = if (wallet.blockchain == Blockchain.BitcoinCash) { + listOf(address) + } else { + address.split(":") + } val noSchemeAddress = when (addressSchemeSplit.size) { 1 -> address // no scheme 2 -> { // scheme @@ -131,6 +133,9 @@ internal class AddressPayIdMiddleware { val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(wallet, supposedAddress) if (failReason == null) { + noSchemeAddress.getQueryParameter("amount")?.toBigDecimalOrNull()?.let { + dispatch(AmountAction.SetAmount(it, false)) + } dispatch(SetWalletAddress(supposedAddress, isUserInput)) dispatch(TransactionExtrasAction.Prepare(wallet.blockchain, address, null)) } else { @@ -141,7 +146,7 @@ internal class AddressPayIdMiddleware { private fun isValidBlockchainAddressAndNotTheSameAsWallet(wallet: Wallet, address: String): Error? { return if (wallet.blockchain.validateAddress(address)) { - if (wallet.addresses.all { it.value != address } ) { + if (wallet.addresses.all { it.value != address }) { null } else { Error.ADDRESS_SAME_AS_WALLET @@ -151,14 +156,10 @@ internal class AddressPayIdMiddleware { } } - //TODO: move to the blockchainSDK - private fun extractAddressFromShareUri(shareUri: String): String { - val sharePrefix = listOf("bitcoin:", "ethereum:", "xrpl:", "litecoin:", "bnb:") - val prefixes = sharePrefix.filter { shareUri.contains(it) } - return if (prefixes.isEmpty()) shareUri else shareUri.replace(prefixes[0], "") - } - private fun String.removeShareUriQuery(): String = this.substringBefore("?") + private fun String.getQueryParameter(name: String): String? { + return this.substringAfter("?").splitToMap("&", "=")[name] + } private fun verifyClipboard(input: String?, appState: AppState?, dispatch: DispatchFunction) { val addressPayId = input ?: return @@ -186,4 +187,11 @@ internal class AddressPayIdMiddleware { private fun isPayIdEnabled(): Boolean { return store.state.globalState.configManager?.config?.isSendingToPayIdEnabled ?: false } +} + +fun String.splitToMap(firstDelimiter: String, secondDelimiter: String): Map { + return this.split(firstDelimiter) + .map { it.split(secondDelimiter) } + .map { it.first() to it.last().toString() } + .toMap() } \ 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 47d30c72d5..115c1c2033 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 @@ -22,6 +22,7 @@ import com.tangem.tap.features.send.redux.states.SendButtonState import com.tangem.tap.features.send.redux.states.TransactionExtrasState import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.scope +import com.tangem.tap.store import com.tangem.tap.tangemSdk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -52,7 +53,7 @@ val sendMiddleware: Middleware = { dispatch, appState -> } private fun verifyAndSendTransaction( - action: SendActionUi.SendAmountToRecipient, appState: AppState?, dispatch: (Action) -> Unit + action: SendActionUi.SendAmountToRecipient, appState: AppState?, dispatch: (Action) -> Unit, ) { val sendState = appState?.sendState ?: return val walletManager = appState.globalState.scanNoteResponse?.walletManager ?: return @@ -68,7 +69,7 @@ private fun verifyAndSendTransaction( when { hadTezosError -> { val reduceAmount = walletManager.wallet.blockchain.minimalAmount() - dispatch(SendAction.Dialog.ShowTezosWarningDialog(reduceCallback = { + dispatch(SendAction.Dialog.TezosWarningDialog(reduceCallback = { dispatch(AmountAction.SetAmount(typedAmount.value!!.minus(reduceAmount), false)) dispatch(AmountActionUi.CheckAmountToSend) }, sendAllCallback = { @@ -94,7 +95,7 @@ private fun sendTransaction( destinationAddress: String, transactionExtras: TransactionExtrasState, card: Card, - dispatch: (Action) -> Unit + dispatch: (Action) -> Unit, ) { dispatch(SendAction.ChangeSendButtonState(SendButtonState.PROGRESS)) var txData = walletManager.createTransaction(amountToSend, feeAmount, destinationAddress) @@ -144,6 +145,8 @@ private fun sendTransaction( when { message == null -> { dispatch(SendAction.SendError(TapError.UnknownError)) + updateFeedbackManager(walletManager, amountToSend, feeAmount, destinationAddress, card) + dispatch(SendAction.Dialog.SendTransactionFails("unknown error")) } message.contains("50002") -> { // user was cancelled the operation by closing the Sdk bottom sheet @@ -157,6 +160,8 @@ private fun sendTransaction( Timber.e(throwable) FirebaseCrashlytics.getInstance().recordException(throwable) dispatch(SendAction.SendError(TapError.CustomError(message))) + updateFeedbackManager(walletManager, amountToSend, feeAmount, destinationAddress, card) + dispatch(SendAction.Dialog.SendTransactionFails(message)) } } } @@ -168,6 +173,29 @@ private fun sendTransaction( } } +private fun updateFeedbackManager( + walletManager: WalletManager, + amountToSend: Amount, + feeAmount: Amount, + destinationAddress: String, + card: Card, +) { + val infoHolder = store.state.globalState.feedbackManager?.infoHolder ?: return + val amountState = store.state.sendState.amountState + + infoHolder.cardId = card.cardId + infoHolder.blockchain = walletManager.wallet.blockchain + infoHolder.sourceAddress = walletManager.wallet.address + infoHolder.destinationAddress = destinationAddress + infoHolder.amount = amountToSend.value?.stripZeroPlainString() ?: "0" + infoHolder.fee = feeAmount.value?.stripZeroPlainString() ?: "0" + infoHolder.cardFirmwareVersion = card.firmwareVersion.version + if (amountState.typeOfAmount is AmountType.Token) { + infoHolder.token = amountState.amountToExtract?.currencySymbol ?: "" + } +// infoHolder.transactionHex = "" +} + fun extractErrorsForAmountField(errors: EnumSet): EnumSet { val showIntoAmountField = EnumSet.noneOf(TransactionError::class.java) errors.forEach { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt index 4144059510..527d3f6545 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt @@ -42,7 +42,8 @@ private class SendReducer : SendInternalReducer { override fun handle(action: SendScreenAction, sendState: SendState): SendState { val result = when (action) { is SendAction.ChangeSendButtonState -> sendState.copy(sendButtonState = action.state) - is SendAction.Dialog.ShowTezosWarningDialog -> sendState.copy(dialog = action) + is SendAction.Dialog.TezosWarningDialog -> sendState.copy(dialog = action) + is SendAction.Dialog.SendTransactionFails -> sendState.copy(dialog = action) is SendAction.Dialog.Hide -> sendState.copy(dialog = null) is SendAction.SetWarnings -> sendState.copy(sendWarningsList = action.warningList) else -> return sendState 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 cba07cdeb5..6ee78a5baf 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 @@ -149,9 +149,14 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { val scannedCode = data?.getStringExtra(ScanQrCodeActivity.SCAN_RESULT) ?: "" if (scannedCode.isEmpty()) return - store.dispatch(PasteAddressPayId(scannedCode)) - store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused)) - store.dispatch(FeeAction.RequestFee) + // Delayed launch is needed in order for the UI to be drawn and to process the sent events. + // If do not use the delay, then etAmount error field is not displayed when + // inserting an incorrect amount by shareUri + imvQrCode.postDelayed({ + store.dispatch(PasteAddressPayId(scannedCode)) + store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused)) + store.dispatch(FeeAction.RequestFee) + }, 200) } private fun setupAmountLayout() { diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt new file mode 100644 index 0000000000..cb65943200 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt @@ -0,0 +1,30 @@ +package com.tangem.tap.features.send.ui.dialogs + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.feedback.SendTransactionFailedEmail +import com.tangem.tap.features.send.redux.SendAction +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.store +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +class SendTransactionFailsDialog { + + companion object { + fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails): AlertDialog { + return AlertDialog.Builder(context).apply { + setTitle(R.string.alert_failed_to_send_transaction_title) + setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, dialog.errorMessage)) + setNeutralButton(R.string.alert_button_send_feedback) { _, _ -> + store.dispatch(GlobalAction.SendFeedback(SendTransactionFailedEmail(dialog.errorMessage))) + } + setPositiveButton(R.string.common_no) { _, _ -> } + setOnDismissListener { store.dispatch(WalletAction.HideDialog) } + }.create() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/TezosWarningDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/TezosWarningDialog.kt index 45b092ea8c..afb6276cb7 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/TezosWarningDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/TezosWarningDialog.kt @@ -9,10 +9,10 @@ import com.tangem.wallet.R class TezosWarningDialog(context: Context) : AlertDialog(context) { companion object { - fun create(context: Context, showDialogData: SendAction.Dialog.ShowTezosWarningDialog): AlertDialog { + fun create(context: Context, showDialogData: SendAction.Dialog.TezosWarningDialog): AlertDialog { val reduceAmount = showDialogData.reduceAmount.toPlainString() return Builder(context).apply { - setTitle(context.getString(R.string.common_warning)) + setTitle(R.string.common_warning) setMessage(context.getString(R.string.xtz_withdrawal_message_warning, reduceAmount)) setNegativeButton(R.string.xtz_withdrawal_message_ignore) { _, _ -> showDialogData.sendAllCallback() 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 8084b7e5d5..973ed08f8b 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 @@ -21,6 +21,7 @@ import com.tangem.tap.features.send.redux.reducers.ReceiptReducer import com.tangem.tap.features.send.redux.states.* import com.tangem.tap.features.send.ui.FeeUiHelper import com.tangem.tap.features.send.ui.SendFragment +import com.tangem.tap.features.send.ui.dialogs.SendTransactionFailsDialog import com.tangem.tap.features.send.ui.dialogs.TezosWarningDialog import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter import com.tangem.tap.store @@ -97,12 +98,18 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber val sendFragment = (fg as? SendFragment) ?: return when (state.dialog) { - is SendAction.Dialog.ShowTezosWarningDialog -> { + is SendAction.Dialog.TezosWarningDialog -> { if (dialog == null) { dialog = TezosWarningDialog.create(fg.requireContext(), state.dialog) dialog?.show() } } + is SendAction.Dialog.SendTransactionFails -> { + if (dialog == null) { + dialog = SendTransactionFailsDialog.create(fg.requireContext(), state.dialog) + dialog?.show() + } + } else -> { dialog?.dismiss() dialog = null @@ -184,22 +191,6 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber val amountToSend = state.viewAmountValue if (!amountToSend.isFromUserInput) fg.etAmountToSend.update(amountToSend.value) -// fg.tvAmountToSendShadow.text = amountToSend -// if (amountToSend.length > 10) { -// post is needed to wait for text size changes -// fg.tvAmountToSendShadow.post { -// fg.etAmountToSend.setTextSize(TypedValue.COMPLEX_UNIT_PX, fg.tvAmountToSendShadow.textSize - 2) -// fg.etAmountToSend.update(amountToSend) -// if (!state.cursorAtTheSamePosition) fg.etAmountToSend.setSelection(amountToSend.length) -// } -// } else { -// val textSize = fg.resources.getDimension(R.dimen.text_size_amount_to_send) -// fg.tvAmountToSendShadow.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) -// fg.etAmountToSend.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) -// fg.etAmountToSend.update(amountToSend) -// if (!state.cursorAtTheSamePosition) fg.etAmountToSend.setSelection(amountToSend.length) -// } - fg.tvAmountCurrency.update(state.mainCurrency.currencySymbol) (fg as? SendFragment)?.saveMainCurrency(state.mainCurrency.type) 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 222d18a108..182e1d63d5 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 @@ -2,6 +2,9 @@ package com.tangem.tap.features.wallet.redux import android.content.Context import com.tangem.blockchain.common.* +import com.tangem.TangemError +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.address.AddressType import com.tangem.commands.common.card.Card import com.tangem.tap.common.redux.NotificationAction @@ -30,7 +33,8 @@ sealed class WalletAction : Action { data class SetArtworkId(val artworkId: String?) : WalletAction() -// sealed class ProcessWallet : WalletAction() { + + // sealed class ProcessWallet : WalletAction() { // data class LoadWallet(val artworkId: String?, val allowTopUp: Boolean // ) : ProcessWallet() { // data class Success(val wallet: Wallet) : ProcessWallet() @@ -45,7 +49,6 @@ sealed class WalletAction : Action { // } // } - sealed class MultiWallet : WalletAction() { data class SetIsMultiwalletAllowed(val isMultiwalletAllowed: Boolean) : MultiWallet() data class AddWalletManagers(val walletManagers: List) : MultiWallet() @@ -68,7 +71,15 @@ sealed class WalletAction : Action { object SaveCardId : CheckSignedHashes() } - data class SetWarnings(val warningList: List) : WalletAction() + object Warnings : WalletAction() { + object CheckIfNeeded : WalletAction() + data class SetWarnings(val warningList: List) : WalletAction() + + object AppRating : WalletAction() { + object SetNeverToShow : WalletAction() + object RemindLater : WalletAction() + } + } data class UpdateWallet(val currency: CryptoCurrencyName? = null) : WalletAction() { object ScheduleUpdatingWallet : WalletAction() @@ -88,40 +99,59 @@ sealed class WalletAction : Action { object Failure : WalletAction() } - - data class CopyAddress(val address: String, val context: Context) : WalletAction() { - object Success : WalletAction(), NotificationAction { - override val messageResource = R.string.wallet_notification_address_copied - } - } - - data class ShareAddress(val address: String, val context: Context) : WalletAction() - - object ShowQrCode : WalletAction() - object HideDialog : WalletAction() - data class ExploreAddress(val exploreUrl: String, val context: Context) : WalletAction() - - object CreateWallet : WalletAction() - object EmptyWallet : WalletAction() object Scan : WalletAction() + class ScanCardFinished(val scanError: TangemError? = null) : WalletAction() data class Send(val amount: Amount? = null) : WalletAction() { data class ChooseCurrency(val amounts: List?) : WalletAction() object Cancel : WalletAction() } - sealed class TopUpAction : WalletAction() { - data class TopUp(val context: Context, val toolbarColor: Int) : TopUpAction() - } + object CreatePayId : WalletAction() { + data class CompleteCreatingPayId(val payId: String) : WalletAction() + data class Success(val payId: String) : WalletAction() + object EmptyField : WalletAction(), ErrorAction { + override val error = TapError.PayIdEmptyField + } - data class ChangeSelectedAddress(val type: AddressType) : WalletAction() + data class CopyAddress(val address: String, val context: Context) : WalletAction() { + object Success : WalletAction(), NotificationAction { + override val messageResource = R.string.wallet_notification_address_copied + } + } - 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 + data class ShareAddress(val address: String, val context: Context) : WalletAction() + + object ShowDialog : WalletAction() { + object QrCode : WalletAction() + object ScanFails : WalletAction() + } + + object HideDialog : WalletAction() + + data class ExploreAddress(val exploreUrl: String, val context: Context) : WalletAction() + + object CreateWallet : WalletAction() + object EmptyWallet : WalletAction() + object Scan : WalletAction() + + data class Send(val amount: Amount? = null) : WalletAction() { + data class ChooseCurrency(val amounts: List?) : WalletAction() + object Cancel : WalletAction() + } + + 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 new file mode 100644 index 0000000000..3f3534891e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletMiddleware.kt @@ -0,0 +1,356 @@ +package com.tangem.tap.features.wallet.redux + +import android.content.Intent +import android.net.Uri +import androidx.browser.customtabs.CustomTabsIntent +import androidx.core.content.ContextCompat +import com.tangem.TangemSdkError +import com.tangem.blockchain.common.* +import com.tangem.blockchain.extensions.SimpleResult +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.getType +import com.tangem.common.extensions.toHexString +import com.tangem.tap.common.analytics.AnalyticsEvent +import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler +import com.tangem.tap.common.extensions.copyToClipboard +import com.tangem.tap.common.extensions.isGreaterThan +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.navigation.AppScreen +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.domain.PayIdManager +import com.tangem.tap.domain.TapError +import com.tangem.tap.domain.TopUpHelper +import com.tangem.tap.domain.configurable.warningMessage.WarningMessage +import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager +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 +import com.tangem.tap.network.NetworkStateChanged +import com.tangem.tap.preferencesStorage +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.rekotlin.Action +import org.rekotlin.Middleware +import java.math.BigDecimal + +class WalletMiddleware { + private val topUpMiddleware = TopUpMiddleware() + + val walletMiddleware: Middleware = { dispatch, state -> + { next -> + { action -> + when (action) { + is WalletAction.TopUpAction -> topUpMiddleware.handle(action) + is WalletAction.LoadWallet -> { + scope.launch { + store.state.globalState.tapWalletManager.loadWalletData() + } + } + is WalletAction.LoadPayId -> { + scope.launch { + store.state.globalState.tapWalletManager.loadPayId() + } + } + is WalletAction.LoadFiatRate -> { + scope.launch { + store.state.globalState.tapWalletManager.loadFiatRate(store.state.globalState.appCurrency) + } + } + is WalletAction.CreateWallet -> { + 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) + } + + } + } + } + } + is WalletAction.UpdateWallet -> { + if (store.state.walletState.state == ProgressState.Done) { + scope.launch { store.state.globalState.tapWalletManager.updateWallet() } + } + } + is WalletAction.UpdateWallet.Success -> setupWalletUpdate(action.wallet) + is WalletAction.LoadWallet.Success -> { + store.dispatch(WalletAction.CheckHashesCountOnline) + if (!store.state.walletState.updatingWallet) setupWalletUpdate(action.wallet) + tryToShowAppRatingWarning(action.wallet) + } + is WalletAction.CreatePayId.CompleteCreatingPayId -> { + scope.launch { + val cardId = store.state.globalState.scanNoteResponse?.card?.cardId + val wallet = store.state.globalState.scanNoteResponse?.walletManager?.wallet + val publicKey = store.state.globalState.scanNoteResponse?.card?.cardPublicKey + if (cardId != null && wallet != null && publicKey != null) { + val result = PayIdManager().setPayId( + cardId, publicKey.toHexString(), + action.payId, wallet.address, wallet.blockchain + ) + withContext(Dispatchers.Main) { + when (result) { + is Result.Success -> + store.dispatch(WalletAction.CreatePayId.Success(action.payId)) + is Result.Failure -> { + val error = result.error as? TapError + ?: TapError.PayIdCreatingError + store.dispatch(WalletAction.CreatePayId.Failure(error)) + } + } + } + } + } + } + is WalletAction.Scan -> { + scope.launch { + 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)) + } + } + store.dispatch(WalletAction.ScanCardFinished()) + } + is CompletionResult.Failure -> { + if (result.error !is TangemSdkError.UserCancelled) { + // Weird things... If you run the code below without coroutines, + // then rescanning will be impossible + scope.launch(Dispatchers.Main) { + store.dispatch(WalletAction.ScanCardFinished(result.error)) + if (store.state.walletState.scanCardFailsCounter >= 2) { + store.dispatch(WalletAction.ShowDialog.ScanFails) + } + } + } + } + } + } + } + is WalletAction.LoadData -> { + scope.launch { + store.state.globalState.scanNoteResponse?.let { + store.state.globalState.tapWalletManager.loadData(it) + } + } + } + is NetworkStateChanged -> { + store.state.globalState.scanNoteResponse?.let { scanNoteResponse -> + store.dispatch(WalletAction.CheckHashesCountOnline) + scope.launch { + store.state.globalState.tapWalletManager.onCardScanned(scanNoteResponse) + } + } + } + is WalletAction.CopyAddress -> { + 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.walletAddresses?.selectedAddress?.exploreUrl) + val intent = Intent(Intent.ACTION_VIEW, uri) + ContextCompat.startActivity(action.context, intent, null) + } + is WalletAction.Send -> { + val newAction = prepareSendAction(action.amount) + store.dispatch(newAction) + if (newAction is PrepareSendScreen) { + store.dispatch(NavigationAction.NavigateTo(AppScreen.Send)) + } + } + is WalletAction.Warnings.CheckIfNeeded -> { + val globalState = store.state.globalState + val validator = globalState.scanNoteResponse?.walletManager as? SignatureCountValidator + globalState.scanNoteResponse?.card?.let { card -> + store.state.globalState.warningManager?.removeWarnings(WarningMessage.Origin.Local) + if (card.getType() != CardType.Release) addWarningMessage(WarningMessagesManager.devCardWarning()) + if (!preferencesStorage.wasCardScannedBefore(card.cardId)) { + checkIfWarningNeeded(card, validator)?.let { addWarningMessage(it) } + } + updateWarningMessages() + } + } + is WalletAction.CheckHashesCountOnline -> checkHashesCountOnline() + is WalletAction.SaveCardId -> { + 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() + } + is WalletAction.Warnings.AppRating.RemindLater -> { + preferencesStorage.appRatingLaunchObserver.applyDelayedShowing() + } + is WalletAction.Warnings.AppRating.SetNeverToShow -> { + preferencesStorage.appRatingLaunchObserver.setNeverToShow() + } + } + next(action) + } + } + } + + private fun tryToShowAppRatingWarning(wallet: Wallet) { + val nonZeroWalletsCount = wallet.amounts.filter { + it.value.value?.isGreaterThan(BigDecimal.ZERO) ?: false + }.size + if (nonZeroWalletsCount > 0) { + preferencesStorage.appRatingLaunchObserver.foundWalletWithFunds() + } + if (preferencesStorage.appRatingLaunchObserver.isReadyToShow()) { + FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_DISPLAYED) + addWarningMessage(WarningMessagesManager.appRatingWarning(), true) + } + } + + private fun setupWalletUpdate(wallet: Wallet) { + if (!wallet.recentTransactions.toPendingTransactions(wallet.address).isNullOrEmpty()) { + store.dispatch(WalletAction.UpdateWallet.ScheduleUpdatingWallet) + scope.launch(Dispatchers.IO) { + delay(10000) + withContext(Dispatchers.Main) { + store.dispatch(WalletAction.UpdateWallet) + } + } + } + } + + + private fun prepareSendAction(amount: Amount?): Action { + return if (amount != null) { + if (amount.type is AmountType.Token) { + PrepareSendScreen(store.state.walletState.wallet?.amounts?.get(AmountType.Coin), amount) + } else { + PrepareSendScreen(amount) + } + } else { + val amounts = store.state.walletState.wallet?.amounts?.toSendableAmounts() + if (amounts?.size ?: 0 > 1) { + WalletAction.Send.ChooseCurrency(amounts) + } else { + val amountToSend = amounts?.first() + PrepareSendScreen(amountToSend) + } + } + } + + private fun checkIfWarningNeeded( + card: Card, signatureCountValidator: SignatureCountValidator? = null, + ): WarningMessage? { + if (card.isTwinCard()) return null + + return if (signatureCountValidator == null) { + if (card.walletSignedHashes ?: 0 > 0) { + WarningMessagesManager.alreadySignedHashesWarning() + } else { + store.dispatch(WalletAction.SaveCardId) + null + } + } else { + store.dispatch(WalletAction.NeedToCheckHashesCountOnline) + null + } + } + + private fun checkHashesCountOnline() { + if (store.state.walletState.hashesCountVerified != false) return + if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) return + + 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 { + val result = validator?.validateSignatureCount(card.walletSignedHashes ?: 0) + withContext(Dispatchers.Main) { + when (result) { + SimpleResult.Success -> { + store.dispatch(WalletAction.ConfirmHashesCount) + store.dispatch(WalletAction.SaveCardId) + } + is SimpleResult.Failure -> + if (result.error is BlockchainSdkError.SignatureCountNotMatched) { + addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true) + } else if (card.walletSignedHashes ?: 0 > 0) { + addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true) + } + } + } + } + } + + private fun addWarningMessage(warning: WarningMessage, autoUpdate: Boolean = false) { + store.state.globalState.warningManager?.addWarning(warning) + if (autoUpdate) updateWarningMessages() + } + + private fun updateWarningMessages() { + val warningManager = store.state.globalState.warningManager ?: return + store.dispatch(WalletAction.Warnings.SetWarnings( + warningManager.getWarnings(WarningMessage.Location.MainScreen))) + } +} + +private class TopUpMiddleware { + fun handle(action: WalletAction.TopUpAction) { + when (action) { + is WalletAction.TopUpAction.TopUp -> { + val config = store.state.globalState.configManager?.config ?: return + val addresses = store.state.walletState.walletAddresses ?: return + if (addresses.list.isEmpty()) return + + val defaultAddress = addresses.list[0].address + val url = TopUpHelper.getUrl( + store.state.walletState.currencyData.currencySymbol!!, + defaultAddress, + config.moonPayApiKey, + config.moonPayApiSecretKey + ) + val customTabsIntent = CustomTabsIntent.Builder() + .setToolbarColor(action.toolbarColor) + .build() + customTabsIntent.launchUrl(action.context, Uri.parse(url)); + } + } + } +} \ No newline at end of file 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 a75c65a889..282d78393a 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 @@ -213,7 +213,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState { } newState = newState.copy(cardImage = Artwork(artworkId = artworkUrl)) } - is WalletAction.ShowQrCode -> { + is WalletAction.ShowDialog.QrCode -> { val selectedWalletData = newState.getWalletData(newState.selectedWallet) newState = newState.copy( walletDialog = WalletDialog.QrDialog( @@ -223,6 +223,9 @@ private fun internalReduce(action: Action, state: AppState): WalletState { ) ) } + is WalletAction.ShowDialog.ScanFails -> { + newState = newState.copy(walletDialog = WalletDialog.ScanFailsDialog) + } is WalletAction.HideDialog -> { newState = newState.copy(walletDialog = null) } @@ -232,7 +235,10 @@ private fun internalReduce(action: Action, state: AppState): WalletState { ) } is WalletAction.Send.Cancel -> newState = newState.copy(walletDialog = null) - is WalletAction.SetWarnings -> newState = newState.copy(mainWarningsList = action.warningList) + is WalletAction.Warnings.SetWarnings -> newState = newState.copy(mainWarningsList = action.warningList) + is WalletAction.TopUpAction -> { + newState = newState.copy(topUpState = handleTopUpActions(action, newState.topUpState)) + } is WalletAction.TopUpAction -> return newState is WalletAction.ChangeSelectedAddress -> { val selectedWalletData = newState.getWalletData(newState.selectedWallet) @@ -246,6 +252,13 @@ private fun internalReduce(action: Action, state: AppState): WalletState { ) newState = newState.copy(wallets = wallets) } + is WalletAction.ScanCardFinished -> { + newState = if (action.scanError == null) { + newState.copy(scanCardFailsCounter = 0) + } else { + newState.copy(scanCardFailsCounter = newState.scanCardFailsCounter + 1) + } + } } return newState } 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 2958717c4a..6bf6a41ffc 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 @@ -25,6 +25,7 @@ data class WalletState( val walletDialog: WalletDialog? = null, val twinCardsState: TwinCardsState? = null, val mainWarningsList: List = mutableListOf(), + val scanCardFailsCounter: Int = 0, val wallets: List = emptyList(), val walletManagers: List = emptyList(), val isMultiwalletAllowed: Boolean = false, @@ -101,6 +102,7 @@ sealed class WalletDialog { ) : WalletDialog() data class SelectAmountToSendDialog(val amounts: List?) : WalletDialog() + object ScanFailsDialog: WalletDialog() } enum class ProgressState { Loading, Done, Error } 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 b7582f4972..4e87055b3d 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 @@ -19,6 +19,8 @@ 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.configurable.warningMessage.WarningMessage +import com.tangem.tap.domain.termsOfUse.CardTou +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.adapters.SpacesItemDecoration @@ -26,6 +28,10 @@ import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter import com.tangem.tap.features.wallet.ui.wallet.MultiWalletView import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView import com.tangem.tap.features.wallet.ui.wallet.WalletView +import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog +import com.tangem.tap.features.wallet.ui.dialogs.PayIdDialog +import com.tangem.tap.features.wallet.ui.dialogs.QrDialog +import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog import com.tangem.tap.store import com.tangem.wallet.R import kotlinx.android.synthetic.main.fragment_wallet.* @@ -160,6 +166,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber R.id.chip_legacy +// is BitcoinAddressType.Segwit -> R.id.chip_default +// is CardanoAddressType.Byron -> R.id.chip_legacy +// is CardanoAddressType.Shelley -> R.id.chip_default +// else -> View.NO_ID +// } +// } +// +// fun idToType(id: Int, blockchain: Blockchain?): AddressType? { +// return when (id) { +// R.id.chip_default -> { +// when (blockchain) { +// Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> BitcoinAddressType.Segwit +// Blockchain.CardanoShelley -> CardanoAddressType.Shelley +// else -> null +// } +// } +// R.id.chip_legacy -> { +// when (blockchain) { +// Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> BitcoinAddressType.Legacy +// Blockchain.CardanoShelley -> CardanoAddressType.Byron +// else -> null +// } +// } +// else -> null +// } +// } +// } +//} + +//TODO: handle scan failed dialog: +//private fun handleDialogs(walletDialog: WalletDialog?) { +// when (walletDialog) { +// is WalletDialog.QrDialog -> { +// if (walletDialog.qrCode != null && walletDialog.shareUrl != null) { +// if (dialog == null) dialog = QrDialog(requireContext()).apply { +// this.showQr( +// walletDialog.qrCode, walletDialog.shareUrl, walletDialog.currencyName +// ) +// } +// } +// } +// is WalletDialog.CreatePayIdDialog -> { +// when (walletDialog.creatingPayIdState) { +// CreatingPayIdState.EnterPayId -> { +// if (dialog == null) dialog = PayIdDialog(requireContext()).apply { +// this.show() +// } +// (dialog as? PayIdDialog)?.stopProgress() +// } +// CreatingPayIdState.Waiting -> (dialog as? PayIdDialog)?.showProgress() +// } +// } +// is WalletDialog.SelectAmountToSendDialog -> { +// if (dialog == null) dialog = AmountToSendDialog(requireContext()).apply { +// this.show(walletDialog.amounts) +// } +// } +// is WalletDialog.ScanFailsDialog -> { +// if (dialog == null) dialog = ScanFailsDialog.create(requireContext()).apply { +// this.show() +// } +// } +// null -> { +// dialog?.dismiss() +// dialog = null +// } +// } +//} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt index 3200ac34ec..cc744be634 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt @@ -8,10 +8,16 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ItemDecoration +import com.google.android.play.core.review.ReviewManagerFactory +import com.tangem.tap.common.analytics.AnalyticsEvent +import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler import com.tangem.tap.common.extensions.getString +import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.configurable.warningMessage.WarningMessage +import com.tangem.tap.features.feedback.RateCanBeBetterEmail +import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.store import com.tangem.wallet.R import kotlinx.android.synthetic.main.layout_warning.view.* @@ -40,7 +46,7 @@ class WarningMessageVH(val view: View) : RecyclerView.ViewHolder(view) { fun bind(warning: WarningMessage) { setBgColor(warning.priority) setText(warning) - setupOkButton(warning) + setupControlButtons(warning) } private fun setText(warning: WarningMessage) { @@ -59,10 +65,50 @@ class WarningMessageVH(val view: View) : RecyclerView.ViewHolder(view) { view.card_view.setCardBackgroundColor(view.context.resources.getColor(color)) } - private fun setupOkButton(warning: WarningMessage) { - view.btn_got_it.show(warning.type == WarningMessage.Type.Temporary) - view.btn_got_it.setOnClickListener { - store.dispatch(GlobalAction.HideWarningMessage(warning)) + private fun setupControlButtons(warning: WarningMessage) { + when (warning.type) { + WarningMessage.Type.Permanent -> { + view.group_controls_temporary.hide() + view.group_controls_rating.hide() + } + WarningMessage.Type.Temporary -> { + view.group_controls_rating.hide() + view.group_controls_temporary.show() + view.btn_got_it.setOnClickListener { store.dispatch(GlobalAction.HideWarningMessage(warning)) } + } + WarningMessage.Type.AppRating -> { + view.group_controls_temporary.hide() + view.group_controls_rating.show() + view.btn_close.setOnClickListener { + FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_DISMISS) + store.dispatch(GlobalAction.HideWarningMessage(warning)) + store.dispatch(WalletAction.Warnings.AppRating.RemindLater) + } + view.btn_can_be_better.setOnClickListener { + FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_NEGATIVE) + store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow) + store.dispatch(GlobalAction.HideWarningMessage(warning)) + store.dispatch(GlobalAction.SendFeedback(RateCanBeBetterEmail())) + } + view.btn_really_cool.setOnClickListener { + FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_POSITIVE) + store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow) + val context = view.context + val reviewManager = ReviewManagerFactory.create(context) + val flow = reviewManager.requestReviewFlow() + flow.addOnCompleteListener { + if (it.isSuccessful) { +// val info = it.result +// Toast.makeText(context, "success", Toast.LENGTH_SHORT).show() + } else { +// Toast.makeText(context, "fail", Toast.LENGTH_SHORT).show() + } + }.addOnFailureListener { +// Toast.makeText(context, "failure", Toast.LENGTH_SHORT).show() + } + store.dispatch(GlobalAction.HideWarningMessage(warning)) + } + } } } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt new file mode 100644 index 0000000000..9a8ff9ace5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt @@ -0,0 +1,30 @@ +package com.tangem.tap.features.wallet.ui.dialogs + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.feedback.ScanFailsEmail +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.store +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +class ScanFailsDialog { + + companion object { + fun create(context: Context): AlertDialog { + return AlertDialog.Builder(context).apply { + setTitle(context.getString(R.string.common_warning)) + setMessage(R.string.alert_troubleshooting_scan_card_title) + setPositiveButton(R.string.alert_button_request_support) { _, _ -> + store.dispatch(GlobalAction.SendFeedback(ScanFailsEmail())) + } + setNegativeButton(R.string.common_cancel) { _, _ -> } + setOnDismissListener { store.dispatch(WalletAction.HideDialog) } + }.create() + } + } + +} \ No newline at end of file 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 08346630f3..9d45dd8c36 100644 --- a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt +++ b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt @@ -11,16 +11,18 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY import com.tangem.tap.common.redux.global.FiatCurrencyName import com.tangem.tap.network.coinmarketcap.FiatCurrency +import java.util.* class PreferencesStorage(applicationContext: Application) { - private val preferences: SharedPreferences by lazy { - applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) - } + private val preferences: SharedPreferences = applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) + + val appRatingLaunchObserver: AppRatingLaunchObserver init { incrementLaunchCounter() + appRatingLaunchObserver = AppRatingLaunchObserver(preferences, getCountOfLaunches()) } private val fiatCurrenciesAdapter: JsonAdapter> by lazy { @@ -97,4 +99,60 @@ class PreferencesStorage(applicationContext: Application) { private const val APP_LAUNCH_COUNT_KEY = "launchCount" } +} + +class AppRatingLaunchObserver( + private val preferences: SharedPreferences, + private val launchCounts: Int, +) { + private val K_SHOW_RATING_AT_LAUNCH_COUNT = "showRatingDialogAtLaunchCount" + private val K_FUNDS_FOUND_DATE = "fundsFoundDate" + private val K_USER_WAS_INTERACT_WITH_RATING = "userWasInteractWithRating" + + private val deferShowing = 20 + private val firstShowing = 3 + private var fundsFoundDate: Calendar? = null + + init { + val dateTimeMs = preferences.getLong(K_FUNDS_FOUND_DATE, -1) + if (dateTimeMs > 0) fundsFoundDate = Calendar.getInstance().apply { timeInMillis = dateTimeMs } + } + + fun foundWalletWithFunds() { + if (fundsFoundDate != null) return + + fundsFoundDate = Calendar.getInstance() + preferences.edit().putLong(K_FUNDS_FOUND_DATE, fundsFoundDate!!.timeInMillis).apply() + } + + fun isReadyToShow(): Boolean { + val fundsDate = fundsFoundDate ?: return false + + if (!userWasInteractWithRating()) { + val diff = Calendar.getInstance().timeInMillis - fundsDate.timeInMillis + val diffInDays = diff / (100 * 60 * 60 * 24) + if (diffInDays >= firstShowing) return true + } + + val nextShowing = getCounterOfNextShowing() + return launchCounts >= nextShowing + } + + fun applyDelayedShowing() { + updateNextShowing(launchCounts + deferShowing) + } + + fun setNeverToShow() { + updateNextShowing(999999999) + } + + private fun updateNextShowing(at: Int) { + val editor = preferences.edit() + editor.putInt(K_SHOW_RATING_AT_LAUNCH_COUNT, at) + editor.putBoolean(K_USER_WAS_INTERACT_WITH_RATING, true) + editor.apply() + } + + private fun userWasInteractWithRating(): Boolean = preferences.getBoolean(K_USER_WAS_INTERACT_WITH_RATING, false) + private fun getCounterOfNextShowing(): Int = preferences.getInt(K_SHOW_RATING_AT_LAUNCH_COUNT, firstShowing) } \ 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 8fe7ff059f..b45e3e92ce 100644 --- a/app/src/main/res/layout/fragment_details.xml +++ b/app/src/main/res/layout/fragment_details.xml @@ -126,6 +126,21 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_signed_hashes_title" /> + + + app:layout_constraintTop_toBottomOf="@id/tv_card_tou" /> + + + app:layout_constraintTop_toBottomOf="@id/tv_send_feedback" /> + + +