Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-03 15:43:15 +05:00
parent caf5b6fae7
commit f2dfe2f8bc
21 changed files with 326 additions and 6 deletions

View file

@ -78,6 +78,7 @@ dependencies {
implementation(projects.common)
implementation(projects.common.routing)
implementation(projects.common.google)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.navigation)

View file

@ -44,6 +44,8 @@ import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.settings.SetGooglePayAvailabilityUseCase
import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase
import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.staking.SendUnsubmittedHashesUseCase
@ -70,6 +72,7 @@ import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.showFragmentAllowingStateLoss
import com.tangem.google.GoogleServicesHelper
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.features.intentHandler.IntentProcessor
@ -83,6 +86,7 @@ import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.tap.routing.RoutingComponent
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ActivityMainBinding
@ -195,6 +199,15 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var onboardingV2FeatureToggles: OnboardingV2FeatureToggles
@Inject
lateinit var setGoogleServicesAvailabilityUseCase: SetGoogleServicesAvailabilityUseCase
@Inject
lateinit var setGooglePayAvailabilityUseCase: SetGooglePayAvailabilityUseCase
@Inject
lateinit var dispatchers: CoroutineDispatcherProvider
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
@ -244,6 +257,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
observeStateUpdates()
observePolkadotAccountHealthCheck()
sendStakingUnsubmittedHashes()
checkGoogleServicesAvailability()
if (intent != null) {
deepLinksRegistry.launch(intent)
@ -605,6 +619,22 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
}
private fun checkGoogleServicesAvailability() {
val isGoogleServicesAvailable = GoogleServicesHelper.checkGoogleServicesAvailability(this)
lifecycleScope.launch {
setGoogleServicesAvailabilityUseCase(isGoogleServicesAvailable)
if (isGoogleServicesAvailable) {
val paymentsClient = GoogleServicesHelper.createPaymentsClient(this@MainActivity)
val isGooglePayAvailable = GoogleServicesHelper.checkGooglePayAvailability(paymentsClient)
setGooglePayAvailabilityUseCase(isGooglePayAvailable)
} else {
setGooglePayAvailabilityUseCase(false)
}
}
}
companion object {
private const val APP_THEME_LOAD_TIMEOUT = 2
}

View file

