Updated on 2026-08-14

This commit is contained in:
Tangem 2022-07-31 14:41:09 +04:00
parent 23fd02dc06
commit 8b30a5251f
85 changed files with 2195 additions and 1534 deletions

View file

@ -9,7 +9,11 @@ import com.tangem.tap.common.ui.SimpleCancelableAlertDialog
import com.tangem.tap.features.details.redux.DetailsDialog
import com.tangem.tap.features.details.redux.PrivacySetting
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.*
import com.tangem.tap.features.details.ui.walletconnect.dialogs.ApproveWcSessionDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.BnbTransactionDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.ClipboardOrScanQrDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionDialog
import com.tangem.tap.features.onboarding.AddressInfoBottomSheetDialog
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.ui.dialog.CreateWalletInterruptDialog
@ -19,7 +23,12 @@ import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.BackupInPro
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.features.wallet.ui.dialogs.*
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendBottomSheetDialog
import com.tangem.tap.features.wallet.ui.dialogs.ChooseTradeActionBottomSheetDialog
import com.tangem.tap.features.wallet.ui.dialogs.RussianCardholdersWarningBottomSheetDialog
import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog
import com.tangem.tap.features.wallet.ui.dialogs.SignedHashesWarningDialog
import com.tangem.tap.features.wallet.ui.dialogs.SimpleOkDialog
import com.tangem.tap.features.wallet.ui.wallet.CurrencySelectionDialog
import com.tangem.tap.store
import com.tangem.wallet.R
@ -143,8 +152,8 @@ class DialogManager : StoreSubscriber<GlobalState> {
RussianCardholdersWarningBottomSheetDialog(context)
is DetailsDialog.ConfirmDisablingSaving -> {
val messageRes = when (state.dialog.setting) {
PrivacySetting.SAVE_CARDS -> R.string.security_and_privacy_save_card_disable_warning
PrivacySetting.SAVE_ACCESS_CODE -> R.string.security_and_privacy_save_password_disable_warning
PrivacySetting.SAVE_CARDS -> R.string.app_settings_off_saved_wallet_alert_message
PrivacySetting.SAVE_ACCESS_CODE -> R.string.app_settings_off_saved_access_code_alert_message
}
SimpleCancelableAlertDialog.create(
titleRes = R.string.common_warning,

View file

@ -0,0 +1,71 @@
package com.tangem.tap.common.compose
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.sp
import com.tangem.wallet.R
object TangemTypography {
val body1 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto)),
fontSize = 16.0.sp,
letterSpacing = 0.5.sp,
lineHeight = 24.0.sp,
)
val body2 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto)),
fontSize = 14.0.sp,
letterSpacing = 0.25.sp,
lineHeight = 20.0.sp,
)
val button = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontSize = 14.0.sp,
letterSpacing = 0.10000000149011612.sp,
lineHeight = 20.0.sp,
)
val caption = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto)),
fontSize = 12.0.sp,
letterSpacing = 0.4000000059604645.sp,
lineHeight = 16.0.sp,
)
val headline1 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto)),
fontSize = 34.0.sp,
letterSpacing = 0.0.sp,
lineHeight = 44.0.sp,
)
val headline2 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontSize = 24.0.sp,
letterSpacing = 0.18000000715255737.sp,
lineHeight = 32.0.sp,
)
val headline3 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontSize = 20.0.sp,
letterSpacing = 0.15000000596046448.sp,
lineHeight = 24.0.sp,
)
val overline = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontSize = 10.0.sp,
letterSpacing = 1.5.sp,
lineHeight = 16.0.sp,
)
val subtitle1 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontSize = 16.0.sp,
letterSpacing = 0.15000000596046448.sp,
lineHeight = 24.0.sp,
)
val subtitle2 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontSize = 14.0.sp,
letterSpacing = 0.10000000149011612.sp,
lineHeight = 24.0.sp,
)
}

View file

@ -7,11 +7,10 @@ import androidx.fragment.app.FragmentManager
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import com.tangem.tap.features.details.ui.DetailsConfirmFragment
import com.tangem.tap.features.details.ui.DetailsFragment
import com.tangem.tap.features.details.ui.DetailsSecurityFragment
import com.tangem.tap.features.details.ui.securityprivacy.ChangeAccessCodeFragment
import com.tangem.tap.features.details.ui.securityprivacy.SecurityAndPrivacyFragment
import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment
import com.tangem.tap.features.details.ui.details.DetailsFragment
import com.tangem.tap.features.details.ui.resetcard.ResetCardFragment
import com.tangem.tap.features.details.ui.securitymode.SecurityModeFragment
import com.tangem.tap.features.details.ui.walletconnect.QrScanFragment
import com.tangem.tap.features.details.ui.walletconnect.WalletConnectSessionsFragment
import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment
@ -94,10 +93,10 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.Wallet -> WalletFragment()
AppScreen.Send -> SendFragment()
AppScreen.Details -> DetailsFragment()
AppScreen.DetailsConfirm -> DetailsConfirmFragment()
AppScreen.DetailsSecurity -> DetailsSecurityFragment()
AppScreen.SecurityAndPrivacy -> SecurityAndPrivacyFragment()
AppScreen.ChangeAccessCode -> ChangeAccessCodeFragment()
AppScreen.DetailsSecurity -> SecurityModeFragment()
AppScreen.CardSettings -> CardSettingsFragment()
AppScreen.AppSettings -> CardSettingsFragment()
AppScreen.ResetToFactory -> ResetCardFragment()
AppScreen.Disclaimer -> DisclaimerFragment()
AppScreen.AddTokens -> AddTokensFragment()
AppScreen.AddCustomToken -> AddCustomTokenFragment()

View file

