Initial commit: Pezkuwi Wallet Android

Complete rebrand of Nova Wallet for Pezkuwichain ecosystem.

## Features
- Full Pezkuwichain support (HEZ & PEZ tokens)
- Polkadot ecosystem compatibility
- Staking, Governance, DeFi, NFTs
- XCM cross-chain transfers
- Hardware wallet support (Ledger, Polkadot Vault)
- WalletConnect v2
- Push notifications

## Languages
- English, Turkish, Kurmanci (Kurdish), Spanish, French, German, Russian, Japanese, Chinese, Korean, Portuguese, Vietnamese

Based on Nova Wallet by Novasama Technologies GmbH
© Dijital Kurdistan Tech Institute 2026
This commit is contained in:
2026-01-23 01:31:12 +03:00
commit 31c8c5995f
7621 changed files with 425838 additions and 0 deletions
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,71 @@
package io.novafoundation.nova.feature_push_notifications
import com.google.firebase.messaging.FirebaseMessaging
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.feature_push_notifications.data.PushNotificationsService
import io.novafoundation.nova.feature_push_notifications.di.PushNotificationsFeatureApi
import io.novafoundation.nova.feature_push_notifications.di.PushNotificationsFeatureComponent
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NotificationHandler
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import kotlin.coroutines.CoroutineContext
class NovaFirebaseMessagingService : FirebaseMessagingService(), CoroutineScope {
override val coroutineContext: CoroutineContext = Dispatchers.Default + SupervisorJob()
@Inject
lateinit var pushNotificationsService: PushNotificationsService
@Inject
lateinit var notificationHandler: NotificationHandler
override fun onCreate() {
super.onCreate()
injectDependencies()
}
override fun onNewToken(token: String) {
pushNotificationsService.onTokenUpdated(token)
}
override fun onMessageReceived(message: RemoteMessage) {
launch {
notificationHandler.handleNotification(message)
}
}
override fun onDestroy() {
super.onDestroy()
coroutineContext.cancel()
}
private fun injectDependencies() {
FeatureUtils.getFeature<PushNotificationsFeatureComponent>(this, PushNotificationsFeatureApi::class.java)
.inject(this)
}
companion object {
suspend fun getToken(): String? {
return runCatching { FirebaseMessaging.getInstance().token.await() }.getOrNull()
}
suspend fun requestToken(): String {
return FirebaseMessaging.getInstance().token.await()
}
suspend fun deleteToken() {
FirebaseMessaging.getInstance().deleteToken()
}
}
}
@@ -0,0 +1,15 @@
package io.novafoundation.nova.feature_push_notifications
import android.os.Bundle
import io.novafoundation.nova.common.navigation.ReturnableRouter
interface PushNotificationsRouter : ReturnableRouter {
fun openPushSettingsWithAccounts()
fun openPushMultisigsSettings(args: Bundle)
fun openPushGovernanceSettings(args: Bundle)
fun openPushStakingSettings(args: Bundle)
}
@@ -0,0 +1,15 @@
package io.novafoundation.nova.feature_push_notifications.data
object NotificationTypes {
const val GOV_NEW_REF = "govNewRef"
const val GOV_STATE = "govState"
const val STAKING_REWARD = "stakingReward"
const val TOKENS_SENT = "tokenSent"
const val TOKENS_RECEIVED = "tokenReceived"
const val APP_NEW_RELEASE = "appNewRelease"
const val NEW_MULTISIG = "newMultisig"
const val MULTISIG_APPROVAL = "multisigApproval"
const val MULTISIG_EXECUTED = "multisigExecuted"
const val MULTISIG_CANCELLED = "multisigCancelled"
}
@@ -0,0 +1,7 @@
package io.novafoundation.nova.feature_push_notifications.data
enum class PushNotificationsAvailabilityState {
AVAILABLE,
GOOGLE_PLAY_INSTALLATION_REQUIRED,
PLAY_SERVICES_REQUIRED
}
@@ -0,0 +1,187 @@
package io.novafoundation.nova.feature_push_notifications.data
import com.google.firebase.messaging.messaging
import android.util.Log
import com.google.android.gms.tasks.OnCompleteListener
import com.google.firebase.Firebase
import com.google.firebase.messaging.FirebaseMessaging
import io.novafoundation.nova.common.data.GoogleApiAvailabilityProvider
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.common.interfaces.BuildTypeProvider
import io.novafoundation.nova.common.interfaces.isMarketRelease
import io.novafoundation.nova.common.utils.coroutines.RootScope
import io.novafoundation.nova.feature_push_notifications.BuildConfig
import io.novafoundation.nova.feature_push_notifications.NovaFirebaseMessagingService
import io.novafoundation.nova.feature_push_notifications.domain.model.PushSettings
import io.novafoundation.nova.feature_push_notifications.data.settings.PushSettingsProvider
import io.novafoundation.nova.feature_push_notifications.data.subscription.PushSubscriptionService
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
const val PUSH_LOG_TAG = "NOVA_PUSH"
private const val PREFS_LAST_SYNC_TIME = "PREFS_LAST_SYNC_TIME"
private const val MIN_DAYS_TO_START_SYNC = 1
private val SAVING_TIMEOUT = 15.seconds
interface PushNotificationsService {
fun onTokenUpdated(token: String)
fun isPushNotificationsEnabled(): Boolean
fun pushNotificationsAvaiabilityState(): PushNotificationsAvailabilityState
suspend fun initPushNotifications(): Result<Unit>
suspend fun updatePushSettings(enabled: Boolean, pushSettings: PushSettings?): Result<Unit>
fun isPushNotificationsAvailable(): Boolean
suspend fun syncSettingsIfNeeded()
}
class RealPushNotificationsService(
private val settingsProvider: PushSettingsProvider,
private val subscriptionService: PushSubscriptionService,
private val rootScope: RootScope,
private val tokenCache: PushTokenCache,
private val googleApiAvailabilityProvider: GoogleApiAvailabilityProvider,
private val pushPermissionRepository: PushPermissionRepository,
private val preferences: Preferences,
private val buildTypeProvider: BuildTypeProvider
) : PushNotificationsService {
// Using to manually sync subscriptions (firestore, topics) after enabling push notifications
private var skipTokenReceivingCallback = false
init {
logToken()
}
override fun onTokenUpdated(token: String) {
if (!isPushNotificationsAvailable()) return
if (!isPushNotificationsEnabled()) return
if (skipTokenReceivingCallback) return
logToken()
rootScope.launch {
tokenCache.updatePushToken(token)
updatePushSettings(isPushNotificationsEnabled(), settingsProvider.getPushSettings())
}
}
override suspend fun updatePushSettings(enabled: Boolean, pushSettings: PushSettings?): Result<Unit> {
if (!isPushNotificationsAvailable()) return googleApiFailureResult()
return runCatching {
withTimeout(SAVING_TIMEOUT) {
handlePushTokenIfNeeded(enabled)
val pushToken = getPushToken()
val oldSettings = settingsProvider.getPushSettings()
subscriptionService.handleSubscription(enabled, pushToken, oldSettings, pushSettings)
settingsProvider.setPushNotificationsEnabled(enabled)
settingsProvider.updateSettings(pushSettings)
updateLastSyncTime()
}
}
}
override fun isPushNotificationsAvailable(): Boolean {
return pushNotificationsAvaiabilityState() == PushNotificationsAvailabilityState.AVAILABLE
}
override fun pushNotificationsAvaiabilityState(): PushNotificationsAvailabilityState {
return when {
!googleApiAvailabilityProvider.isAvailable() -> PushNotificationsAvailabilityState.PLAY_SERVICES_REQUIRED
!BuildConfig.DEBUG && !buildTypeProvider.isMarketRelease() -> PushNotificationsAvailabilityState.GOOGLE_PLAY_INSTALLATION_REQUIRED
else -> PushNotificationsAvailabilityState.AVAILABLE
}
}
override suspend fun syncSettingsIfNeeded() {
if (!isPushNotificationsEnabled()) return
if (!isPushNotificationsAvailable()) return
if (isPermissionsRevoked() || isTimeToSync()) {
val isPermissionGranted = pushPermissionRepository.isPermissionGranted()
updatePushSettings(isPermissionGranted, settingsProvider.getPushSettings())
}
}
override fun isPushNotificationsEnabled(): Boolean {
return settingsProvider.isPushNotificationsEnabled()
}
override suspend fun initPushNotifications(): Result<Unit> {
if (!isPushNotificationsAvailable()) return googleApiFailureResult()
return updatePushSettings(true, settingsProvider.getDefaultPushSettings())
}
private suspend fun handlePushTokenIfNeeded(isEnable: Boolean) {
if (!isPushNotificationsAvailable()) return
if (isEnable == isPushNotificationsEnabled()) return
skipTokenReceivingCallback = true
val pushToken = if (isEnable) {
NovaFirebaseMessagingService.requestToken()
} else {
NovaFirebaseMessagingService.deleteToken()
null
}
tokenCache.updatePushToken(pushToken)
Firebase.messaging.isAutoInitEnabled = isEnable
skipTokenReceivingCallback = false
}
private fun getPushToken(): String? {
return tokenCache.getPushToken()
}
private fun logToken() {
if (!isPushNotificationsEnabled()) return
if (!BuildConfig.DEBUG) return
if (!isPushNotificationsAvailable()) return
FirebaseMessaging.getInstance().token.addOnCompleteListener(
OnCompleteListener { task ->
if (!task.isSuccessful) {
return@OnCompleteListener
}
Log.d(PUSH_LOG_TAG, "FCM token: ${task.result}")
}
)
}
private fun isPermissionsRevoked(): Boolean {
return !pushPermissionRepository.isPermissionGranted()
}
private fun isTimeToSync(): Boolean {
if (!isPushNotificationsEnabled()) return false
val lastSyncTime = getLastSyncTimeIfPushEnabled()
val deltaTimeBetweenNowAndLastSync = System.currentTimeMillis() - lastSyncTime
val wholeDays = deltaTimeBetweenNowAndLastSync.milliseconds.inWholeDays
return wholeDays >= MIN_DAYS_TO_START_SYNC
}
private fun updateLastSyncTime() {
preferences.putLong(PREFS_LAST_SYNC_TIME, System.currentTimeMillis())
}
private fun getLastSyncTimeIfPushEnabled(): Long {
return preferences.getLong(PREFS_LAST_SYNC_TIME, 0)
}
private fun googleApiFailureResult(): Result<Unit> {
return Result.failure(IllegalStateException("Google API is not available"))
}
}
@@ -0,0 +1,25 @@
package io.novafoundation.nova.feature_push_notifications.data
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.ContextCompat
interface PushPermissionRepository {
fun isPermissionGranted(): Boolean
}
class RealPushPermissionRepository(
private val context: Context
) : PushPermissionRepository {
override fun isPermissionGranted(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
} else {
true
}
}
}
@@ -0,0 +1,25 @@
package io.novafoundation.nova.feature_push_notifications.data
import io.novafoundation.nova.common.data.storage.Preferences
private const val PUSH_TOKEN_KEY = "push_token"
interface PushTokenCache {
fun getPushToken(): String?
fun updatePushToken(pushToken: String?)
}
class RealPushTokenCache(
private val preferences: Preferences
) : PushTokenCache {
override fun getPushToken(): String? {
return preferences.getString(PUSH_TOKEN_KEY)
}
override fun updatePushToken(pushToken: String?) {
preferences.putString(PUSH_TOKEN_KEY, pushToken)
}
}
@@ -0,0 +1,39 @@
package io.novafoundation.nova.feature_push_notifications.data.repository
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.feature_push_notifications.domain.interactor.AllowingState
interface MultisigPushAlertRepository {
fun isMultisigsPushAlertWasShown(): Boolean
fun setMultisigsPushAlertWasShown()
fun showAlertAtStartAllowingState(): AllowingState
fun setAlertAtStartAllowingState(state: AllowingState)
}
private const val IS_MULTISIGS_PUSH_ALERT_WAS_SHOWN = "IS_MULTISIGS_PUSH_ALERT_WAS_SHOWN"
private const val MULTISIGS_PUSH_ALERT_ALLOWING_STATE = "MULTISIGS_PUSH_ALERT_ALLOWING_STATE"
class RealMultisigPushAlertRepository(
private val preferences: Preferences
) : MultisigPushAlertRepository {
override fun isMultisigsPushAlertWasShown(): Boolean {
return preferences.getBoolean(IS_MULTISIGS_PUSH_ALERT_WAS_SHOWN, false)
}
override fun setMultisigsPushAlertWasShown() {
preferences.putBoolean(IS_MULTISIGS_PUSH_ALERT_WAS_SHOWN, true)
}
override fun showAlertAtStartAllowingState(): AllowingState {
val state = preferences.getString(MULTISIGS_PUSH_ALERT_ALLOWING_STATE, AllowingState.INITIAL.toString())
return AllowingState.valueOf(state)
}
override fun setAlertAtStartAllowingState(state: AllowingState) {
preferences.putString(MULTISIGS_PUSH_ALERT_ALLOWING_STATE, state.toString())
}
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_push_notifications.data.repository
import io.novafoundation.nova.common.data.storage.Preferences
interface PushSettingsRepository {
fun isMultisigsWasEnabledFirstTime(): Boolean
fun setMultisigsWasEnabledFirstTime()
}
private const val IS_MULTISIG_WAS_ENABLED_FIRST_TIME = "IS_MULTISIG_WAS_ENABLED_FIRST_TIME"
class RealPushSettingsRepository(
private val preferences: Preferences
) : PushSettingsRepository {
override fun isMultisigsWasEnabledFirstTime(): Boolean {
return preferences.getBoolean(IS_MULTISIG_WAS_ENABLED_FIRST_TIME, false)
}
override fun setMultisigsWasEnabledFirstTime() {
preferences.putBoolean(IS_MULTISIG_WAS_ENABLED_FIRST_TIME, true)
}
}
@@ -0,0 +1,19 @@
package io.novafoundation.nova.feature_push_notifications.data.settings
import io.novafoundation.nova.feature_push_notifications.domain.model.PushSettings
import kotlinx.coroutines.flow.Flow
interface PushSettingsProvider {
suspend fun getPushSettings(): PushSettings
suspend fun getDefaultPushSettings(): PushSettings
fun updateSettings(pushWalletSettings: PushSettings?)
fun setPushNotificationsEnabled(isEnabled: Boolean)
fun isPushNotificationsEnabled(): Boolean
fun pushEnabledFlow(): Flow<Boolean>
}
@@ -0,0 +1,12 @@
package io.novafoundation.nova.feature_push_notifications.data.settings
import com.google.gson.GsonBuilder
import io.novafoundation.nova.common.utils.gson.SealedTypeAdapterFactory
import io.novafoundation.nova.feature_push_notifications.data.settings.model.chain.ChainFeatureCacheV1
object PushSettingsSerializer {
fun gson() = GsonBuilder()
.registerTypeAdapterFactory(SealedTypeAdapterFactory.of(ChainFeatureCacheV1::class))
.create()
}
@@ -0,0 +1,75 @@
package io.novafoundation.nova.feature_push_notifications.data.settings
import com.google.gson.Gson
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_push_notifications.data.settings.model.PushSettingsCacheV1
import io.novafoundation.nova.feature_push_notifications.data.settings.model.PushSettingsCacheV2
import io.novafoundation.nova.feature_push_notifications.data.settings.model.VersionedPushSettingsCache
import io.novafoundation.nova.feature_push_notifications.data.settings.model.toCache
import io.novafoundation.nova.feature_push_notifications.domain.model.PushSettings
import kotlinx.coroutines.flow.Flow
private const val PUSH_SETTINGS_KEY = "push_settings"
private const val PREFS_PUSH_NOTIFICATIONS_ENABLED = "push_notifications_enabled"
class RealPushSettingsProvider(
private val gson: Gson,
private val prefs: Preferences,
private val accountRepository: AccountRepository
) : PushSettingsProvider {
override suspend fun getPushSettings(): PushSettings {
return prefs.getString(PUSH_SETTINGS_KEY)
?.let {
gson.fromJson(it, VersionedPushSettingsCache::class.java)
.toPushSettings()
} ?: getDefaultPushSettings()
}
override suspend fun getDefaultPushSettings(): PushSettings {
return PushSettings(
announcementsEnabled = true,
sentTokensEnabled = true,
receivedTokensEnabled = true,
subscribedMetaAccounts = setOf(accountRepository.getSelectedMetaAccount().id),
stakingReward = PushSettings.ChainFeature.All,
governance = emptyMap(),
multisigs = PushSettings.MultisigsState.disabled()
)
}
override fun updateSettings(pushWalletSettings: PushSettings?) {
val versionedCache = pushWalletSettings?.toCache()
?.toVersionedPushSettingsCache()
prefs.putString(PUSH_SETTINGS_KEY, versionedCache?.let(gson::toJson))
}
override fun setPushNotificationsEnabled(isEnabled: Boolean) {
prefs.putBoolean(PREFS_PUSH_NOTIFICATIONS_ENABLED, isEnabled)
}
override fun isPushNotificationsEnabled(): Boolean {
return prefs.getBoolean(PREFS_PUSH_NOTIFICATIONS_ENABLED, false)
}
override fun pushEnabledFlow(): Flow<Boolean> {
return prefs.booleanFlow(PREFS_PUSH_NOTIFICATIONS_ENABLED, false)
}
fun PushSettingsCacheV2.toVersionedPushSettingsCache(): VersionedPushSettingsCache {
return VersionedPushSettingsCache(
version = version,
settings = gson.toJson(this)
)
}
fun VersionedPushSettingsCache.toPushSettings(): PushSettings {
return when (version) {
PushSettingsCacheV1.VERSION -> gson.fromJson(settings, PushSettingsCacheV1::class.java)
PushSettingsCacheV2.VERSION -> gson.fromJson(settings, PushSettingsCacheV2::class.java)
else -> throw IllegalStateException("Unknown push settings version: $version")
}.toPushSettings()
}
}
@@ -0,0 +1,44 @@
package io.novafoundation.nova.feature_push_notifications.data.settings.model
import io.novafoundation.nova.feature_push_notifications.data.settings.model.chain.ChainFeatureCacheV1
import io.novafoundation.nova.feature_push_notifications.data.settings.model.governance.GovernanceStateCacheV1
import io.novafoundation.nova.feature_push_notifications.data.settings.model.governance.MultisigsStateCacheV1
import io.novafoundation.nova.feature_push_notifications.domain.model.PushSettings
fun PushSettings.toCache(): PushSettingsCacheV2 {
return PushSettingsCacheV2(
announcementsEnabled = announcementsEnabled,
sentTokensEnabled = sentTokensEnabled,
receivedTokensEnabled = receivedTokensEnabled,
subscribedMetaAccounts = subscribedMetaAccounts,
stakingReward = stakingReward.toCache(),
governance = governance.mapValues { (_, value) -> value.toCache() },
multisigs = multisigs.toCache()
)
}
fun PushSettings.ChainFeature.toCache(): ChainFeatureCacheV1 {
return when (this) {
is PushSettings.ChainFeature.All -> ChainFeatureCacheV1.All
is PushSettings.ChainFeature.Concrete -> ChainFeatureCacheV1.Concrete(chainIds)
}
}
fun PushSettings.GovernanceState.toCache(): GovernanceStateCacheV1 {
return GovernanceStateCacheV1(
newReferendaEnabled = newReferendaEnabled,
referendumUpdateEnabled = referendumUpdateEnabled,
govMyDelegateVotedEnabled = govMyDelegateVotedEnabled,
tracks = tracks
)
}
fun PushSettings.MultisigsState.toCache(): MultisigsStateCacheV1 {
return MultisigsStateCacheV1(
isEnabled = isEnabled,
isInitialNotificationsEnabled = isInitiatingEnabled,
isApprovalNotificationsEnabled = isApprovingEnabled,
isExecutionNotificationsEnabled = isExecutionEnabled,
isRejectionNotificationsEnabled = isRejectionEnabled
)
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_push_notifications.data.settings.model
import io.novafoundation.nova.feature_push_notifications.domain.model.PushSettings
interface PushSettingsCache {
val version: String
fun toPushSettings(): PushSettings
}
@@ -0,0 +1,36 @@
package io.novafoundation.nova.feature_push_notifications.data.settings.model
import io.novafoundation.nova.feature_push_notifications.data.settings.model.chain.ChainFeatureCacheV1
import io.novafoundation.nova.feature_push_notifications.data.settings.model.chain.toDomain
import io.novafoundation.nova.feature_push_notifications.data.settings.model.governance.GovernanceStateCacheV1
import io.novafoundation.nova.feature_push_notifications.data.settings.model.governance.toDomain
import io.novafoundation.nova.feature_push_notifications.domain.model.PushSettings
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
class PushSettingsCacheV1(
val announcementsEnabled: Boolean,
val sentTokensEnabled: Boolean,
val receivedTokensEnabled: Boolean,
val subscribedMetaAccounts: Set<Long>,
val stakingReward: ChainFeatureCacheV1,
val governance: Map<ChainId, GovernanceStateCacheV1>
) : PushSettingsCache {
companion object {
const val VERSION = "V1"
}
override val version: String = VERSION
override fun toPushSettings(): PushSettings {
return PushSettings(
announcementsEnabled = announcementsEnabled,
sentTokensEnabled = sentTokensEnabled,
receivedTokensEnabled = receivedTokensEnabled,
subscribedMetaAccounts = subscribedMetaAccounts,
stakingReward = stakingReward.toDomain(),
governance = governance.mapValues { (_, value) -> value.toDomain() },
multisigs = PushSettings.MultisigsState.disabled()
)
}
}
@@ -0,0 +1,38 @@
package io.novafoundation.nova.feature_push_notifications.data.settings.model
import io.novafoundation.nova.feature_push_notifications.data.settings.model.chain.ChainFeatureCacheV1
import io.novafoundation.nova.feature_push_notifications.data.settings.model.chain.toDomain
import io.novafoundation.nova.feature_push_notifications.data.settings.model.governance.GovernanceStateCacheV1
import io.novafoundation.nova.feature_push_notifications.data.settings.model.governance.MultisigsStateCacheV1
import io.novafoundation.nova.feature_push_notifications.data.settings.model.governance.toDomain
import io.novafoundation.nova.feature_push_notifications.domain.model.PushSettings
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
class PushSettingsCacheV2(
val announcementsEnabled: Boolean,
val sentTokensEnabled: Boolean,
val receivedTokensEnabled: Boolean,
val subscribedMetaAccounts: Set<Long>,
val stakingReward: ChainFeatureCacheV1,
val governance: Map<ChainId, GovernanceStateCacheV1>,
val multisigs: MultisigsStateCacheV1
) : PushSettingsCache {
companion object {
const val VERSION = "V2"
}
override val version: String = VERSION
override fun toPushSettings(): PushSettings {
return PushSettings(
announcementsEnabled = announcementsEnabled,
sentTokensEnabled = sentTokensEnabled,
receivedTokensEnabled = receivedTokensEnabled,
subscribedMetaAccounts = subscribedMetaAccounts,
stakingReward = stakingReward.toDomain(),
governance = governance.mapValues { (_, value) -> value.toDomain() },
multisigs = multisigs.toDomain()
)
}
}
@@ -0,0 +1,8 @@
package io.novafoundation.nova.feature_push_notifications.data.settings.model
typealias Json = String
class VersionedPushSettingsCache(
val version: String,
val settings: Json
)
@@ -0,0 +1,18 @@
package io.novafoundation.nova.feature_push_notifications.data.settings.model.chain
import io.novafoundation.nova.feature_push_notifications.domain.model.PushSettings
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
sealed class ChainFeatureCacheV1 {
object All : ChainFeatureCacheV1()
data class Concrete(val chainIds: List<ChainId>) : ChainFeatureCacheV1()
}
fun ChainFeatureCacheV1.toDomain(): PushSettings.ChainFeature {
return when (this) {
is ChainFeatureCacheV1.All -> PushSettings.ChainFeature.All
is ChainFeatureCacheV1.Concrete -> PushSettings.ChainFeature.Concrete(chainIds)
}
}
@@ -0,0 +1,20 @@
package io.novafoundation.nova.feature_push_notifications.data.settings.model.governance
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId
import io.novafoundation.nova.feature_push_notifications.domain.model.PushSettings
class GovernanceStateCacheV1(
val newReferendaEnabled: Boolean,
val referendumUpdateEnabled: Boolean,
val govMyDelegateVotedEnabled: Boolean,
val tracks: Set<TrackId>
)
fun GovernanceStateCacheV1.toDomain(): PushSettings.GovernanceState {
return PushSettings.GovernanceState(
newReferendaEnabled = newReferendaEnabled,
referendumUpdateEnabled = referendumUpdateEnabled,
govMyDelegateVotedEnabled = govMyDelegateVotedEnabled,
tracks = tracks
)
}
@@ -0,0 +1,21 @@
package io.novafoundation.nova.feature_push_notifications.data.settings.model.governance
import io.novafoundation.nova.feature_push_notifications.domain.model.PushSettings
class MultisigsStateCacheV1(
val isEnabled: Boolean, // General notifications state. Other states may be enabled to save state when general one is disabled
val isInitialNotificationsEnabled: Boolean,
val isApprovalNotificationsEnabled: Boolean,
val isExecutionNotificationsEnabled: Boolean,
val isRejectionNotificationsEnabled: Boolean
)
fun MultisigsStateCacheV1.toDomain(): PushSettings.MultisigsState {
return PushSettings.MultisigsState(
isEnabled = isEnabled,
isInitiatingEnabled = isInitialNotificationsEnabled,
isApprovingEnabled = isApprovalNotificationsEnabled,
isExecutionEnabled = isExecutionNotificationsEnabled,
isRejectionEnabled = isRejectionNotificationsEnabled
)
}
@@ -0,0 +1,8 @@
package io.novafoundation.nova.feature_push_notifications.data.subscription
import io.novafoundation.nova.feature_push_notifications.domain.model.PushSettings
interface PushSubscriptionService {
suspend fun handleSubscription(pushEnabled: Boolean, token: String?, oldSettings: PushSettings, newSettings: PushSettings?)
}
@@ -0,0 +1,227 @@
package io.novafoundation.nova.feature_push_notifications.data.subscription
import android.util.Log
import com.google.firebase.Firebase
import com.google.firebase.firestore.firestore
import com.google.firebase.messaging.messaging
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.common.utils.CollectionDiffer
import io.novafoundation.nova.common.utils.Identifiable
import io.novafoundation.nova.common.utils.formatting.formatDateISO_8601_NoMs
import io.novafoundation.nova.common.utils.mapOfNotNullValues
import io.novafoundation.nova.common.utils.mapValuesNotNull
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.defaultSubstrateAddress
import io.novafoundation.nova.feature_account_api.domain.model.mainEthereumAddress
import io.novafoundation.nova.feature_push_notifications.BuildConfig
import io.novafoundation.nova.common.data.GoogleApiAvailabilityProvider
import io.novafoundation.nova.feature_push_notifications.data.PUSH_LOG_TAG
import io.novafoundation.nova.feature_push_notifications.domain.model.PushSettings
import io.novafoundation.nova.feature_push_notifications.domain.model.isApprovingEnabledTotal
import io.novafoundation.nova.feature_push_notifications.domain.model.isExecutionEnabledTotal
import io.novafoundation.nova.feature_push_notifications.domain.model.isInitiatingEnabledTotal
import io.novafoundation.nova.feature_push_notifications.domain.model.isRejectionEnabledTotal
import io.novafoundation.nova.runtime.ext.addressOf
import io.novafoundation.nova.runtime.ext.chainIdHexPrefix16
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.ChainsById
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.multiNetwork.chainsById
import java.math.BigInteger
import java.util.UUID
import java.util.Date
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.tasks.await
private const val COLLECTION_NAME = "users"
private const val PREFS_FIRESTORE_UUID = "firestore_uuid"
private const val GOV_STATE_TOPIC_NAME = "govState"
private const val NEW_REFERENDA_TOPIC_NAME = "govNewRef"
class TrackIdentifiable(val chainId: ChainId, val track: BigInteger) : Identifiable {
override val identifier: String = "$chainId:$track"
}
class RealPushSubscriptionService(
private val prefs: Preferences,
private val chainRegistry: ChainRegistry,
private val googleApiAvailabilityProvider: GoogleApiAvailabilityProvider,
private val accountRepository: AccountRepository
) : PushSubscriptionService {
private val generateIdMutex = Mutex()
override suspend fun handleSubscription(pushEnabled: Boolean, token: String?, oldSettings: PushSettings, newSettings: PushSettings?) {
if (!googleApiAvailabilityProvider.isAvailable()) return
val tokenExist = token != null
if (pushEnabled != tokenExist) throw IllegalStateException("Token should exist to enable push notifications")
handleTopics(pushEnabled, oldSettings, newSettings)
handleFirestore(token, newSettings)
if (BuildConfig.DEBUG) {
Log.d(PUSH_LOG_TAG, "Firestore user updated: ${getFirestoreUUID()}")
}
}
private suspend fun getFirestoreUUID(): String {
return generateIdMutex.withLock {
var uuid = prefs.getString(PREFS_FIRESTORE_UUID)
if (uuid == null) {
uuid = UUID.randomUUID().toString()
prefs.putString(PREFS_FIRESTORE_UUID, uuid)
}
uuid
}
}
private suspend fun handleFirestore(token: String?, pushSettings: PushSettings?) {
val hasAccounts = pushSettings?.subscribedMetaAccounts?.any() ?: false
if (token == null || pushSettings == null || !hasAccounts) {
Firebase.firestore.collection(COLLECTION_NAME)
.document(getFirestoreUUID())
.delete()
.await()
} else {
val model = mapToFirestorePushSettings(
token,
Date(),
pushSettings
)
Firebase.firestore.collection(COLLECTION_NAME)
.document(getFirestoreUUID())
.set(model)
.await()
}
}
private fun handleTopics(pushEnabled: Boolean, oldSettings: PushSettings, newSettings: PushSettings?) {
val referendumUpdateTracks = newSettings?.getGovernanceTracksFor { it.referendumUpdateEnabled }
?.takeIf { pushEnabled }
.orEmpty()
val newReferendaTracks = newSettings?.getGovernanceTracksFor { it.newReferendaEnabled }
?.takeIf { pushEnabled }
.orEmpty()
val govStateTracksDiff = CollectionDiffer.findDiff(
oldItems = oldSettings.getGovernanceTracksFor { it.referendumUpdateEnabled },
newItems = referendumUpdateTracks,
forceUseNewItems = false
)
val newReferendaDiff = CollectionDiffer.findDiff(
oldItems = oldSettings.getGovernanceTracksFor { it.newReferendaEnabled },
newItems = newReferendaTracks,
forceUseNewItems = false
)
val announcementsEnabled = newSettings?.announcementsEnabled ?: false
handleSubscription(announcementsEnabled && pushEnabled, "appUpdates")
govStateTracksDiff.added
.map { subscribeToTopic("${GOV_STATE_TOPIC_NAME}_${it.chainId}_${it.track}") }
govStateTracksDiff.removed
.map { unsubscribeFromTopic("${GOV_STATE_TOPIC_NAME}_${it.chainId}_${it.track}") }
newReferendaDiff.added
.map { subscribeToTopic("${NEW_REFERENDA_TOPIC_NAME}_${it.chainId}_${it.track}") }
newReferendaDiff.removed
.map { unsubscribeFromTopic("${NEW_REFERENDA_TOPIC_NAME}_${it.chainId}_${it.track}") }
}
private fun handleSubscription(subscribe: Boolean, topic: String) {
return if (subscribe) {
subscribeToTopic(topic)
} else {
unsubscribeFromTopic(topic)
}
}
private fun subscribeToTopic(topic: String) {
Firebase.messaging.subscribeToTopic(topic)
}
private fun unsubscribeFromTopic(topic: String) {
Firebase.messaging.unsubscribeFromTopic(topic)
}
private suspend fun mapToFirestorePushSettings(
token: String,
date: Date,
settings: PushSettings
): Map<String, Any> {
val chainsById = chainRegistry.chainsById()
val metaAccountsById = accountRepository
.getActiveMetaAccounts()
.associateBy { it.id }
return mapOf(
"pushToken" to token,
"updatedAt" to formatDateISO_8601_NoMs(date),
"wallets" to settings.subscribedMetaAccounts.mapNotNull { mapToFirestoreWallet(it, metaAccountsById, chainsById) },
"notifications" to mapOfNotNullValues(
"stakingReward" to mapToFirestoreChainFeature(settings.stakingReward),
"tokenSent" to settings.sentTokensEnabled.mapToFirestoreChainFeatureOrNull(),
"tokenReceived" to settings.receivedTokensEnabled.mapToFirestoreChainFeatureOrNull(),
"newMultisig" to settings.multisigs.isInitiatingEnabledTotal().mapToFirestoreChainFeatureOrNull(),
"multisigApproval" to settings.multisigs.isApprovingEnabledTotal().mapToFirestoreChainFeatureOrNull(),
"multisigExecuted" to settings.multisigs.isExecutionEnabledTotal().mapToFirestoreChainFeatureOrNull(),
"multisigCancelled" to settings.multisigs.isRejectionEnabledTotal().mapToFirestoreChainFeatureOrNull()
)
)
}
private fun mapToFirestoreWallet(metaId: Long, metaAccountsById: Map<Long, MetaAccount>, chainsById: ChainsById): Map<String, Any>? {
val metaAccount = metaAccountsById[metaId] ?: return null
return mapOfNotNullValues(
"baseEthereum" to metaAccount.mainEthereumAddress(),
"baseSubstrate" to metaAccount.defaultSubstrateAddress,
"chainSpecific" to metaAccount.chainAccounts.mapValuesNotNull { (chainId, chainAccount) ->
val chain = chainsById[chainId] ?: return@mapValuesNotNull null
chain.addressOf(chainAccount.accountId)
}.transfromChainIdsTo16Hex()
.nullIfEmpty()
)
}
private fun mapToFirestoreChainFeature(chainFeature: PushSettings.ChainFeature): Map<String, Any>? {
return when (chainFeature) {
is PushSettings.ChainFeature.All -> mapOf("type" to "all")
is PushSettings.ChainFeature.Concrete -> {
if (chainFeature.chainIds.isEmpty()) {
null
} else {
mapOf("type" to "concrete", "value" to chainFeature.chainIds.transfromChainIdsTo16Hex())
}
}
}
}
private fun Boolean.mapToFirestoreChainFeatureOrNull(): Map<String, Any>? {
return if (this) mapOf("type" to "all") else null
}
private fun PushSettings.getGovernanceTracksFor(filter: (PushSettings.GovernanceState) -> Boolean): List<TrackIdentifiable> {
return governance.filter { (_, state) -> filter(state) }
.flatMap { (chainId, state) -> state.tracks.map { TrackIdentifiable(chainId.chainIdHexPrefix16(), it.value) } }
}
private fun Map<String, Any>.nullIfEmpty(): Map<String, Any>? {
return if (isEmpty()) null else this
}
private fun <T> Map<ChainId, T>.transfromChainIdsTo16Hex(): Map<String, T> {
return mapKeys { (chainId, _) -> chainId.chainIdHexPrefix16() }
}
private fun List<ChainId>.transfromChainIdsTo16Hex(): List<String> {
return map { chainId -> chainId.chainIdHexPrefix16() }
}
}
@@ -0,0 +1,365 @@
package io.novafoundation.nova.feature_push_notifications.di
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkConfigurator
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import com.google.gson.Gson
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet
import io.novafoundation.nova.common.data.network.AppLinksProvider
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.interfaces.ActivityIntentProvider
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigDetailsRepository
import io.novafoundation.nova.feature_account_api.domain.account.identity.IdentityProvider
import io.novafoundation.nova.feature_account_api.domain.account.identity.LocalIdentity
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_assets.presentation.balance.detail.deeplink.AssetDetailsDeepLinkConfigurator
import io.novafoundation.nova.feature_governance_api.presentation.referenda.common.ReferendaStatusFormatter
import io.novafoundation.nova.feature_governance_api.presentation.referenda.details.deeplink.configurators.ReferendumDetailsDeepLinkConfigurator
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
import io.novafoundation.nova.feature_push_notifications.presentation.handling.CompoundNotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.types.SystemNotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NotificationIdProvider
import io.novafoundation.nova.feature_push_notifications.presentation.handling.RealNotificationIdProvider
import io.novafoundation.nova.feature_push_notifications.presentation.handling.types.DebugNotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.types.NewReferendumNotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.types.NewReleaseNotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.types.ReferendumStateUpdateNotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.types.StakingRewardNotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.types.TokenReceivedNotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.types.TokenSentNotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.types.multisig.MultisigTransactionCancelledNotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.types.multisig.MultisigTransactionExecutedNotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.types.multisig.MultisigTransactionInitiatedNotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.types.multisig.MultisigTransactionNewApprovalNotificationHandler
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
@Module()
class NotificationHandlersModule {
@Provides
fun provideNotificationIdProvider(preferences: Preferences): NotificationIdProvider {
return RealNotificationIdProvider(preferences)
}
@Provides
fun provideNotificationManagerCompat(context: Context): NotificationManagerCompat {
return NotificationManagerCompat.from(context)
}
@Provides
@IntoSet
fun systemNotificationHandler(
context: Context,
activityIntentProvider: ActivityIntentProvider,
notificationIdProvider: NotificationIdProvider,
notificationManagerCompat: NotificationManagerCompat,
resourceManager: ResourceManager,
gson: Gson
): NotificationHandler {
return SystemNotificationHandler(context, activityIntentProvider, notificationIdProvider, gson, notificationManagerCompat, resourceManager)
}
@Provides
@IntoSet
fun tokenSentNotificationHandler(
context: Context,
notificationIdProvider: NotificationIdProvider,
notificationManagerCompat: NotificationManagerCompat,
resourceManager: ResourceManager,
gson: Gson,
activityIntentProvider: ActivityIntentProvider,
accountRepository: AccountRepository,
chainRegistry: ChainRegistry,
tokenRepository: TokenRepository,
configurator: AssetDetailsDeepLinkConfigurator
): NotificationHandler {
return TokenSentNotificationHandler(
context,
accountRepository,
tokenRepository,
chainRegistry,
configurator,
activityIntentProvider,
notificationIdProvider,
gson,
notificationManagerCompat,
resourceManager
)
}
@Provides
@IntoSet
fun tokenReceivedNotificationHandler(
context: Context,
notificationIdProvider: NotificationIdProvider,
notificationManagerCompat: NotificationManagerCompat,
resourceManager: ResourceManager,
gson: Gson,
activityIntentProvider: ActivityIntentProvider,
accountRepository: AccountRepository,
chainRegistry: ChainRegistry,
tokenRepository: TokenRepository,
configurator: AssetDetailsDeepLinkConfigurator
): NotificationHandler {
return TokenReceivedNotificationHandler(
context,
accountRepository,
tokenRepository,
configurator,
chainRegistry,
activityIntentProvider,
notificationIdProvider,
gson,
notificationManagerCompat,
resourceManager
)
}
@Provides
@IntoSet
fun stakingRewardNotificationHandler(
context: Context,
notificationIdProvider: NotificationIdProvider,
notificationManagerCompat: NotificationManagerCompat,
resourceManager: ResourceManager,
gson: Gson,
activityIntentProvider: ActivityIntentProvider,
accountRepository: AccountRepository,
chainRegistry: ChainRegistry,
tokenRepository: TokenRepository,
configurator: AssetDetailsDeepLinkConfigurator
): NotificationHandler {
return StakingRewardNotificationHandler(
context,
accountRepository,
tokenRepository,
configurator,
chainRegistry,
activityIntentProvider,
notificationIdProvider,
gson,
notificationManagerCompat,
resourceManager
)
}
@Provides
@IntoSet
fun referendumStateUpdateNotificationHandler(
context: Context,
notificationIdProvider: NotificationIdProvider,
notificationManagerCompat: NotificationManagerCompat,
resourceManager: ResourceManager,
activityIntentProvider: ActivityIntentProvider,
referendaStatusFormatter: ReferendaStatusFormatter,
gson: Gson,
chainRegistry: ChainRegistry,
configurator: ReferendumDetailsDeepLinkConfigurator
): NotificationHandler {
return ReferendumStateUpdateNotificationHandler(
context,
configurator,
referendaStatusFormatter,
chainRegistry,
activityIntentProvider,
notificationIdProvider,
gson,
notificationManagerCompat,
resourceManager
)
}
@Provides
@IntoSet
fun newReleaseNotificationHandler(
context: Context,
appLinksProvider: AppLinksProvider,
activityIntentProvider: ActivityIntentProvider,
notificationIdProvider: NotificationIdProvider,
notificationManagerCompat: NotificationManagerCompat,
resourceManager: ResourceManager,
gson: Gson
): NotificationHandler {
return NewReleaseNotificationHandler(
context,
appLinksProvider,
activityIntentProvider,
notificationIdProvider,
gson,
notificationManagerCompat,
resourceManager
)
}
@Provides
@IntoSet
fun newReferendumNotificationHandler(
context: Context,
notificationIdProvider: NotificationIdProvider,
notificationManagerCompat: NotificationManagerCompat,
activityIntentProvider: ActivityIntentProvider,
resourceManager: ResourceManager,
gson: Gson,
chainRegistry: ChainRegistry,
configurator: ReferendumDetailsDeepLinkConfigurator
): NotificationHandler {
return NewReferendumNotificationHandler(
context,
configurator,
chainRegistry,
activityIntentProvider,
notificationIdProvider,
gson,
notificationManagerCompat,
resourceManager
)
}
@Provides
@IntoSet
fun multisigTransactionInitiatedNotificationHandler(
context: Context,
accountRepository: AccountRepository,
chainRegistry: ChainRegistry,
activityIntentProvider: ActivityIntentProvider,
notificationIdProvider: NotificationIdProvider,
multisigCallFormatter: MultisigCallFormatter,
configurator: MultisigOperationDeepLinkConfigurator,
gson: Gson,
notificationManager: NotificationManagerCompat,
resourceManager: ResourceManager,
@LocalIdentity identityProvider: IdentityProvider
): NotificationHandler {
return MultisigTransactionInitiatedNotificationHandler(
context,
accountRepository,
multisigCallFormatter,
configurator,
chainRegistry,
identityProvider,
activityIntentProvider,
notificationIdProvider,
gson,
notificationManager,
resourceManager
)
}
@Provides
@IntoSet
fun multisigTransactionNewApprovalNotificationHandler(
context: Context,
accountRepository: AccountRepository,
chainRegistry: ChainRegistry,
multisigDetailsRepository: MultisigDetailsRepository,
@LocalIdentity identityProvider: IdentityProvider,
activityIntentProvider: ActivityIntentProvider,
notificationIdProvider: NotificationIdProvider,
multisigCallFormatter: MultisigCallFormatter,
configurator: MultisigOperationDeepLinkConfigurator,
gson: Gson,
notificationManager: NotificationManagerCompat,
resourceManager: ResourceManager,
): NotificationHandler {
return MultisigTransactionNewApprovalNotificationHandler(
context,
accountRepository,
multisigDetailsRepository,
multisigCallFormatter,
configurator,
identityProvider,
chainRegistry,
activityIntentProvider,
notificationIdProvider,
gson,
notificationManager,
resourceManager
)
}
@Provides
@IntoSet
fun multisigTransactionExecutedNotificationHandler(
context: Context,
accountRepository: AccountRepository,
chainRegistry: ChainRegistry,
configurator: MultisigOperationDeepLinkConfigurator,
@LocalIdentity identityProvider: IdentityProvider,
activityIntentProvider: ActivityIntentProvider,
notificationIdProvider: NotificationIdProvider,
multisigCallFormatter: MultisigCallFormatter,
gson: Gson,
notificationManager: NotificationManagerCompat,
resourceManager: ResourceManager,
): NotificationHandler {
return MultisigTransactionExecutedNotificationHandler(
context,
accountRepository,
multisigCallFormatter,
configurator,
identityProvider,
chainRegistry,
activityIntentProvider,
notificationIdProvider,
gson,
notificationManager,
resourceManager
)
}
@Provides
@IntoSet
fun multisigTransactionCancelledNotificationHandler(
context: Context,
accountRepository: AccountRepository,
chainRegistry: ChainRegistry,
configurator: MultisigOperationDeepLinkConfigurator,
@LocalIdentity identityProvider: IdentityProvider,
activityIntentProvider: ActivityIntentProvider,
notificationIdProvider: NotificationIdProvider,
multisigCallFormatter: MultisigCallFormatter,
gson: Gson,
notificationManager: NotificationManagerCompat,
resourceManager: ResourceManager,
): NotificationHandler {
return MultisigTransactionCancelledNotificationHandler(
context,
accountRepository,
multisigCallFormatter,
configurator,
identityProvider,
chainRegistry,
activityIntentProvider,
notificationIdProvider,
gson,
notificationManager,
resourceManager
)
}
@Provides
fun debugNotificationHandler(
context: Context,
activityIntentProvider: ActivityIntentProvider,
notificationManagerCompat: NotificationManagerCompat,
resourceManager: ResourceManager
): DebugNotificationHandler {
return DebugNotificationHandler(context, activityIntentProvider, notificationManagerCompat, resourceManager)
}
@Provides
@FeatureScope
fun provideCompoundNotificationHandler(
handlers: Set<@JvmSuppressWildcards NotificationHandler>,
debugNotificationHandler: DebugNotificationHandler
): NotificationHandler {
val handlersWithDebugHandler = handlers + debugNotificationHandler // Add debug handler as a fallback in the end
return CompoundNotificationHandler(handlersWithDebugHandler)
}
}
@@ -0,0 +1,17 @@
package io.novafoundation.nova.feature_push_notifications.di
import io.novafoundation.nova.feature_push_notifications.NovaFirebaseMessagingService
import io.novafoundation.nova.feature_push_notifications.domain.interactor.PushNotificationsInteractor
import io.novafoundation.nova.feature_push_notifications.domain.interactor.WelcomePushNotificationsInteractor
import io.novafoundation.nova.feature_push_notifications.presentation.multisigsWarning.MultisigPushNotificationsAlertMixinFactory
interface PushNotificationsFeatureApi {
val multisigPushNotificationsAlertMixinFactory: MultisigPushNotificationsAlertMixinFactory
fun inject(service: NovaFirebaseMessagingService)
fun pushNotificationInteractor(): PushNotificationsInteractor
fun welcomePushNotificationsInteractor(): WelcomePushNotificationsInteractor
}
@@ -0,0 +1,75 @@
package io.novafoundation.nova.feature_push_notifications.di
import dagger.BindsInstance
import dagger.Component
import io.novafoundation.nova.common.di.CommonApi
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.list.SelectMultipleWalletsCommunicator
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.list.SelectTracksCommunicator
import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi
import io.novafoundation.nova.feature_governance_api.di.GovernanceFeatureApi
import io.novafoundation.nova.feature_multisig_operations.di.MultisigOperationsFeatureApi
import io.novafoundation.nova.feature_push_notifications.PushNotificationsRouter
import io.novafoundation.nova.feature_push_notifications.data.PushNotificationsService
import io.novafoundation.nova.feature_push_notifications.presentation.governance.PushGovernanceSettingsCommunicator
import io.novafoundation.nova.feature_push_notifications.presentation.governance.di.PushGovernanceSettingsComponent
import io.novafoundation.nova.feature_push_notifications.presentation.multisigs.PushMultisigSettingsCommunicator
import io.novafoundation.nova.feature_push_notifications.presentation.multisigs.di.PushMultisigSettingsComponent
import io.novafoundation.nova.feature_push_notifications.presentation.settings.di.PushSettingsComponent
import io.novafoundation.nova.feature_push_notifications.presentation.staking.PushStakingSettingsCommunicator
import io.novafoundation.nova.feature_push_notifications.presentation.staking.di.PushStakingSettingsComponent
import io.novafoundation.nova.feature_push_notifications.presentation.welcome.di.PushWelcomeComponent
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
import io.novafoundation.nova.runtime.di.RuntimeApi
@Component(
dependencies = [
PushNotificationsFeatureDependencies::class
],
modules = [
PushNotificationsFeatureModule::class
]
)
@FeatureScope
interface PushNotificationsFeatureComponent : PushNotificationsFeatureApi {
fun getPushNotificationService(): PushNotificationsService
fun pushWelcomeComponentFactory(): PushWelcomeComponent.Factory
fun pushSettingsComponentFactory(): PushSettingsComponent.Factory
fun pushGovernanceSettings(): PushGovernanceSettingsComponent.Factory
fun pushStakingSettings(): PushStakingSettingsComponent.Factory
fun pushMultisigSettings(): PushMultisigSettingsComponent.Factory
@Component.Factory
interface Factory {
fun create(
@BindsInstance router: PushNotificationsRouter,
@BindsInstance selectMultipleWalletsCommunicator: SelectMultipleWalletsCommunicator,
@BindsInstance selectTracksCommunicator: SelectTracksCommunicator,
@BindsInstance pushGovernanceSettingsCommunicator: PushGovernanceSettingsCommunicator,
@BindsInstance pushStakingSettingsCommunicator: PushStakingSettingsCommunicator,
@BindsInstance pushMultisigSettingsCommunicator: PushMultisigSettingsCommunicator,
deps: PushNotificationsFeatureDependencies
): PushNotificationsFeatureComponent
}
@Component(
dependencies = [
CommonApi::class,
RuntimeApi::class,
AccountFeatureApi::class,
GovernanceFeatureApi::class,
WalletFeatureApi::class,
AssetsFeatureApi::class,
MultisigOperationsFeatureApi::class
]
)
interface PushNotificationsFeatureDependenciesComponent : PushNotificationsFeatureDependencies
}
@@ -0,0 +1,85 @@
package io.novafoundation.nova.feature_push_notifications.di
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkConfigurator
import android.content.Context
import coil.ImageLoader
import com.google.gson.Gson
import io.novafoundation.nova.common.data.GoogleApiAvailabilityProvider
import io.novafoundation.nova.common.data.network.AppLinksProvider
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.common.interfaces.ActivityIntentProvider
import io.novafoundation.nova.common.interfaces.BuildTypeProvider
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.coroutines.RootScope
import io.novafoundation.nova.common.utils.permissions.PermissionsAskerFactory
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
import io.novafoundation.nova.feature_account_api.data.events.MetaAccountChangesEventBus
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigDetailsRepository
import io.novafoundation.nova.feature_account_api.data.proxy.MetaAccountsUpdatesRegistry
import io.novafoundation.nova.feature_account_api.domain.account.identity.IdentityProvider
import io.novafoundation.nova.feature_account_api.domain.account.identity.LocalIdentity
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_assets.presentation.balance.detail.deeplink.AssetDetailsDeepLinkConfigurator
import io.novafoundation.nova.feature_governance_api.data.source.GovernanceSourceRegistry
import io.novafoundation.nova.feature_governance_api.presentation.referenda.common.ReferendaStatusFormatter
import io.novafoundation.nova.feature_governance_api.presentation.referenda.details.deeplink.configurators.ReferendumDetailsDeepLinkConfigurator
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
interface PushNotificationsFeatureDependencies {
val rootScope: RootScope
val preferences: Preferences
val context: Context
val chainRegistry: ChainRegistry
val permissionsAskerFactory: PermissionsAskerFactory
val resourceManager: ResourceManager
val accountRepository: AccountRepository
val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory
val governanceSourceRegistry: GovernanceSourceRegistry
val imageLoader: ImageLoader
val gson: Gson
val referendaStatusFormatter: ReferendaStatusFormatter
val assetDetailsDeepLinkConfigurator: AssetDetailsDeepLinkConfigurator
val tokenRepository: TokenRepository
val provideActivityIntentProvider: ActivityIntentProvider
val appLinksProvider: AppLinksProvider
val referendumDetailsDeepLinkConfigurator: ReferendumDetailsDeepLinkConfigurator
val multisigOperationDeepLinkConfigurator: MultisigOperationDeepLinkConfigurator
val metaAccountChangesEventBus: MetaAccountChangesEventBus
val googleApiAvailabilityProvider: GoogleApiAvailabilityProvider
val multisigCallFormatter: MultisigCallFormatter
val multisigDetailsRepository: MultisigDetailsRepository
val metaAccountsUpdatesRegistry: MetaAccountsUpdatesRegistry
val automaticInteractionGate: AutomaticInteractionGate
fun buildTypeProvider(): BuildTypeProvider
@LocalIdentity
fun localWithIdentityProvider(): IdentityProvider
}
@@ -0,0 +1,51 @@
package io.novafoundation.nova.feature_push_notifications.di
import io.novafoundation.nova.common.di.FeatureApiHolder
import io.novafoundation.nova.common.di.FeatureContainer
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.list.SelectMultipleWalletsCommunicator
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.list.SelectTracksCommunicator
import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi
import io.novafoundation.nova.feature_governance_api.di.GovernanceFeatureApi
import io.novafoundation.nova.feature_multisig_operations.di.MultisigOperationsFeatureApi
import io.novafoundation.nova.feature_push_notifications.PushNotificationsRouter
import io.novafoundation.nova.feature_push_notifications.presentation.governance.PushGovernanceSettingsCommunicator
import io.novafoundation.nova.feature_push_notifications.presentation.multisigs.PushMultisigSettingsCommunicator
import io.novafoundation.nova.feature_push_notifications.presentation.staking.PushStakingSettingsCommunicator
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
import io.novafoundation.nova.runtime.di.RuntimeApi
import javax.inject.Inject
class PushNotificationsFeatureHolder @Inject constructor(
featureContainer: FeatureContainer,
private val router: PushNotificationsRouter,
private val selectMultipleWalletsCommunicator: SelectMultipleWalletsCommunicator,
private val selectTracksCommunicator: SelectTracksCommunicator,
private val pushGovernanceSettingsCommunicator: PushGovernanceSettingsCommunicator,
private val pushStakingSettingsCommunicator: PushStakingSettingsCommunicator,
private val pushMultisigSettingsCommunicator: PushMultisigSettingsCommunicator
) : FeatureApiHolder(featureContainer) {
override fun initializeDependencies(): Any {
val dependencies = DaggerPushNotificationsFeatureComponent_PushNotificationsFeatureDependenciesComponent.builder()
.commonApi(commonApi())
.runtimeApi(getFeature(RuntimeApi::class.java))
.accountFeatureApi(getFeature(AccountFeatureApi::class.java))
.governanceFeatureApi(getFeature(GovernanceFeatureApi::class.java))
.walletFeatureApi(getFeature(WalletFeatureApi::class.java))
.assetsFeatureApi(getFeature(AssetsFeatureApi::class.java))
.multisigOperationsFeatureApi(getFeature(MultisigOperationsFeatureApi::class.java))
.build()
return DaggerPushNotificationsFeatureComponent.factory()
.create(
router,
selectMultipleWalletsCommunicator,
selectTracksCommunicator,
pushGovernanceSettingsCommunicator,
pushStakingSettingsCommunicator,
pushMultisigSettingsCommunicator,
dependencies
)
}
}
@@ -0,0 +1,203 @@
package io.novafoundation.nova.feature_push_notifications.di
import android.content.Context
import com.google.gson.Gson
import dagger.Module
import dagger.Provides
import io.novafoundation.nova.common.data.GoogleApiAvailabilityProvider
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.interfaces.BuildTypeProvider
import io.novafoundation.nova.common.utils.coroutines.RootScope
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
import io.novafoundation.nova.feature_account_api.data.proxy.MetaAccountsUpdatesRegistry
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_governance_api.data.source.GovernanceSourceRegistry
import io.novafoundation.nova.feature_push_notifications.PushNotificationsRouter
import io.novafoundation.nova.feature_push_notifications.data.PushNotificationsService
import io.novafoundation.nova.feature_push_notifications.data.PushPermissionRepository
import io.novafoundation.nova.feature_push_notifications.data.PushTokenCache
import io.novafoundation.nova.feature_push_notifications.data.RealPushNotificationsService
import io.novafoundation.nova.feature_push_notifications.data.RealPushPermissionRepository
import io.novafoundation.nova.feature_push_notifications.data.RealPushTokenCache
import io.novafoundation.nova.feature_push_notifications.data.repository.MultisigPushAlertRepository
import io.novafoundation.nova.feature_push_notifications.data.repository.PushSettingsRepository
import io.novafoundation.nova.feature_push_notifications.data.repository.RealMultisigPushAlertRepository
import io.novafoundation.nova.feature_push_notifications.data.repository.RealPushSettingsRepository
import io.novafoundation.nova.feature_push_notifications.data.settings.PushSettingsProvider
import io.novafoundation.nova.feature_push_notifications.data.settings.PushSettingsSerializer
import io.novafoundation.nova.feature_push_notifications.data.settings.RealPushSettingsProvider
import io.novafoundation.nova.feature_push_notifications.data.subscription.PushSubscriptionService
import io.novafoundation.nova.feature_push_notifications.data.subscription.RealPushSubscriptionService
import io.novafoundation.nova.feature_push_notifications.domain.interactor.GovernancePushSettingsInteractor
import io.novafoundation.nova.feature_push_notifications.domain.interactor.MultisigPushAlertInteractor
import io.novafoundation.nova.feature_push_notifications.domain.interactor.PushNotificationsInteractor
import io.novafoundation.nova.feature_push_notifications.domain.interactor.RealGovernancePushSettingsInteractor
import io.novafoundation.nova.feature_push_notifications.domain.interactor.RealMultisigPushAlertInteractor
import io.novafoundation.nova.feature_push_notifications.domain.interactor.RealPushNotificationsInteractor
import io.novafoundation.nova.feature_push_notifications.domain.interactor.RealStakingPushSettingsInteractor
import io.novafoundation.nova.feature_push_notifications.domain.interactor.RealWelcomePushNotificationsInteractor
import io.novafoundation.nova.feature_push_notifications.domain.interactor.StakingPushSettingsInteractor
import io.novafoundation.nova.feature_push_notifications.domain.interactor.WelcomePushNotificationsInteractor
import io.novafoundation.nova.feature_push_notifications.presentation.multisigsWarning.MultisigPushNotificationsAlertMixinFactory
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class PushSettingsSerialization
@Module(includes = [NotificationHandlersModule::class])
class PushNotificationsFeatureModule {
@Provides
@FeatureScope
fun providePushTokenCache(
preferences: Preferences
): PushTokenCache {
return RealPushTokenCache(preferences)
}
@Provides
@FeatureScope
@PushSettingsSerialization
fun providePushSettingsGson() = PushSettingsSerializer.gson()
@Provides
@FeatureScope
fun providePushSettingsProvider(
@PushSettingsSerialization gson: Gson,
preferences: Preferences,
accountRepository: AccountRepository
): PushSettingsProvider {
return RealPushSettingsProvider(gson, preferences, accountRepository)
}
@Provides
@FeatureScope
fun providePushSubscriptionService(
prefs: Preferences,
chainRegistry: ChainRegistry,
googleApiAvailabilityProvider: GoogleApiAvailabilityProvider,
accountRepository: AccountRepository
): PushSubscriptionService {
return RealPushSubscriptionService(
prefs,
chainRegistry,
googleApiAvailabilityProvider,
accountRepository
)
}
@Provides
@FeatureScope
fun providePushPermissionRepository(context: Context): PushPermissionRepository {
return RealPushPermissionRepository(context)
}
@Provides
@FeatureScope
fun providePushNotificationsService(
pushSettingsProvider: PushSettingsProvider,
pushSubscriptionService: PushSubscriptionService,
rootScope: RootScope,
pushTokenCache: PushTokenCache,
googleApiAvailabilityProvider: GoogleApiAvailabilityProvider,
pushPermissionRepository: PushPermissionRepository,
preferences: Preferences,
buildTypeProvider: BuildTypeProvider
): PushNotificationsService {
return RealPushNotificationsService(
pushSettingsProvider,
pushSubscriptionService,
rootScope,
pushTokenCache,
googleApiAvailabilityProvider,
pushPermissionRepository,
preferences,
buildTypeProvider
)
}
@Provides
@FeatureScope
fun providePushSettingsRepository(preferences: Preferences): PushSettingsRepository {
return RealPushSettingsRepository(preferences)
}
@Provides
@FeatureScope
fun providePushNotificationsInteractor(
pushNotificationsService: PushNotificationsService,
pushSettingsProvider: PushSettingsProvider,
accountRepository: AccountRepository,
pushSettingsRepository: PushSettingsRepository
): PushNotificationsInteractor {
return RealPushNotificationsInteractor(pushNotificationsService, pushSettingsProvider, accountRepository, pushSettingsRepository)
}
@Provides
@FeatureScope
fun provideWelcomePushNotificationsInteractor(
preferences: Preferences,
pushNotificationsService: PushNotificationsService
): WelcomePushNotificationsInteractor {
return RealWelcomePushNotificationsInteractor(preferences, pushNotificationsService)
}
@Provides
@FeatureScope
fun provideGovernancePushSettingsInteractor(
chainRegistry: ChainRegistry,
governanceSourceRegistry: GovernanceSourceRegistry
): GovernancePushSettingsInteractor {
return RealGovernancePushSettingsInteractor(
chainRegistry,
governanceSourceRegistry
)
}
@Provides
@FeatureScope
fun provideStakingPushSettingsInteractor(chainRegistry: ChainRegistry): StakingPushSettingsInteractor {
return RealStakingPushSettingsInteractor(chainRegistry)
}
@Provides
@FeatureScope
fun provideMultisigPushAlertRepository(
preferences: Preferences
): MultisigPushAlertRepository {
return RealMultisigPushAlertRepository(preferences)
}
@Provides
@FeatureScope
fun provideMultisigPushAlertInteractor(
pushSettingsProvider: PushSettingsProvider,
accountRepository: AccountRepository,
multisigPushAlertRepository: MultisigPushAlertRepository
): MultisigPushAlertInteractor {
return RealMultisigPushAlertInteractor(
pushSettingsProvider,
accountRepository,
multisigPushAlertRepository
)
}
@Provides
@FeatureScope
fun provideMultisigPushNotificationsAlertMixin(
automaticInteractionGate: AutomaticInteractionGate,
interactor: MultisigPushAlertInteractor,
metaAccountsUpdatesRegistry: MetaAccountsUpdatesRegistry,
router: PushNotificationsRouter
): MultisigPushNotificationsAlertMixinFactory {
return MultisigPushNotificationsAlertMixinFactory(
automaticInteractionGate,
interactor,
metaAccountsUpdatesRegistry,
router
)
}
}
@@ -0,0 +1,50 @@
package io.novafoundation.nova.feature_push_notifications.domain.interactor
import io.novafoundation.nova.common.utils.mapToSet
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId
import io.novafoundation.nova.feature_governance_api.data.source.GovernanceSourceRegistry
import io.novafoundation.nova.runtime.ext.defaultComparatorFrom
import io.novafoundation.nova.runtime.ext.openGovIfSupported
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.enabledChainsFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class ChainWithGovTracks(
val chain: Chain,
val govVersion: Chain.Governance,
val tracks: Set<TrackId>
)
interface GovernancePushSettingsInteractor {
fun governanceChainsFlow(): Flow<List<ChainWithGovTracks>>
}
class RealGovernancePushSettingsInteractor(
private val chainRegistry: ChainRegistry,
private val governanceSourceRegistry: GovernanceSourceRegistry
) : GovernancePushSettingsInteractor {
override fun governanceChainsFlow(): Flow<List<ChainWithGovTracks>> {
return chainRegistry.enabledChainsFlow()
.map { chains ->
chains.filter { it.pushSupport }
.flatMap { it.supportedGovTypes() }
.map { (chain, govType) -> ChainWithGovTracks(chain, govType, getTrackIds(chain, govType)) }
.sortedWith(Chain.defaultComparatorFrom(ChainWithGovTracks::chain))
}
}
private fun Chain.supportedGovTypes(): List<Pair<Chain, Chain.Governance>> {
return listOfNotNull(openGovIfSupported()?.let { this to it })
}
private suspend fun getTrackIds(chain: Chain, governance: Chain.Governance): Set<TrackId> {
return governanceSourceRegistry.sourceFor(governance)
.referenda
.getTracks(chain.id)
.mapToSet { it.id }
}
}
@@ -0,0 +1,69 @@
package io.novafoundation.nova.feature_push_notifications.domain.interactor
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.isMultisig
import io.novafoundation.nova.feature_push_notifications.data.repository.MultisigPushAlertRepository
import io.novafoundation.nova.feature_push_notifications.data.settings.PushSettingsProvider
interface MultisigPushAlertInteractor {
fun isPushNotificationsEnabled(): Boolean
fun isAlertAlreadyShown(): Boolean
fun setAlertWasAlreadyShown()
suspend fun allowedToShowAlertAtStart(): Boolean
suspend fun hasMultisigWallets(consumedMetaIdsUpdates: List<Long>): Boolean
}
enum class AllowingState {
INITIAL, ALLOWED, NOT_ALLOWED
}
class RealMultisigPushAlertInteractor(
private val pushSettingsProvider: PushSettingsProvider,
private val accountRepository: AccountRepository,
private val multisigPushAlertRepository: MultisigPushAlertRepository
) : MultisigPushAlertInteractor {
override fun isPushNotificationsEnabled(): Boolean {
return pushSettingsProvider.isPushNotificationsEnabled()
}
override fun isAlertAlreadyShown(): Boolean {
return multisigPushAlertRepository.isMultisigsPushAlertWasShown()
}
override fun setAlertWasAlreadyShown() {
multisigPushAlertRepository.setMultisigsPushAlertWasShown()
}
/**
* We have to check if we can show alert right after user update the app.
* Showing is allowed when user have multisig accounts in first app start after update
*/
override suspend fun allowedToShowAlertAtStart(): Boolean {
val allowingState = multisigPushAlertRepository.showAlertAtStartAllowingState()
if (allowingState == AllowingState.INITIAL) {
val userHasMultisigs = accountRepository.hasMetaAccountsByType(LightMetaAccount.Type.MULTISIG)
if (userHasMultisigs) {
multisigPushAlertRepository.setAlertAtStartAllowingState(AllowingState.ALLOWED)
return true
} else {
multisigPushAlertRepository.setAlertAtStartAllowingState(AllowingState.NOT_ALLOWED)
return false
}
}
return allowingState == AllowingState.ALLOWED
}
override suspend fun hasMultisigWallets(consumedMetaIdsUpdates: List<Long>): Boolean {
val consumedMetaAccountUpdates = accountRepository.getMetaAccountsByIds(consumedMetaIdsUpdates)
return consumedMetaAccountUpdates.any { it.isMultisig() }
}
}
@@ -0,0 +1,143 @@
package io.novafoundation.nova.feature_push_notifications.domain.interactor
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_push_notifications.data.PushNotificationsAvailabilityState
import io.novafoundation.nova.feature_push_notifications.data.PushNotificationsService
import io.novafoundation.nova.feature_push_notifications.data.repository.PushSettingsRepository
import io.novafoundation.nova.feature_push_notifications.data.settings.PushSettingsProvider
import io.novafoundation.nova.feature_push_notifications.domain.model.PushSettings
import kotlinx.coroutines.flow.Flow
interface PushNotificationsInteractor {
suspend fun initialSyncSettings()
fun pushNotificationsEnabledFlow(): Flow<Boolean>
suspend fun initPushSettings(): Result<Unit>
suspend fun updatePushSettings(enable: Boolean, pushSettings: PushSettings): Result<Unit>
suspend fun getPushSettings(): PushSettings
suspend fun getDefaultSettings(): PushSettings
suspend fun getMetaAccounts(metaIds: List<Long>): List<MetaAccount>
fun isPushNotificationsEnabled(): Boolean
fun isMultisigsWasEnabledFirstTime(): Boolean
fun setMultisigsWasEnabledFirstTime()
fun pushNotificationsAvailabilityState(): PushNotificationsAvailabilityState
fun isPushNotificationsAvailable(): Boolean
suspend fun onMetaAccountChange(changed: List<Long>, deleted: List<Long>)
suspend fun filterAvailableMetaIdsAndGetNewState(pushSettings: PushSettings): PushSettings
suspend fun getNewStateForChangedMetaAccounts(currentSettings: PushSettings, newMetaIds: Set<Long>): PushSettings
}
class RealPushNotificationsInteractor(
private val pushNotificationsService: PushNotificationsService,
private val pushSettingsProvider: PushSettingsProvider,
private val accountRepository: AccountRepository,
private val pushSettingsRepository: PushSettingsRepository
) : PushNotificationsInteractor {
override suspend fun initialSyncSettings() {
pushNotificationsService.syncSettingsIfNeeded()
}
override fun pushNotificationsEnabledFlow(): Flow<Boolean> {
return pushSettingsProvider.pushEnabledFlow()
}
override suspend fun initPushSettings(): Result<Unit> {
return pushNotificationsService.initPushNotifications()
}
override suspend fun updatePushSettings(enable: Boolean, pushSettings: PushSettings): Result<Unit> {
return pushNotificationsService.updatePushSettings(enable, pushSettings)
}
override suspend fun getPushSettings(): PushSettings {
return pushSettingsProvider.getPushSettings()
}
override suspend fun getDefaultSettings(): PushSettings {
return pushSettingsProvider.getDefaultPushSettings()
}
override suspend fun getMetaAccounts(metaIds: List<Long>): List<MetaAccount> {
return accountRepository.getMetaAccountsByIds(metaIds)
}
override fun isPushNotificationsEnabled(): Boolean {
return pushSettingsProvider.isPushNotificationsEnabled()
}
override fun isMultisigsWasEnabledFirstTime(): Boolean {
return pushSettingsRepository.isMultisigsWasEnabledFirstTime()
}
override fun setMultisigsWasEnabledFirstTime() {
pushSettingsRepository.setMultisigsWasEnabledFirstTime()
}
override fun isPushNotificationsAvailable(): Boolean {
return pushNotificationsService.isPushNotificationsAvailable()
}
override fun pushNotificationsAvailabilityState(): PushNotificationsAvailabilityState {
return pushNotificationsService.pushNotificationsAvaiabilityState()
}
override suspend fun onMetaAccountChange(changed: List<Long>, deleted: List<Long>) {
if (changed.isEmpty() && deleted.isEmpty()) return
val notificationsEnabled = pushSettingsProvider.isPushNotificationsEnabled()
val noAccounts = accountRepository.getActiveMetaAccountsQuantity() == 0
val pushSettings = pushSettingsProvider.getPushSettings()
val allAffected = (changed + deleted).toSet()
val subscribedAccountsAffected = pushSettings.subscribedMetaAccounts.intersect(allAffected).isNotEmpty()
when {
notificationsEnabled && noAccounts -> pushNotificationsService.updatePushSettings(enabled = false, pushSettings = null)
noAccounts -> pushSettingsProvider.updateSettings(pushWalletSettings = null)
subscribedAccountsAffected -> {
val newSubscribedMetaAccounts = pushSettings.subscribedMetaAccounts - deleted.toSet()
val newEnabledState = notificationsEnabled && newSubscribedMetaAccounts.isNotEmpty()
val newPushSettings = getNewStateForChangedMetaAccounts(pushSettings, newSubscribedMetaAccounts)
pushNotificationsService.updatePushSettings(enabled = newEnabledState, pushSettings = newPushSettings)
}
}
}
override suspend fun filterAvailableMetaIdsAndGetNewState(pushSettings: PushSettings): PushSettings {
val availableMetaIds = accountRepository.getAvailableMetaIdsFromSet(pushSettings.subscribedMetaAccounts)
if (availableMetaIds == pushSettings.subscribedMetaAccounts) return pushSettings
return getNewStateForChangedMetaAccounts(pushSettings, availableMetaIds)
}
override suspend fun getNewStateForChangedMetaAccounts(currentSettings: PushSettings, newMetaIds: Set<Long>): PushSettings {
val noMultisigWalletsForNewAccounts = !accountRepository.hasMetaAccountsByType(newMetaIds, LightMetaAccount.Type.MULTISIG)
if (noMultisigWalletsForNewAccounts) {
val disabledMultisigSettings = PushSettings.MultisigsState.disabled()
return currentSettings.copy(subscribedMetaAccounts = newMetaIds, multisigs = disabledMultisigSettings)
}
return if (!currentSettings.multisigs.isEnabled) {
currentSettings.copy(subscribedMetaAccounts = newMetaIds, multisigs = PushSettings.MultisigsState.enabled())
} else {
currentSettings.copy(subscribedMetaAccounts = newMetaIds)
}
}
}
@@ -0,0 +1,32 @@
package io.novafoundation.nova.feature_push_notifications.domain.interactor
import io.novafoundation.nova.feature_staking_api.data.dashboard.common.supportedStakingOptions
import io.novafoundation.nova.runtime.ext.defaultComparator
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
interface StakingPushSettingsInteractor {
fun stakingChainsFlow(): Flow<List<Chain>>
}
class RealStakingPushSettingsInteractor(
private val chainRegistry: ChainRegistry
) : StakingPushSettingsInteractor {
override fun stakingChainsFlow(): Flow<List<Chain>> {
return chainRegistry.stakingChainsFlow()
.map { chains ->
chains.filter { it.pushSupport }
.sortedWith(Chain.defaultComparator())
}
}
private fun ChainRegistry.stakingChainsFlow(): Flow<List<Chain>> {
return currentChains.map { chains ->
chains.filter { it.supportedStakingOptions() }
}
}
}
@@ -0,0 +1,29 @@
package io.novafoundation.nova.feature_push_notifications.domain.interactor
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.feature_push_notifications.data.PushNotificationsService
interface WelcomePushNotificationsInteractor {
fun needToShowWelcomeScreen(): Boolean
fun setWelcomeScreenShown()
}
class RealWelcomePushNotificationsInteractor(
private val preferences: Preferences,
private val pushNotificationsService: PushNotificationsService
) : WelcomePushNotificationsInteractor {
override fun needToShowWelcomeScreen(): Boolean {
return pushNotificationsService.isPushNotificationsAvailable() &&
preferences.getBoolean(PREFS_WELCOME_SCREEN_SHOWN, true)
}
override fun setWelcomeScreenShown() {
return preferences.putBoolean(PREFS_WELCOME_SCREEN_SHOWN, false)
}
companion object {
private const val PREFS_WELCOME_SCREEN_SHOWN = "welcome_screen_shown"
}
}
@@ -0,0 +1,93 @@
package io.novafoundation.nova.feature_push_notifications.domain.model
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
data class PushSettings(
val announcementsEnabled: Boolean,
val sentTokensEnabled: Boolean,
val receivedTokensEnabled: Boolean,
val subscribedMetaAccounts: Set<Long>,
val stakingReward: ChainFeature,
val governance: Map<ChainId, GovernanceState>,
val multisigs: MultisigsState
) {
data class GovernanceState(
val newReferendaEnabled: Boolean,
val referendumUpdateEnabled: Boolean,
val govMyDelegateVotedEnabled: Boolean,
val tracks: Set<TrackId>
)
data class MultisigsState(
val isEnabled: Boolean, // General notifications state. Other states may be enabled to save state when general one is disabled
val isInitiatingEnabled: Boolean,
val isApprovingEnabled: Boolean,
val isExecutionEnabled: Boolean,
val isRejectionEnabled: Boolean
) {
companion object {
fun disabled() = MultisigsState(
isEnabled = false,
isInitiatingEnabled = false,
isApprovingEnabled = false,
isExecutionEnabled = false,
isRejectionEnabled = false
)
fun enabled() = MultisigsState(
isEnabled = true,
isInitiatingEnabled = true,
isApprovingEnabled = true,
isExecutionEnabled = true,
isRejectionEnabled = true
)
}
}
sealed class ChainFeature {
object All : ChainFeature()
data class Concrete(val chainIds: List<ChainId>) : ChainFeature()
}
fun settingsIsEmpty(): Boolean {
return !announcementsEnabled &&
!sentTokensEnabled &&
!receivedTokensEnabled &&
stakingReward.isEmpty() &&
!isGovEnabled()
}
}
fun PushSettings.ChainFeature.isEmpty(): Boolean {
return when (this) {
is PushSettings.ChainFeature.All -> false
is PushSettings.ChainFeature.Concrete -> chainIds.isEmpty()
}
}
fun PushSettings.ChainFeature.isNotEmpty(): Boolean {
return !isEmpty()
}
fun PushSettings.isGovEnabled(): Boolean {
return governance.values.any {
(it.newReferendaEnabled || it.referendumUpdateEnabled || it.govMyDelegateVotedEnabled) && it.tracks.isNotEmpty()
}
}
fun PushSettings.MultisigsState.isAllTypesDisabled(): Boolean {
return !isInitiatingEnabled && !isApprovingEnabled && !isExecutionEnabled && !isRejectionEnabled
}
fun PushSettings.MultisigsState.disableIfAllTypesDisabled(): PushSettings.MultisigsState {
return if (isAllTypesDisabled()) copy(isEnabled = false) else this
}
fun PushSettings.MultisigsState.isInitiatingEnabledTotal() = isInitiatingEnabled && isEnabled
fun PushSettings.MultisigsState.isApprovingEnabledTotal() = isApprovingEnabled && isEnabled
fun PushSettings.MultisigsState.isExecutionEnabledTotal() = isExecutionEnabled && isEnabled
fun PushSettings.MultisigsState.isRejectionEnabledTotal() = isRejectionEnabled && isEnabled
@@ -0,0 +1,35 @@
package io.novafoundation.nova.feature_push_notifications.presentation.governance
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
data class PushGovernanceModel(
val chainId: ChainId,
val governance: Chain.Governance,
val chainName: String,
val chainIconUrl: String?,
val isEnabled: Boolean,
val isNewReferendaEnabled: Boolean,
val isReferendaUpdatesEnabled: Boolean,
val trackIds: Set<TrackId>
) {
companion object
}
fun PushGovernanceModel.Companion.default(
chain: Chain,
governance: Chain.Governance,
tracks: Set<TrackId>
): PushGovernanceModel {
return PushGovernanceModel(
chainId = chain.id,
governance = governance,
chainName = chain.name,
chainIconUrl = chain.icon,
false,
isNewReferendaEnabled = true,
isReferendaUpdatesEnabled = true,
trackIds = tracks
)
}
@@ -0,0 +1,33 @@
package io.novafoundation.nova.feature_push_notifications.presentation.governance
import android.os.Parcelable
import io.novafoundation.nova.common.navigation.InterScreenRequester
import io.novafoundation.nova.common.navigation.InterScreenResponder
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import java.math.BigInteger
import kotlinx.parcelize.Parcelize
interface PushGovernanceSettingsRequester : InterScreenRequester<PushGovernanceSettingsRequester.Request, PushGovernanceSettingsResponder.Response> {
@Parcelize
class Request(val enabledGovernanceSettings: List<PushGovernanceSettingsPayload>) : Parcelable
}
interface PushGovernanceSettingsResponder : InterScreenResponder<PushGovernanceSettingsRequester.Request, PushGovernanceSettingsResponder.Response> {
@Parcelize
class Response(val enabledGovernanceSettings: List<PushGovernanceSettingsPayload>) : Parcelable
}
interface PushGovernanceSettingsCommunicator : PushGovernanceSettingsRequester, PushGovernanceSettingsResponder
@Parcelize
class PushGovernanceSettingsPayload(
val chainId: ChainId,
val governance: Chain.Governance,
val newReferenda: Boolean,
val referendaUpdates: Boolean,
val delegateVotes: Boolean,
val tracksIds: Set<BigInteger>
) : Parcelable
@@ -0,0 +1,86 @@
package io.novafoundation.nova.feature_push_notifications.presentation.governance
import android.os.Bundle
import androidx.core.view.isVisible
import coil.ImageLoader
import io.novafoundation.nova.common.base.BaseFragment
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.domain.ExtendedLoadingState
import io.novafoundation.nova.common.utils.observe
import io.novafoundation.nova.feature_push_notifications.databinding.FragmentPushGovernanceSettingsBinding
import io.novafoundation.nova.feature_push_notifications.di.PushNotificationsFeatureApi
import io.novafoundation.nova.feature_push_notifications.di.PushNotificationsFeatureComponent
import io.novafoundation.nova.feature_push_notifications.presentation.governance.adapter.PushGovernanceRVItem
import io.novafoundation.nova.feature_push_notifications.presentation.governance.adapter.PushGovernanceSettingsAdapter
import javax.inject.Inject
class PushGovernanceSettingsFragment :
BaseFragment<PushGovernanceSettingsViewModel, FragmentPushGovernanceSettingsBinding>(),
PushGovernanceSettingsAdapter.ItemHandler {
companion object {
private const val KEY_REQUEST = "KEY_REQUEST"
fun getBundle(request: PushGovernanceSettingsRequester.Request): Bundle {
return Bundle().apply {
putParcelable(KEY_REQUEST, request)
}
}
}
override fun createBinding() = FragmentPushGovernanceSettingsBinding.inflate(layoutInflater)
@Inject
lateinit var imageLoader: ImageLoader
private val adapter by lazy(LazyThreadSafetyMode.NONE) {
PushGovernanceSettingsAdapter(imageLoader, this)
}
override fun initViews() {
binder.pushGovernanceToolbar.setHomeButtonListener { viewModel.backClicked() }
binder.pushGovernanceToolbar.setRightActionClickListener { viewModel.clearClicked() }
onBackPressed { viewModel.backClicked() }
binder.pushGovernanceList.adapter = adapter
}
override fun inject() {
FeatureUtils.getFeature<PushNotificationsFeatureComponent>(requireContext(), PushNotificationsFeatureApi::class.java)
.pushGovernanceSettings()
.create(this, argument(KEY_REQUEST))
.inject(this)
}
override fun subscribe(viewModel: PushGovernanceSettingsViewModel) {
viewModel.clearButtonEnabledFlow.observe {
binder.pushGovernanceToolbar.setRightActionEnabled(it)
}
viewModel.governanceSettingsList.observe {
binder.pushGovernanceList.isVisible = it is ExtendedLoadingState.Loaded
binder.pushGovernanceProgress.isVisible = it is ExtendedLoadingState.Loading
if (it is ExtendedLoadingState.Loaded) {
adapter.submitList(it.data)
}
}
}
override fun enableSwitcherClick(item: PushGovernanceRVItem) {
viewModel.enableSwitcherClicked(item)
}
override fun newReferendaClick(item: PushGovernanceRVItem) {
viewModel.newReferendaClicked(item)
}
override fun referendaUpdatesClick(item: PushGovernanceRVItem) {
viewModel.referendaUpdatesClicked(item)
}
override fun tracksClicked(item: PushGovernanceRVItem) {
viewModel.tracksClicked(item)
}
}
@@ -0,0 +1,208 @@
package io.novafoundation.nova.feature_push_notifications.presentation.governance
import io.novafoundation.nova.common.base.BaseViewModel
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.mapToSet
import io.novafoundation.nova.common.utils.updateValue
import io.novafoundation.nova.common.utils.withSafeLoading
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.list.SelectTracksRequester
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.TrackId
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.fromTrackIds
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.toTrackIds
import io.novafoundation.nova.feature_push_notifications.R
import io.novafoundation.nova.feature_push_notifications.PushNotificationsRouter
import io.novafoundation.nova.feature_push_notifications.domain.interactor.ChainWithGovTracks
import io.novafoundation.nova.feature_push_notifications.domain.interactor.GovernancePushSettingsInteractor
import io.novafoundation.nova.feature_push_notifications.presentation.governance.adapter.PushGovernanceRVItem
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novafoundation.nova.runtime.multiNetwork.chainsById
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
private const val MIN_TRACKS = 1
data class GovChainKey(val chainId: ChainId, val governance: Chain.Governance)
class PushGovernanceSettingsViewModel(
private val router: PushNotificationsRouter,
private val interactor: GovernancePushSettingsInteractor,
private val pushGovernanceSettingsResponder: PushGovernanceSettingsResponder,
private val chainRegistry: ChainRegistry,
private val request: PushGovernanceSettingsRequester.Request,
private val selectTracksRequester: SelectTracksRequester,
private val resourceManager: ResourceManager
) : BaseViewModel() {
private val chainsWithTracks = interactor.governanceChainsFlow()
.shareInBackground()
val _changedGovernanceSettingsList: MutableStateFlow<Map<GovChainKey, PushGovernanceModel>> = MutableStateFlow(emptyMap())
val governanceSettingsList = combine(chainsWithTracks, _changedGovernanceSettingsList) { chainsWithTracksQuantity, changedSettings ->
chainsWithTracksQuantity.map { chainAndTracks ->
val pushSettingsModel = changedSettings[chainAndTracks.key()]
?: PushGovernanceModel.default(
chainAndTracks.chain,
chainAndTracks.govVersion,
chainAndTracks.tracks
)
PushGovernanceRVItem(
pushSettingsModel,
formatTracksText(pushSettingsModel.trackIds, chainAndTracks.tracks)
)
}
}.withSafeLoading()
val clearButtonEnabledFlow = _changedGovernanceSettingsList.map {
it.any { it.value.isEnabled }
}
init {
launch {
val chainsById = chainRegistry.chainsById()
_changedGovernanceSettingsList.value = request.enabledGovernanceSettings
.mapNotNull { chainIdToSettings ->
val chain = chainsById[chainIdToSettings.chainId] ?: return@mapNotNull null
mapCommunicatorModelToItem(chainIdToSettings, chain)
}.associateBy { it.key() }
}
subscribeOnSelectTracks()
}
fun backClicked() {
launch {
val enabledGovernanceSettings = _changedGovernanceSettingsList.value
.values
.filter { it.isEnabled }
.map { mapItemToCommunicatorModel(it) }
val response = PushGovernanceSettingsResponder.Response(enabledGovernanceSettings)
pushGovernanceSettingsResponder.respond(response)
router.back()
}
}
fun enableSwitcherClicked(item: PushGovernanceRVItem) {
_changedGovernanceSettingsList.updateValue {
it + item.model.copy(isEnabled = !item.isEnabled)
.enableEverythingIfFeaturesDisabled()
.withKey()
}
}
fun newReferendaClicked(item: PushGovernanceRVItem) {
_changedGovernanceSettingsList.updateValue {
it + item.model.copy(isNewReferendaEnabled = !item.isNewReferendaEnabled)
.disableCompletelyIfFeaturesDisabled()
.withKey()
}
}
fun referendaUpdatesClicked(item: PushGovernanceRVItem) {
_changedGovernanceSettingsList.updateValue {
it + item.model.copy(isReferendaUpdatesEnabled = !item.isReferendaUpdatesEnabled)
.disableCompletelyIfFeaturesDisabled()
.withKey()
}
}
fun tracksClicked(item: PushGovernanceRVItem) {
launch {
val selectedTracks = item.model.trackIds.mapToSet { it.value }
selectTracksRequester.openRequest(SelectTracksRequester.Request(item.chainId, item.governance, selectedTracks, MIN_TRACKS))
}
}
fun clearClicked() {
_changedGovernanceSettingsList.value = emptyMap()
}
private fun mapCommunicatorModelToItem(
item: PushGovernanceSettingsPayload,
chain: Chain
): PushGovernanceModel {
val tracks = item.tracksIds.toTrackIds()
return PushGovernanceModel(
chainId = item.chainId,
governance = item.governance,
chainName = chain.name,
chainIconUrl = chain.icon,
isEnabled = true,
isNewReferendaEnabled = item.newReferenda,
isReferendaUpdatesEnabled = item.referendaUpdates,
trackIds = tracks
)
}
private fun mapItemToCommunicatorModel(item: PushGovernanceModel): PushGovernanceSettingsPayload {
return PushGovernanceSettingsPayload(
item.chainId,
item.governance,
item.isNewReferendaEnabled,
item.isReferendaUpdatesEnabled,
false, // Not supported yet
item.trackIds.fromTrackIds()
)
}
private fun PushGovernanceModel.disableCompletelyIfFeaturesDisabled(): PushGovernanceModel {
if (!isNewReferendaEnabled && !isReferendaUpdatesEnabled) {
return copy(isEnabled = false)
}
return this
}
private fun PushGovernanceModel.enableEverythingIfFeaturesDisabled(): PushGovernanceModel {
if (!isNewReferendaEnabled && !isReferendaUpdatesEnabled) {
return copy(isEnabled = true, isNewReferendaEnabled = true, isReferendaUpdatesEnabled = true)
}
return this
}
private fun subscribeOnSelectTracks() {
selectTracksRequester.responseFlow
.onEach { response ->
val chain = chainRegistry.getChain(response.chainId)
val key = GovChainKey(response.chainId, response.governanceType)
val selectedTracks = response.selectedTracks.toTrackIds()
_changedGovernanceSettingsList.updateValue { governanceSettings ->
val model = governanceSettings[key]?.copy(trackIds = selectedTracks)
?: PushGovernanceModel.default(
chain = chain,
governance = response.governanceType,
tracks = selectedTracks
)
governanceSettings.plus(key to model)
}
}
.launchIn(this)
}
private fun PushGovernanceModel.key() = GovChainKey(chainId, governance)
private fun PushGovernanceModel.withKey() = key() to this
private fun ChainWithGovTracks.key() = GovChainKey(chain.id, govVersion)
private fun formatTracksText(selectedTracks: Set<TrackId>, allTracks: Set<TrackId>): String {
return if (selectedTracks.size == allTracks.size) {
resourceManager.getString(R.string.common_all)
} else {
resourceManager.getString(R.string.selected_tracks_quantity, selectedTracks.size, allTracks.size)
}
}
}
@@ -0,0 +1,22 @@
package io.novafoundation.nova.feature_push_notifications.presentation.governance.adapter
import io.novafoundation.nova.feature_push_notifications.presentation.governance.PushGovernanceModel
data class PushGovernanceRVItem(
val model: PushGovernanceModel,
val tracksText: String
) {
val chainId = model.chainId
val governance = model.governance
val chainName = model.chainName
val chainIconUrl = model.chainIconUrl
val isEnabled = model.isEnabled
val isNewReferendaEnabled = model.isNewReferendaEnabled
val isReferendaUpdatesEnabled = model.isReferendaUpdatesEnabled
}
@@ -0,0 +1,128 @@
package io.novafoundation.nova.feature_push_notifications.presentation.governance.adapter
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import coil.ImageLoader
import io.novafoundation.nova.common.list.PayloadGenerator
import io.novafoundation.nova.common.list.resolvePayload
import io.novafoundation.nova.common.utils.inflater
import io.novafoundation.nova.feature_account_api.presenatation.chain.loadChainIconToTarget
import io.novafoundation.nova.feature_push_notifications.databinding.ItemPushGovernanceSettingsBinding
class PushGovernanceSettingsAdapter(
private val imageLoader: ImageLoader,
private val itemHandler: ItemHandler
) : ListAdapter<PushGovernanceRVItem, PushGovernanceItemViewHolder>(PushGovernanceItemCallback()) {
interface ItemHandler {
fun enableSwitcherClick(item: PushGovernanceRVItem)
fun newReferendaClick(item: PushGovernanceRVItem)
fun referendaUpdatesClick(item: PushGovernanceRVItem)
fun tracksClicked(item: PushGovernanceRVItem)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PushGovernanceItemViewHolder {
return PushGovernanceItemViewHolder(ItemPushGovernanceSettingsBinding.inflate(parent.inflater(), parent, false), imageLoader, itemHandler)
}
override fun onBindViewHolder(holder: PushGovernanceItemViewHolder, position: Int) {
holder.bind(getItem(position))
}
override fun onBindViewHolder(holder: PushGovernanceItemViewHolder, position: Int, payloads: MutableList<Any>) {
resolvePayload(holder, position, payloads) {
val item = getItem(position)
holder.updateListenners(item)
when (it) {
PushGovernanceRVItem::isEnabled -> holder.setEnabled(item)
PushGovernanceRVItem::isNewReferendaEnabled -> holder.setNewReferendaEnabled(item)
PushGovernanceRVItem::isReferendaUpdatesEnabled -> holder.setReferendaUpdatesEnabled(item)
PushGovernanceRVItem::tracksText -> holder.setTracks(item)
}
}
}
}
class PushGovernanceItemCallback() : DiffUtil.ItemCallback<PushGovernanceRVItem>() {
override fun areItemsTheSame(oldItem: PushGovernanceRVItem, newItem: PushGovernanceRVItem): Boolean {
return oldItem.chainId == newItem.chainId
}
override fun areContentsTheSame(oldItem: PushGovernanceRVItem, newItem: PushGovernanceRVItem): Boolean {
return oldItem == newItem
}
override fun getChangePayload(oldItem: PushGovernanceRVItem, newItem: PushGovernanceRVItem): Any? {
return PushGovernancePayloadGenerator.diff(oldItem, newItem)
}
}
class PushGovernanceItemViewHolder(
private val binder: ItemPushGovernanceSettingsBinding,
private val imageLoader: ImageLoader,
private val itemHandler: PushGovernanceSettingsAdapter.ItemHandler
) : ViewHolder(binder.root) {
init {
binder.pushGovernanceItemState.setIconTintColor(null)
}
fun bind(item: PushGovernanceRVItem) {
with(itemView) {
updateListenners(item)
binder.pushGovernanceItemState.setTitle(item.chainName)
imageLoader.loadChainIconToTarget(item.chainIconUrl, context) {
binder.pushGovernanceItemState.setIcon(it)
}
setEnabled(item)
setNewReferendaEnabled(item)
setReferendaUpdatesEnabled(item)
setTracks(item)
}
}
fun setTracks(item: PushGovernanceRVItem) {
binder.pushGovernanceItemTracks.setValue(item.tracksText)
}
fun setEnabled(item: PushGovernanceRVItem) {
with(binder) {
pushGovernanceItemState.setChecked(item.isEnabled)
pushGovernanceItemNewReferenda.isVisible = item.isEnabled
pushGovernanceItemReferendumUpdate.isVisible = item.isEnabled
// pushGovernanceItemDelegateVotes.isVisible = item.isEnabled // currently disabled
pushGovernanceItemTracks.isVisible = item.isEnabled
}
}
fun setNewReferendaEnabled(item: PushGovernanceRVItem) {
binder.pushGovernanceItemNewReferenda.setChecked(item.isNewReferendaEnabled)
}
fun setReferendaUpdatesEnabled(item: PushGovernanceRVItem) {
binder.pushGovernanceItemReferendumUpdate.setChecked(item.isReferendaUpdatesEnabled)
}
fun updateListenners(item: PushGovernanceRVItem) {
binder.pushGovernanceItemState.setOnClickListener { itemHandler.enableSwitcherClick(item) }
binder.pushGovernanceItemNewReferenda.setOnClickListener { itemHandler.newReferendaClick(item) }
binder.pushGovernanceItemReferendumUpdate.setOnClickListener { itemHandler.referendaUpdatesClick(item) }
binder.pushGovernanceItemTracks.setOnClickListener { itemHandler.tracksClicked(item) }
}
}
private object PushGovernancePayloadGenerator : PayloadGenerator<PushGovernanceRVItem>(
PushGovernanceRVItem::isEnabled,
PushGovernanceRVItem::isNewReferendaEnabled,
PushGovernanceRVItem::isReferendaUpdatesEnabled,
PushGovernanceRVItem::tracksText,
)
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_push_notifications.presentation.governance.di
import androidx.fragment.app.Fragment
import dagger.BindsInstance
import dagger.Subcomponent
import io.novafoundation.nova.common.di.scope.ScreenScope
import io.novafoundation.nova.feature_push_notifications.presentation.governance.PushGovernanceSettingsFragment
import io.novafoundation.nova.feature_push_notifications.presentation.governance.PushGovernanceSettingsRequester
@Subcomponent(
modules = [
PushGovernanceSettingsModule::class
]
)
@ScreenScope
interface PushGovernanceSettingsComponent {
@Subcomponent.Factory
interface Factory {
fun create(
@BindsInstance fragment: Fragment,
@BindsInstance request: PushGovernanceSettingsRequester.Request
): PushGovernanceSettingsComponent
}
fun inject(fragment: PushGovernanceSettingsFragment)
}
@@ -0,0 +1,53 @@
package io.novafoundation.nova.feature_push_notifications.presentation.governance.di
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoMap
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.list.SelectTracksCommunicator
import io.novafoundation.nova.feature_push_notifications.PushNotificationsRouter
import io.novafoundation.nova.feature_push_notifications.domain.interactor.GovernancePushSettingsInteractor
import io.novafoundation.nova.feature_push_notifications.presentation.governance.PushGovernanceSettingsCommunicator
import io.novafoundation.nova.feature_push_notifications.presentation.governance.PushGovernanceSettingsRequester
import io.novafoundation.nova.feature_push_notifications.presentation.governance.PushGovernanceSettingsViewModel
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
@Module(includes = [ViewModelModule::class])
class PushGovernanceSettingsModule {
@Provides
@IntoMap
@ViewModelKey(PushGovernanceSettingsViewModel::class)
fun provideViewModel(
router: PushNotificationsRouter,
interactor: GovernancePushSettingsInteractor,
pushGovernanceSettingsCommunicator: PushGovernanceSettingsCommunicator,
chainRegistry: ChainRegistry,
request: PushGovernanceSettingsRequester.Request,
selectTracksCommunicator: SelectTracksCommunicator,
resourceManager: ResourceManager
): ViewModel {
return PushGovernanceSettingsViewModel(
router = router,
interactor = interactor,
pushGovernanceSettingsResponder = pushGovernanceSettingsCommunicator,
chainRegistry = chainRegistry,
request = request,
selectTracksRequester = selectTracksCommunicator,
resourceManager = resourceManager
)
}
@Provides
fun provideViewModelCreator(
fragment: Fragment,
viewModelFactory: ViewModelProvider.Factory
): PushGovernanceSettingsViewModel {
return ViewModelProvider(fragment, viewModelFactory).get(PushGovernanceSettingsViewModel::class.java)
}
}
@@ -0,0 +1,62 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
import androidx.core.app.NotificationManagerCompat
import com.google.firebase.messaging.RemoteMessage
import com.google.gson.Gson
import io.novafoundation.nova.common.interfaces.ActivityIntentProvider
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.fromJson
abstract class BaseNotificationHandler(
private val activityIntentProvider: ActivityIntentProvider,
private val notificationIdProvider: NotificationIdProvider,
private val gson: Gson,
private val notificationManager: NotificationManagerCompat,
val resourceManager: ResourceManager,
private val channel: NovaNotificationChannel,
private val importance: Int = NotificationManager.IMPORTANCE_DEFAULT,
) : NotificationHandler {
final override suspend fun handleNotification(message: RemoteMessage): Boolean {
val channelId = resourceManager.getString(channel.idRes)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
resourceManager.getString(channel.nameRes),
importance
)
notificationManager.createNotificationChannel(channel)
}
return runCatching { handleNotificationInternal(channelId, message) }
.onFailure { it.printStackTrace() }
.getOrNull() ?: false
}
internal fun notify(notification: Notification) {
notificationManager.notify(notificationIdProvider.getId(), notification)
}
internal fun notify(id: Int, notification: Notification) {
notificationManager.notify(id, notification)
}
internal fun activityIntent() = activityIntentProvider.getIntent()
protected abstract suspend fun handleNotificationInternal(channelId: String, message: RemoteMessage): Boolean
internal fun RemoteMessage.getMessageContent(): NotificationData {
val payload: Map<String, Any> = data["payload"]?.let { payload -> gson.fromJson(payload) } ?: emptyMap()
return NotificationData(
type = data.getValue("type"),
chainId = data["chainId"],
payload = payload
)
}
}
@@ -0,0 +1,18 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling
import com.google.firebase.messaging.RemoteMessage
class CompoundNotificationHandler(
val handlers: Set<NotificationHandler>
) : NotificationHandler {
override suspend fun handleNotification(message: RemoteMessage): Boolean {
for (handler in handlers) {
if (handler.handleNotification(message)) {
return true
}
}
return false
}
}
@@ -0,0 +1,7 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling
class NotificationData(
val type: String,
val chainId: String?,
val payload: Map<String, Any>
)
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling
import com.google.firebase.messaging.RemoteMessage
interface NotificationHandler {
/**
* @return true if the notification was handled, false otherwise
*/
suspend fun handleNotification(message: RemoteMessage): Boolean
}
@@ -0,0 +1,136 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.app.NotificationCompat
import io.novafoundation.nova.common.utils.asGsonParsedNumber
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_currency_api.presentation.formatters.formatAsCurrency
import io.novafoundation.nova.feature_governance_api.domain.referendum.list.ReferendumStatusType
import io.novafoundation.nova.feature_push_notifications.R
import io.novafoundation.nova.feature_wallet_api.domain.model.Token
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatPlanks
import io.novafoundation.nova.runtime.ext.chainIdHexPrefix16
import io.novafoundation.nova.runtime.ext.onChainAssetId
import io.novafoundation.nova.runtime.ext.utilityAsset
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chainsById
import java.math.BigInteger
private const val PEDDING_INTENT_REQUEST_CODE = 1
interface PushChainRegestryHolder {
val chainRegistry: ChainRegistry
suspend fun NotificationData.getChain(): Chain {
val chainId = chainId ?: throw NullPointerException("Chain id is null")
return chainRegistry.chainsById()
.mapKeys { it.key.chainIdHexPrefix16() }
.getValue(chainId)
}
}
internal fun NotificationData.requireType(type: String) {
require(this.type == type)
}
/**
* Example: {a_field: {b_field: {c_field: "value"}}}
* To take a value from c_field use getPayloadFieldContent("a_field", "b_field", "c_field")
*/
internal inline fun <reified T> NotificationData.extractPayloadFieldsWithPath(vararg fields: String): T {
val fieldsBeforeLast = fields.dropLast(1)
val last = fields.last()
val lastSearchingValue = fieldsBeforeLast.fold(payload) { acc, field ->
acc[field] as? Map<String, Any> ?: throw NullPointerException("Notification parameter $field is null")
}
val result = lastSearchingValue[last] ?: return null as T
return result as? T ?: throw NullPointerException("Notification parameter $last is null")
}
internal fun NotificationData.extractBigInteger(vararg fields: String): BigInteger {
return extractPayloadFieldsWithPath<Any>(*fields)
.asGsonParsedNumber()
}
internal fun MetaAccount.formattedAccountName(): String {
return "[$name]"
}
fun Context.makePendingIntent(intent: Intent): PendingIntent {
return PendingIntent.getActivity(
this,
PEDDING_INTENT_REQUEST_CODE,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
fun NotificationCompat.Builder.buildWithDefaults(
context: Context,
title: CharSequence,
message: CharSequence,
contentIntent: Intent
): NotificationCompat.Builder {
return setContentTitle(title)
.setContentText(message)
.setSmallIcon(R.drawable.ic_pezkuwi)
.setOnlyAlertOnce(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setStyle(
NotificationCompat.BigTextStyle()
.bigText(message)
)
.setContentIntent(context.makePendingIntent(contentIntent))
}
fun makeNewReleasesIntent(
storeLink: String
): Intent {
return Intent(Intent.ACTION_VIEW).apply { data = Uri.parse(storeLink) }
}
fun ReferendumStatusType.Companion.fromRemoteNotificationType(type: String): ReferendumStatusType {
return when (type) {
"Created" -> ReferendumStatusType.PREPARING
"Deciding" -> ReferendumStatusType.DECIDING
"Confirming" -> ReferendumStatusType.CONFIRMING
"Approved" -> ReferendumStatusType.APPROVED
"Rejected" -> ReferendumStatusType.REJECTED
"TimedOut" -> ReferendumStatusType.TIMED_OUT
"Cancelled" -> ReferendumStatusType.CANCELLED
"Killed" -> ReferendumStatusType.KILLED
else -> throw IllegalArgumentException("Unknown referendum status type: $this")
}
}
fun Chain.assetByOnChainAssetIdOrUtility(assetId: String?): Chain.Asset? {
if (assetId == null) return utilityAsset
return assets.firstOrNull { it.onChainAssetId == assetId }
}
fun notificationAmountFormat(asset: Chain.Asset, token: Token?, amount: BigInteger): String {
val tokenAmount = amount.formatPlanks(asset)
val fiatAmount = token?.planksToFiat(amount)
?.formatAsCurrency(token.currency)
return if (fiatAmount != null) {
"$tokenAmount ($fiatAmount)"
} else {
tokenAmount
}
}
suspend fun AccountRepository.isNotSingleMetaAccount(): Boolean {
return getActiveMetaAccountsQuantity() > 1
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling
import io.novafoundation.nova.common.data.storage.Preferences
interface NotificationIdProvider {
fun getId(): Int
}
class RealNotificationIdProvider(
private val preferences: Preferences
) : NotificationIdProvider {
override fun getId(): Int {
val id = preferences.getInt(KEY, START_ID)
preferences.putInt(KEY, id + 1)
return id
}
companion object {
private const val KEY = "notification_id"
private const val START_ID = 0
}
}
@@ -0,0 +1,20 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling
import androidx.annotation.StringRes
import io.novafoundation.nova.feature_push_notifications.R
enum class NovaNotificationChannel(
@StringRes val idRes: Int,
@StringRes val nameRes: Int
) {
DEFAULT(R.string.default_notification_channel_id, R.string.default_notification_channel_name),
GOVERNANCE(R.string.governance_notification_channel_id, R.string.governance_notification_channel_name),
TRANSACTIONS(R.string.transactions_notification_channel_id, R.string.transactions_notification_channel_name),
STAKING(R.string.staking_notification_channel_id, R.string.staking_notification_channel_name),
MULTISIG(R.string.multisigs_notification_channel_id, R.string.multisigs_notification_channel_name),
}
@@ -0,0 +1,60 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling.types
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.google.firebase.messaging.RemoteMessage
import io.novafoundation.nova.common.interfaces.ActivityIntentProvider
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_push_notifications.BuildConfig
import io.novafoundation.nova.feature_push_notifications.R
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.buildWithDefaults
private const val DEBUG_NOTIFICATION_ID = -1
/**
* A [NotificationHandler] that is used as a fallback if previous handlers didn't handle the notification
*/
class DebugNotificationHandler(
private val context: Context,
private val activityIntentProvider: ActivityIntentProvider,
private val notificationManager: NotificationManagerCompat,
private val resourceManager: ResourceManager
) : NotificationHandler {
override suspend fun handleNotification(message: RemoteMessage): Boolean {
if (!BuildConfig.DEBUG) return false
val channelId = resourceManager.getString(R.string.default_notification_channel_id)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
resourceManager.getString(R.string.default_notification_channel_name),
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(channel)
}
val notification = NotificationCompat.Builder(context, channelId)
.buildWithDefaults(
context,
"Notification handling error!",
"The notification was not handled\n${message.data}",
activityIntentProvider.getIntent()
).build()
notify(notification)
return true
}
private fun notify(notification: Notification) {
notificationManager.notify(DEBUG_NOTIFICATION_ID, notification)
}
}
@@ -0,0 +1,70 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling.types
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.google.firebase.messaging.RemoteMessage
import com.google.gson.Gson
import io.novafoundation.nova.common.interfaces.ActivityIntentProvider
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.formatting.format
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.applyDeepLink
import io.novafoundation.nova.feature_governance_api.presentation.referenda.details.deeplink.configurators.ReferendumDeepLinkData
import io.novafoundation.nova.feature_governance_api.presentation.referenda.details.deeplink.configurators.ReferendumDetailsDeepLinkConfigurator
import io.novafoundation.nova.feature_push_notifications.R
import io.novafoundation.nova.feature_push_notifications.data.NotificationTypes
import io.novafoundation.nova.feature_push_notifications.presentation.handling.BaseNotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NotificationIdProvider
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NovaNotificationChannel
import io.novafoundation.nova.feature_push_notifications.presentation.handling.PushChainRegestryHolder
import io.novafoundation.nova.feature_push_notifications.presentation.handling.buildWithDefaults
import io.novafoundation.nova.feature_push_notifications.presentation.handling.extractBigInteger
import io.novafoundation.nova.feature_push_notifications.presentation.handling.requireType
import io.novafoundation.nova.runtime.ext.isEnabled
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
class NewReferendumNotificationHandler(
private val context: Context,
private val configurator: ReferendumDetailsDeepLinkConfigurator,
override val chainRegistry: ChainRegistry,
activityIntentProvider: ActivityIntentProvider,
notificationIdProvider: NotificationIdProvider,
gson: Gson,
notificationManager: NotificationManagerCompat,
resourceManager: ResourceManager,
) : BaseNotificationHandler(
activityIntentProvider,
notificationIdProvider,
gson,
notificationManager,
resourceManager,
channel = NovaNotificationChannel.GOVERNANCE
),
PushChainRegestryHolder {
override suspend fun handleNotificationInternal(channelId: String, message: RemoteMessage): Boolean {
val content = message.getMessageContent()
content.requireType(NotificationTypes.GOV_NEW_REF)
val chain = content.getChain()
require(chain.isEnabled)
val referendumId = content.extractBigInteger("referendumId")
val notification = NotificationCompat.Builder(context, channelId)
.buildWithDefaults(
context,
resourceManager.getString(R.string.push_new_referendum_title),
resourceManager.getString(R.string.push_new_referendum_message, chain.name, referendumId.format()),
activityIntent().applyDeepLink(
configurator,
ReferendumDeepLinkData(chain.id, referendumId, Chain.Governance.V2)
)
).build()
notify(notification)
return true
}
}
@@ -0,0 +1,56 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling.types
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.google.firebase.messaging.RemoteMessage
import com.google.gson.Gson
import io.novafoundation.nova.common.data.network.AppLinksProvider
import io.novafoundation.nova.common.interfaces.ActivityIntentProvider
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_push_notifications.R
import io.novafoundation.nova.feature_push_notifications.data.NotificationTypes
import io.novafoundation.nova.feature_push_notifications.presentation.handling.BaseNotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NotificationIdProvider
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NovaNotificationChannel
import io.novafoundation.nova.feature_push_notifications.presentation.handling.buildWithDefaults
import io.novafoundation.nova.feature_push_notifications.presentation.handling.extractPayloadFieldsWithPath
import io.novafoundation.nova.feature_push_notifications.presentation.handling.makeNewReleasesIntent
import io.novafoundation.nova.feature_push_notifications.presentation.handling.requireType
class NewReleaseNotificationHandler(
private val context: Context,
private val appLinksProvider: AppLinksProvider,
activityIntentProvider: ActivityIntentProvider,
notificationIdProvider: NotificationIdProvider,
gson: Gson,
notificationManager: NotificationManagerCompat,
resourceManager: ResourceManager,
) : BaseNotificationHandler(
activityIntentProvider,
notificationIdProvider,
gson,
notificationManager,
resourceManager,
channel = NovaNotificationChannel.DEFAULT
) {
override suspend fun handleNotificationInternal(channelId: String, message: RemoteMessage): Boolean {
val content = message.getMessageContent()
content.requireType(NotificationTypes.APP_NEW_RELEASE)
val version = content.extractPayloadFieldsWithPath<String>("version")
val notification = NotificationCompat.Builder(context, channelId)
.buildWithDefaults(
context,
resourceManager.getString(R.string.push_new_update_title),
resourceManager.getString(R.string.push_new_update_message, version),
makeNewReleasesIntent(appLinksProvider.storeUrl)
)
.build()
notify(notification)
return true
}
}
@@ -0,0 +1,107 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling.types
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.google.firebase.messaging.RemoteMessage
import com.google.gson.Gson
import io.novafoundation.nova.common.interfaces.ActivityIntentProvider
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.formatting.format
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.applyDeepLink
import io.novafoundation.nova.feature_governance_api.domain.referendum.list.ReferendumStatusType
import io.novafoundation.nova.feature_governance_api.presentation.referenda.common.ReferendaStatusFormatter
import io.novafoundation.nova.feature_governance_api.presentation.referenda.details.deeplink.configurators.ReferendumDeepLinkData
import io.novafoundation.nova.feature_governance_api.presentation.referenda.details.deeplink.configurators.ReferendumDetailsDeepLinkConfigurator
import io.novafoundation.nova.feature_push_notifications.R
import io.novafoundation.nova.feature_push_notifications.data.NotificationTypes
import io.novafoundation.nova.feature_push_notifications.presentation.handling.BaseNotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NotificationIdProvider
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NovaNotificationChannel
import io.novafoundation.nova.feature_push_notifications.presentation.handling.PushChainRegestryHolder
import io.novafoundation.nova.feature_push_notifications.presentation.handling.buildWithDefaults
import io.novafoundation.nova.feature_push_notifications.presentation.handling.extractBigInteger
import io.novafoundation.nova.feature_push_notifications.presentation.handling.extractPayloadFieldsWithPath
import io.novafoundation.nova.feature_push_notifications.presentation.handling.fromRemoteNotificationType
import io.novafoundation.nova.feature_push_notifications.presentation.handling.requireType
import io.novafoundation.nova.runtime.ext.isEnabled
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import java.math.BigInteger
class ReferendumStateUpdateNotificationHandler(
private val context: Context,
private val configurator: ReferendumDetailsDeepLinkConfigurator,
private val referendaStatusFormatter: ReferendaStatusFormatter,
override val chainRegistry: ChainRegistry,
activityIntentProvider: ActivityIntentProvider,
notificationIdProvider: NotificationIdProvider,
gson: Gson,
notificationManager: NotificationManagerCompat,
resourceManager: ResourceManager,
) : BaseNotificationHandler(
activityIntentProvider,
notificationIdProvider,
gson,
notificationManager,
resourceManager,
channel = NovaNotificationChannel.GOVERNANCE
),
PushChainRegestryHolder {
override suspend fun handleNotificationInternal(channelId: String, message: RemoteMessage): Boolean {
val content = message.getMessageContent()
content.requireType(NotificationTypes.GOV_STATE)
val chain = content.getChain()
require(chain.isEnabled)
val referendumId = content.extractBigInteger("referendumId")
val stateFrom = content.extractPayloadFieldsWithPath<String?>("from")?.let { ReferendumStatusType.fromRemoteNotificationType(it) }
val stateTo = content.extractPayloadFieldsWithPath<String>("to").let { ReferendumStatusType.fromRemoteNotificationType(it) }
val notification = NotificationCompat.Builder(context, channelId)
.buildWithDefaults(
context,
getTitle(stateTo),
getMessage(chain, referendumId, stateFrom, stateTo),
activityIntent().applyDeepLink(
configurator,
ReferendumDeepLinkData(chain.id, referendumId, Chain.Governance.V2)
)
).build()
notify(notification)
return true
}
private fun getTitle(refStateTo: ReferendumStatusType): String {
return when (refStateTo) {
ReferendumStatusType.APPROVED -> resourceManager.getString(R.string.push_referendum_approved_title)
ReferendumStatusType.REJECTED -> resourceManager.getString(R.string.push_referendum_rejected_title)
else -> resourceManager.getString(R.string.push_referendum_status_changed_title)
}
}
private fun getMessage(chain: Chain, referendumId: BigInteger, stateFrom: ReferendumStatusType?, stateTo: ReferendumStatusType): String {
return when {
stateTo == ReferendumStatusType.APPROVED -> resourceManager.getString(R.string.push_referendum_approved_message, chain.name, referendumId.format())
stateTo == ReferendumStatusType.REJECTED -> resourceManager.getString(R.string.push_referendum_rejected_message, chain.name, referendumId.format())
stateFrom == null -> resourceManager.getString(
R.string.push_referendum_to_status_changed_message,
chain.name,
referendumId.format(),
referendaStatusFormatter.formatStatus(stateTo)
)
else -> resourceManager.getString(
R.string.push_referendum_from_to_status_changed_message,
chain.name,
referendumId.format(),
referendaStatusFormatter.formatStatus(stateFrom),
referendaStatusFormatter.formatStatus(stateTo)
)
}
}
}
@@ -0,0 +1,105 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling.types
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.google.firebase.messaging.RemoteMessage
import com.google.gson.Gson
import io.novafoundation.nova.common.interfaces.ActivityIntentProvider
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_assets.presentation.balance.detail.deeplink.AssetDetailsDeepLinkData
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.applyDeepLink
import io.novafoundation.nova.feature_push_notifications.R
import io.novafoundation.nova.feature_push_notifications.data.NotificationTypes
import io.novafoundation.nova.feature_push_notifications.presentation.handling.BaseNotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NotificationIdProvider
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NovaNotificationChannel
import io.novafoundation.nova.feature_push_notifications.presentation.handling.PushChainRegestryHolder
import io.novafoundation.nova.feature_push_notifications.presentation.handling.buildWithDefaults
import io.novafoundation.nova.feature_push_notifications.presentation.handling.extractBigInteger
import io.novafoundation.nova.feature_push_notifications.presentation.handling.extractPayloadFieldsWithPath
import io.novafoundation.nova.feature_push_notifications.presentation.handling.notificationAmountFormat
import io.novafoundation.nova.feature_push_notifications.presentation.handling.formattedAccountName
import io.novafoundation.nova.feature_push_notifications.presentation.handling.isNotSingleMetaAccount
import io.novafoundation.nova.feature_push_notifications.presentation.handling.requireType
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository
import io.novafoundation.nova.runtime.ext.accountIdOf
import io.novafoundation.nova.runtime.ext.isEnabled
import io.novafoundation.nova.runtime.ext.utilityAsset
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import java.math.BigInteger
class StakingRewardNotificationHandler(
private val context: Context,
private val accountRepository: AccountRepository,
private val tokenRepository: TokenRepository,
private val configurator: io.novafoundation.nova.feature_assets.presentation.balance.detail.deeplink.AssetDetailsDeepLinkConfigurator,
override val chainRegistry: ChainRegistry,
activityIntentProvider: ActivityIntentProvider,
notificationIdProvider: NotificationIdProvider,
gson: Gson,
notificationManager: NotificationManagerCompat,
resourceManager: ResourceManager,
) : BaseNotificationHandler(
activityIntentProvider,
notificationIdProvider,
gson,
notificationManager,
resourceManager,
channel = NovaNotificationChannel.STAKING
),
PushChainRegestryHolder {
override suspend fun handleNotificationInternal(channelId: String, message: RemoteMessage): Boolean {
val content = message.getMessageContent()
content.requireType(NotificationTypes.STAKING_REWARD)
val chain = content.getChain()
require(chain.isEnabled)
val recipient = content.extractPayloadFieldsWithPath<String>("recipient")
val amount = content.extractBigInteger("amount")
val metaAccount = accountRepository.findMetaAccount(chain.accountIdOf(recipient), chain.id) ?: return false
val notification = NotificationCompat.Builder(context, channelId)
.buildWithDefaults(
context,
getTitle(metaAccount),
getMessage(chain, amount),
activityIntent().applyDeepLink(
configurator,
AssetDetailsDeepLinkData(recipient, chain.id, chain.utilityAsset.id)
)
).build()
notify(notification)
return true
}
private suspend fun getTitle(metaAccount: MetaAccount): String {
return when {
accountRepository.isNotSingleMetaAccount() -> resourceManager.getString(
R.string.push_staking_reward_many_accounts_title,
metaAccount.formattedAccountName()
)
else -> resourceManager.getString(R.string.push_staking_reward_single_account_title)
}
}
private suspend fun getMessage(
chain: Chain,
amount: BigInteger
): String {
val asset = chain.utilityAsset
val token = tokenRepository.getTokenOrNull(asset)
val formattedAmount = notificationAmountFormat(asset, token, amount)
return resourceManager.getString(R.string.push_staking_reward_message, formattedAmount, chain.name)
}
}
@@ -0,0 +1,45 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling.types
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.google.firebase.messaging.RemoteMessage
import com.google.gson.Gson
import io.novafoundation.nova.common.interfaces.ActivityIntentProvider
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_push_notifications.presentation.handling.BaseNotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NotificationIdProvider
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NovaNotificationChannel
import io.novafoundation.nova.feature_push_notifications.presentation.handling.buildWithDefaults
class SystemNotificationHandler(
private val context: Context,
activityIntentProvider: ActivityIntentProvider,
notificationIdProvider: NotificationIdProvider,
gson: Gson,
notificationManager: NotificationManagerCompat,
resourceManager: ResourceManager,
) : BaseNotificationHandler(
activityIntentProvider,
notificationIdProvider,
gson,
notificationManager,
resourceManager,
channel = NovaNotificationChannel.DEFAULT
) {
override suspend fun handleNotificationInternal(channelId: String, message: RemoteMessage): Boolean {
val notificationPart = message.notification ?: return false
val title = notificationPart.title ?: return false
val body = notificationPart.body ?: return false
val notification = NotificationCompat.Builder(context, channelId)
.buildWithDefaults(context, title, body, activityIntent())
.build()
notify(notification)
return true
}
}
@@ -0,0 +1,105 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling.types
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.google.firebase.messaging.RemoteMessage
import com.google.gson.Gson
import io.novafoundation.nova.common.interfaces.ActivityIntentProvider
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_assets.presentation.balance.detail.deeplink.AssetDetailsDeepLinkConfigurator
import io.novafoundation.nova.feature_assets.presentation.balance.detail.deeplink.AssetDetailsDeepLinkData
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.applyDeepLink
import io.novafoundation.nova.feature_push_notifications.R
import io.novafoundation.nova.feature_push_notifications.data.NotificationTypes
import io.novafoundation.nova.feature_push_notifications.presentation.handling.BaseNotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NotificationIdProvider
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NovaNotificationChannel
import io.novafoundation.nova.feature_push_notifications.presentation.handling.PushChainRegestryHolder
import io.novafoundation.nova.feature_push_notifications.presentation.handling.assetByOnChainAssetIdOrUtility
import io.novafoundation.nova.feature_push_notifications.presentation.handling.buildWithDefaults
import io.novafoundation.nova.feature_push_notifications.presentation.handling.extractBigInteger
import io.novafoundation.nova.feature_push_notifications.presentation.handling.extractPayloadFieldsWithPath
import io.novafoundation.nova.feature_push_notifications.presentation.handling.formattedAccountName
import io.novafoundation.nova.feature_push_notifications.presentation.handling.isNotSingleMetaAccount
import io.novafoundation.nova.feature_push_notifications.presentation.handling.notificationAmountFormat
import io.novafoundation.nova.feature_push_notifications.presentation.handling.requireType
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository
import io.novafoundation.nova.runtime.ext.accountIdOf
import io.novafoundation.nova.runtime.ext.isEnabled
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import java.math.BigInteger
class TokenReceivedNotificationHandler(
private val context: Context,
private val accountRepository: AccountRepository,
private val tokenRepository: TokenRepository,
private val configurator: io.novafoundation.nova.feature_assets.presentation.balance.detail.deeplink.AssetDetailsDeepLinkConfigurator,
override val chainRegistry: ChainRegistry,
activityIntentProvider: ActivityIntentProvider,
notificationIdProvider: NotificationIdProvider,
gson: Gson,
notificationManager: NotificationManagerCompat,
resourceManager: ResourceManager,
) : BaseNotificationHandler(
activityIntentProvider,
notificationIdProvider,
gson,
notificationManager,
resourceManager,
channel = NovaNotificationChannel.TRANSACTIONS
),
PushChainRegestryHolder {
override suspend fun handleNotificationInternal(channelId: String, message: RemoteMessage): Boolean {
val content = message.getMessageContent()
content.requireType(NotificationTypes.TOKENS_RECEIVED)
val chain = content.getChain()
require(chain.isEnabled)
val recipient = content.extractPayloadFieldsWithPath<String>("recipient")
val assetId = content.extractPayloadFieldsWithPath<String?>("assetId")
val amount = content.extractBigInteger("amount")
val asset = chain.assetByOnChainAssetIdOrUtility(assetId) ?: return false
val recipientMetaAccount = accountRepository.findMetaAccount(chain.accountIdOf(recipient), chain.id) ?: return false
val notification = NotificationCompat.Builder(context, channelId)
.buildWithDefaults(
context,
getTitle(recipientMetaAccount),
getMessage(chain, asset, amount),
activityIntent().applyDeepLink(
configurator,
AssetDetailsDeepLinkData(recipient, chain.id, asset.id)
)
).build()
notify(notification)
return true
}
private suspend fun getTitle(senderMetaAccount: MetaAccount?): String {
val accountName = senderMetaAccount?.formattedAccountName()
return when {
accountRepository.isNotSingleMetaAccount() && accountName != null -> resourceManager.getString(R.string.push_token_received_title, accountName)
else -> resourceManager.getString(R.string.push_token_received_no_account_name_title)
}
}
private suspend fun getMessage(
chain: Chain,
asset: Chain.Asset,
amount: BigInteger
): String {
val token = tokenRepository.getTokenOrNull(asset)
val formattedAmount = notificationAmountFormat(asset, token, amount)
return resourceManager.getString(R.string.push_token_received_message, formattedAmount, chain.name)
}
}
@@ -0,0 +1,111 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling.types
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.google.firebase.messaging.RemoteMessage
import com.google.gson.Gson
import io.novafoundation.nova.common.interfaces.ActivityIntentProvider
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_assets.presentation.balance.detail.deeplink.AssetDetailsDeepLinkConfigurator
import io.novafoundation.nova.feature_assets.presentation.balance.detail.deeplink.AssetDetailsDeepLinkData
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.applyDeepLink
import io.novafoundation.nova.feature_push_notifications.R
import io.novafoundation.nova.feature_push_notifications.data.NotificationTypes
import io.novafoundation.nova.feature_push_notifications.presentation.handling.BaseNotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NotificationIdProvider
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NovaNotificationChannel
import io.novafoundation.nova.feature_push_notifications.presentation.handling.PushChainRegestryHolder
import io.novafoundation.nova.feature_push_notifications.presentation.handling.assetByOnChainAssetIdOrUtility
import io.novafoundation.nova.feature_push_notifications.presentation.handling.buildWithDefaults
import io.novafoundation.nova.feature_push_notifications.presentation.handling.extractBigInteger
import io.novafoundation.nova.feature_push_notifications.presentation.handling.extractPayloadFieldsWithPath
import io.novafoundation.nova.feature_push_notifications.presentation.handling.formattedAccountName
import io.novafoundation.nova.feature_push_notifications.presentation.handling.isNotSingleMetaAccount
import io.novafoundation.nova.feature_push_notifications.presentation.handling.notificationAmountFormat
import io.novafoundation.nova.feature_push_notifications.presentation.handling.requireType
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository
import io.novafoundation.nova.runtime.ext.accountIdOf
import io.novafoundation.nova.runtime.ext.isEnabled
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import java.math.BigInteger
class TokenSentNotificationHandler(
private val context: Context,
private val accountRepository: AccountRepository,
private val tokenRepository: TokenRepository,
override val chainRegistry: ChainRegistry,
private val configurator: AssetDetailsDeepLinkConfigurator,
activityIntentProvider: ActivityIntentProvider,
notificationIdProvider: NotificationIdProvider,
gson: Gson,
notificationManager: NotificationManagerCompat,
resourceManager: ResourceManager,
) : BaseNotificationHandler(
activityIntentProvider,
notificationIdProvider,
gson,
notificationManager,
resourceManager,
channel = NovaNotificationChannel.TRANSACTIONS
),
PushChainRegestryHolder {
override suspend fun handleNotificationInternal(channelId: String, message: RemoteMessage): Boolean {
val content = message.getMessageContent()
content.requireType(NotificationTypes.TOKENS_SENT)
val chain = content.getChain()
require(chain.isEnabled)
val sender = content.extractPayloadFieldsWithPath<String>("sender")
val recipient = content.extractPayloadFieldsWithPath<String>("recipient")
val assetId = content.extractPayloadFieldsWithPath<String?>("assetId")
val amount = content.extractBigInteger("amount")
val asset = chain.assetByOnChainAssetIdOrUtility(assetId) ?: return false
val senderMetaAccount = accountRepository.findMetaAccount(chain.accountIdOf(sender), chain.id) ?: return false
val recipientMetaAccount = accountRepository.findMetaAccount(chain.accountIdOf(recipient), chain.id)
val notification = NotificationCompat.Builder(context, channelId)
.buildWithDefaults(
context,
getTitle(senderMetaAccount),
getMessage(chain, recipientMetaAccount, recipient, asset, amount),
activityIntent().applyDeepLink(
configurator,
AssetDetailsDeepLinkData(sender, chain.id, asset.id)
)
).build()
notify(notification)
return true
}
private suspend fun getTitle(senderMetaAccount: MetaAccount?): String {
val accountName = senderMetaAccount?.formattedAccountName()
return when {
accountRepository.isNotSingleMetaAccount() && accountName != null -> resourceManager.getString(R.string.push_token_sent_title, accountName)
else -> resourceManager.getString(R.string.push_token_sent_no_account_name_title)
}
}
private suspend fun getMessage(
chain: Chain,
recipientMetaAccount: MetaAccount?,
recipientAddress: String,
asset: Chain.Asset,
amount: BigInteger
): String {
val token = tokenRepository.getTokenOrNull(asset)
val formattedAmount = notificationAmountFormat(asset, token, amount)
val accountNameOrAddress = recipientMetaAccount?.formattedAccountName() ?: recipientAddress
return resourceManager.getString(R.string.push_token_sent_message, formattedAmount, accountNameOrAddress, chain.name)
}
}
@@ -0,0 +1,62 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling.types.multisig
import androidx.core.app.NotificationManagerCompat
import com.google.gson.Gson
import io.novafoundation.nova.common.address.AddressModel
import io.novafoundation.nova.common.interfaces.ActivityIntentProvider
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
import io.novafoundation.nova.feature_push_notifications.R
import io.novafoundation.nova.feature_push_notifications.presentation.handling.BaseNotificationHandler
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NotificationIdProvider
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NovaNotificationChannel
import io.novafoundation.nova.feature_push_notifications.presentation.handling.PushChainRegestryHolder
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHex
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
abstract class MultisigBaseNotificationHandler(
private val multisigCallFormatter: MultisigCallFormatter,
override val chainRegistry: ChainRegistry,
activityIntentProvider: ActivityIntentProvider,
notificationIdProvider: NotificationIdProvider,
gson: Gson,
notificationManager: NotificationManagerCompat,
resourceManager: ResourceManager
) : BaseNotificationHandler(
activityIntentProvider,
notificationIdProvider,
gson,
notificationManager,
resourceManager,
channel = NovaNotificationChannel.MULTISIG
),
PushChainRegestryHolder {
fun getSubText(metaAccount: MetaAccount): String {
return resourceManager.getString(R.string.multisig_notification_message_header, metaAccount.name)
}
suspend fun getMessage(
chain: Chain,
payload: MultisigNotificationPayload,
footer: String?
): String {
val runtime = chainRegistry.getRuntime(chain.id)
val call = payload.callData?.let { GenericCall.fromHex(runtime, payload.callData) }
return buildString {
val formattedCall = multisigCallFormatter.formatPushNotificationMessage(call, payload.signatory.accountId, chain)
append(formattedCall.formattedCall)
formattedCall.onBehalfOf?.let { appendLine().append(formatOnBehalfOf(it)) }
footer?.let { appendLine().append(it) }
}
}
private fun formatOnBehalfOf(addressMode: AddressModel): String {
return resourceManager.getString(R.string.multisig_notification_on_behalf_of, addressMode.nameOrAddress)
}
}
@@ -0,0 +1,96 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling.types.multisig
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.google.firebase.messaging.RemoteMessage
import com.google.gson.Gson
import io.novafoundation.nova.common.interfaces.ActivityIntentProvider
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperation
import io.novafoundation.nova.feature_account_api.data.multisig.model.createOperationHash
import io.novafoundation.nova.feature_account_api.domain.account.identity.IdentityProvider
import io.novafoundation.nova.feature_account_api.domain.account.identity.LocalIdentity
import io.novafoundation.nova.feature_account_api.domain.account.identity.getNameOrAddress
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.applyDeepLink
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkConfigurator
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkData
import io.novafoundation.nova.feature_push_notifications.R
import io.novafoundation.nova.feature_push_notifications.data.NotificationTypes
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NotificationIdProvider
import io.novafoundation.nova.feature_push_notifications.presentation.handling.PushChainRegestryHolder
import io.novafoundation.nova.feature_push_notifications.presentation.handling.buildWithDefaults
import io.novafoundation.nova.feature_push_notifications.presentation.handling.requireType
import io.novafoundation.nova.runtime.ext.isEnabled
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
class MultisigTransactionCancelledNotificationHandler(
private val context: Context,
private val accountRepository: AccountRepository,
private val multisigCallFormatter: MultisigCallFormatter,
private val configurator: MultisigOperationDeepLinkConfigurator,
@LocalIdentity private val identityProvider: IdentityProvider,
override val chainRegistry: ChainRegistry,
activityIntentProvider: ActivityIntentProvider,
notificationIdProvider: NotificationIdProvider,
gson: Gson,
notificationManager: NotificationManagerCompat,
resourceManager: ResourceManager,
) : MultisigBaseNotificationHandler(
multisigCallFormatter,
chainRegistry,
activityIntentProvider,
notificationIdProvider,
gson,
notificationManager,
resourceManager
),
PushChainRegestryHolder {
override suspend fun handleNotificationInternal(channelId: String, message: RemoteMessage): Boolean {
val content = message.getMessageContent()
content.requireType(NotificationTypes.MULTISIG_CANCELLED)
val chain = content.getChain()
require(chain.isEnabled)
val payload = content.extractMultisigPayload(signatoryRole = "canceller", chain)
val multisigAccount = accountRepository.getMultisigForPayload(chain, payload) ?: return true
val rejecterIdentity = identityProvider.getNameOrAddress(payload.signatory.accountId, chain)
val messageText = getMessage(
chain,
payload,
footer = resourceManager.getString(R.string.multisig_notification_rejected_transaction_message, rejecterIdentity)
)
val operationHash = PendingMultisigOperation.createOperationHash(multisigAccount, chain, payload.callHashString)
val notification = NotificationCompat.Builder(context, channelId)
.setSubText(getSubText(multisigAccount))
.setGroup(operationHash)
.buildWithDefaults(
context,
resourceManager.getString(R.string.multisig_notification_rejected_transaction_title),
messageText,
activityIntent().applyDeepLink(
configurator,
multisigOperationDeepLinkData(multisigAccount, chain, payload, MultisigOperationDeepLinkData.State.Rejected(rejecterIdentity))
)
).build()
notify(notification)
notifyMultisigGroupNotificationWithId(
context = context,
groupId = operationHash,
channelId = channelId,
metaAccount = multisigAccount
)
return true
}
}
@@ -0,0 +1,96 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling.types.multisig
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.google.firebase.messaging.RemoteMessage
import com.google.gson.Gson
import io.novafoundation.nova.common.interfaces.ActivityIntentProvider
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperation
import io.novafoundation.nova.feature_account_api.data.multisig.model.createOperationHash
import io.novafoundation.nova.feature_account_api.domain.account.identity.IdentityProvider
import io.novafoundation.nova.feature_account_api.domain.account.identity.LocalIdentity
import io.novafoundation.nova.feature_account_api.domain.account.identity.getNameOrAddress
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.applyDeepLink
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkConfigurator
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkData
import io.novafoundation.nova.feature_push_notifications.R
import io.novafoundation.nova.feature_push_notifications.data.NotificationTypes
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NotificationIdProvider
import io.novafoundation.nova.feature_push_notifications.presentation.handling.PushChainRegestryHolder
import io.novafoundation.nova.feature_push_notifications.presentation.handling.buildWithDefaults
import io.novafoundation.nova.feature_push_notifications.presentation.handling.requireType
import io.novafoundation.nova.runtime.ext.isEnabled
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
class MultisigTransactionExecutedNotificationHandler(
private val context: Context,
private val accountRepository: AccountRepository,
private val multisigCallFormatter: MultisigCallFormatter,
private val configurator: MultisigOperationDeepLinkConfigurator,
@LocalIdentity private val identityProvider: IdentityProvider,
override val chainRegistry: ChainRegistry,
activityIntentProvider: ActivityIntentProvider,
notificationIdProvider: NotificationIdProvider,
gson: Gson,
notificationManager: NotificationManagerCompat,
resourceManager: ResourceManager,
) : MultisigBaseNotificationHandler(
multisigCallFormatter,
chainRegistry,
activityIntentProvider,
notificationIdProvider,
gson,
notificationManager,
resourceManager
),
PushChainRegestryHolder {
override suspend fun handleNotificationInternal(channelId: String, message: RemoteMessage): Boolean {
val content = message.getMessageContent()
content.requireType(NotificationTypes.MULTISIG_EXECUTED)
val chain = content.getChain()
require(chain.isEnabled)
val payload = content.extractMultisigPayload(signatoryRole = "approver", chain)
val multisigAccount = accountRepository.getMultisigForPayload(chain, payload) ?: return true
val approverIdentity = identityProvider.getNameOrAddress(payload.signatory.accountId, chain)
val messageText = getMessage(
chain,
payload,
footer = resourceManager.getString(R.string.multisig_notification_executed_transaction_message, approverIdentity)
)
val operationHash = PendingMultisigOperation.createOperationHash(multisigAccount, chain, payload.callHashString)
val notification = NotificationCompat.Builder(context, channelId)
.setSubText(getSubText(multisigAccount))
.setGroup(operationHash)
.buildWithDefaults(
context,
resourceManager.getString(R.string.multisig_notification_executed_transaction_title),
messageText,
activityIntent().applyDeepLink(
configurator,
multisigOperationDeepLinkData(multisigAccount, chain, payload, MultisigOperationDeepLinkData.State.Executed(approverIdentity))
)
).build()
notify(notification)
notifyMultisigGroupNotificationWithId(
context = context,
groupId = operationHash,
channelId = channelId,
metaAccount = multisigAccount
)
return true
}
}
@@ -0,0 +1,93 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling.types.multisig
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkConfigurator
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.google.firebase.messaging.RemoteMessage
import com.google.gson.Gson
import io.novafoundation.nova.common.interfaces.ActivityIntentProvider
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperation
import io.novafoundation.nova.feature_account_api.data.multisig.model.createOperationHash
import io.novafoundation.nova.feature_account_api.domain.account.identity.IdentityProvider
import io.novafoundation.nova.feature_account_api.domain.account.identity.LocalIdentity
import io.novafoundation.nova.feature_account_api.domain.account.identity.getNameOrAddress
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.applyDeepLink
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkData
import io.novafoundation.nova.feature_push_notifications.R
import io.novafoundation.nova.feature_push_notifications.data.NotificationTypes
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NotificationIdProvider
import io.novafoundation.nova.feature_push_notifications.presentation.handling.PushChainRegestryHolder
import io.novafoundation.nova.feature_push_notifications.presentation.handling.buildWithDefaults
import io.novafoundation.nova.feature_push_notifications.presentation.handling.requireType
import io.novafoundation.nova.runtime.ext.isEnabled
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
class MultisigTransactionInitiatedNotificationHandler(
private val context: Context,
private val accountRepository: AccountRepository,
private val multisigCallFormatter: MultisigCallFormatter,
private val configurator: MultisigOperationDeepLinkConfigurator,
override val chainRegistry: ChainRegistry,
@LocalIdentity private val identityProvider: IdentityProvider,
activityIntentProvider: ActivityIntentProvider,
notificationIdProvider: NotificationIdProvider,
gson: Gson,
notificationManager: NotificationManagerCompat,
resourceManager: ResourceManager,
) : MultisigBaseNotificationHandler(
multisigCallFormatter,
chainRegistry,
activityIntentProvider,
notificationIdProvider,
gson,
notificationManager,
resourceManager
),
PushChainRegestryHolder {
override suspend fun handleNotificationInternal(channelId: String, message: RemoteMessage): Boolean {
val content = message.getMessageContent()
content.requireType(NotificationTypes.NEW_MULTISIG)
val chain = content.getChain()
require(chain.isEnabled)
val payload = content.extractMultisigPayload(signatoryRole = "initiator", chain)
val multisigAccount = accountRepository.getMultisigForPayload(chain, payload) ?: return true
val initiatorIdentity = identityProvider.getNameOrAddress(payload.signatory.accountId, chain)
val operationHash = PendingMultisigOperation.createOperationHash(multisigAccount, chain, payload.callHashString)
val notification = NotificationCompat.Builder(context, channelId)
.setSubText(getSubText(multisigAccount))
.setGroup(operationHash)
.buildWithDefaults(
context,
resourceManager.getString(R.string.multisig_notification_init_transaction_title),
getMessage(chain, payload, footer = signFooter(initiatorIdentity)),
activityIntent().applyDeepLink(
configurator,
multisigOperationDeepLinkData(multisigAccount, chain, payload, MultisigOperationDeepLinkData.State.Active)
)
).build()
notify(notification)
notifyMultisigGroupNotificationWithId(
context = context,
groupId = operationHash,
channelId = channelId,
metaAccount = multisigAccount
)
return true
}
private fun signFooter(initiatorIdentity: String) = resourceManager.getString(R.string.multisig_notification_initiator_footer, initiatorIdentity)
}
@@ -0,0 +1,105 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling.types.multisig
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkConfigurator
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.google.firebase.messaging.RemoteMessage
import com.google.gson.Gson
import io.novafoundation.nova.common.interfaces.ActivityIntentProvider
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigDetailsRepository
import io.novafoundation.nova.feature_account_api.data.multisig.model.PendingMultisigOperation
import io.novafoundation.nova.feature_account_api.data.multisig.model.createOperationHash
import io.novafoundation.nova.feature_account_api.domain.account.identity.IdentityProvider
import io.novafoundation.nova.feature_account_api.domain.account.identity.LocalIdentity
import io.novafoundation.nova.feature_account_api.domain.account.identity.getNameOrAddress
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.applyDeepLink
import io.novafoundation.nova.feature_multisig_operations.presentation.callFormatting.MultisigCallFormatter
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkData
import io.novafoundation.nova.feature_push_notifications.R
import io.novafoundation.nova.feature_push_notifications.data.NotificationTypes
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NotificationIdProvider
import io.novafoundation.nova.feature_push_notifications.presentation.handling.PushChainRegestryHolder
import io.novafoundation.nova.feature_push_notifications.presentation.handling.buildWithDefaults
import io.novafoundation.nova.feature_push_notifications.presentation.handling.extractPayloadFieldsWithPath
import io.novafoundation.nova.feature_push_notifications.presentation.handling.requireType
import io.novafoundation.nova.runtime.ext.isEnabled
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
class MultisigTransactionNewApprovalNotificationHandler(
private val context: Context,
private val accountRepository: AccountRepository,
private val multisigDetailsRepository: MultisigDetailsRepository,
private val multisigCallFormatter: MultisigCallFormatter,
private val configurator: MultisigOperationDeepLinkConfigurator,
@LocalIdentity private val identityProvider: IdentityProvider,
override val chainRegistry: ChainRegistry,
activityIntentProvider: ActivityIntentProvider,
notificationIdProvider: NotificationIdProvider,
gson: Gson,
notificationManager: NotificationManagerCompat,
resourceManager: ResourceManager,
) : MultisigBaseNotificationHandler(
multisigCallFormatter,
chainRegistry,
activityIntentProvider,
notificationIdProvider,
gson,
notificationManager,
resourceManager
),
PushChainRegestryHolder {
override suspend fun handleNotificationInternal(channelId: String, message: RemoteMessage): Boolean {
val content = message.getMessageContent()
content.requireType(NotificationTypes.MULTISIG_APPROVAL)
val chain = content.getChain()
require(chain.isEnabled)
val payload = content.extractMultisigPayload(signatoryRole = "approver", chain)
val approvals = content.extractPayloadFieldsWithPath<Double?>("approvals")?.toInt() ?: return true
val multisigAccount = accountRepository.getMultisigForPayload(chain, payload) ?: return true
val approverIdentity = identityProvider.getNameOrAddress(payload.signatory.accountId, chain)
val operationHash = PendingMultisigOperation.createOperationHash(multisigAccount, chain, payload.callHashString)
val messageText = getMessage(
chain,
payload,
footer = resourceManager.getString(
R.string.multisig_notification_new_approval_title_additional_message,
approvals,
multisigAccount.threshold
)
)
val notification = NotificationCompat.Builder(context, channelId)
.setSubText(getSubText(multisigAccount))
.setGroup(operationHash)
.buildWithDefaults(
context,
resourceManager.getString(R.string.multisig_notification_new_approval_title, approverIdentity),
messageText,
activityIntent().applyDeepLink(
configurator,
multisigOperationDeepLinkData(multisigAccount, chain, payload, MultisigOperationDeepLinkData.State.Active)
)
).build()
notify(notification)
notifyMultisigGroupNotificationWithId(
context = context,
groupId = operationHash,
channelId = channelId,
metaAccount = multisigAccount
)
return true
}
}
@@ -0,0 +1,96 @@
package io.novafoundation.nova.feature_push_notifications.presentation.handling.types.multisig
import android.app.Notification
import android.content.Context
import androidx.core.app.NotificationCompat
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.MultisigMetaAccount
import io.novafoundation.nova.feature_account_api.domain.model.accountIdKeyIn
import io.novafoundation.nova.feature_account_api.domain.model.requireAddressIn
import io.novafoundation.nova.feature_multisig_operations.presentation.details.deeplink.MultisigOperationDeepLinkData
import io.novafoundation.nova.feature_push_notifications.R
import io.novafoundation.nova.feature_push_notifications.presentation.handling.NotificationData
import io.novafoundation.nova.feature_push_notifications.presentation.handling.extractPayloadFieldsWithPath
import io.novafoundation.nova.runtime.ext.addressOf
import io.novafoundation.nova.runtime.ext.toAccountIdKey
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.extensions.fromHex
class AddressWithAccountId(
val address: String,
val accountId: AccountIdKey
)
class MultisigNotificationPayload(
val multisig: AddressWithAccountId,
val signatory: AddressWithAccountId,
val callHashString: String,
val callHash: AccountIdKey,
val callData: String?
)
fun String.toAddressWithAccountId(chain: Chain) = AddressWithAccountId(this, this.toAccountIdKey(chain))
fun NotificationData.extractMultisigPayload(signatoryRole: String, chain: Chain): MultisigNotificationPayload {
val callHashString = extractPayloadFieldsWithPath<String>("callHash")
return MultisigNotificationPayload(
extractPayloadFieldsWithPath<String>("multisig").toAddressWithAccountId(chain),
extractPayloadFieldsWithPath<String>(signatoryRole).toAddressWithAccountId(chain),
callHashString,
callHashString.fromHex().intoKey(),
extractPayloadFieldsWithPath<String?>("callData")
)
}
suspend fun AccountRepository.getMultisigForPayload(chain: Chain, payload: MultisigNotificationPayload): MultisigMetaAccount? {
return getActiveMetaAccounts()
.filterIsInstance<MultisigMetaAccount>()
.filter { it.accountIdKeyIn(chain) == payload.multisig.accountId }
.getFirstActorExcept(payload.signatory)
}
fun List<MultisigMetaAccount>.getFirstActorExcept(signatory: AddressWithAccountId): MultisigMetaAccount? {
return firstOrNull { it.signatoryAccountId != signatory.accountId }
}
fun multisigOperationDeepLinkData(
metaAccount: MultisigMetaAccount,
chain: Chain,
payload: MultisigNotificationPayload,
operationState: MultisigOperationDeepLinkData.State?
): MultisigOperationDeepLinkData {
return MultisigOperationDeepLinkData(
chain.id,
metaAccount.requireAddressIn(chain),
chain.addressOf(metaAccount.signatoryAccountId),
payload.callHashString,
payload.callData,
operationState
)
}
fun MultisigBaseNotificationHandler.notifyMultisigGroupNotificationWithId(context: Context, groupId: String, channelId: String, metaAccount: MetaAccount) {
val notificationId = createGroupMessageId(groupId)
val notification = createMultisigGroupNotification(context, groupId, channelId, getSubText(metaAccount))
notify(notificationId, notification)
}
fun createGroupMessageId(groupId: String): Int {
return groupId.hashCode()
}
fun createMultisigGroupNotification(context: Context, groupId: String, channelId: String, walletName: String): Notification {
return NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_pezkuwi)
.setStyle(
NotificationCompat.InboxStyle()
.setSummaryText(walletName)
)
.setGroup(groupId)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
.setGroupSummary(true)
.build()
}
@@ -0,0 +1,20 @@
package io.novafoundation.nova.feature_push_notifications.presentation.multisigs
import android.os.Parcelable
import io.novafoundation.nova.common.navigation.InterScreenRequester
import io.novafoundation.nova.common.navigation.InterScreenResponder
import kotlinx.parcelize.Parcelize
interface PushMultisigSettingsRequester : InterScreenRequester<PushMultisigSettingsRequester.Request, PushMultisigSettingsResponder.Response> {
@Parcelize
class Request(val isAtLeastOneMultisigWalletSelected: Boolean, val settings: PushMultisigSettingsModel) : Parcelable
}
interface PushMultisigSettingsResponder : InterScreenResponder<PushMultisigSettingsRequester.Request, PushMultisigSettingsResponder.Response> {
@Parcelize
class Response(val settings: PushMultisigSettingsModel) : Parcelable
}
interface PushMultisigSettingsCommunicator : PushMultisigSettingsRequester, PushMultisigSettingsResponder
@@ -0,0 +1,71 @@
package io.novafoundation.nova.feature_push_notifications.presentation.multisigs
import io.novafoundation.nova.common.base.BaseFragment
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.mixin.impl.observeBrowserEvents
import io.novafoundation.nova.common.utils.FragmentPayloadCreator
import io.novafoundation.nova.common.utils.PayloadCreator
import io.novafoundation.nova.common.utils.payload
import io.novafoundation.nova.common.view.dialog.infoDialog
import io.novafoundation.nova.common.view.settings.SettingsSwitcherView
import io.novafoundation.nova.feature_push_notifications.R
import io.novafoundation.nova.feature_push_notifications.databinding.FragmentPushMultisigSettingsBinding
import io.novafoundation.nova.feature_push_notifications.di.PushNotificationsFeatureApi
import io.novafoundation.nova.feature_push_notifications.di.PushNotificationsFeatureComponent
import kotlinx.coroutines.flow.Flow
class PushMultisigSettingsFragment : BaseFragment<PushMultisigSettingsViewModel, FragmentPushMultisigSettingsBinding>() {
companion object : PayloadCreator<PushMultisigSettingsRequester.Request> by FragmentPayloadCreator()
override fun createBinding() = FragmentPushMultisigSettingsBinding.inflate(layoutInflater)
override fun initViews() {
binder.pushMultisigsToolbar.setHomeButtonListener { viewModel.backClicked() }
onBackPressed { viewModel.backClicked() }
binder.pushMultisigSettingsSwitcher.setOnClickListener { viewModel.switchMultisigNotificationsState() }
binder.pushMultisigInitiatingSwitcher.setOnClickListener { viewModel.switchInitialNotificationsState() }
binder.pushMultisigApprovalSwitcher.setOnClickListener { viewModel.switchApprovingNotificationsState() }
binder.pushMultisigExecutedSwitcher.setOnClickListener { viewModel.switchExecutionNotificationsState() }
binder.pushMultisigRejectedSwitcher.setOnClickListener { viewModel.switchRejectionNotificationsState() }
}
override fun inject() {
FeatureUtils.getFeature<PushNotificationsFeatureComponent>(requireContext(), PushNotificationsFeatureApi::class.java)
.pushMultisigSettings()
.create(this, payload())
.inject(this)
}
override fun subscribe(viewModel: PushMultisigSettingsViewModel) {
observeBrowserEvents(viewModel)
viewModel.isMultisigNotificationsEnabled.observe {
binder.pushMultisigSettingsSwitcher.setChecked(it)
binder.pushMultisigInitiatingSwitcher.isEnabled = it
binder.pushMultisigApprovalSwitcher.isEnabled = it
binder.pushMultisigExecutedSwitcher.isEnabled = it
binder.pushMultisigRejectedSwitcher.isEnabled = it
}
binder.pushMultisigInitiatingSwitcher.bindWithFlow(viewModel.isInitiationEnabled)
binder.pushMultisigApprovalSwitcher.bindWithFlow(viewModel.isApprovingEnabled)
binder.pushMultisigExecutedSwitcher.bindWithFlow(viewModel.isExecutionEnabled)
binder.pushMultisigRejectedSwitcher.bindWithFlow(viewModel.isRejectionEnabled)
viewModel.noOneMultisigWalletSelectedEvent.observeEvent {
infoDialog(requireContext()) {
setTitle(R.string.no_ms_accounts_found_dialog_title)
setMessage(R.string.no_ms_accounts_found_dialog_message)
setNegativeButton(R.string.common_learn_more) { _, _ -> viewModel.learnMoreClicked() }
setPositiveButton(R.string.common_got_it, null)
}
}
}
private fun SettingsSwitcherView.bindWithFlow(flow: Flow<Boolean>) {
flow.observe { setChecked(it) }
}
}
@@ -0,0 +1,30 @@
package io.novafoundation.nova.feature_push_notifications.presentation.multisigs
import android.os.Parcelable
import io.novafoundation.nova.feature_push_notifications.domain.model.PushSettings
import kotlinx.parcelize.Parcelize
@Parcelize
data class PushMultisigSettingsModel(
val isEnabled: Boolean,
val isInitiatingEnabled: Boolean,
val isApprovingEnabled: Boolean,
val isExecutionEnabled: Boolean,
val isRejectionEnabled: Boolean
) : Parcelable
fun PushMultisigSettingsModel.toDomain() = PushSettings.MultisigsState(
isEnabled = isEnabled,
isInitiatingEnabled = isInitiatingEnabled,
isApprovingEnabled = isApprovingEnabled,
isExecutionEnabled = isExecutionEnabled,
isRejectionEnabled = isRejectionEnabled
)
fun PushSettings.MultisigsState.toModel() = PushMultisigSettingsModel(
isEnabled = isEnabled,
isInitiatingEnabled = isInitiatingEnabled,
isApprovingEnabled = isApprovingEnabled,
isExecutionEnabled = isExecutionEnabled,
isRejectionEnabled = isRejectionEnabled
)
@@ -0,0 +1,105 @@
package io.novafoundation.nova.feature_push_notifications.presentation.multisigs
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import io.novafoundation.nova.common.base.BaseViewModel
import io.novafoundation.nova.common.data.network.AppLinksProvider
import io.novafoundation.nova.common.mixin.api.Browserable
import io.novafoundation.nova.common.utils.Event
import io.novafoundation.nova.common.utils.sendEvent
import io.novafoundation.nova.feature_push_notifications.PushNotificationsRouter
import io.novafoundation.nova.feature_push_notifications.domain.model.disableIfAllTypesDisabled
import io.novafoundation.nova.feature_push_notifications.domain.model.isAllTypesDisabled
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
class PushMultisigSettingsViewModel(
private val router: PushNotificationsRouter,
private val pushMultisigSettingsResponder: PushMultisigSettingsResponder,
private val request: PushMultisigSettingsRequester.Request,
private val appLinksProvider: AppLinksProvider
) : BaseViewModel(), Browserable {
private val settingsState = MutableStateFlow(request.settings.toDomain())
val isMultisigNotificationsEnabled = settingsState.map { it.isEnabled }
.distinctUntilChanged()
val isInitiationEnabled = settingsState.map { it.isInitiatingEnabled }
.distinctUntilChanged()
val isApprovingEnabled = settingsState.map { it.isApprovingEnabled }
.distinctUntilChanged()
val isExecutionEnabled = settingsState.map { it.isExecutionEnabled }
.distinctUntilChanged()
val isRejectionEnabled = settingsState.map { it.isRejectionEnabled }
.distinctUntilChanged()
private val _noOneMultisigWalletSelectedEvent = MutableLiveData<Event<Unit>>()
val noOneMultisigWalletSelectedEvent: LiveData<Event<Unit>> = _noOneMultisigWalletSelectedEvent
override val openBrowserEvent = MutableLiveData<Event<String>>()
fun backClicked() {
pushMultisigSettingsResponder.respond(PushMultisigSettingsResponder.Response(settingsState.value.toModel()))
router.back()
}
fun switchMultisigNotificationsState() {
val noMultisigWalletSelected = !request.isAtLeastOneMultisigWalletSelected
if (isMultisigNotificationsDisabled() && noMultisigWalletSelected) {
_noOneMultisigWalletSelectedEvent.sendEvent()
return
}
toggleMultisigEnablingState()
}
private fun isMultisigNotificationsDisabled() = !settingsState.value.isEnabled
private fun toggleMultisigEnablingState() {
settingsState.update {
if (!it.isEnabled && it.isAllTypesDisabled()) {
it.copy(isEnabled = true, isInitiatingEnabled = true, isApprovingEnabled = true, isExecutionEnabled = true, isRejectionEnabled = true)
} else {
it.copy(isEnabled = !it.isEnabled)
}
}
}
fun switchInitialNotificationsState() {
settingsState.update {
it.copy(isInitiatingEnabled = !it.isInitiatingEnabled)
.disableIfAllTypesDisabled()
}
}
fun switchApprovingNotificationsState() {
settingsState.update {
it.copy(isApprovingEnabled = !it.isApprovingEnabled)
.disableIfAllTypesDisabled()
}
}
fun switchExecutionNotificationsState() {
settingsState.update {
it.copy(isExecutionEnabled = !it.isExecutionEnabled)
.disableIfAllTypesDisabled()
}
}
fun switchRejectionNotificationsState() {
settingsState.update {
it.copy(isRejectionEnabled = !it.isRejectionEnabled)
.disableIfAllTypesDisabled()
}
}
fun learnMoreClicked() {
openBrowserEvent.value = Event(appLinksProvider.multisigsWikiUrl)
}
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_push_notifications.presentation.multisigs.di
import androidx.fragment.app.Fragment
import dagger.BindsInstance
import dagger.Subcomponent
import io.novafoundation.nova.common.di.scope.ScreenScope
import io.novafoundation.nova.feature_push_notifications.presentation.multisigs.PushMultisigSettingsFragment
import io.novafoundation.nova.feature_push_notifications.presentation.multisigs.PushMultisigSettingsRequester
@Subcomponent(
modules = [
PushMultisigSettingsModule::class
]
)
@ScreenScope
interface PushMultisigSettingsComponent {
@Subcomponent.Factory
interface Factory {
fun create(
@BindsInstance fragment: Fragment,
@BindsInstance request: PushMultisigSettingsRequester.Request
): PushMultisigSettingsComponent
}
fun inject(fragment: PushMultisigSettingsFragment)
}
@@ -0,0 +1,44 @@
package io.novafoundation.nova.feature_push_notifications.presentation.multisigs.di
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoMap
import io.novafoundation.nova.common.data.network.AppLinksProvider
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
import io.novafoundation.nova.feature_push_notifications.PushNotificationsRouter
import io.novafoundation.nova.feature_push_notifications.presentation.multisigs.PushMultisigSettingsCommunicator
import io.novafoundation.nova.feature_push_notifications.presentation.multisigs.PushMultisigSettingsRequester
import io.novafoundation.nova.feature_push_notifications.presentation.multisigs.PushMultisigSettingsViewModel
@Module(includes = [ViewModelModule::class])
class PushMultisigSettingsModule {
@Provides
@IntoMap
@ViewModelKey(PushMultisigSettingsViewModel::class)
fun provideViewModel(
router: PushNotificationsRouter,
pushMultisigSettingsCommunicator: PushMultisigSettingsCommunicator,
request: PushMultisigSettingsRequester.Request,
appLinksProvider: AppLinksProvider
): ViewModel {
return PushMultisigSettingsViewModel(
router = router,
pushMultisigSettingsResponder = pushMultisigSettingsCommunicator,
request = request,
appLinksProvider = appLinksProvider
)
}
@Provides
fun provideViewModelCreator(
fragment: Fragment,
viewModelFactory: ViewModelProvider.Factory
): PushMultisigSettingsViewModel {
return ViewModelProvider(fragment, viewModelFactory).get(PushMultisigSettingsViewModel::class.java)
}
}
@@ -0,0 +1,27 @@
package io.novafoundation.nova.feature_push_notifications.presentation.multisigsWarning
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import io.novafoundation.nova.common.R
import io.novafoundation.nova.common.utils.WithContextExtensions
import io.novafoundation.nova.common.view.bottomSheet.BaseBottomSheet
import io.novafoundation.nova.feature_push_notifications.databinding.FragmentEnableMultisigPushesWarningBinding
open class MultisigPushNotificationsAlertBottomSheet(
context: Context,
private val onEnableClicked: () -> Unit,
) : BaseBottomSheet<FragmentEnableMultisigPushesWarningBinding>(context, R.style.BottomSheetDialog), WithContextExtensions by WithContextExtensions(context) {
override val binder = FragmentEnableMultisigPushesWarningBinding.inflate(LayoutInflater.from(context))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binder.enableMultisigPushesNotNow.setOnClickListener { dismiss() }
binder.enableMultisigPushesEnable.setOnClickListener {
onEnableClicked()
dismiss()
}
}
}
@@ -0,0 +1,89 @@
package io.novafoundation.nova.feature_push_notifications.presentation.multisigsWarning
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import io.novafoundation.nova.common.utils.Event
import io.novafoundation.nova.common.utils.launchUnit
import io.novafoundation.nova.common.utils.sendEvent
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
import io.novafoundation.nova.common.utils.sequrity.awaitInteractionAllowed
import io.novafoundation.nova.feature_account_api.data.proxy.MetaAccountsUpdatesRegistry
import io.novafoundation.nova.feature_push_notifications.PushNotificationsRouter
import io.novafoundation.nova.feature_push_notifications.domain.interactor.MultisigPushAlertInteractor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
class MultisigPushNotificationsAlertMixinFactory(
private val automaticInteractionGate: AutomaticInteractionGate,
private val interactor: MultisigPushAlertInteractor,
private val metaAccountsUpdatesRegistry: MetaAccountsUpdatesRegistry,
private val router: PushNotificationsRouter
) {
fun create(coroutineScope: CoroutineScope): MultisigPushNotificationsAlertMixin {
return RealMultisigPushNotificationsAlertMixin(
automaticInteractionGate,
interactor,
metaAccountsUpdatesRegistry,
router,
coroutineScope
)
}
}
interface MultisigPushNotificationsAlertMixin {
val showAlertEvent: LiveData<Event<Unit>>
fun subscribeToShowAlert()
fun showPushSettings()
}
class RealMultisigPushNotificationsAlertMixin(
private val automaticInteractionGate: AutomaticInteractionGate,
private val interactor: MultisigPushAlertInteractor,
private val metaAccountsUpdatesRegistry: MetaAccountsUpdatesRegistry,
private val router: PushNotificationsRouter,
private val coroutineScope: CoroutineScope
) : MultisigPushNotificationsAlertMixin {
override val showAlertEvent = MutableLiveData<Event<Unit>>()
override fun subscribeToShowAlert() = coroutineScope.launchUnit {
if (interactor.isAlertAlreadyShown()) return@launchUnit
// We should get this state before multisigs will be discovered so we call this method before interaction gate
val allowedToShowAlertAtStart = interactor.allowedToShowAlertAtStart()
automaticInteractionGate.awaitInteractionAllowed()
if (allowedToShowAlertAtStart) {
showAlert()
return@launchUnit
}
// We have to show alert after user saw new multisigs in account list so we subscribed to its update states
// And show alert when at least one multisig update was consumed
metaAccountsUpdatesRegistry.observeLastConsumedUpdatesMetaIds()
.onEach { consumedMetaIdsUpdates ->
if (interactor.isAlertAlreadyShown()) return@onEach
if (interactor.hasMultisigWallets(consumedMetaIdsUpdates.toList())) {
// We need to check interaction again since app may went to background before consuming updates
automaticInteractionGate.awaitInteractionAllowed()
showAlert()
}
}
.launchIn(coroutineScope)
}
private fun showAlert() {
interactor.setAlertWasAlreadyShown()
showAlertEvent.sendEvent()
}
override fun showPushSettings() {
router.openPushSettingsWithAccounts()
}
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.feature_push_notifications.presentation.multisigsWarning
import io.novafoundation.nova.common.base.BaseScreenMixin
fun BaseScreenMixin<*>.observeEnableMultisigPushesAlert(mixin: MultisigPushNotificationsAlertMixin) {
mixin.showAlertEvent.observeEvent {
MultisigPushNotificationsAlertBottomSheet(providedContext, onEnableClicked = { mixin.showPushSettings() }).show()
}
}
@@ -0,0 +1,71 @@
package io.novafoundation.nova.feature_push_notifications.presentation.settings
import androidx.core.view.isVisible
import io.novafoundation.nova.common.base.BaseFragment
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.mixin.actionAwaitable.setupConfirmationDialog
import io.novafoundation.nova.common.utils.FragmentPayloadCreator
import io.novafoundation.nova.common.utils.PayloadCreator
import io.novafoundation.nova.common.utils.payload
import io.novafoundation.nova.common.utils.permissions.setupPermissionAsker
import io.novafoundation.nova.feature_push_notifications.R
import io.novafoundation.nova.feature_push_notifications.databinding.FragmentPushSettingsBinding
import io.novafoundation.nova.feature_push_notifications.di.PushNotificationsFeatureApi
import io.novafoundation.nova.feature_push_notifications.di.PushNotificationsFeatureComponent
class PushSettingsFragment : BaseFragment<PushSettingsViewModel, FragmentPushSettingsBinding>() {
companion object : PayloadCreator<PushSettingsPayload> by FragmentPayloadCreator()
override fun createBinding() = FragmentPushSettingsBinding.inflate(layoutInflater)
override fun initViews() {
onBackPressed { viewModel.backClicked() }
binder.pushSettingsToolbar.setRightActionClickListener { viewModel.saveClicked() }
binder.pushSettingsToolbar.setHomeButtonListener { viewModel.backClicked() }
binder.pushSettingsEnable.setOnClickListener { viewModel.enableSwitcherClicked() }
binder.pushSettingsWallets.setOnClickListener { viewModel.walletsClicked() }
binder.pushSettingsAnnouncements.setOnClickListener { viewModel.announementsClicked() }
binder.pushSettingsSentTokens.setOnClickListener { viewModel.sentTokensClicked() }
binder.pushSettingsMultisigs.setOnClickListener { viewModel.multisigOperationsClicked() }
binder.pushSettingsReceivedTokens.setOnClickListener { viewModel.receivedTokensClicked() }
binder.pushSettingsGovernance.setOnClickListener { viewModel.governanceClicked() }
binder.pushSettingsStakingRewards.setOnClickListener { viewModel.stakingRewardsClicked() }
}
override fun inject() {
FeatureUtils.getFeature<PushNotificationsFeatureComponent>(requireContext(), PushNotificationsFeatureApi::class.java)
.pushSettingsComponentFactory()
.create(this, payload())
.inject(this)
}
override fun subscribe(viewModel: PushSettingsViewModel) {
setupConfirmationDialog(R.style.AccentNegativeAlertDialogTheme_Reversed, viewModel.closeConfirmationAction)
setupPermissionAsker(viewModel)
viewModel.pushSettingsWasChangedState.observe { binder.pushSettingsToolbar.setRightActionEnabled(it) }
viewModel.savingInProgress.observe { binder.pushSettingsToolbar.showProgress(it) }
viewModel.pushEnabledState.observe { enabled ->
binder.pushSettingsEnable.setChecked(enabled)
binder.pushSettingsAnnouncements.setEnabled(enabled)
binder.pushSettingsSentTokens.setEnabled(enabled)
binder.pushSettingsReceivedTokens.setEnabled(enabled)
binder.pushSettingsMultisigs.setEnabled(enabled)
binder.pushSettingsGovernance.setEnabled(enabled)
binder.pushSettingsStakingRewards.setEnabled(enabled)
}
viewModel.pushWalletsQuantity.observe { binder.pushSettingsWallets.setValue(it) }
viewModel.showNoSelectedWalletsTip.observe { binder.pushSettingsNoSelectedWallets.isVisible = it }
viewModel.pushAnnouncements.observe { binder.pushSettingsAnnouncements.setChecked(it) }
viewModel.pushSentTokens.observe { binder.pushSettingsSentTokens.setChecked(it) }
viewModel.pushReceivedTokens.observe { binder.pushSettingsReceivedTokens.setChecked(it) }
viewModel.pushMultisigsState.observe { binder.pushSettingsMultisigs.setValue(it) }
viewModel.pushGovernanceState.observe { binder.pushSettingsGovernance.setValue(it) }
viewModel.pushStakingRewardsState.observe { binder.pushSettingsStakingRewards.setValue(it) }
}
}
@@ -0,0 +1,26 @@
package io.novafoundation.nova.feature_push_notifications.presentation.settings
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
class PushSettingsPayload(
val enableSwitcherOnStart: Boolean,
val navigation: InstantNavigation
) : Parcelable {
companion object;
sealed interface InstantNavigation : Parcelable {
@Parcelize
data object Nothing : InstantNavigation
@Parcelize
data object WithWalletSelection : InstantNavigation
}
}
fun PushSettingsPayload.Companion.default(enableSwitcherOnStart: Boolean = false) =
PushSettingsPayload(enableSwitcherOnStart, PushSettingsPayload.InstantNavigation.Nothing)
fun PushSettingsPayload.Companion.withWalletSelection(enableSwitcherOnStart: Boolean = false) =
PushSettingsPayload(enableSwitcherOnStart, PushSettingsPayload.InstantNavigation.WithWalletSelection)
@@ -0,0 +1,346 @@
package io.novafoundation.nova.feature_push_notifications.presentation.settings
import android.Manifest
import android.os.Build
import io.novafoundation.nova.common.base.BaseViewModel
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
import io.novafoundation.nova.common.mixin.actionAwaitable.ConfirmationDialogInfo
import io.novafoundation.nova.common.mixin.actionAwaitable.confirmingAction
import io.novafoundation.nova.common.mixin.actionAwaitable.fromRes
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.resources.formatBooleanToState
import io.novafoundation.nova.common.utils.flowOf
import io.novafoundation.nova.common.utils.formatting.format
import io.novafoundation.nova.common.utils.launchUnit
import io.novafoundation.nova.common.utils.permissions.PermissionsAsker
import io.novafoundation.nova.common.utils.toggle
import io.novafoundation.nova.common.utils.updateValue
import io.novafoundation.nova.feature_account_api.domain.model.isMultisig
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.list.SelectMultipleWalletsRequester
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.fromTrackIds
import io.novafoundation.nova.feature_governance_api.data.network.blockhain.model.toTrackIds
import io.novafoundation.nova.feature_push_notifications.R
import io.novafoundation.nova.feature_push_notifications.PushNotificationsRouter
import io.novafoundation.nova.feature_push_notifications.domain.model.PushSettings
import io.novafoundation.nova.feature_push_notifications.domain.model.isGovEnabled
import io.novafoundation.nova.feature_push_notifications.domain.model.isNotEmpty
import io.novafoundation.nova.feature_push_notifications.domain.interactor.PushNotificationsInteractor
import io.novafoundation.nova.feature_push_notifications.presentation.governance.PushGovernanceSettingsPayload
import io.novafoundation.nova.feature_push_notifications.presentation.governance.PushGovernanceSettingsRequester
import io.novafoundation.nova.feature_push_notifications.presentation.governance.PushGovernanceSettingsResponder
import io.novafoundation.nova.feature_push_notifications.presentation.multisigs.PushMultisigSettingsRequester
import io.novafoundation.nova.feature_push_notifications.presentation.multisigs.toDomain
import io.novafoundation.nova.feature_push_notifications.presentation.multisigs.toModel
import io.novafoundation.nova.feature_push_notifications.presentation.staking.PushStakingSettingsPayload
import io.novafoundation.nova.feature_push_notifications.presentation.staking.PushStakingSettingsRequester
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
private const val MIN_WALLETS = 1
private const val MAX_WALLETS = 10
class PushSettingsViewModel(
private val router: PushNotificationsRouter,
private val pushNotificationsInteractor: PushNotificationsInteractor,
private val resourceManager: ResourceManager,
private val walletRequester: SelectMultipleWalletsRequester,
private val pushGovernanceSettingsRequester: PushGovernanceSettingsRequester,
private val pushStakingSettingsRequester: PushStakingSettingsRequester,
private val pushMultisigSettingsRequester: PushMultisigSettingsRequester,
private val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
private val permissionsAsker: PermissionsAsker.Presentation,
private val payload: PushSettingsPayload
) : BaseViewModel(), PermissionsAsker by permissionsAsker {
val closeConfirmationAction = actionAwaitableMixinFactory.confirmingAction<ConfirmationDialogInfo>()
private val oldPushSettingsState = flowOf { pushNotificationsInteractor.getPushSettings() }
.shareInBackground()
val pushEnabledState = MutableStateFlow(pushNotificationsInteractor.isPushNotificationsEnabled())
private val pushSettingsState = MutableStateFlow<PushSettings?>(null)
val pushSettingsWasChangedState = combine(pushEnabledState, pushSettingsState, oldPushSettingsState) { pushEnabled, newState, oldState ->
pushEnabled != pushNotificationsInteractor.isPushNotificationsEnabled() ||
newState != oldState
}
val pushWalletsQuantity = pushSettingsState
.mapNotNull { it?.subscribedMetaAccounts?.size?.format() }
.distinctUntilChanged()
val pushAnnouncements = pushSettingsState.mapNotNull { it?.announcementsEnabled }
.distinctUntilChanged()
val pushSentTokens = pushSettingsState.mapNotNull { it?.sentTokensEnabled }
.distinctUntilChanged()
val pushReceivedTokens = pushSettingsState.mapNotNull { it?.receivedTokensEnabled }
.distinctUntilChanged()
val pushGovernanceState = pushSettingsState.mapNotNull { it }
.map { resourceManager.formatBooleanToState(it.isGovEnabled()) }
.distinctUntilChanged()
val pushMultisigsState = pushSettingsState.mapNotNull { it }
.map { resourceManager.formatBooleanToState(it.multisigs.isEnabled) }
.distinctUntilChanged()
val pushStakingRewardsState = pushSettingsState.mapNotNull { it }
.map { resourceManager.formatBooleanToState(it.stakingReward.isNotEmpty()) }
.distinctUntilChanged()
val showNoSelectedWalletsTip = pushSettingsState
.mapNotNull { it?.subscribedMetaAccounts?.isEmpty() }
.distinctUntilChanged()
private val _savingInProgress = MutableStateFlow(false)
val savingInProgress: Flow<Boolean> = _savingInProgress
init {
initFirstState()
subscribeOnSelectWallets()
subscribeOnGovernanceSettings()
subscribeOnStakingSettings()
subscribeMultisigSettings()
disableNotificationsIfPushSettingsEmpty()
enableSwitcherOnStartIfRequested()
}
private fun initFirstState() {
launch {
val settings = oldPushSettingsState.first()
pushSettingsState.value = pushNotificationsInteractor.filterAvailableMetaIdsAndGetNewState(settings)
openWalletSelectionIfRequested()
}
}
fun backClicked() {
launch {
if (pushSettingsWasChangedState.first()) {
closeConfirmationAction.awaitAction(
ConfirmationDialogInfo.fromRes(
resourceManager,
R.string.common_confirmation_title,
R.string.common_close_confirmation_message,
R.string.common_close,
R.string.common_cancel,
)
)
}
router.back()
}
}
fun saveClicked() {
launch {
_savingInProgress.value = true
val pushSettings = pushSettingsState.value ?: return@launch
pushNotificationsInteractor.updatePushSettings(pushEnabledState.value, pushSettings)
.onSuccess {
enableMultisigWalletIfAtLeastOneSelected(pushSettings)
router.back()
}
.onFailure { showError(it) }
_savingInProgress.value = false
}
}
private fun enableMultisigWalletIfAtLeastOneSelected(pushSettings: PushSettings) {
if (pushSettings.multisigs.isEnabled && isMultisigsStillWasNotEnabled()) {
pushNotificationsInteractor.setMultisigsWasEnabledFirstTime()
}
}
fun enableSwitcherClicked() {
launch {
if (!pushEnabledState.value && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val isPermissionsGranted = permissionsAsker.requirePermissions(Manifest.permission.POST_NOTIFICATIONS)
if (!isPermissionsGranted) {
return@launch
}
}
if (!pushEnabledState.value) {
setDefaultPushSettingsIfEmpty()
}
pushEnabledState.toggle()
}
}
fun walletsClicked() {
selectWallets()
}
fun announementsClicked() {
pushSettingsState.updateValue { it?.copy(announcementsEnabled = !it.announcementsEnabled) }
}
fun sentTokensClicked() {
pushSettingsState.updateValue { it?.copy(sentTokensEnabled = !it.sentTokensEnabled) }
}
fun receivedTokensClicked() {
pushSettingsState.updateValue { it?.copy(receivedTokensEnabled = !it.receivedTokensEnabled) }
}
fun multisigOperationsClicked() = launchUnit {
val settings = pushSettingsState.value ?: return@launchUnit
val isAtLeastOneAccountMultisig = settings.subscribedMetaAccounts.atLeastOneMultisigWalletEnabled()
pushMultisigSettingsRequester.openRequest(
PushMultisigSettingsRequester.Request(isAtLeastOneAccountMultisig, settings.multisigs.toModel())
)
}
fun governanceClicked() {
val settings = pushSettingsState.value ?: return
pushGovernanceSettingsRequester.openRequest(PushGovernanceSettingsRequester.Request(mapGovSettingsToPayload(settings)))
}
fun stakingRewardsClicked() {
val stakingRewards = pushSettingsState.value?.stakingReward ?: return
val settings = when (stakingRewards) {
is PushSettings.ChainFeature.All -> PushStakingSettingsPayload.AllChains
is PushSettings.ChainFeature.Concrete -> PushStakingSettingsPayload.SpecifiedChains(stakingRewards.chainIds.toSet())
}
val request = PushStakingSettingsRequester.Request(settings)
pushStakingSettingsRequester.openRequest(request)
}
private fun subscribeOnSelectWallets() {
walletRequester.responseFlow
.onEach { response ->
val currentState = pushSettingsState.value ?: return@onEach
val newPushSettingsState = pushNotificationsInteractor.getNewStateForChangedMetaAccounts(currentState, response.selectedMetaIds)
pushSettingsState.value = newPushSettingsState
}
.launchIn(this)
}
private fun subscribeOnGovernanceSettings() {
pushGovernanceSettingsRequester.responseFlow
.onEach { response ->
pushSettingsState.updateValue { settings ->
settings?.copy(governance = mapGovSettingsResponseToModel(response))
}
}
.launchIn(this)
}
private fun subscribeOnStakingSettings() {
pushStakingSettingsRequester.responseFlow
.onEach { response ->
val stakingSettings = when (response.settings) {
is PushStakingSettingsPayload.AllChains -> PushSettings.ChainFeature.All
is PushStakingSettingsPayload.SpecifiedChains -> PushSettings.ChainFeature.Concrete(response.settings.enabledChainIds.toList())
}
pushSettingsState.updateValue { settings ->
settings?.copy(stakingReward = stakingSettings)
}
}
.launchIn(this)
}
private fun subscribeMultisigSettings() {
pushMultisigSettingsRequester.responseFlow
.onEach { response ->
pushSettingsState.updateValue { settings ->
settings?.copy(multisigs = response.settings.toDomain())
}
}
.launchIn(this)
}
private fun mapGovSettingsToPayload(pushSettings: PushSettings): List<PushGovernanceSettingsPayload> {
return pushSettings.governance.map { (chainId, govState) ->
PushGovernanceSettingsPayload(
chainId = chainId,
governance = Chain.Governance.V2,
newReferenda = govState.newReferendaEnabled,
referendaUpdates = govState.referendumUpdateEnabled,
delegateVotes = govState.govMyDelegateVotedEnabled,
tracksIds = govState.tracks.fromTrackIds()
)
}
}
private fun mapGovSettingsResponseToModel(response: PushGovernanceSettingsResponder.Response): Map<ChainId, PushSettings.GovernanceState> {
return response.enabledGovernanceSettings
.associateBy { it.chainId }
.mapValues { (_, govState) ->
PushSettings.GovernanceState(
newReferendaEnabled = govState.newReferenda,
referendumUpdateEnabled = govState.referendaUpdates,
govMyDelegateVotedEnabled = govState.delegateVotes,
tracks = govState.tracksIds.toTrackIds()
)
}
}
private fun disableNotificationsIfPushSettingsEmpty() {
pushSettingsState
.filterNotNull()
.onEach { pushSettings ->
if (pushSettings.settingsIsEmpty()) {
pushEnabledState.value = false
}
}
.launchIn(this)
}
private suspend fun setDefaultPushSettingsIfEmpty() {
if (pushSettingsState.value?.settingsIsEmpty() == true) {
pushSettingsState.value = pushNotificationsInteractor.getPushSettings()
}
}
private suspend fun Collection<Long>.atLeastOneMultisigWalletEnabled(): Boolean {
return pushNotificationsInteractor.getMetaAccounts(this.toList())
.any { it.isMultisig() }
}
private fun isMultisigsStillWasNotEnabled() = !pushNotificationsInteractor.isMultisigsWasEnabledFirstTime()
private fun enableSwitcherOnStartIfRequested() {
if (payload.enableSwitcherOnStart) {
pushEnabledState.value = true
}
}
private fun openWalletSelectionIfRequested() {
if (payload.navigation is PushSettingsPayload.InstantNavigation.WithWalletSelection) {
selectWallets()
}
}
private fun selectWallets() {
walletRequester.openRequest(
SelectMultipleWalletsRequester.Request(
titleText = resourceManager.getString(R.string.push_wallets_title, MAX_WALLETS),
currentlySelectedMetaIds = pushSettingsState.value?.subscribedMetaAccounts?.toSet().orEmpty(),
min = MIN_WALLETS,
max = MAX_WALLETS
)
)
}
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_push_notifications.presentation.settings.di
import androidx.fragment.app.Fragment
import dagger.BindsInstance
import dagger.Subcomponent
import io.novafoundation.nova.common.di.scope.ScreenScope
import io.novafoundation.nova.feature_push_notifications.presentation.settings.PushSettingsFragment
import io.novafoundation.nova.feature_push_notifications.presentation.settings.PushSettingsPayload
@Subcomponent(
modules = [
PushSettingsModule::class
]
)
@ScreenScope
interface PushSettingsComponent {
@Subcomponent.Factory
interface Factory {
fun create(
@BindsInstance fragment: Fragment,
@BindsInstance payload: PushSettingsPayload
): PushSettingsComponent
}
fun inject(fragment: PushSettingsFragment)
}
@@ -0,0 +1,70 @@
package io.novafoundation.nova.feature_push_notifications.presentation.settings.di
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoMap
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.permissions.PermissionsAsker
import io.novafoundation.nova.common.utils.permissions.PermissionsAskerFactory
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.list.SelectMultipleWalletsCommunicator
import io.novafoundation.nova.feature_push_notifications.PushNotificationsRouter
import io.novafoundation.nova.feature_push_notifications.domain.interactor.PushNotificationsInteractor
import io.novafoundation.nova.feature_push_notifications.presentation.governance.PushGovernanceSettingsCommunicator
import io.novafoundation.nova.feature_push_notifications.presentation.settings.PushSettingsPayload
import io.novafoundation.nova.feature_push_notifications.presentation.multisigs.PushMultisigSettingsCommunicator
import io.novafoundation.nova.feature_push_notifications.presentation.settings.PushSettingsViewModel
import io.novafoundation.nova.feature_push_notifications.presentation.staking.PushStakingSettingsCommunicator
@Module(includes = [ViewModelModule::class])
class PushSettingsModule {
@Provides
fun providePermissionAsker(
permissionsAskerFactory: PermissionsAskerFactory,
fragment: Fragment,
router: PushNotificationsRouter
) = permissionsAskerFactory.createReturnable(fragment, router)
@Provides
@IntoMap
@ViewModelKey(PushSettingsViewModel::class)
fun provideViewModel(
router: PushNotificationsRouter,
interactor: PushNotificationsInteractor,
resourceManager: ResourceManager,
selectMultipleWalletsCommunicator: SelectMultipleWalletsCommunicator,
pushGovernanceSettingsCommunicator: PushGovernanceSettingsCommunicator,
pushStakingSettingsRequester: PushStakingSettingsCommunicator,
permissionsAsker: PermissionsAsker.Presentation,
actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
pushMultisigSettingsCommunicator: PushMultisigSettingsCommunicator,
payload: PushSettingsPayload
): ViewModel {
return PushSettingsViewModel(
router,
interactor,
resourceManager,
selectMultipleWalletsCommunicator,
pushGovernanceSettingsCommunicator,
pushStakingSettingsRequester,
pushMultisigSettingsCommunicator,
actionAwaitableMixinFactory,
permissionsAsker,
payload
)
}
@Provides
fun provideViewModelCreator(
fragment: Fragment,
viewModelFactory: ViewModelProvider.Factory
): PushSettingsViewModel {
return ViewModelProvider(fragment, viewModelFactory).get(PushSettingsViewModel::class.java)
}
}
@@ -0,0 +1,30 @@
package io.novafoundation.nova.feature_push_notifications.presentation.staking
import android.os.Parcelable
import io.novafoundation.nova.common.navigation.InterScreenRequester
import io.novafoundation.nova.common.navigation.InterScreenResponder
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import kotlinx.parcelize.Parcelize
interface PushStakingSettingsRequester : InterScreenRequester<PushStakingSettingsRequester.Request, PushStakingSettingsResponder.Response> {
@Parcelize
class Request(val settings: PushStakingSettingsPayload) : Parcelable
}
interface PushStakingSettingsResponder : InterScreenResponder<PushStakingSettingsRequester.Request, PushStakingSettingsResponder.Response> {
@Parcelize
class Response(val settings: PushStakingSettingsPayload) : Parcelable
}
interface PushStakingSettingsCommunicator : PushStakingSettingsRequester, PushStakingSettingsResponder
sealed class PushStakingSettingsPayload : Parcelable {
@Parcelize
object AllChains : PushStakingSettingsPayload()
@Parcelize
class SpecifiedChains(val enabledChainIds: Set<ChainId>) : PushStakingSettingsPayload()
}
@@ -0,0 +1,71 @@
package io.novafoundation.nova.feature_push_notifications.presentation.staking
import android.os.Bundle
import androidx.core.view.isVisible
import coil.ImageLoader
import io.novafoundation.nova.common.base.BaseFragment
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.domain.ExtendedLoadingState
import io.novafoundation.nova.feature_push_notifications.databinding.FragmentPushStakingSettingsBinding
import io.novafoundation.nova.feature_push_notifications.di.PushNotificationsFeatureApi
import io.novafoundation.nova.feature_push_notifications.di.PushNotificationsFeatureComponent
import io.novafoundation.nova.feature_push_notifications.presentation.staking.adapter.PushStakingRVItem
import io.novafoundation.nova.feature_push_notifications.presentation.staking.adapter.PushStakingSettingsAdapter
import javax.inject.Inject
class PushStakingSettingsFragment : BaseFragment<PushStakingSettingsViewModel, FragmentPushStakingSettingsBinding>(), PushStakingSettingsAdapter.ItemHandler {
companion object {
private const val KEY_REQUEST = "KEY_REQUEST"
fun getBundle(request: PushStakingSettingsRequester.Request): Bundle {
return Bundle().apply {
putParcelable(KEY_REQUEST, request)
}
}
}
override fun createBinding() = FragmentPushStakingSettingsBinding.inflate(layoutInflater)
@Inject
lateinit var imageLoader: ImageLoader
private val adapter by lazy(LazyThreadSafetyMode.NONE) {
PushStakingSettingsAdapter(imageLoader, this)
}
override fun initViews() {
binder.pushStakingToolbar.setHomeButtonListener { viewModel.backClicked() }
binder.pushStakingToolbar.setRightActionClickListener { viewModel.clearClicked() }
onBackPressed { viewModel.backClicked() }
binder.pushStakingList.adapter = adapter
}
override fun inject() {
FeatureUtils.getFeature<PushNotificationsFeatureComponent>(requireContext(), PushNotificationsFeatureApi::class.java)
.pushStakingSettings()
.create(this, argument(KEY_REQUEST))
.inject(this)
}
override fun subscribe(viewModel: PushStakingSettingsViewModel) {
viewModel.clearButtonEnabledFlow.observe {
binder.pushStakingToolbar.setRightActionEnabled(it)
}
viewModel.stakingSettingsList.observe {
binder.pushStakingList.isVisible = it is ExtendedLoadingState.Loaded
binder.pushStakingProgress.isVisible = it is ExtendedLoadingState.Loading
if (it is ExtendedLoadingState.Loaded) {
adapter.submitList(it.data)
}
}
}
override fun itemClicked(item: PushStakingRVItem) {
viewModel.itemClicked(item)
}
}
@@ -0,0 +1,86 @@
package io.novafoundation.nova.feature_push_notifications.presentation.staking
import io.novafoundation.nova.common.base.BaseViewModel
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.mapToSet
import io.novafoundation.nova.common.utils.toggle
import io.novafoundation.nova.common.utils.updateValue
import io.novafoundation.nova.common.utils.withSafeLoading
import io.novafoundation.nova.feature_push_notifications.PushNotificationsRouter
import io.novafoundation.nova.feature_push_notifications.domain.interactor.StakingPushSettingsInteractor
import io.novafoundation.nova.feature_push_notifications.presentation.staking.adapter.PushStakingRVItem
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
class PushStakingSettingsViewModel(
private val router: PushNotificationsRouter,
private val pushStakingSettingsResponder: PushStakingSettingsCommunicator,
private val chainRegistry: ChainRegistry,
private val request: PushStakingSettingsRequester.Request,
private val resourceManager: ResourceManager,
private val stakingPushSettingsInteractor: StakingPushSettingsInteractor
) : BaseViewModel() {
private val chainsFlow = stakingPushSettingsInteractor.stakingChainsFlow()
val _enabledStakingSettingsList: MutableStateFlow<Set<ChainId>> = MutableStateFlow(emptySet())
val stakingSettingsList = combine(chainsFlow, _enabledStakingSettingsList) { chains, enabledChains ->
chains.map { chain ->
PushStakingRVItem(
chain.id,
chain.name,
chain.icon,
enabledChains.contains(chain.id)
)
}
}.withSafeLoading()
.shareInBackground()
val clearButtonEnabledFlow = _enabledStakingSettingsList.map {
it.isNotEmpty()
}.shareInBackground()
init {
launch {
_enabledStakingSettingsList.value = when (request.settings) {
PushStakingSettingsPayload.AllChains -> chainsFlow.first().mapToSet { it.id }
is PushStakingSettingsPayload.SpecifiedChains -> request.settings.enabledChainIds
}
}
}
fun backClicked() {
launch {
val allChainsIds = chainsFlow.first().mapToSet { it.id }
val enabledChains = _enabledStakingSettingsList.value
val settings = if (enabledChains == allChainsIds) {
PushStakingSettingsPayload.AllChains
} else {
PushStakingSettingsPayload.SpecifiedChains(enabledChains)
}
val response = PushStakingSettingsResponder.Response(settings)
pushStakingSettingsResponder.respond(response)
router.back()
}
}
fun clearClicked() {
_enabledStakingSettingsList.value = emptySet()
}
fun itemClicked(item: PushStakingRVItem) {
_enabledStakingSettingsList.updateValue {
it.toggle(item.chainId)
}
}
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_push_notifications.presentation.staking.adapter
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
data class PushStakingRVItem(
val chainId: ChainId,
val chainName: String,
val chainIconUrl: String?,
val isEnabled: Boolean,
)
@@ -0,0 +1,88 @@
package io.novafoundation.nova.feature_push_notifications.presentation.staking.adapter
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import coil.ImageLoader
import io.novafoundation.nova.common.list.PayloadGenerator
import io.novafoundation.nova.common.list.resolvePayload
import io.novafoundation.nova.common.utils.inflater
import io.novafoundation.nova.feature_account_api.presenatation.chain.loadChainIconToTarget
import io.novafoundation.nova.feature_push_notifications.databinding.ItemPushStakingSettingsBinding
class PushStakingSettingsAdapter(
private val imageLoader: ImageLoader,
private val itemHandler: ItemHandler
) : ListAdapter<PushStakingRVItem, PushStakingItemViewHolder>(PushStakingItemCallback()) {
interface ItemHandler {
fun itemClicked(item: PushStakingRVItem)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PushStakingItemViewHolder {
return PushStakingItemViewHolder(ItemPushStakingSettingsBinding.inflate(parent.inflater(), parent, false), imageLoader, itemHandler)
}
override fun onBindViewHolder(holder: PushStakingItemViewHolder, position: Int) {
holder.bind(getItem(position))
}
override fun onBindViewHolder(holder: PushStakingItemViewHolder, position: Int, payloads: MutableList<Any>) {
resolvePayload(holder, position, payloads) {
val item = getItem(position)
when (it) {
PushStakingRVItem::isEnabled -> holder.setEnabled(item)
}
}
}
}
class PushStakingItemCallback() : DiffUtil.ItemCallback<PushStakingRVItem>() {
override fun areItemsTheSame(oldItem: PushStakingRVItem, newItem: PushStakingRVItem): Boolean {
return oldItem.chainId == newItem.chainId
}
override fun areContentsTheSame(oldItem: PushStakingRVItem, newItem: PushStakingRVItem): Boolean {
return oldItem == newItem
}
override fun getChangePayload(oldItem: PushStakingRVItem, newItem: PushStakingRVItem): Any? {
return PushStakingPayloadGenerator.diff(oldItem, newItem)
}
}
class PushStakingItemViewHolder(
private val binder: ItemPushStakingSettingsBinding,
private val imageLoader: ImageLoader,
private val itemHandler: PushStakingSettingsAdapter.ItemHandler
) : ViewHolder(binder.root) {
init {
binder.pushStakingItem.setIconTintColor(null)
}
fun bind(item: PushStakingRVItem) {
with(binder) {
pushStakingItem.setOnClickListener {
itemHandler.itemClicked(item)
}
pushStakingItem.setTitle(item.chainName)
imageLoader.loadChainIconToTarget(item.chainIconUrl, binder.root.context) {
pushStakingItem.setIcon(it)
}
setEnabled(item)
}
}
fun setEnabled(item: PushStakingRVItem) {
binder.pushStakingItem.setChecked(item.isEnabled)
}
}
private object PushStakingPayloadGenerator : PayloadGenerator<PushStakingRVItem>(
PushStakingRVItem::isEnabled
)
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_push_notifications.presentation.staking.di
import androidx.fragment.app.Fragment
import dagger.BindsInstance
import dagger.Subcomponent
import io.novafoundation.nova.common.di.scope.ScreenScope
import io.novafoundation.nova.feature_push_notifications.presentation.staking.PushStakingSettingsFragment
import io.novafoundation.nova.feature_push_notifications.presentation.staking.PushStakingSettingsRequester
@Subcomponent(
modules = [
PushStakingSettingsModule::class
]
)
@ScreenScope
interface PushStakingSettingsComponent {
@Subcomponent.Factory
interface Factory {
fun create(
@BindsInstance fragment: Fragment,
@BindsInstance request: PushStakingSettingsRequester.Request
): PushStakingSettingsComponent
}
fun inject(fragment: PushStakingSettingsFragment)
}
@@ -0,0 +1,50 @@
package io.novafoundation.nova.feature_push_notifications.presentation.staking.di
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoMap
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_push_notifications.PushNotificationsRouter
import io.novafoundation.nova.feature_push_notifications.domain.interactor.StakingPushSettingsInteractor
import io.novafoundation.nova.feature_push_notifications.presentation.staking.PushStakingSettingsCommunicator
import io.novafoundation.nova.feature_push_notifications.presentation.staking.PushStakingSettingsRequester
import io.novafoundation.nova.feature_push_notifications.presentation.staking.PushStakingSettingsViewModel
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
@Module(includes = [ViewModelModule::class])
class PushStakingSettingsModule {
@Provides
@IntoMap
@ViewModelKey(PushStakingSettingsViewModel::class)
fun provideViewModel(
router: PushNotificationsRouter,
pushStakingSettingsCommunicator: PushStakingSettingsCommunicator,
chainRegistry: ChainRegistry,
request: PushStakingSettingsRequester.Request,
resourceManager: ResourceManager,
stakingPushSettingsInteractor: StakingPushSettingsInteractor
): ViewModel {
return PushStakingSettingsViewModel(
router = router,
pushStakingSettingsResponder = pushStakingSettingsCommunicator,
chainRegistry = chainRegistry,
request = request,
resourceManager = resourceManager,
stakingPushSettingsInteractor = stakingPushSettingsInteractor
)
}
@Provides
fun provideViewModelCreator(
fragment: Fragment,
viewModelFactory: ViewModelProvider.Factory
): PushStakingSettingsViewModel {
return ViewModelProvider(fragment, viewModelFactory).get(PushStakingSettingsViewModel::class.java)
}
}
@@ -0,0 +1,55 @@
package io.novafoundation.nova.feature_push_notifications.presentation.welcome
import io.novafoundation.nova.common.base.BaseFragment
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.mixin.impl.observeBrowserEvents
import io.novafoundation.nova.common.mixin.impl.observeRetries
import io.novafoundation.nova.common.utils.formatting.applyTermsAndPrivacyPolicy
import io.novafoundation.nova.common.utils.permissions.setupPermissionAsker
import io.novafoundation.nova.feature_push_notifications.R
import io.novafoundation.nova.feature_push_notifications.databinding.FragmentPushWelcomeBinding
import io.novafoundation.nova.feature_push_notifications.di.PushNotificationsFeatureApi
import io.novafoundation.nova.feature_push_notifications.di.PushNotificationsFeatureComponent
class PushWelcomeFragment : BaseFragment<PushWelcomeViewModel, FragmentPushWelcomeBinding>() {
override fun createBinding() = FragmentPushWelcomeBinding.inflate(layoutInflater)
override fun initViews() {
binder.pushWelcomeEnableButton.prepareForProgress(this)
binder.pushWelcomeCancelButton.prepareForProgress(this)
binder.pushWelcomeToolbar.setHomeButtonListener { viewModel.backClicked() }
binder.pushWelcomeEnableButton.setOnClickListener { viewModel.askPermissionAndEnablePushNotifications() }
binder.pushWelcomeCancelButton.setOnClickListener { viewModel.backClicked() }
configureTermsAndPrivacy()
}
private fun configureTermsAndPrivacy() {
binder.pushWelcomeTermsAndConditions.applyTermsAndPrivacyPolicy(
R.string.push_welcome_terms_and_conditions,
R.string.common_terms_and_conditions_formatting,
R.string.common_privacy_policy_formatting,
viewModel::termsClicked,
viewModel::privacyClicked
)
}
override fun inject() {
FeatureUtils.getFeature<PushNotificationsFeatureComponent>(requireContext(), PushNotificationsFeatureApi::class.java)
.pushWelcomeComponentFactory()
.create(this)
.inject(this)
}
override fun subscribe(viewModel: PushWelcomeViewModel) {
observeBrowserEvents(viewModel)
observeRetries(viewModel)
setupPermissionAsker(viewModel)
viewModel.buttonState.observe { state ->
binder.pushWelcomeEnableButton.setState(state)
binder.pushWelcomeCancelButton.setState(state)
}
}
}
@@ -0,0 +1,99 @@
package io.novafoundation.nova.feature_push_notifications.presentation.welcome
import android.Manifest
import android.os.Build
import androidx.lifecycle.MutableLiveData
import io.novafoundation.nova.common.base.BaseViewModel
import io.novafoundation.nova.common.data.network.AppLinksProvider
import io.novafoundation.nova.common.mixin.api.Browserable
import io.novafoundation.nova.common.mixin.api.Retriable
import io.novafoundation.nova.common.mixin.api.RetryPayload
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.Event
import io.novafoundation.nova.common.utils.permissions.PermissionsAsker
import io.novafoundation.nova.common.view.ButtonState
import io.novafoundation.nova.feature_push_notifications.R
import io.novafoundation.nova.feature_push_notifications.PushNotificationsRouter
import io.novafoundation.nova.feature_push_notifications.domain.interactor.PushNotificationsInteractor
import io.novafoundation.nova.feature_push_notifications.domain.interactor.WelcomePushNotificationsInteractor
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
class PushWelcomeViewModel(
private val router: PushNotificationsRouter,
private val pushNotificationsInteractor: PushNotificationsInteractor,
private val welcomePushNotificationsInteractor: WelcomePushNotificationsInteractor,
private val permissionsAsker: PermissionsAsker.Presentation,
private val resourceManager: ResourceManager,
private val appLinksProvider: AppLinksProvider
) : BaseViewModel(), PermissionsAsker by permissionsAsker, Retriable, Browserable {
private val _enablingInProgress = MutableStateFlow(false)
override val retryEvent: MutableLiveData<Event<RetryPayload>> = MutableLiveData()
override val openBrowserEvent = MutableLiveData<Event<String>>()
val buttonState = _enablingInProgress.map { inProgress ->
when (inProgress) {
true -> ButtonState.PROGRESS
false -> ButtonState.NORMAL
}
}
fun backClicked() {
welcomePushNotificationsInteractor.setWelcomeScreenShown()
router.back()
}
fun termsClicked() {
openBrowserEvent.value = Event(appLinksProvider.termsUrl)
}
fun privacyClicked() {
openBrowserEvent.value = Event(appLinksProvider.privacyUrl)
}
fun askPermissionAndEnablePushNotifications() {
launch {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val isPermissionsGranted = permissionsAsker.requirePermissions(Manifest.permission.POST_NOTIFICATIONS)
if (!isPermissionsGranted) {
return@launch
}
}
_enablingInProgress.value = true
pushNotificationsInteractor.initPushSettings()
.onSuccess {
welcomePushNotificationsInteractor.setWelcomeScreenShown()
router.back()
}
.onFailure {
when (it) {
is TimeoutCancellationException -> showError(
resourceManager.getString(R.string.common_something_went_wrong_title),
resourceManager.getString(R.string.push_welcome_timeout_error_message)
)
else -> retryDialog()
}
}
_enablingInProgress.value = false
}
}
private fun retryDialog() {
retryEvent.value = Event(
RetryPayload(
title = resourceManager.getString(R.string.common_error_general_title),
message = resourceManager.getString(R.string.common_retry_message),
onRetry = { askPermissionAndEnablePushNotifications() }
)
)
}
}
@@ -0,0 +1,24 @@
package io.novafoundation.nova.feature_push_notifications.presentation.welcome.di
import androidx.fragment.app.Fragment
import dagger.BindsInstance
import dagger.Subcomponent
import io.novafoundation.nova.common.di.scope.ScreenScope
import io.novafoundation.nova.feature_push_notifications.presentation.welcome.PushWelcomeFragment
@Subcomponent(
modules = [
PushWelcomeModule::class
]
)
@ScreenScope
interface PushWelcomeComponent {
@Subcomponent.Factory
interface Factory {
fun create(@BindsInstance fragment: Fragment): PushWelcomeComponent
}
fun inject(fragment: PushWelcomeFragment)
}
@@ -0,0 +1,58 @@
package io.novafoundation.nova.feature_push_notifications.presentation.welcome.di
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoMap
import io.novafoundation.nova.common.data.network.AppLinksProvider
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.permissions.PermissionsAsker
import io.novafoundation.nova.common.utils.permissions.PermissionsAskerFactory
import io.novafoundation.nova.feature_push_notifications.PushNotificationsRouter
import io.novafoundation.nova.feature_push_notifications.domain.interactor.PushNotificationsInteractor
import io.novafoundation.nova.feature_push_notifications.domain.interactor.WelcomePushNotificationsInteractor
import io.novafoundation.nova.feature_push_notifications.presentation.welcome.PushWelcomeViewModel
@Module(includes = [ViewModelModule::class])
class PushWelcomeModule {
@Provides
fun providePermissionAsker(
permissionsAskerFactory: PermissionsAskerFactory,
fragment: Fragment,
router: PushNotificationsRouter
) = permissionsAskerFactory.createReturnable(fragment, router)
@Provides
@IntoMap
@ViewModelKey(PushWelcomeViewModel::class)
fun provideViewModel(
router: PushNotificationsRouter,
interactor: PushNotificationsInteractor,
permissionsAsker: PermissionsAsker.Presentation,
resourceManager: ResourceManager,
welcomePushNotificationsInteractor: WelcomePushNotificationsInteractor,
appLinksProvider: AppLinksProvider
): ViewModel {
return PushWelcomeViewModel(
router,
interactor,
welcomePushNotificationsInteractor,
permissionsAsker,
resourceManager,
appLinksProvider
)
}
@Provides
fun provideViewModelCreator(
fragment: Fragment,
viewModelFactory: ViewModelProvider.Factory
): PushWelcomeViewModel {
return ViewModelProvider(fragment, viewModelFactory).get(PushWelcomeViewModel::class.java)
}
}
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<View
style="@style/Widget.Nova.Puller"
android:layout_marginTop="6dp" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/actionNotAllowedImage"
android:layout_width="88dp"
android:layout_height="88dp"
android:layout_marginTop="20dp"
app:srcCompat="@drawable/ic_bell" />
<TextView
android:id="@+id/actionNotAllowedTitle"
style="@style/TextAppearance.NovaFoundation.SemiBold.Title3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:gravity="center"
android:text="@string/mutisigs_push_notifications_warning_title"
android:textColor="@color/text_primary" />
<TextView
android:id="@+id/actionNotAllowedSubtitle"
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="@string/mutisigs_push_notifications_warning_message"
android:textColor="@color/text_secondary" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clipToPadding="false"
android:orientation="horizontal"
android:paddingVertical="16dp">
<io.novafoundation.nova.common.view.PrimaryButtonV2
android:id="@+id/enableMultisigPushesNotNow"
style="@style/Widget.Nova.MaterialButton.Secondary"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginEnd="6dp"
android:layout_weight="1"
android:text="@string/common_not_now" />
<io.novafoundation.nova.common.view.PrimaryButtonV2
android:id="@+id/enableMultisigPushesEnable"
style="@style/Widget.Nova.MaterialButton.Primary"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="6dp"
android:layout_weight="1"
android:text="@string/common_enable" />
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<io.novafoundation.nova.common.view.Toolbar
android:id="@+id/pushGovernanceToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:dividerVisible="false"
app:layout_constraintTop_toTopOf="parent"
app:textRight="@string/common_clear"
app:titleText="@string/common_governance" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/pushGovernanceList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:clipToPadding="false"
android:paddingHorizontal="16dp"
android:paddingTop="16dp"
android:paddingBottom="24dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pushGovernanceToolbar" />
<ProgressBar
android:id="@+id/pushGovernanceProgress"
style="@style/Widget.Nova.ProgressBar.Indeterminate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pushGovernanceToolbar" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<io.novafoundation.nova.common.view.Toolbar
android:id="@+id/pushMultisigsToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:dividerVisible="false"
app:layout_constraintTop_toTopOf="parent"
app:titleText="@string/push_settings_multisig_operations" />
<io.novafoundation.nova.common.view.settings.SettingsGroupView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp">
<io.novafoundation.nova.common.view.settings.SettingsSwitcherView
android:id="@+id/pushMultisigSettingsSwitcher"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/push_settings_enable" />
</io.novafoundation.nova.common.view.settings.SettingsGroupView>
<io.novafoundation.nova.common.view.settings.SettingsGroupView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="24dp">
<io.novafoundation.nova.common.view.settings.SettingsSwitcherView
android:id="@+id/pushMultisigInitiatingSwitcher"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/multisig_push_settings_initial_switcher" />
<io.novafoundation.nova.common.view.settings.SettingsSwitcherView
android:id="@+id/pushMultisigApprovalSwitcher"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/multisig_push_settings_approval_switcher" />
<io.novafoundation.nova.common.view.settings.SettingsSwitcherView
android:id="@+id/pushMultisigExecutedSwitcher"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/multisig_push_settings_executed_switcher" />
<io.novafoundation.nova.common.view.settings.SettingsSwitcherView
android:id="@+id/pushMultisigRejectedSwitcher"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/multisig_push_settings_rejected_switcher" />
</io.novafoundation.nova.common.view.settings.SettingsGroupView>
</LinearLayout>
@@ -0,0 +1,153 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<io.novafoundation.nova.common.view.Toolbar
android:id="@+id/pushSettingsToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:dividerVisible="false"
app:layout_constraintTop_toTopOf="parent"
app:textRight="@string/common_save"
app:titleText="@string/common_push_notifications" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:clipToPadding="false"
android:paddingHorizontal="16dp"
android:paddingTop="16dp"
android:paddingBottom="24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pushSettingsToolbar">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="300dp"
android:orientation="vertical">
<io.novafoundation.nova.common.view.settings.SettingsGroupView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<io.novafoundation.nova.common.view.settings.SettingsSwitcherView
android:id="@+id/pushSettingsEnable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:icon="@drawable/ic_notifications_outline"
app:title="@string/push_settings_enable" />
<io.novafoundation.nova.common.view.settings.SettingsItemView
android:id="@+id/pushSettingsWallets"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:icon="@drawable/ic_wallet_outline"
app:title="@string/push_settings_wallets"
tools:settingValue="0" />
</io.novafoundation.nova.common.view.settings.SettingsGroupView>
<TextView
android:id="@+id/pushSettingsNoSelectedWallets"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/push_settings_no_selected_wallets"
android:textColor="@color/text_secondary"
android:textSize="12sp" />
<io.novafoundation.nova.common.view.settings.SettingsGroupHeaderView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/settings_general" />
<io.novafoundation.nova.common.view.settings.SettingsGroupView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<io.novafoundation.nova.common.view.settings.SettingsSwitcherView
android:id="@+id/pushSettingsAnnouncements"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/push_settings_announcements" />
</io.novafoundation.nova.common.view.settings.SettingsGroupView>
<io.novafoundation.nova.common.view.settings.SettingsGroupHeaderView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/push_settings_balances" />
<io.novafoundation.nova.common.view.settings.SettingsGroupView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<io.novafoundation.nova.common.view.settings.SettingsSwitcherView
android:id="@+id/pushSettingsSentTokens"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/push_settings_sent_tokens" />
<io.novafoundation.nova.common.view.settings.SettingsSwitcherView
android:id="@+id/pushSettingsReceivedTokens"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/push_settings_received_tokens" />
</io.novafoundation.nova.common.view.settings.SettingsGroupView>
<io.novafoundation.nova.common.view.settings.SettingsGroupHeaderView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/push_settings_others" />
<io.novafoundation.nova.common.view.settings.SettingsGroupView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<io.novafoundation.nova.common.view.settings.SettingsItemView
android:id="@+id/pushSettingsMultisigs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/push_settings_multisig_operations"
tools:settingValue="On" />
<io.novafoundation.nova.common.view.settings.SettingsItemView
android:id="@+id/pushSettingsGovernance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/common_governance"
tools:settingValue="On" />
<io.novafoundation.nova.common.view.settings.SettingsItemView
android:id="@+id/pushSettingsStakingRewards"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/push_settings_staking_rewards"
tools:settingValue="On" />
</io.novafoundation.nova.common.view.settings.SettingsGroupView>
<TextView
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="64dp"
android:layout_marginBottom="8dp"
android:text="Powered by"
android:textColor="@color/text_secondary" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:src="@drawable/ic_web3_alert_icon" />
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<io.novafoundation.nova.common.view.Toolbar
android:id="@+id/pushStakingToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:dividerVisible="false"
app:layout_constraintTop_toTopOf="parent"
app:textRight="@string/common_clear"
app:titleText="@string/push_settings_staking_rewards" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/pushStakingList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:clipToPadding="false"
android:paddingHorizontal="16dp"
android:paddingTop="16dp"
android:paddingBottom="24dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pushStakingToolbar" />
<ProgressBar
android:id="@+id/pushStakingProgress"
style="@style/Widget.Nova.ProgressBar.Indeterminate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pushStakingToolbar" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,180 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<io.novafoundation.nova.common.view.Toolbar
android:id="@+id/pushWelcomeToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:dividerVisible="false"
app:homeButtonIcon="@drawable/ic_close"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/pushWelcomeImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:src="@drawable/ic_bell"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pushWelcomeToolbar" />
<TextView
android:id="@+id/pushWelcomeTitle"
style="@style/TextAppearance.NovaFoundation.Bold.Title2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:paddingHorizontal="16dp"
android:text="@string/push_welcome_title"
android:textColor="@color/text_primary"
app:layout_constraintTop_toBottomOf="@id/pushWelcomeImage" />
<TextView
android:id="@+id/pushWelcomeMessage"
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:paddingHorizontal="16dp"
android:text="@string/push_welcome_message"
android:textColor="@color/text_secondary"
app:layout_constraintTop_toBottomOf="@id/pushWelcomeTitle" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
app:layout_constraintTop_toBottomOf="@id/pushWelcomeMessage">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="32dp"
android:layout_marginHorizontal="48dp"
app:cardBackgroundColor="@color/notification_preview_3_layer_background"
app:cardCornerRadius="18dp"
app:cardElevation="0dp"
app:layout_constraintBottom_toBottomOf="parent" />
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="32dp"
android:layout_marginHorizontal="32dp"
android:layout_marginBottom="16dp"
app:cardBackgroundColor="@color/notification_preview_2_layer_background"
app:cardCornerRadius="18dp"
app:cardElevation="8dp"
app:layout_constraintBottom_toBottomOf="parent" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/pushWelcomeTemplate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="32dp"
app:cardBackgroundColor="@color/notification_preview_1_layer_background"
app:cardCornerRadius="18dp"
app:cardElevation="16dp"
app:layout_constraintBottom_toBottomOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/pushWelcomeLogo"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="14dp"
android:layout_marginTop="12dp"
android:src="@drawable/ic_pezkuwi_logo"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
style="@style/TextAppearance.NovaFoundation.Regular.Caption2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:includeFontPadding="false"
android:text="@string/push_template_app_name_and_time"
android:textColor="@color/chip_text"
app:layout_constraintBottom_toBottomOf="@id/pushWelcomeLogo"
app:layout_constraintStart_toEndOf="@id/pushWelcomeLogo"
app:layout_constraintTop_toTopOf="@id/pushWelcomeLogo" />
<TextView
android:id="@+id/pushWelcomeTemplateTitle"
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:layout_marginTop="8dp"
android:text="@string/push_staking_reward_single_account_title"
android:textColor="@color/text_primary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pushWelcomeLogo" />
<TextView
android:id="@+id/pushWelcomeTemplateMessage"
style="@style/TextAppearance.NovaFoundation.Regular.Caption1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="12dp"
android:text="@string/push_template_message"
android:textColor="@color/chip_text"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pushWelcomeTemplateTitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>
<io.novafoundation.nova.common.view.PrimaryButton
android:id="@+id/pushWelcomeEnableButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="16dp"
android:text="@string/push_welcome_enable_button"
app:appearance="primary"
app:layout_constraintBottom_toTopOf="@id/pushWelcomeCancelButton" />
<io.novafoundation.nova.common.view.PrimaryButton
android:id="@+id/pushWelcomeCancelButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="16dp"
android:text="@string/common_not_now"
app:appearance="secondary"
app:layout_constraintBottom_toTopOf="@id/pushWelcomeTermsAndConditions" />
<TextView
android:id="@+id/pushWelcomeTermsAndConditions"
style="@style/TextAppearance.NovaFoundation.Body1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/x2"
android:layout_marginEnd="@dimen/x2"
android:layout_marginBottom="24dp"
android:gravity="center"
android:text="@string/push_welcome_terms_and_conditions"
android:textColor="@color/text_secondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<io.novafoundation.nova.common.view.settings.SettingsGroupView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp">
<io.novafoundation.nova.common.view.settings.SettingsSwitcherView
android:id="@+id/pushGovernanceItemState"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:icon="@drawable/ic_pezkuwi"
tools:title="Polkdaot" />
<io.novafoundation.nova.common.view.settings.SettingsSwitcherView
android:id="@+id/pushGovernanceItemNewReferenda"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/push_governance_settings_new_referendum" />
<io.novafoundation.nova.common.view.settings.SettingsSwitcherView
android:id="@+id/pushGovernanceItemReferendumUpdate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/push_governance_settings_referendum_update" />
<io.novafoundation.nova.common.view.settings.SettingsItemView
android:id="@+id/pushGovernanceItemTracks"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/delegation_tracks" />
</io.novafoundation.nova.common.view.settings.SettingsGroupView>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<io.novafoundation.nova.common.view.settings.SettingsGroupView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp">
<io.novafoundation.nova.common.view.settings.SettingsSwitcherView
android:id="@+id/pushStakingItem"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:icon="@drawable/ic_pezkuwi"
tools:title="Polkdaot" />
</io.novafoundation.nova.common.view.settings.SettingsGroupView>