@ -4,6 +4,7 @@ import com.tangem.domain.onramp.*
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -123,8 +124,14 @@ internal object OnrampDomainModule {
@Provides
@Singleton
fun provideGetOnrampQuotesUseCase(onrampRepository: OnrampRepository): GetOnrampQuotesUseCase {
return GetOnrampQuotesUseCase(onrampRepository)
fun provideGetOnrampQuotesUseCase(
settingsRepository: SettingsRepository,
onrampRepository: OnrampRepository,
): GetOnrampQuotesUseCase {
return GetOnrampQuotesUseCase(
settingsRepository = settingsRepository,
repository = onrampRepository,
)
}
@Provides

View file

@ -226,4 +226,36 @@ internal object SettingsDomainModule {
fun provideGetUserCountryCodeUseCase(settingsRepository: SettingsRepository): GetUserCountryUseCase {
return GetUserCountryUseCase(settingsRepository)
}
// region Google services availability
@Provides
@Singleton
fun provideSetGoogleServicesAvailabilityUseCase(
settingsRepository: SettingsRepository,
): SetGoogleServicesAvailabilityUseCase {
return SetGoogleServicesAvailabilityUseCase(settingsRepository)
}
@Provides
@Singleton
fun provideIsGoogleServicesAvailableUseCase(
settingsRepository: SettingsRepository,
): IsGoogleServicesAvailableUseCase {
return IsGoogleServicesAvailableUseCase(settingsRepository)
}
@Provides
@Singleton
fun provideSetGooglePayAvailabilityUseCase(
settingsRepository: SettingsRepository,
): SetGooglePayAvailabilityUseCase {
return SetGooglePayAvailabilityUseCase(settingsRepository)
}
@Provides
@Singleton
fun provideIsGooglePayAvailableUseCase(settingsRepository: SettingsRepository): IsGooglePayAvailableUseCase {
return IsGooglePayAvailableUseCase(settingsRepository)
}
// endregion
}

1
common/google/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,15 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.common.google"
}
dependencies {
implementation(deps.googlePlay.services.wallet)
}

View file

@ -0,0 +1,89 @@
package com.tangem.google
import android.content.Context
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.wallet.IsReadyToPayRequest
import com.google.android.gms.wallet.PaymentsClient
import com.google.android.gms.wallet.Wallet
import org.json.JSONArray
import org.json.JSONObject
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
object GoogleServicesHelper {
private val allowedCardNetworks = JSONArray(
listOf(
"AMEX",
"DISCOVER",
"INTERAC",
"JCB",
"MASTERCARD",
"VISA",
),
)
private val allowedCardAuthMethods = JSONArray(
listOf(
"PAN_ONLY",
"CRYPTOGRAM_3DS",
),
)
private val baseCardPaymentMethod: JSONObject = JSONObject().apply {
val parameters = JSONObject().apply {
put("allowedAuthMethods", allowedCardAuthMethods)
put("allowedCardNetworks", allowedCardNetworks)
put("billingAddressRequired", true)
put(
"billingAddressParameters",
JSONObject().apply {
put("format", "FULL")
},
)
}
put("type", "CARD")
put("parameters", parameters)
}
private val baseRequest = JSONObject().apply {
put("apiVersion", 2)
put("apiVersionMinor", 0)
}
private val availabilityRequest = baseRequest.apply {
put("allowedPaymentMethods", JSONArray().put(baseCardPaymentMethod))
}
fun createPaymentsClient(context: Context): PaymentsClient {
val walletOptions = Wallet.WalletOptions.Builder()
.build()
return Wallet.getPaymentsClient(context, walletOptions)
}
fun checkGoogleServicesAvailability(context: Context): Boolean {
val googleApiAvailability = GoogleApiAvailability.getInstance()
val status = googleApiAvailability.isGooglePlayServicesAvailable(context)
return status == ConnectionResult.SUCCESS
}
suspend fun checkGooglePayAvailability(paymentsClient: PaymentsClient): Boolean {
val request = IsReadyToPayRequest.fromJson(availabilityRequest.toString())
val task = paymentsClient.isReadyToPay(request)
return suspendCoroutine<Result<Boolean>> { continuation ->
task.addOnCompleteListener { completedTask ->
try {
val result = completedTask.getResult(ApiException::class.java)
continuation.resume(Result.success(result))
} catch (exception: ApiException) {
continuation.resume(Result.failure(exception))
}
}
}.getOrElse { false }
}
}

View file

@ -107,6 +107,10 @@ object PreferencesKeys {
val ONRAMP_TRANSACTIONS_STATUSES_KEY by lazy { stringPreferencesKey(name = "onrampTransactionsStatuses") }
val IS_GOOGLE_SERVICES_AVAILABLE_KEY by lazy { booleanPreferencesKey(name = "isGoogleServicesAvailable") }
val IS_GOOGLE_PAY_AVAILABLE_KEY by lazy { booleanPreferencesKey(name = "isGooglePayAvailable") }
// region Permission
fun getShouldShowPermission(permission: String) = booleanPreferencesKey("shouldShowPushPermission_$permission")

View file

@ -2,6 +2,7 @@ package com.tangem.data.onramp.converters
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.PaymentMethodType
import com.tangem.utils.converter.TwoWayConverter
internal class PaymentMethodConverter : TwoWayConverter<PaymentMethodDTO, OnrampPaymentMethod> {
@ -9,6 +10,7 @@ internal class PaymentMethodConverter : TwoWayConverter<PaymentMethodDTO, Onramp
id = value.id,
name = value.name,
imageUrl = value.image,
type = PaymentMethodType.getType(value.id),
)
override fun convertBack(value: OnrampPaymentMethod): PaymentMethodDTO = PaymentMethodDTO(

View file

@ -17,6 +17,7 @@ import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.Locale
@Suppress("TooManyFunctions")
internal class DefaultSettingsRepository(
private val appPreferencesStore: AppPreferencesStore,
private val appLogsStore: AppLogsStore,
@ -147,4 +148,26 @@ internal class DefaultSettingsRepository(
userCountryFlow.value = code
}
}
override suspend fun setGoogleServicesAvailability(value: Boolean) {
appPreferencesStore.store(key = PreferencesKeys.IS_GOOGLE_SERVICES_AVAILABLE_KEY, value = value)
}
override suspend fun isGoogleServicesAvailability(): Boolean {
return appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.IS_GOOGLE_SERVICES_AVAILABLE_KEY,
default = false,
)
}
override suspend fun setGooglePayAvailability(value: Boolean) {
appPreferencesStore.store(key = PreferencesKeys.IS_GOOGLE_PAY_AVAILABLE_KEY, value = value)
}
override suspend fun isGooglePayAvailability(): Boolean {
return appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.IS_GOOGLE_PAY_AVAILABLE_KEY,
default = false,
)
}
}

View file

@ -13,5 +13,6 @@ dependencies {
api(projects.domain.wallets.models)
api(projects.domain.core)
api(projects.domain.settings)
implementation(deps.kotlin.serialization)
}

View file