@ -1,5 +1,6 @@
package com.tangem.tap.common.redux.navigation
import android.net.Uri
import androidx.fragment.app.FragmentActivity
import org.rekotlin.Action
import java.lang.ref.WeakReference
@ -15,6 +16,8 @@ sealed class NavigationAction : Action {
data class OpenUrl(val url: String) : NavigationAction()
data class OpenDocument(val url: Uri) : NavigationAction()
data class Share(val data: String) : NavigationAction()
data class ActivityCreated(val activity: WeakReference<FragmentActivity>) : NavigationAction()

View file

@ -1,5 +1,6 @@
package com.tangem.tap.common.redux.navigation
import android.content.Intent
import com.tangem.tap.common.CustomTabsManager
import com.tangem.tap.common.extensions.openFragment
import com.tangem.tap.common.extensions.popBackTo
@ -32,6 +33,11 @@ val navigationMiddleware: Middleware<AppState> = { dispatch, state ->
CustomTabsManager().openUrl(action.url, it)
}
}
is NavigationAction.OpenDocument -> {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = action.url
navState?.activity?.get()?.startActivity(intent)
}
is NavigationAction.Share -> {
navState?.activity?.get()?.let {
it.shareText(action.data)

View file

@ -16,7 +16,7 @@ enum class AppScreen {
OnboardingNote, OnboardingWallet, OnboardingTwins, OnboardingOther,
Wallet, WalletDetails,
Send,
Details, DetailsConfirm, DetailsSecurity, SecurityAndPrivacy, ChangeAccessCode,
Details, DetailsSecurity, CardSettings, AppSettings, ResetToFactory,
AddTokens, AddCustomToken,
WalletConnectSessions,
QrScan

View file

@ -19,6 +19,7 @@ import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.domain.common.ScanResponse
import com.tangem.operations.CommandResponse
import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.pins.CheckUserCodesCommand
@ -144,6 +145,12 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
initialMessage = Message(context.getString(R.string.initial_message_tap_header)))
}
suspend fun scanCard(): CompletionResult<Card> {
return runTaskAsyncReturnOnMain(
ScanTask(),
initialMessage = Message(context.getString(R.string.initial_message_tap_header)))
}
suspend fun <T : CommandResponse> runTaskAsync(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
): CompletionResult<T> =

View file

@ -6,8 +6,8 @@ import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.domain.common.TapWorkarounds
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TapWorkarounds.isTangemNote
import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.common.getTwinCardNumber
import com.tangem.domain.common.isTangemTwin
@ -15,7 +15,6 @@ import com.tangem.operations.attestation.CardVerifyAndGetInfo
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.features.wallet.redux.Artwork
val Card.remainingSignatures: Int?
get() = this.getSingleWallet()?.remainingSignatures
@ -24,7 +23,7 @@ val Card.isWalletDataSupported: Boolean
val Card.isMultiwalletAllowed: Boolean
get() {
return !isTangemTwin() && !isStart2Coin && !TapWorkarounds.isTangemNote(this)
return !isTangemTwin() && !isStart2Coin && !isTangemNote()
&& (firmwareVersion >= FirmwareVersion.MultiWalletAvailable ||
getSingleWallet()?.curve == EllipticCurve.Secp256k1)
}

View file

@ -14,10 +14,14 @@ import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toHexString
import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.domain.common.*
import com.tangem.domain.common.ProductType
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isExcluded
import com.tangem.domain.common.TapWorkarounds.isNotSupportedInThatRelease
import com.tangem.domain.common.TapWorkarounds.isTangemNote
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.common.TwinsHelper
import com.tangem.domain.common.isTangemTwins
import com.tangem.operations.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.operations.ScanTask
@ -56,7 +60,7 @@ class ScanProductTask(
}
val commandProcessor = when {
TapWorkarounds.isTangemNote(card) -> ScanNoteProcessor()
card.isTangemNote() -> ScanNoteProcessor()
card.isTangemTwins() -> ScanTwinProcessor()
else -> ScanWalletProcessor(currenciesRepository, additionalBlockchainsToDerive)
}

View file

@ -3,12 +3,8 @@ package com.tangem.tap.features.details.redux
import com.tangem.blockchain.common.Wallet
import com.tangem.common.card.Card
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TwinCardNumber
import com.tangem.operations.pins.CheckUserCodesResponse
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.domain.termsOfUse.CardTou
import com.tangem.wallet.R
import org.rekotlin.Action
sealed class DetailsAction : Action {
@ -20,31 +16,23 @@ sealed class DetailsAction : Action {
) : DetailsAction()
object ShowDisclaimer : DetailsAction()
data class ReCreateTwinsWallet(val number: TwinCardNumber) : DetailsAction()
object ReCreateTwinsWallet : DetailsAction()
sealed class ResetToFactory : DetailsAction() {
object Check : ResetToFactory()
object Proceed : ResetToFactory() {
object NotAllowedByCard : ResetToFactory(), NotificationAction {
override val messageResource = R.string.error_purge_prohibited
}
object NotEmpty : ResetToFactory(), NotificationAction {
override val messageResource = R.string.details_notification_erase_wallet_not_possible
}
}
object Confirm : ResetToFactory()
object Cancel : ResetToFactory()
object Proceed : ResetToFactory()
data class Confirm(val confirmed: Boolean) : ResetToFactory()
object Failure : ResetToFactory()
object Success : ResetToFactory()
}
object CreateBackup : DetailsAction()
object ScanCard : DetailsAction()
data class PrepareCardSettingsData(val card: Card) : DetailsAction()
object ResetCardSettingsData : DetailsAction()
sealed class ManageSecurity : DetailsAction() {
data class CheckCurrentSecurityOption(val card: Card) : ManageSecurity()
data class SetCurrentOption(val userCodes: CheckUserCodesResponse) : ManageSecurity()
object OpenSecurity : ManageSecurity()
data class SelectOption(val option: SecurityOption) : ManageSecurity()
object SaveChanges : ManageSecurity() {
@ -52,19 +40,15 @@ sealed class DetailsAction : Action {
object Failure : ManageSecurity()
}
data class ConfirmSelection(val option: SecurityOption) : ManageSecurity() {
object AlreadySet : ManageSecurity(), NotificationAction {
override val messageResource = R.string.details_notification_security_option_already_active
}
}
object ChangeAccessCode : ManageSecurity()
}
sealed class ManagePrivacy : DetailsAction() {
sealed class AppSettings : DetailsAction() {
data class SwitchPrivacySetting(val allow: Boolean, val setting: PrivacySetting) :
ManagePrivacy()
AppSettings()
data class ConfirmSwitchingSetting(val allow: Boolean, val setting: PrivacySetting) :
ManagePrivacy()
AppSettings()
}
data class ChangeAppCurrency(val fiatCurrency: FiatCurrency) : DetailsAction()

View file

@ -1,9 +1,8 @@
package com.tangem.tap.features.details.redux
import com.tangem.common.CompletionResult
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.TangemSdkError
import com.tangem.operations.pins.CheckUserCodesResponse
import com.tangem.domain.common.isTangemTwins
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.common.extensions.dispatchDialogShow
@ -36,10 +35,15 @@ class DetailsMiddleware {
when (action) {
is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action)
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action)
is DetailsAction.ManagePrivacy -> managePrivacyMiddleware.handle(action)
is DetailsAction.AppSettings -> managePrivacyMiddleware.handle(action)
is DetailsAction.ShowDisclaimer -> {
store.dispatch(DisclaimerAction.ShowAcceptedDisclaimer)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
val uri = store.state.detailsState.cardTermsOfUseUrl
if (uri !=null) {
store.dispatch(NavigationAction.OpenDocument(uri))
} else {
store.dispatch(DisclaimerAction.ShowAcceptedDisclaimer)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
}
}
is DetailsAction.ReCreateTwinsWallet -> {
val wallet =
@ -64,11 +68,23 @@ class DetailsMiddleware {
store.dispatch(
GlobalAction.Onboarding.Start(
it,
fromHomeScreen = false
)
fromHomeScreen = false,
),
)
}
}
DetailsAction.ScanCard -> {
scope.launch {
when (val result = tangemSdkManager.scanCard()) {
is CompletionResult.Success -> {
val card = result.data
store.dispatchOnMain(DetailsAction.PrepareCardSettingsData(card))
}
is CompletionResult.Failure -> {
}
}
}
}
}
next(action)
}
@ -79,36 +95,31 @@ class DetailsMiddleware {
fun handle(action: DetailsAction.ResetToFactory) {
when (action) {
is DetailsAction.ResetToFactory.Proceed -> {
when (store.state.detailsState.eraseWalletState) {
EraseWalletState.Allowed ->
store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsConfirm))
EraseWalletState.NotAllowedByCard ->
store.dispatch(DetailsAction.ResetToFactory.Proceed.NotAllowedByCard)
EraseWalletState.NotEmpty ->
store.dispatch(DetailsAction.ResetToFactory.Proceed.NotEmpty)
else -> { /* no-op */
}
val card = store.state.detailsState.cardSettingsState?.card ?: return
if (card.isTangemTwins()) {
store.dispatch(DetailsAction.ReCreateTwinsWallet)
return
}
}
is DetailsAction.ResetToFactory.Cancel -> {
store.dispatch(NavigationAction.PopBackTo())
}
is DetailsAction.ResetToFactory.Confirm -> {
val card = store.state.detailsState.scanResponse?.card ?: return
scope.launch {
val result = tangemSdkManager.resetToFactorySettings(card)
withContext(Dispatchers.Main) {
when (result) {
is CompletionResult.Success -> {
currenciesRepository.removeCurrencies(card.cardId)
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
val returnScreen =
if (card.cardId == store.state.globalState.scanResponse?.card?.cardId) {
null
} else {
AppScreen.Home
}
store.dispatch(NavigationAction.PopBackTo(returnScreen))
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
store.state.globalState.analyticsHandlers?.logCardSdkError(
error,
Analytics.ActionToLog.PurgeWallet,
card = store.state.detailsState.scanResponse?.card
card = store.state.detailsState.scanResponse?.card,
)
}
}
@ -125,54 +136,13 @@ class DetailsMiddleware {
class ManageSecurityMiddleware {
fun handle(action: DetailsAction.ManageSecurity) {
when (action) {
is DetailsAction.ManageSecurity.CheckCurrentSecurityOption -> {
if (action.card.firmwareVersion >= FirmwareVersion.IsAccessCodeStatusAvailable) {
// for a card that meets this condition, we can get these statuses from it
val simulatedResponse = CheckUserCodesResponse(
action.card.isAccessCodeSet, action.card.isPasscodeSet ?: false
)
store.dispatch(
DetailsAction.ManageSecurity.SetCurrentOption(
simulatedResponse
)
)
store.dispatch(DetailsAction.ManageSecurity.OpenSecurity)
} else {
scope.launch {
when (val response =
tangemSdkManager.checkUserCodes(action.card.cardId)) {
is CompletionResult.Success -> {
store.dispatchOnMain(
DetailsAction.ManageSecurity.SetCurrentOption(
response.data
)
)
store.dispatchOnMain(DetailsAction.ManageSecurity.OpenSecurity)
}
is CompletionResult.Failure -> {
}
}
}
}
}
is DetailsAction.ManageSecurity.OpenSecurity -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsSecurity))
}
is DetailsAction.ManageSecurity.ConfirmSelection -> {
if (action.option != store.state.detailsState.securityScreenState?.currentOption) {
if (action.option == SecurityOption.LongTap) {
store.dispatch(DetailsAction.ManageSecurity.SaveChanges)
} else {
store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsConfirm))
}
} else {
store.dispatch(DetailsAction.ManageSecurity.ConfirmSelection.AlreadySet)
}
}
is DetailsAction.ManageSecurity.SaveChanges -> {
val cardId = store.state.detailsState.scanResponse?.card?.cardId
val selectedOption =
store.state.detailsState.securityScreenState?.selectedOption
store.state.detailsState.cardSettingsState?.manageSecurityState?.selectedOption
scope.launch {
val result = when (selectedOption) {
SecurityOption.LongTap -> tangemSdkManager.setLongTap(cardId)
@ -186,9 +156,7 @@ class DetailsMiddleware {
selectedOption?.let {
store.dispatch(GlobalAction.UpdateSecurityOptions(it))
}
if (selectedOption != SecurityOption.LongTap) {
store.dispatch(NavigationAction.PopBackTo())
}
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Success)
}
is CompletionResult.Failure -> {
@ -198,9 +166,9 @@ class DetailsMiddleware {
actionToLog = Analytics.ActionToLog.ChangeSecOptions,
parameters = mapOf(
AnalyticsParam.NEW_SECURITY_OPTION to
(selectedOption?.name ?: "")
(selectedOption?.name ?: ""),
),
card = store.state.detailsState.scanResponse?.card
card = store.state.detailsState.scanResponse?.card,
)
}
store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Failure)
@ -211,6 +179,12 @@ class DetailsMiddleware {
}
}
}
is DetailsAction.ManageSecurity.ChangeAccessCode -> {
val card = store.state.detailsState.cardSettingsState?.card ?: return
scope.launch {
tangemSdkManager.setAccessCode(card.cardId)
}
}
else -> { /* no-op */
}
}
@ -218,18 +192,18 @@ class DetailsMiddleware {
}
class ManagePrivacyMiddleware {
fun handle(action: DetailsAction.ManagePrivacy) {
fun handle(action: DetailsAction.AppSettings) {
when (action) {
is DetailsAction.ManagePrivacy.ConfirmSwitchingSetting -> {
is DetailsAction.AppSettings.ConfirmSwitchingSetting -> {
// TODO()
}
is DetailsAction.ManagePrivacy.SwitchPrivacySetting -> {
is DetailsAction.AppSettings.SwitchPrivacySetting -> {
if (action.allow) {
store.dispatch(
DetailsAction.ManagePrivacy.ConfirmSwitchingSetting(
DetailsAction.AppSettings.ConfirmSwitchingSetting(
action.allow,
action.setting
)
action.setting,
),
)
} else {
store.dispatchDialogShow(DetailsDialog.ConfirmDisablingSaving(action.setting))

View file

@ -1,13 +1,12 @@
package com.tangem.tap.features.details.redux
import com.tangem.common.card.Card
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TapWorkarounds.isTangemNote
import com.tangem.domain.common.isTangemTwin
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.extensions.isWalletDataSupported
import com.tangem.tap.domain.extensions.signedHashesCount
import com.tangem.tap.features.wallet.models.hasSendableAmountsOrPendingTransactions
import com.tangem.tap.store
import org.rekotlin.Action
import java.util.*
@ -26,13 +25,17 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
is DetailsAction.PrepareScreen -> {
handlePrepareScreen(action, detailsState)
}
is DetailsAction.PrepareCardSettingsData -> {
hanldePrepareCardSettingsScreen(action.card, detailsState)
}
is DetailsAction.ResetCardSettingsData -> detailsState.copy(cardSettingsState = null)
is DetailsAction.ResetToFactory -> {
handleEraseWallet(action, detailsState)
}
is DetailsAction.ManageSecurity -> {
handleSecurityAction(action, detailsState)
}
is DetailsAction.ManagePrivacy -> {
is DetailsAction.AppSettings -> {
handlePrivacyAction(action, detailsState)
}
is DetailsAction.ChangeAppCurrency ->
@ -50,43 +53,66 @@ private fun handlePrepareScreen(
return DetailsState(
scanResponse = action.scanResponse,
wallets = action.wallets,
cardInfo = action.scanResponse.card.toCardInfo(),
cardTermsOfUseUrl = action.cardTou.getUrl(action.scanResponse.card),
createBackupAllowed = action.scanResponse.card.backupStatus == Card.BackupStatus.NoBackup,
appCurrency = store.state.globalState.appCurrency
appCurrency = store.state.globalState.appCurrency,
)
}
private fun hanldePrepareCardSettingsScreen(
card: Card, state: DetailsState,
): DetailsState {
val cardSettingsState = CardSettingsState(
cardInfo = card.toCardInfo(),
manageSecurityState = prepareSecurityOptions(card),
card = card,
resetCardAllowed = isResetToFactoryAllowedByCard(card)
)
return state.copy(cardSettingsState = cardSettingsState)
}
private fun prepareSecurityOptions(card: Card): ManageSecurityState {
val securityOption = when {
card.isAccessCodeSet -> {
SecurityOption.AccessCode
}
card.isPasscodeSet == true -> {
SecurityOption.PassCode
}
else -> {
SecurityOption.LongTap
}
}
val allowedSecurityOptions = when {
card.isStart2Coin || card.isTangemNote() -> {
EnumSet.of(SecurityOption.LongTap)
}
card.settings.isBackupAllowed -> {
EnumSet.of(securityOption)
}
else -> prepareAllowedSecurityOptions(card, securityOption)
}
return ManageSecurityState(
currentOption = securityOption,
allowedOptions = allowedSecurityOptions,
selectedOption = securityOption,
)
}
private fun isResetToFactoryAllowedByCard(card: Card): Boolean {
val notAllowedByAnyWallet = card.wallets.any { it.settings.isPermanent }
val notAllowedByCard = notAllowedByAnyWallet ||
(card.isWalletDataSupported && (!card.isTangemNote() && !card.settings.isBackupAllowed))
return !notAllowedByCard
}
private fun handleEraseWallet(
action: DetailsAction.ResetToFactory,
state: DetailsState,
): DetailsState {
return when (action) {
DetailsAction.ResetToFactory.Check -> {
val card = state.scanResponse?.card
val notAllowedByAnyWallet = card?.wallets?.any { it.settings.isPermanent } ?: false
val notAllowedByCard = notAllowedByAnyWallet ||
(card?.isWalletDataSupported == true &&
(!state.scanResponse.isTangemNote() && !state.scanResponse.supportsBackup()))
val notEmpty = state.wallets.any { it.hasSendableAmountsOrPendingTransactions() }
val eraseWalletState = when {
notAllowedByCard -> EraseWalletState.NotAllowedByCard
notEmpty -> EraseWalletState.NotEmpty
else -> EraseWalletState.Allowed
}
state.copy(eraseWalletState = eraseWalletState)
}
DetailsAction.ResetToFactory.Proceed -> {
if (state.eraseWalletState == EraseWalletState.Allowed) {
state.copy(confirmScreenState = ConfirmScreenState.EraseWallet)
} else {
state
}
}
DetailsAction.ResetToFactory.Cancel -> state.copy(eraseWalletState = null)
DetailsAction.ResetToFactory.Failure -> state.copy(eraseWalletState = null)
DetailsAction.ResetToFactory.Success -> state.copy(eraseWalletState = null)
is DetailsAction.ResetToFactory.Confirm ->
state.copy(cardSettingsState = state.cardSettingsState?.copy(resetConfirmed = action.confirmed))
else -> state
}
}
@ -95,62 +121,27 @@ private fun handleSecurityAction(
action: DetailsAction.ManageSecurity, state: DetailsState,
): DetailsState {
return when (action) {
is DetailsAction.ManageSecurity.SetCurrentOption -> {
val securityOption = when {
action.userCodes.isAccessCodeSet -> {
SecurityOption.AccessCode
}
action.userCodes.isPasscodeSet -> {
SecurityOption.PassCode
}
else -> {
SecurityOption.LongTap
}
}
state.copy(securityScreenState = SecurityScreenState(currentOption = securityOption))
}
is DetailsAction.ManageSecurity.OpenSecurity -> {
val allowedSecurityOptions = when {
state.scanResponse?.card?.isStart2Coin == true ||
state.scanResponse?.isTangemNote() == true -> {
EnumSet.of(SecurityOption.LongTap)
}
state.scanResponse?.supportsBackup() == true -> {
EnumSet.of(state.securityScreenState?.currentOption)
}
else -> prepareAllowedSecurityOptions(
state.scanResponse?.card, state.securityScreenState?.currentOption
)
}
state.copy(securityScreenState = state.securityScreenState?.copy(
allowedOptions = allowedSecurityOptions,
selectedOption = state.securityScreenState.currentOption
))
}
is DetailsAction.ManageSecurity.SelectOption -> {
state.copy(securityScreenState = state.securityScreenState?.copy(
selectedOption = action.option
))
}
is DetailsAction.ManageSecurity.ConfirmSelection -> {
val confirmScreenState = when (action.option) {
SecurityOption.LongTap -> ConfirmScreenState.LongTap
SecurityOption.PassCode -> ConfirmScreenState.PassCode
SecurityOption.AccessCode -> ConfirmScreenState.AccessCode
}
state.copy(confirmScreenState = confirmScreenState)
val manageSecurityState = state.cardSettingsState?.manageSecurityState?.copy(
selectedOption = action.option,
)
state.copy(cardSettingsState = state.cardSettingsState?.copy(manageSecurityState = manageSecurityState))
}
is DetailsAction.ManageSecurity.SaveChanges.Success -> {
// Setting options to show only LongTap from now on for non-twins
val manageSecurityState = state.cardSettingsState?.manageSecurityState?.copy(
currentOption = state.cardSettingsState.manageSecurityState.selectedOption,
allowedOptions = state.scanResponse?.card?.let {
prepareAllowedSecurityOptions(
it, state.cardSettingsState.manageSecurityState.selectedOption,
)
} ?: EnumSet.of(SecurityOption.LongTap),
)
state.copy(
securityScreenState = state.securityScreenState?.copy(
currentOption = state.securityScreenState.selectedOption,
allowedOptions = state.scanResponse?.card?.let {
prepareAllowedSecurityOptions(
it, state.securityScreenState.selectedOption
)
} ?: EnumSet.of(SecurityOption.LongTap)
))
cardSettingsState = state.cardSettingsState?.copy(
manageSecurityState = manageSecurityState,
),
)
}
else -> state
@ -158,16 +149,16 @@ private fun handleSecurityAction(
}
private fun handlePrivacyAction(
action: DetailsAction.ManagePrivacy, state: DetailsState,
action: DetailsAction.AppSettings, state: DetailsState,
): DetailsState {
return when (action) {
is DetailsAction.ManagePrivacy.ConfirmSwitchingSetting -> {
is DetailsAction.AppSettings.ConfirmSwitchingSetting -> {
when (action.setting) {
PrivacySetting.SAVE_CARDS -> state.copy(saveCards = action.allow)
PrivacySetting.SAVE_ACCESS_CODE -> state.copy(savePasswords = action.allow)
}
}
is DetailsAction.ManagePrivacy.SwitchPrivacySetting -> state
is DetailsAction.AppSettings.SwitchPrivacySetting -> state
}
}
@ -188,7 +179,6 @@ private fun prepareAllowedSecurityOptions(
return allowedSecurityOptions
}
private fun Card.toCardInfo(): CardInfo {
val cardId = this.cardId.chunked(4).joinToString(separator = " ")
val issuer = this.issuer.name

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.details.redux
import android.net.Uri
import com.tangem.blockchain.common.Wallet
import com.tangem.common.card.Card
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.entities.FiatCurrency
@ -15,11 +16,9 @@ import kotlin.properties.ReadOnlyProperty
data class DetailsState(
val scanResponse: ScanResponse? = null,
val wallets: List<Wallet> = emptyList(),
val cardInfo: CardInfo? = null,
val eraseWalletState: EraseWalletState? = null,
val confirmScreenState: ConfirmScreenState? = null,
val securityScreenState: SecurityScreenState? = null,
val cardSettingsState: CardSettingsState? = null,
val cardTermsOfUseUrl: Uri? = null,
val privacyPolicyUrl: String? = null,
val createBackupAllowed: Boolean = false,
val appCurrency: FiatCurrency = FiatCurrency.Default,
val saveCards: Boolean = true,
@ -42,9 +41,15 @@ data class CardInfo(
val signedHashes: Int,
)
enum class EraseWalletState { Allowed, NotAllowedByCard, NotEmpty }
enum class ConfirmScreenState { EraseWallet, LongTap, AccessCode, PassCode }
data class SecurityScreenState(
data class CardSettingsState(
val cardInfo: CardInfo,
val card: Card,
val manageSecurityState: ManageSecurityState?,
val resetCardAllowed: Boolean,
val resetConfirmed: Boolean = false
)
data class ManageSecurityState(
val currentOption: SecurityOption = SecurityOption.LongTap,
val selectedOption: SecurityOption = currentOption,
val allowedOptions: EnumSet<SecurityOption> = EnumSet.allOf(SecurityOption::class.java),
@ -57,7 +62,7 @@ enum class SecurityOption { LongTap, PassCode, AccessCode }
sealed interface DetailsDialog : StateDialog {
data class ConfirmDisablingSaving(val setting: PrivacySetting) : DetailsDialog {
val onOk: () -> Unit =
{ store.dispatch(DetailsAction.ManagePrivacy.ConfirmSwitchingSetting(false, setting)) }
{ store.dispatch(DetailsAction.AppSettings.ConfirmSwitchingSetting(false, setting)) }
}
}

View file

@ -1,88 +0,0 @@
package com.tangem.tap.features.details.ui
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.tap.common.extensions.getDrawable
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.ConfirmScreenState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentDetailsConfirmBinding
import org.rekotlin.StoreSubscriber
class DetailsConfirmFragment : Fragment(R.layout.fragment_details_confirm),
StoreSubscriber<DetailsState> {
private val binding: FragmentDetailsConfirmBinding by viewBinding(FragmentDetailsConfirmBinding::bind)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
setState(state)
}
private fun setState(state: DetailsState) = with(binding) {
when (state.confirmScreenState) {
ConfirmScreenState.EraseWallet -> {
toolbar.title = getString(R.string.details_row_title_reset_factory_settings)
tvWarningDescription.text = getString(R.string.details_row_title_reset_factory_settings_warning)
btnConfirm.text = getString(R.string.details_row_title_reset_factory_settings)
btnConfirm.setCompoundDrawablesRelativeWithIntrinsicBounds(
null, null, getDrawable(R.drawable.ic_send), null
)
btnConfirm.setOnClickListener { store.dispatch(DetailsAction.ResetToFactory.Confirm) }
}
ConfirmScreenState.LongTap, ConfirmScreenState.AccessCode,
ConfirmScreenState.PassCode -> {
toolbar.title = getString(R.string.details_manage_security_title)
tvWarningDescription.text = getString(R.string.details_security_management_warning)
btnConfirm.text = getString(R.string.common_save_changes)
btnConfirm.setCompoundDrawablesRelativeWithIntrinsicBounds(
null, null, getDrawable(R.drawable.ic_save), null
)
btnConfirm.setOnClickListener { store.dispatch(DetailsAction.ManageSecurity.SaveChanges) }
}
}
}
}

View file

@ -1,149 +0,0 @@
package com.tangem.tap.features.details.ui
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.domain.common.getTwinCardIdForUser
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.feedback.FeedbackEmail
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.feedback.FeedbackEmail
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentDetailsBinding
import org.rekotlin.StoreSubscriber
class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<DetailsState> {
private val binding: FragmentDetailsBinding by viewBinding(FragmentDetailsBinding::bind)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
setState(state)
}
private fun setState(state: DetailsState) = with (binding) {
if (state.cardInfo != null) {
val cardId = if (state.isTangemTwins) {
state.scanResponse?.card?.getTwinCardIdForUser()
} else {
state.cardInfo.cardId
}
tvCardId.text = cardId
tvIssuer.text = state.cardInfo.issuer
tvSignedHashes.text = getString(
R.string.details_row_subtitle_signed_hashes_format,
state.cardInfo.signedHashes.toString()
)
}
tvSignedHashes.show(!state.isTangemTwins)
tvSignedHashesTitle.show(!state.isTangemTwins)
tvDisclaimer.setOnClickListener { store.dispatch(DetailsAction.ShowDisclaimer) }
tvCardTou.show(state.cardTermsOfUseUrl != null)
tvCardTou.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = state.cardTermsOfUseUrl
startActivity(intent)
}
if (state.isTangemTwins) {
tvResetToFactory.show()
tvResetToFactory.text = getText(R.string.details_row_title_twins_recreate)
tvResetToFactory.setOnClickListener {
store.dispatch(DetailsAction.ReCreateTwinsWallet(state.twinCardsState.cardNumber!!))
}
} else {
tvResetToFactory.show()
tvResetToFactory.text = getText(R.string.details_row_title_reset_factory_settings)
tvResetToFactory.setOnClickListener {
store.dispatch(DetailsAction.ResetToFactory.Check)
store.dispatch(DetailsAction.ResetToFactory.Proceed)
}
}
tvCreateBackup.show(state.createBackupAllowed)
tvCreateBackup.setOnClickListener {
store.dispatch(DetailsAction.CreateBackup)
}
tvAppCurrency.show(state.scanResponse?.card?.isMultiwalletAllowed != true)
tvAppCurrencyTitle.show(state.scanResponse?.card?.isMultiwalletAllowed != true)
tvAppCurrency.text = state.appCurrency.code
tvAppCurrency.setOnClickListener {
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
tvSendFeedback.setOnClickListener {
store.dispatch(GlobalAction.SendEmail(FeedbackEmail()))
}
tvWalletConnect.show(state.scanResponse?.card?.isMultiwalletAllowed == true)
tvWalletConnect.setOnClickListener {
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
}
tvSupport.setOnClickListener {
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
}
llManageSecurity.setOnClickListener {
store.dispatch(DetailsAction.ManageSecurity.CheckCurrentSecurityOption(state.scanResponse!!.card))
tvSecurityAndPrivacy.setOnClickListener {
store.dispatch(NavigationAction.NavigateTo(AppScreen.SecurityAndPrivacy))
}
}
}

View file

@ -1,147 +0,0 @@
package com.tangem.tap.features.details.ui
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.common.card.Card
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentDetailsSecurityBinding
import org.rekotlin.StoreSubscriber
import java.util.*
class DetailsSecurityFragment : Fragment(R.layout.fragment_details_security),
StoreSubscriber<DetailsState> {
private val binding: FragmentDetailsSecurityBinding by viewBinding(
FragmentDetailsSecurityBinding::bind
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
setOnClickListeners()
}
private fun setOnClickListeners() {
binding.btnSaveChanges.setOnClickListener {
store.state.detailsState.securityScreenState?.selectedOption?.let {
store.dispatch(DetailsAction.ManageSecurity.ConfirmSelection(it))
}
}
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
selectSecurityOption(state.securityScreenState?.selectedOption)
for (option in SecurityOption.values()) {
setupOption(
option,
state.securityScreenState?.allowedOptions
?: EnumSet.noneOf(SecurityOption::class.java)
)
}
binding.tvAccessCodeUnavailableDisclaimer.show(
state.scanResponse?.card?.backupStatus == Card.BackupStatus.NoBackup
)
}
private fun selectSecurityOption(securityOption: SecurityOption?) = with(binding) {
radiobuttonLongTap.isChecked =
securityOption == SecurityOption.LongTap && radiobuttonLongTap.isEnabled
radiobuttonPasscode.isChecked =
securityOption == SecurityOption.PassCode && radiobuttonPasscode.isEnabled
radiobuttonAccessCode.isChecked =
securityOption == SecurityOption.AccessCode && radiobuttonAccessCode.isEnabled
}
private fun setupOption(option: SecurityOption, allowedOptions: EnumSet<SecurityOption>) {
when (option) {
SecurityOption.LongTap -> { enableLongTap(allowedOptions.contains(option)) }
SecurityOption.PassCode -> { enablePasscode(allowedOptions.contains(option)) }
SecurityOption.AccessCode -> { enableAccessCode(allowedOptions.contains(option)) }
}
}
private fun enableLongTap(enable: Boolean) = with(binding) {
groupLongTap.show(enable)
val alpha = if (enable) 1f else 0.5f
tvLongTapDescription.alpha = alpha
tvLongTapTitle.alpha = alpha
radiobuttonLongTap.alpha = alpha
radiobuttonLongTap.isEnabled = enable
vLongTap.isClickable = enable
if (enable) {
vLongTap.setOnClickListener {
store.dispatch(DetailsAction.ManageSecurity.SelectOption(SecurityOption.LongTap))
}
}
}
private fun enablePasscode(enable: Boolean) = with(binding) {
groupPasscode.show(enable)
val alpha = if (enable) 1f else 0.5f
tvPasscodeDescription.alpha = alpha
tvPasscodeTitle.alpha = alpha
radiobuttonPasscode.alpha = alpha
radiobuttonPasscode.isEnabled = enable
vPasscode.isClickable = enable
if (enable) {
vPasscode.setOnClickListener {
store.dispatch(DetailsAction.ManageSecurity.SelectOption(SecurityOption.PassCode))
}
}
}
private fun enableAccessCode(enable: Boolean) = with(binding) {
groupAccessCode.show(enable)
val alpha = if (enable) 1f else 0.5f
tvAccessCodeDescription.alpha = alpha
tvAccessCodeTitle.alpha = alpha
radiobuttonAccessCode.alpha = alpha
radiobuttonAccessCode.isEnabled = enable
vAccessCode.isClickable = enable
if (enable) {
vAccessCode.setOnClickListener {
store.dispatch(DetailsAction.ManageSecurity.SelectOption(SecurityOption.AccessCode))
}
}
}
}

View file

@ -0,0 +1,3 @@
package com.tangem.tap.features.details.ui.appsettings
class AppSettingsFragment

View file

@ -0,0 +1,9 @@
package com.tangem.tap.features.details.ui.appsettings
import androidx.compose.runtime.Composable
@Composable
fun AppSettingsScreen(
) {
}

View file

@ -0,0 +1,74 @@
package com.tangem.tap.features.details.ui.cardsettings
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
class CardSettingsFragment : Fragment(), StoreSubscriber<DetailsState> {
private val viewModel = CardSettingsViewModel(store)
private var screenState: MutableState<CardSettingsScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState, context))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
AppCompatTheme {
CardSettingsScreen(
state = screenState.value,
onBackPressed = {
store.dispatch(DetailsAction.ResetCardSettingsData)
store.dispatch(NavigationAction.PopBackTo())
},
)
}
}
}
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
screenState.value = viewModel.updateState(state.cardSettingsState, context)
}
}

View file

@ -0,0 +1,175 @@
package com.tangem.tap.features.details.ui.cardsettings
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@Composable
fun CardSettingsScreen(
state: CardSettingsScreenState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
SettingsScreensScaffold(
content =
if (state.cardDetails == null) {
{ CardSettingsReadCard(state.onScanCardClick, modifier = modifier) }
} else {
{ CardSettings(state = state, modifier = modifier) }
},
titleRes = R.string.card_settings_title,
onBackClick = onBackPressed,
)
}
@Composable
fun CardSettingsReadCard(
onScanCardClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween,
) {
Box(
modifier = modifier
.fillMaxWidth(),
) {
Image(
modifier = modifier
.fillMaxWidth()
.padding(start = 80.dp, end = 80.dp, top = 70.dp)
.rotate(-15f),
painter = painterResource(id = R.drawable.card_placeholder_secondary),
contentDescription = "",
contentScale = ContentScale.FillWidth,
)
Image(
modifier = modifier
.fillMaxWidth()
.padding(start = 60.dp, end = 60.dp)
.rotate(-1f),
painter = painterResource(id = R.drawable.card_placeholder_black),
contentDescription = "",
contentScale = ContentScale.FillWidth,
)
}
Column(
modifier = modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 32.dp),
) {
Text(
text = stringResource(id = R.string.scan_card_settings_title),
color = colorResource(id = R.color.text_primary_1),
style = TangemTypography.headline3,
)
Spacer(modifier = modifier.size(20.dp))
Text(
text = stringResource(id = R.string.scan_card_settings_message),
color = colorResource(id = R.color.text_secondary),
style = TangemTypography.body1,
)
Spacer(modifier = modifier.size(29.dp))
DetailsMainButton(
title = stringResource(id = R.string.scan_card_settings_button),
onClick = onScanCardClick,
modifier = modifier,
)
}
}
}
@Composable
fun CardSettings(
state: CardSettingsScreenState,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
) {
state.cardDetails?.map {
val paddingBottom = when (it) {
is CardInfo.CardId, is CardInfo.Issuer -> 12.dp
is CardInfo.SignedHashes -> 14.dp
is CardInfo.SecurityMode -> 16.dp
is CardInfo.ChangeAccessCode -> 16.dp
is CardInfo.ResetToFactorySettings -> 28.dp
}
val paddingTop = when (it) {
is CardInfo.CardId -> 0.dp
is CardInfo.Issuer -> 12.dp
is CardInfo.SignedHashes -> 12.dp
is CardInfo.SecurityMode -> 14.dp
is CardInfo.ChangeAccessCode -> 16.dp
is CardInfo.ResetToFactorySettings -> 16.dp
}
Column(
modifier = modifier
.fillMaxWidth()
.clickable(
enabled = it.clickable,
onClick = { state.onElementClick(it) },
)
.padding(start = 20.dp, end = 20.dp, bottom = paddingBottom, top = paddingTop),
) {
val titleColor = if (it.clickable) R.color.text_primary_1 else R.color.text_tertiary
val subtitleColor = if (it.clickable) R.color.text_secondary else R.color.text_tertiary
Text(
text = stringResource(id = it.titleRes),
color = colorResource(id = titleColor),
style = TangemTypography.subtitle1,
)
Spacer(modifier = modifier.size(4.dp))
Text(
text = it.subtitle,
color = colorResource(id = subtitleColor),
style = TangemTypography.body2,
)
}
}
}
}
@Composable
@Preview
fun CardSettingsPreview() {
CardSettingsScreen(state = CardSettingsScreenState(onScanCardClick = {}) {}, {})
}

View file

@ -0,0 +1,36 @@
package com.tangem.tap.features.details.ui.cardsettings
import com.tangem.wallet.R
data class CardSettingsScreenState(
val cardDetails: List<CardInfo>? = null,
val onScanCardClick: () -> Unit,
val onElementClick: (CardInfo) -> Unit
)
sealed class CardInfo(
val titleRes: Int, val subtitle: String, val clickable: Boolean = false
) {
class CardId(subtitle: String) : CardInfo(R.string.details_row_title_cid, subtitle)
class Issuer(subtitle: String) : CardInfo(R.string.details_row_title_issuer, subtitle)
class SignedHashes(subtitle: String) : CardInfo(R.string.details_row_title_signed_hashes, subtitle)
class SecurityMode(
subtitle: String,
clickable: Boolean,
) : CardInfo(R.string.card_settings_security_mode, subtitle, clickable)
class ChangeAccessCode(subtitle: String) : CardInfo(
R.string.card_settings_change_access_code,
subtitle,
true,
)
class ResetToFactorySettings(subtitle: String) : CardInfo(
R.string.details_row_title_reset_factory_settings,
subtitle,
true,
)
}

View file

@ -0,0 +1,95 @@
package com.tangem.tap.features.details.ui.cardsettings
import android.content.Context
import com.tangem.domain.common.getTwinCardIdForUser
import com.tangem.domain.common.isTangemTwins
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.CardSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.ui.securitymode.toTitleRes
import com.tangem.wallet.R
import org.rekotlin.Store
class CardSettingsViewModel(private val store: Store<AppState>) {
fun updateState(state: CardSettingsState?, context: Context?): CardSettingsScreenState {
return if (state?.manageSecurityState == null) {
CardSettingsScreenState(
null, onElementClick = {},
onScanCardClick = {
store.dispatch(DetailsAction.ScanCard)
},
)
} else {
val cardId = if (state.card.isTangemTwins()) {
state.card.getTwinCardIdForUser()
} else {
state.cardInfo.cardId
}
val cardDetails: MutableList<CardInfo> = listOfNotNull(
CardInfo.CardId(cardId),
CardInfo.Issuer(state.cardInfo.issuer),
).toMutableList()
context?.let { context ->
if (!state.card.isTangemTwins()) {
cardDetails.add(
CardInfo.SignedHashes(
context
.getString(
R.string.details_row_subtitle_signed_hashes_format,
state.cardInfo.signedHashes.toString(),
),
),
)
}
cardDetails.add(
CardInfo.SecurityMode(
subtitle = context.getString(state.manageSecurityState.currentOption.toTitleRes()),
clickable = state.manageSecurityState.allowedOptions.size > 1,
),
)
if (state.card.backupStatus?.isActive == true && state.card.isAccessCodeSet) {
cardDetails.add(
CardInfo.ChangeAccessCode(
subtitle = context.getString(R.string.card_settings_change_access_code_footer),
),
)
}
if (state.resetCardAllowed) {
cardDetails.add(
CardInfo.ResetToFactorySettings(
subtitle = context.getString(R.string.card_settings_reset_factory_footer),
),
)
}
}
CardSettingsScreenState(
cardDetails,
onScanCardClick = { },
onElementClick = {
handleClickingItem(it)
},
)
}
}
private fun handleClickingItem(item: CardInfo) {
when (item) {
is CardInfo.ChangeAccessCode -> {
store.dispatch(DetailsAction.ManageSecurity.ChangeAccessCode)
}
is CardInfo.ResetToFactorySettings -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.ResetToFactory))
}
is CardInfo.SecurityMode -> {
store.dispatch(DetailsAction.ManageSecurity.OpenSecurity)
}
else -> {}
}
}
}

View file

@ -0,0 +1,112 @@
package com.tangem.tap.features.details.ui.common
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.wallet.R
@Composable
fun SettingsScreensScaffold(
content: @Composable () -> Unit,
background: @Composable (() -> Unit)? = null,
titleRes: Int,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
BackHandler(true, onBackClick)
Scaffold(
topBar = { EmptyTopBarWithNavigation(onBackClick = onBackClick) },
modifier = modifier.systemBarsPadding(),
backgroundColor = colorResource(id = R.color.background_primary),
) {
Box(modifier = modifier.fillMaxSize()) {
background?.invoke()
Column(
modifier = modifier.fillMaxWidth(),
) {
Text(
text = stringResource(id = titleRes),
modifier = modifier.padding(start = 20.dp, end = 20.dp, bottom = 52.dp),
style = TangemTypography.headline1,
color = colorResource(id = R.color.text_primary_1),
)
content()
}
}
}
}
@Composable
fun EmptyTopBarWithNavigation(
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
TopAppBar(
title = { },
navigationIcon =
{
IconButton(onClick = onBackClick) {
Icon(
painterResource(id = R.drawable.ic_back), "",
tint = colorResource(id = R.color.icon_primary_1),
)
}
},
backgroundColor = colorResource(id = R.color.background_primary),
elevation = 0.dp,
)
}
@Composable
fun DetailsMainButton(
title: String,
enabled: Boolean = true,
onClick: (() -> Unit),
modifier: Modifier = Modifier,
) {
Button(
onClick = onClick,
modifier = modifier
.fillMaxWidth()
.height(48.dp),
shape = RoundedCornerShape(12.dp),
enabled = enabled,
colors = ButtonDefaults.buttonColors(
backgroundColor = colorResource(R.color.button_primary),
contentColor = colorResource(R.color.text_primary_2),
disabledBackgroundColor = colorResource(R.color.button_disabled),
disabledContentColor = colorResource(R.color.text_disabled),
),
) {
Text(text = title)
Spacer(modifier = modifier.size(8.dp))
Icon(painter = painterResource(id = R.drawable.ic_tangem), contentDescription = "")
}
}

View file

@ -0,0 +1,136 @@
package com.tangem.tap.features.details.ui.details
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
class DetailsFragment : Fragment(), StoreSubscriber<DetailsState> {
private val detailsViewModel = DetailsViewModel(store)
private var detailsScreenState: MutableState<DetailsScreenState> =
mutableStateOf(detailsViewModel.updateState(store.state.detailsState))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
AppCompatTheme {
DetailsScreen(
state = detailsScreenState.value,
onBackPressed = { store.dispatch(NavigationAction.PopBackTo()) },
)
}
}
}
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
// override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// super.onViewCreated(view, savedInstanceState)
//
// binding.toolbar.setNavigationOnClickListener {
// store.dispatch(NavigationAction.PopBackTo())
// }
// }
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
detailsScreenState.value = detailsViewModel.updateState(state)
}
// private fun setState(state: DetailsState) = with(binding) {
//
// if (state.cardInfo != null) {
// val cardId = if (state.isTangemTwins) {
// state.scanResponse?.card?.getTwinCardIdForUser()
// } else {
// state.cardInfo.cardId
// }
// tvCardId.text = cardId
// tvIssuer.text = state.cardInfo.issuer
// tvSignedHashes.text = getString(
// R.string.details_row_subtitle_signed_hashes_format,
// state.cardInfo.signedHashes.toString()
// )
// }
//
// tvSignedHashes.show(!state.isTangemTwins)
// tvSignedHashesTitle.show(!state.isTangemTwins)
//
// tvDisclaimer.setOnClickListener { store.dispatch(DetailsAction.ShowDisclaimer) }
//
//
// if (state.isTangemTwins) {
// tvResetToFactory.show()
// tvResetToFactory.text = getText(R.string.details_row_title_twins_recreate)
// tvResetToFactory.setOnClickListener {
// store.dispatch(DetailsAction.ReCreateTwinsWallet(state.twinCardsState.cardNumber!!))
// }
// } else {
// tvResetToFactory.show()
// tvResetToFactory.text = getText(R.string.details_row_title_reset_factory_settings)
// tvResetToFactory.setOnClickListener {
// store.dispatch(DetailsAction.ResetToFactory.Check)
// store.dispatch(DetailsAction.ResetToFactory.Proceed)
// }
// }
//
//
// tvAppCurrency.show(state.scanResponse?.card?.isMultiwalletAllowed != true)
// tvAppCurrencyTitle.show(state.scanResponse?.card?.isMultiwalletAllowed != true)
// tvAppCurrency.text = state.appCurrency.code
//
// tvAppCurrency.setOnClickListener {
// store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
// }
//
// tvSecurityAndPrivacy.setOnClickListener {
// store.dispatch(DetailsAction.ManageSecurity.CheckCurrentSecurityOption(state.scanResponse!!.card))
// tvSecurityAndPrivacy.setOnClickListener {
// store.dispatch(NavigationAction.NavigateTo(AppScreen.SecurityAndPrivacy))
// }
//
// }
//
// }
}

View file

@ -0,0 +1,184 @@
package com.tangem.tap.features.details.ui.details
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@Composable
fun DetailsScreen(
state: DetailsScreenState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
SettingsScreensScaffold(
content = {
Content(state = state, modifier = modifier)
},
titleRes = R.string.details_title,
onBackClick = onBackPressed,
)
}
@Composable
fun Content(
state: DetailsScreenState,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(bottom = 40.dp),
) {
state.elements.map {
if (it == SettingsElement.WalletConnect) {
WalletConnectDetailsItem(
onItemsClick = state.onItemsClick, modifier = modifier
)
} else {
DetailsItem(
item = it, onItemsClick = state.onItemsClick,
modifier = modifier
)
}
}
Spacer(modifier = Modifier.weight(1f))
TangemSocialAccounts(state.tangemLinks, state.onSocialNetworkClick)
Spacer(modifier = Modifier.size(12.dp))
Text(
text = "${stringResource(id = state.appNameRes)} ${state.tangemVersion}",
style = TangemTypography.caption,
color = colorResource(id = R.color.text_tertiary),
modifier = modifier.padding(start = 16.dp, end = 16.dp),
)
}
}
@Composable
fun WalletConnectDetailsItem(
onItemsClick: (SettingsElement) -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.height(84.dp)
.clickable { onItemsClick(SettingsElement.WalletConnect) },
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(id = R.drawable.ic_walletconnect),
contentDescription = stringResource(id = R.string.wallet_connect_title),
modifier = modifier.padding(start = 20.dp, end = 20.dp),
tint = colorResource(id = R.color.all_colors_azure),
)
Column(
modifier = modifier.height(56.dp),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Center,
) {
Text(
text = stringResource(id = R.string.wallet_connect_title),
modifier = modifier.padding(end = 20.dp, bottom = 4.dp),
style = TangemTypography.headline3,
color = colorResource(id = R.color.text_primary_1),
)
Text(
text = stringResource(id = R.string.wallet_connect_subtitle),
modifier = modifier.padding(end = 20.dp, bottom = 4.dp),
style = TangemTypography.body1,
color = colorResource(id = R.color.text_secondary),
)
}
}
}
@Composable
fun DetailsItem(
item: SettingsElement,
onItemsClick: (SettingsElement) -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.height(56.dp)
.fillMaxWidth()
.clickable { onItemsClick(item) },
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(id = item.iconRes),
contentDescription = stringResource(id = item.titleRes),
modifier = modifier.padding(start = 20.dp, end = 20.dp),
tint = colorResource(id = R.color.icon_secondary),
)
Text(
text = stringResource(id = item.titleRes),
modifier = modifier.padding(end = 20.dp),
style = TangemTypography.subtitle1,
color = colorResource(id = R.color.text_primary_1),
)
}
}
@Composable
fun TangemSocialAccounts(
links: List<TangemLink>,
onSocialNetworkClick: (String) -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.padding(start = 8.dp, end = 8.dp)
) {
links.map {
Icon(
painter = painterResource(id = it.iconRes),
contentDescription = "",
modifier = modifier
.padding(8.dp)
.clickable { onSocialNetworkClick(it.url) }
,
tint = colorResource(id = R.color.icon_informative),
)
}
}
}
@Composable
@Preview
fun Preview() {
DetailsScreen(
state = DetailsScreenState(
SettingsElement.values().toList(),
TangemSocialAccounts.accountsEn,
"Tangem 2.14.12 (343)",
{}, {}
),
{},
)
}

