Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-04 19:56:55 +03:00
commit 0159b2f1ff
53 changed files with 650 additions and 462 deletions

View file

@ -48,7 +48,6 @@ import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.tester.api.TesterMenuLauncher
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.google.GoogleServicesHelper
@ -447,12 +446,11 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
} else {
lifecycleScope.launch {
val shouldShowTos = !cardRepository.isTangemTOSAccepted()
val shouldShowInitialPush = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrElse { false }
val route = when {
shouldShowTos -> AppRoute.Disclaimer(isTosAccepted = false)
shouldShowInitialPush -> AppRoute.PushNotification
else -> AppRoute.Home
val route = if (shouldShowTos) {
AppRoute.Disclaimer(isTosAccepted = false)
} else {
AppRoute.Home
}
store.dispatchNavigationAction { replaceAll(route) }

View file

@ -3,10 +3,16 @@ package com.tangem.tap.data
import com.google.firebase.messaging.FirebaseMessaging
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import kotlinx.coroutines.tasks.await
import timber.log.Timber
import javax.inject.Inject
internal class FirebasePushNotificationsTokenProvider @Inject constructor() : PushNotificationsTokenProvider {
override suspend fun getToken(): String {
return FirebaseMessaging.getInstance().token.await()
return try {
FirebaseMessaging.getInstance().token.await()
} catch (ex: Exception) {
Timber.e(ex)
""
}
}
}

View file

@ -4,26 +4,35 @@ import com.tangem.common.services.secure.SecureStorage
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.tap.domain.userWalletList.repository.SelectedUserWalletRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
internal class DefaultSelectedUserWalletRepository(
private val secureStorage: SecureStorage,
private val dispatchers: CoroutineDispatcherProvider,
) : SelectedUserWalletRepository {
private val mutex = Mutex()
override suspend fun get(): UserWalletId? = withContext(dispatchers.io) {
secureStorage.get(StorageKey.SelectedWalletId.name)
?.decodeToString(throwOnInvalidSequence = true)
?.let { UserWalletId(it) }
mutex.withLock {
secureStorage.get(StorageKey.SelectedWalletId.name)
?.decodeToString(throwOnInvalidSequence = true)
?.let { UserWalletId(it) }
}
}
override suspend fun set(walletId: UserWalletId?) = withContext(dispatchers.io) {
if (walletId == null) {
secureStorage.delete(StorageKey.SelectedWalletId.name)
} else {
secureStorage.store(
data = walletId.stringValue.encodeToByteArray(throwOnInvalidSequence = true),
account = StorageKey.SelectedWalletId.name,
)
mutex.withLock {
if (walletId == null) {
secureStorage.delete(StorageKey.SelectedWalletId.name)
} else {
secureStorage.store(
data = walletId.stringValue.encodeToByteArray(throwOnInvalidSequence = true),
account = StorageKey.SelectedWalletId.name,
)
}
}
}

View file

@ -23,6 +23,8 @@ import com.tangem.features.nft.component.NFTComponent
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
import com.tangem.features.onramp.component.*
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacksStub
import com.tangem.features.pushnotifications.api.PushNotificationsParams
import com.tangem.features.send.v2.api.NFTSendComponent
import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.send.v2.api.SendEntryPointComponent
@ -380,7 +382,9 @@ internal class ChildFactory @Inject constructor(
is AppRoute.PushNotification -> {
createComponentChild(
context = context,
params = PushNotificationsComponent.Params.Route(AppRoute.Home),
params = PushNotificationsParams(
modelCallbacks = PushNotificationsModelCallbacksStub(),
),
componentFactory = pushNotificationsComponentFactory,
)
}

View file

@ -21,4 +21,7 @@ internal class AndroidAppInfoProvider @Inject constructor(
get() = TimeZone.getDefault().id
override val appVersion: String
get() = appVersionProvider.versionName
override val isHuaweiDevice: Boolean
get() = Build.MANUFACTURER.equals("HUAWEI", ignoreCase = true) ||
Build.BRAND.equals("HUAWEI", ignoreCase = true)
}

View file

@ -134,6 +134,18 @@ object PreferencesKeys {
val NOTIFICATIONS_ENABLED_STATES_KEY by lazy { stringPreferencesKey(name = "notificationsEnabledStates") }
val NOTIFICATIONS_AUTOMATICALLY_ENABLED_STATES_KEY by lazy {
stringPreferencesKey(
name = "notificationsAutomaticallyEnabledStates",
)
}
val NOTIFICATIONS_USER_ALLOW_SEND_ADDRESSES_KEY by lazy {
booleanPreferencesKey(
name = "userAllowSendAddresses",
)
}
val TRON_NETWORK_FEE_NOTIFICATION_SHOW_COUNT_KEY by lazy {
intPreferencesKey(name = "tronNetworkFeeNotificationShowCount")
}

View file

@ -66,7 +66,7 @@ fun Showcase(
}
@Composable
private fun ShowcaseButtons(
fun ShowcaseButtons(
primaryButtonText: TextReference,
secondaryButtonText: TextReference,
onPrimaryClick: () -> Unit,

View file

@ -0,0 +1,43 @@
package com.tangem.core.ui.utils
import android.os.Build
import androidx.annotation.ChecksSdkIntAtLeast
import androidx.compose.runtime.Composable
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
/**
* Returns push permission requester.
* Handles granting permission from app settings.
*/
@Suppress("LongParameterList")
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun requestPermission(permission: String, onAllow: () -> Unit, onDeny: () -> Unit): () -> Unit {
val permissionState = rememberPermissionState(
permission = permission,
onPermissionResult = { isGranted ->
if (isGranted) {
onAllow()
} else {
onDeny()
}
},
)
return when {
permissionState.status.isGranted == false -> {
if (isRequirePushPermission) {
permissionState::launchPermissionRequest
} else {
onDeny // on versions below Tiramisu call onDeny directly and then open settings
}
}
else -> {
onAllow
}
}
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
private val isRequirePushPermission = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU

View file

@ -1,33 +0,0 @@
package com.tangem.core.ui.utils
import androidx.compose.runtime.Composable
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberPermissionState
/**
* Returns push permission requester.
* Handles granting permission from app settings.
*/
@Suppress("LongParameterList")
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun requestPushPermission(pushPermission: String?, onAllow: () -> Unit, onDeny: () -> Unit): () -> Unit {
val permissionState = pushPermission?.let { permission ->
rememberPermissionState(
permission = permission,
onPermissionResult = { isGranted ->
if (isGranted) {
onAllow()
} else {
onDeny()
}
},
)
}
return if (permissionState == null) {
{}
} else {
permissionState::launchPermissionRequest
}
}

View file

@ -7,4 +7,5 @@ interface AppInfoProvider {
val language: String
val timezone: String
val appVersion: String
val isHuaweiDevice: Boolean
}

View file

@ -10,7 +10,9 @@ import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
class UserTokensResponseAddressesEnricher @Inject constructor(
private val notificationsFeatureToggles: NotificationsFeatureToggles,
@ -28,7 +30,10 @@ class UserTokensResponseAddressesEnricher @Inject constructor(
return withContext(dispatchers.default) {
val networksStatuses = if (isNotificationsEnabled) {
multiNetworkStatusSupplier.invoke(MultiNetworkStatusProducer.Params(userWalletId)).first()
withTimeoutOrNull(
FETCH_TIMEOUT_SECONDS.seconds,
{ multiNetworkStatusSupplier.invoke(MultiNetworkStatusProducer.Params(userWalletId)).first() },
) ?: emptySet()
} else {
emptySet()
}
@ -61,4 +66,8 @@ class UserTokensResponseAddressesEnricher @Inject constructor(
response.copy(tokens = enrichedTokens, notifyStatus = isNotificationsEnabled)
}
}
companion object {
private const val FETCH_TIMEOUT_SECONDS = 3
}
}

View file

@ -82,4 +82,36 @@ internal class DefaultNotificationsRepository @Inject constructor(
NotificationsEligibleNetworkConverter.convert(it)
}
}
override suspend fun shouldShowSubscribeOnNotificationsAfterUpdate(): Boolean {
return appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.NOTIFICATIONS_USER_ALLOW_SEND_ADDRESSES_KEY,
) == null
}
override suspend fun isUserAllowToSubscribeOnPushNotifications(): Boolean {
return appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.NOTIFICATIONS_USER_ALLOW_SEND_ADDRESSES_KEY,
default = false,
)
}
override suspend fun setUserAllowToSubscribeOnPushNotifications(value: Boolean) {
appPreferencesStore.store(PreferencesKeys.NOTIFICATIONS_USER_ALLOW_SEND_ADDRESSES_KEY, value)
}
override suspend fun getWalletAutomaticallyEnabledList(): List<String> = appPreferencesStore
.getObjectMapSync<Boolean>(PreferencesKeys.NOTIFICATIONS_AUTOMATICALLY_ENABLED_STATES_KEY).map {
it.key
}
override suspend fun setNotificationsWasEnabledAutomatically(userWalletId: String) {
appPreferencesStore.editData {
it.setObjectMap(
key = PreferencesKeys.NOTIFICATIONS_AUTOMATICALLY_ENABLED_STATES_KEY,
value = it.getObjectMap<Boolean>(PreferencesKeys.NOTIFICATIONS_AUTOMATICALLY_ENABLED_STATES_KEY)
.plus(userWalletId to true),
)
}
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.notifications
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import javax.inject.Inject
class GetIsHuaweiDeviceWithoutGoogleServicesUseCase @Inject constructor(
private val appInfoProvider: AppInfoProvider,
private val pushNotificationsTokenProvider: PushNotificationsTokenProvider,
) {
suspend operator fun invoke(): Boolean {
return appInfoProvider.isHuaweiDevice && pushNotificationsTokenProvider.getToken().isEmpty()
}
}

View file

@ -21,4 +21,14 @@ interface NotificationsRepository {
@Throws
suspend fun getEligibleNetworks(): List<NotificationsEligibleNetwork>
suspend fun shouldShowSubscribeOnNotificationsAfterUpdate(): Boolean
suspend fun isUserAllowToSubscribeOnPushNotifications(): Boolean
suspend fun setUserAllowToSubscribeOnPushNotifications(value: Boolean)
suspend fun getWalletAutomaticallyEnabledList(): List<String>
suspend fun setNotificationsWasEnabledAutomatically(userWalletId: String)
}

View file

@ -0,0 +1,11 @@
package com.tangem.domain.wallets.usecase
import com.tangem.sdk.api.TangemSdkManager
import javax.inject.Inject
class GetIsBiometricsEnabledUseCase @Inject constructor(
private val tangemSdkManager: TangemSdkManager,
) {
operator fun invoke(): Boolean = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() ?: false
}

View file

@ -0,0 +1,27 @@
package com.tangem.domain.wallets.usecase
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import javax.inject.Inject
/**
* Use case for retrieving wallets where automatically enabling push notifications was not applied.
* * This use case filters out wallets that have already had push notifications automatically enabled
* from the complete list of user wallets, returning only those wallets that still need to have
* push notifications automatically enabled.
* * @property userWalletsListManager Manager for user wallets list operations
* @property dispatchers Coroutine dispatcher provider for background operations
*/
class GetWalletsForAutomaticallyPushEnablingUseCase @Inject constructor(
private val userWalletsListManager: UserWalletsListManager,
private val dispatchers: CoroutineDispatcherProvider,
) {
suspend operator fun invoke(walletsListWherePushWasEnabled: List<UserWalletId>): List<UserWalletId> =
withContext(dispatchers.default) {
val allLocalWallets = userWalletsListManager.userWalletsSync.map { it.walletId }
allLocalWallets - walletsListWherePushWasEnabled.toSet()
}
}

View file

@ -37,6 +37,7 @@ dependencies {
implementation(projects.domain.models)
implementation(projects.domain.card)
implementation(projects.domain.settings)
implementation(projects.domain.notifications)
/** Feature modules */
implementation(projects.features.disclaimer.api)

View file

@ -3,7 +3,7 @@ package com.tangem.features.disclaimer.impl.entity
internal data class DisclaimerUM(
val url: String,
val isTosAccepted: Boolean,
val onAccept: (Boolean) -> Unit,
val onAccept: () -> Unit,
val popBack: () -> Unit,
)

View file

@ -7,6 +7,8 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.settings.NeverRequestPermissionUseCase
import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
@ -26,6 +28,9 @@ internal class DisclaimerModel @Inject constructor(
private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase,
private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase,
private val appFinisher: AppFinisher,
private val notificationsRepository: NotificationsRepository,
private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase,
paramsContainer: ParamsContainer,
) : Model() {
@ -40,13 +45,14 @@ internal class DisclaimerModel @Inject constructor(
),
)
private fun onAccept(shouldAskPushPermission: Boolean) = modelScope.launch {
private fun onAccept() = modelScope.launch {
if (params.isTosAccepted) {
router.pop()
} else {
cardRepository.acceptTangemTOS()
if (shouldAskPushPermission) {
val shouldAskPushPermission = notificationsRepository.shouldShowSubscribeOnNotificationsAfterUpdate()
val isHuaweiDevice = getIsHuaweiDeviceWithoutGoogleServicesUseCase()
if (shouldAskPushPermission && !isHuaweiDevice) {
router.push(AppRoute.PushNotification)
} else {
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)

View file

@ -15,9 +15,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.web.WebView
import com.google.accompanist.web.WebViewNavigator
import com.google.accompanist.web.rememberWebViewNavigator
@ -38,7 +35,6 @@ import com.tangem.features.disclaimer.impl.R
import com.tangem.features.disclaimer.impl.entity.DisclaimerUM
import com.tangem.features.disclaimer.impl.entity.DummyDisclaimer
import com.tangem.features.disclaimer.impl.local.localTermsOfServices
import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.withDebounce
import java.nio.charset.StandardCharsets
@ -166,15 +162,11 @@ private fun WebViewNavigator.loadLocalToS() {
)
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun BoxScope.DisclaimerButton(onAccept: (Boolean) -> Unit) {
val isPermissionGranted = getPushPermissionOrNull()?.let { permission ->
rememberPermissionState(permission = permission).status.isGranted
} ?: true
private fun BoxScope.DisclaimerButton(onAccept: () -> Unit) {
PrimaryButton(
text = stringResourceSafe(id = R.string.common_accept),
onClick = { onAccept(!isPermissionGranted) },
onClick = onAccept,
colors = ButtonColors(
containerColor = TangemColorPalette.Light4,
contentColor = TangemColorPalette.Dark6,

View file

@ -9,13 +9,12 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
import com.tangem.domain.settings.ShouldAskPermissionUseCase
import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeComponent
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent
import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent
import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeComponent
import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.launch
@ -32,7 +31,6 @@ internal class AddExistingWalletModel @Inject constructor(
val addExistingWalletImportModelCallbacks = AddExistingWalletImportModelCallbacks()
val manualBackupCompletedComponentModelCallbacks = ManualBackupCompletedComponentModelCallbacks()
val accessCodeModelCallbacks = AccessCodeModelCallbacks()
val pushNotificationsComponentModelCallbacks = PushNotificationsComponentModelCallbacks()
val mobileWalletSetupFinishedComponentModelCallbacks = MobileWalletSetupFinishedComponentModelCallbacks()
val stackNavigation = StackNavigation<AddExistingWalletRoute>()
@ -89,12 +87,6 @@ internal class AddExistingWalletModel @Inject constructor(
}
}
inner class PushNotificationsComponentModelCallbacks : PushNotificationsComponent.ModelCallbacks {
override fun onResult() {
stackNavigation.replaceCurrent(AddExistingWalletRoute.SetupFinished)
}
}
inner class MobileWalletSetupFinishedComponentModelCallbacks : MobileWalletSetupFinishedComponent.ModelCallbacks {
override fun onContinueClick() {
router.replaceAll(AppRoute.Wallet)

View file

@ -2,13 +2,15 @@ package com.tangem.features.hotwallet.addexistingwallet.root.routing
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeComponent
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent
import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeComponent
import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacksStub
import com.tangem.features.pushnotifications.api.PushNotificationsParams
import javax.inject.Inject
internal class AddExistingWalletChildFactory @Inject constructor(
@ -47,8 +49,8 @@ internal class AddExistingWalletChildFactory @Inject constructor(
)
is AddExistingWalletRoute.PushNotifications -> pushNotificationsComponent.create(
context = childContext,
params = PushNotificationsComponent.Params.Callbacks(
callbacks = model.pushNotificationsComponentModelCallbacks,
params = PushNotificationsParams(
modelCallbacks = PushNotificationsModelCallbacksStub(),
),
)
AddExistingWalletRoute.SetupFinished -> MobileWalletSetupFinishedComponent(

View file

@ -33,7 +33,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.MultiWall
import com.tangem.features.onboarding.v2.multiwallet.impl.child.chooseoption.Wallet1ChooseOptionComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.MultiWalletCreateWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.MultiWalletFinalizeComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.Wallet1ScanPrimaryComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.MultiWalletScanPrimaryComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.MultiWalletSeedPhraseComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletModel
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
@ -159,7 +159,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
backButtonClickFlow = backButtonClickFlow,
onBack = { stackNavigation.pop() },
)
ScanPrimary -> Wallet1ScanPrimaryComponent(
ScanPrimary -> MultiWalletScanPrimaryComponent(
context = childContext,
params = childParams,
onDone = { handleNavigationEvent(AddBackupDevice) },

View file

@ -84,6 +84,15 @@ class MultiWalletBackupModel @Inject constructor(
}
analyticsEventHandler.send(OnboardingEvent.Backup.Started)
// Clear any saved backup before starting the backup process
// also clears the primary card if it was set
backupService.discardSavedBackup()
// Primary card from ScanTask or from the ScanPrimaryModel or from MultiWalletCreateWalletModel
// always not null for this step
val primaryCard = requireNotNull(scanResponse.primaryCard)
backupService.setPrimaryCard(primaryCard)
}
private fun getInitState(): MultiWalletBackupUM {
@ -93,24 +102,11 @@ class MultiWalletBackupModel @Inject constructor(
finalizeButtonEnabled = false,
addBackupButtonEnabled = true,
addBackupButtonLoading = false,
onAddBackupClick = ::startBackupWallet,
onAddBackupClick = ::addBackupCardWithService,
onFinalizeButtonClick = ::onFinalizeClick,
)
}
private fun startBackupWallet() {
if (state.value.numberOfBackupCards == 0 && scanResponse.primaryCard != null) {
backupService.discardSavedBackup()
}
val primaryCard = scanResponse.primaryCard
if (primaryCard != null) {
backupService.setPrimaryCard(primaryCard)
}
addBackupCardWithService()
}
private fun setNumberOfBackupCards(number: Int) {
// set state for adding backup cards and disable button if there is more than 2 backup cards
_uiState.update { st ->

View file

@ -8,18 +8,18 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.model.Wallet1ScanPrimaryModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.ui.Wallet1ScanPrimary
import com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.model.MultiWalletScanPrimaryModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.ui.MultiWalletScanPrimary
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
internal class Wallet1ScanPrimaryComponent(
internal class MultiWalletScanPrimaryComponent(
context: AppComponentContext,
params: MultiWalletChildParams,
onDone: () -> Unit,
) : AppComponentContext by context, ComposableContentComponent {
private val model: Wallet1ScanPrimaryModel = getOrCreateModel(params)
private val model: MultiWalletScanPrimaryModel = getOrCreateModel(params)
init {
params.innerNavigation.update {
@ -38,7 +38,7 @@ internal class Wallet1ScanPrimaryComponent(
@Composable
override fun Content(modifier: Modifier) {
Wallet1ScanPrimary(
MultiWalletScanPrimary(
isRing = model.isRing,
onScanPrimaryClick = model::onScanPrimaryClick,
)

View file

@ -5,21 +5,24 @@ import com.tangem.common.CompletionResult
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ModelScoped
internal class Wallet1ScanPrimaryModel @Inject constructor(
internal class MultiWalletScanPrimaryModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val backupServiceHolder: BackupServiceHolder,
private val cardRepository: CardRepository,
) : Model() {
private val params = paramsContainer.require<MultiWalletChildParams>()
@ -31,9 +34,20 @@ internal class Wallet1ScanPrimaryModel @Inject constructor(
fun onScanPrimaryClick() {
val backupService = backupServiceHolder.backupService.get() ?: return
val iconScanRes = R.drawable.img_hand_scan_ring.takeIf { isRing }
backupService.readPrimaryCard(iconScanRes = iconScanRes, cardId = scanResponse.card.cardId) { result ->
when (result) {
is CompletionResult.Success -> {
modelScope.launch {
cardRepository.startCardActivation(cardId = scanResponse.card.cardId)
}
params.multiWalletState.update {
it.copy(
currentScanResponse = scanResponse.copy(
primaryCard = result.data,
),
)
}
modelScope.launch { onDone.emit(Unit) }
}
is CompletionResult.Failure -> Unit

View file

@ -17,7 +17,7 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.impl.R
@Composable
internal fun Wallet1ScanPrimary(isRing: Boolean, onScanPrimaryClick: () -> Unit, modifier: Modifier = Modifier) {
internal fun MultiWalletScanPrimary(isRing: Boolean, onScanPrimaryClick: () -> Unit, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
@ -66,7 +66,7 @@ internal fun Wallet1ScanPrimary(isRing: Boolean, onScanPrimaryClick: () -> Unit,
@Composable
private fun Preview() {
TangemThemePreview {
Wallet1ScanPrimary(
MultiWalletScanPrimary(
isRing = true,
onScanPrimaryClick = {},
)

View file

@ -9,7 +9,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model.Mul
import com.tangem.features.onboarding.v2.multiwallet.impl.child.chooseoption.model.Wallet1ChooseOptionModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.model.MultiWalletCreateWalletModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.model.MultiWalletFinalizeModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.model.Wallet1ScanPrimaryModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.model.MultiWalletScanPrimaryModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.MultiWalletSeedPhraseModel
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletModel
import dagger.Binds
@ -70,6 +70,6 @@ internal interface ModelModule {
@Binds
@IntoMap
@ClassKey(Wallet1ScanPrimaryModel::class)
fun provideModel8(model: Wallet1ScanPrimaryModel): Model
@ClassKey(MultiWalletScanPrimaryModel::class)
fun provideModel8(model: MultiWalletScanPrimaryModel): Model
}

View file

@ -24,6 +24,9 @@ data class OnboardingMultiWalletState(
* -> [Done]
*
* Wallet2/Ring
*
* ScanPrimary -> | (BackupService is cleared, no PrimaryCard)
* |
* CreateWallet -> SeedPhrase -> AddBackupDevice -> Finalize -> [Done]
* |
* -> AddBackupDevice -> Finalize -> [Done]

View file

@ -0,0 +1,9 @@
package com.tangem.features.pushnotifications.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
interface PushNotificationsBottomSheetComponent : ComposableBottomSheetComponent {
interface Factory : ComponentFactory<PushNotificationsParams, PushNotificationsBottomSheetComponent>
}

View file

@ -1,19 +1,9 @@
package com.tangem.features.pushnotifications.api
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface PushNotificationsComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Params, PushNotificationsComponent>
interface ModelCallbacks {
fun onResult()
}
sealed class Params {
data class Callbacks(val callbacks: ModelCallbacks) : Params()
data class Route(val route: AppRoute) : Params()
}
interface Factory : ComponentFactory<PushNotificationsParams, PushNotificationsComponent>
}

View file

@ -0,0 +1,17 @@
package com.tangem.features.pushnotifications.api
interface PushNotificationsModelCallbacks {
fun onAllowSystemPermission()
fun onDenySystemPermission()
fun onDismiss()
}
class PushNotificationsModelCallbacksStub(
val onAllowSystemPermission: () -> Unit = {},
val onDenySystemPermission: () -> Unit = {},
val onDismiss: () -> Unit = {},
) : PushNotificationsModelCallbacks {
override fun onAllowSystemPermission() = onAllowSystemPermission.invoke()
override fun onDenySystemPermission() = onDenySystemPermission.invoke()
override fun onDismiss() = onDismiss.invoke()
}

View file

@ -0,0 +1,6 @@
package com.tangem.features.pushnotifications.api
data class PushNotificationsParams(
val isBottomSheet: Boolean = false,
val modelCallbacks: PushNotificationsModelCallbacks,
)

View file

@ -16,6 +16,8 @@ dependencies {
implementation(deps.androidx.activity.compose)
/** Compose */
implementation(deps.compose.material3)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.foundation)
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.compose.accompanist.permission)
@ -39,6 +41,7 @@ dependencies {
/** Domain module */
implementation(projects.domain.settings)
implementation(projects.domain.notifications.toggles)
implementation(projects.domain.notifications)
/** Feature modules */
implementation(projects.features.pushNotifications.api)

View file

@ -0,0 +1,62 @@
package com.tangem.features.pushnotifications.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent
import com.tangem.features.pushnotifications.api.PushNotificationsParams
import com.tangem.features.pushnotifications.impl.model.PushNotificationsModel
import com.tangem.features.pushnotifications.impl.presentation.ui.PushNotificationsBottomSheet
import com.tangem.features.pushnotifications.impl.presentation.ui.PushNotificationsContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultPushNotificationsBottomSheetComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: PushNotificationsParams,
) : PushNotificationsBottomSheetComponent, AppComponentContext by appComponentContext {
private val model: PushNotificationsModel = getOrCreateModel(params)
@AssistedFactory
interface Factory : PushNotificationsBottomSheetComponent.Factory {
override fun create(
context: AppComponentContext,
params: PushNotificationsParams,
): DefaultPushNotificationsBottomSheetComponent
}
override fun dismiss() {
params.modelCallbacks.onDismiss()
}
@Composable
override fun BottomSheet() {
val state by model.state.collectAsState()
val bottomSheetConfig = remember(key1 = this) {
TangemBottomSheetConfig(
isShown = true,
onDismissRequest = ::dismiss,
content = TangemBottomSheetConfigContent.Empty,
)
}
PushNotificationsBottomSheet(
config = bottomSheetConfig,
) {
PushNotificationsContent(
onAllowClick = model::onAllowClick,
onLaterClick = model::onLaterClick,
onAllowPermission = model::onAllowPermission,
onDenyPermission = model::onDenyPermission,
showNotificationsInfo = state.showInfoAboutNotifications,
)
}
}
}

View file

@ -11,6 +11,7 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.core.ui.utils.findActivity
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.pushnotifications.api.PushNotificationsParams
import com.tangem.features.pushnotifications.impl.model.PushNotificationsModel
import com.tangem.features.pushnotifications.impl.presentation.ui.PushNotificationsScreen
import dagger.assisted.Assisted
@ -19,7 +20,7 @@ import dagger.assisted.AssistedInject
internal class DefaultPushNotificationsComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: PushNotificationsComponent.Params,
@Assisted private val params: PushNotificationsParams,
) : PushNotificationsComponent, AppComponentContext by appComponentContext {
private val model: PushNotificationsModel = getOrCreateModel(params)
@ -31,8 +32,8 @@ internal class DefaultPushNotificationsComponent @AssistedInject constructor(
BackHandler(onBack = { activity.finish() })
NavigationBar3ButtonsScrim()
PushNotificationsScreen(
onRequest = model::onRequest,
onNeverRequest = model::onNeverRequest,
onAllowClick = model::onAllowClick,
onLaterClick = model::onLaterClick,
onAllowPermission = model::onAllowPermission,
onDenyPermission = model::onDenyPermission,
showNotificationsInfo = state.showInfoAboutNotifications,
@ -43,7 +44,7 @@ internal class DefaultPushNotificationsComponent @AssistedInject constructor(
interface Factory : PushNotificationsComponent.Factory {
override fun create(
context: AppComponentContext,
params: PushNotificationsComponent.Params,
params: PushNotificationsParams,
): DefaultPushNotificationsComponent
}
}

View file

@ -1,7 +1,9 @@
package com.tangem.features.pushnotifications.impl.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.pushnotifications.impl.DefaultPushNotificationsBottomSheetComponent
import com.tangem.features.pushnotifications.impl.DefaultPushNotificationsComponent
import com.tangem.features.pushnotifications.impl.model.PushNotificationsModel
import dagger.Binds
@ -15,6 +17,11 @@ import dagger.multibindings.IntoMap
@InstallIn(SingletonComponent::class)
internal interface PushNotificationsModule {
@Binds
fun bindBottomSheetComponentFactory(
impl: DefaultPushNotificationsBottomSheetComponent.Factory,
): PushNotificationsBottomSheetComponent.Factory
@Binds
fun bindComponentFactory(impl: DefaultPushNotificationsComponent.Factory): PushNotificationsComponent.Factory

View file

@ -1,9 +1,9 @@
package com.tangem.features.pushnotifications.impl.model
internal interface PushNotificationsClickIntents {
fun onRequest()
fun onAllowClick()
fun onNeverRequest()
fun onLaterClick()
fun onAllowPermission()

View file

@ -1,16 +1,18 @@
package com.tangem.features.pushnotifications.impl.model
import androidx.compose.runtime.Stable
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
import com.tangem.domain.settings.NeverRequestPermissionUseCase
import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.pushnotifications.api.PushNotificationsParams
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.pushnotifications.impl.presentation.state.PushNotificationsUM
@ -20,9 +22,9 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ModelScoped
@Suppress("LongParameterList")
internal class PushNotificationsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
@ -30,10 +32,11 @@ internal class PushNotificationsModel @Inject constructor(
private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase,
private val appRouter: AppRouter,
private val analyticHandler: AnalyticsEventHandler,
notificationsFeatureToggles: NotificationsFeatureToggles,
private val notificationsFeatureToggles: NotificationsFeatureToggles,
private val notificationsRepository: NotificationsRepository,
) : Model(), PushNotificationsClickIntents {
private val params: PushNotificationsComponent.Params = paramsContainer.require()
val params: PushNotificationsParams = paramsContainer.require()
private val _state = MutableStateFlow(
PushNotificationsUM(
@ -43,20 +46,33 @@ internal class PushNotificationsModel @Inject constructor(
val state = _state.asStateFlow()
override fun onRequest() {
override fun onAllowClick() {
if (notificationsFeatureToggles.isNotificationsEnabled) {
modelScope.launch {
notificationsRepository.setUserAllowToSubscribeOnPushNotifications(true)
}
}
analyticHandler.send(
PushNotificationAnalyticEvents.ButtonAllow(AnalyticsParam.ScreensSources.Stories),
)
}
override fun onNeverRequest() {
override fun onLaterClick() {
if (notificationsFeatureToggles.isNotificationsEnabled) {
modelScope.launch {
notificationsRepository.setUserAllowToSubscribeOnPushNotifications(false)
}
}
analyticHandler.send(
PushNotificationAnalyticEvents.ButtonLater(AnalyticsParam.ScreensSources.Stories),
)
modelScope.launch {
neverRequestPermissionUseCase(PUSH_PERMISSION)
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)
onResult()
params.modelCallbacks.onDenySystemPermission()
if (!params.isBottomSheet) {
appRouter.push(AppRoute.Home)
}
}
}
@ -67,7 +83,10 @@ internal class PushNotificationsModel @Inject constructor(
modelScope.launch {
neverRequestPermissionUseCase(PUSH_PERMISSION)
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)
onResult()
params.modelCallbacks.onAllowSystemPermission()
if (!params.isBottomSheet) {
appRouter.push(AppRoute.Home)
}
}
}
@ -78,17 +97,9 @@ internal class PushNotificationsModel @Inject constructor(
modelScope.launch {
neverRequestPermissionUseCase(PUSH_PERMISSION)
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)
onResult()
}
}
private fun onResult() {
when (params) {
is PushNotificationsComponent.Params.Callbacks -> {
params.callbacks.onResult()
}
is PushNotificationsComponent.Params.Route -> {
appRouter.push(params.route)
params.modelCallbacks.onDenySystemPermission()
if (!params.isBottomSheet) {
appRouter.push(AppRoute.Home)
}
}
}

View file

@ -0,0 +1,118 @@
package com.tangem.features.pushnotifications.impl.presentation.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH28
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.components.showcase.ShowcaseButtons
import com.tangem.core.ui.components.showcase.ShowcaseContent
import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.requestPermission
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun PushNotificationsBottomSheet(config: TangemBottomSheetConfig, content: @Composable () -> Unit) {
TangemModalBottomSheet<TangemBottomSheetConfigContent>(
config = config,
title = {
TangemModalBottomSheetTitle(
endIconRes = R.drawable.ic_close_24,
onEndClick = config.onDismissRequest,
)
},
) {
content()
}
}
@Composable
internal fun PushNotificationsContent(
onAllowClick: () -> Unit,
onLaterClick: () -> Unit,
onAllowPermission: () -> Unit,
onDenyPermission: () -> Unit,
showNotificationsInfo: Boolean,
) {
val requestPushPermission = requestPermission(
onAllow = onAllowPermission,
onDeny = onDenyPermission,
permission = PUSH_PERMISSION,
)
Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
ShowcaseContent(
headerIconRes = R.drawable.ic_notifications_unread_24,
headerText = resourceReference(R.string.user_push_notification_agreement_header),
showcaseItems = persistentListOf(
ShowcaseItemModel(
R.drawable.ic_rocket_launch_24,
resourceReference(R.string.user_push_notification_agreement_argument_one),
),
ShowcaseItemModel(
R.drawable.ic_storefront_24,
resourceReference(R.string.user_push_notification_agreement_argument_two),
),
).let { baseItems ->
if (showNotificationsInfo) {
baseItems.add(
ShowcaseItemModel(
R.drawable.ic_notifications_24,
resourceReference(R.string.user_push_notification_agreement_argument_three),
),
)
} else {
baseItems
}
},
modifier = Modifier.padding(top = TangemTheme.dimens.spacing40),
)
SpacerH28()
ShowcaseButtons(
primaryButtonText = resourceReference(R.string.common_allow),
onPrimaryClick = {
onAllowClick()
requestPushPermission()
},
secondaryButtonText = resourceReference(R.string.common_later),
onSecondaryClick = {
onLaterClick()
},
)
}
}
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_PushNotificationsBottomSheet() {
TangemThemePreview {
PushNotificationsBottomSheet(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
) {
PushNotificationsContent(
onAllowClick = {},
onLaterClick = {},
onAllowPermission = {},
onDenyPermission = {},
showNotificationsInfo = true,
)
}
}
}

View file

@ -1,29 +1,29 @@
package com.tangem.features.pushnotifications.impl.presentation.ui
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.showcase.Showcase
import com.tangem.core.ui.components.showcase.model.ShowcaseButtonModel
import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.utils.requestPushPermission
import com.tangem.core.ui.utils.requestPermission
import com.tangem.feature.pushnotifications.impl.R
import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun PushNotificationsScreen(
onRequest: () -> Unit,
onNeverRequest: () -> Unit,
onAllowClick: () -> Unit,
onLaterClick: () -> Unit,
onAllowPermission: () -> Unit,
onDenyPermission: () -> Unit,
showNotificationsInfo: Boolean,
) {
val requestPushPermission = requestPushPermission(
val requestPushPermission = requestPermission(
onAllow = onAllowPermission,
onDeny = onDenyPermission,
pushPermission = getPushPermissionOrNull(),
permission = PUSH_PERMISSION,
)
Showcase(
@ -53,13 +53,15 @@ internal fun PushNotificationsScreen(
primaryButton = ShowcaseButtonModel(
buttonText = resourceReference(R.string.common_allow),
onClick = {
onRequest()
onAllowClick()
requestPushPermission()
},
),
secondaryButton = ShowcaseButtonModel(
buttonText = resourceReference(R.string.common_later),
onClick = onNeverRequest,
onClick = {
onLaterClick()
},
),
modifier = Modifier.systemBarsPadding(),
)

View file

@ -26,6 +26,8 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.nft.DisableWalletNFTUseCase
import com.tangem.domain.nft.EnableWalletNFTUseCase
import com.tangem.domain.nft.GetWalletNFTEnabledUseCase
import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
import com.tangem.domain.settings.repositories.PermissionRepository
import com.tangem.domain.models.wallet.UserWallet
@ -74,6 +76,8 @@ internal class WalletSettingsModel @Inject constructor(
private val settingsManager: SettingsManager,
private val permissionsRepository: PermissionRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val notificationsRepository: NotificationsRepository,
private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase,
) : Model() {
val params: WalletSettingsComponent.Params = paramsContainer.require()
@ -97,6 +101,8 @@ internal class WalletSettingsModel @Inject constructor(
) { maybeWallet, nftEnabled, notificationsEnabled ->
val wallet = maybeWallet.getOrNull() ?: return@combine
val isRenameWalletAvailable = getShouldSaveUserWalletsSyncUseCase()
val isNeedShowNotifications = notificationsToggles.isNotificationsEnabled &&
!getIsHuaweiDeviceWithoutGoogleServicesUseCase()
state.update { value ->
value.copy(
items = buildItems(
@ -105,7 +111,7 @@ internal class WalletSettingsModel @Inject constructor(
isRenameWalletAvailable = isRenameWalletAvailable,
isNFTEnabled = nftEnabled,
isNotificationsEnabled = notificationsEnabled,
isNotificationsFeatureEnabled = notificationsToggles.isNotificationsEnabled,
isNotificationsFeatureEnabled = isNeedShowNotifications,
isNotificationsPermissionGranted = isNotificationsPermissionGranted(),
isHotWalletEnabled = hotWalletFeatureToggles.isHotWalletEnabled,
),
@ -231,6 +237,10 @@ internal class WalletSettingsModel @Inject constructor(
private fun onCheckedNotificationsChange(isChecked: Boolean) {
modelScope.launch {
if (isChecked) {
if (getIsHuaweiDeviceWithoutGoogleServicesUseCase()) {
showHuaweiDialog()
return@launch
}
state.update { value ->
value.copy(
requestPushNotificationsPermission = true,
@ -243,6 +253,21 @@ internal class WalletSettingsModel @Inject constructor(
}
}
private fun showHuaweiDialog() {
val message = DialogMessage(
message = resourceReference(R.string.wallet_settings_push_notifications_huawei_warning),
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.common_ok),
warning = true,
onClick = {},
)
},
)
messageSender.send(message)
}
private fun onNotificationsDescriptionClick() {
bottomSheetNavigation.activate(NetworksAvailableForNotificationBSConfig)
}
@ -261,6 +286,7 @@ internal class WalletSettingsModel @Inject constructor(
if (isGranted) {
modelScope.launch {
setNotificationsEnabledUseCase(params.userWalletId, true).onRight {
notificationsRepository.setNotificationsWasEnabledAutomatically(params.userWalletId.stringValue)
analyticsEventHandler.send(PushNotificationAnalyticEvents.NotificationsEnabled(true))
}
}

View file

@ -8,6 +8,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
@ -28,12 +29,12 @@ import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.WalletSettingsScreenTestTags
import com.tangem.core.ui.utils.requestPushPermission
import com.tangem.core.ui.utils.requestPermission
import com.tangem.feature.walletsettings.component.preview.PreviewWalletSettingsComponent
import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM
import com.tangem.feature.walletsettings.entity.WalletSettingsUM
import com.tangem.feature.walletsettings.impl.R
import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
@Composable
internal fun WalletSettingsScreen(
@ -119,14 +120,16 @@ private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) {
}
}
val requestPushPermission = requestPushPermission(
val requestPushPermission = requestPermission(
onAllow = { state.onPushNotificationPermissionGranted(true) },
onDeny = { state.onPushNotificationPermissionGranted(false) },
pushPermission = getPushPermissionOrNull(),
permission = PUSH_PERMISSION,
)
if (state.requestPushNotificationsPermission) {
requestPushPermission()
LaunchedEffect(Unit) {
requestPushPermission()
}
}
}

View file

@ -95,6 +95,8 @@ dependencies {
implementation(projects.domain.visa)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.notifications)
implementation(projects.domain.notifications.toggles)
/** Feature Apis */
implementation(projects.features.details.api)

View file

@ -21,6 +21,8 @@ import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen
import com.tangem.feature.walletsettings.component.RenameWalletComponent
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.markets.entry.MarketsEntryComponent
import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent
import com.tangem.features.pushnotifications.api.PushNotificationsParams
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -32,6 +34,7 @@ internal class WalletComponent @AssistedInject constructor(
private val renameWalletComponentFactory: RenameWalletComponent.Factory,
private val marketsEntryComponentFactory: MarketsEntryComponent.Factory,
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: WalletModel = getOrCreateModel()
@ -66,6 +69,13 @@ internal class WalletComponent @AssistedInject constructor(
),
)
}
WalletDialogConfig.AskForPushNotifications -> pushNotificationsBottomSheetComponent.create(
context = childByContext(componentContext),
params = PushNotificationsParams(
isBottomSheet = true,
modelCallbacks = model.askForPushNotificationsModelCallbacks,
),
)
}
},
)

View file

@ -11,16 +11,18 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase
import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
import com.tangem.domain.settings.*
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.domain.wallets.usecase.*
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
@ -28,7 +30,6 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWal
import com.tangem.feature.wallet.presentation.wallet.domain.*
import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.model.PushNotificationsBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent.DemonstrateWalletsScrollPreview.Direction
@ -37,8 +38,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.*
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.*
@ -79,11 +80,18 @@ internal class WalletModel @Inject constructor(
private val tokensFeatureToggles: TokensFeatureToggles,
private val observeAndClearNFTCacheIfNeedUseCase: ObserveAndClearNFTCacheIfNeedUseCase,
private val walletDeepLinkActionListener: WalletDeepLinkActionListener,
private val notificationsRepository: NotificationsRepository,
private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase,
private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase,
private val notificationsFeatureToggles: NotificationsFeatureToggles,
private val getIsBiometryIsEnabledUseCase: GetIsBiometricsEnabledUseCase,
private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase,
val screenLifecycleProvider: ScreenLifecycleProvider,
val innerWalletRouter: InnerWalletRouter,
) : Model() {
val askBiometryModelCallbacks = AskBiometryModelCallbacks()
val askForPushNotificationsModelCallbacks = AskForPushNotificationsCallbacks()
val uiState: StateFlow<WalletScreenState> = stateHolder.uiState
private val walletsUpdateJobHolder = JobHolder()
@ -106,6 +114,7 @@ internal class WalletModel @Inject constructor(
subscribeOnSelectedWalletFlow()
subscribeToScreenBackgroundState()
subscribeOnPushNotificationsPermission()
enableNotificationsIfNeeded()
clickIntents.initialize(innerWalletRouter, modelScope)
}
@ -201,20 +210,19 @@ internal class WalletModel @Inject constructor(
private fun subscribeOnPushNotificationsPermission() {
modelScope.launch {
val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION)
val isPushPermissionAvailable = getPushPermissionOrNull() != null
if (!shouldRequestPush || !isPushPermissionAvailable) return@launch
val shouldAskPermission = shouldAskPermissionUseCase(PUSH_PERMISSION)
val afterUpdate = notificationsRepository.shouldShowSubscribeOnNotificationsAfterUpdate()
val isBiometricsEnabled = getIsBiometryIsEnabledUseCase()
val isHuaweiDevice = getIsHuaweiDeviceWithoutGoogleServicesUseCase()
val shouldShowBottomSheet = shouldAskPermission || afterUpdate
if (!isBiometricsEnabled) return@launch
if (isHuaweiDevice) return@launch
if (!shouldShowBottomSheet) return@launch
delay(timeMillis = 1_800)
stateHolder.showBottomSheet(
content = PushNotificationsBottomSheetConfig(
onRequest = clickIntents::onRequestPushPermission,
onNeverRequest = { clickIntents.onNeverAskPushPermission(false) },
onAllow = clickIntents::onAllowPushPermission,
onDeny = clickIntents::onDenyPushPermission,
),
onDismiss = { clickIntents.onNeverAskPushPermission(true) },
innerWalletRouter.dialogNavigation.activate(
configuration = WalletDialogConfig.AskForPushNotifications,
)
}
}
@ -435,6 +443,8 @@ internal class WalletModel @Inject constructor(
it.copy(selectedWalletIndex = action.selectedWalletIndex)
}
}
enableNotificationsIfNeeded()
}
private fun deleteWallet(action: WalletsUpdateActionResolver.Action.DeleteWallet) {
@ -552,6 +562,26 @@ internal class WalletModel @Inject constructor(
}
}
private fun enableNotificationsIfNeeded() {
if (!notificationsFeatureToggles.isNotificationsEnabled) return
modelScope.launch {
val isUserAllowToEnableNotifications = notificationsRepository.isUserAllowToSubscribeOnPushNotifications()
if (isUserAllowToEnableNotifications) {
val alreadyEnabledWallets = notificationsRepository.getWalletAutomaticallyEnabledList().map {
UserWalletId(it)
}
val walletsListWhichShouldBeEnabled = getWalletsListForEnablingUseCase(alreadyEnabledWallets)
walletsListWhichShouldBeEnabled.forEach { userWalletId ->
setNotificationsEnabledUseCase(userWalletId, true).onRight {
notificationsRepository.setNotificationsWasEnabledAutomatically(userWalletId.stringValue)
}.onLeft {
Timber.e(it)
}
}
}
}
}
inner class AskBiometryModelCallbacks : AskBiometryComponent.ModelCallbacks {
override fun onAllowed() {
analyticsEventsHandler.send(MainScreenAnalyticsEvent.EnableBiometrics(AnalyticsParam.OnOffState.On))
@ -564,6 +594,23 @@ internal class WalletModel @Inject constructor(
}
}
inner class AskForPushNotificationsCallbacks : PushNotificationsModelCallbacks {
override fun onAllowSystemPermission() {
innerWalletRouter.dialogNavigation.dismiss()
enableNotificationsIfNeeded()
}
override fun onDenySystemPermission() {
innerWalletRouter.dialogNavigation.dismiss()
enableNotificationsIfNeeded()
}
override fun onDismiss() {
innerWalletRouter.dialogNavigation.dismiss()
}
}
private companion object {
const val REFRESH_WALLET_BACKGROUND_TIMER_MILLIS = 10000L
const val EXPRESS_STATUS_UPDATE_DELAY = 10000L

View file

@ -305,7 +305,7 @@ internal enum class Wallet2CobrandImage(
USA(
cards2ResId = R.drawable.ill_usa_card2_120_106,
cards3ResId = R.drawable.ill_usa_card3_120_106,
batchIds = setOf("AF91"),
batchIds = setOf("AF91", "AF990017"),
),
VeChain(

View file

@ -16,4 +16,7 @@ internal sealed interface WalletDialogConfig {
@Serializable
data object AskForBiometry : WalletDialogConfig
@Serializable
data object AskForPushNotifications : WalletDialogConfig
}

View file

@ -75,7 +75,6 @@ import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.walletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder
import com.tangem.feature.wallet.presentation.wallet.ui.components.PushNotificationsBottomSheet
import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet
import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList
import com.tangem.feature.wallet.presentation.wallet.ui.components.common.*
@ -709,7 +708,6 @@ private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) {
is ChooseAddressBottomSheetConfig -> ChooseAddressBottomSheet(config = bottomSheetConfig)
is BalancesAndLimitsBottomSheetConfig -> BalancesAndLimitsBottomSheet(config = bottomSheetConfig)
is VisaTxDetailsBottomSheetConfig -> VisaTxDetailsBottomSheet(config = bottomSheetConfig)
is PushNotificationsBottomSheetConfig -> PushNotificationsBottomSheet(config = bottomSheetConfig)
is ExpressStatusBottomSheetConfig -> ExpressStatusBottomSheet(config = bottomSheetConfig)
}
}

View file

@ -1,111 +0,0 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.showcase.ShowcaseContent
import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.requestPushPermission
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.PushNotificationsBottomSheetConfig
import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun PushNotificationsBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<PushNotificationsBottomSheetConfig>(config = config) {
PushNotificationsSheetContent(content = it, onDismiss = config.onDismissRequest)
}
}
@Composable
private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetConfig, onDismiss: () -> Unit) {
val requestPushPermission = requestPushPermission(
pushPermission = getPushPermissionOrNull(),
onAllow = {
content.onAllow()
onDismiss()
},
onDeny = {
content.onDeny()
onDismiss()
},
)
Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
ShowcaseContent(
headerIconRes = R.drawable.ic_notifications_unread_24,
headerText = resourceReference(R.string.user_push_notification_agreement_header),
showcaseItems = persistentListOf(
ShowcaseItemModel(
iconRes = R.drawable.ic_rocket_launch_24,
text = resourceReference(R.string.user_push_notification_agreement_argument_one),
),
ShowcaseItemModel(
iconRes = R.drawable.ic_storefront_24,
text = resourceReference(R.string.user_push_notification_agreement_argument_two),
),
),
modifier = Modifier.padding(top = TangemTheme.dimens.spacing40),
)
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
modifier = Modifier.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
top = TangemTheme.dimens.spacing40,
bottom = TangemTheme.dimens.spacing16,
),
) {
SecondaryButton(
text = stringResourceSafe(R.string.common_later),
onClick = {
content.onNeverRequest()
onDismiss()
},
modifier = Modifier.weight(1f),
)
PrimaryButton(
text = stringResourceSafe(R.string.common_allow),
onClick = {
content.onRequest()
requestPushPermission()
},
modifier = Modifier.weight(1f),
)
}
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PushNotificationsSheetContent_Preview() {
TangemThemePreview {
PushNotificationsSheetContent(
PushNotificationsBottomSheetConfig(
onRequest = {},
onNeverRequest = {},
onAllow = {},
onDeny = {},
),
onDismiss = {},
)
}
}
// endregion

View file

@ -7,7 +7,7 @@
tangemBlockchainSdk = "releases-5.27.0-1126"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "releases-5.27.0-507"
tangemCardSdk = "releases-5.27.0-510"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
tangemVico = "2.0.0-alpha.25-tangem12"
#tangemVico = "0.0.1" # Keep it! - used for local builds ^

View file

@ -1,169 +0,0 @@
{
"amplitudeApiKey": "place_your_key_here_if_needed",
"appsFlyer": {
"appsFlyerDevKey": "place_your_key_here_if_needed",
"appsFlyerAppID": "place_your_key_here_if_needed"
},
"blockchairApiKeys": ["place_your_key_here_if_needed"],
"blockchairAuthorizationToken": "",
"blockcypherTokens": [
"place_your_key_here_if_needed",
"place_your_key_here_if_needed",
"place_your_key_here_if_needed"
],
"bscQuiknodeApiKey": "",
"bscQuiknodeSubdomain": "place_your_data_here",
"getBlockAccessTokens": {
"avalanche": {
"jsonRpc": "place_your_key_here_if_needed"
},
"ethereum": {
"jsonRpc": "place_your_key_here_if_needed"
},
"ethereumClassic": {
"jsonRpc": "place_your_key_here_if_needed"
},
"fantom": {
"jsonRpc": "place_your_key_here_if_needed"
},
"rsk": {
"jsonRpc": "place_your_key_here_if_needed"
},
"bsc": {
"jsonRpc": "place_your_key_here_if_needed"
},
"polygon": {
"jsonRpc": "place_your_key_here_if_needed"
},
"xdai": {
"jsonRpc": "place_your_key_here_if_needed"
},
"cronos": {
"jsonRpc": "place_your_key_here_if_needed"
},
"solana": {
"jsonRpc": "place_your_key_here_if_needed"
},
"ton": {
"jsonRpc": "place_your_key_here_if_needed"
},
"tron": {
"rest": "place_your_key_here_if_needed"
},
"cosmos-hub": {
"rest": "place_your_key_here_if_needed"
},
"near": {
"jsonRpc": "place_your_key_here_if_needed"
},
"xrp": {
"jsonRpc": "place_your_key_here_if_needed"
},
"cardano": {
"rosetta": "place_your_key_here_if_needed"
},
"dogecoin": {
"blockBookRest": "place_your_key_here_if_needed",
"jsonRpc": "place_your_key_here_if_needed"
},
"litecoin": {
"blockBookRest": "place_your_key_here_if_needed",
"jsonRpc": "place_your_key_here_if_needed"
},
"dash": {
"blockBookRest": "place_your_key_here_if_needed",
"jsonRpc": "place_your_key_here_if_needed"
},
"bitcoin": {
"blockBookRest": "place_your_key_here_if_needed",
"jsonRpc": "place_your_key_here_if_needed"
},
"aptos": {
"rest": "place_your_key_here_if_needed"
},
"algorand": {
"rest": "place_your_key_here_if_needed"
},
"polygon-zkevm": {
"jsonRpc": "place_your_key_here_if_needed"
},
"zksync": {
"jsonRpc": "place_your_key_here_if_needed"
},
"base": {
"jsonRpc": "place_your_key_here_if_needed"
},
"blast": {
"jsonRpc": "place_your_key_here_if_needed"
},
"filecoin": {
"jsonRpc": "place_your_key_here_if_needed"
},
"arbitrum-one": {
"jsonRpc": "place_your_key_here_if_needed"
},
"bitcoinCash": {
"blockBookRest": "place_your_key_here_if_needed",
"jsonRpc": "place_your_key_here_if_needed"
},
"kusama": {
"jsonRpc": "place_your_key_here_if_needed"
},
"moonbeam": {
"jsonRpc": "place_your_key_here_if_needed"
},
"optimism": {
"jsonRpc": "place_your_key_here_if_needed"
},
"polkadot": {
"jsonRpc": "place_your_key_here_if_needed"
},
"shibarium": {
"jsonRpc": "place_your_key_here_if_needed"
},
"sui": {
"jsonRpc": "place_your_key_here_if_needed"
},
"telos": {
"jsonRpc": "place_your_key_here_if_needed"
},
"tezos": {
"rest": "place_your_key_here_if_needed"
}
},
"kaspaSecondaryApiUrl": "place_your_kaspa_api_here",
"infuraProjectId": "place_your_key_here_if_needed",
"mercuryoSecret": "place_your_key_here_if_needed",
"mercuryoWidgetId": "place_your_key_here_if_needed",
"moonPayApiKey": "place_your_key_here_if_needed",
"moonPayApiSecretKey": "place_your_key_here_if_needed",
"nowNodesApiKey": "place_your_key_here_if_needed",
"tonCenterApiKey": {
"mainnet": "place_your_key_here_if_needed",
"testnet": "place_your_key_here_if_needed"
},
"quiknodeApiKey": "",
"quiknodeSubdomain": "place_your_data_here_if_needed",
"tronGridApiKey": "place_your_key_here_if_needed",
"walletConnectProjectId": "place_your_key_here_if_needed",
"chiaFireAcademyApiKey": "place_your_key_here_if_needed",
"chiaTangemApiKey": "place_your_key_here_if_needed",
"express": {
"apiKey": "place_your_key_here_if_needed",
"signVerifierPublicKey": "place_your_key_here_if_needed"
},
"devExpress": {
"apiKey": "place_your_key_here_if_needed",
"signVerifierPublicKey": "place_your_key_here_if_needed"
},
"hederaArkhiaKey": "place_your_key_here_if_needed",
"polygonScanApiKey": "place_your_key_here_if_needed",
"koinosProApiKey": "place_your_key_here_if_needed",
"stakeKitApiKey": "place_your_key_here_if_needed",
"bittensorDwellirKey": "place_your_key_here_if_needed",
"bittensorOnfinalityKey": "place_your_key_here_if_needed",
"alephiumTangemApiKey": "place_your_key_here_if_needed",
"moralisApiKey": "place_your_key_here_if_needed",
"nftScanApiKey": "place_your_key_here_if_needed",
"blockaidApiKey": "place_your_key_here_if_needed"
}

@ -1 +1 @@
Subproject commit 35539d4e63d1f3ec4ec4f625f7875684ed5033c4
Subproject commit bc4cd430853ca794614b8d5163c9b28b9ca26112