Updated on 2026-08-14

This commit is contained in:
Tangem 2023-07-24 15:37:38 +03:00
commit e3c0b0b311
47 changed files with 528 additions and 187 deletions

@ -1 +1 @@
Subproject commit fad890b2a0b552be60d124949ca0a3a4d672dac1
Subproject commit b791bd4cf6c5eca9778f89e87cd62b72d24f5ce9

View file

@ -289,9 +289,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
store.dispatchOnMain(WelcomeAction.SetInitialIntent(intentWhichStartedActivity))
scope.launch {
val handler = BackgroundScanIntentHandler(hasSavedUserWalletsProvider = { true })
val isBackgroundScanNotHandled = handler.handleIntent(intentWhichStartedActivity)
val isBackgroundScanHandled = handler.handleIntent(intentWhichStartedActivity)
val hasNotIncompletedBackup = !backupService.hasIncompletedBackup
if (isBackgroundScanNotHandled && hasNotIncompletedBackup) {
if (!isBackgroundScanHandled && hasNotIncompletedBackup) {
store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics)
}
}

View file

@ -18,7 +18,8 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import java.math.BigDecimal
import java.util.*
import java.util.Currency
import java.util.UUID
class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
@ -82,16 +83,18 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
suspend fun checkIfGooglePayAvailable(googlePayService: GooglePayService): Result<Boolean> {
this.googlePayService = googlePayService
return googlePayService.checkIfGooglePayAvailable()
return googlePayService.checkIfGooglePayAvailable(shopifyService.shop.merchantID != null)
}
fun buyWithGooglePay(productType: ProductType) {
val totalPrice = checkouts[productType]!!.totalPriceV2.amount
googlePayService.payWithGooglePay(
totalPriceCents = totalPrice,
currencyCode = checkouts[productType]!!.currencyCode.name,
merchantID = shopifyService.shop.merchantID,
)
shopifyService.shop.merchantID?.let { merchantId ->
val totalPrice = checkouts[productType]!!.totalPriceV2.amount
googlePayService.payWithGooglePay(
totalPriceCents = totalPrice,
currencyCode = checkouts[productType]!!.currencyCode.name,
merchantID = merchantId,
)
}
}
// fun subscribeToGooglePayResult(
@ -185,7 +188,6 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
),
appliedDiscount = it.getAppliedDiscount(),
),
)
}
return Result.failure(result.exceptionOrNull()!!)

View file