View file

@ -0,0 +1,55 @@
package com.tangem.tap.features.details.ui.details
import com.tangem.wallet.R
data class DetailsScreenState(
val elements: List<SettingsElement>,
val tangemLinks: List<TangemLink>,
val tangemVersion: String,
val onItemsClick: (SettingsElement) -> Unit,
val onSocialNetworkClick: (String) -> Unit,
) {
val appNameRes: Int = R.string.app_name
}
enum class SettingsElement(
val iconRes: Int,
val titleRes: Int,
) {
WalletConnect(R.drawable.ic_walletconnect, R.string.wallet_connect_title),
Chat(R.drawable.ic_chat, R.string.details_ask_a_question),
SendFeedback(R.drawable.ic_comment, R.string.details_row_title_send_feedback),
CardSettings(R.drawable.ic_card_settings, R.string.card_settings_title),
AppSettings(R.drawable.ic_settings, R.string.app_settings_title),
LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup),
TermsOfService(R.drawable.ic_text, R.string.disclaimer_title),
PrivacyPolicy(R.drawable.ic_lock, R.string.details_row_privacy_policy),
;
}
data class TangemLink(
val iconRes: Int,
val url: String,
)
object TangemSocialAccounts {
val accountsEn: List<TangemLink> = listOf(
TangemLink(R.drawable.ic_telegram, "https://t.me/TangemCards"),
TangemLink(R.drawable.ic_twitter, "https://twitter.com/tangem"),
TangemLink(R.drawable.ic_facebook, "https://m.facebook.com/TangemCards/"),
TangemLink(R.drawable.ic_instagram, "https://instagram.com/tangemcards"),
TangemLink(R.drawable.ic_github, "https://github.com/tangem"),
TangemLink(R.drawable.ic_youtube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"),
TangemLink(R.drawable.ic_linkedin, "https://www.linkedin.com/company/tangem"),
)
val accountsRu: List<TangemLink> = listOf(
TangemLink(R.drawable.ic_telegram, "https://t.me/tangem_ru"),
TangemLink(R.drawable.ic_twitter, "https://twitter.com/tangem"),
TangemLink(R.drawable.ic_facebook, "https://m.facebook.com/TangemCards/"),
TangemLink(R.drawable.ic_instagram, "https://instagram.com/tangemcards"),
TangemLink(R.drawable.ic_github, "https://github.com/tangem"),
TangemLink(R.drawable.ic_youtube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"),
TangemLink(R.drawable.ic_linkedin, "https://www.linkedin.com/company/tangem"),
)
}

View file

@ -0,0 +1,91 @@
package com.tangem.tap.features.details.ui.details
import com.tangem.tap.common.feedback.FeedbackEmail
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.home.LocaleRegionProvider
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.wallet.BuildConfig
import org.rekotlin.Store
class DetailsViewModel(private val store: Store<AppState>) {
fun updateState(state: DetailsState): DetailsScreenState {
val settings = SettingsElement.values().mapNotNull {
when (it) {
SettingsElement.WalletConnect -> {
if (state.scanResponse?.card?.isMultiwalletAllowed == true) it else null
}
SettingsElement.LinkMoreCards -> {
if (state.createBackupAllowed) it else null
}
SettingsElement.PrivacyPolicy -> {
if (state.privacyPolicyUrl != null) it else null
}
else -> it
}
}
return DetailsScreenState(
settings,
tangemLinks = getSocialLinks(),
tangemVersion = getTangemAppVersion(),
onItemsClick = { handleClickingSettingsItem(it) },
onSocialNetworkClick = { handleSocialNetworkClick(it) },
)
}
private fun handleSocialNetworkClick(url: String) {
store.dispatch(NavigationAction.OpenUrl(url))
}
private fun handleClickingSettingsItem(item: SettingsElement) {
when (item) {
SettingsElement.WalletConnect -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
}
SettingsElement.Chat -> {
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
}
SettingsElement.SendFeedback -> {
store.dispatch(GlobalAction.SendEmail(FeedbackEmail()))
}
SettingsElement.CardSettings -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings))
}
SettingsElement.AppSettings -> {
// store.dispatch(NavigationAction.NavigateTo()) TODO: To be available later
}
SettingsElement.LinkMoreCards -> {
store.dispatch(DetailsAction.CreateBackup)
}
SettingsElement.TermsOfService -> {
store.dispatch(DetailsAction.ShowDisclaimer)
}
SettingsElement.PrivacyPolicy -> {
// TODO: To be available later
}
}
}
private fun getSocialLinks(): List<TangemLink> {
val locale = LocaleRegionProvider().getRegion()
return if (locale.lowercase() == RUSSIA_COUNTRY_CODE) {
TangemSocialAccounts.accountsRu
} else {
TangemSocialAccounts.accountsEn
}
}
private fun getTangemAppVersion(): String {
val versionCode: Int = BuildConfig.VERSION_CODE
val versionName: String = BuildConfig.VERSION_NAME
return "$versionName ($versionCode)"
}
}

