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,
+)