Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-30 12:06:31 +04:00
parent 2105360004
commit d76fcd143c
24 changed files with 645 additions and 5 deletions

View file

@ -310,4 +310,8 @@
</intent-filter>
</service>
</application>
<queries>
<!-- Needed from Android 11 to open Google Wallet for payment with Visa card -->
<package android:name="com.google.android.apps.walletnfcrel" />
</queries>
</manifest>

View file

@ -3,6 +3,10 @@ package com.tangem.tap.data
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.VisaAuthTokens
@ -21,6 +25,7 @@ internal class DefaultTangemPayStorage @Inject constructor(
@ApplicationContext applicationContext: Context,
@NetworkMoshi moshi: Moshi,
private val dispatcherProvider: CoroutineDispatcherProvider,
private val appPreferencesStore: AppPreferencesStore,
) : TangemPayStorage {
private val secureStorage by lazy {
@ -75,6 +80,20 @@ internal class DefaultTangemPayStorage @Inject constructor(
secureStorage.get(createOrderIdKey(customerWalletAddress))?.decodeToString(throwOnInvalidSequence = true)
}
override suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean {
return withContext(dispatcherProvider.io) {
appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress),
) == true
}
}
override suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean) {
withContext(dispatcherProvider.io) {
appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), isDone)
}
}
override suspend fun clearOrderId(customerWalletAddress: String) = withContext(dispatcherProvider.io) {
secureStorage.delete(createOrderIdKey(customerWalletAddress))
}
@ -84,6 +103,7 @@ internal class DefaultTangemPayStorage @Inject constructor(
secureStorage.delete(createCustomerAddressKey(userWalletId))
secureStorage.delete(createKey(customerWalletAddress))
secureStorage.delete(createOrderIdKey(customerWalletAddress))
appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false)
}
private fun createCustomerAddressKey(userWalletId: UserWalletId): String = userWalletId.stringValue

View file

@ -181,6 +181,9 @@ object PreferencesKeys {
fun getHotWalletUnlockDeadlineKey(attemptId: String) =
longPreferencesKey(name = "hotWalletUnlockDeadline_$attemptId")
fun getTangemPayAddToWalletKey(customerWalletAddress: String) =
booleanPreferencesKey("tangem_pay_add_to_wallet_done_key_$customerWalletAddress")
// endregion
}

View file

@ -18,5 +18,9 @@ interface TangemPayStorage {
suspend fun clearOrderId(customerWalletAddress: String)
suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean
suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean)
suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String)
}

View file

@ -1523,9 +1523,11 @@
<string name="yield_module_stop_earning_sheet_fee_note">Les frais de réseau seront déduits du montant que vous retirez.</string>
<string name="yield_module_supply_apr">Rendement annuel brut (APY)</string>
<string name="yield_module_token_details_earn_notification_apy">APY</string>
<string name="yield_module_token_details_earn_notification_description">Les intérêts sont cumulés automatiquement.</string>
<string name="yield_module_token_details_earn_notification_earning_on_your_balance_subtitle">Les intérêts sont cumulés automatiquement.</string>
<string name="yield_module_token_details_earn_notification_earning_on_your_balance_title">Mode de rendement</string>
<string name="yield_module_token_details_earn_notification_processing">Traitement de votre dépôt</string>
<string name="yield_module_token_details_earn_notification_title">Mode Rendement</string>
<string name="yield_module_transfer_mode_automatic">Automatique</string>
<string name="yield_module_unable_to_cover_fee_description">Déposez %1$s %2$s pour couvrir les frais de réseau liés aux transactions.</string>
<string name="yield_module_unable_to_cover_fee_title">Impossible de couvrir les frais %s</string>

View file

@ -1338,6 +1338,15 @@
<string name="tangempay_card_details_error_text">Failed to load data. Try again later.</string>
<string name="tangempay_card_details_freeze_card">Freeze Card</string>
<string name="tangempay_card_details_hide_text">Hide</string>
<string name="tangempay_card_details_open_wallet_button">Open Google Wallet</string>
<string name="tangempay_card_details_open_wallet_notification_subtitle">Set up Tangem Pay in a few taps and start paying with Google Pay.</string>
<string name="tangempay_card_details_open_wallet_notification_title">Add your card to Google Pay</string>
<string name="tangempay_card_details_open_wallet_step_1">Open Google Wallet</string>
<string name="tangempay_card_details_open_wallet_step_2">Tap “Add a card”</string>
<string name="tangempay_card_details_open_wallet_step_3">Enter the card details manually</string>
<string name="tangempay_card_details_open_wallet_step_4">Verify card using the OTP sent to your device.</string>
<string name="tangempay_card_details_open_wallet_step_5">All set! Your card is ready to use.</string>
<string name="tangempay_card_details_open_wallet_title">Add card to Google Pay</string>
<string name="tangempay_card_details_receive_error_description">Technical issues detected. Please try again later or contact support.</string>
<string name="tangempay_card_details_receive_error_title">Receive unavailable now</string>
<string name="tangempay_card_details_reveal_text">Reveal</string>

