appsonair-react-native-apppush 0.0.1-alpha
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 +540 -0
- package/android/build.gradle +177 -0
- package/android/gradle.properties +15 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/appsonairreactnativeapppush/AppsonairReactNativeApppushModuleImpl.kt +767 -0
- package/android/src/main/java/com/appsonairreactnativeapppush/AppsonairReactNativeApppushPackage.kt +43 -0
- package/android/src/newarch/java/com/appsonairreactnativeapppush/AppsonairReactNativeApppushModule.kt +199 -0
- package/android/src/oldarch/java/com/appsonairreactnativeapppush/AppsonairReactNativeApppushModule.kt +249 -0
- package/appsonair-react-native-apppush.podspec +77 -0
- package/ios/AppsonairReactNativeApppush-Bridging-Header.h +12 -0
- package/ios/AppsonairReactNativeApppush.h +28 -0
- package/ios/AppsonairReactNativeApppush.mm +494 -0
- package/ios/AppsonairReactNativeApppushImpl.swift +594 -0
- package/lib/commonjs/NativeAppsonairApppush.js +48 -0
- package/lib/commonjs/NativeAppsonairApppush.js.map +1 -0
- package/lib/commonjs/index.js +686 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/NativeAppsonairApppush.js +47 -0
- package/lib/module/NativeAppsonairApppush.js.map +1 -0
- package/lib/module/index.js +612 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/src/NativeAppsonairApppush.d.ts +134 -0
- package/lib/typescript/commonjs/src/NativeAppsonairApppush.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +392 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/types.d.ts +221 -0
- package/lib/typescript/commonjs/src/types.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/src/NativeAppsonairApppush.d.ts +134 -0
- package/lib/typescript/module/src/NativeAppsonairApppush.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +392 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -0
- package/lib/typescript/module/src/types.d.ts +221 -0
- package/lib/typescript/module/src/types.d.ts.map +1 -0
- package/package.json +119 -0
- package/react-native.config.js +14 -0
- package/src/NativeAppsonairApppush.ts +188 -0
- package/src/index.tsx +745 -0
- package/src/types.ts +284 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Logicwind
|
|
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,540 @@
|
|
|
1
|
+
# appsonair-react-native-apppush
|
|
2
|
+
|
|
3
|
+
Push notifications for React Native
|
|
4
|
+
|
|
5
|
+
Works on both React Native architectures. New Arch gets a real Codegen
|
|
6
|
+
TurboModule, Old Arch gets a Bridge NativeModule — picked at build time, same JS
|
|
7
|
+
API either way. Nothing in your app code changes.
|
|
8
|
+
|
|
9
|
+
> [!WARNING]
|
|
10
|
+
> **Alpha release — not for production use.**
|
|
11
|
+
>
|
|
12
|
+
> `0.0.1-alpha` is an early preview, intended for evaluation, prototypes, and
|
|
13
|
+
> internal test builds. Do **not** ship it in a production app or one with a
|
|
14
|
+
> large user base.
|
|
15
|
+
>
|
|
16
|
+
> - The public API may change without notice and may not stay source-compatible —
|
|
17
|
+
> expect to update your integration between releases.
|
|
18
|
+
> - Breaking changes are not limited to major versions while the package is pre-1.0.
|
|
19
|
+
> - The native SDKs underneath are pre-release too (`0.0.2-alpha`), and carry the
|
|
20
|
+
> same caveats.
|
|
21
|
+
> - Not yet proven at scale; some behaviour is still unverified in real-world use.
|
|
22
|
+
>
|
|
23
|
+
> Pin this exact version rather than a version range, and re-test on every upgrade.
|
|
24
|
+
|
|
25
|
+
> [!NOTE]
|
|
26
|
+
> Both native SDKs resolve on their own — Android from JitPack, iOS from
|
|
27
|
+
> CocoaPods — so the only setup you need is in [Setup](#setup). The backend *is*
|
|
28
|
+
> live: the SDKs register the device and sync subscription state — tags,
|
|
29
|
+
> language, opt-in — to `/v1/subscriptions`.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
> **Alpha.** Pin this exact version — the API may change between releases. See
|
|
36
|
+
> [the notice above](#appsonair-react-native-apppush) before adopting it.
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npm install appsonair-react-native-apppush@0.0.1-alpha
|
|
40
|
+
npx pod-install # iOS only
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`npm install appsonair-react-native-apppush` on its own will not find it: alpha
|
|
44
|
+
releases are published under the `alpha` dist-tag, not `latest`.
|
|
45
|
+
|
|
46
|
+
Autolinking does the rest — nothing to add to `MainApplication.kt` or `AppDelegate`.
|
|
47
|
+
|
|
48
|
+
| Requires | |
|
|
49
|
+
|---|---|
|
|
50
|
+
| React Native | 0.71+ |
|
|
51
|
+
| iOS | 15.0+ |
|
|
52
|
+
| Android | minSdk 24 |
|
|
53
|
+
| Kotlin | 1.9+ (whatever your RN version pins — see below) |
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import AppPushService from 'appsonair-react-native-apppush';
|
|
61
|
+
|
|
62
|
+
async function setupPush() {
|
|
63
|
+
await AppPushService.initialize({ debug: __DEV__ });
|
|
64
|
+
|
|
65
|
+
const granted = await AppPushService.requestPermission();
|
|
66
|
+
if (!granted) return;
|
|
67
|
+
|
|
68
|
+
// Resolves `null` until registration finishes — call it *after* permission.
|
|
69
|
+
const token = await AppPushService.getToken();
|
|
70
|
+
console.log('push token', token);
|
|
71
|
+
|
|
72
|
+
await AppPushService.login('user-42');
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Subscribe to events once near app start, and keep them for the app's lifetime:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { useEffect } from 'react';
|
|
80
|
+
import AppPushService from 'appsonair-react-native-apppush';
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
const subs = [
|
|
84
|
+
AppPushService.onNotificationReceived(({ notification }) => {
|
|
85
|
+
console.log('foreground', notification.title, notification.data);
|
|
86
|
+
}),
|
|
87
|
+
|
|
88
|
+
AppPushService.onNotificationOpened(({ notification, actionId, url }) => {
|
|
89
|
+
if (url) navigate(url);
|
|
90
|
+
else if (actionId === 'reply') openReply(notification.id);
|
|
91
|
+
}),
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
return () => subs.forEach((s) => s.remove());
|
|
95
|
+
}, []);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Named exports work too, if you prefer them or want tree-shaking:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { initialize, getToken, onNotificationOpened, user } from 'appsonair-react-native-apppush';
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Setup
|
|
107
|
+
|
|
108
|
+
### iOS
|
|
109
|
+
|
|
110
|
+
1. **Signing & Capabilities → + Capability → Push Notifications.**
|
|
111
|
+
2. Add **Background Modes → Remote notifications** if you use silent push.
|
|
112
|
+
3. Add your AppsOnAir app id to `Info.plist`:
|
|
113
|
+
|
|
114
|
+
```xml
|
|
115
|
+
<key>AppsonairAppId</key>
|
|
116
|
+
<string>your-app-id</string>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
<details>
|
|
120
|
+
<summary><b>App Groups</b> — needed for delivery receipts from a Notification Service Extension</summary>
|
|
121
|
+
|
|
122
|
+
iOS resolves the group itself: an `AppsOnAirAppGroup` string in
|
|
123
|
+
`Info.plist`, else the convention `group.<your-bundle-id>.appsonair`.
|
|
124
|
+
|
|
125
|
+
Enable that group on **both** the app target and your Notification Service
|
|
126
|
+
Extension — otherwise the extension's delivery receipts degrade silently.
|
|
127
|
+
</details>
|
|
128
|
+
|
|
129
|
+
<details>
|
|
130
|
+
<summary><b>Build fails with <code>__swift_FORCE_LOAD_$_swiftCompatibility56</code></b> — required if your app target has no Swift file</summary>
|
|
131
|
+
|
|
132
|
+
This wrapper and the SDKs beneath it are Swift static libraries. Xcode only adds
|
|
133
|
+
the Swift runtime search paths to a target that *contains* Swift, so a pure
|
|
134
|
+
Objective-C app (`AppDelegate.mm`, `main.m`, nothing else) fails to link:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
Undefined symbols: __swift_FORCE_LOAD_$_swiftCompatibility56
|
|
138
|
+
referenced from libAppsOnAir-Core.a, libAppsOnAir-AppPush.a,
|
|
139
|
+
libReachabilitySwift.a, libappsonair-react-native-apppush.a
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Simplest fix** — add any empty `.swift` file to your app target and let Xcode
|
|
143
|
+
create the bridging header it offers. The file can stay empty.
|
|
144
|
+
|
|
145
|
+
**Or** add the paths in your Podfile's `post_install` (what `example/ios/Podfile` does):
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
installer.aggregate_targets.each do |aggregate_target|
|
|
149
|
+
aggregate_target.user_project.native_targets.each do |target|
|
|
150
|
+
target.build_configurations.each do |build_config|
|
|
151
|
+
build_config.build_settings['LIBRARY_SEARCH_PATHS'] = [
|
|
152
|
+
'$(inherited)',
|
|
153
|
+
# DT_TOOLCHAIN_DIR, not TOOLCHAIN_DIR -- the latter resolves to the
|
|
154
|
+
# Metal toolchain under Xcode 26 and the path does not exist.
|
|
155
|
+
'$(DT_TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)',
|
|
156
|
+
'/usr/lib/swift'
|
|
157
|
+
]
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
aggregate_target.user_project.save
|
|
161
|
+
end
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
This bites on Old Arch first. New Arch builds often link anyway because other
|
|
165
|
+
Swift-bearing pods pull the paths in — that's luck, not design. Apply it regardless.
|
|
166
|
+
</details>
|
|
167
|
+
|
|
168
|
+
<details>
|
|
169
|
+
<summary><b>Background sync</b> — extra <code>Info.plist</code> key</summary>
|
|
170
|
+
|
|
171
|
+
```xml
|
|
172
|
+
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
|
173
|
+
<array><string>com.appsonair.push.background-sync</string></array>
|
|
174
|
+
```
|
|
175
|
+
</details>
|
|
176
|
+
|
|
177
|
+
<details>
|
|
178
|
+
<summary><b><code>AppsOnAir-Core</code> is pinned to <code>>= 1.2.3</code></b> — only matters if your Podfile.lock already carries an older one</summary>
|
|
179
|
+
|
|
180
|
+
This package declares `AppsOnAir-Core >= 1.2.3` alongside the Push SDK itself.
|
|
181
|
+
The floor is deliberate: `AppsOnAir-AppPush.podspec` depends on
|
|
182
|
+
`AppsOnAir-Core` with no constraint, but `AppsOnAirDeviceInfo` calls
|
|
183
|
+
`AppsOnAirCoreServices.getDeviceMetadata()`, which only exists in 1.2.x.
|
|
184
|
+
|
|
185
|
+
A fresh install resolves the newest Core and never notices. The case this covers
|
|
186
|
+
is an app that already pins an older Core in its `Podfile.lock` — likely if you
|
|
187
|
+
also use AppLink, AppSync or AppRemark — where an unconstrained dependency stays
|
|
188
|
+
satisfied by 1.1.1 and the Push SDK then fails to compile.
|
|
189
|
+
|
|
190
|
+
If CocoaPods reports a conflict with a Core version another pod requires:
|
|
191
|
+
|
|
192
|
+
```sh
|
|
193
|
+
pod update AppsOnAir-Core
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The pin goes away once the upstream podspec carries its own floor.
|
|
197
|
+
</details>
|
|
198
|
+
|
|
199
|
+
<details>
|
|
200
|
+
<summary><b>Pointing at a local iOS SDK checkout</b> — for SDK development, or if the pod hasn't propagated yet</summary>
|
|
201
|
+
|
|
202
|
+
This package depends on `AppsOnAir-AppPush` `0.0.2-alpha` from the CocoaPods
|
|
203
|
+
trunk, so `npx pod-install` normally needs no help. To build against a local
|
|
204
|
+
checkout instead, declare it in your app's Podfile — a `:path` declaration wins
|
|
205
|
+
over this package's dependency line:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
# ios/Podfile
|
|
209
|
+
pod 'AppsOnAir-AppPush', :path => '../../appsonair-ios-push-notification'
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
If `pod install` can't find the pod at all, refresh your spec repo first with
|
|
213
|
+
`pod repo update`.
|
|
214
|
+
</details>
|
|
215
|
+
|
|
216
|
+
### Android
|
|
217
|
+
|
|
218
|
+
The native SDK is on JitPack, and Firebase is required.
|
|
219
|
+
|
|
220
|
+
```groovy
|
|
221
|
+
// android/settings.gradle
|
|
222
|
+
dependencyResolutionManagement {
|
|
223
|
+
repositories {
|
|
224
|
+
google()
|
|
225
|
+
mavenCentral()
|
|
226
|
+
maven { url 'https://jitpack.io' } // AppsOnAir Push SDK + AppsOnAir Core
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Add the Google Services plugin and your `google-services.json` as you would for
|
|
232
|
+
any FCM app. `POST_NOTIFICATIONS` (Android 13+) is requested for you by
|
|
233
|
+
`requestPermission()`.
|
|
234
|
+
|
|
235
|
+
<details>
|
|
236
|
+
<summary><b>Kotlin version</b> — why this package pins the stdlib, and what to do if your app still fails to compile</summary>
|
|
237
|
+
|
|
238
|
+
The published native SDK (`0.0.2-alpha`) is compiled with **Kotlin 2.2.10** and
|
|
239
|
+
sets no `languageVersion` floor, so its classes carry metadata `2.2.0`. No React
|
|
240
|
+
Native release ships a Kotlin compiler that can read that — 0.76 pins 1.9.24,
|
|
241
|
+
0.77–0.78 pin 2.0.21, 0.79–0.81 pin 2.1.x — and raising your app's Kotlin to 2.2
|
|
242
|
+
is not a fix either: KGP 2.2 breaks React Native's own Gradle plugin with
|
|
243
|
+
`Found interface KotlinTopLevelExtension, but class was expected`.
|
|
244
|
+
|
|
245
|
+
This package works around it in `android/build.gradle` so you don't have to:
|
|
246
|
+
`-Xskip-metadata-version-check` lets this module read the SDK's classes, and a
|
|
247
|
+
`strictly` pin holds `kotlin-stdlib` at your app's Kotlin version — without that
|
|
248
|
+
second half the SDK drags stdlib 2.2.10 onto **your app's** compile classpath and
|
|
249
|
+
your own Kotlin stops compiling.
|
|
250
|
+
|
|
251
|
+
If your app still fails with `Module was compiled with an incompatible version of
|
|
252
|
+
Kotlin`, something else on your classpath is pulling the newer stdlib back in.
|
|
253
|
+
Check with:
|
|
254
|
+
|
|
255
|
+
```sh
|
|
256
|
+
cd android && ./gradlew :app:dependencies --configuration debugCompileClasspath | grep kotlin-stdlib
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Both workarounds are temporary; they go away when the SDK republishes with an
|
|
260
|
+
older language version.
|
|
261
|
+
</details>
|
|
262
|
+
|
|
263
|
+
<details>
|
|
264
|
+
<summary><b>Overriding the Push SDK coordinate</b> — for a local build or a private repo</summary>
|
|
265
|
+
|
|
266
|
+
This package pins the released SDK. To point at something else, set the coordinate
|
|
267
|
+
from your app's `android/gradle.properties` — no need to patch this package:
|
|
268
|
+
|
|
269
|
+
```properties
|
|
270
|
+
# The default.
|
|
271
|
+
AppsonairReactNativeApppush_pushSdkCoordinate=com.github.apps-on-air:appsonair-android-push-notification:0.0.2-alpha
|
|
272
|
+
```
|
|
273
|
+
</details>
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Recipes
|
|
278
|
+
|
|
279
|
+
**Ask for permission at the right moment, not on launch**
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
if (await AppPushService.getPermission()) return; // already granted, don't re-prompt
|
|
283
|
+
|
|
284
|
+
// `fallbackToSettings` sends the user to the OS settings screen when they've
|
|
285
|
+
// already permanently denied — without it the prompt just fails silently.
|
|
286
|
+
const granted = await AppPushService.requestPermission({ fallbackToSettings: true });
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Identify a user, and tag them for targeting**
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
await AppPushService.login('user-42');
|
|
293
|
+
await AppPushService.user.addTags({ plan: 'pro', locale: 'en-IN' });
|
|
294
|
+
await AppPushService.user.addEmail('someone@example.com');
|
|
295
|
+
|
|
296
|
+
// On sign-out
|
|
297
|
+
await AppPushService.logout();
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Deep link from a notification tap**
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
AppPushService.onNotificationOpened(({ notification, actionId, url }) => {
|
|
304
|
+
if (actionId) return handleAction(actionId, notification);
|
|
305
|
+
if (url) return Linking.openURL(url);
|
|
306
|
+
navigateToInbox();
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Suppress a foreground notification** (Android only — see [Platform differences](#platform-differences))
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
AppPushService.onNotificationWillDisplay(({ notification, preventDefault }) => {
|
|
314
|
+
// Must be called synchronously — native is holding the notification.
|
|
315
|
+
if (notification.data.screen === currentScreen) preventDefault();
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Keep the badge tidy**
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
await AppPushService.badge.set(unreadCount);
|
|
323
|
+
await AppPushService.badge.clear();
|
|
324
|
+
await AppPushService.badge.setAutoClearOnForeground(true); // iOS only
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Forward the token to your own backend**
|
|
328
|
+
|
|
329
|
+
There's no `onTokenUpdated` event today, so poll for it after permission resolves:
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
const token = await AppPushService.getToken();
|
|
333
|
+
if (token) await api.registerDevice(token);
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## API
|
|
339
|
+
|
|
340
|
+
Everything returns a `Promise` (except `isInitialized()`, which is synchronous)
|
|
341
|
+
and rejects rather than throwing natively.
|
|
342
|
+
|
|
343
|
+
Calling anything before `initialize()` resolves rejects with `notInitialized` on
|
|
344
|
+
both platforms — without that guard the native Android SDK would throw
|
|
345
|
+
`IllegalStateException` and crash the app.
|
|
346
|
+
|
|
347
|
+
**Lifecycle**
|
|
348
|
+
|
|
349
|
+
| | |
|
|
350
|
+
|---|---|
|
|
351
|
+
| `initialize(config?)` | `{ debug?: boolean }`. Await once at app start. |
|
|
352
|
+
| `isInitialized()` | Synchronous `boolean`. |
|
|
353
|
+
|
|
354
|
+
**Identity**
|
|
355
|
+
|
|
356
|
+
| | |
|
|
357
|
+
|---|---|
|
|
358
|
+
| `login(externalId)` · `setUserId(externalId)` | Aliases. Rejects on an empty string. |
|
|
359
|
+
| `logout()` | Back to an anonymous subscription. |
|
|
360
|
+
| `getExternalId()` | `null` while anonymous. |
|
|
361
|
+
| `getDeviceId()` | AppsOnAir device id. |
|
|
362
|
+
| `getSubscriptionId()` | Backend-assigned; `null` until registered. |
|
|
363
|
+
|
|
364
|
+
**Token**
|
|
365
|
+
|
|
366
|
+
| | |
|
|
367
|
+
|---|---|
|
|
368
|
+
| `getToken()` · `getDeviceToken()` | Aliases. APNs hex on iOS, FCM token on Android. `null` before registration. |
|
|
369
|
+
|
|
370
|
+
**Permissions**
|
|
371
|
+
|
|
372
|
+
| | |
|
|
373
|
+
|---|---|
|
|
374
|
+
| `requestPermission({ fallbackToSettings? })` | Resolves the resulting grant state. Rejects with `noActivity` on Android if there's no foreground Activity. |
|
|
375
|
+
| `getPermission()` | Fresh read on both platforms. |
|
|
376
|
+
| `getPermissionStatus()` | Five states on iOS; **only `authorized` / `denied` on Android**. |
|
|
377
|
+
| `canRequestPermission()` | Semantics differ per platform — see [Platform differences](#platform-differences). |
|
|
378
|
+
| `registerForProvisionalAuthorization()` | iOS only. |
|
|
379
|
+
|
|
380
|
+
**`notifications`**
|
|
381
|
+
|
|
382
|
+
| | |
|
|
383
|
+
|---|---|
|
|
384
|
+
| `clearAll()` | |
|
|
385
|
+
| `remove(id)` | Payload `notification_id`. |
|
|
386
|
+
| `removeMany(ids)` | One native call on iOS; loops on Android. |
|
|
387
|
+
| `removeGroup(groupKey)` | Android only. |
|
|
388
|
+
|
|
389
|
+
**`badge`**
|
|
390
|
+
|
|
391
|
+
| | |
|
|
392
|
+
|---|---|
|
|
393
|
+
| `get()` · `set(n)` · `increment(delta?)` · `clear()` | Best-effort on Android — see [Platform differences](#platform-differences). |
|
|
394
|
+
| `setAutoClearOnForeground(enabled)` | iOS only. |
|
|
395
|
+
|
|
396
|
+
**`user`**
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
user.getAppsOnAirId() user.getExternalId()
|
|
400
|
+
user.addTag(k, v) user.addTags(obj) user.getTags()
|
|
401
|
+
user.removeTag(k) user.removeTags(keys)
|
|
402
|
+
user.addAlias(l, id) user.addAliases(obj)
|
|
403
|
+
user.removeAlias(l) user.removeAliases(labels)
|
|
404
|
+
user.addEmail(addr) user.removeEmail(addr)
|
|
405
|
+
user.setLanguage(code) user.getLanguage()
|
|
406
|
+
user.getPushSubscription() // { id, token, optedIn }
|
|
407
|
+
user.optIn() user.optOut() user.getOptedIn()
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**`consent`** — `setRequired` / `getRequired` / `setGiven` / `getGiven`
|
|
411
|
+
(stored, but [not enforced yet](#platform-differences)).
|
|
412
|
+
|
|
413
|
+
**`debug`** — `setLogLevel(level)`, safe to call before `initialize()`.
|
|
414
|
+
|
|
415
|
+
### Events
|
|
416
|
+
|
|
417
|
+
Each returns a `Subscription` with `.remove()`.
|
|
418
|
+
|
|
419
|
+
| Event | Payload |
|
|
420
|
+
|---|---|
|
|
421
|
+
| `onNotificationReceived` | `{ notification }` |
|
|
422
|
+
| `onNotificationOpened` | `{ notification, actionId, url }` |
|
|
423
|
+
| `onNotificationWillDisplay` | `{ notification, preventDefault() }` |
|
|
424
|
+
| `onPermissionChanged` | `{ granted }` |
|
|
425
|
+
| `onSubscriptionChanged` | `{ previous, current }` |
|
|
426
|
+
| `onUserStateChanged` | `{ current }` |
|
|
427
|
+
| `onTokenUpdated` | `{ token, environment }` — `environment` is `'sandbox'` / `'production'` on iOS, `null` on Android. |
|
|
428
|
+
| `onError` | `{ code, message }` — SDK-level failures that happen outside any call you made. |
|
|
429
|
+
| `onSilentNotification` | `{ data }` — iOS sends `content-available`, Android needs a `silent: "true"` data key. |
|
|
430
|
+
| `onInstallationIdUpdated` | `{ id }` — Firebase Installation ID. **Android only.** |
|
|
431
|
+
|
|
432
|
+
Subscribe to `onTokenUpdated` if your backend stores the device token — it is
|
|
433
|
+
the only way to hear about a mid-session rotation, which `getToken()` at startup
|
|
434
|
+
will miss. Events raised before the first subscriber exists are dropped rather
|
|
435
|
+
than queued, so read the corresponding getter once after subscribing if you also
|
|
436
|
+
need the current value.
|
|
437
|
+
|
|
438
|
+
On the wire these are `AppsonairPush:onNotificationReceived`,
|
|
439
|
+
`AppsonairPush:onNotificationOpened` and so on — both native bridges emit that
|
|
440
|
+
prefix. You only need the raw names if you subscribe through
|
|
441
|
+
`NativeEventEmitter` yourself instead of using the helpers above.
|
|
442
|
+
|
|
443
|
+
## Platform differences
|
|
444
|
+
|
|
445
|
+
Not wrapper bugs — places where the two native SDKs genuinely behave differently
|
|
446
|
+
and no bridge can honestly hide it. Each cites its row in
|
|
447
|
+
`CROSS_PLATFORM_PARITY.md` in the Android SDK repo.
|
|
448
|
+
|
|
449
|
+
| Area | Behaviour |
|
|
450
|
+
|---|---|
|
|
451
|
+
| **`preventDefault()`** (F7) | **Android only.** Android calls foreground listeners on FCM's background thread, so the bridge can wait for your handler. iOS returns presentation options synchronously — the notification is already on screen by the time JS runs. The event fires on both. |
|
|
452
|
+
| **`canRequestPermission()`** (E2) | iOS returns `true` only before the user has ever been asked. **Android returns `true` whenever permission isn't granted, including after a permanent denial.** Don't use it to decide whether to show a pre-prompt cross-platform. |
|
|
453
|
+
| **`user.addEmail()`** (A5) | Android persists emails but never reads them back on restart, so they're lost on relaunch. Needs an SDK fix. |
|
|
454
|
+
| **Badges** (G1, G3) | iOS uses the OS API. Android uses launcher broadcasts that **silently do nothing outside Samsung / MIUI / ASUS**, and `badge.get()` returns the SDK's own persisted count, which can drift from what's on screen. |
|
|
455
|
+
| **`user.getTags()`** | Both are backend-backed, but refresh differently. **Android fetches on every call**, so it sees other devices' writes immediately and can reject on network failure. **iOS reads a local cache** the SDK refreshes on `initialize()`, on first registration, on `login()`, and on every tag write — so a tag set elsewhere mid-session lands on iOS only after one of those. Neither call fetches before the device has registered; both fall back to the local cache. |
|
|
456
|
+
| **`user.getOptedIn()`** | Android reads the stored value from `/subscriptions`; iOS computes the flag it would send, which also requires OS permission. Revoking permission in Settings flips iOS to `false` while Android still reports the server's value. Both differ from `getPushSubscription().optedIn`, which is purely local on both. |
|
|
457
|
+
| **Payload keys** (C1, F1) | `subtitle`, `attachments`, `badgeIncrement`, `campaignId`, `templateId`, `sentAt` are iOS-only; `sound` and `channelId` are Android-only. The wrapper resolves the rest to `null` / `[]`, so the object shape is always the same. |
|
|
458
|
+
| **Device language** (1.5) | Android reads it live; iOS snapshots it once at init, so a mid-session change is stale on iOS. |
|
|
459
|
+
| **Consent flags** (I5) | `consentRequired` / `consentGiven` round-trip but **neither SDK enforces them yet**. Not a compliance control. |
|
|
460
|
+
| **`notifications.remove()`** (F2) | iOS matches the request identifier exactly; Android hashes the id, so two ids could in principle collide. |
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## Troubleshooting
|
|
465
|
+
|
|
466
|
+
**"doesn't seem to be linked"** — rebuild after installing (a Metro restart isn't
|
|
467
|
+
enough), run `npx pod-install`, and check you're not on Expo Go, which can't load
|
|
468
|
+
custom native modules.
|
|
469
|
+
|
|
470
|
+
**iOS token never arrives** — confirm the Push Notifications capability is on the
|
|
471
|
+
target and `AppsonairAppId` is in `Info.plist`. The SDK logs the reason:
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
await AppPushService.debug.setLogLevel('debug'); // before initialize()
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**Android taps do nothing on warm start** — the wrapper registers RN's
|
|
478
|
+
`ActivityEventListener` and forwards `onNewIntent` itself, so this shouldn't
|
|
479
|
+
happen. If it does, confirm your `MainActivity` isn't consuming the intent before
|
|
480
|
+
React Native sees it.
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## Example app
|
|
485
|
+
|
|
486
|
+
`example/` is a full RN app that exercises the public API and reports which
|
|
487
|
+
native path actually loaded, so you can run the same screen against both
|
|
488
|
+
architectures and compare.
|
|
489
|
+
|
|
490
|
+
```sh
|
|
491
|
+
npm install # from the repo root -- installs the example workspace too
|
|
492
|
+
|
|
493
|
+
cd example
|
|
494
|
+
npm run android:new # newArchEnabled=true -> TurboModule
|
|
495
|
+
npm run android:old # newArchEnabled=false -> Bridge
|
|
496
|
+
npm run ios:new # RCT_NEW_ARCH_ENABLED=1 pod install, then run
|
|
497
|
+
npm run ios:old # RCT_NEW_ARCH_ENABLED=0 pod install, then run
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Architecture is a build-time choice on both platforms — a Metro reload won't
|
|
501
|
+
switch it, and Android needs `./gradlew clean` between switches because Gradle
|
|
502
|
+
caches the generated Codegen sources.
|
|
503
|
+
|
|
504
|
+
The example resolves the **published** native SDKs (`0.0.2-alpha`) the same way
|
|
505
|
+
your app does — JitPack on Android, CocoaPods on iOS. Building against sibling
|
|
506
|
+
checkouts of the SDKs instead is opt-in: `-PappsonairLocalSdk=true` on Android,
|
|
507
|
+
`APPSONAIR_LOCAL_SDK=1` on iOS. See `example/README.md`.
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
## Contributing
|
|
512
|
+
|
|
513
|
+
<details>
|
|
514
|
+
<summary><b>How both architectures are supported</b></summary>
|
|
515
|
+
|
|
516
|
+
- **One spec.** `src/NativeAppsonairApppush.ts` is the Codegen spec, and also the
|
|
517
|
+
only method list — the JS layer, both Android modules and the iOS bridge are
|
|
518
|
+
all checked against it.
|
|
519
|
+
- **Android** compiles `src/newarch` or `src/oldarch` depending on
|
|
520
|
+
`newArchEnabled`. Both declare the same class in the same package and differ
|
|
521
|
+
only in their base class (`NativeAppsonairApppushSpec` vs
|
|
522
|
+
`ReactContextBaseJavaModule`); all behaviour lives in the shared
|
|
523
|
+
`AppsonairReactNativeApppushModuleImpl`.
|
|
524
|
+
- **iOS** uses one `RCT_EXPORT_METHOD` body per method. Codegen derives its ObjC
|
|
525
|
+
selectors from the same JS names the macro does, so a single implementation
|
|
526
|
+
satisfies the `NativeAppsonairApppushSpec` protocol on New Arch and registers with
|
|
527
|
+
the bridge on Old. `#ifdef RCT_NEW_ARCH_ENABLED` covers only the adopted
|
|
528
|
+
protocol and `getTurboModule:`.
|
|
529
|
+
|
|
530
|
+
**Adding a method** means changing all four: the spec, both Android modules, and
|
|
531
|
+
the iOS `.mm` + Swift impl. Codegen catches a missing Android override and a
|
|
532
|
+
missing iOS selector at compile time — but the Old Architecture module isn't
|
|
533
|
+
checked, so that one's on you.
|
|
534
|
+
</details>
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
## License
|
|
539
|
+
|
|
540
|
+
MIT
|