View file

@ -0,0 +1,70 @@
package com.tangem.tap.features.details.ui.resetcard
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
class ResetCardFragment : Fragment(), StoreSubscriber<DetailsState> {
private val viewModel = ResetCardViewModel(store)
private var screenState: MutableState<ResetCardScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState, context))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
AppCompatTheme {
ResetCardScreen(
state = screenState.value,
onBackPressed = { store.dispatch(NavigationAction.PopBackTo()) },
)
}
}
}
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
screenState.value = viewModel.updateState(state.cardSettingsState, context)
}
}

View file

@ -0,0 +1,127 @@
package com.tangem.tap.features.details.ui.resetcard
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.IconToggleButton
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@Composable
fun ResetCardScreen(
state: ResetCardScreenState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
SettingsScreensScaffold(
content = { ResetCardView(state = state, modifier = modifier) },
background =
{ Image(painter = painterResource(id = R.drawable.ic_reset_background), contentDescription = "") },
titleRes = R.string.details_row_title_reset_factory_settings,
onBackClick = onBackPressed,
)
}
@Composable
fun ResetCardView(
state: ResetCardScreenState,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Bottom,
) {
Text(
text = stringResource(id = R.string.common_attention),
modifier = modifier.padding(start = 20.dp, end = 20.dp),
style = TangemTypography.headline3,
color = colorResource(id = R.color.text_primary_1),
)
Spacer(modifier = modifier.size(24.dp))
Text(
text = stringResource(id = R.string.reset_factory_warning),
modifier = modifier.padding(start = 20.dp, end = 20.dp),
style = TangemTypography.body1,
color = colorResource(id = R.color.text_secondary),
)
Spacer(modifier = modifier.size(44.dp))
Row(
modifier = modifier
.fillMaxWidth()
.padding(end = 20.dp),
) {
IconToggleButton(
checked = state.accepted,
onCheckedChange = state.onAcceptWarningToggleClick,
modifier = modifier.padding(start = 20.dp, end = 20.dp),
) {
Icon(
painter = painterResource(
if (state.accepted) {
R.drawable.ic_accepted
} else {
R.drawable.ic_unticked
},
),
contentDescription = null,
tint = if (state.accepted) {
colorResource(id = R.color.icon_accent)
} else {
colorResource(id = R.color.icon_secondary)
},
)
}
Text(
text = stringResource(id = R.string.reset_factory_confirmation),
style = TangemTypography.body2,
color = colorResource(id = R.color.text_secondary),
)
}
Spacer(modifier = modifier.size(32.dp))
Box(
modifier = modifier
.padding(start = 16.dp, end = 16.dp, bottom = 32.dp),
) {
DetailsMainButton(
title = stringResource(id = R.string.reset_factory_button),
onClick = state.onResetButtonClick,
enabled = state.resetButtonEnabled
)
}
}
}
@Composable
@Preview
fun ResetCardScreenPreview(
) {
ResetCardScreen(state = ResetCardScreenState(onAcceptWarningToggleClick = {}, accepted = true) {}, {})
}

