tauri-plugin-mobile-push-api 0.1.0 → 0.1.2

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/README.md ADDED
@@ -0,0 +1,452 @@
1
+ # tauri-plugin-mobile-push
2
+
3
+ Push notifications for Tauri v2 apps on iOS (APNs) and Android (FCM).
4
+
5
+ [![Crates.io](https://img.shields.io/crates/v/tauri-plugin-mobile-push.svg)](https://crates.io/crates/tauri-plugin-mobile-push)
6
+ [![npm](https://img.shields.io/npm/v/tauri-plugin-mobile-push-api.svg)](https://www.npmjs.com/package/tauri-plugin-mobile-push-api)
7
+
8
+ A Tauri v2 plugin that provides native remote push notification support using Apple Push Notification service (APNs) on iOS and Firebase Cloud Messaging (FCM) on Android. Unlike `tauri-plugin-notification` which only handles local notifications, this plugin handles **server-sent remote push notifications** -- the kind you need for chat apps, alerts, and any real-time engagement.
9
+
10
+ The plugin uses **explicit AppDelegate delegation** instead of method swizzling, making it reliable, transparent, and compatible with iOS 26+ where swizzling-based approaches break.
11
+
12
+ ## Features
13
+
14
+ - **APNs on iOS** -- native device token registration and push delivery
15
+ - **FCM on Android** -- Firebase Cloud Messaging integration with automatic token management
16
+ - **Foreground notifications** -- receive and display pushes while the app is open
17
+ - **Notification tap handling** -- deep-link into your app when users tap a notification
18
+ - **Token refresh events** -- stay in sync when the OS rotates device tokens
19
+ - **No method swizzling** -- explicit delegation pattern that is debuggable and future-proof
20
+ - **Desktop no-op** -- compiles on macOS/Windows/Linux without error; commands return `Err` at runtime so you can gate push logic behind platform checks
21
+ - **TypeScript API** -- fully typed async functions and event listeners
22
+
23
+ ## Platform Support
24
+
25
+ | Platform | Push Token | Foreground Notifications | Notification Tap | Token Refresh |
26
+ |----------|-----------|--------------------------|------------------|---------------|
27
+ | iOS 13+ | APNs device token (hex) | Yes | Yes | Yes |
28
+ | Android 7+ (API 24) | FCM registration token | Yes | Yes | Yes |
29
+ | Desktop | No-op (returns error) | N/A | N/A | N/A |
30
+
31
+ ## Why This Plugin?
32
+
33
+ **The official `tauri-plugin-notification` only supports local notifications.** It cannot receive server-sent pushes. If you need to send notifications from your backend to your users' devices, you need this plugin.
34
+
35
+ **Third-party alternatives use method swizzling**, which intercepts Objective-C method calls at runtime. This technique is fragile -- it breaks when multiple plugins swizzle the same methods, produces difficult-to-debug failures, and Apple has been deprecating the APIs that enable it. On iOS 26+, swizzling-based push plugins can silently fail.
36
+
37
+ **This plugin uses explicit AppDelegate delegation.** You create a small `AppDelegate.swift` file that forwards APNs callbacks to the plugin via `NotificationCenter`. This approach is:
38
+
39
+ - **Reliable** -- no hidden runtime magic that can silently break
40
+ - **Debuggable** -- you can set breakpoints in the delegate methods and see exactly what happens
41
+ - **Future-proof** -- uses standard Apple APIs that will not be deprecated
42
+ - **Composable** -- works alongside any other plugins or libraries without conflicts
43
+
44
+ ## Installation
45
+
46
+ ### Rust
47
+
48
+ Add to `src-tauri/Cargo.toml`:
49
+
50
+ ```toml
51
+ [dependencies]
52
+ # From crates.io
53
+ tauri-plugin-mobile-push = "0.1"
54
+
55
+ # Or from git
56
+ tauri-plugin-mobile-push = { git = "https://github.com/yanqianglu/tauri-plugin-mobile-push" }
57
+ ```
58
+
59
+ ### JavaScript / TypeScript
60
+
61
+ ```bash
62
+ npm install tauri-plugin-mobile-push-api
63
+ # or
64
+ pnpm add tauri-plugin-mobile-push-api
65
+ # or
66
+ bun add tauri-plugin-mobile-push-api
67
+ ```
68
+
69
+ Requires `@tauri-apps/api` >= 2.0.0 as a peer dependency.
70
+
71
+ ### Capabilities
72
+
73
+ Add to your capabilities file (e.g., `src-tauri/capabilities/mobile.json`):
74
+
75
+ ```json
76
+ {
77
+ "permissions": ["mobile-push:default"]
78
+ }
79
+ ```
80
+
81
+ This grants both `allow-request-permission` and `allow-get-token`.
82
+
83
+ ### Plugin Registration
84
+
85
+ In `src-tauri/src/lib.rs`:
86
+
87
+ ```rust
88
+ tauri::Builder::default()
89
+ .plugin(tauri_plugin_mobile_push::init())
90
+ // ... other plugins
91
+ .run(tauri::generate_context!())
92
+ .expect("error while running tauri application");
93
+ ```
94
+
95
+ ## Setup
96
+
97
+ ### iOS
98
+
99
+ #### 1. Enable Push Notifications Capability
100
+
101
+ In Xcode, select your target, go to **Signing & Capabilities**, and add the **Push Notifications** capability. This adds the `aps-environment` entitlement automatically.
102
+
103
+ #### 2. Create AppDelegate.swift
104
+
105
+ Create the file at `src-tauri/gen/apple/Sources/AppDelegate.swift`. This file forwards APNs callbacks to the plugin -- it is required because the plugin does **not** use method swizzling.
106
+
107
+ ```swift
108
+ import SwiftUI
109
+ import Tauri
110
+ import UIKit
111
+ import UserNotifications
112
+ import WebKit
113
+
114
+ class AppDelegate: TauriAppDelegate {
115
+ override func application(
116
+ _ application: UIApplication,
117
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
118
+ ) -> Bool {
119
+ // Set self as the notification center delegate so foreground
120
+ // notifications and tap events are routed to this class.
121
+ UNUserNotificationCenter.current().delegate = self
122
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
123
+ }
124
+
125
+ // Called by iOS when APNs registration succeeds.
126
+ // Converts the raw token data to a hex string and posts it
127
+ // so the plugin can resolve the pending getToken() call.
128
+ override func application(
129
+ _ application: UIApplication,
130
+ didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
131
+ ) {
132
+ super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
133
+
134
+ let hex = deviceToken.map { String(format: "%02x", $0) }.joined()
135
+ NotificationCenter.default.post(
136
+ name: Notification.Name("APNsTokenReceived"),
137
+ object: nil,
138
+ userInfo: ["token": hex]
139
+ )
140
+ }
141
+
142
+ // Called by iOS when APNs registration fails.
143
+ override func application(
144
+ _ application: UIApplication,
145
+ didFailToRegisterForRemoteNotificationsWithError error: Error
146
+ ) {
147
+ super.application(application, didFailToRegisterForRemoteNotificationsWithError: error)
148
+
149
+ NotificationCenter.default.post(
150
+ name: Notification.Name("APNsRegistrationFailed"),
151
+ object: nil,
152
+ userInfo: ["error": error.localizedDescription]
153
+ )
154
+ }
155
+ }
156
+
157
+ // MARK: - UNUserNotificationCenterDelegate
158
+
159
+ extension AppDelegate: UNUserNotificationCenterDelegate {
160
+ // Called when a notification arrives while the app is in the foreground.
161
+ // Posts to the plugin and shows the notification as a banner.
162
+ func userNotificationCenter(
163
+ _ center: UNUserNotificationCenter,
164
+ willPresent notification: UNNotification,
165
+ withCompletionHandler completionHandler:
166
+ @escaping (UNNotificationPresentationOptions) -> Void
167
+ ) {
168
+ let userInfo = notification.request.content.userInfo
169
+ NotificationCenter.default.post(
170
+ name: Notification.Name("PushNotificationReceived"),
171
+ object: nil,
172
+ userInfo: userInfo as? [String: Any]
173
+ )
174
+ completionHandler([.banner, .sound, .badge])
175
+ }
176
+
177
+ // Called when the user taps a notification.
178
+ func userNotificationCenter(
179
+ _ center: UNUserNotificationCenter,
180
+ didReceive response: UNNotificationResponse,
181
+ withCompletionHandler completionHandler: @escaping () -> Void
182
+ ) {
183
+ let userInfo = response.notification.request.content.userInfo
184
+ NotificationCenter.default.post(
185
+ name: Notification.Name("PushNotificationTapped"),
186
+ object: nil,
187
+ userInfo: userInfo as? [String: Any]
188
+ )
189
+ completionHandler()
190
+ }
191
+ }
192
+ ```
193
+
194
+ #### 3. Entitlements
195
+
196
+ Ensure your `.entitlements` file includes:
197
+
198
+ ```xml
199
+ <key>aps-environment</key>
200
+ <string>development</string>
201
+ ```
202
+
203
+ Change to `production` for App Store / TestFlight builds. If you added the Push Notifications capability via Xcode, this is handled automatically.
204
+
205
+ ### Android
206
+
207
+ #### 1. Add Firebase
208
+
209
+ 1. Create a Firebase project at [console.firebase.google.com](https://console.firebase.google.com/) and add your Android app.
210
+ 2. Download `google-services.json` and place it in `src-tauri/gen/android/app/`.
211
+ 3. Configure your Gradle files:
212
+
213
+ ```kotlin
214
+ // project-level build.gradle.kts
215
+ plugins {
216
+ id("com.google.gms.google-services") version "4.4.2" apply false
217
+ }
218
+
219
+ // app-level build.gradle.kts
220
+ plugins {
221
+ id("com.google.gms.google-services")
222
+ }
223
+
224
+ dependencies {
225
+ implementation(platform("com.google.firebase:firebase-bom:33.8.0"))
226
+ implementation("com.google.firebase:firebase-messaging")
227
+ }
228
+ ```
229
+
230
+ #### 2. Register the FCM Service
231
+
232
+ Add to your `AndroidManifest.xml` inside the `<application>` tag:
233
+
234
+ ```xml
235
+ <service
236
+ android:name="app.tauri.mobilepush.FCMService"
237
+ android:exported="false">
238
+ <intent-filter>
239
+ <action android:name="com.google.firebase.MESSAGING_EVENT" />
240
+ </intent-filter>
241
+ </service>
242
+ ```
243
+
244
+ This registers the plugin's `FCMService` which forwards incoming messages and token refreshes to the Tauri event system.
245
+
246
+ ## Usage
247
+
248
+ ### Request Permission
249
+
250
+ Shows the system permission dialog on iOS. On Android 13+ (API 33), requests the `POST_NOTIFICATIONS` runtime permission. Earlier Android versions return `{ granted: true }` immediately.
251
+
252
+ ```typescript
253
+ import { requestPermission } from "tauri-plugin-mobile-push-api";
254
+
255
+ const { granted } = await requestPermission();
256
+ if (!granted) {
257
+ console.warn("Push notification permission denied");
258
+ }
259
+ ```
260
+
261
+ ### Get Device Token
262
+
263
+ Returns the APNs device token (hex string) on iOS or the FCM registration token on Android. On iOS, this triggers `registerForRemoteNotifications()` and resolves when the OS delivers the token via the AppDelegate.
264
+
265
+ ```typescript
266
+ import { getToken } from "tauri-plugin-mobile-push-api";
267
+
268
+ const token = await getToken();
269
+ console.log("Device push token:", token);
270
+ ```
271
+
272
+ ### Complete Registration Flow
273
+
274
+ The typical integration: request permission, get the token, and register it with your backend.
275
+
276
+ ```typescript
277
+ import {
278
+ requestPermission,
279
+ getToken,
280
+ onNotificationReceived,
281
+ onNotificationTapped,
282
+ onTokenRefresh,
283
+ } from "tauri-plugin-mobile-push-api";
284
+
285
+ // 1. Request permission
286
+ const { granted } = await requestPermission();
287
+ if (!granted) {
288
+ console.warn("Push permission denied");
289
+ return;
290
+ }
291
+
292
+ // 2. Get the device push token
293
+ const token = await getToken();
294
+
295
+ // 3. Send token to your backend
296
+ await fetch("https://your-api.com/push/register", {
297
+ method: "POST",
298
+ headers: { "Content-Type": "application/json" },
299
+ body: JSON.stringify({ token, platform: "ios" }),
300
+ });
301
+
302
+ // 4. Listen for foreground notifications
303
+ const unsubReceived = await onNotificationReceived((notification) => {
304
+ console.log("Received:", notification.title, notification.body);
305
+ console.log("Custom data:", notification.data);
306
+ });
307
+
308
+ // 5. Listen for notification taps (user opened app from notification)
309
+ const unsubTapped = await onNotificationTapped((notification) => {
310
+ console.log("Tapped:", notification.data);
311
+ // Navigate to the relevant screen based on notification.data
312
+ });
313
+
314
+ // 6. Listen for token refreshes (re-register with your backend)
315
+ const unsubToken = await onTokenRefresh(({ token }) => {
316
+ console.log("Token refreshed:", token);
317
+ // Send new token to your backend
318
+ });
319
+
320
+ // Cleanup when your component unmounts
321
+ unsubReceived.unregister();
322
+ unsubTapped.unregister();
323
+ unsubToken.unregister();
324
+ ```
325
+
326
+ ### Listen for Notification Taps
327
+
328
+ When a user taps a notification, your app opens and the tap event fires with the notification payload. Use this to deep-link to the relevant screen.
329
+
330
+ ```typescript
331
+ import { onNotificationTapped } from "tauri-plugin-mobile-push-api";
332
+
333
+ const unsub = await onNotificationTapped((notification) => {
334
+ const { screen, id } = notification.data as { screen: string; id: string };
335
+ // Navigate based on the custom data in the push payload
336
+ navigateTo(screen, id);
337
+ });
338
+ ```
339
+
340
+ ### Listen for Token Refresh
341
+
342
+ The OS may rotate device tokens at any time. When this happens, send the new token to your backend.
343
+
344
+ ```typescript
345
+ import { onTokenRefresh } from "tauri-plugin-mobile-push-api";
346
+
347
+ const unsub = await onTokenRefresh(({ token }) => {
348
+ fetch("https://your-api.com/push/register", {
349
+ method: "POST",
350
+ headers: { "Content-Type": "application/json" },
351
+ body: JSON.stringify({ token }),
352
+ });
353
+ });
354
+ ```
355
+
356
+ ## API Reference
357
+
358
+ ### Commands
359
+
360
+ #### `requestPermission()`
361
+
362
+ ```typescript
363
+ function requestPermission(): Promise<{ granted: boolean }>;
364
+ ```
365
+
366
+ Request push notification permission from the user.
367
+
368
+ - **iOS**: Triggers the system permission dialog requesting `.alert`, `.badge`, and `.sound`.
369
+ - **Android 13+**: Requests the `POST_NOTIFICATIONS` runtime permission.
370
+ - **Android < 13**: Returns `{ granted: true }` immediately (no runtime permission needed).
371
+ - **Desktop**: Returns an error.
372
+
373
+ #### `getToken()`
374
+
375
+ ```typescript
376
+ function getToken(): Promise<string>;
377
+ ```
378
+
379
+ Get the current device push token.
380
+
381
+ - **iOS**: Calls `UIApplication.shared.registerForRemoteNotifications()`, waits for the APNs callback, and returns the device token as a hex string.
382
+ - **Android**: Calls `FirebaseMessaging.getInstance().token` and returns the FCM registration token.
383
+ - **Desktop**: Returns an error.
384
+
385
+ ### Events
386
+
387
+ All event listeners return `Promise<PluginListener>`. Call `.unregister()` on the returned listener to stop receiving events.
388
+
389
+ #### `onNotificationReceived(handler)`
390
+
391
+ ```typescript
392
+ function onNotificationReceived(
393
+ handler: (notification: PushNotification) => void,
394
+ ): Promise<PluginListener>;
395
+ ```
396
+
397
+ Fires when a push notification arrives while the app is in the **foreground**. On iOS, the notification is also displayed as a banner (with sound and badge).
398
+
399
+ #### `onNotificationTapped(handler)`
400
+
401
+ ```typescript
402
+ function onNotificationTapped(
403
+ handler: (notification: PushNotification) => void,
404
+ ): Promise<PluginListener>;
405
+ ```
406
+
407
+ Fires when the user **taps** a push notification to open the app. Use this for deep linking.
408
+
409
+ #### `onTokenRefresh(handler)`
410
+
411
+ ```typescript
412
+ function onTokenRefresh(
413
+ handler: (payload: { token: string }) => void,
414
+ ): Promise<PluginListener>;
415
+ ```
416
+
417
+ Fires when the OS issues a new push token (APNs token refresh on iOS, FCM token rotation on Android). Send the new token to your backend whenever this fires.
418
+
419
+ ### Types
420
+
421
+ ```typescript
422
+ /** Payload delivered with push notification events. */
423
+ interface PushNotification {
424
+ title?: string;
425
+ body?: string;
426
+ data: Record<string, unknown>;
427
+ badge?: number;
428
+ sound?: string;
429
+ }
430
+ ```
431
+
432
+ ## Sending Push Notifications from Your Server
433
+
434
+ Once you have the device token, send pushes from your backend via:
435
+
436
+ - **iOS (APNs):** Use the [APNs HTTP/2 API](https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns) with a `.p8` signing key or `.p12` certificate.
437
+ - **Android (FCM):** Use the [FCM HTTP v1 API](https://firebase.google.com/docs/cloud-messaging/send-message) with a service account.
438
+
439
+ The notification payload should include `title`, `body`, and any custom `data` fields your app needs. These will be delivered to your `onNotificationReceived` and `onNotificationTapped` handlers.
440
+
441
+ ## Architecture
442
+
443
+ The plugin is structured as a standard Tauri v2 plugin with platform-specific native implementations:
444
+
445
+ - **Rust core** (`src/`) -- plugin registration, command definitions, and a desktop no-op fallback
446
+ - **Swift** (`ios/`) -- `MobilePushPlugin` receives APNs callbacks via `NotificationCenter` posts from your AppDelegate
447
+ - **Kotlin** (`android/`) -- `MobilePushPlugin` wraps Firebase Messaging; `FCMService` extends `FirebaseMessagingService` to forward messages and token refreshes
448
+ - **TypeScript** (`guest-js/`) -- thin async wrappers over `invoke()` and `addPluginListener()` from `@tauri-apps/api`
449
+
450
+ ## License
451
+
452
+ Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT License](LICENSE-MIT) at your option.
package/dist/index.cjs CHANGED
@@ -39,21 +39,21 @@ async function getToken() {
39
39
  }
40
40
  async function onNotificationReceived(handler) {
41
41
  return (0, import_core.addPluginListener)(
42
- "plugin:mobile-push",
42
+ "mobile-push",
43
43
  "notification-received",
44
44
  handler
45
45
  );
46
46
  }
47
47
  async function onNotificationTapped(handler) {
48
48
  return (0, import_core.addPluginListener)(
49
- "plugin:mobile-push",
49
+ "mobile-push",
50
50
  "notification-tapped",
51
51
  handler
52
52
  );
53
53
  }
54
54
  async function onTokenRefresh(handler) {
55
55
  return (0, import_core.addPluginListener)(
56
- "plugin:mobile-push",
56
+ "mobile-push",
57
57
  "token-received",
58
58
  handler
59
59
  );
package/dist/index.js CHANGED
@@ -14,21 +14,21 @@ async function getToken() {
14
14
  }
15
15
  async function onNotificationReceived(handler) {
16
16
  return addPluginListener(
17
- "plugin:mobile-push",
17
+ "mobile-push",
18
18
  "notification-received",
19
19
  handler
20
20
  );
21
21
  }
22
22
  async function onNotificationTapped(handler) {
23
23
  return addPluginListener(
24
- "plugin:mobile-push",
24
+ "mobile-push",
25
25
  "notification-tapped",
26
26
  handler
27
27
  );
28
28
  }
29
29
  async function onTokenRefresh(handler) {
30
30
  return addPluginListener(
31
- "plugin:mobile-push",
31
+ "mobile-push",
32
32
  "token-received",
33
33
  handler
34
34
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tauri-plugin-mobile-push-api",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "TypeScript API for tauri-plugin-mobile-push — push notifications for iOS (APNs) and Android (FCM)",
5
5
  "license": "(MIT OR Apache-2.0)",
6
6
  "repository": {