Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-19 11:46:38 +03:00
commit 3efdb25a4e
33 changed files with 452 additions and 26 deletions

View file

@ -0,0 +1,17 @@
package com.tangem.tap.common.settings
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import com.tangem.core.navigation.settings.SettingsManager
internal class IntentSettingsManager(val context: Context) : SettingsManager {
override fun openSettings() {
val openSettingsIntent = Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", context.packageName, null),
)
context.startActivity(openSettingsIntent)
}
}

View file

@ -3,10 +3,12 @@ package com.tangem.tap.di
import android.content.Context
import com.tangem.core.navigation.feedback.FeedbackManager
import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.tap.common.feedback.ProxyFeedbackManager
import com.tangem.tap.common.finisher.AndroidAppFinisher
import com.tangem.tap.common.settings.IntentSettingsManager
import com.tangem.tap.common.share.IntentShareManager
import com.tangem.tap.common.url.CustomTabsUrlOpener
import dagger.Module
@ -35,4 +37,8 @@ internal object UtilsModule {
@Provides
@Singleton
fun provideAppFinisher(@ApplicationContext context: Context): AppFinisher = AndroidAppFinisher(context)
@Provides
@Singleton
fun provideSettingsManager(@ApplicationContext context: Context): SettingsManager = IntentSettingsManager(context)
}

View file

@ -96,17 +96,17 @@ object PreferencesKeys {
fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region")
// region Permission
fun getShouldShowPermission(permission: Int) = booleanPreferencesKey("shouldShowPushPermission_$permission")
fun getShouldShowPermission(permission: String) = booleanPreferencesKey("shouldShowPushPermission_$permission")
fun getShouldShowInitialPermissionScreen(permission: Int) =
fun getShouldShowInitialPermissionScreen(permission: String) =
booleanPreferencesKey("shouldShowInitialPushPermissionScreen_$permission")
fun getIsFirstTimeAskingPermission(permission: Int) =
fun getIsFirstTimeAskingPermission(permission: String) =
booleanPreferencesKey("shouldAskInitialPushPermission_$permission")
fun getPermissionLaunchCount(permission: Int) = intPreferencesKey("pushPermissionLaunchCount_$permission")
fun getPermissionLaunchCount(permission: String) = intPreferencesKey("pushPermissionLaunchCount_$permission")
fun getPermissionDaysCount(permission: Int) = longPreferencesKey("pushPermissionDaysCount_$permission")
fun getPermissionDaysCount(permission: String) = longPreferencesKey("pushPermissionDaysCount_$permission")
// endregion
}

View file

@ -42,5 +42,9 @@
{
"name": "DETAILS_REDESIGN_ENABLED",
"version": "undefined"
},
{
"name": "PUSH_NOTIFICATIONS_ENABLED",
"version": "undefined"
}
]

View file

@ -0,0 +1,5 @@
package com.tangem.core.navigation.settings
class DummySettingsManager : SettingsManager {
override fun openSettings() { /* no-op */ }
}

View file

@ -0,0 +1,5 @@
package com.tangem.core.navigation.settings
interface SettingsManager {
fun openSettings()
}

View file

@ -43,6 +43,7 @@ dependencies {
/** Other libraries */
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.compose.accompanist.permission)
implementation(deps.material)
implementation(deps.compose.shimmer)
implementation(deps.kotlin.immutable.collections)

View file

@ -0,0 +1,55 @@
package com.tangem.core.ui.utils
import android.Manifest
import android.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.permissions.shouldShowRationale
/**
* Returns push permission requester.
* Handles granting permission from app settings.
*/
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun requestPushPermission(
isFirstTimeAsking: Boolean,
isClicked: MutableState<Boolean>,
onAllow: () -> Unit,
onDeny: () -> Unit,
onOpenSettings: () -> Unit,
): () -> Unit {
val permissionState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permission = Manifest.permission.POST_NOTIFICATIONS
val tempPermissionState = rememberPermissionState(permission = permission)
rememberPermissionState(permission = permission) {
when {
it -> onAllow()
!tempPermissionState.status.shouldShowRationale && !isFirstTimeAsking -> onOpenSettings()
else -> onDeny()
}
}
} else {
null
}
// Check if user granted permission and close bottom sheet
LaunchedEffect(key1 = permissionState?.status, isClicked) {
if (!isClicked.value) return@LaunchedEffect
if (permissionState?.status?.isGranted == true) {
onAllow()
} else {
onDeny()
}
}
return if (permissionState == null) {
onOpenSettings
} else {
permissionState::launchPermissionRequest
}
}

