Skip to content
This repository has been archived by the owner on Sep 14, 2024. It is now read-only.

feat: Firebase setup - Added Android native method for requesting permissions and fetching device tokens. #3

Merged
merged 14 commits into from
Feb 23, 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
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,17 @@ jobs:
yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}"

build-ios:
runs-on: macos-latest
runs-on: macos-14
env:
TURBO_CACHE_DIR: .turbo/ios
steps:
- name: Checkout
uses: actions/checkout@v3

- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable

- name: Setup
uses: ./.github/actions/setup

Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,23 @@ Andriod support is coming soon. Checkout [#1](https://github.com/candlefinance/p
2. If your AppDelegate is in Objective-C (`.mm|.m|.h`), create a new `AppDelegate.swift` file and bridging header then delete the Objective-C AppDelegate and main.m file. Finally copy the contents of the example app's [AppDelegate.swift](./example/ios/AppDelegate.swift) and [bridge header](./example/ios/PushExample-Bridging-Header.h) to your project.
3. Make sure you're on `iOS 15` or later.

### Android

- [x] Request permissions
- [x] Register for FCM token
- [ ] Remote push notifications
- [ ] Foreground
- [ ] Background
- [ ] Opened by tapping on the notification
- [ ] Local push notifications

#### Setup

1. Add permissions in [AndroidManifest.xml](./example/android/app/src/main/AndroidManifest.xml)
2. Add `google-services.json` in `android/app` directory from Firebase console.

## API

<br>
The following code is used to handle push notifications on the React Native side:

Expand Down
2 changes: 2 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,7 @@ dependencies {
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation(platform("com.google.firebase:firebase-bom:32.7.2"))
implementation "com.google.firebase:firebase-messaging"
}

4 changes: 2 additions & 2 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.candlefinance.push">
</manifest>
package="com.candlefinance.push">
</manifest>
21 changes: 21 additions & 0 deletions android/src/main/AndroidManifestNew.xml
Original file line number Diff line number Diff line change
@@ -1,2 +1,23 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<service
android:exported="false"
android:name=".FirebaseMessagingService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_default_notification" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value='@string/default_notification_channel_id' />
<meta-data
android:name="firebase_messaging_auto_init_enabled"
android:value="false" />
<meta-data
android:name="firebase_analytics_collection_enabled"
android:value="false" />
</application>
</manifest>
31 changes: 31 additions & 0 deletions android/src/main/java/com/candlefinance/push/ContextHolder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.candlefinance.push
import com.facebook.react.bridge.ReactContext

class ContextHolder private constructor() {
private lateinit var applicationContext: ReactContext

companion object {

@Volatile private var instance: ContextHolder? = null

fun getInstance() =
instance ?: synchronized(this) {
instance ?: ContextHolder().also { instance = it }
}
}

fun setApplicationContext(context: ReactContext) {
if (!::applicationContext.isInitialized) {
applicationContext = context
}
}

fun getApplicationContext(): ReactContext {
if (!::applicationContext.isInitialized) {
throw IllegalStateException("Application context not initialized. Call setApplicationContext() first.")
}
return applicationContext
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.candlefinance.push

import android.util.Log
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import org.json.JSONObject
import java.lang.Exception
import java.util.UUID

class FirebaseMessagingService : FirebaseMessagingService() {

override fun onSendError(msgId: String, exception: Exception) {
super.onSendError(msgId, exception)
RNEventEmitter.sendEvent(errorReceived, exception.message)
}

override fun onMessageReceived(remoteMessage: RemoteMessage) {
val title = remoteMessage.notification?.title.toString()
val body = remoteMessage.notification?.body.toString()

val formattedData = JSONObject()

val apsData = JSONObject()
apsData.put("alert", JSONObject().apply {
put("title", title)
put("body", body)
})
formattedData.put("payload", JSONObject().apply {
put("aps", apsData)
put("custom", JSONObject(mapToString(remoteMessage.data)))
})

formattedData.put("kind", getAppState())
formattedData.put("uuid", UUID.randomUUID().toString())

// Send the event
RNEventEmitter.sendEvent(notificationReceived, formattedData.toString())
NotificationUtils
.sendNotification(
ContextHolder.getInstance().getApplicationContext(), title, body
)
if (remoteMessage.data.isNotEmpty()) {
Log.d(TAG, "Message data payload: ${remoteMessage.data}")
}
remoteMessage.notification?.let {
Log.d(TAG, "Message Notification Body: ${it.body}")
}
}

private fun mapToString(map: Map<String, String>): String {
val json = JSONObject()
for ((key, value) in map) {
json.put(key, value)
}
return json.toString()
}

// TODO: make this actually work
private fun getAppState(): String {
val reactContext = ContextHolder.getInstance().getApplicationContext()
val currentActivity = reactContext.currentActivity
return if (currentActivity == null) {
"background"
} else {
"foreground"
}
}

override fun onNewToken(token: String) {
Log.d(TAG,token)
RNEventEmitter.sendEvent(deviceTokenReceived, token)
}

companion object {
private const val TAG = "MyFirebaseMsgService"
const val notificationReceived = "notificationReceived"
const val deviceTokenReceived = "deviceTokenReceived"
const val errorReceived = "errorReceived"
}
}
69 changes: 69 additions & 0 deletions android/src/main/java/com/candlefinance/push/NotificationUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.candlefinance.push

import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import androidx.core.app.NotificationCompat
import com.google.android.gms.common.internal.ResourceUtils
import java.util.Locale


object NotificationUtils {

private const val NOTIFICATION_ID = 123321

fun createNotificationChannel(
context: Context,
channelId: String,
channelName: String,
channelDescription: String
) {
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
channelName,
NotificationManager.IMPORTANCE_HIGH
)
channel.description = channelDescription
notificationManager.createNotificationChannel(channel)
}
}

fun createDefaultChannelForFCM(context:Context){
val defaultChannelId= context.resources.getString(R.string.default_notification_channel_id)
val defaultChannelName = context.resources.getString(R.string.default_notification_channel_name)
this
.createNotificationChannel(context, defaultChannelId, defaultChannelName,"Default channel created by RN Push module to All FCM notification")
}
fun sendNotification(context: Context, title: String, message: String) {
vijaygojiya marked this conversation as resolved.
Show resolved Hide resolved
val defaultChannelId= context.resources.getString(R.string.default_notification_channel_id)
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val builder = NotificationCompat.Builder(context, defaultChannelId)
.setSmallIcon(getResourceIdByName("ic_default_notification","drawable"))
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)

notificationManager.notify(NOTIFICATION_ID, builder.build())
}

@SuppressLint("DiscouragedApi")
fun getResourceIdByName(name: String?, type: String): Int {
var name = name
if (name.isNullOrEmpty()) {
return 0
}
name = name.lowercase(Locale.getDefault()).replace("-", "_")
synchronized(ResourceUtils::class.java) {

val context = ContextHolder.getInstance().getApplicationContext()
val packageName = context.packageName
return context.resources.getIdentifier(name, type, packageName)
}
}
}
113 changes: 108 additions & 5 deletions android/src/main/java/com/candlefinance/push/PushModule.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
package com.candlefinance.push