View file

@ -10,6 +10,7 @@ import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.CardDetailsRequest
import com.tangem.datasource.api.pay.models.request.SetPinRequest
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.pay.model.SetPinResult
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.model.TangemPayCardDetails
@ -24,6 +25,7 @@ internal class DefaultCardDetailsRepository @Inject constructor(
private val visaLibLoader: VisaLibLoader,
private val apiConfigsManager: ApiConfigsManager,
private val rainCryptoUtil: RainCryptoUtil,
private val storage: TangemPayStorage,
) : CardDetailsRepository {
override suspend fun getCardBalance(): Either<UniversalError, TangemPayCardBalance> {
@ -98,6 +100,18 @@ internal class DefaultCardDetailsRepository @Inject constructor(
}
}
override suspend fun isAddToWalletDone(): Either<UniversalError, Boolean> {
return requestHelper.runWithErrorLogs(TAG) {
storage.getAddToWalletDone(requestHelper.getCustomerWalletAddress())
}
}
override suspend fun setAddToWalletAsDone(): Either<UniversalError, Unit> {
return requestHelper.runWithErrorLogs(TAG) {
storage.storeAddToWalletDone(requestHelper.getCustomerWalletAddress(), isDone = true)
}
}
private suspend fun getPublicKeyBase64(): String {
val env = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.TangemPay).environment
return when (env) {

View file

@ -13,4 +13,8 @@ interface CardDetailsRepository {
suspend fun revealCardDetails(): Either<UniversalError, TangemPayCardDetails>
suspend fun setPin(pin: String): Either<UniversalError, SetPinResult>
suspend fun isAddToWalletDone(): Either<UniversalError, Boolean>
suspend fun setAddToWalletAsDone(): Either<UniversalError, Unit>
}

View file

@ -67,6 +67,10 @@ class DefaultTangemPayDetailsContainerComponent @AssistedInject constructor(
TangemPayDetailsInnerRoute.ChangePINSuccess -> TangemPayChangePinSuccessComponent(
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
)
TangemPayDetailsInnerRoute.AddToWallet -> TangemPayAddToWalletComponent(
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
params = params,
)
}
private fun onChildBack() {

View file

@ -0,0 +1,38 @@
package com.tangem.features.tangempay.components
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.tangempay.components.cardDetails.DefaultTangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.model.TangemPayAddToWalletModel
import com.tangem.features.tangempay.ui.TangemPayAddToWalletScreen
internal class TangemPayAddToWalletComponent(
private val appComponentContext: AppComponentContext,
private val params: TangemPayDetailsContainerComponent.Params,
) : AppComponentContext by appComponentContext, ComposableContentComponent {
private val model: TangemPayAddToWalletModel = getOrCreateModel()
private val cardDetailsBlockComponent = DefaultTangemPayCardDetailsBlockComponent(
appComponentContext = child("cardDetailsBlockComponent"),
params = TangemPayCardDetailsBlockComponent.Params(params = params),
)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
BackHandler(onBack = router::pop)
TangemPayAddToWalletScreen(
state = state,
cardDetailsBlockComponent = cardDetailsBlockComponent,
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.features.tangempay.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.tangempay.model.TangemPayAddToWalletModel
import com.tangem.features.tangempay.model.TangemPayCardDetailsBlockModel
import com.tangem.features.tangempay.model.TangemPayChangePinModel
import com.tangem.features.tangempay.model.TangemPayDetailsModel
@ -41,4 +42,9 @@ internal interface TangemPayModelModule {
@IntoMap
@ClassKey(TangemPayCardDetailsBlockModel::class)
fun bindTangemPayCardDetailsBlockModel(model: TangemPayCardDetailsBlockModel): Model
@Binds
@IntoMap
@ClassKey(TangemPayAddToWalletModel::class)
fun bindTangemPayAddToWalletModel(model: TangemPayAddToWalletModel): Model
}

View file

@ -0,0 +1,16 @@
package com.tangem.features.tangempay.entity
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
internal data class TangemPayAddToWalletUM(
val steps: ImmutableList<TangemPayAddToWalletStepItemUM>,
val showAddToWalletButton: Boolean,
val onBackClick: () -> Unit,
val onClickOpenWallet: () -> Unit,
)
internal data class TangemPayAddToWalletStepItemUM(
val count: Int,
val text: TextReference,
)

View file

@ -45,6 +45,7 @@ internal class TangemPayDetailsStateFactory(
),
),
),
addToWalletBlockState = null,
isBalanceHidden = false,
)
}

View file

@ -9,6 +9,7 @@ internal data class TangemPayDetailsUM(
val topBarConfig: TangemPayDetailsTopBarConfig,
val pullToRefreshConfig: PullToRefreshConfig,
val balanceBlockState: TangemPayDetailsBalanceBlockState,
val addToWalletBlockState: AddToWalletBlockState?,
val isBalanceHidden: Boolean,
)
@ -41,4 +42,9 @@ internal sealed class TangemPayDetailsBalanceBlockState {
data class Error(
override val actionButtons: ImmutableList<ActionButtonConfig>,
) : TangemPayDetailsBalanceBlockState()
}
}
internal data class AddToWalletBlockState(
val onClick: () -> Unit,
val onClickClose: () -> Unit,
)

