diff --git a/.run/iosApp.run.xml b/.run/iosApp.run.xml index ba840de..5b54005 100644 --- a/.run/iosApp.run.xml +++ b/.run/iosApp.run.xml @@ -1,7 +1,7 @@ - + - + \ No newline at end of file diff --git a/README.md b/README.md index f8add87..647ebf9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ # Play Deals -[![Static Badge](https://img.shields.io/badge/Android-black?logo=android&logoColor=white&color=%234889f5)](https://play.google.com/store/apps/details?id=me.sujanpoudel.playdeals)   -[![Static Badge](https://img.shields.io/badge/IOS-grey?logo=apple)](https://github.com/psuzn/app-deals/releases/latest)    -[![Static Badge](https://img.shields.io/badge/macOS-black?logo=apple)](https://github.com/psuzn/app-deals/releases/latest)   -[![Static Badge](https://img.shields.io/badge/Windows-green?logo=windows&color=blue)](https://github.com/psuzn/app-deals/releases/latest)   -[![Static Badge](https://img.shields.io/badge/Linux-white?logo=linux&logoColor=white&color=grey)](https://github.com/psuzn/app-deals/releases/latest)   + +[![Static Badge](https://img.shields.io/badge/Android-black?logo=android&logoColor=white&color=%234889f5)](https://play.google.com/store/apps/details?id=me.sujanpoudel.playdeals) +  +[![Static Badge](https://img.shields.io/badge/IOS-grey?logo=apple)](https://github.com/psuzn/app-deals/releases/latest) +   +[![Static Badge](https://img.shields.io/badge/macOS-black?logo=apple)](https://github.com/psuzn/app-deals/releases/latest) +  +[![Static Badge](https://img.shields.io/badge/Windows-green?logo=windows&color=blue)](https://github.com/psuzn/app-deals/releases/latest) +  +[![Static Badge](https://img.shields.io/badge/Linux-white?logo=linux&logoColor=white&color=grey)](https://github.com/psuzn/app-deals/releases/latest) +  ![Static Badge](https://img.shields.io/badge/License-GPL--v3-brightgreen) [![Lint and verify](https://github.com/psuzn/App-deals/actions/workflows/lint.yaml/badge.svg?branch=develop)](https://github.com/psuzn/App-deals/actions/workflows/lint.yaml) @@ -16,6 +22,9 @@ Play deals is a simple app that aggregates the paid apps that have ongoing deals and discounts, aka you can get the paid apps free or with discount. +| | | +|-----------------------------------------------------|:----------------------------------------------------:| + ## Download You can download the app from [play store](https://play.google.com/store/apps/details?id=me.sujanpoudel.playdeals) or @@ -44,7 +53,8 @@ It shares same business logic and UI across all the platform. - [Compose Multiplatform UI](https://www.jetbrains.com/lp/compose-multiplatform/) to build the cross platform UI. - [Ktor](https://www.jetbrains.com/lp/compose-multiplatform/) for the http/api calls. - [Kamel](https://github.com/Kamel-Media/Kamel) for loading images. -- [Kodein](https://www.google.com/search?q=kodein&sourceid=chrome&ie=UTF-8) for dependency injection. +- [Kodein](https://github.com/kosi-libs/Kodein) for dependency injection. +- [SQLDelight](https://github.com/cashapp/sqldelight) to offline cache using sqlite. ### UI Navigation @@ -60,7 +70,7 @@ This loosely follows mvvm architecture inspired from android's view model. **In Priority order** : -- [ ] Add Offline Caches for the apps +- [x] Add Offline Caches for the apps - [ ] Push Notifications - [ ] Add ability to add/request new app deal from app diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 23f1ed8..00713e9 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -2,6 +2,7 @@ plugins { kotlin("multiplatform") id("com.android.application") id("org.jetbrains.compose") + id("com.google.gms.google-services") } kotlin { @@ -10,8 +11,12 @@ kotlin { val androidMain by getting { dependencies { implementation(project(":shared")) - api("androidx.activity:activity-compose:1.7.2") - api("androidx.core:core-ktx:1.12.0") + implementation("androidx.activity:activity-compose:1.8.1") + implementation("androidx.core:core-ktx:1.12.0") + + implementation(platform("com.google.firebase:firebase-bom:${Versions.FIREBASE_BOM}")) + implementation("com.google.firebase:firebase-analytics-ktx") + implementation("com.google.firebase:firebase-messaging-ktx") } } } @@ -38,13 +43,26 @@ android { targetCompatibility = JavaVersion.VERSION_17 } + + signingConfigs { + getByName("debug") { + storeFile = file("./key.debug.jks") + storePassword = "play-deals" + keyAlias = "key0" + keyPassword = "play-deals" + } + } + buildTypes { - getByName("release") { + release { isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) } + debug { + isMinifyEnabled = false + } } } diff --git a/androidApp/key.debug.jks b/androidApp/key.debug.jks new file mode 100644 index 0000000..896689f Binary files /dev/null and b/androidApp/key.debug.jks differ diff --git a/androidApp/src/androidMain/AndroidManifest.xml b/androidApp/src/androidMain/AndroidManifest.xml index 1707e02..172b035 100644 --- a/androidApp/src/androidMain/AndroidManifest.xml +++ b/androidApp/src/androidMain/AndroidManifest.xml @@ -2,12 +2,15 @@ + + + + + + + + + + + + + + + + + diff --git a/androidApp/src/androidMain/kotlin/me/sujanpoudel/playdeals/FcmService.kt b/androidApp/src/androidMain/kotlin/me/sujanpoudel/playdeals/FcmService.kt new file mode 100644 index 0000000..c75bdf9 --- /dev/null +++ b/androidApp/src/androidMain/kotlin/me/sujanpoudel/playdeals/FcmService.kt @@ -0,0 +1,74 @@ +package me.sujanpoudel.playdeals + +import android.annotation.SuppressLint +import android.app.NotificationManager +import android.content.Context +import android.content.pm.PackageManager +import com.google.firebase.messaging.CommonNotificationBuilder +import com.google.firebase.messaging.Constants.MessageNotificationKeys +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.ImageDownload +import com.google.firebase.messaging.NotificationParams +import com.google.firebase.messaging.RemoteMessage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull +import kotlin.time.Duration.Companion.seconds + +@SuppressLint("MissingFirebaseInstanceTokenRefresh") +class FcmService : FirebaseMessagingService() { + + override fun onMessageReceived(message: RemoteMessage) { + val data = message.toIntent().extras + + if (!NotificationParams.isNotification(data)) { + return super.onMessageReceived(message) + } + + showNotification(NotificationParams(data!!)) + } +} + +private fun Context.manifestMetadata() = try { + applicationContext.packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA) + .metaData +} catch (e: PackageManager.NameNotFoundException) { + null +} + +@SuppressLint("VisibleForTests") +private fun Context.showNotification(notificationParams: NotificationParams) { + val manifestMetadata = manifestMetadata() + val channel = CommonNotificationBuilder.getOrCreateChannel( + this, + notificationParams.notificationChannelId, + manifestMetadata, + ) + + val info = CommonNotificationBuilder.createNotificationInfo( + this, + this, + notificationParams, + channel, + manifestMetadata, + ) + + val url = notificationParams.getString(MessageNotificationKeys.IMAGE_URL) + + CoroutineScope(Dispatchers.IO).launch { + if (url != null) { + val image = withTimeoutOrNull(10.seconds) { + try { + ImageDownload.create(url)?.blockingDownload() + } catch (_: Exception) { + null + } + } + info.notificationBuilder.setLargeIcon(image) + } + + val notificationManager = getSystemService(FirebaseMessagingService.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.notify(info.tag, info.id, info.notificationBuilder.build()) + } +} diff --git a/androidApp/src/androidMain/kotlin/me/sujanpoudel/playdeals/MainActivity.kt b/androidApp/src/androidMain/kotlin/me/sujanpoudel/playdeals/MainActivity.kt index e05654a..3bc31c2 100644 --- a/androidApp/src/androidMain/kotlin/me/sujanpoudel/playdeals/MainActivity.kt +++ b/androidApp/src/androidMain/kotlin/me/sujanpoudel/playdeals/MainActivity.kt @@ -11,12 +11,11 @@ import me.sujanpoudel.playdeals.common.navigation.BackPressConsumer import me.sujanpoudel.playdeals.common.navigation.LocalBackPressConsumer class MainActivity : ComponentActivity() { - private val callBack = - object : OnBackPressedCallback(false) { - override fun handleOnBackPressed() { - backPressConsumer.onBackPress() - } + private val callBack = object : OnBackPressedCallback(false) { + override fun handleOnBackPressed() { + backPressConsumer.onBackPress() } + } private val backPressConsumer: BackPressConsumer = BackPressConsumer { diff --git a/androidApp/src/androidMain/res/drawable-anydpi/ic_notification.xml b/androidApp/src/androidMain/res/drawable-anydpi/ic_notification.xml new file mode 100644 index 0000000..f5cb82b --- /dev/null +++ b/androidApp/src/androidMain/res/drawable-anydpi/ic_notification.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/androidApp/src/androidMain/res/drawable-hdpi/ic_notification.png b/androidApp/src/androidMain/res/drawable-hdpi/ic_notification.png new file mode 100644 index 0000000..8afd296 Binary files /dev/null and b/androidApp/src/androidMain/res/drawable-hdpi/ic_notification.png differ diff --git a/androidApp/src/androidMain/res/drawable-mdpi/ic_notification.png b/androidApp/src/androidMain/res/drawable-mdpi/ic_notification.png new file mode 100644 index 0000000..0ead4c0 Binary files /dev/null and b/androidApp/src/androidMain/res/drawable-mdpi/ic_notification.png differ diff --git a/androidApp/src/androidMain/res/drawable-xhdpi/ic_notification.png b/androidApp/src/androidMain/res/drawable-xhdpi/ic_notification.png new file mode 100644 index 0000000..f1cb079 Binary files /dev/null and b/androidApp/src/androidMain/res/drawable-xhdpi/ic_notification.png differ diff --git a/androidApp/src/androidMain/res/drawable-xxhdpi/ic_notification.png b/androidApp/src/androidMain/res/drawable-xxhdpi/ic_notification.png new file mode 100644 index 0000000..3350ee7 Binary files /dev/null and b/androidApp/src/androidMain/res/drawable-xxhdpi/ic_notification.png differ diff --git a/androidApp/src/androidMain/res/values-night/styles.xml b/androidApp/src/androidMain/res/values-night/styles.xml index 5919791..fab6adb 100644 --- a/androidApp/src/androidMain/res/values-night/styles.xml +++ b/androidApp/src/androidMain/res/values-night/styles.xml @@ -7,4 +7,4 @@ false - \ No newline at end of file + diff --git a/androidApp/src/androidMain/res/values/colors.xml b/androidApp/src/androidMain/res/values/colors.xml new file mode 100644 index 0000000..5e4a4d6 --- /dev/null +++ b/androidApp/src/androidMain/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #7477CC + diff --git a/androidApp/src/androidMain/res/values/styles.xml b/androidApp/src/androidMain/res/values/styles.xml index a755897..2471de5 100644 --- a/androidApp/src/androidMain/res/values/styles.xml +++ b/androidApp/src/androidMain/res/values/styles.xml @@ -5,4 +5,4 @@ true true - \ No newline at end of file + diff --git a/androidApp/src/google-services.json b/androidApp/src/google-services.json new file mode 100644 index 0000000..bc0d5d7 --- /dev/null +++ b/androidApp/src/google-services.json @@ -0,0 +1,40 @@ +{ + "project_info": { + "project_number": "408920570359", + "firebase_url": "https://play-deals.firebaseio.com", + "project_id": "play-deals", + "storage_bucket": "play-deals.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:408920570359:android:73507efea95268337b16f2", + "android_client_info": { + "package_name": "me.sujanpoudel.playdeals" + } + }, + "oauth_client": [ + { + "client_id": "408920570359-vcp440gf1q7gm4ae8mnv9k0v1hsoqfcj.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAc5FcH3QJ3tlgqwXJDHrApEegd-CzB654" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "408920570359-i6c0bhijsqshb1ok3d9v6ucvpj0p44vq.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index e8f5d4c..249ff11 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,6 +13,7 @@ plugins { id("org.jlleitschuh.gradle.ktlint") version "11.5.0" apply true id("com.codingfeline.buildkonfig") version "0.14.0" apply false id("app.cash.sqldelight") version Versions.SQLDELIGHT apply false + id("com.google.gms.google-services") version "4.3.15" apply false } allprojects { diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index a727b08..7380dff 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -4,8 +4,9 @@ object Versions { const val COMPOSE = "1.5.1" const val SETTINGS = "1.0.0" const val SQLDELIGHT = "2.0.0" + const val FIREBASE_BOM = "32.3.1" - const val KTOR = "2.3.4" + const val KTOR = "2.3.5" const val KODE_IN = "7.20.2" const val COROUTINE = "1.7.3" const val KOTLINX_DATE_TIME = "0.4.0" diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 471a96a..40607cd 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -54,12 +54,54 @@ path = "Preview Content"; sourceTree = ""; }; + 660552A59E317B1A0521E533 /* xcuserdata */ = { + isa = PBXGroup; + children = ( + 66055D0E5EE5D965A6F2784A /* psuzn.xcuserdatad */, + ); + name = xcuserdata; + path = iosApp.xcworkspace/xcuserdata; + sourceTree = ""; + }; + 66055371CAB4F04A02474CCD /* psuzn.xcuserdatad */ = { + isa = PBXGroup; + children = ( + 660556F1E49162B926360BDA /* xcschemes */, + ); + name = psuzn.xcuserdatad; + path = iosApp.xcodeproj/xcuserdata/psuzn.xcuserdatad; + sourceTree = ""; + }; + 660556F1E49162B926360BDA /* xcschemes */ = { + isa = PBXGroup; + children = ( + ); + path = xcschemes; + sourceTree = ""; + }; + 66055A63D5516D613AE11F11 /* xcschemes */ = { + isa = PBXGroup; + children = ( + ); + path = xcschemes; + sourceTree = ""; + }; + 66055D0E5EE5D965A6F2784A /* psuzn.xcuserdatad */ = { + isa = PBXGroup; + children = ( + 66055A63D5516D613AE11F11 /* xcschemes */, + ); + path = psuzn.xcuserdatad; + sourceTree = ""; + }; 7555FF72242A565900829871 = { isa = PBXGroup; children = ( 7555FF7D242A565900829871 /* iosApp */, 7555FF7C242A565900829871 /* Products */, D1373ED9D54E3DB0D54869DB /* Pods */, + 660552A59E317B1A0521E533 /* xcuserdata */, + 66055371CAB4F04A02474CCD /* psuzn.xcuserdatad */, ); sourceTree = ""; }; diff --git a/media/screenshot-black.jpg b/media/screenshot-black.jpg new file mode 100644 index 0000000..883c880 Binary files /dev/null and b/media/screenshot-black.jpg differ diff --git a/media/screenshot-dark.jpg b/media/screenshot-dark.jpg new file mode 100644 index 0000000..cd50c60 Binary files /dev/null and b/media/screenshot-dark.jpg differ diff --git a/media/screenshot-light.jpg b/media/screenshot-light.jpg new file mode 100644 index 0000000..e01ff56 Binary files /dev/null and b/media/screenshot-light.jpg differ diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 246f141..000a92e 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -47,14 +47,14 @@ kotlin { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.COROUTINE}") implementation("org.jetbrains.kotlinx:kotlinx-datetime:${Versions.KOTLINX_DATE_TIME}") - implementation("org.kodein.di:kodein-di:${Versions.KODE_IN}") + api("org.kodein.di:kodein-di:${Versions.KODE_IN}") implementation("io.ktor:ktor-client-core:${Versions.KTOR}") implementation("io.ktor:ktor-client-content-negotiation:${Versions.KTOR}") implementation("io.ktor:ktor-serialization-kotlinx-json:${Versions.KTOR}") implementation("io.ktor:ktor-client-logging:${Versions.KTOR}") - implementation("media.kamel:kamel-image:0.6.0") + implementation("media.kamel:kamel-image:0.8.3") implementation("com.russhwolf:multiplatform-settings:${Versions.SETTINGS}") implementation("com.russhwolf:multiplatform-settings-no-arg:${Versions.SETTINGS}") @@ -70,6 +70,11 @@ kotlin { implementation("androidx.appcompat:appcompat:1.6.1") implementation("app.cash.sqldelight:android-driver:${Versions.SQLDELIGHT}") implementation("androidx.startup:startup-runtime:1.1.1") + implementation("com.google.accompanist:accompanist-permissions:0.32.0") + + implementation(platform("com.google.firebase:firebase-bom:${Versions.FIREBASE_BOM}")) + implementation("com.google.firebase:firebase-analytics-ktx") + implementation("com.google.firebase:firebase-messaging-ktx") } } @@ -133,10 +138,14 @@ android { } buildTypes { - getByName("release") { - isMinifyEnabled = false + release { + isMinifyEnabled = true proguardFiles("proguard-rules.pro") } + + debug { + isMinifyEnabled = false + } } } diff --git a/shared/src/androidMain/AndroidManifest.xml b/shared/src/androidMain/AndroidManifest.xml index 325edd1..42c8260 100644 --- a/shared/src/androidMain/AndroidManifest.xml +++ b/shared/src/androidMain/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> + = derivedStateOf { + when (state.status) { + is com.google.accompanist.permissions.PermissionStatus.Denied -> if (state.status.shouldShowRationale) { + PermissionStatus.Denied + } else { + PermissionStatus.NotAsked + } + + com.google.accompanist.permissions.PermissionStatus.Granted -> PermissionStatus.Granted + } + } + + override fun askForPermission() = state.launchPermissionRequest() + + override fun showRationale() = context.openAppSettingScreen() + + companion object { + @Composable + fun rememberPermissionManager(permission: String): PermissionManager { + val state = rememberPermissionState(permission = permission) + val context = LocalContext.current + return remember { + AndroidPermissionManager(context, state) + } + } + } + } + +private fun Context.openAppSettingScreen() { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + intent.data = Uri.parse("package:$packageName") + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) +} diff --git a/shared/src/androidMain/kotlin/me/sujanpoudel/playdeals/common/App.kt b/shared/src/androidMain/kotlin/me/sujanpoudel/playdeals/common/App.kt index 0c8e35e..7ffc3d3 100644 --- a/shared/src/androidMain/kotlin/me/sujanpoudel/playdeals/common/App.kt +++ b/shared/src/androidMain/kotlin/me/sujanpoudel/playdeals/common/App.kt @@ -1,8 +1,31 @@ package me.sujanpoudel.playdeals.common +import android.Manifest +import android.os.Build import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import me.sujanpoudel.playdeals.common.pushNotification.AndroidNotificationManager +import me.sujanpoudel.playdeals.common.pushNotification.LocalNotificationManager +import me.sujanpoudel.playdeals.common.pushNotification.LocalPushNotificationPermissionManager @Composable fun PlayDealsAppAndroid() { - PlayDealsApp() + val permissionManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + AndroidPermissionManager.rememberPermissionManager(Manifest.permission.POST_NOTIFICATIONS) + } else { + PermissionManager.ALWAYS_GRANTED_PERMISSION + } + + val context = LocalContext.current + + val notificationManager = remember { AndroidNotificationManager(context) } + + CompositionLocalProvider( + LocalPushNotificationPermissionManager provides permissionManager, + LocalNotificationManager provides notificationManager, + ) { + PlayDealsApp() + } } diff --git a/shared/src/androidMain/kotlin/me/sujanpoudel/playdeals/common/pushNotification/AndroidNotificationManager.kt b/shared/src/androidMain/kotlin/me/sujanpoudel/playdeals/common/pushNotification/AndroidNotificationManager.kt new file mode 100644 index 0000000..426dbab --- /dev/null +++ b/shared/src/androidMain/kotlin/me/sujanpoudel/playdeals/common/pushNotification/AndroidNotificationManager.kt @@ -0,0 +1,32 @@ +package me.sujanpoudel.playdeals.common.pushNotification + +import android.app.NotificationChannel +import android.content.Context +import com.google.firebase.messaging.FirebaseMessaging +import com.google.firebase.messaging.FirebaseMessagingService + +class AndroidNotificationManager(context: Context) : NotificationManager { + + init { + context.registerNotificationChannels() + } + + override fun subscribeToTopic(topic: String) { + FirebaseMessaging.getInstance().subscribeToTopic(topic) + } + + override fun unSubscribeFromTopic(topic: String) { + FirebaseMessaging.getInstance().unsubscribeFromTopic(topic) + } +} + +fun Context.registerNotificationChannels() { + val notificationManager = + getSystemService(FirebaseMessagingService.NOTIFICATION_SERVICE) as android.app.NotificationManager + + PushNotificationTopic.entries + .forEach { + val channel = NotificationChannel(it.identifier, it.label, android.app.NotificationManager.IMPORTANCE_DEFAULT) + notificationManager.createNotificationChannel(channel) + } +} diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/PermissionManager.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/PermissionManager.kt new file mode 100644 index 0000000..923ed19 --- /dev/null +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/PermissionManager.kt @@ -0,0 +1,36 @@ +package me.sujanpoudel.playdeals.common + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf + +interface PermissionManager { + + val permissionState: State + fun askForPermission() + + fun showRationale() + + companion object { + val NONE = object : PermissionManager { + override val permissionState: State = mutableStateOf(PermissionStatus.NotAsked) + + override fun askForPermission() {} + + override fun showRationale() {} + } + + val ALWAYS_GRANTED_PERMISSION = object : PermissionManager { + override val permissionState: State = mutableStateOf(PermissionStatus.Granted) + + override fun askForPermission() {} + + override fun showRationale() {} + } + } +} + +enum class PermissionStatus { + Granted, + Denied, + NotAsked, +} diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/PlayDealApp.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/PlayDealApp.kt index c16447d..a076f94 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/PlayDealApp.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/PlayDealApp.kt @@ -11,19 +11,26 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import kotlinx.coroutines.delay import me.sujanpoudel.playdeals.common.di.PrimaryDI import me.sujanpoudel.playdeals.common.domain.persistent.AppPreferences import me.sujanpoudel.playdeals.common.navigation.NavGraph import me.sujanpoudel.playdeals.common.navigation.NavHost +import me.sujanpoudel.playdeals.common.pushNotification.NotificationManager +import me.sujanpoudel.playdeals.common.pushNotification.current +import me.sujanpoudel.playdeals.common.pushNotification.pushNotificationPermissionManager +import me.sujanpoudel.playdeals.common.pushNotification.syncNotificationTopics import me.sujanpoudel.playdeals.common.strings.LocalAppLanguage import me.sujanpoudel.playdeals.common.ui.screens.ChangeLogScreen import me.sujanpoudel.playdeals.common.ui.screens.home.HomeScreen import me.sujanpoudel.playdeals.common.ui.screens.newDeal.NewDealScreen import me.sujanpoudel.playdeals.common.ui.screens.settings.SettingsScreen +import me.sujanpoudel.playdeals.common.ui.screens.settings.notificationSettings.NotificationSettingsScreen import me.sujanpoudel.playdeals.common.ui.theme.AppTheme import org.kodein.di.direct import org.kodein.di.instance @@ -32,6 +39,7 @@ enum class Screens { Home, NEW_DEAL, SETTINGS, + NOTIFICATION_SETTING, CHANGELOG, } @@ -48,6 +56,10 @@ private val navGraph = NavGraph { SettingsScreen() } + destination(Screens.NOTIFICATION_SETTING) { + NotificationSettingsScreen() + } + destination(Screens.CHANGELOG) { ChangeLogScreen() } @@ -58,7 +70,26 @@ private val navGraph = NavGraph { @Composable fun PlayDealsApp() { val preferences = remember { PrimaryDI.direct.instance() } + val notificationPermissionManager = PermissionManager.pushNotificationPermissionManager + val notificationManager = NotificationManager.current + val appLanguage by preferences.appLanguage.collectAsState() + val notificationPermissionState by notificationPermissionManager.permissionState + + LaunchedEffect(notificationPermissionState) { + delay(1000) + when (notificationPermissionState) { + PermissionStatus.Granted, + PermissionStatus.Denied, + -> return@LaunchedEffect + + PermissionStatus.NotAsked -> notificationPermissionManager.askForPermission() + } + } + + LaunchedEffect(notificationManager) { + notificationManager.syncNotificationTopics(preferences) + } CompositionLocalProvider( LocalAppLanguage provides appLanguage, diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/di/viewModel.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/di/viewModel.kt index 8dcada3..2e9c4c4 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/di/viewModel.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/di/viewModel.kt @@ -3,6 +3,7 @@ package me.sujanpoudel.playdeals.common.di import me.sujanpoudel.playdeals.common.ui.screens.home.HomeScreenViewModel import me.sujanpoudel.playdeals.common.ui.screens.newDeal.NewDealScreenViewModel import me.sujanpoudel.playdeals.common.ui.screens.settings.SettingsScreenViewModel +import me.sujanpoudel.playdeals.common.ui.screens.settings.notificationSettings.NotificationSettingsScreenViewModel import org.kodein.di.DI import org.kodein.di.bindProvider import org.kodein.di.instance @@ -19,4 +20,6 @@ internal val viewModelModule = DI.Module("viewModel") { bindProvider { NewDealScreenViewModel() } bindProvider { SettingsScreenViewModel(instance()) } + + bindProvider { NotificationSettingsScreenViewModel(instance()) } } diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/persistent/AppPreferences.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/persistent/AppPreferences.kt index ac9ad77..7bb2f6d 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/persistent/AppPreferences.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/domain/persistent/AppPreferences.kt @@ -12,7 +12,11 @@ class AppPreferences(private val settings: ObservableSettings) { private object Keys { const val APPEARANCE_MODE = "APPEARANCE_MODE" const val DEVELOPER_MODE = "DEVELOPER_MODE_ENABLED" + const val NEW_DEAL_NOTIFICATION = "NEW_DEAL_NOTIFICATION" + const val NEW_DISCOUNT_DEAL_NOTIFICATION = "NEW_DISCOUNT_DEAL_NOTIFICATION" + const val SUMMARY_NOTIFICATION = "SUMMARY_NOTIFICATION" + const val PREFERRED_LANGUAGE = "PREFERRED_LANGUAGE" const val LAST_UPDATED_TIME = "LAST_UPDATED_TIME" const val CHANGELOG_SHOWN_FOR_VERSION = "CHANGELOG_SHOWN_FOR_VERSION" @@ -37,7 +41,9 @@ class AppPreferences(private val settings: ObservableSettings) { ) val developerMode = settings.boolSettingState(Keys.DEVELOPER_MODE, false) - val newDealNotification = settings.boolSettingState(Keys.NEW_DEAL_NOTIFICATION, true) + val subscribeToFreeDeals = settings.boolSettingState(Keys.NEW_DEAL_NOTIFICATION, true) + val subscribeToDiscountDeals = settings.boolSettingState(Keys.NEW_DISCOUNT_DEAL_NOTIFICATION, true) + val subscribeDealSummary = settings.boolSettingState(Keys.SUMMARY_NOTIFICATION, true) fun getChangelogShownVersion() = settings.getIntOrNull(Keys.CHANGELOG_SHOWN_FOR_VERSION) fun setChangelogShownVersion(version: Int) = settings.set(Keys.CHANGELOG_SHOWN_FOR_VERSION, version) diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/pushNotification/NotificationManager.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/pushNotification/NotificationManager.kt new file mode 100644 index 0000000..96ece10 --- /dev/null +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/pushNotification/NotificationManager.kt @@ -0,0 +1,62 @@ +package me.sujanpoudel.playdeals.common.pushNotification + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.compositionLocalOf +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import me.sujanpoudel.playdeals.common.domain.persistent.AppPreferences +import me.sujanpoudel.playdeals.common.utils.isDebugBuild +import kotlin.coroutines.coroutineContext + +interface NotificationManager { + fun subscribeToTopic(topic: String) + + fun unSubscribeFromTopic(topic: String) + + companion object { + val NONE = object : NotificationManager { + override fun subscribeToTopic(topic: String) {} + + override fun unSubscribeFromTopic(topic: String) {} + } + } +} + +val NotificationManager.Companion.current + @Composable + @ReadOnlyComposable + get() = LocalNotificationManager.current + +val LocalNotificationManager = compositionLocalOf { + NotificationManager.NONE +} + +suspend fun NotificationManager.syncNotificationTopics(appPreferences: AppPreferences) { + listOf( + appPreferences.developerMode to PushNotificationTopic.MAINTENANCE_LOG, + appPreferences.subscribeToFreeDeals to PushNotificationTopic.FREE_DEAL, + appPreferences.subscribeToDiscountDeals to PushNotificationTopic.DISCOUNT_DEAL, + appPreferences.subscribeDealSummary to PushNotificationTopic.DEALS_SUMMARY, + ).forEach { (subscriptionFlow, topic) -> + + CoroutineScope(coroutineContext).launch { + subscriptionFlow.collectLatest { subscribed -> + if (subscribed) { + subscribeToTopic(topic.identifier) + } else { + unSubscribeFromTopic(topic.identifier) + } + + if (isDebugBuild) { + if (subscribed) { + subscribeToTopic("${topic.identifier}-dev") + } else { + unSubscribeFromTopic("${topic.identifier}-dev") + } + } + } + } + } +} diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/pushNotification/PushNotificationPermissionManager.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/pushNotification/PushNotificationPermissionManager.kt new file mode 100644 index 0000000..3e48f53 --- /dev/null +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/pushNotification/PushNotificationPermissionManager.kt @@ -0,0 +1,15 @@ +package me.sujanpoudel.playdeals.common.pushNotification + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.compositionLocalOf +import me.sujanpoudel.playdeals.common.PermissionManager + +val LocalPushNotificationPermissionManager = compositionLocalOf { + PermissionManager.NONE +} + +val PermissionManager.Companion.pushNotificationPermissionManager + @Composable + @ReadOnlyComposable + get() = LocalPushNotificationPermissionManager.current diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/pushNotification/PushNotificationTopic.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/pushNotification/PushNotificationTopic.kt new file mode 100644 index 0000000..99ad85e --- /dev/null +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/pushNotification/PushNotificationTopic.kt @@ -0,0 +1,8 @@ +package me.sujanpoudel.playdeals.common.pushNotification + +enum class PushNotificationTopic(val identifier: String, val label: String) { + DISCOUNT_DEAL("discount-deal", "Discount Deal"), + FREE_DEAL("free-deal", "Free Deal"), + DEALS_SUMMARY("deals-summary", "Deal Summary"), + MAINTENANCE_LOG("dev-log", "Log"), +} diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/AppStrings.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/AppStrings.kt index 78ec1e9..3c3b63a 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/AppStrings.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/AppStrings.kt @@ -25,8 +25,8 @@ interface AppStrings { // settings screen val appearance: String val appearanceModeDescription: String - val dontMissDeal: String - val dontMissDealDescription: String + val pushNotification: String + val pushNotificationDescription: String val themePreference: String val chooseLanguage: String val close: String @@ -35,5 +35,21 @@ interface AppStrings { val changelog: String val oldChangeLog: String val findMeOn: String + + // new deal screen val addNewDeal: String + + // push notification settings screen + val subscribeToAllNewDeals: String + val subscribeToAllNewDealsDescription: String + val subscribeToFreeDeals: String + val subscribeToFreeDealsDescription: String + val subscribeToDiscountFreeDeal: String + val subscribeToDiscountDealDescription: String + val subscribeToSummary: String + val subscribeToSummaryDescription: String + val developerMode: String + val developerModeDescription: String + val permissionRequired: String + val grantPermission: String } diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/en.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/en.kt index d6c20f1..4a9c741 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/en.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/en.kt @@ -25,8 +25,8 @@ object StringEn : AppStrings { // Settings Screen override val appearance = "Appearance" override val appearanceModeDescription = "Choose your light ot dark theme preference" - override val dontMissDeal = "Don't miss any deals" - override val dontMissDealDescription = "Get notification for all new app deals" + override val pushNotification = "Push Notifications" + override val pushNotificationDescription = "Manage when you want to be notified" override val themePreference = "Theme Preference" override val chooseLanguage = "Choose Language" @@ -38,4 +38,22 @@ object StringEn : AppStrings { // new deal screen override val addNewDeal = "Add New Deal" + + // push notification settings + override val subscribeToAllNewDeals = "Don't miss any new deals" + override val subscribeToAllNewDealsDescription = "Receive notification for every new deal." + + override val subscribeToFreeDeals = "Know when something is free" + override val subscribeToFreeDealsDescription = "Receive notification when a paid app is available for free." + + override val subscribeToDiscountFreeDeal = "Discount notification" + override val subscribeToDiscountDealDescription = "Be notified when a paid app has some discount but is not free." + + override val subscribeToSummary = "Receive the summary notification" + override val subscribeToSummaryDescription = "Receive a summary notification every 24 hour or so." + + override val developerMode = "Enable developer mode" + override val developerModeDescription = "Be a nerd and receive notification for all the server log." + override val permissionRequired = "Permission is required to show \n the notification." + override val grantPermission = "Grant Permission" } diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/settings/SettingsScreen.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/settings/SettingsScreen.kt index 671b632..81c0119 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/settings/SettingsScreen.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/settings/SettingsScreen.kt @@ -2,6 +2,7 @@ package me.sujanpoudel.playdeals.common.ui.components.settings import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column @@ -48,6 +49,7 @@ object SettingsScreen { .fillMaxWidth() .clickable(onClick = onClick) .padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), ) { Column( modifier = Modifier.weight(1f), @@ -56,6 +58,7 @@ object SettingsScreen { text = title, style = MaterialTheme.typography.titleMedium, ) + Text( text = description, style = MaterialTheme.typography.bodySmall, diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/dialogs/NotificationPermissionDialog.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/dialogs/NotificationPermissionDialog.kt new file mode 100644 index 0000000..3391516 --- /dev/null +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/dialogs/NotificationPermissionDialog.kt @@ -0,0 +1,7 @@ +package me.sujanpoudel.playdeals.common.ui.dialogs + +import androidx.compose.runtime.Composable + +@Composable +fun NotificationPermissionDialog() { +} diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/SettingsScreen.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/SettingsScreen.kt index c8c002d..cc5d7d3 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/SettingsScreen.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/SettingsScreen.kt @@ -4,16 +4,21 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.DarkMode import androidx.compose.material.icons.filled.LightMode import androidx.compose.material.icons.outlined.DarkMode +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import me.sujanpoudel.playdeals.common.Screens +import me.sujanpoudel.playdeals.common.navigation.Navigator +import me.sujanpoudel.playdeals.common.pushNotification.NotificationManager +import me.sujanpoudel.playdeals.common.pushNotification.current import me.sujanpoudel.playdeals.common.strings.Strings import me.sujanpoudel.playdeals.common.ui.components.common.Scaffold import me.sujanpoudel.playdeals.common.ui.components.settings.SettingsScreen.AppearanceModeSetting @@ -38,35 +43,35 @@ fun SettingsScreen() { val viewModel = viewModel() val appearanceMode by viewModel.appearanceMode.collectAsState() - val newDealNotification by viewModel.newDealNotification.collectAsState() val developerModeEnabled by viewModel.developerModeEnabled.collectAsState() val appLanguage by viewModel.appLanguage.collectAsState() + val navigator = Navigator.current + val notificationManager = NotificationManager.current + Scaffold(title = Strings.settings) { Column( modifier = Modifier.fillMaxSize(), ) { - AppearanceModeSetting(appearanceMode, viewModel::setAppearanceMode) - - SettingItem( - title = Strings.dontMissDeal, - description = Strings.dontMissDealDescription, - onClick = { viewModel.setNewDealNotificationEnabled(newDealNotification.not()) }, - ) { - Switch( - checked = newDealNotification, - onCheckedChange = { - viewModel.setNewDealNotificationEnabled(newDealNotification.not()) + if (notificationManager != NotificationManager.NONE) { + SettingItem( + title = Strings.pushNotification, + description = Strings.pushNotificationDescription, + onClick = { navigator.push(Screens.NOTIFICATION_SETTING) }, + rightAction = { + Icon(Icons.Default.ChevronRight, contentDescription = "", tint = MaterialTheme.colorScheme.primary) }, ) } + AppearanceModeSetting(appearanceMode, viewModel::setAppearanceMode) + LanguageSetting(appLanguage, viewModel::setAppLanguage) Spacer(Modifier.weight(1f)) Footer( - onClick = { viewModel.setDeveloperModeEnabled(developerModeEnabled.not()) }, + onClick = { viewModel.setDeveloperModeEnabled(true) }, color = if (developerModeEnabled) { MaterialTheme.colorScheme.primary } else { diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/SettingsScreenViewModel.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/SettingsScreenViewModel.kt index 704ae5e..a81ecb0 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/SettingsScreenViewModel.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/SettingsScreenViewModel.kt @@ -11,7 +11,6 @@ class SettingsScreenViewModel( ) : ViewModel() { val appearanceMode: StateFlow = appPreferences.appearanceMode - val newDealNotification: StateFlow = appPreferences.newDealNotification val developerModeEnabled: StateFlow = appPreferences.developerMode val appLanguage: StateFlow = appPreferences.appLanguage @@ -23,10 +22,6 @@ class SettingsScreenViewModel( appPreferences.developerMode.update(enabled) } - fun setNewDealNotificationEnabled(enabled: Boolean) { - appPreferences.newDealNotification.update(enabled) - } - fun setAppLanguage(appLanguage: AppLanguage) { appPreferences.appLanguage.update(appLanguage) } diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/notificationSettings/NotificationSettingsScreen.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/notificationSettings/NotificationSettingsScreen.kt new file mode 100644 index 0000000..1562123 --- /dev/null +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/notificationSettings/NotificationSettingsScreen.kt @@ -0,0 +1,167 @@ +package me.sujanpoudel.playdeals.common.ui.screens.settings.notificationSettings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import me.sujanpoudel.playdeals.common.PermissionManager +import me.sujanpoudel.playdeals.common.PermissionStatus +import me.sujanpoudel.playdeals.common.pushNotification.pushNotificationPermissionManager +import me.sujanpoudel.playdeals.common.strings.Strings +import me.sujanpoudel.playdeals.common.ui.components.common.Scaffold +import me.sujanpoudel.playdeals.common.ui.components.settings.SettingsScreen.SettingItem +import me.sujanpoudel.playdeals.common.viewModel.viewModel + +@Composable +fun NotificationSettingsScreen() { + val viewModel = viewModel() + val notificationPermissionManager = PermissionManager.pushNotificationPermissionManager + + val isFreeDealNotificationEnabled by viewModel.freeDealNotification.collectAsState() + val isDiscountNotificationEnabled by viewModel.nonFreeDealNotification.collectAsState() + val isSummaryNotificationEnabled by viewModel.summaryNotification.collectAsState() + val isDeveloperModeEnabled by viewModel.developerMode.collectAsState() + + val isAllDealNotificationEnabled by derivedStateOf { isFreeDealNotificationEnabled && isDiscountNotificationEnabled } + val isNotificationEnabled by derivedStateOf { + isFreeDealNotificationEnabled || isDiscountNotificationEnabled || + isSummaryNotificationEnabled || isDeveloperModeEnabled + } + + val permissionStatus by notificationPermissionManager.permissionState + val permissionGranted by derivedStateOf { permissionStatus == PermissionStatus.Granted } + + Scaffold(title = Strings.pushNotification) { + Column( + modifier = Modifier.fillMaxSize(), + ) { + SettingItem( + title = Strings.subscribeToAllNewDeals, + description = Strings.subscribeToAllNewDealsDescription, + onClick = { viewModel.subscribeToAllDeals(isAllDealNotificationEnabled.not()) }, + rightAction = { + Switch( + checked = isAllDealNotificationEnabled, + onCheckedChange = { + viewModel.subscribeToAllDeals(isAllDealNotificationEnabled.not()) + }, + ) + }, + ) + + Divider() + + SettingItem( + title = Strings.subscribeToFreeDeals, + description = Strings.subscribeToFreeDealsDescription, + onClick = { viewModel.subscribeToFreeDeals(isFreeDealNotificationEnabled.not()) }, + rightAction = { + Switch( + checked = isFreeDealNotificationEnabled, + onCheckedChange = { + viewModel.subscribeToFreeDeals(isFreeDealNotificationEnabled.not()) + }, + ) + }, + ) + + SettingItem( + title = Strings.subscribeToDiscountFreeDeal, + description = Strings.subscribeToDiscountDealDescription, + onClick = { viewModel.subscribeDiscountDeals(isDiscountNotificationEnabled.not()) }, + rightAction = { + Switch( + checked = isDiscountNotificationEnabled, + onCheckedChange = { + viewModel.subscribeDiscountDeals(isDiscountNotificationEnabled.not()) + }, + ) + }, + ) + + Divider() + + SettingItem( + title = Strings.subscribeToSummary, + description = Strings.subscribeToSummaryDescription, + onClick = { viewModel.subscribeToSummary(isSummaryNotificationEnabled.not()) }, + rightAction = { + Switch( + checked = isSummaryNotificationEnabled, + onCheckedChange = { + viewModel.subscribeToSummary(isSummaryNotificationEnabled.not()) + }, + ) + }, + ) + + if (viewModel.wasDeveloperModeEnabled) { + SettingItem( + title = Strings.developerMode, + description = Strings.developerModeDescription, + onClick = { viewModel.setDeveloperMode(isDeveloperModeEnabled.not()) }, + rightAction = { + Switch( + checked = isDeveloperModeEnabled, + onCheckedChange = { + viewModel.setDeveloperMode(isDeveloperModeEnabled.not()) + }, + ) + }, + ) + } + + Spacer( + modifier = Modifier.weight(1f), + ) + + if (!permissionGranted && isNotificationEnabled) { + Card( + modifier = Modifier.fillMaxWidth() + .padding(16.dp), + ) { + Text( + Strings.grantPermission, + modifier = Modifier.fillMaxWidth() + .padding(top = 16.dp) + .align(Alignment.CenterHorizontally), + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium), + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + + TextButton( + modifier = Modifier + .padding(top = 8.dp) + .align(Alignment.CenterHorizontally), + onClick = { + if (permissionStatus == PermissionStatus.Denied) { + notificationPermissionManager.showRationale() + } else if (permissionStatus == PermissionStatus.NotAsked) { + notificationPermissionManager.askForPermission() + } + }, + ) { + Text(Strings.grantPermission) + } + } + } + } + } +} diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/notificationSettings/NotificationSettingsScreenViewModel.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/notificationSettings/NotificationSettingsScreenViewModel.kt new file mode 100644 index 0000000..590e47c --- /dev/null +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/notificationSettings/NotificationSettingsScreenViewModel.kt @@ -0,0 +1,26 @@ +package me.sujanpoudel.playdeals.common.ui.screens.settings.notificationSettings + +import kotlinx.coroutines.flow.StateFlow +import me.sujanpoudel.playdeals.common.domain.persistent.AppPreferences +import me.sujanpoudel.playdeals.common.viewModel.ViewModel + +class NotificationSettingsScreenViewModel( + private val appPreferences: AppPreferences, +) : ViewModel() { + + val freeDealNotification: StateFlow = appPreferences.subscribeToFreeDeals + val nonFreeDealNotification: StateFlow = appPreferences.subscribeToDiscountDeals + val summaryNotification: StateFlow = appPreferences.subscribeDealSummary + val wasDeveloperModeEnabled = appPreferences.developerMode.value + val developerMode: StateFlow = appPreferences.developerMode + + fun subscribeToAllDeals(enabled: Boolean) { + subscribeToFreeDeals(enabled) + subscribeDiscountDeals(enabled) + } + + fun subscribeToFreeDeals(enabled: Boolean) = appPreferences.subscribeToFreeDeals.update(enabled) + fun subscribeDiscountDeals(enabled: Boolean) = appPreferences.subscribeToDiscountDeals.update(enabled) + fun subscribeToSummary(enabled: Boolean) = appPreferences.subscribeDealSummary.update(enabled) + fun setDeveloperMode(enabled: Boolean) = appPreferences.developerMode.update(enabled) +} diff --git a/shared/src/commonMain/sqldelight/migrations/0.sqm b/shared/src/commonMain/sqldelight/migrations/0.sqm index 86170f3..44f2a44 100644 --- a/shared/src/commonMain/sqldelight/migrations/0.sqm +++ b/shared/src/commonMain/sqldelight/migrations/0.sqm @@ -2,7 +2,7 @@ import kotlin.String; import kotlin.collections.List; import kotlinx.datetime.Instant; -CREATE TABLE Deal +CREATE TABLE IF NOT EXISTS Deal ( id TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL, diff --git a/shared/src/desktopMain/kotlin/me/sujanpoudel/playdeals/common/domain/persistent/db/sqlDriver.desktop.kt b/shared/src/desktopMain/kotlin/me/sujanpoudel/playdeals/common/domain/persistent/db/sqlDriver.desktop.kt index 0406a5e..46963d3 100644 --- a/shared/src/desktopMain/kotlin/me/sujanpoudel/playdeals/common/domain/persistent/db/sqlDriver.desktop.kt +++ b/shared/src/desktopMain/kotlin/me/sujanpoudel/playdeals/common/domain/persistent/db/sqlDriver.desktop.kt @@ -6,6 +6,8 @@ import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver import me.sujanpoudel.playdeals.common.Constants import me.sujanpoudel.playdeals.common.SqliteDatabase -actual fun createSqlDriver(): SqlDriver = JdbcSqliteDriver("jdbc:sqlite:${Constants.DATABASE_NAME}").apply { - SqliteDatabase.Schema.synchronous().create(this) -} +actual fun createSqlDriver(): SqlDriver = JdbcSqliteDriver( + url = "jdbc:sqlite:${Constants.DATABASE_NAME}", + schema = SqliteDatabase.Schema.synchronous(), + migrateEmptySchema = true, +)