@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 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
+ }
@@ -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
+ }
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ ios: {},
5
+ android: {
6
+ sourceDir: "./android"
7
+ }
8
+ }
9
+ }
10
+ };
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";