View file

@ -0,0 +1,10 @@
package com.tangem.tap.features.details.ui.resetcard
data class ResetCardScreenState(
val accepted: Boolean = false,
val onAcceptWarningToggleClick: (Boolean) -> Unit,
val onResetButtonClick: () -> Unit,
) {
val resetButtonEnabled: Boolean
get() = accepted
}

View file

@ -0,0 +1,18 @@
package com.tangem.tap.features.details.ui.resetcard
import android.content.Context
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.CardSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
import org.rekotlin.Store
class ResetCardViewModel(private val store: Store<AppState>) {
fun updateState(state: CardSettingsState?, context: Context?): ResetCardScreenState {
return ResetCardScreenState(
accepted = state?.resetConfirmed ?: false,
onAcceptWarningToggleClick = { store.dispatch(DetailsAction.ResetToFactory.Confirm(it)) },
onResetButtonClick = { store.dispatch(DetailsAction.ResetToFactory.Proceed) }
)
}
}

View file

@ -0,0 +1,70 @@
package com.tangem.tap.features.details.ui.securitymode
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
class SecurityModeFragment : Fragment(), StoreSubscriber<DetailsState> {
private val viewModel = SecurityModeViewModel(store)
private var screenState: MutableState<SecurityModeScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.manageSecurityState))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
AppCompatTheme {
SecurityModeScreen(
state = screenState.value,
onBackPressed = { store.dispatch(NavigationAction.PopBackTo()) },
)
}
}
}
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
screenState.value = viewModel.updateState(state.cardSettingsState?.manageSecurityState)
}
}