import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.core.content.ContextCompat
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise
import com.facebook.react.modules.core.PermissionAwareActivity
import com.facebook.react.modules.core.PermissionListener
import com.google.android.gms.tasks.OnCompleteListener
import com.google.firebase.messaging.FirebaseMessaging

class PushModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
Expand All @@ -12,11 +21,105 @@ class PushModule(reactContext: ReactApplicationContext) :
return NAME
}

// Example method
// See https://reactnative.dev/docs/native-modules-android
override fun initialize() {
super.initialize()
NotificationUtils.createDefaultChannelForFCM(reactApplicationContext)
ContextHolder.getInstance().setApplicationContext(reactApplicationContext)
}


@ReactMethod
fun getAuthorizationStatus(promise: Promise) {
val context = reactApplicationContext.baseContext

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) ==
PackageManager.PERMISSION_GRANTED
) {
promise.resolve(2) // authorized
} else {
promise.resolve(1) // denied
}
} else {
promise.resolve(2) // authorized
}
}

@ReactMethod
fun requestPermissions(promise: Promise) {
val context = reactApplicationContext.baseContext

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {

if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) ==
PackageManager.PERMISSION_GRANTED
) {
registerForToken(promise)
} else {
val activity = reactApplicationContext.currentActivity

if (activity is PermissionAwareActivity) {
val currentRequestCode = 83834

val listener = PermissionListener { requestCode: Int, _: Array<String>, grantResults: IntArray ->
if (requestCode == currentRequestCode) {
val isPermissionGranted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
if (isPermissionGranted) {
registerForToken(promise)
return@PermissionListener true
}
return@PermissionListener false
}
return@PermissionListener false
}

// Replace this with the appropriate permission for push notifications
activity.requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), currentRequestCode, listener)
} else {
promise.reject("NO_ACTIVITY", "No PermissionAwareActivity was found! Make sure the app has launched before calling this function.")
}
}

} else {
promise.resolve(true)
}
}

@ReactMethod
fun registerForToken(promise: Promise) {
FirebaseMessaging.getInstance().isAutoInitEnabled=true;
FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
if (!task.isSuccessful) {
RNEventEmitter.sendEvent(FirebaseMessagingService.errorReceived, "Fetching FCM registration token failed ${task.exception?.message}")
return@OnCompleteListener
}
val token = task.result
RNEventEmitter.sendEvent(FirebaseMessagingService.deviceTokenReceived, token)
})
promise.resolve(true)
}

@ReactMethod
fun isRegisteredForRemoteNotifications(promise: Promise) {
FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
if (!task.isSuccessful) {
RNEventEmitter.sendEvent(FirebaseMessagingService.errorReceived, "Fetching FCM registration token failed ${task.exception?.message}")
promise.reject("NO_TOKEN", "No token found ${task.exception?.message}")
return@OnCompleteListener
}
val token = task.result
promise.resolve(true)
})
}

@ReactMethod
fun addListener(type: String?) {
// Keep: Required for RN built in Event Emitter Calls.
}

@ReactMethod
fun multiply(a: Double, b: Double, promise: Promise) {
promise.resolve(a * b)
fun removeListeners(type: Int?) {
// Keep: Required for RN built in Event Emitter Calls.
}

companion object {
Expand Down
Loading
Loading