Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding web support (js and wasm targets) #51

Merged
merged 6 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,20 @@
![badge-android](http://img.shields.io/badge/platform-android-6EDB8D.svg?style=flat)
![badge-ios](http://img.shields.io/badge/platform-ios-CDCDCD.svg?style=flat)
![badge-desktop](https://img.shields.io/badge/platform-desktop-3474eb.svg?style=flat)
![badge-js](https://img.shields.io/badge/platform-js-fcba03.svg?style=flat)
![badge-wasm](https://img.shields.io/badge/platform-wasm-331f06.svg?style=flat)




Simple and easy to use Kotlin Multiplatform Push Notification library (using Firebase Cloud Messaging) targeting ios and android.
This library is used in [FindTravelNow](https://github.com/mirzemehdi/FindTravelNow-KMM/) production KMP project.
You can check out [Documentation](https://mirzemehdi.github.io/KMPNotifier) for full library api information.

## Features
- 🔔 Local and Push Notification (Firebase Cloud Messaging)
- 📱 Multiplatform (android, iOS and desktop(alpha)) (Desktop supports only local notification for now)
- 🔔 Local Notification (android, ios, desktop, js and wasm targets)
- 🔔 Push Notification (Firebase Cloud Messaging) (android and ios only)
- 📱 Multiplatform (android, iOS, desktop and web (js and wasm))

## Installation
Before starting you need to setup basic setup using Firebase official guideline (like initializing project in Firebase, adding `google-services.json` to android, `GoogleService-Info.plist` to iOS).
Expand Down Expand Up @@ -68,7 +73,8 @@ plugins {
### Platform Setup
In all platforms on Application Start you need to initialize library using
```kotlin
NotifierManager.initialize(NotificationPlatformConfiguration) //passing android or ios configuration depending on the platform
//passing android, ios, desktop or web configuration depending on the platform
NotifierManager.initialize(NotificationPlatformConfiguration)
```

<details>
Expand Down Expand Up @@ -184,6 +190,21 @@ fun main() = application {
}
```

### Web Setup (Js and Wasm)
On
```kotlin
fun main() {

NotifierManager.initialize(
NotificationPlatformConfiguration.Web(
askNotificationPermissionOnStart = true,
notificationIconPath = null
)
)

}
```




Expand All @@ -193,7 +214,7 @@ fun main() = application {
You can send either local or push notification.

### Local Notification

Local notifications are supported on Android, iOS, JS and wasm targets.
#### Send notification

```kotlin
Expand All @@ -215,7 +236,7 @@ notifier.removeAll() //Removes all notification
```

### Push Notification

Push notifications are supported only for Android and iOS.
#### Listen for push notification token changes
In this method you can send notification token to the server.

Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ plugins {

allprojects {
group = "io.github.mirzemehdi"
version = "1.1.0"
version = "1.2.0-alpha"
val sonatypeUsername = gradleLocalProperties(rootDir).getProperty("sonatypeUsername")
val sonatypePassword = gradleLocalProperties(rootDir).getProperty("sonatypePassword")
val gpgKeySecret = gradleLocalProperties(rootDir).getProperty("gpgKeySecret")
Expand Down
2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\=

#Compose
org.jetbrains.compose.experimental.uikit.enabled=true
org.jetbrains.compose.experimental.wasm.enabled=true


#Android
android.useAndroidX=true
Expand Down
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[versions]
compose = "1.6.7"
compose-plugin = "1.6.10"
agp = "8.1.4"
agp = "8.2.0"
android-minSdk = "21"
android-compileSdk = "34"
android-targetSdk = "34"
Expand All @@ -15,7 +15,7 @@ androidx-espresso-core = "3.5.1"
androidx-startup-runtime = "1.1.1"
kotlin = "2.0.0"
junit = "4.13.2"
koin = "3.5.6"
koin = "3.6.0-Beta5"
kotlinx-binary-validator = "0.13.2"
dokka = "1.9.10"
firebase-messaging = "24.0.0"
Expand Down
8 changes: 8 additions & 0 deletions kmpnotifier/api/android/kmpnotifier.api
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ public final class com/mmk/kmpnotifier/notification/configuration/NotificationPl
public fun toString ()Ljava/lang/String;
}

public final class com/mmk/kmpnotifier/notification/configuration/NotificationPlatformConfiguration$Web : com/mmk/kmpnotifier/notification/configuration/NotificationPlatformConfiguration {
public fun <init> ()V
public fun <init> (ZLjava/lang/String;)V
public synthetic fun <init> (ZLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getAskNotificationPermissionOnStart ()Z
public final fun getNotificationIconPath ()Ljava/lang/String;
}

public final class com/mmk/kmpnotifier/permission/AndroidPermissionUtil {
public fun <init> (Landroidx/activity/ComponentActivity;)V
public final fun askNotificationPermission (Lkotlin/jvm/functions/Function1;)V
Expand Down
8 changes: 8 additions & 0 deletions kmpnotifier/api/jvm/kmpnotifier.api
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ public final class com/mmk/kmpnotifier/notification/configuration/NotificationPl
public fun toString ()Ljava/lang/String;
}

public final class com/mmk/kmpnotifier/notification/configuration/NotificationPlatformConfiguration$Web : com/mmk/kmpnotifier/notification/configuration/NotificationPlatformConfiguration {
public fun <init> ()V
public fun <init> (ZLjava/lang/String;)V
public synthetic fun <init> (ZLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getAskNotificationPermissionOnStart ()Z
public final fun getNotificationIconPath ()Ljava/lang/String;
}

public abstract interface class com/mmk/kmpnotifier/permission/PermissionUtil {
public abstract fun askNotificationPermission (Lkotlin/jvm/functions/Function0;)V
public abstract fun hasNotificationPermission (Lkotlin/jvm/functions/Function1;)V
Expand Down
13 changes: 13 additions & 0 deletions kmpnotifier/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig

plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
Expand All @@ -16,6 +19,16 @@ kotlin {
}
}

@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
}
js(IR) {
nodejs()
browser()
binaries.library()
}

jvm()
iosX64()
iosArm64()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ private fun Koin.onLibraryInitialized() {
if (askNotificationPermissionOnStart) permissionUtil.askNotificationPermission()
}

Platform.Web -> {
val askNotificationPermissionOnStart =
(configuration as? NotificationPlatformConfiguration.Web)?.askNotificationPermissionOnStart
?: true
if (askNotificationPermissionOnStart) permissionUtil.askNotificationPermission()
}

}
}

Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ internal sealed interface Platform {
data object Android : Platform
data object Ios : Platform
data object Desktop : Platform
data object Web : Platform
}
internal expect val platformModule: Module
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.mmk.kmpnotifier.notification


internal class EmptyPushNotifierImpl : PushNotifier {
override suspend fun getToken(): String? {
println("Not implemented: Get firebase token returns null")
return null
}

override suspend fun deleteMyToken() {
println("Not implemented: Delete firebase token is called")
}

override suspend fun subscribeToTopic(topic: String) {
println("Not implemented: Subscribe firebase topic is called")
}

override suspend fun unSubscribeFromTopic(topic: String) {
println("Not implemented: Unsubscribe firebase topic is called")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,34 @@ public sealed interface NotificationPlatformConfiguration {
) : NotificationPlatformConfiguration


/**
* Desktop notification customization.
*
* @param showPushNotification Default value is true,
* by default when push notification is received it will be shown to user.
* When set to false, it will not be shown to user, but you can still get notification content using
* @see com.mmk.kmpnotifier.notification.NotifierManager.Listener.onPushNotification
*
* @param notificationIconPath Notification icon path

*/
public data class Desktop(
public val showPushNotification: Boolean = true,
public val notificationIconPath: String? = null
) : NotificationPlatformConfiguration

/**
* Web notification customization.
*
* @param askNotificationPermissionOnStart Default value is true, when library is initialized it
* will ask notification permission automatically from the user.
* By setting askNotificationPermissionOnStart false, you can customize to ask permission whenever you want.
*
*@param notificationIconPath Notification icon path
*
*/
public class Web(
public val askNotificationPermissionOnStart: Boolean = true,
public val notificationIconPath: String? = null
) : NotificationPlatformConfiguration
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package com.mmk.kmpnotifier.permission

internal class DesktopPermissionUtil:PermissionUtil {
internal class EmptyPermissionUtilImpl : PermissionUtil {
override fun hasNotificationPermission(onPermissionResult: (Boolean) -> Unit) {
println("Desktop has permission result")
println("Not implemented: has permission result: true")
onPermissionResult(true)
}

override fun askNotificationPermission(onPermissionGranted: () -> Unit) {
println("Desktop ask permission")
println("Not implemented: granted permission by default")
onPermissionGranted()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.mmk.kmpnotifier.di

import com.mmk.kmpnotifier.notification.EmptyPushNotifierImpl
import com.mmk.kmpnotifier.notification.Notifier
import com.mmk.kmpnotifier.notification.PushNotifier
import com.mmk.kmpnotifier.notification.WebConsoleNotifier
import com.mmk.kmpnotifier.notification.configuration.NotificationPlatformConfiguration
import com.mmk.kmpnotifier.permission.EmptyPermissionUtilImpl
import com.mmk.kmpnotifier.permission.PermissionUtil
import com.mmk.kmpnotifier.permission.WebPermissionUtilImpl
import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.bind
import org.koin.dsl.module


internal actual val platformModule = module {
factory { Platform.Web } bind Platform::class
factoryOf(::WebPermissionUtilImpl) bind PermissionUtil::class
factory {
val configuration =
get<NotificationPlatformConfiguration>() as NotificationPlatformConfiguration.Web
WebConsoleNotifier(configuration = configuration, permissionUtil = get())
} bind Notifier::class
factoryOf(::EmptyPushNotifierImpl) bind PushNotifier::class
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.mmk.kmpnotifier.notification

import com.mmk.kmpnotifier.notification.configuration.NotificationPlatformConfiguration
import com.mmk.kmpnotifier.permission.PermissionUtil
import kotlinx.browser.window
import org.w3c.notifications.Notification
import org.w3c.notifications.NotificationOptions
import kotlin.random.Random


internal class WebConsoleNotifier(
private val permissionUtil: PermissionUtil,
private val configuration: NotificationPlatformConfiguration.Web
) : Notifier {

override fun notify(title: String, body: String, payloadData: Map<String, String>): Int {
val notificationID = Random.nextInt(0, Int.MAX_VALUE)
notify(notificationID, title, body, payloadData)
return notificationID
}


override fun notify(id: Int, title: String, body: String, payloadData: Map<String, String>) {
if (isNotificationSupported().not()) {
alertNotification(body)
return
}
permissionUtil.askNotificationPermission {
permissionUtil.hasNotificationPermission { hasPermission ->
if (hasPermission) showNotification(title = title, body = body)
else alertNotification(body)
}
}
}

override fun remove(id: Int) {
println("remove notification is not implemented ")

}

override fun removeAll() {
println("remove notification is not implemented ")
}

private fun showNotification(title: String, body: String) {
val options = NotificationOptions(body = body, icon = configuration.notificationIconPath)
Notification(title, options)
}

private fun alertNotification(message: String) {
window.alert(message)
}

private fun isNotificationSupported(): Boolean {
return js("typeof Notification !== 'undefined'").unsafeCast<Boolean>()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.mmk.kmpnotifier.permission

import org.w3c.notifications.GRANTED
import org.w3c.notifications.Notification
import org.w3c.notifications.NotificationPermission

internal class WebPermissionUtilImpl : PermissionUtil {
override fun hasNotificationPermission(onPermissionResult: (Boolean) -> Unit) {
val permission = Notification.permission
onPermissionResult(permission == NotificationPermission.GRANTED)
}

override fun askNotificationPermission(onPermissionGranted: () -> Unit) {
Notification.requestPermission().then {
onPermissionGranted()
null
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.mmk.kmpnotifier.di

import com.mmk.kmpnotifier.firebase.FirebaseDesktopPushNotifier
import com.mmk.kmpnotifier.notification.DesktopNotifierFactory
import com.mmk.kmpnotifier.notification.EmptyPushNotifierImpl
import com.mmk.kmpnotifier.notification.Notifier
import com.mmk.kmpnotifier.notification.PushNotifier
import com.mmk.kmpnotifier.notification.configuration.NotificationPlatformConfiguration
import com.mmk.kmpnotifier.permission.DesktopPermissionUtil
import com.mmk.kmpnotifier.permission.EmptyPermissionUtilImpl
import com.mmk.kmpnotifier.permission.PermissionUtil
import org.koin.core.module.Module
import org.koin.core.module.dsl.factoryOf
Expand All @@ -20,6 +20,6 @@ internal actual val platformModule: Module = module {
get<NotificationPlatformConfiguration>() as NotificationPlatformConfiguration.Desktop
DesktopNotifierFactory.getNotifier(configuration = configuration)
} bind Notifier::class
factoryOf(::DesktopPermissionUtil) bind PermissionUtil::class
factoryOf(::FirebaseDesktopPushNotifier) bind PushNotifier::class
factoryOf(::EmptyPermissionUtilImpl) bind PermissionUtil::class
factoryOf(::EmptyPushNotifierImpl) bind PushNotifier::class
}
Loading
Loading