View file

@ -0,0 +1,123 @@
package com.tangem.tap.features.details.ui.securitymode
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.RadioButton
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@Composable
fun SecurityModeScreen(
state: SecurityModeScreenState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
SettingsScreensScaffold(
content = { SecurityModeOptions(state = state, modifier = modifier) },
titleRes = R.string.card_settings_security_mode,
onBackClick = onBackPressed
)
}
@Composable
fun SecurityModeOptions(
state: SecurityModeScreenState,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(bottom = 28.dp),
verticalArrangement = Arrangement.SpaceBetween,
) {
Column(modifier = modifier.fillMaxWidth()) {
state.availableOptions.map {
val selected = it == state.selectedSecurityMode
val title = it.toTitleRes()
val subtitle = when (it) {
SecurityOption.LongTap -> R.string.details_manage_security_long_tap_description
SecurityOption.PassCode -> R.string.details_manage_security_passcode_description
SecurityOption.AccessCode -> R.string.details_manage_security_access_code_description
}
Row(
modifier = modifier
.fillMaxWidth()
.selectable(
selected = selected, onClick = { state.onNewModeSelected(it) },
)
.padding(start = 20.dp, end = 20.dp, bottom = 32.dp),
) {
RadioButton(
selected = selected, onClick = null,
modifier = modifier.padding(end = 20.dp),
)
Column {
Text(
text = stringResource(id = title),
style = TangemTypography.subtitle1,
color = colorResource(id = R.color.text_primary_1),
)
Spacer(modifier = modifier.size(4.dp))
Text(
text = stringResource(id = subtitle),
style = TangemTypography.body2,
color = colorResource(id = R.color.text_secondary),
)
}
}
}
}
DetailsMainButton(
title = stringResource(id = R.string.common_save_changes),
enabled = state.isSaveChangesEnabled,
onClick = state.onSaveChangesClicked,
modifier = modifier
.padding(start = 20.dp, end = 20.dp),
)
}
}
@Preview
@Composable
fun SecurityModeScreenPreview() {
SecurityModeScreen(
state = SecurityModeScreenState(
availableOptions = SecurityOption.values().toList(),
selectedSecurityMode = SecurityOption.LongTap,
isSaveChangesEnabled = false,
onNewModeSelected = {},
onSaveChangesClicked = {},
),
{}
)
}