View file

@ -0,0 +1,59 @@
package com.tangem.features.tangempay.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayAddToWalletStepItemUM
import com.tangem.features.tangempay.entity.TangemPayAddToWalletUM
import com.tangem.features.tangempay.utils.GoogleWalletUtil
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@Stable
@ModelScoped
internal class TangemPayAddToWalletModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val googleWalletUtil: GoogleWalletUtil,
) : Model() {
val uiState: StateFlow<TangemPayAddToWalletUM>
field = MutableStateFlow(getInitialState())
@Suppress("MagicNumber")
private fun getInitialState(): TangemPayAddToWalletUM {
return TangemPayAddToWalletUM(
steps = persistentListOf(
TangemPayAddToWalletStepItemUM(
count = 1,
text = resourceReference(R.string.tangempay_card_details_open_wallet_step_1),
),
TangemPayAddToWalletStepItemUM(
count = 2,
text = resourceReference(R.string.tangempay_card_details_open_wallet_step_2),
),
TangemPayAddToWalletStepItemUM(
count = 3,
text = resourceReference(R.string.tangempay_card_details_open_wallet_step_3),
),
TangemPayAddToWalletStepItemUM(
count = 4,
text = resourceReference(R.string.tangempay_card_details_open_wallet_step_4),
),
TangemPayAddToWalletStepItemUM(
count = 5,
text = resourceReference(R.string.tangempay_card_details_open_wallet_step_5),
),
),
showAddToWalletButton = googleWalletUtil.isWalletAvailable(),
onBackClick = router::pop,
onClickOpenWallet = googleWalletUtil::openWallet,
)
}
}

View file