View file

@ -16,26 +16,27 @@ internal class DefaultPermissionRepository(
private val appPreferencesStore: AppPreferencesStore,
) : PermissionRepository {
override suspend fun shouldInitiallyShowPermissionScreen(permission: Int): Boolean {
override suspend fun shouldInitiallyShowPermissionScreen(permission: String): Boolean {
val key = getShouldShowInitialPermissionScreen(permission)
val initialPermissionScreen = appPreferencesStore.getSyncOrDefault(key = key, default = true)
if (initialPermissionScreen) appPreferencesStore.store(key = key, value = false)
return initialPermissionScreen
}
override suspend fun isFirstTimeAskingPermission(permission: Int): Boolean = appPreferencesStore.getSyncOrDefault(
key = getIsFirstTimeAskingPermission(permission),
default = true,
)
override suspend fun isFirstTimeAskingPermission(permission: String): Boolean =
appPreferencesStore.getSyncOrDefault(
key = getIsFirstTimeAskingPermission(permission),
default = true,
)
override suspend fun setFirstTimeAskingPermission(permission: Int, value: Boolean) {
override suspend fun setFirstTimeAskingPermission(permission: String, value: Boolean) {
appPreferencesStore.store(
key = getIsFirstTimeAskingPermission(permission),
value = value,
)
}
override suspend fun shouldAskPermission(permission: Int): Boolean {
override suspend fun shouldAskPermission(permission: String): Boolean {
val shouldAskPermission = appPreferencesStore.getSyncOrDefault(getShouldShowPermission(permission), true)
val delayedLaunches = appPreferencesStore.getSyncOrDefault(getPermissionLaunchCount(permission), 0)
val delayedDays = appPreferencesStore.getSyncOrDefault(getPermissionDaysCount(permission), 0)
@ -47,11 +48,11 @@ internal class DefaultPermissionRepository(
return shouldAskPermission && isDaysDelayed && isLaunchesDelayed
}
override suspend fun neverAskPermission(permission: Int) {
override suspend fun neverAskPermission(permission: String) {
appPreferencesStore.store(key = getShouldShowPermission(permission), value = false)
}
override suspend fun delayPermissionAsking(permission: Int) {
override suspend fun delayPermissionAsking(permission: String) {
appPreferencesStore.editData {
val appLaunchCounter = it.getOrDefault(PreferencesKeys.APP_LAUNCH_COUNT_KEY, 0)
val nowMillis = SystemClock.elapsedRealtime()

View file

@ -7,7 +7,7 @@ class DelayPermissionRequestUseCase(
private val repository: PermissionRepository,
) {
suspend operator fun invoke(permission: Int): Either<Throwable, Unit> = Either.catch {
suspend operator fun invoke(permission: String): Either<Throwable, Unit> = Either.catch {
repository.delayPermissionAsking(permission)
}
}

View file

@ -5,7 +5,7 @@ import com.tangem.domain.settings.repositories.PermissionRepository
class IsFirstTimeAskingPermissionUseCase(private val repository: PermissionRepository) {
suspend operator fun invoke(permission: Int): Either<Throwable, Boolean> = Either.catch {
suspend operator fun invoke(permission: String): Either<Throwable, Boolean> = Either.catch {
repository.isFirstTimeAskingPermission(permission)
}
}

View file

@ -7,7 +7,7 @@ class NeverRequestPermissionUseCase(
private val repository: PermissionRepository,
) {
suspend operator fun invoke(permission: Int): Either<Throwable, Unit> = Either.catch {
suspend operator fun invoke(permission: String): Either<Throwable, Unit> = Either.catch {
repository.neverAskPermission(permission)
}
}

View file

@ -5,7 +5,7 @@ import com.tangem.domain.settings.repositories.PermissionRepository
class SetFirstTimeAskingPermissionUseCase(private val repository: PermissionRepository) {
suspend operator fun invoke(permission: Int): Either<Throwable, Unit> = Either.catch {
suspend operator fun invoke(permission: String): Either<Throwable, Unit> = Either.catch {
repository.setFirstTimeAskingPermission(permission, false)
}
}

View file

@ -6,5 +6,5 @@ class ShouldAskPermissionUseCase(
private val repository: PermissionRepository,
) {
suspend operator fun invoke(permission: Int): Boolean = repository.shouldAskPermission(permission)
suspend operator fun invoke(permission: String): Boolean = repository.shouldAskPermission(permission)
}

View file

@ -5,7 +5,7 @@ import com.tangem.domain.settings.repositories.PermissionRepository
class ShouldInitiallyAskPermissionUseCase(private val repository: PermissionRepository) {
suspend operator fun invoke(permission: Int): Either<Throwable, Boolean> = Either.catch {
suspend operator fun invoke(permission: String): Either<Throwable, Boolean> = Either.catch {
repository.shouldInitiallyShowPermissionScreen(permission)
}
}

View file

@ -6,34 +6,34 @@ interface PermissionRepository {
* Return true if should display screen ONCE asking to allow [permission].
* False otherwise or screen already was displayed
*/
suspend fun shouldInitiallyShowPermissionScreen(permission: Int): Boolean
suspend fun shouldInitiallyShowPermissionScreen(permission: String): Boolean
/**
* Indicates which time [permission] was asked via platform dialog.
* NOTE: Use this method to indicate either reroute to settings or display platform dialog.
*/
suspend fun isFirstTimeAskingPermission(permission: Int): Boolean
suspend fun isFirstTimeAskingPermission(permission: String): Boolean
/**
* Sets value indicating that [permission] was asked via platform dialog.
* NOTE: Use this method to indicate either reroute to settings or display platform dialog.
*/
suspend fun setFirstTimeAskingPermission(permission: Int, value: Boolean)
suspend fun setFirstTimeAskingPermission(permission: String, value: Boolean)
/**
* Is clear to ask [permission].
* User could already granted or permanently denied permission
* Or there is an active delay before next request
*/
suspend fun shouldAskPermission(permission: Int): Boolean
suspend fun shouldAskPermission(permission: String): Boolean
/**
* Permanently deny [permission] and never request again
*/
suspend fun neverAskPermission(permission: Int)
suspend fun neverAskPermission(permission: String)
/**
* Delay next [permission] request for some time or active sessions
*/
suspend fun delayPermissionAsking(permission: Int)
suspend fun delayPermissionAsking(permission: String)
}

View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,15 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.features.pushnotifications.api"
}
dependencies {
/** AndroidX */
implementation(deps.androidx.fragment.ktx)
}

View file

@ -0,0 +1,9 @@
package com.tangem.features.pushnotifications.api.featuretoggles
/**
* Push notifications feature toggles
*/
interface PushNotificationsFeatureToggles {
/** Availability of push notifications */
val isRedesignedSendEnabled: Boolean
}

View file

@ -0,0 +1,8 @@
package com.tangem.features.pushnotifications.api.navigation
import androidx.fragment.app.Fragment
interface PushNotificationsRouter {
fun entryFragment(): Fragment
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.pushnotifications.api.utils
import android.Manifest
import android.os.Build
val PUSH_PERMISSION = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.POST_NOTIFICATIONS
} else {
"android.permission.POST_NOTIFICATIONS"
}

View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,45 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.feature.pushnotifications.impl"
}
dependencies {
/** AndroidX */
implementation(deps.androidx.fragment.ktx)
implementation(deps.androidx.activity.compose)
/** Compose */
implementation(deps.compose.foundation)
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.compose.accompanist.permission)
/** Other dependencies */
implementation(deps.timber)
implementation(deps.arrow.core)
implementation(deps.kotlin.immutable.collections)
/** Core modules */
implementation(projects.core.ui)
implementation(projects.core.featuretoggles)
/** Common modules */
implementation(projects.common.routing)
/** Domain module */
implementation(projects.domain.settings)
/** Feature modules */
implementation(projects.features.pushNotifications.api)
/** DI */
implementation(deps.hilt.android)
implementation(project(":core:navigation"))
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,39 @@
package com.tangem.features.pushnotifications.impl
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.features.pushnotifications.impl.presentation.ui.PushNotificationsScreen
import com.tangem.features.pushnotifications.impl.presentation.viewmodel.PushNotificationViewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class PushNotificationsFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
private val viewModel by viewModels<PushNotificationViewModel>()
@Composable
override fun ScreenContent(modifier: Modifier) {
BackHandler(onBack = requireActivity()::finish)
NavigationBar3ButtonsScrim()
PushNotificationsScreen(
onShowAllow = viewModel::onAllowPermission,
onAllow = viewModel::onAllowedPermission,
onLater = viewModel::onAskLater,
onOpenSettings = viewModel::openSettings,
)
}
companion object {
/** Create push notifications fragment instance */
fun create(): PushNotificationsFragment = PushNotificationsFragment()
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.features.pushnotifications.impl.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.features.pushnotifications.impl.featuretoggles.DefaultPushNotificationsFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
* DI module provides implementation of [PushNotificationsFeatureToggles]
*/
@Module
@InstallIn(SingletonComponent::class)
internal object PushNotificationsFeatureTogglesModule {
@Provides
@Singleton
fun provideSendFeatureToggles(featureTogglesManager: FeatureTogglesManager): PushNotificationsFeatureToggles {
return DefaultPushNotificationsFeatureToggles(featureTogglesManager = featureTogglesManager)
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.features.pushnotifications.impl.di
import com.tangem.common.routing.AppRouter
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
import com.tangem.features.pushnotifications.impl.navigation.DefaultPushNotificationsRouter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.scopes.ActivityScoped
/**
* DI module provides implementation of [PushNotificationsRouter]
*/
@Module
@InstallIn(ActivityComponent::class)
object PushNotificationsModule {
@Provides
@ActivityScoped
fun provideDisclaimerRouter(appRouter: AppRouter): PushNotificationsRouter {
return DefaultPushNotificationsRouter(appRouter)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.pushnotifications.impl.featuretoggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
internal class DefaultPushNotificationsFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : PushNotificationsFeatureToggles {
override val isRedesignedSendEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("PUSH_NOTIFICATIONS_ENABLED")
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.pushnotifications.impl.navigation
import androidx.fragment.app.Fragment
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.features.pushnotifications.impl.PushNotificationsFragment
import javax.inject.Inject
internal class DefaultPushNotificationsRouter @Inject constructor(
private val appRouter: AppRouter,
) : InnerPushNotificationsRouter {
override fun entryFragment(): Fragment = PushNotificationsFragment.create()
override fun openHome() {
appRouter.push(AppRoute.Home)
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.features.pushnotifications.impl.navigation
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
interface InnerPushNotificationsRouter : PushNotificationsRouter {
fun openHome()
}

View file

@ -0,0 +1,59 @@
package com.tangem.features.pushnotifications.impl.presentation.ui
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.feature.pushnotifications.impl.R
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun PushNotificationsScreen(
onShowAllow: () -> Unit,
onAllow: () -> Unit,
onLater: () -> Unit,
onOpenSettings: () -> Unit,
) {
val isClicked = remember { mutableStateOf(false) }
val requestPushPermission = requestPushPermission(
isFirstTimeAsking = true,
isClicked = isClicked,
onAllow = onAllow,
onDeny = onLater,
onOpenSettings = onOpenSettings,
)
Showcase(
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),
),
),
primaryButton = ShowcaseButtonModel(
buttonText = resourceReference(R.string.common_allow),
onClick = {
isClicked.value = true
onShowAllow()
requestPushPermission()
},
),
secondaryButton = ShowcaseButtonModel(
buttonText = resourceReference(R.string.common_later),
onClick = onLater,
),
modifier = Modifier.systemBarsPadding(),
)
}

View file

@ -0,0 +1,46 @@
package com.tangem.features.pushnotifications.impl.presentation.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.domain.settings.DelayPermissionRequestUseCase
import com.tangem.domain.settings.NeverRequestPermissionUseCase
import com.tangem.domain.settings.SetFirstTimeAskingPermissionUseCase
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.pushnotifications.impl.navigation.DefaultPushNotificationsRouter
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
internal class PushNotificationViewModel @Inject constructor(
private val setFirstTimeAskingPermissionUseCase: SetFirstTimeAskingPermissionUseCase,
private val delayPermissionRequestUseCase: DelayPermissionRequestUseCase,
private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase,
private val router: DefaultPushNotificationsRouter,
private val settingsManager: SettingsManager,
) : ViewModel(), PushNotificationsClickIntents {
override fun onAskLater() {
viewModelScope.launch {
delayPermissionRequestUseCase(PUSH_PERMISSION)
setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION)
}
router.openHome()
}
override fun onAllowPermission() {
viewModelScope.launch {
setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION)
}
}
override fun onAllowedPermission() {
viewModelScope.launch {
neverRequestPermissionUseCase(PUSH_PERMISSION)
}
router.openHome()
}
override fun openSettings() = settingsManager.openSettings()
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.pushnotifications.impl.presentation.viewmodel
internal interface PushNotificationsClickIntents {
fun onAskLater()
fun onAllowPermission()
fun onAllowedPermission()
fun openSettings()
}

View file

@ -180,6 +180,9 @@ include(":features:details:impl")
include(":features:disclaimer:api")
include(":features:disclaimer:impl")
include(":features:push-notifications:api")
include(":features:push-notifications:impl")
// endregion Feature modules
// region Domain modules