View file

@ -0,0 +1,20 @@
package com.tangem.tap.features.details.ui.securitymode
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.wallet.R
data class SecurityModeScreenState(
val availableOptions: List<SecurityOption>,
val selectedSecurityMode: SecurityOption,
val isSaveChangesEnabled: Boolean,
val onNewModeSelected: (SecurityOption) -> Unit,
val onSaveChangesClicked: () -> Unit,
)
fun SecurityOption.toTitleRes(): Int {
return when (this) {
SecurityOption.LongTap -> R.string.details_manage_security_long_tap
SecurityOption.PassCode -> R.string.details_manage_security_passcode
SecurityOption.AccessCode -> R.string.details_manage_security_access_code
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.features.details.ui.securitymode
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.ManageSecurityState
import com.tangem.tap.features.details.redux.SecurityOption
import org.rekotlin.Store
class SecurityModeViewModel(val store: Store<AppState>) {
fun updateState(state: ManageSecurityState?): SecurityModeScreenState {
if (state == null) return SecurityModeScreenState(
emptyList(), SecurityOption.LongTap, false, {},
{},
)
return SecurityModeScreenState(
state.allowedOptions.toList(),
selectedSecurityMode = state.selectedOption,
isSaveChangesEnabled = state.selectedOption != state.currentOption,
onNewModeSelected = { store.dispatch(DetailsAction.ManageSecurity.SelectOption(it)) },
onSaveChangesClicked = { store.dispatch(DetailsAction.ManageSecurity.SaveChanges) },
)
}
}

View file

@ -1,51 +0,0 @@
package com.tangem.tap.features.details.ui.securityprivacy
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ViewComposeFragmentBinding
import org.rekotlin.StoreSubscriber
class ChangeAccessCodeFragment : BaseStoreFragment(R.layout.view_compose_fragment),
StoreSubscriber<DetailsState> {
private val binding: ViewComposeFragmentBinding
by viewBinding(ViewComposeFragmentBinding::bind)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
})
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
}
override fun newState(state: DetailsState) {
}
override fun subscribeToStore() {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
storeSubscribersList.add(this)
}
}