@ -20,6 +20,7 @@ import com.tangem.features.tangempay.entity.TangemPayDetailsStateFactory
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.features.tangempay.model.listener.CardDetailsEvent
import com.tangem.features.tangempay.model.listener.CardDetailsEventListener
import com.tangem.features.tangempay.model.transformers.DetailsAddToWalletBannerTransformer
import com.tangem.features.tangempay.model.transformers.DetailsBalanceTransformer
import com.tangem.features.tangempay.model.transformers.TangemPayDetailsRefreshTransformer
import com.tangem.features.tangempay.navigation.TangemPayDetailsInnerRoute
@ -63,10 +64,12 @@ internal class TangemPayDetailsModel @Inject constructor(
private val refreshStateJobHolder = JobHolder()
private val fetchBalanceJobHolder = JobHolder()
private val addToWalletBannerJobHolder = JobHolder()
val bottomSheetNavigation: SlotNavigation<TangemPayDetailsNavigation> = SlotNavigation()
init {
fetchAddToWalletBanner()
fetchBalance()
}
@ -105,6 +108,19 @@ internal class TangemPayDetailsModel @Inject constructor(
}.saveIn(fetchBalanceJobHolder)
}
private fun fetchAddToWalletBanner() {
modelScope.launch {
val isDone = cardDetailsRepository.isAddToWalletDone().getOrNull() ?: false
uiState.update(
transformer = DetailsAddToWalletBannerTransformer(
onClickBanner = ::onClickAddToWalletBlock,
onClickCloseBanner = ::onClickCloseAddToWalletBlock,
isDone = isDone,
),
)
}.saveIn(addToWalletBannerJobHolder)
}
private fun onRefreshSwipe(refreshState: ShowRefreshState) {
modelScope.launch {
cardDetailsEventListener.send(CardDetailsEvent.Hide)
@ -114,6 +130,23 @@ internal class TangemPayDetailsModel @Inject constructor(
}.saveIn(refreshStateJobHolder)
}
private fun onClickAddToWalletBlock() {
router.push(TangemPayDetailsInnerRoute.AddToWallet)
}
private fun onClickCloseAddToWalletBlock() {
modelScope.launch {
cardDetailsRepository.setAddToWalletAsDone()
uiState.update(
transformer = DetailsAddToWalletBannerTransformer(
onClickBanner = ::onClickAddToWalletBlock,
onClickCloseBanner = ::onClickCloseAddToWalletBlock,
isDone = true,
),
)
}.saveIn(addToWalletBannerJobHolder)
}
override fun onTransactionClick(item: TangemPayTxHistoryItem) {
bottomSheetNavigation.activate(TangemPayDetailsNavigation.TransactionDetails(item))
}

View file

@ -0,0 +1,22 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.features.tangempay.entity.AddToWalletBlockState
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.utils.transformer.Transformer
internal class DetailsAddToWalletBannerTransformer(
private val onClickBanner: () -> Unit,
private val onClickCloseBanner: () -> Unit,
private val isDone: Boolean,
) : Transformer<TangemPayDetailsUM> {
override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM {
return prevState.copy(
addToWalletBlockState = if (isDone) {
null
} else {
AddToWalletBlockState(onClick = onClickBanner, onClickClose = onClickCloseBanner)
},
)
}
}

View file

@ -13,4 +13,7 @@ internal sealed class TangemPayDetailsInnerRoute : Route {
@Serializable
data object ChangePINSuccess : TangemPayDetailsInnerRoute()
@Serializable
data object AddToWallet : TangemPayDetailsInnerRoute()
}

View file

@ -0,0 +1,107 @@
package com.tangem.features.tangempay.ui
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.AddToWalletBlockState
private const val GRADIENT_START_COLOR = 0xFF252934
private const val GRADIENT_END_COLOR = 0xFF12141E
private const val GRADIENT_OFFSET_X = 0f
private const val GRADIENT_OFFSET_Y = 80F
private const val GRADIENT_RADIUS = 200F
@Composable
internal fun TangemPayAddToWalletBlock(state: AddToWalletBlockState, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(
brush = Brush.radialGradient(
colors = listOf(Color(GRADIENT_START_COLOR), Color(GRADIENT_END_COLOR)),
center = Offset(GRADIENT_OFFSET_X, GRADIENT_OFFSET_Y),
radius = GRADIENT_RADIUS,
),
)
.clickable(onClick = state.onClick),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(R.drawable.img_google_wallet_48),
contentDescription = null,
modifier = Modifier.size(36.dp).clip(CircleShape),
)
Spacer(Modifier.width(12.dp))
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.Center,
) {
Text(
text = stringResourceSafe(R.string.tangempay_card_details_open_wallet_notification_title),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.constantWhite,
)
Spacer(Modifier.height(6.dp))
Text(
text = stringResourceSafe(R.string.tangempay_card_details_open_wallet_notification_subtitle),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
Box(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.TopEnd,
) {
Image(
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.clickable(onClick = state.onClickClose)
.padding(4.dp)
.size(12.dp),
painter = painterResource(id = R.drawable.ic_close_24),
colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive),
contentDescription = null,
)
}
}
}
}
@Preview(showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewTangemPayAddToWalletBlock() {
TangemThemePreview {
TangemPayAddToWalletBlock(AddToWalletBlockState({}, {}))
}
}

View file

@ -0,0 +1,192 @@
package com.tangem.features.tangempay.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.tangempay.components.cardDetails.PreviewTangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayAddToWalletStepItemUM
import com.tangem.features.tangempay.entity.TangemPayAddToWalletUM
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun TangemPayAddToWalletScreen(
state: TangemPayAddToWalletUM,
cardDetailsBlockComponent: TangemPayCardDetailsBlockComponent,
modifier: Modifier = Modifier,
) {
val listState = rememberLazyListState()
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val cardDetailsState by cardDetailsBlockComponent.state.collectAsStateWithLifecycle()
Column(
modifier = modifier
.fillMaxSize()
.navigationBarsPadding(),
) {
AppBarWithBackButton(
modifier = Modifier.statusBarsPadding(),
iconRes = R.drawable.ic_close_24,
onBackClick = state.onBackClick,
)
LazyColumn(
modifier = Modifier
.fillMaxSize()
.weight(1f),
state = listState,
contentPadding = PaddingValues(bottom = TangemTheme.dimens.spacing16 + bottomBarHeight),
) {
item(key = TangemPayCardDetailsUM::class.java) {
TangemPayCardDetailsBlockItem(component = cardDetailsBlockComponent, state = cardDetailsState)
}
item(key = TangemPayAddToWalletUM::class.java) {
Text(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 24.dp)
.fillMaxWidth(),
text = stringResourceSafe(R.string.tangempay_card_details_open_wallet_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
}
itemsIndexed(
items = state.steps,
key = { _, step -> "${TangemPayAddToWalletStepItemUM::class.java}${step.count}" },
) { idx, step ->
StepItem(
modifier = Modifier.padding(bottom = if (idx < state.steps.lastIndex) 16.dp else 0.dp),
stepNumber = step.count,
title = step.text,
)
}
}
SecondaryButton(
modifier = Modifier
.imePadding()
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
.fillMaxWidth(),
text = resourceReference(R.string.common_got_it).resolveReference(),
onClick = state.onBackClick,
)
if (state.showAddToWalletButton) {
PrimaryButton(
modifier = Modifier
.imePadding()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(),
text = resourceReference(R.string.tangempay_card_details_open_wallet_button).resolveReference(),
onClick = state.onClickOpenWallet,
)
}
}
}
@Composable
private fun StepItem(stepNumber: Int, title: TextReference, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.background(color = Color.Transparent, shape = TangemTheme.shapes.roundedCorners8)
.padding(horizontal = 24.dp),
verticalAlignment = Alignment.Top,
) {
Box(
modifier = Modifier
.size(24.dp)
.background(color = TangemColorPalette.Azure, shape = CircleShape),
contentAlignment = Alignment.Center,
) {
Text(
text = stepNumber.toString(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary2,
)
}
Spacer(modifier = Modifier.width(12.dp))
Text(
text = title.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
}
}
@Preview(showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewTangemPayAddToWalletScreen() {
TangemThemePreview {
TangemPayAddToWalletScreen(
state = TangemPayAddToWalletUM(
steps = persistentListOf(
TangemPayAddToWalletStepItemUM(
count = 1,
text = resourceReference(R.string.tangempay_card_details_open_wallet_step_1),
),
TangemPayAddToWalletStepItemUM(
count = 2,
text = resourceReference(R.string.tangempay_card_details_open_wallet_step_2),
),
TangemPayAddToWalletStepItemUM(
count = 3,
text = resourceReference(R.string.tangempay_card_details_open_wallet_step_3),
),
TangemPayAddToWalletStepItemUM(
count = 4,
text = resourceReference(R.string.tangempay_card_details_open_wallet_step_4),
),
TangemPayAddToWalletStepItemUM(
count = 5,
text = resourceReference(R.string.tangempay_card_details_open_wallet_step_5),
),
),
showAddToWalletButton = true,
onBackClick = {},
onClickOpenWallet = {},
),
cardDetailsBlockComponent = PreviewTangemPayCardDetailsBlockComponent(
TangemPayCardDetailsUM(
number = "•••• •••• •••• 1245",
expiry = "••/••",
cvv = "•••",
onCopy = {},
onClick = {},
buttonText = TextReference.Res(R.string.tangempay_card_details_hide_text),
),
),
)
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.features.tangempay.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
@Composable
internal fun TangemPayCardDetailsBlockItem(
component: TangemPayCardDetailsBlockComponent,
state: TangemPayCardDetailsUM,
modifier: Modifier = Modifier,
) {
component.CardDetailsBlockContent(state = state, modifier = modifier)
}

View file

@ -90,10 +90,21 @@ internal fun TangemPayDetailsScreen(
)
},
)
with(cardDetailsBlockComponent) {
item(TangemPayCardDetailsUM::class.java) {
CardDetailsBlockContent(state = cardDetailsState)
}
if (state.addToWalletBlockState != null) {
item(
key = AddToWalletBlockState::class.java,
content = {
TangemPayAddToWalletBlock(
state = state.addToWalletBlockState,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing12)
.padding(horizontal = TangemTheme.dimens.spacing16),
)
},
)
}
item(TangemPayCardDetailsUM::class.java) {
TangemPayCardDetailsBlockItem(component = cardDetailsBlockComponent, state = cardDetailsState)
}
with(txHistoryComponent) { txHistoryContent(listState = listState, state = txHistoryState) }
}
@ -300,12 +311,14 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<Ta
fiatBalance = "$1234.56",
isBalanceFlickering = false,
),
addToWalletBlockState = AddToWalletBlockState({}, {}),
isBalanceHidden = false,
),
TangemPayDetailsUM(
topBarConfig = TangemPayDetailsTopBarConfig(onBackClick = {}, items = null),
pullToRefreshConfig = PullToRefreshConfig(isRefreshing = false, onRefresh = {}),
balanceBlockState = TangemPayDetailsBalanceBlockState.Loading(actionButtons = persistentListOf()),
addToWalletBlockState = AddToWalletBlockState({}, {}),
isBalanceHidden = false,
),
),

