Implement internal periodic notifications

This commit is contained in:
Rafał Kobyłko 2024-01-07 21:07:11 +01:00
parent 9c12a99460
commit 13721cdab9
26 changed files with 665 additions and 22 deletions

View File

@ -0,0 +1,296 @@
{
"formatVersion": 1,
"database": {
"version": 13,
"identityHash": "00659315e21cc36273e2fcd4ae35ce93",
"entities": [
{
"tableName": "local_services",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `secret` TEXT NOT NULL, `serviceTypeId` TEXT, `iconCollectionId` TEXT, `source` TEXT, `otpLink` TEXT, `otpLabel` TEXT, `otpAccount` TEXT, `otpIssuer` TEXT, `otpDigits` INTEGER, `otpPeriod` INTEGER, `otpAlgorithm` TEXT, `backupSyncStatus` TEXT NOT NULL, `updatedAt` INTEGER NOT NULL, `badgeColor` TEXT, `selectedImageType` TEXT, `labelText` TEXT, `labelBackgroundColor` TEXT, `groupId` TEXT, `isDeleted` INTEGER, `authType` TEXT, `hotpCounter` INTEGER, `hotpCounterTimestamp` INTEGER, `revealTimestamp` INTEGER, `assignedDomains` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "secret",
"columnName": "secret",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serviceTypeId",
"columnName": "serviceTypeId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "iconCollectionId",
"columnName": "iconCollectionId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "source",
"columnName": "source",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "otpLink",
"columnName": "otpLink",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "otpLabel",
"columnName": "otpLabel",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "otpAccount",
"columnName": "otpAccount",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "otpIssuer",
"columnName": "otpIssuer",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "otpDigits",
"columnName": "otpDigits",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "otpPeriod",
"columnName": "otpPeriod",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "otpAlgorithm",
"columnName": "otpAlgorithm",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "backupSyncStatus",
"columnName": "backupSyncStatus",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "updatedAt",
"columnName": "updatedAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "badgeColor",
"columnName": "badgeColor",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "selectedImageType",
"columnName": "selectedImageType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "labelText",
"columnName": "labelText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "labelBackgroundColor",
"columnName": "labelBackgroundColor",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "groupId",
"columnName": "groupId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isDeleted",
"columnName": "isDeleted",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "authType",
"columnName": "authType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "hotpCounter",
"columnName": "hotpCounter",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "hotpCounterTimestamp",
"columnName": "hotpCounterTimestamp",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "revealTimestamp",
"columnName": "revealTimestamp",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "assignedDomains",
"columnName": "assignedDomains",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "paired_browsers",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `extensionPublicKey` TEXT NOT NULL, `pairedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "extensionPublicKey",
"columnName": "extensionPublicKey",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "pairedAt",
"columnName": "pairedAt",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "notifications",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `category` TEXT NOT NULL, `link` TEXT NOT NULL, `message` TEXT NOT NULL, `publishTime` INTEGER NOT NULL, `push` INTEGER NOT NULL, `platform` TEXT NOT NULL, `isRead` INTEGER NOT NULL, `periodicType` TEXT, `internalRoute` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "category",
"columnName": "category",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "link",
"columnName": "link",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "publishTime",
"columnName": "publishTime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "push",
"columnName": "push",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "platform",
"columnName": "platform",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isRead",
"columnName": "isRead",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "periodicType",
"columnName": "periodicType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "internalRoute",
"columnName": "internalRoute",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '00659315e21cc36273e2fcd4ae35ce93')"
]
}
}

View File

@ -31,12 +31,13 @@ import java.text.Normalizer
autoMigrations = [
AutoMigration(from = 7, to = 8),
AutoMigration(from = 8, to = 9),
AutoMigration(from = 12, to = 13),
]
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
companion object {
const val DB_VERSION = 12
const val DB_VERSION = 13
}
abstract fun serviceDao(): ServiceDao

View File

@ -10,8 +10,9 @@ import com.twofasapp.base.lifecycle.AuthAware
import com.twofasapp.base.lifecycle.AuthLifecycle
import com.twofasapp.data.session.SettingsRepository
import com.twofasapp.designsystem.AppThemeState
import com.twofasapp.workmanager.SyncTimeWorkDispatcher
import com.twofasapp.workmanager.OnAppStartWork
import com.twofasapp.workmanager.OnAppUpdatedWorkDispatcher
import com.twofasapp.workmanager.SyncTimeWorkDispatcher
import org.koin.android.ext.android.get
import org.koin.android.ext.android.inject
import org.koin.core.parameter.parametersOf
@ -32,6 +33,7 @@ class StartActivity : AppCompatActivity(), AuthAware {
onAppUpdatedWorkDispatcher.dispatch()
syncTimeWorkDispatcher.dispatch()
OnAppStartWork.dispatch(this)
if (savedInstanceState == null) {
authTracker.onSplashScreen()

View File

@ -0,0 +1,165 @@
package com.twofasapp.workmanager
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.twofasapp.android.navigation.Screen
import com.twofasapp.data.browserext.BrowserExtRepository
import com.twofasapp.data.notifications.NotificationsRepository
import com.twofasapp.data.notifications.domain.Notification
import com.twofasapp.data.notifications.domain.PeriodicNotificationType
import com.twofasapp.data.services.BackupRepository
import com.twofasapp.data.services.ServicesRepository
import com.twofasapp.data.session.SessionRepository
import kotlinx.coroutines.flow.first
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import timber.log.Timber
import java.time.Duration
import java.time.Instant
import java.util.UUID
class OnAppStartWork(
private val context: Context,
params: WorkerParameters,
) : CoroutineWorker(context, params), KoinComponent {
companion object {
fun dispatch(context: Context) {
val request: OneTimeWorkRequest = OneTimeWorkRequestBuilder<OnAppStartWork>()
.build()
Timber.d("Append new OnAppStartWork")
WorkManager.getInstance(context)
.enqueueUniqueWork("OnAppStartWork", ExistingWorkPolicy.APPEND_OR_REPLACE, request)
}
}
private val sessionRepository: SessionRepository by inject()
private val notificationsRepository: NotificationsRepository by inject()
private val backupRepository: BackupRepository by inject()
private val servicesRepository: ServicesRepository by inject()
private val browserExtRepository: BrowserExtRepository by inject()
override suspend fun doWork(): Result {
checkPeriodicNotifications()
return Result.success()
}
private suspend fun checkPeriodicNotifications() {
if (sessionRepository.isOnboardingDisplayed().not()) {
Timber.d("This is first app launch -> do nothing")
// This is first app launch -> do nothing
return
}
val n = notificationsRepository.getPeriodicNotificationCounter()
val timestampMillis = notificationsRepository.getPeriodicNotificationTimestamp()
val timestamp = Instant.ofEpochMilli(timestampMillis)
val now = Instant.now()
Timber.d("n = $n")
if (timestampMillis != 0L && now.isBefore(timestamp.plusSeconds(Duration.ofDays(30).toSeconds()))) {
Timber.d("Less than 30 days")
// Finish when last notification was triggered less than 30 days ago
return
}
val notificationToShow = when (n) {
-1 -> PeriodicNotificationType.TipsAndTricks
0 -> PeriodicNotificationType.Backup
1 -> PeriodicNotificationType.BrowserExtension
2 -> PeriodicNotificationType.Donate
else -> return
}
showNotification(notificationToShow)
notificationsRepository.setPeriodicNotificationCounter(if (n == 2) 0 else n + 1)
notificationsRepository.setPeriodicNotificationTimestamp(now.toEpochMilli())
}
private suspend fun showNotification(type: PeriodicNotificationType) {
Timber.d("Show notification: $type")
notificationsRepository.clearPeriodicNotifications()
when (type) {
PeriodicNotificationType.TipsAndTricks -> {
notificationsRepository.insertPeriodicNotification(
type = type,
notification = createPeriodicNotification(
category = Notification.Category.Youtube,
message = context.getString(com.twofasapp.locale.R.string.periodic_notification_tips),
link = "https://2fas.com/2fasauth-tutorial",
)
)
}
PeriodicNotificationType.Backup -> {
if (servicesRepository.getServices().isNotEmpty() && backupRepository.observeCloudBackupStatus().first().active.not()) {
// User has services but backup is inactive
notificationsRepository.insertPeriodicNotification(
type = type,
notification = createPeriodicNotification(
category = Notification.Category.Updates,
message = context.getString(com.twofasapp.locale.R.string.periodic_notification_backup),
link = "",
internalRoute = Screen.Backup.route,
)
)
}
}
PeriodicNotificationType.BrowserExtension -> {
if (servicesRepository.getServices().isNotEmpty() && browserExtRepository.observePairedBrowsers().first().isEmpty()) {
notificationsRepository.insertPeriodicNotification(
type = type,
notification = createPeriodicNotification(
category = Notification.Category.News,
message = context.getString(com.twofasapp.locale.R.string.periodic_notification_browser_extension),
link = "https://2fas.com/browser-extension/",
)
)
}
}
PeriodicNotificationType.Donate -> {
if (servicesRepository.getServices().isNotEmpty()) {
notificationsRepository.insertPeriodicNotification(
type = type,
notification = createPeriodicNotification(
category = Notification.Category.Features,
message = context.getString(com.twofasapp.locale.R.string.periodic_notification_donate),
link = "https://2fas.com/donate/",
)
)
}
}
}
}
private fun createPeriodicNotification(
category: Notification.Category,
message: String,
link: String,
internalRoute: String? = null,
): Notification {
return Notification(
id = UUID.randomUUID().toString(),
category = category,
link = link,
message = message,
publishTime = Instant.now().toEpochMilli(),
push = false,
platform = "android",
isRead = false,
internalRoute = internalRoute,
)
}
}

View File

@ -5,7 +5,7 @@
Release: Working copy
Locale: de-DE, German (Germany)
Exported by: rafakob
Exported at: Thu, 21 Dec 2023 12:34:52 -0800
Exported at: Sun, 07 Jan 2024 11:57:56 -0800
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- InfoPlist.strings
@ -824,4 +824,8 @@
<string name="settings__manage_tokens">Token verwalten</string>
<string name="settings__info_footer">Deine Unterstützung ermöglicht uns die Entwicklung neuer Funktionen und Verbesserungen. Danke dir!</string>
<string name="settings__trash_option">Wiederherstellen aus dem Papierkorb</string>
<string name="periodic_notification_tips">Schön, dass du bei 2FAS dabei bist! 🌟 Nimm dir einen Moment Zeit, um hilfreiche Tipps &amp; Tricks 🛠️ in der App für mehr Sicherheit zu entdecken 🔒.</string>
<string name="periodic_notification_backup">Aktiviere 2FAS Backup &amp; Sync, um sicherzustellen, dass du nie ausgesperrt wirst, selbst wenn du dein Telefon verlierst - Dein Seelenfrieden mit einem Fingertipp! 🔐📱</string>
<string name="periodic_notification_browser_extension">Beschleunige deine Anmeldungen mit der 2FAS-Browsererweiterung! 🚀 Jetzt herunterladen für eine schnellere und bequemere Authentifizierung. 🌐✨</string>
<string name="periodic_notification_donate">Wir sind dankbar für deine Unterstützung von 2FAS! 🌟 Wenn du unsere App hilfreich findest, ziehe eine Spende in Betracht, um uns zu helfen, deine digitale Welt sicher zu halten. Jedes bisschen hilft! 🙏💙</string>
</resources>

View File

@ -5,7 +5,7 @@
Release: Working copy
Locale: es-ES, Spanish (Spain)
Exported by: rafakob
Exported at: Thu, 21 Dec 2023 12:34:52 -0800
Exported at: Sun, 07 Jan 2024 11:57:56 -0800
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- InfoPlist.strings
@ -824,4 +824,8 @@
<string name="settings__manage_tokens">Gestionar tokens</string>
<string name="settings__info_footer">Tu apoyo nos permite desarrollar nuevas características y mejoras. ¡Gracias!</string>
<string name="settings__trash_option">Recuperar de la papelera</string>
<string name="periodic_notification_tips">Glad you\'re with us at 2FAS! 🌟 Take a moment to discover helpful Tips &amp; Tricks 🛠️ in the app for enhanced security 🔒.</string>
<string name="periodic_notification_backup">Enable 2FAS Backup &amp; Sync to ensure you never get locked out, even if you lose your phone - your peace of mind in one tap! 🔐📱</string>
<string name="periodic_notification_browser_extension">Speed up your logins with the 2FAS Browser Extension! 🚀 Download now for a faster, more convenient authentication. 🌐✨</string>
<string name="periodic_notification_donate">We\'re grateful for your support of 2FAS! 🌟 If you find our app helpful, consider making a donation to help us keep your digital world secure. Every bit helps! 🙏💙</string>
</resources>

View File

@ -5,7 +5,7 @@
Release: Working copy
Locale: fr-FR, French (France)
Exported by: rafakob
Exported at: Thu, 21 Dec 2023 12:34:52 -0800
Exported at: Sun, 07 Jan 2024 11:57:56 -0800
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- InfoPlist.strings
@ -824,4 +824,8 @@
<string name="settings__manage_tokens">Gérer les jetons</string>
<string name="settings__info_footer">Votre soutien nous permet de développer de nouvelles fonctionnalités et\naméliorations. Merci!</string>
<string name="settings__trash_option">Récupérer de la corbeille</string>
<string name="periodic_notification_tips">Glad you\'re with us at 2FAS! 🌟 Take a moment to discover helpful Tips &amp; Tricks 🛠️ in the app for enhanced security 🔒.</string>
<string name="periodic_notification_backup">Enable 2FAS Backup &amp; Sync to ensure you never get locked out, even if you lose your phone - your peace of mind in one tap! 🔐📱</string>
<string name="periodic_notification_browser_extension">Speed up your logins with the 2FAS Browser Extension! 🚀 Download now for a faster, more convenient authentication. 🌐✨</string>
<string name="periodic_notification_donate">We\'re grateful for your support of 2FAS! 🌟 If you find our app helpful, consider making a donation to help us keep your digital world secure. Every bit helps! 🙏💙</string>
</resources>

View File

@ -5,7 +5,7 @@
Release: Working copy
Locale: id-ID, Indonesian (Indonesia)
Exported by: rafakob
Exported at: Thu, 21 Dec 2023 12:34:52 -0800
Exported at: Sun, 07 Jan 2024 11:57:56 -0800
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- InfoPlist.strings
@ -818,4 +818,8 @@
<string name="settings__manage_tokens">Mengelola token</string>
<string name="settings__info_footer">Dukungan Anda memungkinkan kami untuk mengembangkan fitur-fitur baru dan\nperbaikan. Terima kasih!</string>
<string name="settings__trash_option">Ambil dari tempat sampah</string>
<string name="periodic_notification_tips">Glad you\'re with us at 2FAS! 🌟 Take a moment to discover helpful Tips &amp; Tricks 🛠️ in the app for enhanced security 🔒.</string>
<string name="periodic_notification_backup">Enable 2FAS Backup &amp; Sync to ensure you never get locked out, even if you lose your phone - your peace of mind in one tap! 🔐📱</string>
<string name="periodic_notification_browser_extension">Speed up your logins with the 2FAS Browser Extension! 🚀 Download now for a faster, more convenient authentication. 🌐✨</string>
<string name="periodic_notification_donate">We\'re grateful for your support of 2FAS! 🌟 If you find our app helpful, consider making a donation to help us keep your digital world secure. Every bit helps! 🙏💙</string>
</resources>

View File

@ -5,7 +5,7 @@
Release: Working copy
Locale: it-IT, Italian (Italy)
Exported by: rafakob
Exported at: Thu, 21 Dec 2023 12:34:52 -0800
Exported at: Sun, 07 Jan 2024 11:57:56 -0800
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- InfoPlist.strings
@ -824,4 +824,8 @@
<string name="settings__manage_tokens">Gestisci i token</string>
<string name="settings__info_footer">Il tuo supporto ci permette di sviluppare nuove funzionalità e miglioramenti. Grazie!</string>
<string name="settings__trash_option">Ripristina dal cestino</string>
<string name="periodic_notification_tips">Glad you\'re with us at 2FAS! 🌟 Take a moment to discover helpful Tips &amp; Tricks 🛠️ in the app for enhanced security 🔒.</string>
<string name="periodic_notification_backup">Enable 2FAS Backup &amp; Sync to ensure you never get locked out, even if you lose your phone - your peace of mind in one tap! 🔐📱</string>
<string name="periodic_notification_browser_extension">Speed up your logins with the 2FAS Browser Extension! 🚀 Download now for a faster, more convenient authentication. 🌐✨</string>
<string name="periodic_notification_donate">We\'re grateful for your support of 2FAS! 🌟 If you find our app helpful, consider making a donation to help us keep your digital world secure. Every bit helps! 🙏💙</string>
</resources>

View File

@ -5,7 +5,7 @@
Release: Working copy
Locale: nl-NL, Dutch (Netherlands)
Exported by: rafakob
Exported at: Thu, 21 Dec 2023 12:34:52 -0800
Exported at: Sun, 07 Jan 2024 11:57:56 -0800
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- InfoPlist.strings
@ -824,4 +824,8 @@
<string name="settings__manage_tokens">Tokens beheren</string>
<string name="settings__info_footer">Dankzij uw steun kunnen we nieuwe functies en\nverbeteringen. Hartelijk dank!</string>
<string name="settings__trash_option">Ophalen uit prullenbak</string>
<string name="periodic_notification_tips">Glad you\'re with us at 2FAS! 🌟 Take a moment to discover helpful Tips &amp; Tricks 🛠️ in the app for enhanced security 🔒.</string>
<string name="periodic_notification_backup">Enable 2FAS Backup &amp; Sync to ensure you never get locked out, even if you lose your phone - your peace of mind in one tap! 🔐📱</string>
<string name="periodic_notification_browser_extension">Speed up your logins with the 2FAS Browser Extension! 🚀 Download now for a faster, more convenient authentication. 🌐✨</string>
<string name="periodic_notification_donate">We\'re grateful for your support of 2FAS! 🌟 If you find our app helpful, consider making a donation to help us keep your digital world secure. Every bit helps! 🙏💙</string>
</resources>

View File

@ -5,7 +5,7 @@
Release: Working copy
Locale: pl-PL, Polish (Poland)
Exported by: rafakob
Exported at: Thu, 21 Dec 2023 12:34:52 -0800
Exported at: Sun, 07 Jan 2024 11:57:56 -0800
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- InfoPlist.strings
@ -830,4 +830,8 @@
<string name="settings__manage_tokens">Zarządzaj tokenami</string>
<string name="settings__info_footer">Twoje wsparcie pozwala nam rozwijać nowe funkcje i ulepszać aplikację. Dziękujemy!</string>
<string name="settings__trash_option">Przywróć z kosza</string>
<string name="periodic_notification_tips">Glad you\'re with us at 2FAS! 🌟 Take a moment to discover helpful Tips &amp; Tricks 🛠️ in the app for enhanced security 🔒.</string>
<string name="periodic_notification_backup">Enable 2FAS Backup &amp; Sync to ensure you never get locked out, even if you lose your phone - your peace of mind in one tap! 🔐📱</string>
<string name="periodic_notification_browser_extension">Speed up your logins with the 2FAS Browser Extension! 🚀 Download now for a faster, more convenient authentication. 🌐✨</string>
<string name="periodic_notification_donate">We\'re grateful for your support of 2FAS! 🌟 If you find our app helpful, consider making a donation to help us keep your digital world secure. Every bit helps! 🙏💙</string>
</resources>

View File

@ -5,7 +5,7 @@
Release: Working copy
Locale: pt-BR, Brazilian Portuguese
Exported by: rafakob
Exported at: Thu, 21 Dec 2023 12:34:52 -0800
Exported at: Sun, 07 Jan 2024 11:57:56 -0800
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- InfoPlist.strings
@ -824,4 +824,8 @@
<string name="settings__manage_tokens">Gerenciar tokens</string>
<string name="settings__info_footer">O seu apoio nos permite desenvolver novas funcionalidades e melhorias. Obrigado!\n\n\n\n\n\n</string>
<string name="settings__trash_option">Recuperar da lixeira</string>
<string name="periodic_notification_tips">Glad you\'re with us at 2FAS! 🌟 Take a moment to discover helpful Tips &amp; Tricks 🛠️ in the app for enhanced security 🔒.</string>
<string name="periodic_notification_backup">Enable 2FAS Backup &amp; Sync to ensure you never get locked out, even if you lose your phone - your peace of mind in one tap! 🔐📱</string>
<string name="periodic_notification_browser_extension">Speed up your logins with the 2FAS Browser Extension! 🚀 Download now for a faster, more convenient authentication. 🌐✨</string>
<string name="periodic_notification_donate">We\'re grateful for your support of 2FAS! 🌟 If you find our app helpful, consider making a donation to help us keep your digital world secure. Every bit helps! 🙏💙</string>
</resources>

View File

@ -5,7 +5,7 @@
Release: Working copy
Locale: pt-PT, Portuguese (Portugal)
Exported by: rafakob
Exported at: Thu, 21 Dec 2023 12:34:52 -0800
Exported at: Sun, 07 Jan 2024 11:57:56 -0800
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- InfoPlist.strings
@ -824,4 +824,8 @@
<string name="settings__manage_tokens">Manage tokens</string>
<string name="settings__info_footer">Your support allows us to develop new features and\nimprovements. Thank you!</string>
<string name="settings__trash_option">Retrieve from trash</string>
<string name="periodic_notification_tips">Glad you\'re with us at 2FAS! 🌟 Take a moment to discover helpful Tips &amp; Tricks 🛠️ in the app for enhanced security 🔒.</string>
<string name="periodic_notification_backup">Enable 2FAS Backup &amp; Sync to ensure you never get locked out, even if you lose your phone - your peace of mind in one tap! 🔐📱</string>
<string name="periodic_notification_browser_extension">Speed up your logins with the 2FAS Browser Extension! 🚀 Download now for a faster, more convenient authentication. 🌐✨</string>
<string name="periodic_notification_donate">We\'re grateful for your support of 2FAS! 🌟 If you find our app helpful, consider making a donation to help us keep your digital world secure. Every bit helps! 🙏💙</string>
</resources>

View File

@ -5,7 +5,7 @@
Release: Working copy
Locale: tr-TR, Turkish (Turkey)
Exported by: rafakob
Exported at: Thu, 21 Dec 2023 12:34:52 -0800
Exported at: Sun, 07 Jan 2024 11:57:56 -0800
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- InfoPlist.strings
@ -824,4 +824,8 @@
<string name="settings__manage_tokens">Jetonları yönetin</string>
<string name="settings__info_footer">Desteğiniz yeni özellikler geliştirmemize ve\niyileştirmeler. Teşekkür ederim!</string>
<string name="settings__trash_option">Çöp kutusundan al</string>
<string name="periodic_notification_tips">Glad you\'re with us at 2FAS! 🌟 Take a moment to discover helpful Tips &amp; Tricks 🛠️ in the app for enhanced security 🔒.</string>
<string name="periodic_notification_backup">Enable 2FAS Backup &amp; Sync to ensure you never get locked out, even if you lose your phone - your peace of mind in one tap! 🔐📱</string>
<string name="periodic_notification_browser_extension">Speed up your logins with the 2FAS Browser Extension! 🚀 Download now for a faster, more convenient authentication. 🌐✨</string>
<string name="periodic_notification_donate">We\'re grateful for your support of 2FAS! 🌟 If you find our app helpful, consider making a donation to help us keep your digital world secure. Every bit helps! 🙏💙</string>
</resources>

View File

@ -5,7 +5,7 @@
Release: Working copy
Locale: uk-UA, Ukrainian (Ukraine)
Exported by: rafakob
Exported at: Thu, 21 Dec 2023 12:34:52 -0800
Exported at: Sun, 07 Jan 2024 11:57:56 -0800
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- InfoPlist.strings
@ -830,4 +830,8 @@
<string name="settings__manage_tokens">Manage tokens</string>
<string name="settings__info_footer">Your support allows us to develop new features and\nimprovements. Thank you!</string>
<string name="settings__trash_option">Retrieve from trash</string>
<string name="periodic_notification_tips">Glad you\'re with us at 2FAS! 🌟 Take a moment to discover helpful Tips &amp; Tricks 🛠️ in the app for enhanced security 🔒.</string>
<string name="periodic_notification_backup">Enable 2FAS Backup &amp; Sync to ensure you never get locked out, even if you lose your phone - your peace of mind in one tap! 🔐📱</string>
<string name="periodic_notification_browser_extension">Speed up your logins with the 2FAS Browser Extension! 🚀 Download now for a faster, more convenient authentication. 🌐✨</string>
<string name="periodic_notification_donate">We\'re grateful for your support of 2FAS! 🌟 If you find our app helpful, consider making a donation to help us keep your digital world secure. Every bit helps! 🙏💙</string>
</resources>

View File

@ -5,7 +5,7 @@
Release: Working copy
Locale: en, English
Exported by: rafakob
Exported at: Thu, 21 Dec 2023 12:34:52 -0800
Exported at: Sun, 07 Jan 2024 11:57:56 -0800
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- InfoPlist.strings
@ -824,4 +824,8 @@
<string name="settings__manage_tokens">Manage tokens</string>
<string name="settings__info_footer">Your support allows us to develop new features and\nimprovements. Thank you!</string>
<string name="settings__trash_option">Retrieve from trash</string>
<string name="periodic_notification_tips">Glad you\'re with us at 2FAS! 🌟 Take a moment to discover helpful Tips &amp; Tricks 🛠️ in the app for enhanced security 🔒.</string>
<string name="periodic_notification_backup">Enable 2FAS Backup &amp; Sync to ensure you never get locked out, even if you lose your phone - your peace of mind in one tap! 🔐📱</string>
<string name="periodic_notification_browser_extension">Speed up your logins with the 2FAS Browser Extension! 🚀 Download now for a faster, more convenient authentication. 🌐✨</string>
<string name="periodic_notification_donate">We\'re grateful for your support of 2FAS! 🌟 If you find our app helpful, consider making a donation to help us keep your digital world secure. Every bit helps! 🙏💙</string>
</resources>

View File

@ -1,6 +1,7 @@
package com.twofasapp.data.notifications
import com.twofasapp.data.notifications.domain.Notification
import com.twofasapp.data.notifications.domain.PeriodicNotificationType
import kotlinx.coroutines.flow.Flow
interface NotificationsRepository {
@ -8,4 +9,10 @@ interface NotificationsRepository {
suspend fun fetchNotifications(sinceMillis: Long)
suspend fun readAllNotifications()
fun hasUnreadNotifications(): Flow<Boolean>
suspend fun getPeriodicNotificationCounter(): Int
suspend fun setPeriodicNotificationCounter(counter: Int)
suspend fun getPeriodicNotificationTimestamp(): Long
suspend fun setPeriodicNotificationTimestamp(timestamp: Long)
suspend fun clearPeriodicNotifications()
suspend fun insertPeriodicNotification(type: PeriodicNotificationType, notification: Notification)
}

View File

@ -2,6 +2,7 @@ package com.twofasapp.data.notifications
import com.twofasapp.common.coroutines.Dispatchers
import com.twofasapp.data.notifications.domain.Notification
import com.twofasapp.data.notifications.domain.PeriodicNotificationType
import com.twofasapp.data.notifications.local.NotificationsLocalSource
import com.twofasapp.data.notifications.mappper.asDomain
import com.twofasapp.data.notifications.remote.NotificationsRemoteSource
@ -47,4 +48,40 @@ internal class NotificationsRepositoryImpl(
private fun List<Notification>.sortedByTime(): List<Notification> {
return sortedWith(compareBy({ it.isRead }, { it.publishTime.unaryMinus() }))
}
override suspend fun getPeriodicNotificationCounter(): Int {
return withContext(dispatchers.io) {
local.getPeriodicNotificationCounter()
}
}
override suspend fun setPeriodicNotificationCounter(counter: Int) {
withContext(dispatchers.io) {
local.setPeriodicNotificationCounter(counter)
}
}
override suspend fun getPeriodicNotificationTimestamp(): Long {
return withContext(dispatchers.io) {
local.getPeriodicNotificationTimestamp()
}
}
override suspend fun setPeriodicNotificationTimestamp(timestamp: Long) {
withContext(dispatchers.io) {
local.setPeriodicNotificationTimestamp(timestamp)
}
}
override suspend fun clearPeriodicNotifications() {
withContext(dispatchers.io) {
local.clearPeriodicNotifications()
}
}
override suspend fun insertPeriodicNotification(type: PeriodicNotificationType, notification: Notification) {
withContext(dispatchers.io) {
local.insertPeriodicNotification(type, notification)
}
}
}

View File

@ -4,6 +4,7 @@ data class Notification(
val id: String,
val category: Category,
val link: String,
val internalRoute: String?,
val message: String,
val publishTime: Long,
val push: Boolean,

View File

@ -0,0 +1,9 @@
package com.twofasapp.data.notifications.domain
enum class PeriodicNotificationType {
TipsAndTricks,
Backup,
BrowserExtension,
Donate,
;
}

View File

@ -1,6 +1,11 @@
package com.twofasapp.data.notifications.local
import androidx.room.*
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import com.twofasapp.data.notifications.local.model.NotificationEntity
import kotlinx.coroutines.flow.Flow
@ -44,6 +49,9 @@ interface NotificationsDao {
@Query("DELETE FROM notifications WHERE id IN (:ids)")
fun delete(ids: List<String>)
@Query("DELETE FROM notifications")
@Query("DELETE FROM notifications WHERE periodicType IS NOT NULL")
fun deleteAllPeriodic()
@Query("DELETE FROM notifications WHERE periodicType IS NULL")
fun deleteAll()
}

View File

@ -1,15 +1,23 @@
package com.twofasapp.data.notifications.local
import com.twofasapp.data.notifications.domain.Notification
import com.twofasapp.data.notifications.domain.PeriodicNotificationType
import com.twofasapp.data.notifications.mappper.asDomain
import com.twofasapp.data.notifications.mappper.asEntity
import com.twofasapp.storage.PlainPreferences
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
internal class NotificationsLocalSource(
private val notificationsDao: NotificationsDao,
private val preferences: PlainPreferences,
) {
companion object {
private const val KeyPeriodicNotificationCounter = "periodicNotificationCounter"
private const val KeyPeriodicNotificationTimestamp = "periodicNotificationTimestamp" // Time when last notification was triggered
}
suspend fun getNotifications(): List<Notification> {
return notificationsDao.select().map { it.asDomain() }
}
@ -29,4 +37,28 @@ internal class NotificationsLocalSource(
*notificationsDao.select().map { it.copy(isRead = true) }.toTypedArray()
)
}
suspend fun getPeriodicNotificationCounter(): Int {
return preferences.getInt(KeyPeriodicNotificationCounter) ?: -1
}
suspend fun setPeriodicNotificationCounter(counter: Int) {
preferences.putInt(KeyPeriodicNotificationCounter, counter)
}
suspend fun getPeriodicNotificationTimestamp(): Long {
return preferences.getLong(KeyPeriodicNotificationTimestamp) ?: 0
}
suspend fun setPeriodicNotificationTimestamp(timestamp: Long) {
preferences.putLong(KeyPeriodicNotificationTimestamp, timestamp)
}
suspend fun clearPeriodicNotifications() {
notificationsDao.deleteAllPeriodic()
}
suspend fun insertPeriodicNotification(periodicType: PeriodicNotificationType, notification: Notification) {
notificationsDao.insert(notification.asEntity(periodicType.name))
}
}

View File

@ -13,4 +13,6 @@ data class NotificationEntity(
val push: Boolean,
val platform: String,
val isRead: Boolean,
val periodicType: String?,
val internalRoute: String?,
)

View File

@ -4,7 +4,7 @@ import com.twofasapp.data.notifications.domain.Notification
import com.twofasapp.data.notifications.local.model.NotificationEntity
import com.twofasapp.data.notifications.remote.model.NotificationJson
internal fun Notification.asEntity() = NotificationEntity(
internal fun Notification.asEntity(periodicType: String? = null) = NotificationEntity(
id = id,
category = category.name,
link = link,
@ -13,6 +13,8 @@ internal fun Notification.asEntity() = NotificationEntity(
push = push,
platform = platform,
isRead = isRead,
periodicType = periodicType,
internalRoute = internalRoute,
)
internal fun NotificationEntity.asDomain() = Notification(
@ -24,6 +26,7 @@ internal fun NotificationEntity.asDomain() = Notification(
push = push,
platform = platform,
isRead = isRead,
internalRoute = internalRoute,
)
internal fun NotificationJson.asDomain() = Notification(
@ -35,4 +38,5 @@ internal fun NotificationJson.asDomain() = Notification(
push = push,
platform = platform,
isRead = false,
internalRoute = null,
)

View File

@ -45,7 +45,19 @@ fun NavGraphBuilder.homeNavigation(
}
composable(Screen.Notifications.route) {
NotificationsScreen()
NotificationsScreen(
openInternalRoute = { route ->
when (route) {
Screen.Backup.route -> {
navController.navigate(Screen.Backup.routeWithArgs(NavArg.TurnOnBackup to true))
}
else -> {
navController.navigate(route)
}
}
}
)
}
composable(Screen.EditService.route, listOf(NavArg.ServiceId)) {

View File

@ -38,12 +38,14 @@ import org.koin.androidx.compose.koinViewModel
@Composable
internal fun NotificationsScreen(
viewModel: NotificationsViewModel = koinViewModel(),
openInternalRoute: (String) -> Unit,
) {
val notifications by viewModel.notificationsList.collectAsStateWithLifecycle()
ScreenContent(
notifications = notifications,
onNotificationClick = { viewModel.onNotificationClick(it) }
onNotificationClick = { viewModel.onNotificationClick(it) },
onInternalRouteClick = openInternalRoute,
)
}
@ -51,6 +53,7 @@ internal fun NotificationsScreen(
private fun ScreenContent(
notifications: List<Notification>,
onNotificationClick: (Notification) -> Unit,
onInternalRouteClick: (String) -> Unit,
) {
val uriHandler = LocalUriHandler.current
val context = LocalContext.current
@ -77,10 +80,24 @@ private fun ScreenContent(
Notification(
notification = notification,
modifier = Modifier
.clickable {
.clickable(
notification.link.isNotBlank() || notification.internalRoute
.isNullOrBlank()
.not()
) {
onNotificationClick(notification)
if (notification.link.isNotBlank()) {
uriHandler.openSafely(notification.link, context)
}
if (notification.internalRoute
.isNullOrBlank()
.not()
) {
onInternalRouteClick(notification.internalRoute.orEmpty())
}
}
.background(if (notification.isRead) TwTheme.color.surface else TwTheme.color.background)
.padding(16.dp)
)
@ -133,6 +150,8 @@ private fun Notification(
Spacer(modifier = Modifier.width(24.dp))
if (notification.link.isNotBlank()) {
Icon(painter = TwIcons.ExternalLink, contentDescription = null, tint = TwTheme.color.iconTint)
}
}
}