@radhya/mach-push-react-native 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +37 -0
- package/android/build.gradle +25 -0
- package/android/src/main/AndroidManifest.xml +12 -0
- package/android/src/main/java/dev/getmach/push/MachPushEventBus.kt +46 -0
- package/android/src/main/java/dev/getmach/push/MachPushMessagingService.kt +90 -0
- package/android/src/main/java/dev/getmach/push/MachPushModule.kt +162 -0
- package/android/src/main/java/dev/getmach/push/MachPushPackage.kt +16 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +123 -0
- package/getmach-push-react-native.podspec +17 -0
- package/ios/MachPush.swift +195 -0
- package/ios/MachPushAppDelegateInterceptor.m +73 -0
- package/ios/MachPushBridge.m +20 -0
- package/ios/MachPushNotificationDelegate.swift +124 -0
- package/package.json +44 -0
- package/react-native.config.js +10 -0
- package/src/index.ts +215 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Radhya Softlabs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @radhya/mach-push-react-native
|
|
2
|
+
|
|
3
|
+
Provider-neutral MACH Push for bare React Native and Expo development builds.
|
|
4
|
+
|
|
5
|
+
The recommended setup requires no application wiring:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
mach push setup
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The command installs this package, adds automatic startup to the app entry
|
|
12
|
+
point, and creates `mach-push.ts` (or `.js`) with editable receive, tap,
|
|
13
|
+
registration, permission, and error callbacks. Re-running setup preserves that
|
|
14
|
+
file.
|
|
15
|
+
|
|
16
|
+
For a custom integration, use the low-level client:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { createMachPush } from "@radhya/mach-push-react-native";
|
|
20
|
+
|
|
21
|
+
const push = createMachPush({
|
|
22
|
+
projectId: "project_...",
|
|
23
|
+
appId: "push_app_...",
|
|
24
|
+
publishableKey: "mach_pk_...",
|
|
25
|
+
environment: __DEV__ ? "development" : "production",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await push.requestPermission();
|
|
29
|
+
const registration = await push.register();
|
|
30
|
+
|
|
31
|
+
push.addNotificationReceivedListener(console.log);
|
|
32
|
+
push.addNotificationResponseReceivedListener(console.log);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Expo Go is not supported. Expo apps use `@radhya/mach/expo-plugin` from the
|
|
36
|
+
main `@radhya/mach` package and then create a development or production build.
|
|
37
|
+
Android requires the app's Firebase configuration.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.getOrDefault = { name, fallback ->
|
|
3
|
+
rootProject.ext.has(name) ? rootProject.ext.get(name) : fallback
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
apply plugin: "com.android.library"
|
|
8
|
+
apply plugin: "kotlin-android"
|
|
9
|
+
|
|
10
|
+
android {
|
|
11
|
+
namespace "dev.getmach.push"
|
|
12
|
+
compileSdkVersion getOrDefault("compileSdkVersion", 35)
|
|
13
|
+
|
|
14
|
+
defaultConfig {
|
|
15
|
+
minSdkVersion getOrDefault("minSdkVersion", 23)
|
|
16
|
+
targetSdkVersion getOrDefault("targetSdkVersion", 35)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
dependencies {
|
|
21
|
+
implementation "com.facebook.react:react-android:+"
|
|
22
|
+
implementation "com.google.firebase:firebase-messaging:24.1.0"
|
|
23
|
+
implementation "androidx.core:core-ktx:1.15.0"
|
|
24
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib"
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
3
|
+
<application>
|
|
4
|
+
<service
|
|
5
|
+
android:name=".MachPushMessagingService"
|
|
6
|
+
android:exported="false">
|
|
7
|
+
<intent-filter>
|
|
8
|
+
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
|
9
|
+
</intent-filter>
|
|
10
|
+
</service>
|
|
11
|
+
</application>
|
|
12
|
+
</manifest>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
package dev.getmach.push
|
|
2
|
+
|
|
3
|
+
import android.os.Bundle
|
|
4
|
+
import com.facebook.react.bridge.Arguments
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
7
|
+
|
|
8
|
+
object MachPushEventBus {
|
|
9
|
+
const val RECEIVED_EVENT = "MachPushNotificationReceived"
|
|
10
|
+
const val OPENED_EVENT = "MachPushNotificationOpened"
|
|
11
|
+
const val EXTRA_PAYLOAD = "mach.push.payload"
|
|
12
|
+
const val EXTRA_TITLE = "mach.push.title"
|
|
13
|
+
const val EXTRA_BODY = "mach.push.body"
|
|
14
|
+
|
|
15
|
+
@Volatile
|
|
16
|
+
private var reactContext: ReactApplicationContext? = null
|
|
17
|
+
|
|
18
|
+
fun attach(context: ReactApplicationContext) {
|
|
19
|
+
reactContext = context
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
fun emit(name: String, title: String?, body: String?, data: Map<String, String>) {
|
|
23
|
+
val context = reactContext ?: return
|
|
24
|
+
if (!context.hasActiveReactInstance()) return
|
|
25
|
+
val payload = Arguments.createMap()
|
|
26
|
+
payload.putString("title", title)
|
|
27
|
+
payload.putString("body", body)
|
|
28
|
+
payload.putMap("data", Arguments.makeNativeMap(data))
|
|
29
|
+
payload.putString("platform", "android")
|
|
30
|
+
context
|
|
31
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
32
|
+
.emit(name, payload)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fun fromBundle(bundle: Bundle?): Map<String, String> {
|
|
36
|
+
if (bundle == null) return emptyMap()
|
|
37
|
+
return bundle.keySet()
|
|
38
|
+
.filterNot {
|
|
39
|
+
it == EXTRA_TITLE ||
|
|
40
|
+
it == EXTRA_BODY ||
|
|
41
|
+
it == EXTRA_PAYLOAD ||
|
|
42
|
+
it.startsWith("mach_")
|
|
43
|
+
}
|
|
44
|
+
.associateWith { bundle.get(it)?.toString().orEmpty() }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
package dev.getmach.push
|
|
2
|
+
|
|
3
|
+
import android.app.Notification
|
|
4
|
+
import android.app.NotificationChannel
|
|
5
|
+
import android.app.NotificationManager
|
|
6
|
+
import android.app.PendingIntent
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import android.content.Intent
|
|
9
|
+
import android.os.Build
|
|
10
|
+
import androidx.core.app.NotificationCompat
|
|
11
|
+
import com.google.firebase.messaging.FirebaseMessagingService
|
|
12
|
+
import com.google.firebase.messaging.RemoteMessage
|
|
13
|
+
import kotlin.math.absoluteValue
|
|
14
|
+
|
|
15
|
+
class MachPushMessagingService : FirebaseMessagingService() {
|
|
16
|
+
override fun onNewToken(token: String) {
|
|
17
|
+
getSharedPreferences("mach.push", Context.MODE_PRIVATE)
|
|
18
|
+
.edit()
|
|
19
|
+
.putString("nativeToken", token)
|
|
20
|
+
.apply()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
override fun onMessageReceived(message: RemoteMessage) {
|
|
24
|
+
val title = message.notification?.title ?: message.data["mach_title"]
|
|
25
|
+
val body = message.notification?.body ?: message.data["mach_body"]
|
|
26
|
+
val publicData = message.data.filterKeys { !it.startsWith("mach_") }
|
|
27
|
+
MachPushEventBus.emit(
|
|
28
|
+
MachPushEventBus.RECEIVED_EVENT,
|
|
29
|
+
title,
|
|
30
|
+
body,
|
|
31
|
+
publicData
|
|
32
|
+
)
|
|
33
|
+
showForegroundNotification(title, body, message.data)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private fun showForegroundNotification(
|
|
37
|
+
title: String?,
|
|
38
|
+
body: String?,
|
|
39
|
+
data: Map<String, String>
|
|
40
|
+
) {
|
|
41
|
+
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
42
|
+
val channelId = data["mach_android_channel_id"] ?: "mach_push_default"
|
|
43
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
|
44
|
+
manager.getNotificationChannel(channelId) == null
|
|
45
|
+
) {
|
|
46
|
+
manager.createNotificationChannel(
|
|
47
|
+
NotificationChannel(
|
|
48
|
+
channelId,
|
|
49
|
+
"Notifications",
|
|
50
|
+
NotificationManager.IMPORTANCE_HIGH
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
|
|
56
|
+
?: Intent()
|
|
57
|
+
launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
58
|
+
launchIntent.putExtra(MachPushEventBus.EXTRA_PAYLOAD, true)
|
|
59
|
+
launchIntent.putExtra(MachPushEventBus.EXTRA_TITLE, title)
|
|
60
|
+
launchIntent.putExtra(MachPushEventBus.EXTRA_BODY, body)
|
|
61
|
+
data.forEach { (key, value) -> launchIntent.putExtra(key, value) }
|
|
62
|
+
val pendingIntent = PendingIntent.getActivity(
|
|
63
|
+
this,
|
|
64
|
+
messageNotificationId(data),
|
|
65
|
+
launchIntent,
|
|
66
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
67
|
+
)
|
|
68
|
+
val icon = applicationInfo.icon.takeIf { it != 0 } ?: android.R.drawable.ic_dialog_info
|
|
69
|
+
val notification = NotificationCompat.Builder(this, channelId)
|
|
70
|
+
.setSmallIcon(icon)
|
|
71
|
+
.setContentTitle(title ?: applicationInfo.loadLabel(packageManager))
|
|
72
|
+
.setContentText(body)
|
|
73
|
+
.setStyle(NotificationCompat.BigTextStyle().bigText(body))
|
|
74
|
+
.setAutoCancel(true)
|
|
75
|
+
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
76
|
+
.setContentIntent(pendingIntent)
|
|
77
|
+
.apply {
|
|
78
|
+
if (data["mach_sound"] != null) {
|
|
79
|
+
setDefaults(Notification.DEFAULT_SOUND)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
.build()
|
|
83
|
+
manager.notify(messageNotificationId(data), notification)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private fun messageNotificationId(data: Map<String, String>): Int {
|
|
87
|
+
return (data["mach_notification_id"]?.hashCode() ?: System.nanoTime().hashCode())
|
|
88
|
+
.absoluteValue
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
package dev.getmach.push
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.app.Activity
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.content.Context
|
|
7
|
+
import android.content.pm.PackageManager
|
|
8
|
+
import android.os.Build
|
|
9
|
+
import com.facebook.react.bridge.Arguments
|
|
10
|
+
import com.facebook.react.bridge.Promise
|
|
11
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
12
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
13
|
+
import com.facebook.react.bridge.ReactMethod
|
|
14
|
+
import com.facebook.react.bridge.ActivityEventListener
|
|
15
|
+
import com.facebook.react.modules.core.PermissionAwareActivity
|
|
16
|
+
import com.facebook.react.modules.core.PermissionListener
|
|
17
|
+
import com.google.firebase.messaging.FirebaseMessaging
|
|
18
|
+
import java.util.Locale
|
|
19
|
+
import java.util.TimeZone
|
|
20
|
+
import java.util.UUID
|
|
21
|
+
|
|
22
|
+
class MachPushModule(
|
|
23
|
+
private val context: ReactApplicationContext
|
|
24
|
+
) : ReactContextBaseJavaModule(context), ActivityEventListener {
|
|
25
|
+
private val preferences =
|
|
26
|
+
context.getSharedPreferences("mach.push", Context.MODE_PRIVATE)
|
|
27
|
+
|
|
28
|
+
init {
|
|
29
|
+
MachPushEventBus.attach(context)
|
|
30
|
+
context.addActivityEventListener(this)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
override fun getName() = "MachPush"
|
|
34
|
+
|
|
35
|
+
@ReactMethod
|
|
36
|
+
fun requestPermission(promise: Promise) {
|
|
37
|
+
if (Build.VERSION.SDK_INT < 33 ||
|
|
38
|
+
context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) ==
|
|
39
|
+
PackageManager.PERMISSION_GRANTED
|
|
40
|
+
) {
|
|
41
|
+
promise.resolve("granted")
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
val activity = currentActivity as? PermissionAwareActivity
|
|
45
|
+
if (activity == null) {
|
|
46
|
+
promise.reject("MACH_PUSH_PERMISSION", "A foreground activity is required.")
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
val listener = PermissionListener { requestCode, _, grantResults ->
|
|
50
|
+
if (requestCode != 9472) return@PermissionListener false
|
|
51
|
+
promise.resolve(
|
|
52
|
+
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED)
|
|
53
|
+
"granted"
|
|
54
|
+
else
|
|
55
|
+
"denied"
|
|
56
|
+
)
|
|
57
|
+
true
|
|
58
|
+
}
|
|
59
|
+
activity.requestPermissions(
|
|
60
|
+
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
|
61
|
+
9472,
|
|
62
|
+
listener
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@ReactMethod
|
|
67
|
+
fun getInstallationId(promise: Promise) {
|
|
68
|
+
val existing = preferences.getString("installationId", null)
|
|
69
|
+
if (existing != null) {
|
|
70
|
+
promise.resolve(existing)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
val value = UUID.randomUUID().toString()
|
|
74
|
+
preferences.edit().putString("installationId", value).apply()
|
|
75
|
+
promise.resolve(value)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@ReactMethod
|
|
79
|
+
fun getNativeToken(promise: Promise) {
|
|
80
|
+
FirebaseMessaging.getInstance().token
|
|
81
|
+
.addOnSuccessListener { token ->
|
|
82
|
+
preferences.edit().putString("nativeToken", token).apply()
|
|
83
|
+
promise.resolve(token)
|
|
84
|
+
}
|
|
85
|
+
.addOnFailureListener { error ->
|
|
86
|
+
promise.reject("MACH_PUSH_REGISTRATION", error)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@ReactMethod
|
|
91
|
+
fun getDeviceInfo(promise: Promise) {
|
|
92
|
+
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
|
93
|
+
val result = Arguments.createMap()
|
|
94
|
+
result.putString("nativeAppId", context.packageName)
|
|
95
|
+
result.putString("deviceName", "${Build.MANUFACTURER} ${Build.MODEL}".trim())
|
|
96
|
+
result.putString("appVersion", packageInfo.versionName ?: "")
|
|
97
|
+
result.putString("osVersion", Build.VERSION.RELEASE ?: "")
|
|
98
|
+
result.putString("locale", Locale.getDefault().toLanguageTag())
|
|
99
|
+
result.putString("timezone", TimeZone.getDefault().id)
|
|
100
|
+
promise.resolve(result)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@ReactMethod
|
|
104
|
+
fun getMachPushToken(promise: Promise) {
|
|
105
|
+
promise.resolve(preferences.getString("machToken", null))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@ReactMethod
|
|
109
|
+
fun setMachPushToken(token: String?, promise: Promise) {
|
|
110
|
+
val editor = preferences.edit()
|
|
111
|
+
if (token == null) {
|
|
112
|
+
editor.remove("machToken")
|
|
113
|
+
} else {
|
|
114
|
+
editor.putString("machToken", token)
|
|
115
|
+
}
|
|
116
|
+
editor.apply()
|
|
117
|
+
promise.resolve(null)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@ReactMethod
|
|
121
|
+
fun getInitialNotification(promise: Promise) {
|
|
122
|
+
val intent = currentActivity?.intent
|
|
123
|
+
if (intent?.getBooleanExtra(MachPushEventBus.EXTRA_PAYLOAD, false) != true) {
|
|
124
|
+
promise.resolve(null)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
promise.resolve(notificationMap(intent))
|
|
128
|
+
intent.removeExtra(MachPushEventBus.EXTRA_PAYLOAD)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@ReactMethod
|
|
132
|
+
fun addListener(@Suppress("UNUSED_PARAMETER") eventName: String) = Unit
|
|
133
|
+
|
|
134
|
+
@ReactMethod
|
|
135
|
+
fun removeListeners(@Suppress("UNUSED_PARAMETER") count: Int) = Unit
|
|
136
|
+
|
|
137
|
+
override fun onNewIntent(intent: Intent) {
|
|
138
|
+
if (!intent.getBooleanExtra(MachPushEventBus.EXTRA_PAYLOAD, false)) return
|
|
139
|
+
val data = MachPushEventBus.fromBundle(intent.extras)
|
|
140
|
+
MachPushEventBus.emit(
|
|
141
|
+
MachPushEventBus.OPENED_EVENT,
|
|
142
|
+
intent.getStringExtra(MachPushEventBus.EXTRA_TITLE),
|
|
143
|
+
intent.getStringExtra(MachPushEventBus.EXTRA_BODY),
|
|
144
|
+
data
|
|
145
|
+
)
|
|
146
|
+
intent.removeExtra(MachPushEventBus.EXTRA_PAYLOAD)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
override fun onActivityResult(
|
|
150
|
+
activity: Activity,
|
|
151
|
+
requestCode: Int,
|
|
152
|
+
resultCode: Int,
|
|
153
|
+
data: Intent?
|
|
154
|
+
) = Unit
|
|
155
|
+
|
|
156
|
+
private fun notificationMap(intent: Intent) = Arguments.createMap().apply {
|
|
157
|
+
putString("title", intent.getStringExtra(MachPushEventBus.EXTRA_TITLE))
|
|
158
|
+
putString("body", intent.getStringExtra(MachPushEventBus.EXTRA_BODY))
|
|
159
|
+
putMap("data", Arguments.makeNativeMap(MachPushEventBus.fromBundle(intent.extras)))
|
|
160
|
+
putString("platform", "android")
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package dev.getmach.push
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class MachPushPackage : ReactPackage {
|
|
9
|
+
override fun createNativeModules(
|
|
10
|
+
reactContext: ReactApplicationContext
|
|
11
|
+
): List<NativeModule> = listOf(MachPushModule(reactContext))
|
|
12
|
+
|
|
13
|
+
override fun createViewManagers(
|
|
14
|
+
reactContext: ReactApplicationContext
|
|
15
|
+
): List<ViewManager<*, *>> = emptyList()
|
|
16
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { MachPushClient, type MachPushConfig, type MachPushRegistration } from "@radhya/mach-push-core";
|
|
2
|
+
import { type EmitterSubscription } from "react-native";
|
|
3
|
+
export interface MachPushNotification {
|
|
4
|
+
title?: string;
|
|
5
|
+
body?: string;
|
|
6
|
+
data: Record<string, string>;
|
|
7
|
+
platform: "ios" | "android";
|
|
8
|
+
}
|
|
9
|
+
export declare class ReactNativeMachPush extends MachPushClient {
|
|
10
|
+
addNotificationReceivedListener(listener: (notification: MachPushNotification) => void): EmitterSubscription;
|
|
11
|
+
addNotificationResponseReceivedListener(listener: (notification: MachPushNotification) => void): EmitterSubscription;
|
|
12
|
+
getLastNotificationResponseAsync(): Promise<MachPushNotification | null>;
|
|
13
|
+
}
|
|
14
|
+
export declare function createMachPush(config: MachPushConfig): ReactNativeMachPush;
|
|
15
|
+
export type MachPushPermissionStatus = "granted" | "denied" | "provisional";
|
|
16
|
+
export interface MachPushSetupCallbacks {
|
|
17
|
+
onNotificationReceived?: (notification: MachPushNotification) => void | Promise<void>;
|
|
18
|
+
onNotificationOpened?: (notification: MachPushNotification) => void | Promise<void>;
|
|
19
|
+
onRegistered?: (registration: MachPushRegistration) => void | Promise<void>;
|
|
20
|
+
onPermissionChanged?: (status: MachPushPermissionStatus) => void | Promise<void>;
|
|
21
|
+
onError?: (error: Error) => void | Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export interface MachPushSetupResult {
|
|
24
|
+
client: ReactNativeMachPush;
|
|
25
|
+
permission: MachPushPermissionStatus;
|
|
26
|
+
registration?: MachPushRegistration;
|
|
27
|
+
removeListeners(): void;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Starts the complete default MACH Push experience:
|
|
31
|
+
* listeners, cold-start response, permission request, and device registration.
|
|
32
|
+
*
|
|
33
|
+
* Applications that need a custom permission moment can keep using
|
|
34
|
+
* `createMachPush` directly.
|
|
35
|
+
*/
|
|
36
|
+
export declare function setupMachPush(config: MachPushConfig, callbacks?: MachPushSetupCallbacks): Promise<MachPushSetupResult>;
|
|
37
|
+
export * from "@radhya/mach-push-core";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.ReactNativeMachPush = void 0;
|
|
18
|
+
exports.createMachPush = createMachPush;
|
|
19
|
+
exports.setupMachPush = setupMachPush;
|
|
20
|
+
const mach_push_core_1 = require("@radhya/mach-push-core");
|
|
21
|
+
const react_native_1 = require("react-native");
|
|
22
|
+
const NativeMachPush = react_native_1.NativeModules.MachPush;
|
|
23
|
+
function requireNativeModule() {
|
|
24
|
+
if (!NativeMachPush) {
|
|
25
|
+
throw new Error("MACH Push native module is unavailable. Rebuild the native app after installing @radhya/mach-push-react-native. Expo Go is not supported.");
|
|
26
|
+
}
|
|
27
|
+
return NativeMachPush;
|
|
28
|
+
}
|
|
29
|
+
const notificationEmitter = NativeMachPush
|
|
30
|
+
? new react_native_1.NativeEventEmitter(NativeMachPush)
|
|
31
|
+
: null;
|
|
32
|
+
class ReactNativeMachPush extends mach_push_core_1.MachPushClient {
|
|
33
|
+
addNotificationReceivedListener(listener) {
|
|
34
|
+
if (!notificationEmitter)
|
|
35
|
+
requireNativeModule();
|
|
36
|
+
return notificationEmitter.addListener("MachPushNotificationReceived", listener);
|
|
37
|
+
}
|
|
38
|
+
addNotificationResponseReceivedListener(listener) {
|
|
39
|
+
if (!notificationEmitter)
|
|
40
|
+
requireNativeModule();
|
|
41
|
+
return notificationEmitter.addListener("MachPushNotificationOpened", listener);
|
|
42
|
+
}
|
|
43
|
+
getLastNotificationResponseAsync() {
|
|
44
|
+
return requireNativeModule().getInitialNotification();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.ReactNativeMachPush = ReactNativeMachPush;
|
|
48
|
+
function createMachPush(config) {
|
|
49
|
+
if (react_native_1.Platform.OS !== "ios" && react_native_1.Platform.OS !== "android") {
|
|
50
|
+
throw new Error("MACH Push supports iOS and Android native applications.");
|
|
51
|
+
}
|
|
52
|
+
const native = requireNativeModule();
|
|
53
|
+
const adapter = {
|
|
54
|
+
platform: react_native_1.Platform.OS,
|
|
55
|
+
requestPermission: () => native.requestPermission(),
|
|
56
|
+
getInstallationId: () => native.getInstallationId(),
|
|
57
|
+
getNativeToken: () => native.getNativeToken(),
|
|
58
|
+
getDeviceInfo: () => native.getDeviceInfo(),
|
|
59
|
+
getMachPushToken: () => native.getMachPushToken(),
|
|
60
|
+
setMachPushToken: (token) => native.setMachPushToken(token),
|
|
61
|
+
};
|
|
62
|
+
return new ReactNativeMachPush(config, adapter);
|
|
63
|
+
}
|
|
64
|
+
function asError(value) {
|
|
65
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
66
|
+
}
|
|
67
|
+
async function reportCallbackError(callback, value) {
|
|
68
|
+
try {
|
|
69
|
+
await callback?.(asError(value));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Application error handlers must never interrupt push registration.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function invokeCallback(callback, value, onError) {
|
|
76
|
+
try {
|
|
77
|
+
await callback?.(value);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
await reportCallbackError(onError, error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Starts the complete default MACH Push experience:
|
|
85
|
+
* listeners, cold-start response, permission request, and device registration.
|
|
86
|
+
*
|
|
87
|
+
* Applications that need a custom permission moment can keep using
|
|
88
|
+
* `createMachPush` directly.
|
|
89
|
+
*/
|
|
90
|
+
async function setupMachPush(config, callbacks = {}) {
|
|
91
|
+
const client = createMachPush(config);
|
|
92
|
+
const received = client.addNotificationReceivedListener((notification) => {
|
|
93
|
+
void invokeCallback(callbacks.onNotificationReceived, notification, callbacks.onError);
|
|
94
|
+
});
|
|
95
|
+
const opened = client.addNotificationResponseReceivedListener((notification) => {
|
|
96
|
+
void invokeCallback(callbacks.onNotificationOpened, notification, callbacks.onError);
|
|
97
|
+
});
|
|
98
|
+
const removeListeners = () => {
|
|
99
|
+
received.remove();
|
|
100
|
+
opened.remove();
|
|
101
|
+
};
|
|
102
|
+
try {
|
|
103
|
+
const initial = await client.getLastNotificationResponseAsync();
|
|
104
|
+
if (initial) {
|
|
105
|
+
await invokeCallback(callbacks.onNotificationOpened, initial, callbacks.onError);
|
|
106
|
+
}
|
|
107
|
+
const permission = await client.requestPermission();
|
|
108
|
+
await invokeCallback(callbacks.onPermissionChanged, permission, callbacks.onError);
|
|
109
|
+
if (permission === "denied") {
|
|
110
|
+
return { client, permission, removeListeners };
|
|
111
|
+
}
|
|
112
|
+
const registration = await client.register();
|
|
113
|
+
await invokeCallback(callbacks.onRegistered, registration, callbacks.onError);
|
|
114
|
+
return { client, permission, registration, removeListeners };
|
|
115
|
+
}
|
|
116
|
+
catch (value) {
|
|
117
|
+
const error = asError(value);
|
|
118
|
+
await reportCallbackError(callbacks.onError, error);
|
|
119
|
+
removeListeners();
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
__exportStar(require("@radhya/mach-push-core"), exports);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "getmach-push-react-native"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.license = package["license"]
|
|
10
|
+
s.author = "MACH"
|
|
11
|
+
s.homepage = "https://getmach.dev"
|
|
12
|
+
s.platforms = { :ios => "13.0" }
|
|
13
|
+
s.source = { :git => "https://bitbucket.org/nitscoder/mach-api.git", :tag => s.version }
|
|
14
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
15
|
+
s.swift_version = "5.9"
|
|
16
|
+
s.dependency "React-Core"
|
|
17
|
+
end
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import React
|
|
3
|
+
import UIKit
|
|
4
|
+
import UserNotifications
|
|
5
|
+
|
|
6
|
+
@objc(MachPush)
|
|
7
|
+
final class MachPush: RCTEventEmitter {
|
|
8
|
+
private var nativeTokenResolvers: [RCTPromiseResolveBlock] = []
|
|
9
|
+
private var nativeTokenRejecters: [RCTPromiseRejectBlock] = []
|
|
10
|
+
private var hasEventListeners = false
|
|
11
|
+
|
|
12
|
+
override init() {
|
|
13
|
+
super.init()
|
|
14
|
+
NotificationCenter.default.addObserver(
|
|
15
|
+
self,
|
|
16
|
+
selector: #selector(didRegisterToken(_:)),
|
|
17
|
+
name: Notification.Name("MachPushDidRegisterTokenNotification"),
|
|
18
|
+
object: nil
|
|
19
|
+
)
|
|
20
|
+
NotificationCenter.default.addObserver(
|
|
21
|
+
self,
|
|
22
|
+
selector: #selector(didReceiveNotification(_:)),
|
|
23
|
+
name: MachPushNotificationReceived,
|
|
24
|
+
object: nil
|
|
25
|
+
)
|
|
26
|
+
NotificationCenter.default.addObserver(
|
|
27
|
+
self,
|
|
28
|
+
selector: #selector(didOpenNotification(_:)),
|
|
29
|
+
name: MachPushNotificationOpened,
|
|
30
|
+
object: nil
|
|
31
|
+
)
|
|
32
|
+
MachPushNotificationDelegate.shared.install()
|
|
33
|
+
NotificationCenter.default.addObserver(
|
|
34
|
+
self,
|
|
35
|
+
selector: #selector(didFailRegistration(_:)),
|
|
36
|
+
name: Notification.Name("MachPushDidFailRegistrationNotification"),
|
|
37
|
+
object: nil
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
deinit {
|
|
42
|
+
NotificationCenter.default.removeObserver(self)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@objc static override func requiresMainQueueSetup() -> Bool { true }
|
|
46
|
+
|
|
47
|
+
override func supportedEvents() -> [String]! {
|
|
48
|
+
[
|
|
49
|
+
"MachPushNotificationReceived",
|
|
50
|
+
"MachPushNotificationOpened",
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override func startObserving() {
|
|
55
|
+
hasEventListeners = true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override func stopObserving() {
|
|
59
|
+
hasEventListeners = false
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@objc(requestPermission:rejecter:)
|
|
63
|
+
func requestPermission(
|
|
64
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
65
|
+
reject: @escaping RCTPromiseRejectBlock
|
|
66
|
+
) {
|
|
67
|
+
UNUserNotificationCenter.current().requestAuthorization(
|
|
68
|
+
options: [.alert, .badge, .sound, .provisional]
|
|
69
|
+
) { granted, error in
|
|
70
|
+
if let error {
|
|
71
|
+
reject("MACH_PUSH_PERMISSION", error.localizedDescription, error)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
|
75
|
+
DispatchQueue.main.async {
|
|
76
|
+
UIApplication.shared.registerForRemoteNotifications()
|
|
77
|
+
}
|
|
78
|
+
if settings.authorizationStatus == .provisional {
|
|
79
|
+
resolve("provisional")
|
|
80
|
+
} else {
|
|
81
|
+
resolve(granted ? "granted" : "denied")
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@objc(getInstallationId:rejecter:)
|
|
88
|
+
func getInstallationId(
|
|
89
|
+
resolve: RCTPromiseResolveBlock,
|
|
90
|
+
reject: RCTPromiseRejectBlock
|
|
91
|
+
) {
|
|
92
|
+
let defaults = UserDefaults.standard
|
|
93
|
+
if let existing = defaults.string(forKey: "mach.push.installationId") {
|
|
94
|
+
resolve(existing)
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
let value = UUID().uuidString.lowercased()
|
|
98
|
+
defaults.set(value, forKey: "mach.push.installationId")
|
|
99
|
+
resolve(value)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@objc(getNativeToken:rejecter:)
|
|
103
|
+
func getNativeToken(
|
|
104
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
105
|
+
reject: @escaping RCTPromiseRejectBlock
|
|
106
|
+
) {
|
|
107
|
+
if let token = UserDefaults.standard.string(forKey: "mach.push.nativeToken") {
|
|
108
|
+
resolve(token)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
nativeTokenResolvers.append(resolve)
|
|
112
|
+
nativeTokenRejecters.append(reject)
|
|
113
|
+
DispatchQueue.main.async {
|
|
114
|
+
UIApplication.shared.registerForRemoteNotifications()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@objc(getDeviceInfo:rejecter:)
|
|
119
|
+
func getDeviceInfo(
|
|
120
|
+
resolve: RCTPromiseResolveBlock,
|
|
121
|
+
reject: RCTPromiseRejectBlock
|
|
122
|
+
) {
|
|
123
|
+
resolve([
|
|
124
|
+
"nativeAppId": Bundle.main.bundleIdentifier ?? "",
|
|
125
|
+
"deviceName": UIDevice.current.model,
|
|
126
|
+
"appVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "",
|
|
127
|
+
"osVersion": UIDevice.current.systemVersion,
|
|
128
|
+
"locale": Locale.current.identifier,
|
|
129
|
+
"timezone": TimeZone.current.identifier
|
|
130
|
+
])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@objc(getMachPushToken:rejecter:)
|
|
134
|
+
func getMachPushToken(
|
|
135
|
+
resolve: RCTPromiseResolveBlock,
|
|
136
|
+
reject: RCTPromiseRejectBlock
|
|
137
|
+
) {
|
|
138
|
+
resolve(UserDefaults.standard.string(forKey: "mach.push.machToken"))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@objc(setMachPushToken:resolver:rejecter:)
|
|
142
|
+
func setMachPushToken(
|
|
143
|
+
token: String?,
|
|
144
|
+
resolve: RCTPromiseResolveBlock,
|
|
145
|
+
reject: RCTPromiseRejectBlock
|
|
146
|
+
) {
|
|
147
|
+
if let token {
|
|
148
|
+
UserDefaults.standard.set(token, forKey: "mach.push.machToken")
|
|
149
|
+
} else {
|
|
150
|
+
UserDefaults.standard.removeObject(forKey: "mach.push.machToken")
|
|
151
|
+
}
|
|
152
|
+
resolve(nil)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@objc(getInitialNotification:rejecter:)
|
|
156
|
+
func getInitialNotification(
|
|
157
|
+
resolve: RCTPromiseResolveBlock,
|
|
158
|
+
reject: RCTPromiseRejectBlock
|
|
159
|
+
) {
|
|
160
|
+
resolve(MachPushNotificationDelegate.shared.takeLastOpened())
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@objc private func didRegisterToken(_ notification: Notification) {
|
|
164
|
+
guard let token = notification.userInfo?["token"] as? String else { return }
|
|
165
|
+
let resolvers = nativeTokenResolvers
|
|
166
|
+
nativeTokenResolvers.removeAll()
|
|
167
|
+
nativeTokenRejecters.removeAll()
|
|
168
|
+
resolvers.forEach { $0(token) }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@objc private func didFailRegistration(_ notification: Notification) {
|
|
172
|
+
let error = notification.userInfo?["error"] as? NSError
|
|
173
|
+
let rejecters = nativeTokenRejecters
|
|
174
|
+
nativeTokenResolvers.removeAll()
|
|
175
|
+
nativeTokenRejecters.removeAll()
|
|
176
|
+
rejecters.forEach {
|
|
177
|
+
$0(
|
|
178
|
+
"MACH_PUSH_REGISTRATION",
|
|
179
|
+
error?.localizedDescription ?? "APNs registration failed.",
|
|
180
|
+
error
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@objc private func didReceiveNotification(_ notification: Notification) {
|
|
186
|
+
guard hasEventListeners, let value = notification.object else { return }
|
|
187
|
+
sendEvent(withName: "MachPushNotificationReceived", body: value)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@objc private func didOpenNotification(_ notification: Notification) {
|
|
191
|
+
guard hasEventListeners, let value = notification.object else { return }
|
|
192
|
+
MachPushNotificationDelegate.shared.clearLastOpened()
|
|
193
|
+
sendEvent(withName: "MachPushNotificationOpened", body: value)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
#import <UIKit/UIKit.h>
|
|
3
|
+
#import <objc/runtime.h>
|
|
4
|
+
|
|
5
|
+
NSString * const MachPushDidRegisterTokenNotification = @"MachPushDidRegisterTokenNotification";
|
|
6
|
+
NSString * const MachPushDidFailRegistrationNotification = @"MachPushDidFailRegistrationNotification";
|
|
7
|
+
|
|
8
|
+
static BOOL MachPushSwizzle(Class cls, SEL original, SEL replacement) {
|
|
9
|
+
Method replacementMethod = class_getInstanceMethod(cls, replacement);
|
|
10
|
+
if (!replacementMethod) return NO;
|
|
11
|
+
Method originalMethod = class_getInstanceMethod(cls, original);
|
|
12
|
+
if (!originalMethod) {
|
|
13
|
+
class_addMethod(cls, original, method_getImplementation(replacementMethod), method_getTypeEncoding(replacementMethod));
|
|
14
|
+
return NO;
|
|
15
|
+
}
|
|
16
|
+
method_exchangeImplementations(originalMethod, replacementMethod);
|
|
17
|
+
return YES;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static BOOL MachPushHadRegisterHandler = NO;
|
|
21
|
+
static BOOL MachPushHadFailureHandler = NO;
|
|
22
|
+
|
|
23
|
+
@interface NSObject (MachPushAppDelegateInterceptor)
|
|
24
|
+
@end
|
|
25
|
+
|
|
26
|
+
@implementation NSObject (MachPushAppDelegateInterceptor)
|
|
27
|
+
|
|
28
|
+
+ (void)load {
|
|
29
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
30
|
+
Class appDelegateClass = NSClassFromString(@"AppDelegate");
|
|
31
|
+
if (!appDelegateClass) return;
|
|
32
|
+
MachPushHadRegisterHandler = MachPushSwizzle(
|
|
33
|
+
appDelegateClass,
|
|
34
|
+
@selector(application:didRegisterForRemoteNotificationsWithDeviceToken:),
|
|
35
|
+
@selector(mach_application:didRegisterForRemoteNotificationsWithDeviceToken:)
|
|
36
|
+
);
|
|
37
|
+
MachPushHadFailureHandler = MachPushSwizzle(
|
|
38
|
+
appDelegateClass,
|
|
39
|
+
@selector(application:didFailToRegisterForRemoteNotificationsWithError:),
|
|
40
|
+
@selector(mach_application:didFailToRegisterForRemoteNotificationsWithError:)
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
- (void)mach_application:(UIApplication *)application
|
|
46
|
+
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
|
|
47
|
+
const unsigned char *bytes = deviceToken.bytes;
|
|
48
|
+
NSMutableString *token = [NSMutableString stringWithCapacity:deviceToken.length * 2];
|
|
49
|
+
for (NSUInteger index = 0; index < deviceToken.length; index++) {
|
|
50
|
+
[token appendFormat:@"%02x", bytes[index]];
|
|
51
|
+
}
|
|
52
|
+
[[NSUserDefaults standardUserDefaults] setObject:token forKey:@"mach.push.nativeToken"];
|
|
53
|
+
[[NSNotificationCenter defaultCenter]
|
|
54
|
+
postNotificationName:MachPushDidRegisterTokenNotification
|
|
55
|
+
object:nil
|
|
56
|
+
userInfo:@{ @"token": token }];
|
|
57
|
+
if (MachPushHadRegisterHandler) {
|
|
58
|
+
[self mach_application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
- (void)mach_application:(UIApplication *)application
|
|
63
|
+
didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
|
|
64
|
+
[[NSNotificationCenter defaultCenter]
|
|
65
|
+
postNotificationName:MachPushDidFailRegistrationNotification
|
|
66
|
+
object:nil
|
|
67
|
+
userInfo:@{ @"error": error }];
|
|
68
|
+
if (MachPushHadFailureHandler) {
|
|
69
|
+
[self mach_application:application didFailToRegisterForRemoteNotificationsWithError:error];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTEventEmitter.h>
|
|
3
|
+
|
|
4
|
+
@interface RCT_EXTERN_MODULE(MachPush, RCTEventEmitter)
|
|
5
|
+
RCT_EXTERN_METHOD(requestPermission:(RCTPromiseResolveBlock)resolve
|
|
6
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
7
|
+
RCT_EXTERN_METHOD(getInstallationId:(RCTPromiseResolveBlock)resolve
|
|
8
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
9
|
+
RCT_EXTERN_METHOD(getNativeToken:(RCTPromiseResolveBlock)resolve
|
|
10
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
11
|
+
RCT_EXTERN_METHOD(getDeviceInfo:(RCTPromiseResolveBlock)resolve
|
|
12
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
13
|
+
RCT_EXTERN_METHOD(getMachPushToken:(RCTPromiseResolveBlock)resolve
|
|
14
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
15
|
+
RCT_EXTERN_METHOD(setMachPushToken:(NSString * _Nullable)token
|
|
16
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
17
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
18
|
+
RCT_EXTERN_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve
|
|
19
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
20
|
+
@end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UserNotifications
|
|
3
|
+
|
|
4
|
+
let MachPushNotificationReceived =
|
|
5
|
+
Notification.Name("MachPushNotificationReceived")
|
|
6
|
+
let MachPushNotificationOpened =
|
|
7
|
+
Notification.Name("MachPushNotificationOpened")
|
|
8
|
+
|
|
9
|
+
final class MachPushNotificationDelegate: NSObject,
|
|
10
|
+
UNUserNotificationCenterDelegate
|
|
11
|
+
{
|
|
12
|
+
static let shared = MachPushNotificationDelegate()
|
|
13
|
+
|
|
14
|
+
private var forwardDelegate: UNUserNotificationCenterDelegate?
|
|
15
|
+
private var lastOpened: [String: Any]?
|
|
16
|
+
|
|
17
|
+
func install() {
|
|
18
|
+
DispatchQueue.main.async {
|
|
19
|
+
let center = UNUserNotificationCenter.current()
|
|
20
|
+
if (center.delegate as AnyObject?) !== self {
|
|
21
|
+
self.forwardDelegate = center.delegate
|
|
22
|
+
center.delegate = self
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func takeLastOpened() -> [String: Any]? {
|
|
28
|
+
defer { lastOpened = nil }
|
|
29
|
+
return lastOpened
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func clearLastOpened() {
|
|
33
|
+
lastOpened = nil
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private func payload(
|
|
37
|
+
_ notification: UNNotification
|
|
38
|
+
) -> [String: Any] {
|
|
39
|
+
let content = notification.request.content
|
|
40
|
+
var data: [String: String] = [:]
|
|
41
|
+
for (key, value) in content.userInfo {
|
|
42
|
+
let name = String(describing: key)
|
|
43
|
+
if name == "aps" || name.hasPrefix("mach_") { continue }
|
|
44
|
+
if let text = value as? String {
|
|
45
|
+
data[name] = text
|
|
46
|
+
} else if let number = value as? NSNumber {
|
|
47
|
+
data[name] = number.stringValue
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return [
|
|
51
|
+
"title": content.title,
|
|
52
|
+
"body": content.body,
|
|
53
|
+
"data": data,
|
|
54
|
+
"platform": "ios",
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func userNotificationCenter(
|
|
59
|
+
_ center: UNUserNotificationCenter,
|
|
60
|
+
willPresent notification: UNNotification,
|
|
61
|
+
withCompletionHandler completionHandler:
|
|
62
|
+
@escaping (UNNotificationPresentationOptions) -> Void
|
|
63
|
+
) {
|
|
64
|
+
let value = payload(notification)
|
|
65
|
+
NotificationCenter.default.post(
|
|
66
|
+
name: MachPushNotificationReceived,
|
|
67
|
+
object: value
|
|
68
|
+
)
|
|
69
|
+
let machOptions: UNNotificationPresentationOptions
|
|
70
|
+
if #available(iOS 14.0, *) {
|
|
71
|
+
machOptions = [.banner, .list, .sound, .badge]
|
|
72
|
+
} else {
|
|
73
|
+
machOptions = [.alert, .sound, .badge]
|
|
74
|
+
}
|
|
75
|
+
if let forwardDelegate,
|
|
76
|
+
forwardDelegate.responds(
|
|
77
|
+
to: #selector(
|
|
78
|
+
UNUserNotificationCenterDelegate.userNotificationCenter(
|
|
79
|
+
_:willPresent:withCompletionHandler:
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
{
|
|
84
|
+
forwardDelegate.userNotificationCenter?(
|
|
85
|
+
center,
|
|
86
|
+
willPresent: notification
|
|
87
|
+
) { forwardedOptions in
|
|
88
|
+
completionHandler(forwardedOptions.union(machOptions))
|
|
89
|
+
}
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
completionHandler(machOptions)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func userNotificationCenter(
|
|
96
|
+
_ center: UNUserNotificationCenter,
|
|
97
|
+
didReceive response: UNNotificationResponse,
|
|
98
|
+
withCompletionHandler completionHandler: @escaping () -> Void
|
|
99
|
+
) {
|
|
100
|
+
let value = payload(response.notification)
|
|
101
|
+
lastOpened = value
|
|
102
|
+
NotificationCenter.default.post(
|
|
103
|
+
name: MachPushNotificationOpened,
|
|
104
|
+
object: value
|
|
105
|
+
)
|
|
106
|
+
if let forwardDelegate,
|
|
107
|
+
forwardDelegate.responds(
|
|
108
|
+
to: #selector(
|
|
109
|
+
UNUserNotificationCenterDelegate.userNotificationCenter(
|
|
110
|
+
_:didReceive:withCompletionHandler:
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
{
|
|
115
|
+
forwardDelegate.userNotificationCenter?(
|
|
116
|
+
center,
|
|
117
|
+
didReceive: response,
|
|
118
|
+
withCompletionHandler: completionHandler
|
|
119
|
+
)
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
completionHandler()
|
|
123
|
+
}
|
|
124
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@radhya/mach-push-react-native",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MACH Push for React Native and Expo development builds",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://bitbucket.org/nitscoder/mach-api.git",
|
|
9
|
+
"directory": "packages/push-react-native"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://getmach.dev/push",
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"main": "dist/index.js",
|
|
16
|
+
"types": "dist/index.d.ts",
|
|
17
|
+
"react-native": "src/index.ts",
|
|
18
|
+
"files": [
|
|
19
|
+
"android",
|
|
20
|
+
"ios",
|
|
21
|
+
"src",
|
|
22
|
+
"dist",
|
|
23
|
+
"react-native.config.js",
|
|
24
|
+
"getmach-push-react-native.podspec",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@radhya/mach-push-core": "^0.1.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"react": ">=18",
|
|
36
|
+
"react-native": ">=0.73"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/react": "^19.1.0",
|
|
40
|
+
"react": "19.1.0",
|
|
41
|
+
"react-native": "0.81.5",
|
|
42
|
+
"typescript": "^5.9.3"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MachPushClient,
|
|
3
|
+
type MachPushConfig,
|
|
4
|
+
type MachPushDeviceInfo,
|
|
5
|
+
type MachPushNativeAdapter,
|
|
6
|
+
type MachPushRegistration,
|
|
7
|
+
} from "@radhya/mach-push-core";
|
|
8
|
+
import {
|
|
9
|
+
NativeEventEmitter,
|
|
10
|
+
NativeModules,
|
|
11
|
+
Platform,
|
|
12
|
+
type EmitterSubscription,
|
|
13
|
+
} from "react-native";
|
|
14
|
+
|
|
15
|
+
export interface MachPushNotification {
|
|
16
|
+
title?: string;
|
|
17
|
+
body?: string;
|
|
18
|
+
data: Record<string, string>;
|
|
19
|
+
platform: "ios" | "android";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface NativeMachPushModule {
|
|
23
|
+
requestPermission(): Promise<"granted" | "denied" | "provisional">;
|
|
24
|
+
getInstallationId(): Promise<string>;
|
|
25
|
+
getNativeToken(): Promise<string>;
|
|
26
|
+
getDeviceInfo(): Promise<MachPushDeviceInfo>;
|
|
27
|
+
getMachPushToken(): Promise<string | null>;
|
|
28
|
+
setMachPushToken(token: string | null): Promise<void>;
|
|
29
|
+
getInitialNotification(): Promise<MachPushNotification | null>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const NativeMachPush = NativeModules.MachPush as
|
|
33
|
+
| NativeMachPushModule
|
|
34
|
+
| undefined;
|
|
35
|
+
|
|
36
|
+
function requireNativeModule(): NativeMachPushModule {
|
|
37
|
+
if (!NativeMachPush) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
"MACH Push native module is unavailable. Rebuild the native app after installing @radhya/mach-push-react-native. Expo Go is not supported.",
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return NativeMachPush;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const notificationEmitter = NativeMachPush
|
|
46
|
+
? new NativeEventEmitter(NativeMachPush as any)
|
|
47
|
+
: null;
|
|
48
|
+
|
|
49
|
+
export class ReactNativeMachPush extends MachPushClient {
|
|
50
|
+
addNotificationReceivedListener(
|
|
51
|
+
listener: (notification: MachPushNotification) => void,
|
|
52
|
+
): EmitterSubscription {
|
|
53
|
+
if (!notificationEmitter) requireNativeModule();
|
|
54
|
+
return notificationEmitter!.addListener(
|
|
55
|
+
"MachPushNotificationReceived",
|
|
56
|
+
listener,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
addNotificationResponseReceivedListener(
|
|
61
|
+
listener: (notification: MachPushNotification) => void,
|
|
62
|
+
): EmitterSubscription {
|
|
63
|
+
if (!notificationEmitter) requireNativeModule();
|
|
64
|
+
return notificationEmitter!.addListener(
|
|
65
|
+
"MachPushNotificationOpened",
|
|
66
|
+
listener,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getLastNotificationResponseAsync() {
|
|
71
|
+
return requireNativeModule().getInitialNotification();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createMachPush(config: MachPushConfig): ReactNativeMachPush {
|
|
76
|
+
if (Platform.OS !== "ios" && Platform.OS !== "android") {
|
|
77
|
+
throw new Error("MACH Push supports iOS and Android native applications.");
|
|
78
|
+
}
|
|
79
|
+
const native = requireNativeModule();
|
|
80
|
+
const adapter: MachPushNativeAdapter = {
|
|
81
|
+
platform: Platform.OS,
|
|
82
|
+
requestPermission: () => native.requestPermission(),
|
|
83
|
+
getInstallationId: () => native.getInstallationId(),
|
|
84
|
+
getNativeToken: () => native.getNativeToken(),
|
|
85
|
+
getDeviceInfo: () => native.getDeviceInfo(),
|
|
86
|
+
getMachPushToken: () => native.getMachPushToken(),
|
|
87
|
+
setMachPushToken: (token) => native.setMachPushToken(token),
|
|
88
|
+
};
|
|
89
|
+
return new ReactNativeMachPush(config, adapter);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type MachPushPermissionStatus =
|
|
93
|
+
| "granted"
|
|
94
|
+
| "denied"
|
|
95
|
+
| "provisional";
|
|
96
|
+
|
|
97
|
+
export interface MachPushSetupCallbacks {
|
|
98
|
+
onNotificationReceived?: (
|
|
99
|
+
notification: MachPushNotification,
|
|
100
|
+
) => void | Promise<void>;
|
|
101
|
+
onNotificationOpened?: (
|
|
102
|
+
notification: MachPushNotification,
|
|
103
|
+
) => void | Promise<void>;
|
|
104
|
+
onRegistered?: (
|
|
105
|
+
registration: MachPushRegistration,
|
|
106
|
+
) => void | Promise<void>;
|
|
107
|
+
onPermissionChanged?: (
|
|
108
|
+
status: MachPushPermissionStatus,
|
|
109
|
+
) => void | Promise<void>;
|
|
110
|
+
onError?: (error: Error) => void | Promise<void>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface MachPushSetupResult {
|
|
114
|
+
client: ReactNativeMachPush;
|
|
115
|
+
permission: MachPushPermissionStatus;
|
|
116
|
+
registration?: MachPushRegistration;
|
|
117
|
+
removeListeners(): void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function asError(value: unknown): Error {
|
|
121
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function reportCallbackError(
|
|
125
|
+
callback: MachPushSetupCallbacks["onError"],
|
|
126
|
+
value: unknown,
|
|
127
|
+
) {
|
|
128
|
+
try {
|
|
129
|
+
await callback?.(asError(value));
|
|
130
|
+
} catch {
|
|
131
|
+
// Application error handlers must never interrupt push registration.
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function invokeCallback<T>(
|
|
136
|
+
callback: ((value: T) => void | Promise<void>) | undefined,
|
|
137
|
+
value: T,
|
|
138
|
+
onError: MachPushSetupCallbacks["onError"],
|
|
139
|
+
) {
|
|
140
|
+
try {
|
|
141
|
+
await callback?.(value);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
await reportCallbackError(onError, error);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Starts the complete default MACH Push experience:
|
|
149
|
+
* listeners, cold-start response, permission request, and device registration.
|
|
150
|
+
*
|
|
151
|
+
* Applications that need a custom permission moment can keep using
|
|
152
|
+
* `createMachPush` directly.
|
|
153
|
+
*/
|
|
154
|
+
export async function setupMachPush(
|
|
155
|
+
config: MachPushConfig,
|
|
156
|
+
callbacks: MachPushSetupCallbacks = {},
|
|
157
|
+
): Promise<MachPushSetupResult> {
|
|
158
|
+
const client = createMachPush(config);
|
|
159
|
+
const received = client.addNotificationReceivedListener((notification) => {
|
|
160
|
+
void invokeCallback(
|
|
161
|
+
callbacks.onNotificationReceived,
|
|
162
|
+
notification,
|
|
163
|
+
callbacks.onError,
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
const opened = client.addNotificationResponseReceivedListener(
|
|
167
|
+
(notification) => {
|
|
168
|
+
void invokeCallback(
|
|
169
|
+
callbacks.onNotificationOpened,
|
|
170
|
+
notification,
|
|
171
|
+
callbacks.onError,
|
|
172
|
+
);
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
const removeListeners = () => {
|
|
176
|
+
received.remove();
|
|
177
|
+
opened.remove();
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const initial = await client.getLastNotificationResponseAsync();
|
|
182
|
+
if (initial) {
|
|
183
|
+
await invokeCallback(
|
|
184
|
+
callbacks.onNotificationOpened,
|
|
185
|
+
initial,
|
|
186
|
+
callbacks.onError,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const permission = await client.requestPermission();
|
|
191
|
+
await invokeCallback(
|
|
192
|
+
callbacks.onPermissionChanged,
|
|
193
|
+
permission,
|
|
194
|
+
callbacks.onError,
|
|
195
|
+
);
|
|
196
|
+
if (permission === "denied") {
|
|
197
|
+
return { client, permission, removeListeners };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const registration = await client.register();
|
|
201
|
+
await invokeCallback(
|
|
202
|
+
callbacks.onRegistered,
|
|
203
|
+
registration,
|
|
204
|
+
callbacks.onError,
|
|
205
|
+
);
|
|
206
|
+
return { client, permission, registration, removeListeners };
|
|
207
|
+
} catch (value) {
|
|
208
|
+
const error = asError(value);
|
|
209
|
+
await reportCallbackError(callbacks.onError, error);
|
|
210
|
+
removeListeners();
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export * from "@radhya/mach-push-core";
|