pushwoosh-react-native-plugin 6.1.61 → 7.0.1

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 CHANGED
@@ -25,7 +25,8 @@
25
25
 
26
26
  - [Integration Guide](https://docs.pushwoosh.com/platform-docs/pushwoosh-sdk/cross-platform-frameworks/react-native/integrating-react-native-plugin) — step-by-step setup
27
27
  - [API Reference](docs/README.md) — full API documentation
28
- - [Sample Project](https://github.com/Pushwoosh/pushwoosh-react-native-plugin/tree/master/example/demoapp) — ready-to-run demo app
28
+ - [Sample Project](https://github.com/Pushwoosh/pushwoosh-react-native-plugin/tree/master/example/demoapp) — ready-to-run demo app on the current React Native (New Architecture)
29
+ - [Legacy Sample Project](https://github.com/Pushwoosh/pushwoosh-react-native-plugin/tree/master/example/demoapp-legacy) — the same demo on React Native 0.74 with the legacy architecture
29
30
 
30
31
  ## Features
31
32
 
@@ -76,6 +77,29 @@ The wizard will automatically add all required configuration.
76
77
 
77
78
  The Pushwoosh plugin already includes the `firebase-messaging` dependency, so you do not need to add it manually.
78
79
 
80
+ ### React Native versions and the New Architecture
81
+
82
+ The plugin is a Turbo Native Module and also works on the legacy bridge. One package covers both:
83
+
84
+ | React Native | Architecture | How the plugin runs |
85
+ |---|---|---|
86
+ | 0.74 – 0.81, `newArchEnabled=false` (default) | Legacy bridge | Bridge module |
87
+ | 0.74 – 0.81, `newArchEnabled=true` / `RCT_NEW_ARCH_ENABLED=1` | New Architecture | Codegen TurboModule |
88
+ | 0.82 and later | New Architecture only (bridgeless) | Codegen TurboModule |
89
+
90
+ No interop layer is involved on the New Architecture. Events keep their API on both architectures:
91
+
92
+ ```javascript
93
+ import { DeviceEventEmitter } from 'react-native';
94
+
95
+ DeviceEventEmitter.addListener('pushOpened', (push) => { /* ... */ });
96
+ DeviceEventEmitter.addListener('pushReceived', (push) => { /* ... */ });
97
+ ```
98
+
99
+ `presentInboxUI()` presents a native screen (a view controller on iOS, an Activity on Android) outside the React view tree, so it does not depend on Fabric. React Native below 0.74 is supported by plugin versions 6.1.x.
100
+
101
+ Both setups ship as samples with identical screens: `example/demoapp` (React Native 0.87, New Architecture) and `example/demoapp-legacy` (React Native 0.74, legacy architecture).
102
+
79
103
  ## AI-Assisted Integration
80
104
 
81
105
  Integrate the Pushwoosh React Native plugin using AI coding assistants (Claude Code, Cursor, GitHub Copilot, etc.).
package/index.d.ts CHANGED
@@ -87,6 +87,10 @@ declare module 'pushwoosh-react-native-plugin' {
87
87
  isCommunicationEnabled(success: (isEnabled: boolean)=> void): void;
88
88
  setCommunicationEnabled(enabled: boolean, success?: () => void, fail?: (error: Error) => void): void;
89
89
 
90
+ //Rich Media presentation style
91
+ setRichMediaType(type: number): void;
92
+ getRichMediaType(callback: (type: number) => void): void;
93
+ RichMediaStyle: { MODAL: number; LEGACY: number };
90
94
  }
91
95
 
92
96
  const Pushwoosh: Pushwoosh;
package/index.js CHANGED
@@ -201,9 +201,11 @@
201
201
  * @module pushwoosh-react-native-plugin
202
202
  */
203
203
 
204
- import { NativeModules } from 'react-native';
204
+ import NativePushwoosh from './src/specs/NativePushwoosh';
205
205
 
206
- const PushwooshModule = NativeModules.Pushwoosh;
206
+ // Resolved through TurboModuleRegistry: the codegen TurboModule on the New Architecture, the
207
+ // bridge module on the legacy architecture.
208
+ const PushwooshModule = NativePushwoosh;
207
209
 
208
210
  //Constant: RichMediaStyle
209
211
  //Rich Media presentation style constants.
@@ -348,7 +350,7 @@ class PushNotification {
348
350
  if (!fail) {
349
351
  fail = function(error) {};
350
352
  }
351
- PushwooshModule.register(success, fail);
353
+ PushwooshModule.registerForPushNotifications(success, fail);
352
354
  }
353
355
 
354
356
  //Function: unregister
package/package.json CHANGED
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "pushwoosh-react-native-plugin",
3
- "version": "6.1.61",
3
+ "version": "7.0.1",
4
4
  "description": "This plugin allows you to send and receive push notifications. Powered by Pushwoosh (www.pushwoosh.com).",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
+ "codegenConfig": {
8
+ "name": "RNPushwooshSpec",
9
+ "type": "modules",
10
+ "jsSrcsDir": "src/specs",
11
+ "android": {
12
+ "javaPackageName": "com.pushwoosh.reactnativeplugin"
13
+ }
14
+ },
7
15
  "repository": {
8
16
  "type": "git",
9
17
  "url": "git+https://github.com/Pushwoosh/pushwoosh-react-native-plugin.git"
@@ -17,6 +25,9 @@
17
25
  "bugs": {
18
26
  "url": "https://github.com/Pushwoosh/pushwoosh-react-native-plugin/issues"
19
27
  },
28
+ "peerDependencies": {
29
+ "react-native": ">=0.74.0"
30
+ },
20
31
  "rnpm": {
21
32
  "android": {
22
33
  "sourceDir": "./src/android"
@@ -1,20 +1,24 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = "pushwoosh-react-native-plugin"
3
- s.version = "6.1.61"
3
+ s.version = "7.0.1"
4
4
  s.summary = "React Native Pushwoosh Push Notifications module"
5
5
  s.requires_arc = true
6
6
  s.author = 'Pushwoosh'
7
7
  s.license = 'MIT'
8
8
  s.homepage = 'n/a'
9
9
  s.source = { :git => "https://github.com/Pushwoosh/pushwoosh-react-native-plugin.git" }
10
- s.source_files = 'src/ios/PushwooshPlugin/Pushwoosh.{h,m}', 'src/ios/PushwooshPlugin/PWEventDispatcher.{h,m}'
10
+ s.source_files = 'src/ios/PushwooshPlugin/Pushwoosh.{h,mm}', 'src/ios/PushwooshPlugin/PWEventDispatcher.{h,m}'
11
11
  s.platform = :ios, "13.0"
12
- s.xcconfig = {
13
- "HEADER_SEARCH_PATHS" => "${PODS_ROOT}/Headers/Public/React"
14
- }
15
12
  s.static_framework = true
16
13
 
17
- s.dependency 'React'
14
+ # React Native 0.71+ adds React-Core and, when the New Architecture is on, the codegen and
15
+ # TurboModule dependencies together with the RCT_NEW_ARCH_ENABLED define.
16
+ if respond_to?(:install_modules_dependencies, true)
17
+ install_modules_dependencies(s)
18
+ else
19
+ s.dependency "React-Core"
20
+ end
21
+
18
22
  s.dependency 'PushwooshXCFramework', '7.2.6'
19
- s.dependency 'PushwooshInboxUIXCFramework'
23
+ s.dependency 'PushwooshInboxUIXCFramework', '7.0.42'
20
24
  end
@@ -20,28 +20,135 @@ allprojects {
20
20
  }
21
21
  }
22
22
 
23
+ // [major, minor] of the react-native the app resolves, or null when it cannot be found.
24
+ // Every node_modules above the android project is a candidate: the template keeps react-native in
25
+ // the one next door, a monorepo hoists it any number of levels up. The included build of the React
26
+ // Native Gradle Plugin names the node_modules the app really resolves, though only a 0.74-style
27
+ // settings.gradle exposes it - from 0.82 the include sits in pluginManagement and stays private.
28
+ // `node --print` answers the way Metro does and comes last: it needs node on the Gradle PATH,
29
+ // which an IDE launched from the Finder does not have.
30
+ def reactNativeVersion() {
31
+ def candidates = []
32
+ for (def dir = rootProject.projectDir; dir != null; dir = dir.parentFile) {
33
+ candidates += new File(dir, "node_modules/react-native/package.json")
34
+ }
35
+ project.gradle.includedBuilds.each { included ->
36
+ candidates += new File(included.projectDir, "../../react-native/package.json")
37
+ }
38
+ try {
39
+ def resolved = ["node", "--print", "require.resolve('react-native/package.json')"]
40
+ .execute(null, rootProject.projectDir).text.trim()
41
+ if (!resolved.isEmpty()) {
42
+ candidates += new File(resolved)
43
+ }
44
+ } catch (Exception ignored) {
45
+ }
46
+
47
+ for (candidate in candidates) {
48
+ if (!candidate.isFile()) {
49
+ continue
50
+ }
51
+ def version = new groovy.json.JsonSlurper().parseText(candidate.text).version
52
+ def match = version =~ /^(\d+)\.(\d+)\./
53
+ if (match) {
54
+ return [match[0][1].toInteger(), match[0][2].toInteger()]
55
+ }
56
+ }
57
+ return null
58
+ }
59
+
60
+ // What the app asked for, or null when it did not ask. The React Native Gradle Plugin ORs the two
61
+ // spellings, so either one set to true turns the New Architecture on and a plain
62
+ // newArchEnabled=false does not veto a react-scoped true.
63
+ def newArchProperty() {
64
+ def names = ["newArchEnabled", "react.newArchEnabled"]
65
+ if (names.any { project.hasProperty(it) && project.property(it).toString().toBoolean() }) {
66
+ return true
67
+ }
68
+ if (names.any { project.hasProperty(it) }) {
69
+ return false
70
+ }
71
+ return null
72
+ }
73
+
74
+ // The architecture is the app's choice; this answers it the way the React Native Gradle Plugin
75
+ // does. Below 0.82 the app opts in with newArchEnabled; from 0.82 React Native is bridgeless only
76
+ // and ignores the flag. Guessing wrong is silent and lands on the customer: src/oldarch compiled
77
+ // into a bridgeless app leaves TurboModuleRegistry.get() answering null and every plugin call
78
+ // throwing, with a green build. So an undecidable case fails the build instead.
79
+ def resolveNewArchitectureEnabled() {
80
+ def version = reactNativeVersion()
81
+ if (version != null && (version[0] > 0 || version[1] >= 82)) {
82
+ return true
83
+ }
84
+
85
+ def asked = newArchProperty()
86
+ if (asked != null) {
87
+ return asked
88
+ }
89
+
90
+ if (version == null) {
91
+ throw new GradleException(
92
+ "pushwoosh-react-native-plugin: cannot tell which React Native architecture to build. " +
93
+ "No react-native/package.json was found from ${rootProject.projectDir} and neither " +
94
+ "newArchEnabled nor react.newArchEnabled is set. Set newArchEnabled in " +
95
+ "android/gradle.properties to the architecture the app runs.")
96
+ }
97
+ return false
98
+ }
99
+
100
+ // Once per configuration: the lookup walks the file system and may spawn node.
101
+ if (!project.hasProperty("pushwooshNewArchEnabled")) {
102
+ project.ext.pushwooshNewArchEnabled = resolveNewArchitectureEnabled()
103
+ }
104
+ def isNewArchitectureEnabled = project.ext.pushwooshNewArchEnabled
105
+
23
106
  apply plugin: 'com.android.library'
107
+ if (isNewArchitectureEnabled) {
108
+ // Generates NativePushwooshSpec from the spec in ../specs, configured by ../package.json
109
+ apply plugin: 'com.facebook.react'
110
+ }
24
111
 
25
112
  android {
26
113
  compileSdkVersion 34
27
114
  defaultConfig {
28
115
  minSdkVersion 21
29
116
  targetSdkVersion 34
117
+ // What a consuming app must keep to run minified. The library itself is not minified, so
118
+ // proguardFiles here would configure nothing; consumerProguardFiles is what travels.
119
+ consumerProguardFiles 'proguard-rules.pro'
30
120
  }
31
121
  buildTypes {
32
122
  release {
33
123
  minifyEnabled false
34
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
124
+ }
125
+ }
126
+ sourceSets {
127
+ main {
128
+ if (isNewArchitectureEnabled) {
129
+ java.srcDirs += ["src/newarch/java"]
130
+ } else {
131
+ java.srcDirs += ["src/oldarch/java"]
132
+ }
133
+ }
134
+ // src/test runs under both architectures; the per-architecture suites assert what the
135
+ // switch above must have produced
136
+ test {
137
+ if (isNewArchitectureEnabled) {
138
+ java.srcDirs += ["src/testNewarch/java"]
139
+ } else {
140
+ java.srcDirs += ["src/testOldarch/java"]
141
+ }
35
142
  }
36
143
  }
37
144
  }
38
145
 
39
146
  ext {
40
- pushwoosh = "6.12.0"
147
+ pushwoosh = "6.13.0"
41
148
  }
42
149
 
43
150
  dependencies {
44
- implementation 'com.facebook.react:react-native:+'
151
+ implementation "com.facebook.react:react-android"
45
152
  implementation "com.pushwoosh:pushwoosh:${pushwoosh}"
46
153
  implementation "com.pushwoosh:pushwoosh-firebase:${pushwoosh}"
47
154
  implementation "com.pushwoosh:pushwoosh-huawei:${pushwoosh}"
@@ -54,5 +161,10 @@ dependencies {
54
161
  implementation "androidx.activity:activity:1.8.0"
55
162
  implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22"
56
163
  implementation "com.google.firebase:firebase-messaging:23.1.0"
164
+
165
+ // Test-only, run with `make android-test`.
166
+ testImplementation "junit:junit:4.13.2"
167
+ testImplementation "org.robolectric:robolectric:4.14.1"
168
+ testImplementation "org.mockito:mockito-core:5.14.2"
57
169
  }
58
170
 
@@ -15,3 +15,19 @@
15
15
  #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16
16
  # public *;
17
17
  #}
18
+
19
+ # Shipped to the consuming app through consumerProguardFiles.
20
+ #
21
+ # The Pushwoosh SDK stores its requests with WorkManager, which keeps them in a Room database.
22
+ # room-runtime ships `-keep class * extends androidx.room.RoomDatabase`, which holds the generated
23
+ # class but not the constructor Room instantiates it with, and the R8 full mode AGP 8 runs by
24
+ # default drops that constructor. A minified Release app then dies on startup in androidx.startup
25
+ # with "Failed to create an instance of class androidx.work.impl.WorkDatabase".
26
+ -keep class * extends androidx.room.RoomDatabase { <init>(); }
27
+
28
+ # The same shape one layer up: WorkManager instantiates the work request's input merger by name,
29
+ # and work-runtime ships `-keep class * extends androidx.work.InputMerger`, which holds the class
30
+ # but not that constructor. Under R8 full mode every worker the SDK enqueues then dies before it
31
+ # runs with "Could not create Input Merger androidx.work.OverwritingInputMerger", so registering
32
+ # for pushes reports neither a token nor an error - the app just never hears back.
33
+ -keep class * extends androidx.work.InputMerger { <init>(); }
@@ -6,14 +6,13 @@ import android.util.Log;
6
6
  import java.util.ArrayList;
7
7
  import java.util.Iterator;
8
8
 
9
+ import com.facebook.react.bridge.Arguments;
9
10
  import com.facebook.react.bridge.ReadableArray;
10
11
  import com.facebook.react.bridge.ReadableMap;
11
12
  import com.facebook.react.bridge.ReadableMapKeySetIterator;
12
13
  import com.facebook.react.bridge.ReadableType;
13
14
  import com.facebook.react.bridge.WritableArray;
14
15
  import com.facebook.react.bridge.WritableMap;
15
- import com.facebook.react.bridge.WritableNativeArray;
16
- import com.facebook.react.bridge.WritableNativeMap;
17
16
  import com.pushwoosh.inbox.data.InboxMessage;
18
17
  import com.pushwoosh.internal.utils.JsonUtils;
19
18
  import com.pushwoosh.internal.utils.PWLog;
@@ -115,7 +114,7 @@ public final class ConversionUtil {
115
114
  }
116
115
 
117
116
  public static WritableMap toWritableMap(JSONObject jsonObject) {
118
- WritableNativeMap result = new WritableNativeMap();
117
+ WritableMap result = Arguments.createMap();
119
118
 
120
119
  Iterator<String> keys = jsonObject.keys();
121
120
  while (keys.hasNext()) {
@@ -152,7 +151,7 @@ public final class ConversionUtil {
152
151
  }
153
152
 
154
153
  public static WritableArray toWritableArray(JSONArray array) {
155
- WritableNativeArray result = new WritableNativeArray();
154
+ WritableArray result = Arguments.createArray();
156
155
 
157
156
  for (int i = 0; i < array.length(); i++) {
158
157
  try {
@@ -9,6 +9,7 @@ import com.facebook.react.bridge.Callback;
9
9
  import com.facebook.react.bridge.ReactContext;
10
10
  import com.facebook.react.bridge.WritableMap;
11
11
  import com.facebook.react.modules.core.DeviceEventManagerModule;
12
+ import com.pushwoosh.internal.utils.PWLog;
12
13
 
13
14
  public class EventDispatcher {
14
15
 
@@ -37,7 +38,15 @@ public class EventDispatcher {
37
38
 
38
39
  List<Callback> list = subscribers.get(event);
39
40
  for (Callback subscriber : list) {
40
- subscriber.invoke(objects);
41
+ try {
42
+ subscriber.invoke(objects);
43
+ } catch (Exception e) {
44
+ // A callback bound to a runtime that is already gone throws, and React Native
45
+ // throws on a second invocation of a callback as well. The subscribers behind
46
+ // it, and the clear() below, have to happen anyway - otherwise the bad callback
47
+ // stays in the list and takes down every dispatch that follows.
48
+ PWLog.exception(e);
49
+ }
41
50
  }
42
51
 
43
52
  // A native module is supposed to invoke its callback only once!
@@ -1,33 +1,46 @@
1
1
  package com.pushwoosh.reactnativeplugin;
2
2
 
3
+ import androidx.annotation.Nullable;
4
+
5
+ import com.facebook.react.BaseReactPackage;
3
6
  import com.facebook.react.ReactPackage;
4
- import com.facebook.react.bridge.JavaScriptModule;
5
7
  import com.facebook.react.bridge.NativeModule;
6
8
  import com.facebook.react.bridge.ReactApplicationContext;
9
+ import com.facebook.react.module.model.ReactModuleInfo;
10
+ import com.facebook.react.module.model.ReactModuleInfoProvider;
11
+ import com.facebook.react.turbomodule.core.interfaces.TurboModule;
7
12
 
8
- import java.util.ArrayList;
9
- import java.util.Collections;
10
- import java.util.List;
13
+ import java.util.HashMap;
14
+ import java.util.Map;
11
15
 
12
- public class PushwooshPackage implements ReactPackage {
13
- // Deprecated from RN 0.47
14
- public List<Class<? extends JavaScriptModule>> createJSModules() {
15
- return Collections.emptyList();
16
- }
16
+ // BaseReactPackage already implements ReactPackage; it is repeated because the autolinking scan
17
+ // of the React Native 0.74 CLI only recognises "implements ReactPackage" or "extends TurboReactPackage"
18
+ public class PushwooshPackage extends BaseReactPackage implements ReactPackage {
17
19
 
20
+ @Nullable
18
21
  @Override
19
- public List<com.facebook.react.uimanager.ViewManager> createViewManagers(ReactApplicationContext reactContext) {
20
- return Collections.emptyList();
22
+ public NativeModule getModule(String name, ReactApplicationContext reactContext) {
23
+ if (PushwooshPlugin.MODULE_NAME.equals(name)) {
24
+ return new PushwooshPlugin(reactContext);
25
+ }
26
+ return null;
21
27
  }
22
28
 
23
29
  @Override
24
- public List<NativeModule> createNativeModules(
25
- ReactApplicationContext reactContext) {
26
- List<NativeModule> modules = new ArrayList<>();
27
-
28
- modules.add(new PushwooshPlugin(reactContext));
29
-
30
- return modules;
30
+ public ReactModuleInfoProvider getReactModuleInfoProvider() {
31
+ return () -> {
32
+ // On the New Architecture PushwooshPluginSpec extends the codegen spec, which is a TurboModule
33
+ boolean isTurboModule = TurboModule.class.isAssignableFrom(PushwooshPlugin.class);
34
+ Map<String, ReactModuleInfo> modules = new HashMap<>();
35
+ modules.put(PushwooshPlugin.MODULE_NAME, new ReactModuleInfo(
36
+ PushwooshPlugin.MODULE_NAME,
37
+ PushwooshPlugin.class.getName(),
38
+ false, // canOverrideExistingModule
39
+ false, // needsEagerInit: the launch push is cached until JS calls init
40
+ false, // isCxxModule
41
+ isTurboModule
42
+ ));
43
+ return modules;
44
+ };
31
45
  }
32
46
  }
33
-
@@ -8,14 +8,14 @@ import android.graphics.Color;
8
8
  import androidx.annotation.NonNull;
9
9
  import androidx.annotation.Nullable;
10
10
 
11
+ import com.facebook.react.bridge.Arguments;
11
12
  import com.facebook.react.bridge.Callback;
12
13
  import com.facebook.react.bridge.LifecycleEventListener;
13
14
  import com.facebook.react.bridge.ReactApplicationContext;
14
- import com.facebook.react.bridge.ReactContextBaseJavaModule;
15
15
  import com.facebook.react.bridge.ReactMethod;
16
16
  import com.facebook.react.bridge.ReadableArray;
17
17
  import com.facebook.react.bridge.ReadableMap;
18
- import com.facebook.react.bridge.WritableNativeArray;
18
+ import com.facebook.react.bridge.WritableArray;
19
19
  import com.pushwoosh.Pushwoosh;
20
20
  import com.pushwoosh.RegisterForPushNotificationsResultData;
21
21
  import com.pushwoosh.badge.PushwooshBadge;
@@ -50,26 +50,50 @@ import java.util.Collection;
50
50
  import java.util.HashMap;
51
51
  import java.util.Map;
52
52
 
53
- public class PushwooshPlugin extends ReactContextBaseJavaModule implements LifecycleEventListener {
53
+ public class PushwooshPlugin extends PushwooshPluginSpec implements LifecycleEventListener {
54
54
 
55
55
  static final String TAG = "ReactNativePlugin";
56
+ // Same as the iOS class name: React Native resolves a TurboModule by class name first
57
+ public static final String MODULE_NAME = "PushwooshPlugin";
56
58
  private static final String PUSH_OPEN_EVENT = "PwPushOpened";
57
59
  private static final String PUSH_OPEN_JS_EVENT = "pushOpened";
58
60
 
59
61
  private static final String PUSH_RECEIVED_EVENT = Pushwoosh.PUSH_RECEIVE_EVENT;
60
62
  private static final String PUSH_RECEIVED_JS_EVENT = "pushReceived";
61
63
 
62
- private static EventDispatcher mEventDispatcher = new EventDispatcher();
63
-
64
- private static String sReceivedPushData;
65
- private static boolean sReceivedPushCallbackRegistered = false;
66
-
67
- private static String sStartPushData;
68
- private static boolean sPushCallbackRegistered = false;
69
-
70
- private static boolean sInitialized = false;
64
+ // Per bundle. The callbacks belong to the JS runtime that subscribed them and "JS has called
65
+ // init()" is true of that runtime alone, so this state goes away with the module React Native
66
+ // tears down - a late teardown has nothing left to reach into the next module with.
67
+ private final EventDispatcher mEventDispatcher = new EventDispatcher();
68
+ private boolean mInitialized = false;
69
+ private boolean mPushCallbackRegistered = false;
70
+ private boolean mReceivedPushCallbackRegistered = false;
71
+
72
+ // Per process. A push handed over before JS could take it outlives the React instance it
73
+ // arrived at: the next init() replays it.
74
+ private static PendingPush sReceivedPush;
75
+ private static PendingPush sStartPush;
71
76
  private static final Object sStartPushLock = new Object();
72
77
 
78
+ // A push and what has already been done with it. Both paths to JS are replayed to a bundle
79
+ // that comes later - the device event by init(), the deprecated callback by onPushOpen() /
80
+ // onPushReceived() - and each of them only until the push has gone that way once. Without
81
+ // that, every new React instance was handed the last push of the process again: a pushOpened
82
+ // re-routing a deep link the app had already handled, and a launch push taking the place of
83
+ // the one-shot callback registration the new bundle was making.
84
+ private static final class PendingPush {
85
+ final String data;
86
+ boolean sentAsEvent;
87
+ boolean sentToCallback;
88
+
89
+ PendingPush(String data) {
90
+ this.data = data;
91
+ }
92
+ }
93
+
94
+ // The module JS is talking to, published on the first call from JS rather than in the
95
+ // constructor: TurboModuleManager.invalidate() constructs the module JS never required, and
96
+ // such a stillborn one must not take the live module's place.
73
97
  private static PushwooshPlugin INSTANCE = null;
74
98
 
75
99
  private InboxUiStyleManager inboxUiInboxUiStyleManager;
@@ -77,8 +101,6 @@ public class PushwooshPlugin extends ReactContextBaseJavaModule implements Lifec
77
101
  public PushwooshPlugin(ReactApplicationContext reactContext) {
78
102
  super(reactContext);
79
103
 
80
- INSTANCE = this;
81
-
82
104
  reactContext.addLifecycleEventListener(this);
83
105
 
84
106
  Context applicationContext = AndroidPlatformModule.getApplicationContext();
@@ -91,7 +113,7 @@ public class PushwooshPlugin extends ReactContextBaseJavaModule implements Lifec
91
113
 
92
114
  @Override
93
115
  public String getName() {
94
- return "Pushwoosh";
116
+ return MODULE_NAME;
95
117
  }
96
118
 
97
119
  @ReactMethod
@@ -120,16 +142,22 @@ public class PushwooshPlugin extends ReactContextBaseJavaModule implements Lifec
120
142
  Pushwoosh.getInstance().setAppId(appId);
121
143
 
122
144
  synchronized (sStartPushLock) {
123
- if (sReceivedPushData != null) {
124
- sendEvent(PUSH_RECEIVED_JS_EVENT, ConversionUtil.stringToJSONObject(sReceivedPushData));
145
+ if (sReceivedPush != null && !sReceivedPush.sentAsEvent) {
146
+ sendEvent(PUSH_RECEIVED_JS_EVENT, ConversionUtil.stringToJSONObject(sReceivedPush.data));
147
+ sReceivedPush.sentAsEvent = true;
125
148
  }
126
149
 
127
- if (sStartPushData != null) {
128
- sendEvent(PUSH_OPEN_JS_EVENT, ConversionUtil.stringToJSONObject(sStartPushData));
150
+ if (sStartPush != null && !sStartPush.sentAsEvent) {
151
+ sendEvent(PUSH_OPEN_JS_EVENT, ConversionUtil.stringToJSONObject(sStartPush.data));
152
+ sStartPush.sentAsEvent = true;
129
153
  }
130
- }
131
154
 
132
- sInitialized = true;
155
+ // Under the same lock as the replay above: a push taking the lock between the replay
156
+ // and this flag would find the gate closed, skip its own send, and never be replayed
157
+ // either.
158
+ mInitialized = true;
159
+ INSTANCE = this;
160
+ }
133
161
 
134
162
  if (success != null) {
135
163
  success.invoke();
@@ -137,7 +165,7 @@ public class PushwooshPlugin extends ReactContextBaseJavaModule implements Lifec
137
165
  }
138
166
 
139
167
  @ReactMethod
140
- public void register(final Callback success, final Callback error) {
168
+ public void registerForPushNotifications(final Callback success, final Callback error) {
141
169
  Pushwoosh.getInstance().registerForPushNotifications(new RegisterForPushNotificationCallback(success, error));
142
170
  }
143
171
 
@@ -162,13 +190,16 @@ public class PushwooshPlugin extends ReactContextBaseJavaModule implements Lifec
162
190
  @ReactMethod
163
191
  public void onPushOpen(Callback callback) {
164
192
  synchronized (sStartPushLock) {
165
- if (!sPushCallbackRegistered && sStartPushData != null) {
166
- callback.invoke(ConversionUtil.toWritableMap(ConversionUtil.stringToJSONObject(sStartPushData)));
167
- sPushCallbackRegistered = true;
193
+ INSTANCE = this;
194
+
195
+ if (!mPushCallbackRegistered && sStartPush != null && !sStartPush.sentToCallback) {
196
+ callback.invoke(ConversionUtil.toWritableMap(ConversionUtil.stringToJSONObject(sStartPush.data)));
197
+ sStartPush.sentToCallback = true;
198
+ mPushCallbackRegistered = true;
168
199
  return;
169
200
  }
170
201
 
171
- sPushCallbackRegistered = true;
202
+ mPushCallbackRegistered = true;
172
203
  mEventDispatcher.subscribe(PUSH_OPEN_EVENT, callback);
173
204
  }
174
205
  }
@@ -176,13 +207,16 @@ public class PushwooshPlugin extends ReactContextBaseJavaModule implements Lifec
176
207
  @ReactMethod
177
208
  public void onPushReceived(Callback callback) {
178
209
  synchronized (sStartPushLock) {
179
- if (!sReceivedPushCallbackRegistered && sReceivedPushData != null) {
180
- callback.invoke(ConversionUtil.toWritableMap(ConversionUtil.stringToJSONObject(sReceivedPushData)));
181
- sReceivedPushCallbackRegistered = true;
210
+ INSTANCE = this;
211
+
212
+ if (!mReceivedPushCallbackRegistered && sReceivedPush != null && !sReceivedPush.sentToCallback) {
213
+ callback.invoke(ConversionUtil.toWritableMap(ConversionUtil.stringToJSONObject(sReceivedPush.data)));
214
+ sReceivedPush.sentToCallback = true;
215
+ mReceivedPushCallbackRegistered = true;
182
216
  return;
183
217
  }
184
218
 
185
- sReceivedPushCallbackRegistered = true;
219
+ mReceivedPushCallbackRegistered = true;
186
220
  mEventDispatcher.subscribe(PUSH_RECEIVED_EVENT, callback);
187
221
  }
188
222
  }
@@ -193,7 +227,7 @@ public class PushwooshPlugin extends ReactContextBaseJavaModule implements Lifec
193
227
  }
194
228
 
195
229
  @ReactMethod
196
- public void removeListeners(Integer count) {
230
+ public void removeListeners(double count) {
197
231
  // Required for NativeEventEmitter. No need to do anything here.
198
232
  }
199
233
 
@@ -313,22 +347,22 @@ public class PushwooshPlugin extends ReactContextBaseJavaModule implements Lifec
313
347
 
314
348
  JSONObject params = ConversionUtil.toJsonObject(data);
315
349
 
350
+ // optString answers "" for a missing key, never null: without a message there is nothing
351
+ // to show, and empty user data has no business reaching the payload.
316
352
  String message = params.optString("msg");
317
- if (message == null){
353
+ if (message.isEmpty()){
354
+ PWLog.error(TAG, "createLocalNotification: msg is required");
318
355
  return;
319
356
  }
320
357
  int seconds = params.optInt("seconds");
321
358
  Bundle extras = new Bundle();
322
359
  String userData = params.optString("userData");
323
- if (userData!=null){
360
+ if (!userData.isEmpty()){
324
361
  extras.putString("u", userData);
325
362
  }
326
363
 
327
- LocalNotification.Builder builder = new LocalNotification.Builder();
328
- if (extras != null){
329
- builder.setExtras(extras);
330
- }
331
- LocalNotification notification = builder
364
+ LocalNotification notification = new LocalNotification.Builder()
365
+ .setExtras(extras)
332
366
  .setMessage(message)
333
367
  .setDelay(seconds)
334
368
  .build();
@@ -351,8 +385,8 @@ public class PushwooshPlugin extends ReactContextBaseJavaModule implements Lifec
351
385
  }
352
386
 
353
387
  @ReactMethod
354
- public void setApplicationIconBadgeNumber(int badgeNumber) {
355
- PushwooshBadge.setBadgeNumber(badgeNumber);
388
+ public void setApplicationIconBadgeNumber(double badgeNumber) {
389
+ PushwooshBadge.setBadgeNumber((int) badgeNumber);
356
390
  }
357
391
 
358
392
  @ReactMethod
@@ -361,8 +395,8 @@ public class PushwooshPlugin extends ReactContextBaseJavaModule implements Lifec
361
395
  }
362
396
 
363
397
  @ReactMethod
364
- public void addToApplicationIconBadgeNumber(int badgeNumber) {
365
- PushwooshBadge.addBadgeNumber(badgeNumber);
398
+ public void addToApplicationIconBadgeNumber(double badgeNumber) {
399
+ PushwooshBadge.addBadgeNumber((int) badgeNumber);
366
400
  }
367
401
 
368
402
  @ReactMethod
@@ -381,18 +415,22 @@ public class PushwooshPlugin extends ReactContextBaseJavaModule implements Lifec
381
415
  }
382
416
 
383
417
  @ReactMethod
384
- public void setColorLED(int color) {
385
- PushwooshNotificationSettings.setColorLED(color);
418
+ public void setColorLED(double color) {
419
+ // The spec types a colour as a JS number, so it arrives as a double, and an opaque ARGB
420
+ // value does not fit a signed int: 0xFFFF0000 is 4294901760. A direct double -> int
421
+ // narrowing clamps that to Integer.MAX_VALUE, while going through long keeps the bits the
422
+ // SDK expects (-65536).
423
+ PushwooshNotificationSettings.setColorLED((int) (long) color);
386
424
  }
387
425
 
388
426
  @ReactMethod
389
- public void setSoundType(int type) {
390
- PushwooshNotificationSettings.setSoundNotificationType(SoundType.fromInt(type));
427
+ public void setSoundType(double type) {
428
+ PushwooshNotificationSettings.setSoundNotificationType(SoundType.fromInt((int) type));
391
429
  }
392
430
 
393
431
  @ReactMethod
394
- public void setVibrateType(int type) {
395
- PushwooshNotificationSettings.setVibrateNotificationType(VibrateType.fromInt(type));
432
+ public void setVibrateType(double type) {
433
+ PushwooshNotificationSettings.setVibrateNotificationType(VibrateType.fromInt((int) type));
396
434
  }
397
435
 
398
436
 
@@ -455,7 +493,7 @@ public class PushwooshPlugin extends ReactContextBaseJavaModule implements Lifec
455
493
  try {
456
494
  if (result.isSuccess() && result.getData() != null) {
457
495
  ArrayList<InboxMessage> messagesList = new ArrayList<>(result.getData());
458
- WritableNativeArray writableArray = new WritableNativeArray();
496
+ WritableArray writableArray = Arguments.createArray();
459
497
  for (InboxMessage message : messagesList) {
460
498
  writableArray.pushMap(ConversionUtil.inboxMessageToWritableMap(message));
461
499
  }
@@ -551,8 +589,8 @@ public class PushwooshPlugin extends ReactContextBaseJavaModule implements Lifec
551
589
  }
552
590
 
553
591
  @ReactMethod
554
- public void setRichMediaType(int type) {
555
- RichMediaType richMediaType = type == 0 ? RichMediaType.MODAL : RichMediaType.DEFAULT;
592
+ public void setRichMediaType(double type) {
593
+ RichMediaType richMediaType = (int) type == 0 ? RichMediaType.MODAL : RichMediaType.DEFAULT;
556
594
  RichMediaManager.setRichMediaType(richMediaType);
557
595
  }
558
596
 
@@ -562,6 +600,17 @@ public class PushwooshPlugin extends ReactContextBaseJavaModule implements Lifec
562
600
  callback.invoke(type.ordinal());
563
601
  }
564
602
 
603
+ // iOS-only setting. Android shows a notification for a push that arrives in the foreground,
604
+ // which is what the iOS default reports; the methods exist so the spec is fully implemented.
605
+ @ReactMethod
606
+ public void setShowPushnotificationAlert(boolean showPushnotificationAlert) {
607
+ }
608
+
609
+ @ReactMethod
610
+ public void getShowPushnotificationAlert(Callback callback) {
611
+ callback.invoke(true);
612
+ }
613
+
565
614
  ///
566
615
  /// LifecycleEventListener callbacks
567
616
  ///
@@ -580,11 +629,33 @@ public class PushwooshPlugin extends ReactContextBaseJavaModule implements Lifec
580
629
  public void onHostDestroy() {
581
630
  PWLog.noise(TAG, "Host destroyed");
582
631
 
583
- sPushCallbackRegistered = false;
584
- sStartPushData = null;
632
+ // Nothing is reset here: the host going down is the activity, not the JS runtime. React
633
+ // Native keeps the context and the JS bundle across activities, JS does not call init() or
634
+ // onPushOpen() a second time, and a push opened after the user left by Back has to reach
635
+ // that bundle. Everything JS set up is reset in invalidate(), where the runtime ends.
636
+ }
637
+
638
+ // React Native tears the module down with its React instance (legacy ModuleHolder.destroy(),
639
+ // TurboModuleManager.invalidate()). Everything JS set up lives on this module and goes with it;
640
+ // only the module's place as the live one has to be given up here. The cached push stays - the
641
+ // next init() replays it.
642
+ @Override
643
+ public void invalidate() {
644
+ super.invalidate();
645
+ PWLog.noise(TAG, "Module invalidated");
646
+
647
+ getReactApplicationContext().removeLifecycleEventListener(this);
585
648
 
586
- sReceivedPushCallbackRegistered = false;
587
- sReceivedPushData = null;
649
+ // A push can be delivered on the FCM thread while the runtime is going down, and every
650
+ // other accessor of the field holds the lock.
651
+ synchronized (sStartPushLock) {
652
+ // Only if this module is still the live one. The next module publishes itself on the
653
+ // first call from JS and the order of the two events is not guaranteed, so a late
654
+ // teardown must not unseat a module that has already taken over.
655
+ if (INSTANCE == this) {
656
+ INSTANCE = null;
657
+ }
658
+ }
588
659
  }
589
660
 
590
661
  ///
@@ -596,12 +667,21 @@ public class PushwooshPlugin extends ReactContextBaseJavaModule implements Lifec
596
667
 
597
668
  try {
598
669
  synchronized (sStartPushLock) {
599
- sStartPushData = pushData;
600
- if (sPushCallbackRegistered) {
601
- mEventDispatcher.dispatchEvent(PUSH_OPEN_EVENT, ConversionUtil.toWritableMap(ConversionUtil.stringToJSONObject(pushData)));
670
+ PendingPush push = new PendingPush(pushData);
671
+ sStartPush = push;
672
+
673
+ PushwooshPlugin plugin = INSTANCE;
674
+ if (plugin == null || !plugin.hasLiveRuntime()) {
675
+ return;
676
+ }
677
+
678
+ if (plugin.mPushCallbackRegistered) {
679
+ plugin.mEventDispatcher.dispatchEvent(PUSH_OPEN_EVENT, ConversionUtil.toWritableMap(ConversionUtil.stringToJSONObject(pushData)));
680
+ push.sentToCallback = true;
602
681
  }
603
- if (sInitialized && INSTANCE != null) {
604
- INSTANCE.sendEvent(PUSH_OPEN_JS_EVENT, ConversionUtil.stringToJSONObject(pushData));
682
+ if (plugin.mInitialized) {
683
+ plugin.sendEvent(PUSH_OPEN_JS_EVENT, ConversionUtil.stringToJSONObject(pushData));
684
+ push.sentAsEvent = true;
605
685
  }
606
686
  }
607
687
  } catch (Exception e) {
@@ -614,17 +694,33 @@ public class PushwooshPlugin extends ReactContextBaseJavaModule implements Lifec
614
694
  mEventDispatcher.sendJSEvent(getReactApplicationContext(), event, ConversionUtil.toWritableMap(params));
615
695
  }
616
696
 
697
+ // The React instance behind the module is still running. One on its way down is not: emitting
698
+ // into it throws, the catch around the callers swallows the failure, and the push is gone.
699
+ // Held back, it stays cached for the next init(). Callers hold sStartPushLock.
700
+ private boolean hasLiveRuntime() {
701
+ return getReactApplicationContext().hasActiveReactInstance();
702
+ }
703
+
617
704
  static void messageReceived(String pushData) {
618
705
  PWLog.info(TAG, "Push received: " + pushData);
619
706
 
620
707
  try {
621
708
  synchronized (sStartPushLock) {
622
- sReceivedPushData = pushData;
623
- if (sReceivedPushCallbackRegistered) {
624
- mEventDispatcher.dispatchEvent(PUSH_RECEIVED_EVENT, ConversionUtil.toWritableMap(ConversionUtil.stringToJSONObject(pushData)));
709
+ PendingPush push = new PendingPush(pushData);
710
+ sReceivedPush = push;
711
+
712
+ PushwooshPlugin plugin = INSTANCE;
713
+ if (plugin == null || !plugin.hasLiveRuntime()) {
714
+ return;
715
+ }
716
+
717
+ if (plugin.mReceivedPushCallbackRegistered) {
718
+ plugin.mEventDispatcher.dispatchEvent(PUSH_RECEIVED_EVENT, ConversionUtil.toWritableMap(ConversionUtil.stringToJSONObject(pushData)));
719
+ push.sentToCallback = true;
625
720
  }
626
- if (sInitialized && INSTANCE != null) {
627
- INSTANCE.sendEvent(PUSH_RECEIVED_JS_EVENT, ConversionUtil.stringToJSONObject(pushData));
721
+ if (plugin.mInitialized) {
722
+ plugin.sendEvent(PUSH_RECEIVED_JS_EVENT, ConversionUtil.stringToJSONObject(pushData));
723
+ push.sentAsEvent = true;
628
724
  }
629
725
  }
630
726
  } catch (Exception e) {
@@ -0,0 +1,10 @@
1
+ package com.pushwoosh.reactnativeplugin;
2
+
3
+ import com.facebook.react.bridge.ReactApplicationContext;
4
+
5
+ // New Architecture: NativePushwooshSpec is generated by codegen from src/specs/NativePushwoosh.ts.
6
+ abstract class PushwooshPluginSpec extends NativePushwooshSpec {
7
+ PushwooshPluginSpec(ReactApplicationContext reactContext) {
8
+ super(reactContext);
9
+ }
10
+ }
@@ -0,0 +1,11 @@
1
+ package com.pushwoosh.reactnativeplugin;
2
+
3
+ import com.facebook.react.bridge.ReactApplicationContext;
4
+ import com.facebook.react.bridge.ReactContextBaseJavaModule;
5
+
6
+ // Legacy architecture: a plain bridge module whose methods are exported by @ReactMethod.
7
+ abstract class PushwooshPluginSpec extends ReactContextBaseJavaModule {
8
+ PushwooshPluginSpec(ReactApplicationContext reactContext) {
9
+ super(reactContext);
10
+ }
11
+ }
@@ -24,6 +24,12 @@
24
24
  #import "PushwooshInboxUI.h"
25
25
  #endif
26
26
 
27
+ #ifdef RCT_NEW_ARCH_ENABLED
28
+ #import <RNPushwooshSpec/RNPushwooshSpec.h>
29
+
30
+ @interface PushwooshPlugin: RCTEventEmitter<NativePushwooshSpec, PushNotificationDelegate>
31
+ #else
27
32
  @interface PushwooshPlugin: RCTEventEmitter<RCTBridgeModule, PushNotificationDelegate>
33
+ #endif
28
34
 
29
35
  @end
@@ -1,5 +1,5 @@
1
1
  //
2
- // Pushwoosh.m
2
+ // Pushwoosh.mm
3
3
  // Pushwoosh React Native Plugin
4
4
  // (c) Pushwoosh 2016
5
5
  //
@@ -69,6 +69,7 @@ static PWEmailsCompletion emailsCompletionHandler(NSUInteger addressCount, RCTRe
69
69
  }
70
70
 
71
71
  static NSDictionary * gStartPushData = nil;
72
+ static BOOL gStartPushReplayed = NO; // Launch push already replayed into the JS events by init:
72
73
  static NSURL * gPushDeepLinkURL = nil; // Deep link URL for New Architecture support
73
74
  static NSString * gEarlyHandledHash = nil; // Last push hash the cold-start delegate accepted, to avoid double handling
74
75
  static NSString * const kRegistrationSuccesEvent = @"PWRegistrationSuccess";
@@ -144,17 +145,17 @@ static void pwplugin_getInitialURL(id self, SEL _cmd, RCTPromiseResolveBlock res
144
145
  }
145
146
  }
146
147
 
147
- void pushwoosh_swizzle(Class class, SEL fromChange, SEL toChange, IMP impl, const char * signature) {
148
+ void pushwoosh_swizzle(Class cls, SEL fromChange, SEL toChange, IMP impl, const char * signature) {
148
149
  Method method = nil;
149
- method = class_getInstanceMethod(class, fromChange);
150
+ method = class_getInstanceMethod(cls, fromChange);
150
151
 
151
152
  if (method) {
152
153
  //method exists add a new method and swap with original
153
- class_addMethod(class, toChange, impl, signature);
154
- method_exchangeImplementations(class_getInstanceMethod(class, fromChange), class_getInstanceMethod(class, toChange));
154
+ class_addMethod(cls, toChange, impl, signature);
155
+ method_exchangeImplementations(class_getInstanceMethod(cls, fromChange), class_getInstanceMethod(cls, toChange));
155
156
  } else {
156
157
  //just add as orignal method
157
- class_addMethod(class, fromChange, impl, signature);
158
+ class_addMethod(cls, fromChange, impl, signature);
158
159
  }
159
160
  }
160
161
 
@@ -173,7 +174,15 @@ void pushwoosh_swizzle(Class class, SEL fromChange, SEL toChange, IMP impl, cons
173
174
 
174
175
  #pragma mark - Pushwoosh RCTBridgeModule
175
176
 
176
- RCT_EXPORT_MODULE(Pushwoosh);
177
+ // No custom name: React Native looks a TurboModule up by NSClassFromString(name) before it
178
+ // consults the RCT_EXPORT_MODULE registry, and "Pushwoosh" is the native SDK's own class.
179
+ RCT_EXPORT_MODULE();
180
+
181
+ #ifdef RCT_NEW_ARCH_ENABLED
182
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
183
+ return std::make_shared<facebook::react::NativePushwooshSpecJSI>(params);
184
+ }
185
+ #endif
177
186
 
178
187
  - (dispatch_queue_t)methodQueue {
179
188
  return dispatch_get_main_queue();
@@ -250,32 +259,43 @@ RCT_EXPORT_METHOD(init:(NSDictionary*)config success:(RCTResponseSenderBlock)suc
250
259
  success(@[]);
251
260
  }
252
261
 
253
- if (gStartPushData) {
254
- NSURL *deepLink = pwplugin_deepLinkURLFromPushLink(gStartPushData[@"l"]);
262
+ // Once per launch push. Neither source clears itself here - gStartPushData is consumed by the
263
+ // legacy onPushOpen:, launchNotification belongs to the SDK - so a second init() (a consent
264
+ // flow initialising after the user agreed, an app switching application code, a Fast Refresh
265
+ // reload) used to replay the same push as pushReceived and pushOpened again and to re-arm a
266
+ // deep link that Linking.getInitialURL() had already consumed, sending the user back to the
267
+ // screen the push opened the first time.
268
+ if (!gStartPushReplayed) {
269
+ gStartPushReplayed = YES;
255
270
 
256
- if (deepLink) {
257
- if (!gPushDeepLinkURL) {
258
- gPushDeepLinkURL = deepLink;
259
- }
271
+ if (gStartPushData) {
272
+ NSURL *deepLink = pwplugin_deepLinkURLFromPushLink(gStartPushData[@"l"]);
260
273
 
261
- // Writing the link into launchOptions is what makes RCTLinking report it, but only a
262
- // real bridge accepts the write. Bridgeless reads it from the swizzled getInitialURL.
263
- if (pwplugin_isRealBridge(self.bridge)) {
264
- NSMutableDictionary *launchOptions = self.bridge.launchOptions.mutableCopy;
265
- launchOptions[UIApplicationLaunchOptionsURLKey] = deepLink;
266
- [self.bridge setValue:launchOptions forKey:@"launchOptions"];
274
+ if (deepLink) {
275
+ if (!gPushDeepLinkURL) {
276
+ gPushDeepLinkURL = deepLink;
277
+ }
278
+
279
+ // Writing the link into launchOptions is what makes RCTLinking report it, but only
280
+ // a real bridge accepts the write. Bridgeless reads it from the swizzled
281
+ // getInitialURL.
282
+ if (pwplugin_isRealBridge(self.bridge)) {
283
+ NSMutableDictionary *launchOptions = self.bridge.launchOptions.mutableCopy;
284
+ launchOptions[UIApplicationLaunchOptionsURLKey] = deepLink;
285
+ [self.bridge setValue:launchOptions forKey:@"launchOptions"];
286
+ }
267
287
  }
288
+
289
+ [self sendJSEvent:kPushReceivedJSEvent withArgs:gStartPushData];
290
+ [self sendJSEvent:kPushOpenJSEvent withArgs:gStartPushData];
291
+ } else if([PushNotificationManager pushManager].launchNotification) {
292
+ [self sendJSEvent:kPushReceivedJSEvent withArgs:[PushNotificationManager pushManager].launchNotification];
293
+ [self sendJSEvent:kPushOpenJSEvent withArgs:[PushNotificationManager pushManager].launchNotification];
268
294
  }
269
-
270
- [self sendJSEvent:kPushReceivedJSEvent withArgs:gStartPushData];
271
- [self sendJSEvent:kPushOpenJSEvent withArgs:gStartPushData];
272
- } else if([PushNotificationManager pushManager].launchNotification) {
273
- [self sendJSEvent:kPushReceivedJSEvent withArgs:[PushNotificationManager pushManager].launchNotification];
274
- [self sendJSEvent:kPushOpenJSEvent withArgs:[PushNotificationManager pushManager].launchNotification];
275
295
  }
276
296
  }
277
297
 
278
- RCT_EXPORT_METHOD(register:(RCTResponseSenderBlock)success error:(RCTResponseSenderBlock)error) {
298
+ RCT_EXPORT_METHOD(registerForPushNotifications:(RCTResponseSenderBlock)success error:(RCTResponseSenderBlock)error) {
279
299
  [[PWEventDispatcher sharedDispatcher] subscribe:success toEvent:kRegistrationSuccesEvent];
280
300
  [[PWEventDispatcher sharedDispatcher] subscribe:error toEvent:kRegistrationErrorEvent];
281
301
 
@@ -410,13 +430,13 @@ RCT_EXPORT_METHOD(setUserId:(NSString*)userId success:(RCTResponseSenderBlock)su
410
430
  }];
411
431
  }
412
432
 
413
- RCT_EXPORT_METHOD(postEvent:(NSString*)event withAttributes:(NSDictionary*)attributes) {
433
+ RCT_EXPORT_METHOD(postEvent:(NSString*)event attributes:(NSDictionary*)attributes) {
414
434
  [[PWInAppManager sharedManager] postEvent:event withAttributes:attributes];
415
435
  }
416
436
 
417
- RCT_EXPORT_METHOD(setApplicationIconBadgeNumber:(nonnull NSNumber*)badgeNumber) {
437
+ RCT_EXPORT_METHOD(setApplicationIconBadgeNumber:(double)badgeNumber) {
418
438
  dispatch_async(dispatch_get_main_queue(), ^{
419
- [UIApplication sharedApplication].applicationIconBadgeNumber = [badgeNumber integerValue];
439
+ [UIApplication sharedApplication].applicationIconBadgeNumber = (NSInteger)badgeNumber;
420
440
  });
421
441
  }
422
442
 
@@ -432,9 +452,9 @@ RCT_EXPORT_METHOD(getApplicationIconBadgeNumber:(RCTResponseSenderBlock)callback
432
452
  }
433
453
  }
434
454
 
435
- RCT_EXPORT_METHOD(addToApplicationIconBadgeNumber:(nonnull NSNumber*)badgeNumber) {
455
+ RCT_EXPORT_METHOD(addToApplicationIconBadgeNumber:(double)badgeNumber) {
436
456
  dispatch_async(dispatch_get_main_queue(), ^{
437
- [UIApplication sharedApplication].applicationIconBadgeNumber += [badgeNumber integerValue];
457
+ [UIApplication sharedApplication].applicationIconBadgeNumber += (NSInteger)badgeNumber;
438
458
  });
439
459
  }
440
460
 
@@ -448,7 +468,7 @@ RCT_EXPORT_METHOD(presentInboxUI:(NSDictionary *)styleDictionary) {
448
468
  inboxViewController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Close", @"Close") style:UIBarButtonItemStylePlain target:self action:@selector(closeInbox)];
449
469
  [[PushwooshPlugin findRootViewController] presentViewController:[[UINavigationController alloc] initWithRootViewController:inboxViewController] animated:YES completion:nil];
450
470
 
451
- __weak typeof (self) wself = self;
471
+ __weak __typeof__(self) wself = self;
452
472
  inboxViewController.onMessageClickBlock = ^(NSObject<PWInboxMessageProtocol> *message) {
453
473
  if (message.type == PWInboxMessageTypeDeeplink) {
454
474
  [wself closeInbox];
@@ -484,15 +504,19 @@ RCT_EXPORT_METHOD(messagesCount:(RCTResponseSenderBlock)callback) {
484
504
 
485
505
  RCT_EXPORT_METHOD(loadMessages:(RCTResponseSenderBlock)success fail:(RCTResponseSenderBlock)fail) {
486
506
  [PWInbox loadMessagesWithCompletion:^(NSArray<NSObject<PWInboxMessageProtocol> *> *messages, NSError *error) {
507
+ if (error) {
508
+ if (fail) {
509
+ fail(@[ objectOrNull([error localizedDescription]) ]);
510
+ }
511
+ return;
512
+ }
513
+
487
514
  if (success) {
488
- NSMutableArray* array = [[NSMutableArray alloc] init];
489
- for (NSObject<PWInboxMessageProtocol>* message in messages) {
490
- NSDictionary* dict = [self inboxMessageToDictionary:message];
491
- [array addObject:dict];
492
- }
493
- success( @[ array ]);
494
- } else if (error != nil && fail != nil) {
495
- fail(@[ error ]);
515
+ NSMutableArray *array = [[NSMutableArray alloc] init];
516
+ for (NSObject<PWInboxMessageProtocol> *message in messages) {
517
+ [array addObject:[self inboxMessageToDictionary:message]];
518
+ }
519
+ success(@[ array ]);
496
520
  }
497
521
  }];
498
522
  }
@@ -584,25 +608,50 @@ API_AVAILABLE(ios(10.0)) {
584
608
  NSString *currentHash = content.userInfo[@"p"];
585
609
  BOOL isNeedToImplementInPlugin = [[PushwooshPlugin getPluginImplementationInfoPlistKey] boolValue];
586
610
 
611
+ // 0 when this notification is not ours to present: someone else's, a repeat of the same
612
+ // message, or a Pushwoosh push the app asked us not to show an alert for.
613
+ UNNotificationPresentationOptions options = 0;
614
+
587
615
  if (isPushwooshMessage && isNeedToImplementInPlugin && ![lastPushwooshHash isEqualToString:currentHash]) {
588
616
  lastPushwooshHash = currentHash;
589
617
 
590
618
  if ([PushNotificationManager pushManager].showPushnotificationAlert) {
591
- completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionAlert);
619
+ options = UNNotificationPresentationOptionSound | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionAlert;
592
620
  } else {
593
621
  [[PushNotificationManager pushManager] handlePushReceived:content.userInfo];
594
622
  }
595
623
 
596
624
  [self sendJSEvent:kPushReceivedJSEvent withArgs:content.userInfo];
597
625
  }
598
-
626
+
627
+ // UNUserNotificationCenter waits for exactly one answer: a second call is an error it logs,
628
+ // and no call at all leaves the notification unpresented. Both used to be reachable here -
629
+ // our own alert answered and the app's delegate answered again with the same handler, while a
630
+ // notification that is neither ours nor claimed by an app delegate was never answered at all.
631
+ // So whoever answers first decides and the rest is swallowed.
632
+ __block BOOL answered = NO;
633
+ void (^answerOnce)(UNNotificationPresentationOptions) = ^(UNNotificationPresentationOptions presentationOptions) {
634
+ if (answered) {
635
+ return;
636
+ }
637
+ answered = YES;
638
+ completionHandler(presentationOptions);
639
+ };
640
+
641
+ // The SDK's alert goes up straight away, as before.
642
+ if (options != 0) {
643
+ answerOnce(options);
644
+ }
645
+
646
+ // The app's own delegate still gets the callback either way. A Pushwoosh message is handed
647
+ // over with a delay, so the SDK presents its own alert first.
599
648
  if (_originalNotificationCenterDelegate != nil &&
600
649
  _originalNotificationCenterDelegateResponds.willPresentNotification) {
601
650
 
602
651
  dispatch_block_t presentationBlock = ^{
603
652
  [self->_originalNotificationCenterDelegate userNotificationCenter:center
604
653
  willPresentNotification:notification
605
- withCompletionHandler:completionHandler];
654
+ withCompletionHandler:answerOnce];
606
655
  };
607
656
 
608
657
  if (isPushwooshMessage) {
@@ -610,14 +659,10 @@ API_AVAILABLE(ios(10.0)) {
610
659
  } else {
611
660
  presentationBlock();
612
661
  }
662
+ return;
613
663
  }
614
-
615
- if (_originalNotificationCenterDelegate != nil &&
616
- _originalNotificationCenterDelegateResponds.willPresentNotification) {
617
- [_originalNotificationCenterDelegate userNotificationCenter:center
618
- willPresentNotification:notification
619
- withCompletionHandler:completionHandler];
620
- }
664
+
665
+ answerOnce(options);
621
666
  }
622
667
 
623
668
  - (BOOL)isContentAvailablePush:(NSDictionary *)userInfo {
@@ -737,7 +782,8 @@ API_AVAILABLE(ios(10.0)) {
737
782
 
738
783
  - (NSString*)dateToString:(NSDate*)date {
739
784
  NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
740
- [formatter setDateFormat:@"yyyy-MM-dd'T'H:mm:ssZ"];
785
+ [formatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
786
+ [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZ"];
741
787
  return [formatter stringFromDate:date];
742
788
  }
743
789
 
@@ -979,6 +1025,25 @@ RCT_EXPORT_METHOD(enableHuaweiPushNotifications) {
979
1025
  // available in Android only
980
1026
  }
981
1027
 
1028
+ #pragma mark - Android-only settings
1029
+
1030
+ // Declared so the TurboModule spec is fully implemented on iOS: a call from JS is a no-op here
1031
+ // instead of a TypeError. The settings exist only in the Android SDK.
1032
+
1033
+ RCT_EXPORT_METHOD(setMultiNotificationMode:(BOOL)on) {}
1034
+
1035
+ RCT_EXPORT_METHOD(setLightScreenOnNotification:(BOOL)on) {}
1036
+
1037
+ RCT_EXPORT_METHOD(setEnableLED:(BOOL)on) {}
1038
+
1039
+ RCT_EXPORT_METHOD(setColorLED:(double)color) {}
1040
+
1041
+ RCT_EXPORT_METHOD(setSoundType:(double)type) {}
1042
+
1043
+ RCT_EXPORT_METHOD(setVibrateType:(double)type) {}
1044
+
1045
+ RCT_EXPORT_METHOD(setNotificationIconBackgroundColor:(NSString *)color) {}
1046
+
982
1047
  RCT_EXPORT_METHOD(registerSMSNumber:(NSString *)phoneNumber) {
983
1048
  [[Pushwoosh sharedInstance] registerSmsNumber:phoneNumber];
984
1049
  }
@@ -987,8 +1052,8 @@ RCT_EXPORT_METHOD(registerWhatsappNumber:(NSString *)phoneNumber) {
987
1052
  [[Pushwoosh sharedInstance] registerWhatsappNumber:phoneNumber];
988
1053
  }
989
1054
 
990
- RCT_EXPORT_METHOD(setRichMediaType:(nonnull NSNumber *)type) {
991
- PWRichMediaPresentationStyle style = [type integerValue] == 0
1055
+ RCT_EXPORT_METHOD(setRichMediaType:(double)type) {
1056
+ PWRichMediaPresentationStyle style = (NSInteger)type == 0
992
1057
  ? PWRichMediaPresentationStyleModal
993
1058
  : PWRichMediaPresentationStyleLegacy;
994
1059
  [Pushwoosh.media setRichMediaPresentationStyle:style];
@@ -1052,6 +1117,7 @@ RCT_EXPORT_METHOD(getRichMediaType:(RCTResponseSenderBlock)callback) {
1052
1117
  - (void)onPushReceived:(PushNotificationManager *)manager withNotification:(NSDictionary *)pushNotification onStart:(BOOL)onStart {
1053
1118
  if (onStart) {
1054
1119
  gStartPushData = pushNotification;
1120
+ gStartPushReplayed = NO;
1055
1121
  // Save deep link URL for New Architecture (Linking.getInitialURL support)
1056
1122
  NSURL *deepLink = pwplugin_deepLinkURLFromPushLink(pushNotification[@"l"]);
1057
1123
  if (deepLink) {
@@ -1062,6 +1128,7 @@ RCT_EXPORT_METHOD(getRichMediaType:(RCTResponseSenderBlock)callback) {
1062
1128
  - (void)onPushAccepted:(PushNotificationManager *)manager withNotification:(NSDictionary *)pushNotification onStart:(BOOL)onStart {
1063
1129
  if (onStart) {
1064
1130
  gStartPushData = pushNotification;
1131
+ gStartPushReplayed = NO;
1065
1132
  // Save deep link URL for New Architecture (Linking.getInitialURL support)
1066
1133
  NSURL *deepLink = pwplugin_deepLinkURLFromPushLink(pushNotification[@"l"]);
1067
1134
  if (deepLink) {
@@ -0,0 +1,11 @@
1
+ {
2
+ "private": true,
3
+ "codegenConfig": {
4
+ "name": "RNPushwooshSpec",
5
+ "type": "modules",
6
+ "jsSrcsDir": "specs",
7
+ "android": {
8
+ "javaPackageName": "com.pushwoosh.reactnativeplugin"
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,73 @@
1
+ import type {TurboModule} from 'react-native';
2
+ import {TurboModuleRegistry} from 'react-native';
3
+
4
+ // Codegen input. Parameter names become the Objective-C selector labels (init:success:error:)
5
+ // and the Java parameter names, so they must match the native implementations.
6
+ export interface Spec extends TurboModule {
7
+ setReverseProxy(url: string, headers: Object | null): void;
8
+ init(config: Object, success: (result: Object) => void, error: (message: string) => void): void;
9
+ // `register` is a C++ keyword, so codegen cannot emit a method of that name
10
+ registerForPushNotifications(success: (token: string) => void, error: (message: string) => void): void;
11
+ unregister(success: (token: string) => void, error: (message: string) => void): void;
12
+ onPushOpen(callback: (push: Object) => void): void;
13
+ addListener(eventName: string): void;
14
+ removeListeners(count: number): void;
15
+ setEmails(emails: Array<string>, success: () => void, error: (message: string) => void): void;
16
+ setUserEmails(
17
+ userId: string,
18
+ emails: Array<string>,
19
+ success: () => void,
20
+ error: (message: string) => void,
21
+ ): void;
22
+ registerSMSNumber(phoneNumber: string): void;
23
+ registerWhatsappNumber(phoneNumber: string): void;
24
+ setTags(tags: Object, success: () => void, error: (message: string) => void): void;
25
+ getTags(success: (tags: Object) => void, error: (message: string) => void): void;
26
+ setShowPushnotificationAlert(showPushnotificationAlert: boolean): void;
27
+ getShowPushnotificationAlert(callback: (showPushnotificationAlert: boolean) => void): void;
28
+ getPushToken(callback: (token: string) => void): void;
29
+ getHwid(callback: (hwid: string) => void): void;
30
+ getUserId(callback: (userId: string) => void): void;
31
+ setUserId(userId: string, success: () => void, error: (message: string) => void): void;
32
+ postEvent(event: string, attributes: Object): void;
33
+ setApplicationIconBadgeNumber(badgeNumber: number): void;
34
+ getApplicationIconBadgeNumber(callback: (badgeNumber: number) => void): void;
35
+ addToApplicationIconBadgeNumber(badgeNumber: number): void;
36
+ setMultiNotificationMode(on: boolean): void;
37
+ setLightScreenOnNotification(on: boolean): void;
38
+ setEnableLED(on: boolean): void;
39
+ setColorLED(color: number): void;
40
+ setSoundType(type: number): void;
41
+ setVibrateType(type: number): void;
42
+ presentInboxUI(style: Object | null): void;
43
+ messagesWithNoActionPerformedCount(callback: (count: number) => void): void;
44
+ unreadMessagesCount(callback: (count: number) => void): void;
45
+ messagesCount(callback: (count: number) => void): void;
46
+ loadMessages(success: (messages: Array<Object>) => void, fail: (message: string) => void): void;
47
+ readMessage(code: string): void;
48
+ readMessages(codes: Array<string>): void;
49
+ deleteMessage(code: string): void;
50
+ deleteMessages(codes: Array<string>): void;
51
+ performAction(code: string): void;
52
+ isCommunicationEnabled(callback: (enabled: boolean) => void): void;
53
+ setCommunicationEnabled(
54
+ enabled: boolean,
55
+ success: () => void,
56
+ error: (message: string) => void,
57
+ ): void;
58
+ setNotificationIconBackgroundColor(color: string): void;
59
+ setLanguage(language: string): void;
60
+ enableHuaweiPushNotifications(): void;
61
+ setRichMediaType(type: number): void;
62
+ getRichMediaType(callback: (type: number) => void): void;
63
+ createLocalNotification(params: Object): void;
64
+ clearLocalNotification(): void;
65
+ clearNotificationCenter(): void;
66
+ }
67
+
68
+ // `get`, not `getEnforcing`: importing the package on a build without the native module (Jest,
69
+ // an unlinked app) must not throw; a method call does, as it always did.
70
+ //
71
+ // The module is named after its Objective-C class. React Native resolves a TurboModule by
72
+ // NSClassFromString(name) first, and "Pushwoosh" is the native SDK's own class.
73
+ export default TurboModuleRegistry.get<Spec>('PushwooshPlugin');