View file

@ -1,110 +0,0 @@
package com.tangem.tap.features.details.ui.securityprivacy
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.PrivacySetting
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentDetailsSecurityPrivacyBinding
import org.rekotlin.StoreSubscriber
class SecurityAndPrivacyFragment : BaseStoreFragment(R.layout.fragment_details_security_privacy),
StoreSubscriber<DetailsState> {
private val binding: FragmentDetailsSecurityPrivacyBinding
by viewBinding(FragmentDetailsSecurityPrivacyBinding::bind)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
})
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
setListeners()
}
private fun setListeners() = with(binding) {
tvAccessCode.setOnClickListener {
store.dispatch(NavigationAction.NavigateTo(AppScreen.ChangeAccessCode))
}
switchSaveCards.setOnCheckedChangeListener { _, isChecked ->
store.dispatch(
DetailsAction.ManagePrivacy.SwitchPrivacySetting(
allow = isChecked,
setting = PrivacySetting.SAVE_CARDS
)
)
}
switchSavePasswords.setOnCheckedChangeListener { _, isChecked ->
store.dispatch(
DetailsAction.ManagePrivacy.SwitchPrivacySetting(
allow = isChecked,
setting = PrivacySetting.SAVE_ACCESS_CODE
)
)
}
}
override fun newState(state: DetailsState) {
with(binding) {
val card = state.scanResponse?.card
llAccessManagement.show(card != null)
if (card != null) {
llAccessManagement.setOnClickListener {
store.dispatch(
DetailsAction.ManageSecurity.CheckCurrentSecurityOption(
card
)
)
}
}
val currentSecurity = when (state.securityScreenState?.currentOption) {
SecurityOption.LongTap -> R.string.details_manage_security_long_tap
SecurityOption.PassCode -> R.string.details_manage_security_passcode
SecurityOption.AccessCode -> R.string.details_manage_security_access_code
null -> null
}
currentSecurity?.let { tvAccessManagement.text = getString(it) }
switchSaveCards.isChecked = state.saveCards
switchSavePasswords.isChecked = state.savePasswords
tvAccessCode.show(state.scanResponse?.card?.isAccessCodeSet == true)
}
}
override fun subscribeToStore() {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
storeSubscribersList.add(this)
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.tap.features.details.ui.walletconnect
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun WalletConnectScreen(
modifier: Modifier = Modifier,
) {
}
//data class WalletConnectScreenState(
// sessions: WcSessionScreenState
//)
//
//data class WcSessionScreenState(
// val icon: Bitmap,
//
//)