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 7 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>
12 changes: 12 additions & 0 deletions android/src/main/AndroidManifestNew.xml
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<service
android:exported="false"
android:name=".MyFirebaseMessagingService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="RNFireDefaultChannelID" />
</application>
</manifest>
14 changes: 14 additions & 0 deletions android/src/main/java/com/candlefinance/push/ContextHolder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.candlefinance.push;
import com.facebook.react.bridge.ReactContext;

public class ContextHolder {
vijaygojiya marked this conversation as resolved.
Show resolved Hide resolved
private static ReactContext applicationContext;

public static ReactContext getApplicationContext() {
return applicationContext;
}

public static void setApplicationContext(ReactContext applicationContext) {
ContextHolder.applicationContext = (ReactContext) applicationContext;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.candlefinance.push

import android.util.Log
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage

class MyFirebaseMessagingService : FirebaseMessagingService() {

// fun onSendError(messageId: String?, sendError: Exception?) {
vijaygojiya marked this conversation as resolved.
Show resolved Hide resolved
// //TODO handler error
// Log.d(TAG,"on error")
// }
vijaygojiya marked this conversation as resolved.
Show resolved Hide resolved
override fun onMessageReceived(remoteMessage: RemoteMessage) {
val title = remoteMessage.notification?.title.toString();
val body = remoteMessage.notification?.body.toString();

// TODO remove after testing
RNEventEmitter.sendEvent("deviceTokenReceived",title)
vijaygojiya marked this conversation as resolved.
Show resolved Hide resolved

NotificationUtils.sendNotification(ContextHolder.getApplicationContext(),title,body)
if (remoteMessage.data.isNotEmpty()) {
Log.d(TAG, "Message data payload: ${remoteMessage.data}")
}
// Check if message contains a notification payload.
remoteMessage.notification?.let {
Log.d(TAG, "Message Notification Body: ${it.body}")
}
}

override fun onNewToken(token: String) {
Log.d(TAG,token)
RNEventEmitter.sendEvent("deviceTokenReceived",token)
}
companion object {
private const val TAG = "MyFirebaseMsgService"
}


}
59 changes: 59 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,59 @@
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 val CHANNEL_ID = "RNFireDefaultChannelID"
private val CHANNEL_NAME = "Firebase Default"
private val NOTIFICATION_ID = 123321
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#? do we need to inject these from the JS side? If so, we might need to add an init function. I have ideas but never done Android push.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Will do more R& D on it and improve.


fun createDefaultNotificationChannel (context:Context){
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
)
channel.description = "All by default notification channel for all firebase notification"
notificationManager.createNotificationChannel(channel)
}

}
fun sendNotification(context: Context, title: String, message: String) {
vijaygojiya marked this conversation as resolved.
Show resolved Hide resolved
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(getResourceIdByName("ic_launcher","mipmap"))
.setContentTitle(title)
.setContentText(message)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#? do we need to inject this, too? Is this like the app icon shown in the push?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Essentially, we now require an icon when sending notifications, and I've also introduced a feature to easily change this icon. Developers can customize the notification icon by adding a new asset to the 'drawable' folder with the exact name ic_default_notification.

.setPriority(NotificationCompat.PRIORITY_DEFAULT).setChannelId(CHANNEL_ID)

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("-", "_")
val key = name + "_" + type
synchronized(ResourceUtils::class.java) {

val context = ContextHolder.getApplicationContext()
val packageName = context.packageName
return context.resources.getIdentifier(name, type, packageName)
}
}
}
99 changes: 94 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,13 +21,93 @@ class PushModule(reactContext: ReactApplicationContext) :
return NAME
}

// Example method
// See https://reactnative.dev/docs/native-modules-android
override fun initialize() {
super.initialize()
NotificationUtils.createDefaultNotificationChannel(reactApplicationContext)
if (ContextHolder.getApplicationContext() == null) {
ContextHolder.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
) {
promise
.resolve(true)
} 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
promise.resolve(isPermissionGranted)
return@PermissionListener true
}
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().token.addOnCompleteListener(OnCompleteListener { task ->
vijaygojiya marked this conversation as resolved.
Show resolved Hide resolved
if (!task.isSuccessful) {
Log.w("getTokenError", "Fetching FCM registration token failed", task.exception)
promise.reject(task.exception)
return@OnCompleteListener
}

val token = task.result
promise.resolve(token)
})
}
@ReactMethod
fun multiply(a: Double, b: Double, promise: Promise) {
promise.resolve(a * b)
fun addListener(type: String?) {
// Keep: Required for RN built in Event Emitter Calls.
}

@ReactMethod
fun removeListeners(type: Int?) {
// Keep: Required for RN built in Event Emitter Calls.
}
companion object {
vijaygojiya marked this conversation as resolved.
Show resolved Hide resolved
const val NAME = "Push"
}
Expand Down
33 changes: 33 additions & 0 deletions android/src/main/java/com/candlefinance/push/RNEventEmitter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.candlefinance.push;

import android.util.Log;

import com.facebook.react.modules.core.DeviceEventManagerModule;

vijaygojiya marked this conversation as resolved.
Show resolved Hide resolved
public class RNEventEmitter {

private static final RNEventEmitter sharedInstance =
new RNEventEmitter();

public static RNEventEmitter getSharedInstance() {
return sharedInstance;
}


static void sendEvent(String eventName, String eventMap) {
var reactContext = ContextHolder.getApplicationContext();
try {

if (reactContext == null) {
return;
}
vijaygojiya marked this conversation as resolved.
Show resolved Hide resolved

reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, eventMap);

} catch (Exception e) {
Log.e("SEND_EVENT", "", e);
}
}
}
1 change: 1 addition & 0 deletions example/android/app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
apply plugin: "com.google.gms.google-services"

/**
* This is the configuration block to customize your React Native Android app.
Expand Down
29 changes: 29 additions & 0 deletions example/android/app/google-services.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "123",
"project_id": "123",
"storage_bucket": "exampe-fffc.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "Example",
"android_client_info": {
"package_name": "com.pushexample"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": ""
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}
1 change: 1 addition & 0 deletions example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

<application
android:name=".MainApplication"
Expand Down
Loading
Loading