@ -7,4 +7,34 @@ data class OnrampPaymentMethod(
val id: String,
val name: String,
val imageUrl: String,
)
val type: PaymentMethodType,
)
enum class PaymentMethodType(val id: String?) {
GOOGLE_PAY(id = "google-pay"),
CARD(id = "card"),
OTHER(id = null),
;
fun getPriority(isGooglePayEnabled: Boolean): Int = if (isGooglePayEnabled) {
when (this) {
GOOGLE_PAY -> 0
CARD -> 1
OTHER -> 2
}
} else {
when (this) {
CARD -> 0
GOOGLE_PAY -> 1
OTHER -> 2
}
}
companion object {
fun getType(id: String): PaymentMethodType = when (id) {
GOOGLE_PAY.id -> GOOGLE_PAY
CARD.id -> CARD
else -> OTHER
}
}
}

View file

@ -5,15 +5,34 @@ import arrow.core.left
import arrow.core.right
import com.tangem.domain.onramp.model.OnrampQuote
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.math.BigDecimal
class GetOnrampQuotesUseCase(private val repository: OnrampRepository) {
class GetOnrampQuotesUseCase(
private val settingsRepository: SettingsRepository,
private val repository: OnrampRepository,
) {
operator fun invoke(): Flow<Either<Throwable, List<OnrampQuote>>> {
return repository.getQuotes()
.map<List<OnrampQuote>, Either<Throwable, List<OnrampQuote>>> { it.right() }
.map<List<OnrampQuote>, Either<Throwable, List<OnrampQuote>>> { quotes ->
val isGooglePayAvailable = settingsRepository.isGooglePayAvailability()
quotes.groupBy { it.paymentMethod.type }
.asSequence()
.sortedBy { it.key.getPriority(isGooglePayAvailable) }
.map { grouped ->
grouped.value.sortedByDescending {
(it as? OnrampQuote.Data)?.toAmount?.value ?: BigDecimal.ZERO
}
}
.toList()
.flatten()
.right()
}
.catch { emit(it.left()) }
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.domain.settings
import arrow.core.Either
import com.tangem.domain.settings.repositories.SettingsRepository
class IsGooglePayAvailableUseCase(
private val settingsRepository: SettingsRepository,
) {
suspend operator fun invoke() = Either.catch {
settingsRepository.isGooglePayAvailability()
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.domain.settings
import arrow.core.Either
import com.tangem.domain.settings.repositories.SettingsRepository
class IsGoogleServicesAvailableUseCase(
private val settingsRepository: SettingsRepository,
) {
suspend operator fun invoke() = Either.catch {
settingsRepository.isGoogleServicesAvailability()
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.domain.settings
import arrow.core.Either
import com.tangem.domain.settings.repositories.SettingsRepository
class SetGooglePayAvailabilityUseCase(
private val settingsRepository: SettingsRepository,
) {
suspend operator fun invoke(isAvailable: Boolean) = Either.catch {
settingsRepository.setGooglePayAvailability(isAvailable)
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.domain.settings
import arrow.core.Either
import com.tangem.domain.settings.repositories.SettingsRepository
class SetGoogleServicesAvailabilityUseCase(
private val settingsRepository: SettingsRepository,
) {
suspend operator fun invoke(isAvailable: Boolean) = Either.catch {
settingsRepository.setGoogleServicesAvailability(isAvailable)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.domain.settings.repositories
import com.tangem.domain.settings.usercountry.models.UserCountry
@Suppress("TooManyFunctions")
interface SettingsRepository {
suspend fun shouldShowSaveUserWalletScreen(): Boolean
@ -39,4 +40,12 @@ interface SettingsRepository {
suspend fun getUserCountryCodeSync(): UserCountry?
suspend fun fetchUserCountryCode()
suspend fun setGoogleServicesAvailability(value: Boolean)
suspend fun isGoogleServicesAvailability(): Boolean
suspend fun setGooglePayAvailability(value: Boolean)
suspend fun isGooglePayAvailability(): Boolean
}

View file

@ -42,6 +42,7 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.settings)
/** DI */
implementation(deps.hilt.android)

View file

@ -31,6 +31,7 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@Suppress("LongParameterList")
@ -122,7 +123,9 @@ internal class SelectProviderModel @Inject constructor(
}
}
private fun List<OnrampProviderWithQuote>.toProvidersListItems() = mapIndexed { index, quote ->
private fun List<OnrampProviderWithQuote>.toProvidersListItems() = sortedByDescending {
(it as? OnrampProviderWithQuote.Data)?.toAmount?.value ?: BigDecimal.ZERO
}.mapIndexed { index, quote ->
when (quote) {
is OnrampProviderWithQuote.Data -> {
val rate = quote.toAmount.value.format {

View file

@ -140,6 +140,7 @@ include(":core:pagination")
// region Common modules
include(":common:ui")
include(":common:google")
// endregion
// region Libs modules