@ -6,11 +6,7 @@ import android.app.Activity.RESULT_OK
import android.content.Intent
import android.util.Log
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.wallet.AutoResolveHelper
import com.google.android.gms.wallet.IsReadyToPayRequest
import com.google.android.gms.wallet.PaymentData
import com.google.android.gms.wallet.PaymentDataRequest
import com.google.android.gms.wallet.PaymentsClient
import com.google.android.gms.wallet.*
import com.tangem.common.core.TangemSdkError
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -24,7 +20,8 @@ class GooglePayService(private val paymentsClient: PaymentsClient, private val a
// var responseCallback: ((Result<PaymentData>) -> Unit)? = null
suspend fun checkIfGooglePayAvailable(): Result<Boolean> {
suspend fun checkIfGooglePayAvailable(isMerchantAvailable: Boolean): Result<Boolean> {
if (!isMerchantAvailable) return Result.success(false)
val isReadyToPayJson = GooglePayUtil.isReadyToPayRequest() ?: return Result.success(false)
val request = IsReadyToPayRequest.fromJson(isReadyToPayJson.toString())

View file

@ -7,6 +7,7 @@ import com.tangem.TangemSdk
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.*
import com.tangem.common.biometric.BiometricManager
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.*
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.usersCode.UserCodeRepository
@ -230,4 +231,28 @@ class TangemSdkManager(private val cardSdkConfigRepository: CardSdkConfigReposit
fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String {
return resources.getString(stringResId, *formatArgs)
}
fun setAccessCodeRequestPolicy(useBiometricsForAccessCode: Boolean) {
tangemSdk.config.userCodeRequestPolicy = if (useBiometricsForAccessCode) {
UserCodeRequestPolicy.AlwaysWithBiometrics(codeType = UserCodeType.AccessCode)
} else {
UserCodeRequestPolicy.Default
}
}
fun useBiometricsForAccessCode(): Boolean {
val policy = tangemSdk.config.userCodeRequestPolicy
return policy is UserCodeRequestPolicy.AlwaysWithBiometrics && policy.codeType == UserCodeType.AccessCode
}
companion object {
val config = Config(
linkedTerminal = true,
allowUntrustedCards = true,
filter = CardFilter(
allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(),
maxFirmwareVersion = FirmwareVersion(major = 6, minor = 21),
),
)
}
}

View file

@ -64,7 +64,7 @@ sealed class TapSdkError(override val messageResId: Int?) : TangemError(code = 5
override var customMessage: String = code.toString()
object CardForDifferentApp : TapSdkError(R.string.alert_unsupported_card)
object CardNotSupportedByRelease : TapSdkError(R.string.error_update_app)
object CardNotSupportedByRelease : TapSdkError(R.string.error_wrong_card_type)
}
fun TapErrors.assembleErrors(): MutableList<Pair<Int, List<Any>?>> {

View file

@ -9,6 +9,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.TapWorkarounds.canSkipBackup
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.ScanResponse
@ -153,7 +154,13 @@ internal object LegacyScanProcessor {
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
Analytics.addContext(scanResponse)
onWalletNotCreated()
store.dispatchOnMain(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = true))
// must check skip backup using card canSkipBackup
store.dispatchOnMain(
GlobalAction.Onboarding.Start(
scanResponse = scanResponse,
canSkipBackup = scanResponse.card.canSkipBackup,
),
)
val appScreen = OnboardingHelper.whereToNavigate(scanResponse)
navigateTo(appScreen) { onProgressStateChange(it) }
} else {

View file

@ -7,6 +7,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.common.TapWorkarounds.canSkipBackup
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.models.scan.ScanResponse
@ -50,7 +51,13 @@ class CheckForOnboardingChain(
return when {
OnboardingHelper.isOnboardingCase(previousChainResult) -> {
Analytics.addContext(previousChainResult)
store.dispatchOnMain(GlobalAction.Onboarding.Start(previousChainResult, canSkipBackup = true))
// must check skip backup using card canSkipBackup
store.dispatchOnMain(
GlobalAction.Onboarding.Start(
scanResponse = previousChainResult,
canSkipBackup = previousChainResult.card.canSkipBackup,
),
)
val appScreen = OnboardingHelper.whereToNavigate(previousChainResult)
ScanChainException.OnboardingNeeded(appScreen).left()
}

View file

@ -51,7 +51,7 @@ class ScanProductTask(
}
val cardDto = CardDTO(card)
val error = getErrorIfExcludedCard(cardDto)
val error = getErrorIfExcludedCard(cardDto, card)
if (error != null) {
callback(CompletionResult.Failure(error))
return
@ -81,9 +81,11 @@ class ScanProductTask(
}
}
private fun getErrorIfExcludedCard(card: CardDTO): TangemError? {
if (card.isExcluded) return TapSdkError.CardForDifferentApp
if (card.isNotSupportedInThatRelease) return TapSdkError.CardNotSupportedByRelease
private fun getErrorIfExcludedCard(cardDto: CardDTO, card: Card): TangemError? {
if (cardDto.isExcluded) return TapSdkError.CardForDifferentApp
if (cardDto.isNotSupportedInThatRelease) return TapSdkError.CardNotSupportedByRelease
// todo check isImported to prevent using old app with imported wallet, remove before wallet 2.0 enabled ([REDACTED_TASK_KEY])
if (card.wallets.any { it.isImported }) return TapSdkError.CardNotSupportedByRelease
return null
}
}

View file

@ -100,7 +100,7 @@ internal class BiometricUserWalletsListManager(
} else {
val isWalletSaved = state.value.userWallets
.any {
it.walletId == userWallet.walletId || it.cardsInWallet.contains(userWallet.cardId)
it.walletId == userWallet.walletId
}
if (isWalletSaved) {

View file

@ -6,6 +6,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer
import com.tangem.tap.domain.walletconnect2.domain.WcRequest
import com.tangem.tap.domain.walletconnect2.domain.models.*
import com.walletconnect.android.Core
import com.walletconnect.android.CoreClient
@ -87,7 +88,10 @@ class WalletConnectRepositoryImpl @Inject constructor(
private fun defineWalletDelegate(): Web3Wallet.WalletDelegate {
return object : Web3Wallet.WalletDelegate {
override fun onSessionProposal(sessionProposal: Wallet.Model.SessionProposal) {
override fun onSessionProposal(
sessionProposal: Wallet.Model.SessionProposal,
verifyContext: Wallet.Model.VerifyContext,
) {
// Triggered when wallet receives the session proposal sent by a Dapp
Timber.d("sessionProposal: $sessionProposal")
this@WalletConnectRepositoryImpl.sessionProposal = sessionProposal
@ -106,7 +110,10 @@ class WalletConnectRepositoryImpl @Inject constructor(
}
}
override fun onSessionRequest(sessionRequest: Wallet.Model.SessionRequest) {
override fun onSessionRequest(
sessionRequest: Wallet.Model.SessionRequest,
verifyContext: Wallet.Model.VerifyContext,
) {
// Triggered when a Dapp sends SessionRequest to sign a transaction or a message
Timber.d("sessionRequest: $sessionRequest")
val request = wcRequestDeserializer.deserialize(
@ -115,22 +122,38 @@ class WalletConnectRepositoryImpl @Inject constructor(
)
Timber.d("sessionRequestParsed: $request")
scope.launch {
_events.emit(
WalletConnectEvents.SessionRequest(
request = request,
chainId = sessionRequest.chainId,
when (request) {
is WcRequest.AddChain -> {
// we can send approval automatically, because in WC 2.0 the list of chains is approved when
// initial connection is established
sendRequest(
topic = sessionRequest.topic,
id = sessionRequest.request.id,
metaUrl = sessionRequest.peerMetaData?.url ?: "",
metaName = sessionRequest.peerMetaData?.name ?: "",
),
)
result = "",
)
}
else ->
scope.launch {
_events.emit(
WalletConnectEvents.SessionRequest(
request = request,
chainId = sessionRequest.chainId,
topic = sessionRequest.topic,
id = sessionRequest.request.id,
metaUrl = sessionRequest.peerMetaData?.url ?: "",
metaName = sessionRequest.peerMetaData?.name ?: "",
),
)
}
}
}
override fun onAuthRequest(authRequest: Wallet.Model.AuthRequest) {
override fun onAuthRequest(
authRequest: Wallet.Model.AuthRequest,
verifyContext: Wallet.Model.VerifyContext,
) {
// Triggered when Dapp / Requester makes an authorization request
Timber.d("onAuthRequest: $authRequest")
}
override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) {
@ -174,6 +197,7 @@ class WalletConnectRepositoryImpl @Inject constructor(
override fun onError(error: Wallet.Model.Error) {
// Triggered whenever there is an issue inside the SDK
Timber.d("onError: $error")
}
}
}

View file

@ -227,7 +227,15 @@ class WalletConnectInteractor(
}
}
fun isWalletConnectUri(uri: String): Boolean {
return uri.lowercase().startsWith(WC_SCHEME)
}
private suspend fun prepareRequestData(sessionRequest: WalletConnectEvents.SessionRequest): WcPreparedRequest? {
return sessionRequestConverter.prepareRequest(sessionRequest, userWalletId)
}
companion object {
private const val WC_SCHEME = "wc"
}
}

View file

@ -1,9 +1,6 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.*
import com.tangem.datasource.di.SdkMoshi
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WCBinanceTxConfirmParam
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceCancelOrder
@ -24,6 +21,7 @@ enum class WcJrpcMethods(val code: String) {
BNB_SIGN("bnb_sign"),
BNB_TRANSACTION_CONFIRM("bnb_tx_confirmation"),
SIGN_TRANSACTION("trust_signTransaction"),
WALLET_ADD_ETHEREUM_CHAIN("wallet_addEthereumChain"),
;
companion object {
@ -33,13 +31,25 @@ enum class WcJrpcMethods(val code: String) {
@JsonClass(generateAdapter = true)
data class WCSignTransaction(
@Json(name = "network")
val network: Int,
@Json(name = "transaction")
val transaction: String,
) : WcRequestData
@JsonClass(generateAdapter = true)
data class WcAddChain(
@Json(name = "chainId")
val chainId: String,
) : WcRequestData
@JsonClass(generateAdapter = true)
data class WcEthereumSignMessage(
@Json(name = "raw")
val raw: List<String>,
@Json(name = "type")
val type: WCSignType,
) : WcRequestData {
enum class WCSignType {
@ -71,15 +81,34 @@ data class WcEthereumSignMessage(
@JsonClass(generateAdapter = true)
data class WcEthereumTransaction(
@Json(name = "from")
val from: String,
@Json(name = "to")
val to: String?,
@Json(name = "nonce")
val nonce: String?,
@Json(name = "gasPrice")
val gasPrice: String?,
@Json(name = "maxFeePerGas")
val maxFeePerGas: String?,
@Json(name = "maxPriorityFeePerGas")
val maxPriorityFeePerGas: String?,
@Json(name = "gas")
val gas: String?,
@Json(name = "gasLimit")
val gasLimit: String?,
@Json(name = "value")
val value: String?,
@Json(name = "data")
val data: String,
) : WcRequestData
@ -96,6 +125,7 @@ sealed class WcRequest(open val data: WcRequestData) {
data class BnbTransfer(override val data: WcBinanceTransferOrder) : WcRequest(data)
data class BnbTxConfirm(override val data: WCBinanceTxConfirmParam) : WcRequest(data)
data class SignTransaction(override val data: WCSignTransaction) : WcRequest(data)
data class AddChain(override val data: WcAddChain) : WcRequest(data)
data class CustomRequest(override val data: WcCustomRequestData) : WcRequest(data)
}
@ -167,6 +197,12 @@ class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi
).fromJsonFirstOrNull(params) ?: return customRequest
WcRequest.SignTransaction(data = deserializedParams)
}
WcJrpcMethods.WALLET_ADD_ETHEREUM_CHAIN -> {
val deserializedParams: WcAddChain = moshi.adapter<List<WcAddChain>>(
Types.newParameterizedType(List::class.java, WcAddChain::class.java),
).fromJsonFirstOrNull(params) ?: return customRequest
WcRequest.AddChain(data = deserializedParams)
}
}
}

View file

@ -70,7 +70,7 @@ class WalletConnectMiddleware {
}
is WalletConnectAction.StartWalletConnect -> {
val uri = action.copiedUri
if (uri != null && WalletConnectManager.isCorrectWcUri(uri)) {
if (uri != null && isWalletConnectUri(uri)) {
store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri))
} else {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.QrScan))
@ -442,4 +442,8 @@ class WalletConnectMiddleware {
)
return walletState.getWalletManager(blockchainNetwork)
}
private fun isWalletConnectUri(uri: String): Boolean {
return WalletConnectManager.isCorrectWcUri(uri) || walletConnectInteractor.isWalletConnectUri(uri)
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.TapWorkarounds.canSkipBackup
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.ProductType
@ -37,7 +38,10 @@ object OnboardingHelper {
}
}
response.cardTypesResolver.isWallet2() -> {
// TODO for Shiba disabled check wallet 2, and only check canSkipBackup, enable when release wallet 2
// ([REDACTED_TASK_KEY])
// response.cardTypesResolver.isWallet2() -> {
!response.card.canSkipBackup -> {
val emptyWallets = response.card.wallets.isEmpty()
val activationInProgress = cardInfoStorage.isActivationInProgress(cardId)
val backupNotActive = response.card.backupStatus?.isActive != true

View file

@ -9,6 +9,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.BlockchainNetwork
import com.tangem.domain.common.TapWorkarounds.canSkipBackup
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
@ -340,7 +341,9 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
is CompletionResult.Failure -> {
val error = result.error
if (error is TangemSdkError.BackupFailedNotEmptyWallets &&
onboardingWalletState.wallet2State != null
// todo disabled this check in task with shiba ([REDACTED_TASK_KEY]) and added canSkipBackup
// && onboardingWalletState.wallet2State != null
card?.canSkipBackup == false
) {
store.dispatchOnMain(
GlobalAction.ShowDialog(

View file

@ -12,6 +12,7 @@ import com.tangem.tap.common.extensions.toFormattedCryptoCurrencyString
import com.tangem.tap.common.extensions.toFormattedFiatValue
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.models.WalletWarning
import com.tangem.tap.features.wallet.redux.WalletMainButton
import com.tangem.tap.features.wallet.redux.utils.UNKNOWN_AMOUNT_SIGN
@ -26,15 +27,14 @@ internal fun WalletDataModel.mainButton(blockchainAmount: BigDecimal): WalletMai
internal fun WalletDataModel.hasPendingTransactions(): Boolean {
// for now check pending ongoing only just for BTC, later test and add other utxo networks
// disabled for release 4.8, test and enable in 4.9
// val isBitcoinBlockchain =
// currency.blockchain == Blockchain.Bitcoin || currency.blockchain == Blockchain.BitcoinTestnet
// if (currency.isBlockchain() && isBitcoinBlockchain) {
// val outgoingTransactions = status.pendingTransactions.filter {
// it.type == PendingTransactionType.Outgoing
// }
// return outgoingTransactions.isEmpty()
// }
val isBitcoinBlockchain =
currency.blockchain == Blockchain.Bitcoin || currency.blockchain == Blockchain.BitcoinTestnet
if (currency.isBlockchain() && isBitcoinBlockchain) {
val outgoingTransactions = status.pendingTransactions.filter {
it.type == PendingTransactionType.Outgoing
}
return outgoingTransactions.isEmpty()
}
return status.pendingTransactions.isEmpty()
}

View file

@ -7,7 +7,6 @@ import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.datasource.api.common.createRetrofitInstance
import com.tangem.domain.common.extensions.withIOContext
import com.tangem.tap.common.extensions.urlEncode
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
@ -103,39 +102,37 @@ class MoonPayService(
cryptoCurrencyName: CryptoCurrencyName,
fatCurrency: String,
walletAddress: String,
): String? {
): String {
if (action == CurrencyExchangeManager.Action.Buy) throw UnsupportedOperationException()
val uri = Uri.Builder()
.scheme(SCHEME)
.authority(URL_SELL)
.appendQueryParameter("apiKey", apiKey.urlEncode())
.appendQueryParameter("baseCurrencyCode", cryptoCurrencyName.urlEncode())
.appendQueryParameter("refundWalletAddress", walletAddress.urlEncode())
.appendQueryParameter("redirectURL", "tangem://sell-request.tangem.com".urlEncode())
.appendQueryParameter("apiKey", apiKey)
.appendQueryParameter("baseCurrencyCode", cryptoCurrencyName)
.appendQueryParameter("refundWalletAddress", walletAddress)
.appendQueryParameter("redirectURL", "tangem://sell-request.tangem.com")
val originalQuery = uri.build().toString()
val originalQuery = uri.build().encodedQuery ?: uri.build().toString()
val signature = createSignature(originalQuery)
uri.appendQueryParameter("signature", signature.urlEncode())
uri.appendQueryParameter("signature", signature)
val url = uri.build().toString()
return url
return uri.build().toString()
}
override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? {
val url = Uri.Builder()
override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String {
return Uri.Builder()
.scheme(SCHEME)
.authority(URL_SELL)
.appendPath("transaction_receipt")
.appendQueryParameter("transactionId", transactionId).build().toString()
return url
}
private fun createSignature(data: String): String {
val sha256Hmac = Mac.getInstance("HmacSHA256")
val secretKey = SecretKeySpec(secretKey.toByteArray(), "HmacSHA256")
sha256Hmac.init(secretKey)
val sha256encoded = sha256Hmac.doFinal(data.toByteArray())
val sha256encoded = sha256Hmac.doFinal("?$data".toByteArray())
return Base64.encodeToString(sha256encoded, Base64.NO_WRAP)
}

View file

@ -12,6 +12,8 @@ import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.walletStores.WalletStoresManager
import com.tangem.tap.features.wallet.redux.WalletState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import org.rekotlin.Store
import javax.inject.Inject
@ -22,6 +24,15 @@ import javax.inject.Inject
class AppStateHolder @Inject constructor() : WalletsStateHolder, NavigationStateHolder {
override var userWalletsListManager: UserWalletsListManager? = null
set(value) {
field = value
_userWalletsListManagerFlow.value = value
}
override val userWalletListManagerFlow: Flow<UserWalletsListManager?>
get() = _userWalletsListManagerFlow
private val _userWalletsListManagerFlow = MutableStateFlow<UserWalletsListManager?>(null)
@Deprecated("Use scan response from selected user wallet")
var scanResponse: ScanResponse? = null

View file

@ -3,6 +3,8 @@ package com.tangem.tap.proxy.di
import androidx.compose.ui.text.intl.Locale
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.feature.learn2earn.domain.api.Learn2earnDependencyProvider
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.TransactionManager
@ -15,6 +17,8 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import javax.inject.Singleton
@Module
@ -62,10 +66,19 @@ class ProxyModule {
@Singleton
fun provideLear2earnDependencies(appStateHolder: AppStateHolder): Learn2earnDependencyProvider {
return object : Learn2earnDependencyProvider {
override fun getUserCountryCodeProvider(): () -> String = {
appStateHolder.mainStore?.state?.globalState?.userCountryCode ?: Locale.current.language
@OptIn(ExperimentalCoroutinesApi::class)
override fun getCardTypeResolverFlow(): Flow<CardTypesResolver?> {
return appStateHolder.userWalletListManagerFlow
.flatMapLatest { manager ->
manager?.selectedUserWallet
?.map { it.scanResponse.cardTypesResolver }
?: flowOf(null)
}
}
override fun getLocaleProvider(): () -> String = { Locale.current.language }
override fun getWebViewAuthCredentialsProvider(): () -> String? = {
appStateHolder.mainStore?.state?.globalState?.configManager?.config?.tangemComAuthorization
}

View file

@ -5,8 +5,10 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ShopifyShop(
@Json(name = "domain")
val domain: String,
@Json(name = "storefrontApiKeyAndroid")
val storefrontApiKey: String,
val merchantID: String,
@Json(name = "merchantID")
val merchantID: String?,
)

View file

@ -9,7 +9,7 @@
},
{
"name": "NEW_CARD_SCANNING_ENABLED",
"version": "4.9.0"
"version": "4.10.0"
},
{
"name": "REDESIGNED_WALLET_SCREEN_ENABLED",
@ -17,7 +17,7 @@
},
{
"name": "1INCH_LEARN_2_EARN_ENABLED",
"version": "undefined"
"version": "4.9.0"
},
{
"name": "REDESIGNED_TOKEN_DETAIL_SCREEN_ENABLED",

View file

@ -68,7 +68,6 @@
<string name="common_exchange">Обменять</string>
<string name="common_explore_transaction_history">Посмотреть историю транзакций</string>
<string name="common_explorer">Обозреватель</string>
<string name="common_learn_and_earn">Учись и получай бонусы!</string>
<string name="common_like">Нравится</string>
<string name="common_main_network">Основная сеть</string>
<string name="common_no">Нет</string>
@ -139,6 +138,7 @@
<string name="disclaimer_error_loading">Проверьте подключение с интернетом или переключитесь на другую сеть</string>
<string name="disclaimer_title">Условия использования</string>
<string name="error_update_app">К сожалению, текущая версия приложения не готова к работе с этой картой, проверьте наличие обновлений</string>
<string name="error_wrong_card_type">Данное приложение не предназначено для работы с этой картой или требует обновления</string>
<string name="error_wrong_wallet_tapped">Вы использовали карту от другого кошелька. Приложите карту, связанную с этим кошельком.</string>
<string name="exchange_receive_view_header">Вы получаете</string>
<string name="exchange_send_view_header">Вы отправляете</string>
@ -163,10 +163,10 @@
<string name="key_invalidated_warning_description">Вы обновили данные биометрии, отсканируйте свою карту для входа</string>
<string name="main_get_bonus_subtitle">Вы успешно прошли все уроки и теперь можете получить 1INCH токены</string>
<plurals name="main_learn_subtitle">
<item quantity="one">Пройдите 3 урока и получите %d 1INCH токен на свой кошелек</item>
<item quantity="few">Пройдите 3 урока и получите %d 1INCH токена на свой кошелек</item>
<item quantity="many">Пройдите 3 урока и получите %d 1INCH токена на свой кошелек</item>
<item quantity="other">Пройдите 3 урока и получите %d 1INCH токенов на свой кошелек</item>
<item quantity="one">Пройдите 3 урока и получите %d 1INCH токен на свой кошелек</item>
<item quantity="few">Пройдите 3 урока и получите %d 1INCH токена на свой кошелек</item>
<item quantity="many">Пройдите 3 урока и получите %d 1INCH токенов на свой кошелек</item>
<item quantity="other">Пройдите 3 урока и получите %d 1INCH токенов на свой кошелек</item>
</plurals>
<string name="main_manage_tokens">Управление токенами</string>
<string name="main_no_backup_warning_subtitle">Чтобы защитить свои активы, мы советуем вам выполнить эту процедуру</string>
@ -214,6 +214,7 @@
<string name="onboarding_exit_alert_message">В этом случае вам будет необходимо начать процесс заново.</string>
<string name="onboarding_exit_alert_title">Вы хотите выйти из процесса активации?</string>
<string name="onboarding_getting_started">Подготовка</string>
<string name="onboarding_linking_error_card_with_wallets">Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Хотите сбросить его и использовать карту для бэкапа?</string>
<string name="onboarding_navbar_pin">Код доступа</string>
<string name="onboarding_navbar_register_wallet">Подключиться</string>
<string name="onboarding_navbar_title_creating_backup">Резервная копия</string>

View file

@ -187,6 +187,7 @@
<string name="onboarding_exit_alert_message">這此情況,您必須要重新開始</string>
<string name="onboarding_exit_alert_title">您想要離開啟用程序嗎?</string>
<string name="onboarding_getting_started">開始</string>
<string name="onboarding_linking_error_card_with_wallets">您要添加的卡上已經創建了另一個錢包。你想重置它並將卡用於新錢包嗎</string>
<string name="onboarding_navbar_pin">PIN 碼</string>
<string name="onboarding_navbar_register_wallet">連接</string>
<string name="onboarding_navbar_title_creating_backup">創建備份</string>

View file

@ -137,6 +137,7 @@
<string name="disclaimer_error_loading">Check your internet connection or switch to a different network</string>
<string name="disclaimer_title">Terms of Service</string>
<string name="error_update_app">Oops, the current version of the application is not ready to work with this card, please check for updates.</string>
<string name="error_wrong_card_type">This application is not designed to work with this card or needs to be updated</string>
<string name="error_wrong_wallet_tapped">You have used a card from another wallet. Tap the card associated with this wallet</string>
<string name="exchange_receive_view_header">You Receive</string>
<string name="exchange_send_view_header">You Send</string>
@ -161,8 +162,8 @@
<string name="key_invalidated_warning_description">You have updated biometrics, scan your card to enter</string>
<string name="main_get_bonus_subtitle">You have completed all of the lessons, and are now eligible to receive your 1INCH tokens</string>
<plurals name="main_learn_subtitle">
<item quantity="one">Complete three lessons and receive %d 1INCH token to your wallet</item>
<item quantity="other">Complete three lessons and receive %d 1INCH tokens to your wallet</item>
<item quantity="one">Complete three lessons and receive %d 1INCH token to your wallet</item>
<item quantity="other">Complete three lessons and receive %d 1INCH tokens to your wallet</item>
</plurals>
<string name="main_manage_tokens">Manage tokens</string>
<string name="main_no_backup_warning_subtitle">To protect your assets, we advise you to carry out this procedure</string>

View file

@ -29,7 +29,8 @@ internal class TangemCardTypesResolver(
}
override fun isWallet2(): Boolean {
return card.firmwareVersion >= FirmwareVersion.KeysImportAvailable && card.settings.isKeysImportAllowed
// todo for now disabled to prevent Shiba cards using as wallet 2, enable when release wallet 2.0 ([REDACTED_TASK_KEY])
return false // card.firmwareVersion >= FirmwareVersion.KeysImportAvailable && card.settings.isKeysImportAllowed
}
override fun isTangemTwins(): Boolean = productType == ProductType.Twins

View file

@ -3,6 +3,7 @@ package com.tangem.domain.common
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.card.Card
import com.tangem.common.card.FirmwareVersion
import com.tangem.domain.models.scan.CardDTO
import java.util.*
@ -23,6 +24,10 @@ object TapWorkarounds {
val CardDTO.isTestCard: Boolean
get() = batchId == TEST_CARD_BATCH && cardId.startsWith(TEST_CARD_ID_STARTS_WITH)
// for cards 6.21 and higher backup is not skippable
val CardDTO.canSkipBackup: Boolean
get() = this.firmwareVersion < FirmwareVersion.KeysImportAvailable
val CardDTO.useOldStyleDerivation: Boolean
get() = batchId == "AC01" || batchId == "AC02" || batchId == "CB95"

View file

@ -190,4 +190,6 @@ fun Blockchain.isSupportedInApp(): Boolean {
private val excludedBlockchains = listOf(
Blockchain.Unknown,
Blockchain.Ducatus,
Blockchain.Telos, // disable in 4.9
Blockchain.TelosTestnet, // disable in 4.9
)

View file

@ -1,6 +1,10 @@
package com.tangem.domain.wallets.legacy
import kotlinx.coroutines.flow.Flow
interface WalletsStateHolder {
val userWalletsListManager: UserWalletsListManager?
val userWalletListManagerFlow: Flow<UserWalletsListManager?>
}

View file

@ -10,6 +10,7 @@ plugins {
dependencies {
/** Core modules */
implementation(project(":common"))
implementation(project(":domain:legacy"))
implementation(project(":core:analytics"))
implementation(projects.core.analytics.models)
implementation(project(":core:featuretoggles"))
@ -36,6 +37,7 @@ dependencies {
implementation(deps.compose.foundation)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.accompanist.webView)
/** Preferences */
implementation(deps.krateSharedPref)

View file

@ -121,6 +121,7 @@ internal class DefaultLearn2earnRepository(
promoCode = null,
isRegisteredInPromotion = false,
isAlreadyReceivedAward = false,
isLearningStageFinished = false,
)
private companion object {

View file

@ -5,6 +5,7 @@ package com.tangem.feature.learn2earn.data.models
*/
data class PromoUserData(
val promoCode: String?,
val isLearningStageFinished: Boolean,
val isRegisteredInPromotion: Boolean,
val isAlreadyReceivedAward: Boolean,
)

View file

@ -2,8 +2,10 @@ package com.tangem.feature.learn2earn.domain
import android.net.Uri
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.datasource.api.promotion.models.AbstractPromotionResponse
import com.tangem.datasource.api.promotion.models.PromotionInfoResponse
import com.tangem.datasource.demo.DemoModeDatasource
import com.tangem.feature.learn2earn.analytics.AnalyticsParam
import com.tangem.feature.learn2earn.analytics.Learn2earnEvents.*
import com.tangem.feature.learn2earn.data.api.Learn2earnRepository
import com.tangem.feature.learn2earn.data.models.PromoUserData
import com.tangem.feature.learn2earn.data.toggles.Learn2earnFeatureToggleManager
@ -14,27 +16,45 @@ import com.tangem.feature.learn2earn.domain.models.toDomainError
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.lib.crypto.models.Currency
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
@Suppress("LongParameterList")
internal class DefaultLearn2earnInteractor(
private val featureToggleManager: Learn2earnFeatureToggleManager,
private val repository: Learn2earnRepository,
private val userWalletManager: UserWalletManager,
private val derivationManager: DerivationManager,
private val analyticsEventHandler: AnalyticsEventHandler,
dependencyProvider: Learn2earnDependencyProvider,
private val analytics: AnalyticsEventHandler,
private val demoModeDatasource: DemoModeDatasource,
private val dependencyProvider: Learn2earnDependencyProvider,
dispatchers: AppCoroutineDispatcherProvider,
) : Learn2earnInteractor {
override var webViewResultHandler: WebViewResultHandler? = null
private lateinit var promotion: Promotion
private val scope = CoroutineScope(Job() + dispatchers.io + FeatureCoroutineExceptionHandler.create("mainScope"))
private val isTangemWalletFlow = MutableStateFlow(false)
private var isTangemWalletFlowJob: Job? = null
private var isTangemWalletFlowSync = false
private val webViewUriBuilder: WebViewUriBuilder by lazy {
WebViewUriBuilder(
authCredentialsProvider = dependencyProvider.getWebViewAuthCredentialsProvider(),
userCountryCodeProvider = dependencyProvider.getUserCountryCodeProvider(),
localeLanguageProvider = dependencyProvider.getLocaleProvider(),
promoCodeProvider = { repository.getUserData().promoCode },
)
}
@ -47,28 +67,27 @@ internal class DefaultLearn2earnInteractor(
initPromotionInfo()
}
override fun getIsTangemWalletFlow(): Flow<Boolean> = isTangemWalletFlow
override fun isUserHadPromoCode(): Boolean {
return repository.getUserData().promoCode != null
}
override fun isNeedToShowViewOnStoriesScreen(): Boolean {
return promotionIsActive()
}
override suspend fun isNeedToShowViewOnMainScreen(): Boolean {
if (!promotionIsActive()) return false
override suspend fun validateUserWallet(): Result<Unit> {
val promoCode = repository.getUserData().promoCode
val userWalletId = userWalletManager.getWalletId()
return if (promoCode == null) {
val response = repository.validate(userWalletId)
response.valid == true
val domainError = if (promoCode == null) {
repository.validate(userWalletId).error
} else {
val response = repository.validateCode(userWalletId, promoCode)
when (val error = response.error?.toDomainError()) {
null -> response.valid == true
else -> error !is PromotionError.CodeWasNotAppliedInShop
}
repository.validateCode(userWalletId, promoCode).error
}?.toDomainError()
return if (domainError == null) {
Result.success(Unit)
} else {
handlePromotionError(domainError)
Result.failure(domainError)
}
}
@ -76,11 +95,12 @@ internal class DefaultLearn2earnInteractor(
return repository.getUserData().isRegisteredInPromotion
}
override fun getAwardAmount(): Int {
override fun getAwardAmount(): Int = try {
val promoCode = repository.getUserData().promoCode
val awardAmount = promotion.getPromotionInfo().getData(promoCode).award.toInt()
return awardAmount
promotion.getPromotionInfo().getData(promoCode).award.toInt()
} catch (ex: NullPointerException) {
Timber.e(ex)
0
}
override fun getAwardNetworkName(): String {
@ -96,22 +116,21 @@ internal class DefaultLearn2earnInteractor(
val walletId = userWalletManager.getWalletId()
val promoCode = repository.getUserData().promoCode
val error = if (promoCode == null) {
val domainError = if (promoCode == null) {
requestAward(walletId, awardCurrency)
} else {
requestAwardWithPromoCode(walletId, awardCurrency, promoCode)
}
return if (error == null) {
return if (domainError == null) {
Result.success(Unit)
} else {
val domainError = error.toDomainError()
handlePromotionError(domainError)
Result.failure(domainError)
}
}
private suspend fun requestAward(walletId: String, awardCurrency: Currency): AbstractPromotionResponse.Error? {
private suspend fun requestAward(walletId: String, awardCurrency: Currency): PromotionError? {
val validateResponse = repository.validate(walletId)
return if (validateResponse.valid == true) {
val address = getWalletAddressForAward(awardCurrency)
@ -124,14 +143,14 @@ internal class DefaultLearn2earnInteractor(
}
} else {
validateResponse.error
}
}?.toDomainError()
}
private suspend fun requestAwardWithPromoCode(
walletId: String,
awardCurrency: Currency,
promoCode: String,
): AbstractPromotionResponse.Error? {
): PromotionError? {
val codeValidateResponse = repository.validateCode(walletId, promoCode)
return if (codeValidateResponse.valid == true) {
val address = getWalletAddressForAward(awardCurrency)
@ -144,7 +163,7 @@ internal class DefaultLearn2earnInteractor(
}
} else {
codeValidateResponse.error
}
}?.toDomainError()
}
private suspend fun getWalletAddressForAward(currency: Currency): String {
@ -180,11 +199,11 @@ internal class DefaultLearn2earnInteractor(
}
override fun buildUriForNewUser(): Uri {
return webViewUriBuilder.buildUriForNewUser()
return webViewUriBuilder.buildUriForNewUser(repository.getUserData().isLearningStageFinished)
}
override fun buildUriForOldUser(): Uri {
return webViewUriBuilder.buildUriForOldUser()
return webViewUriBuilder.buildUriForOldUser(repository.getUserData().isLearningStageFinished)
}
override fun getBasicAuthHeaders(): ArrayList<String> {
@ -194,11 +213,22 @@ internal class DefaultLearn2earnInteractor(
override fun handleRedirect(uri: Uri): WebViewAction {
val result = webViewUriParser.parse(uri)
when (result) {
is WebViewResult.PromoCode -> {
is WebViewResult.NewUserLearningFinished -> {
analytics.send(PromoScreen.SuccessScreenOpened(AnalyticsParam.ClientType.New()))
updateUserData {
it.copy(
promoCode = result.promoCode,
isRegisteredInPromotion = true,
isLearningStageFinished = true,
)
}
}
is WebViewResult.OldUserLearningFinished -> {
analytics.send(PromoScreen.SuccessScreenOpened(AnalyticsParam.ClientType.Old()))
updateUserData {
it.copy(
promoCode = null,
isLearningStageFinished = true,
)
}
}
@ -206,7 +236,7 @@ internal class DefaultLearn2earnInteractor(
updateUserData { it.copy(isRegisteredInPromotion = true) }
}
is WebViewResult.Learn2earnAnalyticsEvent -> {
analyticsEventHandler.send(result.event)
analytics.send(result.event)
}
WebViewResult.Empty -> Unit
}
@ -215,21 +245,35 @@ internal class DefaultLearn2earnInteractor(
return result.toWebViewAction()
}
private fun promotionIsActive(): Boolean {
val userData = repository.getUserData()
override fun isPromotionActive(): Boolean {
subscribeToCardTypeResolverFlow()
val isActive = when {
demoModeDatasource.isDemoModeActive -> false
!featureToggleManager.isLearn2earnEnabled -> false
userData.isAlreadyReceivedAward -> false
promotion.isError() -> false
else -> {
val data = promotion.getPromotionInfo().getData(userData.promoCode)
data.status == PromotionInfoResponse.Status.ACTIVE
}
repository.getUserData().isAlreadyReceivedAward -> false
else -> !promotion.isError()
}
return isActive
}
override fun isPromotionActiveOnStories(): Boolean {
return if (isPromotionActive()) {
promotion.getPromotionInfo().newCard.status == PromotionInfoResponse.Status.ACTIVE
} else {
false
}
}
override fun isPromotionActiveOnMain(): Boolean {
return when {
!isPromotionActive() -> false
!isTangemWalletFlowSync -> false
else -> promotion.getPromotionInfo().oldCard.status == PromotionInfoResponse.Status.ACTIVE
}
}
private suspend fun initPromotionInfo() {
promotion = if (featureToggleManager.isLearn2earnEnabled) {
repository.getPromotionInfo()
@ -287,20 +331,33 @@ internal class DefaultLearn2earnInteractor(
}
}
private fun subscribeToCardTypeResolverFlow() {
if (isTangemWalletFlowJob == null || isTangemWalletFlowJob?.isCancelled == true) {
isTangemWalletFlowJob = dependencyProvider.getCardTypeResolverFlow()
.onEach {
isTangemWalletFlowSync = it?.isTangemWallet() ?: false
isTangemWalletFlow.emit(isTangemWalletFlowSync)
}
.launchIn(scope)
}
}
private fun Promotion.PromotionInfo.getData(promoCode: String?): PromotionInfoResponse.Data {
return if (promoCode == null) {
newCard
} else {
oldCard
} else {
newCard
}
}
private fun WebViewResult.toWebViewAction(): WebViewAction {
return when (this) {
WebViewResult.Empty -> WebViewAction.PROCEED
is WebViewResult.NewUserLearningFinished,
WebViewResult.OldUserLearningFinished,
is WebViewResult.Learn2earnAnalyticsEvent,
-> WebViewAction.NOTHING
WebViewResult.ReadyForAward -> WebViewAction.FINISH_SESSION
is WebViewResult.Learn2earnAnalyticsEvent -> WebViewAction.NOTHING
is WebViewResult.PromoCode -> WebViewAction.NOTHING
WebViewResult.Empty -> WebViewAction.PROCEED
}
}
}

View file

@ -8,12 +8,12 @@ import com.tangem.feature.learn2earn.impl.BuildConfig
*/
internal class WebViewUriBuilder(
private val authCredentialsProvider: () -> String?,
private val userCountryCodeProvider: () -> String,
private val localeLanguageProvider: () -> String,
private val promoCodeProvider: () -> String?,
) {
fun buildUriForNewUser(): Uri {
val builder = makeWebViewUriBuilder()
fun buildUriForNewUser(learningIsFinished: Boolean): Uri {
val builder = makeWebViewUriBuilder(learningIsFinished)
.appendQueryParameter("type", QUERY_NEW_CARD)
promoCodeProvider.invoke()?.let {
@ -23,9 +23,9 @@ internal class WebViewUriBuilder(
return builder.build()
}
fun buildUriForOldUser(): Uri {
val builder = makeWebViewUriBuilder()
.appendQueryParameter("type", QUERY_EXISTED_CARD)
fun buildUriForOldUser(learningIsFinished: Boolean): Uri {
val builder = makeWebViewUriBuilder(learningIsFinished)
.appendQueryParameter("type", QUERY_EXISTING_CARD)
return builder.build()
}
@ -39,15 +39,25 @@ internal class WebViewUriBuilder(
}
}
private fun makeWebViewUriBuilder(): Uri.Builder = Uri.Builder().apply {
private fun makeWebViewUriBuilder(learningIsFinished: Boolean): Uri.Builder = Uri.Builder().apply {
scheme(SCHEME)
if (BuildConfig.DEBUG) {
authority(DEV_BASE_URL)
} else {
authority(BASE_URL)
}
appendPath(userCountryCodeProvider.invoke())
appendPath(getLocaleLanguage(localeLanguageProvider.invoke()))
appendPath(PATH_PROMOTION)
appendQueryParameter(QUERY_FINISHED, learningIsFinished.toString())
}
// TODO: locale: This can be used by another feature. Move it to the appropriate location
private fun getLocaleLanguage(language: String): String {
return if (LOCALE_LANG_RU.equals(language, true) || LOCALE_LANG_BY.equals(language, true)) {
LOCALE_LANG_RU
} else {
LOCALE_LANG_EN
}
}
private companion object {
@ -57,8 +67,13 @@ internal class WebViewUriBuilder(
const val PATH_PROMOTION = "promotion"
const val QUERY_NEW_CARD = "new-card"
const val QUERY_EXISTED_CARD = "existed-card"
const val QUERY_EXISTING_CARD = "existing-card"
const val QUERY_FINISHED = "finished"
const val DEV_BASE_URL = "devweb.tangem.com"
const val LOCALE_LANG_RU = "ru"
const val LOCALE_LANG_BY = "by"
const val LOCALE_LANG_EN = "en"
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.feature.learn2earn.domain
import android.net.Uri
import com.tangem.feature.learn2earn.analytics.AnalyticsParam
import com.tangem.feature.learn2earn.analytics.Learn2earnEvents
import com.tangem.feature.learn2earn.analytics.Learn2earnEvents.PromoScreen
import com.tangem.feature.learn2earn.domain.api.WebViewResult
@ -22,14 +21,17 @@ internal class WebViewUriParser(
WebViewResult.Learn2earnAnalyticsEvent(event)
}
}
isPromoCodeRedirect(uri) -> {
isSuccessNewUserRedirect(uri) -> {
val promoCode = extractPromoCode(uri)
if (promoCode == null) {
WebViewResult.Empty
} else {
WebViewResult.PromoCode(promoCode)
WebViewResult.NewUserLearningFinished(promoCode)
}
}
isSuccessOldUserRedirect(uri) -> {
WebViewResult.OldUserLearningFinished
}
isReadyForAwardRedirect(uri) -> {
WebViewResult.ReadyForAward
}
@ -40,13 +42,18 @@ internal class WebViewUriParser(
return uri.lastPathSegment == PATH_READY_FOR_AWARD
}
private fun isPromoCodeRedirect(uri: Uri): Boolean {
return uri.lastPathSegment == PATH_PROMO_CODE_CREATED &&
private fun isSuccessNewUserRedirect(uri: Uri): Boolean {
return uri.lastPathSegment == PATH_LEARNING_SUCCESS &&
uri.queryParameterNames.contains(QUERY_PROMO_CODE)
}
private fun isSuccessOldUserRedirect(uri: Uri): Boolean {
return uri.lastPathSegment == PATH_LEARNING_SUCCESS &&
!uri.queryParameterNames.contains(QUERY_PROMO_CODE)
}
private fun extractPromoCode(uri: Uri): String? {
return if (isPromoCodeRedirect(uri)) {
return if (isSuccessNewUserRedirect(uri)) {
uri.getQueryParameter(QUERY_PROMO_CODE)
} else {
null
@ -68,16 +75,14 @@ internal class WebViewUriParser(
val analyticsEvent = when (event) {
EVENT_PROMO_BUY -> PromoScreen.ButtonBuy()
EVENT_PROMO_SUCCESS_NEW_USER -> PromoScreen.SuccessScreenOpened(AnalyticsParam.ClientType.New())
EVENT_PROMO_SUCCESS_OLD_USER -> PromoScreen.SuccessScreenOpened(AnalyticsParam.ClientType.Old())
else -> null
}
return analyticsEvent
}
private companion object {
const val PATH_PROMO_CODE_CREATED = "code-created"
const val PATH_READY_FOR_AWARD = "ready-for-existed-card-award"
const val PATH_LEARNING_SUCCESS = "success"
const val PATH_READY_FOR_AWARD = "ready-for-existing-card-award"
const val PATH_ANALYTICS = "analytics"
const val QUERY_PROMO_CODE = "code"
@ -85,7 +90,5 @@ internal class WebViewUriParser(
const val QUERY_PROGRAM_NAME = "programName"
const val EVENT_PROMO_BUY = "promotion-buy"
const val EVENT_PROMO_SUCCESS_NEW_USER = "promotion-success-new-user"
const val EVENT_PROMO_SUCCESS_OLD_USER = "promotion-success-old-user"
}
}

View file

@ -1,5 +1,8 @@
package com.tangem.feature.learn2earn.domain.api
import com.tangem.domain.common.CardTypesResolver
import kotlinx.coroutines.flow.Flow
/**
* Interface, a wrapper that allows us to get values from the AppStateHolder located in the app module
*
@ -7,7 +10,9 @@ package com.tangem.feature.learn2earn.domain.api
*/
interface Learn2earnDependencyProvider {
fun getUserCountryCodeProvider(): () -> String
fun getCardTypeResolverFlow(): Flow<CardTypesResolver?>
fun getLocaleProvider(): () -> String
fun getWebViewAuthCredentialsProvider(): () -> String?
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.learn2earn.domain.api
import android.net.Uri
import kotlinx.coroutines.flow.Flow
/**
[REDACTED_AUTHOR]
@ -11,11 +12,17 @@ interface Learn2earnInteractor : WebViewRedirectHandler {
suspend fun init()
fun getIsTangemWalletFlow(): Flow<Boolean>
fun isUserHadPromoCode(): Boolean
fun isNeedToShowViewOnStoriesScreen(): Boolean
fun isPromotionActive(): Boolean
suspend fun isNeedToShowViewOnMainScreen(): Boolean
fun isPromotionActiveOnStories(): Boolean
fun isPromotionActiveOnMain(): Boolean
suspend fun validateUserWallet(): Result<Unit>
fun isUserRegisteredInPromotion(): Boolean

View file

@ -12,8 +12,14 @@ interface WebViewResultHandler {
}
sealed class WebViewResult {
object Empty : WebViewResult()
data class PromoCode(val promoCode: String) : WebViewResult()
data class NewUserLearningFinished(val promoCode: String?) : WebViewResult()
object OldUserLearningFinished : WebViewResult()
object ReadyForAward : WebViewResult()
data class Learn2earnAnalyticsEvent(val event: AnalyticsEvent) : WebViewResult()
}

View file

@ -3,6 +3,7 @@ package com.tangem.feature.learn2earn.domain.di
import android.content.Context
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.datasource.demo.DemoModeDatasource
import com.tangem.feature.learn2earn.data.api.Learn2earnRepository
import com.tangem.feature.learn2earn.data.toggles.DefaultLearn2earnFeatureToggleManager
import com.tangem.feature.learn2earn.data.toggles.Learn2earnFeatureToggleManager
@ -13,6 +14,7 @@ import com.tangem.feature.learn2earn.domain.api.WebViewRedirectHandler
import com.tangem.feature.learn2earn.presentation.Learn2earnRouter
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -45,14 +47,18 @@ internal class Learn2earnDomainModule {
userWalletManager: UserWalletManager,
analyticsEventHandler: AnalyticsEventHandler,
derivationManager: DerivationManager,
demoModeDatasource: DemoModeDatasource,
dispatchers: AppCoroutineDispatcherProvider,
): Learn2earnInteractor {
return DefaultLearn2earnInteractor(
featureToggleManager = featureToggleManager,
repository = repository,
userWalletManager = userWalletManager,
derivationManager = derivationManager,
analyticsEventHandler = analyticsEventHandler,
analytics = analyticsEventHandler,
demoModeDatasource = demoModeDatasource,
dependencyProvider = dependencyProvider,
dispatchers = dispatchers,
)
}

View file

@ -6,18 +6,21 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.extensions.TextReference
import com.tangem.feature.learn2earn.analytics.AnalyticsParam
import com.tangem.feature.learn2earn.analytics.Learn2earnEvents
import com.tangem.feature.learn2earn.domain.api.Learn2earnInteractor
import com.tangem.feature.learn2earn.domain.api.WebViewResult
import com.tangem.feature.learn2earn.domain.api.WebViewResultHandler
import com.tangem.feature.learn2earn.domain.models.PromotionError
import com.tangem.feature.learn2earn.impl.R
import com.tangem.feature.learn2earn.presentation.ui.state.*
import com.tangem.lib.crypto.models.errors.UserCancelledException
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
/**
@ -42,7 +45,25 @@ class Learn2earnViewModel @Inject constructor(
private set
init {
uiState = uiState.updateStoriesVisibility(interactor.isNeedToShowViewOnStoriesScreen())
subscribeToWalletChanges()
uiState = uiState
.updateStoriesVisibility(isVisible = interactor.isPromotionActiveOnStories())
.updateGetBonusVisibility(isVisible = interactor.isPromotionActiveOnMain())
}
private fun subscribeToWalletChanges() {
viewModelScope.launch(dispatchers.io) {
interactor.getIsTangemWalletFlow()
.collect { isTangemWallet ->
updateUi {
uiState
.updateGetBonusVisibility(
isVisible = isTangemWallet && interactor.isPromotionActiveOnMain(),
)
.changeGetBonusDescription(getBonusDescription())
}
}
}
}
fun onMainScreenCreated() {
@ -54,15 +75,63 @@ class Learn2earnViewModel @Inject constructor(
}
private fun updateMainScreenViews() {
if (!interactor.isPromotionActiveOnMain()) {
uiState = uiState.updateGetBonusVisibility(isVisible = false)
return
}
viewModelScope.launch(dispatchers.io) {
if (interactor.isNeedToShowViewOnMainScreen()) {
updateUi {
uiState.changeGetBounsDescription(getBonusDescription())
.updateGetBonusVisibility(isVisible = true)
runCatching { interactor.validateUserWallet() }
.onSuccess { result ->
result.fold(
onSuccess = {
updateUi {
uiState
.updateStoriesVisibility(isVisible = interactor.isPromotionActiveOnStories())
.updateGetBonusVisibility(isVisible = interactor.isPromotionActiveOnMain())
.changeGetBonusDescription(getBonusDescription())
}
},
onFailure = {
if (interactor.isPromotionActive()) {
val error = it as? PromotionError ?: return@launch
updateViewsVisibilityOnError(error)
} else {
updateUi { uiState.updateViewsVisibility(isVisible = false) }
}
},
)
}
} else {
.onFailure {
Timber.e(it)
updateUi { uiState.updateGetBonusVisibility(isVisible = false) }
}
}
}
private suspend fun updateViewsVisibilityOnError(error: PromotionError) {
when (error) {
is PromotionError.ProgramNotFound,
is PromotionError.ProgramWasEnd,
is PromotionError.CodeWasAlreadyUsed,
is PromotionError.WalletAlreadyHasAward,
is PromotionError.CardAlreadyHasAward,
-> {
updateUi { uiState.updateViewsVisibility(isVisible = false) }
}
is PromotionError.CodeWasNotAppliedInShop -> {
updateUi { uiState.updateGetBonusVisibility(isVisible = false) }
}
is PromotionError.CodeNotFound,
-> {
updateUi {
uiState.updateViewsVisibility(isVisible = true)
.changeGetBonusDescription(getBonusDescription())
}
}
is PromotionError.UnknownError,
PromotionError.NetworkUnreachable,
-> Unit
}
}
@ -72,14 +141,17 @@ class Learn2earnViewModel @Inject constructor(
}
private fun onButtonMainClick() {
if (!interactor.isUserHadPromoCode() && !interactor.isUserRegisteredInPromotion()) {
if (interactor.isUserHadPromoCode() || interactor.isUserRegisteredInPromotion()) {
analytics.send(Learn2earnEvents.MainScreen.NoticeLear2earn(AnalyticsParam.ClientType.Old()))
requestAward()
} else {
analytics.send(Learn2earnEvents.MainScreen.NoticeLear2earn(AnalyticsParam.ClientType.New()))
subscribeToWebViewResultEvents()
router.openWebView(interactor.buildUriForOldUser(), interactor.getBasicAuthHeaders())
return
}
}
analytics.send(Learn2earnEvents.MainScreen.NoticeLear2earn(AnalyticsParam.ClientType.Old()))
private fun requestAward() {
viewModelScope.launch(dispatchers.io) {
updateUi { uiState.updateProgress(showProgress = true) }
@ -89,7 +161,7 @@ class Learn2earnViewModel @Inject constructor(
onSuccess = {
analytics.send(Learn2earnEvents.MainScreen.NoticeClaimSuccess())
val onHideDialog = {
uiState = uiState.updateGetBonusVisibility(isVisible = false)
uiState = uiState.updateViewsVisibility(isVisible = false)
.hideDialog()
}
val successDialog = MainScreenState.Dialog.Claimed(
@ -152,7 +224,7 @@ class Learn2earnViewModel @Inject constructor(
.updateGetBonusVisibility(isVisible = false)
}
MainScreenState.Dialog.Error(
error = error,
textReference = TextReference.Str(error.description),
onOk = onHideDialog,
onDismissRequest = onHideDialog,
)
@ -160,7 +232,7 @@ class Learn2earnViewModel @Inject constructor(
is PromotionError.UnknownError, PromotionError.NetworkUnreachable -> {
val onHideDialog = { uiState = uiState.hideDialog() }
MainScreenState.Dialog.Error(
error = error,
textReference = TextReference.Res(R.string.common_server_unavailable),
onOk = onHideDialog,
onDismissRequest = onHideDialog,
)
@ -171,9 +243,19 @@ class Learn2earnViewModel @Inject constructor(
private fun subscribeToWebViewResultEvents() {
interactor.webViewResultHandler = object : WebViewResultHandler {
override fun handleResult(result: WebViewResult) {
interactor.webViewResultHandler = null
if (result is WebViewResult.ReadyForAward) {
updateMainScreenViews()
when (result) {
is WebViewResult.NewUserLearningFinished -> {
interactor.webViewResultHandler = null
}
WebViewResult.ReadyForAward -> {
interactor.webViewResultHandler = null
updateMainScreenViews()
requestAward()
}
is WebViewResult.OldUserLearningFinished,
is WebViewResult.Learn2earnAnalyticsEvent,
WebViewResult.Empty,
-> Unit
}
}
}

View file

@ -77,7 +77,7 @@ internal fun GetBonusView(state: MainScreenState, modifier: Modifier = Modifier)
Text(
text = state.description.subtitle.resolveReference(),
style = TangemTypography.caption,
color = TangemTheme.colors.text.secondary,
color = TangemTheme.colors.text.tertiary,
)
}
}

View file

@ -5,6 +5,7 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.TextButton
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.feature.learn2earn.impl.R
import com.tangem.feature.learn2earn.presentation.ui.state.MainScreenState
@ -72,7 +73,7 @@ private fun ErrorDialog(dialog: MainScreenState.Dialog.Error) {
Text(text = stringResource(id = R.string.common_error))
},
text = {
Text(text = dialog.error.description)
Text(text = dialog.textReference.resolveReference())
},
confirmButton = {
TextButton(

View file

@ -2,7 +2,6 @@ package com.tangem.feature.learn2earn.presentation.ui.state
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.WrappedList
import com.tangem.feature.learn2earn.domain.models.PromotionError
import com.tangem.feature.learn2earn.impl.R
/**
@ -62,7 +61,7 @@ data class MainScreenState(
) : Dialog()
data class Error(
val error: PromotionError,
val textReference: TextReference,
val onOk: () -> Unit,
val onDismissRequest: () -> Unit,
) : Dialog()

View file

@ -43,7 +43,12 @@ internal fun Learn2earnState.updateGetBonusVisibility(isVisible: Boolean): Learn
}
}
internal fun Learn2earnState.changeGetBounsDescription(description: MainScreenState.Description): Learn2earnState {
internal fun Learn2earnState.updateViewsVisibility(isVisible: Boolean): Learn2earnState {
return updateStoriesVisibility(isVisible)
.updateGetBonusVisibility(isVisible)
}
internal fun Learn2earnState.changeGetBonusDescription(description: MainScreenState.Description): Learn2earnState {
return if (mainScreenState.description == description) {
this
} else {

View file

@ -42,6 +42,7 @@ class Learn2earnWebViewActivity : AppCompatActivity() {
headers = webViewData.headers,
finishSessionHandler = { finish() },
)
webView.loadUrl(
webViewData.uri.toString(),
webViewData.headers,

View file

@ -22,17 +22,10 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.core.widget.NestedScrollView>
android:layout_marginTop="?attr/actionBarSize" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -73,8 +73,8 @@ mviCore = "1.3.1"
kotlinSerialization = "1.4.1"
arrow = "1.2.0"
reactiveNetwork = "3.0.8"
walletConnectCore = "1.17.0"
walletConnectWeb3 = "1.10.0"
walletConnectCore = "1.18.0"
walletConnectWeb3 = "1.11.0"
prettyLogger = "2.2.0"
okHttp-prettyLogging = "3.1.0"
# endregion Other libraries
@ -82,7 +82,7 @@ okHttp-prettyLogging = "3.1.0"
# region Tangem
tangemBlockchainSdk = "develop-289"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-273"
tangemCardSdk = "develop-278"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds
# endregion Tangem