View file

@ -0,0 +1,40 @@
package com.tangem.features.tangempay.utils
import android.content.Context
import android.content.Intent
import dagger.hilt.android.qualifiers.ApplicationContext
import timber.log.Timber
import javax.inject.Inject
private const val TAG = "GoogleWalletUtil"
private const val WALLET_PACKAGE_NAME = "com.google.android.apps.walletnfcrel"
internal class GoogleWalletUtil @Inject constructor(
@ApplicationContext private val context: Context,
) {
private var walletIntent: Intent? = null
fun isWalletAvailable(): Boolean = getWalletIntent() != null
fun openWallet() {
val intent = getWalletIntent()
if (intent != null) {
context.startActivity(intent)
}
}
private fun getWalletIntent(): Intent? {
return if (walletIntent != null) {
walletIntent
} else {
try {
context.packageManager.getLaunchIntentForPackage(WALLET_PACKAGE_NAME)
?.apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
.also { walletIntent = it }
} catch (exception: Exception) {
Timber.tag(TAG).e(exception)
null
}
}
}
}

View file

@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M24,0L24,0A24,24 0,0 1,48 24L48,24A24,24 0,0 1,24 48L24,48A24,24 0,0 1,0 24L0,24A24,24 0,0 1,24 0z"
android:fillColor="#393C45"/>
<path
android:pathData="M34.092,21.69V18.629C34.092,16.861 32.652,15.42 30.884,15.42H17.116C15.348,15.436 13.907,16.877 13.907,18.629V21.69C13.907,22.001 14.153,22.247 14.464,22.247H33.536C33.847,22.247 34.092,22.001 34.092,21.69Z"
android:fillColor="#34A853"
android:fillType="evenOdd"/>
<path
android:pathData="M34.092,24.375V21.33C34.092,19.562 32.652,18.121 30.884,18.121H17.116C15.348,18.121 13.907,19.562 13.907,21.33V24.391C13.907,24.702 14.153,24.948 14.464,24.948H33.536C33.847,24.931 34.092,24.686 34.092,24.375Z"
android:fillColor="#FBBC04"
android:fillType="evenOdd"/>
<path
android:pathData="M34.092,27.077V24.015C34.092,22.247 32.652,20.807 30.884,20.807H17.116C15.348,20.807 13.907,22.247 13.907,24.015V27.077C13.907,27.388 14.153,27.633 14.464,27.633H33.536C33.847,27.617 34.092,27.371 34.092,27.077Z"
android:fillColor="#EA4335"
android:fillType="evenOdd"/>
<path
android:pathData="M26.742,25.603L13.907,22.623V29.368C13.907,31.136 15.348,32.576 17.116,32.576H30.884C32.652,32.576 34.092,31.136 34.092,29.368V22.492L31.031,24.719C29.787,25.619 28.232,25.946 26.742,25.603Z"
android:fillColor="#4285F4"
android:fillType="evenOdd"/>
</vector>