pushwoosh-react-native-plugin 7.0.0 → 7.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pushwoosh-react-native-plugin",
3
- "version": "7.0.0",
3
+ "version": "7.0.2",
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",
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = "pushwoosh-react-native-plugin"
3
- s.version = "7.0.0"
3
+ s.version = "7.0.2"
4
4
  s.summary = "React Native Pushwoosh Push Notifications module"
5
5
  s.requires_arc = true
6
6
  s.author = 'Pushwoosh'
@@ -19,6 +19,6 @@ Pod::Spec.new do |s|
19
19
  s.dependency "React-Core"
20
20
  end
21
21
 
22
- s.dependency 'PushwooshXCFramework', '7.2.6'
23
- s.dependency 'PushwooshInboxUIXCFramework'
22
+ s.dependency 'PushwooshXCFramework', '7.2.7'
23
+ s.dependency 'PushwooshInboxUIXCFramework', '7.0.42'
24
24
  end
@@ -144,7 +144,7 @@ android {
144
144
  }
145
145
 
146
146
  ext {
147
- pushwoosh = "6.13.0"
147
+ pushwoosh = "6.15.0"
148
148
  }
149
149
 
150
150
  dependencies {
@@ -31,8 +31,12 @@ public final class ConversionUtil {
31
31
  }
32
32
 
33
33
  public static JSONObject toJsonObject(ReadableMap readableMap) {
34
- ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
35
34
  JSONObject result = new JSONObject();
35
+ if (readableMap == null) {
36
+ PWLog.error(PushwooshPlugin.TAG, "Could not convert object: map is null.");
37
+ return result;
38
+ }
39
+ ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
36
40
  while (iterator.hasNextKey()) {
37
41
  String key = iterator.nextKey();
38
42
  ReadableType type = readableMap.getType(key);
@@ -215,6 +219,10 @@ public final class ConversionUtil {
215
219
 
216
220
  public static ArrayList<String> messageCodesArrayToArrayList(ReadableArray readableArray) {
217
221
  ArrayList<String> result = new ArrayList<>();
222
+ if (readableArray == null) {
223
+ PWLog.error(PushwooshPlugin.TAG, "Could not convert message codes: array is null.");
224
+ return result;
225
+ }
218
226
  for (int i = 0; i < readableArray.size(); i++) {
219
227
  ReadableType indexType = readableArray.getType(i);
220
228
  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!
@@ -7,8 +7,10 @@ import android.graphics.drawable.BitmapDrawable;
7
7
  import android.graphics.drawable.Drawable;
8
8
 
9
9
  import com.facebook.react.bridge.ReadableMap;
10
+ import com.facebook.react.bridge.ReadableType;
10
11
  import com.pushwoosh.inbox.ui.PushwooshInboxStyle;
11
12
  import com.pushwoosh.inbox.ui.model.customizing.formatter.InboxDateFormatter;
13
+ import com.pushwoosh.internal.utils.PWLog;
12
14
 
13
15
  import java.io.IOException;
14
16
  import java.net.URL;
@@ -53,22 +55,45 @@ class InboxUiStyleManager {
53
55
  }
54
56
 
55
57
  private void setDateFormat(ReadableMap mapStyle) {
56
- if (!mapStyle.hasKey(DATE_FORMAT_KEY)) {
57
- return;
58
- }
59
- String dateFormat = mapStyle.getString(DATE_FORMAT_KEY);
58
+ String dateFormat = getString(mapStyle, DATE_FORMAT_KEY);
60
59
  if (dateFormat != null && !dateFormat.isEmpty()) {
61
- ReactInboxDateFormatter inboxDateFormatter = new ReactInboxDateFormatter(dateFormat);
62
- PushwooshInboxStyle.INSTANCE.setDateFormatter(inboxDateFormatter);
60
+ try {
61
+ PushwooshInboxStyle.INSTANCE.setDateFormatter(new ReactInboxDateFormatter(dateFormat));
62
+ } catch (IllegalArgumentException e) {
63
+ PWLog.error(PushwooshPlugin.TAG, "Invalid inbox dateFormat pattern: " + dateFormat, e);
64
+ }
63
65
  }
64
66
  }
65
67
 
66
68
  private void setTexts(ReadableMap mapStyle) {
67
69
  PushwooshInboxStyle PWInboxStyle = PushwooshInboxStyle.INSTANCE;
68
- if (mapStyle.hasKey(LIST_ERROR_MESSAGE_KEY))
69
- PWInboxStyle.setListErrorMessage(mapStyle.getString(LIST_ERROR_MESSAGE_KEY));
70
- if (mapStyle.hasKey(LIST_EMPTY_MESSAGE_KEY))
71
- PWInboxStyle.setListEmptyText(mapStyle.getString(LIST_EMPTY_MESSAGE_KEY));
70
+ String listErrorMessage = getString(mapStyle, LIST_ERROR_MESSAGE_KEY);
71
+ if (listErrorMessage != null)
72
+ PWInboxStyle.setListErrorMessage(listErrorMessage);
73
+ String listEmptyMessage = getString(mapStyle, LIST_EMPTY_MESSAGE_KEY);
74
+ if (listEmptyMessage != null)
75
+ PWInboxStyle.setListEmptyText(listEmptyMessage);
76
+ }
77
+
78
+ // A production ReadableNativeMap throws out of presentInboxUI() on a typed read of a
79
+ // mismatched or null value, so the type is checked before the value is taken.
80
+ private boolean hasTypedValue(ReadableMap mapStyle, String key, ReadableType type, String label) {
81
+ if (!mapStyle.hasKey(key)) {
82
+ return false;
83
+ }
84
+ if (mapStyle.getType(key) != type) {
85
+ PWLog.error(PushwooshPlugin.TAG, "Inbox style " + key + " must be " + label + "; ignored.");
86
+ return false;
87
+ }
88
+ return true;
89
+ }
90
+
91
+ private String getString(ReadableMap mapStyle, String key) {
92
+ return hasTypedValue(mapStyle, key, ReadableType.String, "a string") ? mapStyle.getString(key) : null;
93
+ }
94
+
95
+ private Integer getColor(ReadableMap mapStyle, String key) {
96
+ return hasTypedValue(mapStyle, key, ReadableType.Number, "a number") ? mapStyle.getInt(key) : null;
72
97
  }
73
98
 
74
99
  private void setImages(ReadableMap mapStyle) {
@@ -87,11 +112,15 @@ class InboxUiStyleManager {
87
112
  }
88
113
 
89
114
  private Drawable getImage(ReadableMap mapStyle, String key) {
90
- if (!mapStyle.hasKey(key)) {
115
+ if (!hasTypedValue(mapStyle, key, ReadableType.Map, "an object with a uri")) {
116
+ return null;
117
+ }
118
+ ReadableMap imageMap = mapStyle.getMap(key);
119
+ if (!imageMap.hasKey(URI_KEY) || imageMap.getType(URI_KEY) != ReadableType.String) {
120
+ PWLog.error(PushwooshPlugin.TAG, "Inbox style " + key + "." + URI_KEY + " must be a string; ignored.");
91
121
  return null;
92
122
  }
93
- ReadableMap defaultImageIcon = mapStyle.getMap(key);
94
- String uri = defaultImageIcon.getString(URI_KEY);
123
+ String uri = imageMap.getString(URI_KEY);
95
124
  try {
96
125
  return getDrawable(uri);
97
126
  } catch (IOException e) {
@@ -113,36 +142,49 @@ class InboxUiStyleManager {
113
142
  private void setColors(ReadableMap mapStyle) {
114
143
  PushwooshInboxStyle PWInboxStyle = PushwooshInboxStyle.INSTANCE;
115
144
 
116
- if (mapStyle.hasKey(ACCENT_COLOR_KEY))
117
- PWInboxStyle.setAccentColor(mapStyle.getInt(ACCENT_COLOR_KEY));
118
- if (mapStyle.hasKey(HIGHLIGHT_COLOR_KEY))
119
- PWInboxStyle.setHighlightColor(mapStyle.getInt(HIGHLIGHT_COLOR_KEY));
120
- if (mapStyle.hasKey(BACKGROUND_COLOR_KEY))
121
- PWInboxStyle.setBackgroundColor(mapStyle.getInt(BACKGROUND_COLOR_KEY));
122
- if (mapStyle.hasKey(DIVIDER_COLOR_KEY))
123
- PWInboxStyle.setDividerColor(mapStyle.getInt(DIVIDER_COLOR_KEY));
124
-
125
- if (mapStyle.hasKey(DATE_COLOR_KEY))
126
- PWInboxStyle.setDateColor(mapStyle.getInt(DATE_COLOR_KEY));
127
- if (mapStyle.hasKey(READ_DATE_COLOR_KEY))
128
- PWInboxStyle.setReadDateColor(mapStyle.getInt(READ_DATE_COLOR_KEY));
129
-
130
- if (mapStyle.hasKey(TITLE_COLOR_KEY))
131
- PWInboxStyle.setTitleColor(mapStyle.getInt(TITLE_COLOR_KEY));
132
- if (mapStyle.hasKey(READ_TITLE_COLOR_KEY))
133
- PWInboxStyle.setReadTitleColor(mapStyle.getInt(READ_TITLE_COLOR_KEY));
134
-
135
- if (mapStyle.hasKey(DESCRIPTION_COLOR_KEY))
136
- PWInboxStyle.setDescriptionColor(mapStyle.getInt(DESCRIPTION_COLOR_KEY));
137
- if (mapStyle.hasKey(READ_DESCRIPTION_COLOR_KEY))
138
- PWInboxStyle.setReadDescriptionColor(mapStyle.getInt(READ_DESCRIPTION_COLOR_KEY));
139
-
140
- if (mapStyle.hasKey(BAR_BACKGROUND_COLOR))
141
- PWInboxStyle.setBarBackgroundColor(mapStyle.getInt(BAR_BACKGROUND_COLOR));
142
- if (mapStyle.hasKey(BAR_ACCENT_COLOR))
143
- PWInboxStyle.setBarAccentColor(mapStyle.getInt(BAR_ACCENT_COLOR));
144
- if (mapStyle.hasKey(BAR_TEXT_COLOR))
145
- PWInboxStyle.setBarTextColor(mapStyle.getInt(BAR_TEXT_COLOR));
145
+ Integer accentColor = getColor(mapStyle, ACCENT_COLOR_KEY);
146
+ if (accentColor != null)
147
+ PWInboxStyle.setAccentColor(accentColor);
148
+ Integer highlightColor = getColor(mapStyle, HIGHLIGHT_COLOR_KEY);
149
+ if (highlightColor != null)
150
+ PWInboxStyle.setHighlightColor(highlightColor);
151
+ Integer backgroundColor = getColor(mapStyle, BACKGROUND_COLOR_KEY);
152
+ if (backgroundColor != null)
153
+ PWInboxStyle.setBackgroundColor(backgroundColor);
154
+ Integer dividerColor = getColor(mapStyle, DIVIDER_COLOR_KEY);
155
+ if (dividerColor != null)
156
+ PWInboxStyle.setDividerColor(dividerColor);
157
+
158
+ Integer dateColor = getColor(mapStyle, DATE_COLOR_KEY);
159
+ if (dateColor != null)
160
+ PWInboxStyle.setDateColor(dateColor);
161
+ Integer readDateColor = getColor(mapStyle, READ_DATE_COLOR_KEY);
162
+ if (readDateColor != null)
163
+ PWInboxStyle.setReadDateColor(readDateColor);
164
+
165
+ Integer titleColor = getColor(mapStyle, TITLE_COLOR_KEY);
166
+ if (titleColor != null)
167
+ PWInboxStyle.setTitleColor(titleColor);
168
+ Integer readTitleColor = getColor(mapStyle, READ_TITLE_COLOR_KEY);
169
+ if (readTitleColor != null)
170
+ PWInboxStyle.setReadTitleColor(readTitleColor);
171
+
172
+ Integer descriptionColor = getColor(mapStyle, DESCRIPTION_COLOR_KEY);
173
+ if (descriptionColor != null)
174
+ PWInboxStyle.setDescriptionColor(descriptionColor);
175
+ Integer readDescriptionColor = getColor(mapStyle, READ_DESCRIPTION_COLOR_KEY);
176
+ if (readDescriptionColor != null)
177
+ PWInboxStyle.setReadDescriptionColor(readDescriptionColor);
178
+
179
+ Integer barBackgroundColor = getColor(mapStyle, BAR_BACKGROUND_COLOR);
180
+ if (barBackgroundColor != null)
181
+ PWInboxStyle.setBarBackgroundColor(barBackgroundColor);
182
+ Integer barAccentColor = getColor(mapStyle, BAR_ACCENT_COLOR);
183
+ if (barAccentColor != null)
184
+ PWInboxStyle.setBarAccentColor(barAccentColor);
185
+ Integer barTextColor = getColor(mapStyle, BAR_TEXT_COLOR);
186
+ if (barTextColor != null)
187
+ PWInboxStyle.setBarTextColor(barTextColor);
146
188
  }
147
189
 
148
190
  private class ReactInboxDateFormatter implements InboxDateFormatter {
@@ -61,17 +61,39 @@ public class PushwooshPlugin extends PushwooshPluginSpec implements LifecycleEve
61
61
  private static final String PUSH_RECEIVED_EVENT = Pushwoosh.PUSH_RECEIVE_EVENT;
62
62
  private static final String PUSH_RECEIVED_JS_EVENT = "pushReceived";
63
63
 
64
- private static EventDispatcher mEventDispatcher = new EventDispatcher();
65
-
66
- private static String sReceivedPushData;
67
- private static boolean sReceivedPushCallbackRegistered = false;
68
-
69
- private static String sStartPushData;
70
- private static boolean sPushCallbackRegistered = false;
71
-
72
- 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;
73
76
  private static final Object sStartPushLock = new Object();
74
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.
75
97
  private static PushwooshPlugin INSTANCE = null;
76
98
 
77
99
  private InboxUiStyleManager inboxUiInboxUiStyleManager;
@@ -79,8 +101,6 @@ public class PushwooshPlugin extends PushwooshPluginSpec implements LifecycleEve
79
101
  public PushwooshPlugin(ReactApplicationContext reactContext) {
80
102
  super(reactContext);
81
103
 
82
- INSTANCE = this;
83
-
84
104
  reactContext.addLifecycleEventListener(this);
85
105
 
86
106
  Context applicationContext = AndroidPlatformModule.getApplicationContext();
@@ -110,7 +130,7 @@ public class PushwooshPlugin extends PushwooshPluginSpec implements LifecycleEve
110
130
 
111
131
  @ReactMethod
112
132
  public void init(ReadableMap config, Callback success, Callback error) {
113
- String appId = config.hasKey("pw_appid") ? config.getString("pw_appid") : null;
133
+ String appId = config != null && config.hasKey("pw_appid") ? config.getString("pw_appid") : null;
114
134
 
115
135
  if (appId == null) {
116
136
  if (error != null) {
@@ -122,18 +142,21 @@ public class PushwooshPlugin extends PushwooshPluginSpec implements LifecycleEve
122
142
  Pushwoosh.getInstance().setAppId(appId);
123
143
 
124
144
  synchronized (sStartPushLock) {
125
- if (sReceivedPushData != null) {
126
- 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;
127
148
  }
128
149
 
129
- if (sStartPushData != null) {
130
- 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;
131
153
  }
132
154
 
133
155
  // Under the same lock as the replay above: a push taking the lock between the replay
134
- // and this flag would find sInitialized false, skip its own send, and never be
135
- // replayed either.
136
- sInitialized = true;
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;
137
160
  }
138
161
 
139
162
  if (success != null) {
@@ -167,13 +190,16 @@ public class PushwooshPlugin extends PushwooshPluginSpec implements LifecycleEve
167
190
  @ReactMethod
168
191
  public void onPushOpen(Callback callback) {
169
192
  synchronized (sStartPushLock) {
170
- if (!sPushCallbackRegistered && sStartPushData != null) {
171
- callback.invoke(ConversionUtil.toWritableMap(ConversionUtil.stringToJSONObject(sStartPushData)));
172
- 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;
173
199
  return;
174
200
  }
175
201
 
176
- sPushCallbackRegistered = true;
202
+ mPushCallbackRegistered = true;
177
203
  mEventDispatcher.subscribe(PUSH_OPEN_EVENT, callback);
178
204
  }
179
205
  }
@@ -181,13 +207,16 @@ public class PushwooshPlugin extends PushwooshPluginSpec implements LifecycleEve
181
207
  @ReactMethod
182
208
  public void onPushReceived(Callback callback) {
183
209
  synchronized (sStartPushLock) {
184
- if (!sReceivedPushCallbackRegistered && sReceivedPushData != null) {
185
- callback.invoke(ConversionUtil.toWritableMap(ConversionUtil.stringToJSONObject(sReceivedPushData)));
186
- 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;
187
216
  return;
188
217
  }
189
218
 
190
- sReceivedPushCallbackRegistered = true;
219
+ mReceivedPushCallbackRegistered = true;
191
220
  mEventDispatcher.subscribe(PUSH_RECEIVED_EVENT, callback);
192
221
  }
193
222
  }
@@ -412,12 +441,11 @@ public class PushwooshPlugin extends PushwooshPluginSpec implements LifecycleEve
412
441
  }
413
442
 
414
443
  Activity currentActivity = getCurrentActivity();
415
- Intent intent = new Intent(currentActivity, InboxActivity.class);
416
- if (currentActivity != null) {
417
- currentActivity.startActivity(intent);
418
- }else {
444
+ if (currentActivity == null) {
419
445
  PWLog.error(TAG, "current activity is null");
446
+ return;
420
447
  }
448
+ currentActivity.startActivity(new Intent(currentActivity, InboxActivity.class));
421
449
  }
422
450
 
423
451
  @ReactMethod
@@ -535,6 +563,10 @@ public class PushwooshPlugin extends PushwooshPluginSpec implements LifecycleEve
535
563
 
536
564
  @ReactMethod
537
565
  public void setNotificationIconBackgroundColor(String color) {
566
+ if (color == null || color.isEmpty()) {
567
+ PWLog.error(TAG, "setNotificationIconBackgroundColor: color is required");
568
+ return;
569
+ }
538
570
  int intColor;
539
571
  try {
540
572
  intColor = Color.parseColor(color);
@@ -600,29 +632,32 @@ public class PushwooshPlugin extends PushwooshPluginSpec implements LifecycleEve
600
632
  public void onHostDestroy() {
601
633
  PWLog.noise(TAG, "Host destroyed");
602
634
 
603
- // A push can be delivered on the FCM thread while the host is going down, and every other
604
- // accessor of these fields holds the lock.
605
- synchronized (sStartPushLock) {
606
- // Only for the module this host is taking down. A restarted host builds a new
607
- // PushwooshPlugin, and the order of the two events is not guaranteed, so a late
608
- // teardown must not reset what the new module has already set up.
609
- if (INSTANCE != this) {
610
- return;
611
- }
635
+ // Nothing is reset here: the host going down is the activity, not the JS runtime. React
636
+ // Native keeps the context and the JS bundle across activities, JS does not call init() or
637
+ // onPushOpen() a second time, and a push opened after the user left by Back has to reach
638
+ // that bundle. Everything JS set up is reset in invalidate(), where the runtime ends.
639
+ }
612
640
 
613
- sPushCallbackRegistered = false;
614
- sStartPushData = null;
641
+ // React Native tears the module down with its React instance (legacy ModuleHolder.destroy(),
642
+ // TurboModuleManager.invalidate()). Everything JS set up lives on this module and goes with it;
643
+ // only the module's place as the live one has to be given up here. The cached push stays - the
644
+ // next init() replays it.
645
+ @Override
646
+ public void invalidate() {
647
+ super.invalidate();
648
+ PWLog.noise(TAG, "Module invalidated");
615
649
 
616
- sReceivedPushCallbackRegistered = false;
617
- sReceivedPushData = null;
650
+ getReactApplicationContext().removeLifecycleEventListener(this);
618
651
 
619
- // These are what openPush()/messageReceived() consult before sending to JS. Left
620
- // standing, a push arriving after the host died was handed to a context with no JS
621
- // runtime, the broad catch there swallowed the failure, and the push was gone for
622
- // good - the cache that would have replayed it on the next init() had just been
623
- // cleared above. Cleared, the same push is cached and the next init() replays it.
624
- sInitialized = false;
625
- INSTANCE = null;
652
+ // A push can be delivered on the FCM thread while the runtime is going down, and every
653
+ // other accessor of the field holds the lock.
654
+ synchronized (sStartPushLock) {
655
+ // Only if this module is still the live one. The next module publishes itself on the
656
+ // first call from JS and the order of the two events is not guaranteed, so a late
657
+ // teardown must not unseat a module that has already taken over.
658
+ if (INSTANCE == this) {
659
+ INSTANCE = null;
660
+ }
626
661
  }
627
662
  }
628
663
 
@@ -635,12 +670,21 @@ public class PushwooshPlugin extends PushwooshPluginSpec implements LifecycleEve
635
670
 
636
671
  try {
637
672
  synchronized (sStartPushLock) {
638
- sStartPushData = pushData;
639
- if (sPushCallbackRegistered) {
640
- mEventDispatcher.dispatchEvent(PUSH_OPEN_EVENT, ConversionUtil.toWritableMap(ConversionUtil.stringToJSONObject(pushData)));
673
+ PendingPush push = new PendingPush(pushData);
674
+ sStartPush = push;
675
+
676
+ PushwooshPlugin plugin = INSTANCE;
677
+ if (plugin == null || !plugin.hasLiveRuntime()) {
678
+ return;
641
679
  }
642
- if (sInitialized && INSTANCE != null) {
643
- INSTANCE.sendEvent(PUSH_OPEN_JS_EVENT, ConversionUtil.stringToJSONObject(pushData));
680
+
681
+ if (plugin.mPushCallbackRegistered) {
682
+ plugin.mEventDispatcher.dispatchEvent(PUSH_OPEN_EVENT, ConversionUtil.toWritableMap(ConversionUtil.stringToJSONObject(pushData)));
683
+ push.sentToCallback = true;
684
+ }
685
+ if (plugin.mInitialized) {
686
+ plugin.sendEvent(PUSH_OPEN_JS_EVENT, ConversionUtil.stringToJSONObject(pushData));
687
+ push.sentAsEvent = true;
644
688
  }
645
689
  }
646
690
  } catch (Exception e) {
@@ -653,17 +697,33 @@ public class PushwooshPlugin extends PushwooshPluginSpec implements LifecycleEve
653
697
  mEventDispatcher.sendJSEvent(getReactApplicationContext(), event, ConversionUtil.toWritableMap(params));
654
698
  }
655
699
 
700
+ // The React instance behind the module is still running. One on its way down is not: emitting
701
+ // into it throws, the catch around the callers swallows the failure, and the push is gone.
702
+ // Held back, it stays cached for the next init(). Callers hold sStartPushLock.
703
+ private boolean hasLiveRuntime() {
704
+ return getReactApplicationContext().hasActiveReactInstance();
705
+ }
706
+
656
707
  static void messageReceived(String pushData) {
657
708
  PWLog.info(TAG, "Push received: " + pushData);
658
709
 
659
710
  try {
660
711
  synchronized (sStartPushLock) {
661
- sReceivedPushData = pushData;
662
- if (sReceivedPushCallbackRegistered) {
663
- mEventDispatcher.dispatchEvent(PUSH_RECEIVED_EVENT, ConversionUtil.toWritableMap(ConversionUtil.stringToJSONObject(pushData)));
712
+ PendingPush push = new PendingPush(pushData);
713
+ sReceivedPush = push;
714
+
715
+ PushwooshPlugin plugin = INSTANCE;
716
+ if (plugin == null || !plugin.hasLiveRuntime()) {
717
+ return;
718
+ }
719
+
720
+ if (plugin.mReceivedPushCallbackRegistered) {
721
+ plugin.mEventDispatcher.dispatchEvent(PUSH_RECEIVED_EVENT, ConversionUtil.toWritableMap(ConversionUtil.stringToJSONObject(pushData)));
722
+ push.sentToCallback = true;
664
723
  }
665
- if (sInitialized && INSTANCE != null) {
666
- INSTANCE.sendEvent(PUSH_RECEIVED_JS_EVENT, ConversionUtil.stringToJSONObject(pushData));
724
+ if (plugin.mInitialized) {
725
+ plugin.sendEvent(PUSH_RECEIVED_JS_EVENT, ConversionUtil.stringToJSONObject(pushData));
726
+ push.sentAsEvent = true;
667
727
  }
668
728
  }
669
729
  } catch (Exception e) {
@@ -3,11 +3,18 @@ package com.pushwoosh.reactnativeplugin.internal;
3
3
  import com.pushwoosh.internal.PluginProvider;
4
4
 
5
5
  public class ReactNativePluginProvider implements PluginProvider {
6
+ private static final String PLUGIN_VERSION = "7.0.2";
7
+
6
8
  @Override
7
9
  public String getPluginType() {
8
10
  return "React Native";
9
11
  }
10
12
 
13
+ // No @Override: getPluginVersion() lands in PluginProvider only in the next Android SDK release.
14
+ public String getPluginVersion() {
15
+ return PLUGIN_VERSION;
16
+ }
17
+
11
18
  @Override
12
19
  public int richMediaStartDelay() {
13
20
  return DEFAULT_RICH_MEDIA_START_DELAY;
@@ -72,6 +72,17 @@ static NSDictionary * gStartPushData = nil;
72
72
  static BOOL gStartPushReplayed = NO; // Launch push already replayed into the JS events by init:
73
73
  static NSURL * gPushDeepLinkURL = nil; // Deep link URL for New Architecture support
74
74
  static NSString * gEarlyHandledHash = nil; // Last push hash the cold-start delegate accepted, to avoid double handling
75
+ static NSString * gPendingWebLaunchLink = nil; // http(s) link of the launch push, until the SDK hands it to RCTLinkingManager
76
+ static NSUInteger gPendingWebLaunchLinkGeneration = 0; // Bumped on every arm, so a stale delivery timeout leaves a newer link alone
77
+ static NSString * gLastArmedWebLaunchLink = nil; // Survives delivery, so re-arming the same push is recognised
78
+ static BOOL gInitialURLAnswered = NO; // JS called getInitialURL() since gLastArmedWebLaunchLink was first armed
79
+ static BOOL gPluginInitialized = NO; // init: ran, so JS is up and a tapped push is no longer a launch push
80
+ static NSMutableArray<void (^)(NSString *)> * gInitialURLWaiters = nil; // getInitialURL() calls held for the pending link
81
+ static NSUInteger gInitialURLHoldGeneration = 0; // Bumped on every settle, so a stale hold timeout leaves a newer hold alone
82
+ // The SDK waits 0.2 s after accepting the push, then fetches apple-app-site-association with a 5 s timeout.
83
+ static NSTimeInterval const kPWWebLaunchLinkDeliveryTimeout = 6.0;
84
+ // Covers the SDK's delay and a cached or quick verdict without stalling an app whose link never reaches RN.
85
+ static NSTimeInterval const kPWInitialURLHoldTimeout = 1.0;
75
86
  static NSString * const kRegistrationSuccesEvent = @"PWRegistrationSuccess";
76
87
  static NSString * const kRegistrationErrorEvent = @"PWRegistrationError";
77
88
  static NSString * const kPushReceivedEvent = @"PWPushReceived";
@@ -80,6 +91,25 @@ static NSString * const kPushOpenEvent = @"PWPushOpen";
80
91
  static NSString * const kPushOpenJSEvent = @"pushOpened";
81
92
  static NSString * const kPushReceivedJSEvent = @"pushReceived";
82
93
 
94
+ static NSString * const kPWFrameworkType = @"React Native";
95
+ static NSString * const kPWFrameworkVersion = @"7.0.2";
96
+
97
+ /// Reports the framework type and the plugin version to the native core, on the cores that take them.
98
+ /// Dispatched by selector name: setFrameworkType:version: lands in a core newer than the pinned one.
99
+ static void pwplugin_setFrameworkTelemetry(NSString *type, NSString *version) {
100
+ Class configuration = [Pushwoosh configure];
101
+ SEL selector = NSSelectorFromString(@"setFrameworkType:version:");
102
+
103
+ if (![configuration respondsToSelector:selector]) {
104
+ return;
105
+ }
106
+
107
+ #pragma clang diagnostic push
108
+ #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
109
+ [configuration performSelector:selector withObject:type withObject:version];
110
+ #pragma clang diagnostic pop
111
+ }
112
+
83
113
  /// Returns the push link only when it is a deep link this app can route itself, otherwise nil.
84
114
  ///
85
115
  /// http/https links are left to the native SDK: it checks the host's apple-app-site-association
@@ -101,6 +131,78 @@ static NSURL * pwplugin_deepLinkURLFromPushLink(id link) {
101
131
  return url;
102
132
  }
103
133
 
134
+ /// Returns the http(s) push link as `URL.absoluteString`, the form RCTLinkingManager posts, otherwise nil.
135
+ static NSString * pwplugin_webLinkFromPushLink(id link) {
136
+ if (![link isKindOfClass:[NSString class]] || [link length] == 0) {
137
+ return nil;
138
+ }
139
+
140
+ NSURL *url = [NSURL URLWithString:link];
141
+ NSString *scheme = url.scheme.lowercaseString;
142
+ if ([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"]) {
143
+ return url.absoluteString;
144
+ }
145
+
146
+ return nil;
147
+ }
148
+
149
+ /// Answers every getInitialURL() call held for the pending link: with `url` when it arrived, otherwise
150
+ /// with what RCTLinkingManager itself reports. JS has read its initial URL either way.
151
+ static void pwplugin_settleInitialURLWaiters(NSString *url) {
152
+ NSArray<void (^)(NSString *)> *waiters = gInitialURLWaiters;
153
+ gInitialURLWaiters = nil;
154
+ gInitialURLHoldGeneration++;
155
+ gInitialURLAnswered = YES;
156
+ for (void (^waiter)(NSString *) in waiters) {
157
+ waiter(url);
158
+ }
159
+ }
160
+
161
+ /// Arms the launch push's web link for the SDK's delivery window; called by every launch-push site.
162
+ /// A push without a web link disarms it, so a later Safari open of the same URL is not caught.
163
+ static void pwplugin_rememberWebLaunchLink(NSDictionary *push) {
164
+ NSString *link = pwplugin_webLinkFromPushLink(push[@"l"]);
165
+ // The SDK accepts the same launch push at init() again, possibly after JS read getInitialURL().
166
+ if (!link || ![link isEqualToString:gLastArmedWebLaunchLink]) {
167
+ gInitialURLAnswered = NO;
168
+ }
169
+ gLastArmedWebLaunchLink = link;
170
+ gPendingWebLaunchLink = link;
171
+ NSUInteger generation = ++gPendingWebLaunchLinkGeneration;
172
+
173
+ if (!link) {
174
+ if (gInitialURLWaiters) {
175
+ pwplugin_settleInitialURLWaiters(nil);
176
+ }
177
+ return;
178
+ }
179
+
180
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kPWWebLaunchLinkDeliveryTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
181
+ if (generation == gPendingWebLaunchLinkGeneration) {
182
+ gPendingWebLaunchLink = nil;
183
+ if (gInitialURLWaiters) {
184
+ pwplugin_settleInitialURLWaiters(nil);
185
+ }
186
+ }
187
+ });
188
+ }
189
+
190
+ /// Catches the launch push's Universal Link on its way to RCTLinkingManager: resolves held getInitialURL()
191
+ /// calls, or parks it for the first one; a link after getInitialURL() answered is left to the `url` event.
192
+ static void pwplugin_handleOpenURLNotification(NSNotification *notification) {
193
+ id url = notification.userInfo[@"url"];
194
+ if (!gPendingWebLaunchLink || ![url isKindOfClass:[NSString class]] || ![url isEqualToString:gPendingWebLaunchLink]) {
195
+ return;
196
+ }
197
+
198
+ gPendingWebLaunchLink = nil;
199
+ if (gInitialURLWaiters) {
200
+ pwplugin_settleInitialURLWaiters(url);
201
+ } else if (!gInitialURLAnswered) {
202
+ gPushDeepLinkURL = [NSURL URLWithString:url];
203
+ }
204
+ }
205
+
104
206
  // Bridgeless hands a module an RCTBridgeProxy: an NSProxy that carries none of NSObject's KVC and
105
207
  // turns an unknown selector into a logged no-op. object_getClass asks without messaging it.
106
208
  static BOOL pwplugin_isRealBridge(id bridge) {
@@ -124,25 +226,50 @@ static BOOL pwplugin_isRealBridge(id bridge) {
124
226
  - (void)pwplugin_original_getInitialURL:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
125
227
  @end
126
228
 
229
+ static void pwplugin_originalGetInitialURL(id self, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject) {
230
+ SEL originalSelector = @selector(pwplugin_original_getInitialURL:reject:);
231
+ if ([self respondsToSelector:originalSelector]) {
232
+ void (*originalIMP)(id, SEL, RCTPromiseResolveBlock, RCTPromiseRejectBlock);
233
+ originalIMP = (void (*)(id, SEL, RCTPromiseResolveBlock, RCTPromiseRejectBlock))objc_msgSend;
234
+ originalIMP(self, originalSelector, resolve, reject);
235
+ } else {
236
+ resolve([NSNull null]);
237
+ }
238
+ }
239
+
127
240
  // Swizzled getInitialURL for RCTLinkingManager (New Architecture support)
128
241
  static void pwplugin_getInitialURL(id self, SEL _cmd, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject) {
129
242
  // If we have a saved deep link URL from push notification, return it
130
243
  if (gPushDeepLinkURL) {
131
244
  NSURL *url = gPushDeepLinkURL;
132
245
  gPushDeepLinkURL = nil; // Clear after use (one-time)
246
+ gInitialURLAnswered = YES;
133
247
  resolve(url.absoluteString);
134
248
  return;
135
249
  }
136
250
 
137
- // Otherwise call the original implementation
138
- SEL originalSelector = @selector(pwplugin_original_getInitialURL:reject:);
139
- if ([self respondsToSelector:originalSelector]) {
140
- void (*originalIMP)(id, SEL, RCTPromiseResolveBlock, RCTPromiseRejectBlock);
141
- originalIMP = (void (*)(id, SEL, RCTPromiseResolveBlock, RCTPromiseRejectBlock))objc_msgSend;
142
- originalIMP(self, originalSelector, resolve, reject);
143
- } else {
144
- resolve([NSNull null]);
251
+ if (gPendingWebLaunchLink && !gInitialURLAnswered) {
252
+ if (!gInitialURLWaiters) {
253
+ gInitialURLWaiters = [NSMutableArray array];
254
+ NSUInteger hold = gInitialURLHoldGeneration;
255
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kPWInitialURLHoldTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
256
+ if (hold == gInitialURLHoldGeneration) {
257
+ pwplugin_settleInitialURLWaiters(nil);
258
+ }
259
+ });
260
+ }
261
+ [gInitialURLWaiters addObject:[^(NSString *url) {
262
+ if (url) {
263
+ resolve(url);
264
+ } else {
265
+ pwplugin_originalGetInitialURL(self, resolve, reject);
266
+ }
267
+ } copy]];
268
+ return;
145
269
  }
270
+
271
+ gInitialURLAnswered = YES;
272
+ pwplugin_originalGetInitialURL(self, resolve, reject);
146
273
  }
147
274
 
148
275
  void pushwoosh_swizzle(Class cls, SEL fromChange, SEL toChange, IMP impl, const char * signature) {
@@ -210,7 +337,9 @@ RCT_EXPORT_METHOD(init:(NSDictionary*)config success:(RCTResponseSenderBlock)suc
210
337
  return;
211
338
  }
212
339
 
340
+ gPluginInitialized = YES;
213
341
  [PushNotificationManager initializeWithAppCode:appCode appName:nil];
342
+ pwplugin_setFrameworkTelemetry(kPWFrameworkType, kPWFrameworkVersion);
214
343
  [[PushNotificationManager pushManager] sendAppOpen];
215
344
  [PushNotificationManager pushManager].delegate = self;
216
345
 
@@ -1118,6 +1247,7 @@ RCT_EXPORT_METHOD(getRichMediaType:(RCTResponseSenderBlock)callback) {
1118
1247
  if (onStart) {
1119
1248
  gStartPushData = pushNotification;
1120
1249
  gStartPushReplayed = NO;
1250
+ pwplugin_rememberWebLaunchLink(pushNotification);
1121
1251
  // Save deep link URL for New Architecture (Linking.getInitialURL support)
1122
1252
  NSURL *deepLink = pwplugin_deepLinkURLFromPushLink(pushNotification[@"l"]);
1123
1253
  if (deepLink) {
@@ -1129,6 +1259,7 @@ RCT_EXPORT_METHOD(getRichMediaType:(RCTResponseSenderBlock)callback) {
1129
1259
  if (onStart) {
1130
1260
  gStartPushData = pushNotification;
1131
1261
  gStartPushReplayed = NO;
1262
+ pwplugin_rememberWebLaunchLink(pushNotification);
1132
1263
  // Save deep link URL for New Architecture (Linking.getInitialURL support)
1133
1264
  NSURL *deepLink = pwplugin_deepLinkURLFromPushLink(pushNotification[@"l"]);
1134
1265
  if (deepLink) {
@@ -1181,9 +1312,12 @@ static void pwplugin_acceptColdStartPushOnce(NSDictionary *userInfo) {
1181
1312
  - (void)userNotificationCenter:(UNUserNotificationCenter *)center
1182
1313
  didReceiveNotificationResponse:(UNNotificationResponse *)response
1183
1314
  withCompletionHandler:(void (^)(void))completionHandler API_AVAILABLE(ios(10.0)) {
1184
- if ([response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
1315
+ BOOL dismissed = [response.actionIdentifier isEqualToString:UNNotificationDismissActionIdentifier];
1316
+ // After init the plugin's own delegate or the app accepts the tap; a dismissed push opened nothing.
1317
+ if (!gPluginInitialized && !dismissed && [response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
1185
1318
  NSDictionary *userInfo = response.notification.request.content.userInfo;
1186
1319
  NSURL *appSchemeDeepLink = pwplugin_deepLinkURLFromPushLink(userInfo[@"l"]);
1320
+ pwplugin_rememberWebLaunchLink(userInfo);
1187
1321
 
1188
1322
  if (appSchemeDeepLink) {
1189
1323
  // Routing it through the SDK too would deliver the deep link twice.
@@ -1216,6 +1350,19 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response
1216
1350
 
1217
1351
  static PWEarlyNotificationDelegate *gEarlyDelegate = nil;
1218
1352
 
1353
+ // Wraps the delegate the app set in didFinishLaunching. A scene-based app gets the launch push's
1354
+ // response before the main queue drains, so installing it any later loses the push.
1355
+ static void pwplugin_installEarlyNotificationDelegate(void) {
1356
+ if (gEarlyDelegate) {
1357
+ return;
1358
+ }
1359
+
1360
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
1361
+ gEarlyDelegate = [[PWEarlyNotificationDelegate alloc] init];
1362
+ gEarlyDelegate.originalDelegate = center.delegate;
1363
+ center.delegate = gEarlyDelegate;
1364
+ }
1365
+
1219
1366
  @implementation RCTLinkingManager (PushwooshDeepLink)
1220
1367
 
1221
1368
  + (void)load {
@@ -1230,25 +1377,25 @@ static PWEarlyNotificationDelegate *gEarlyDelegate = nil;
1230
1377
  "v@:@@"
1231
1378
  );
1232
1379
 
1233
- // Set up early notification delegate to capture push on cold start
1234
- if (@available(iOS 10.0, *)) {
1235
- dispatch_async(dispatch_get_main_queue(), ^{
1236
- UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
1237
- gEarlyDelegate = [[PWEarlyNotificationDelegate alloc] init];
1238
- gEarlyDelegate.originalDelegate = center.delegate;
1239
- center.delegate = gEarlyDelegate;
1240
- });
1241
- }
1380
+ // RN keeps the notification name file-local; the string is its contract with the app delegate.
1381
+ [[NSNotificationCenter defaultCenter] addObserverForName:@"RCTOpenURLNotification"
1382
+ object:nil
1383
+ queue:nil
1384
+ usingBlock:^(NSNotification *notification) {
1385
+ pwplugin_handleOpenURLNotification(notification);
1386
+ }];
1242
1387
 
1243
- // Also check launchOptions when app finishes launching
1244
1388
  [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification
1245
1389
  object:nil
1246
1390
  queue:nil
1247
1391
  usingBlock:^(NSNotification *notification) {
1392
+ pwplugin_installEarlyNotificationDelegate();
1393
+
1248
1394
  NSDictionary *launchOptions = notification.userInfo;
1249
1395
  NSDictionary *remoteNotification = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey];
1250
1396
  if (remoteNotification) {
1251
1397
  NSURL *deepLink = pwplugin_deepLinkURLFromPushLink(remoteNotification[@"l"]);
1398
+ pwplugin_rememberWebLaunchLink(remoteNotification);
1252
1399
  if (deepLink) {
1253
1400
  if (!gPushDeepLinkURL) {
1254
1401
  gPushDeepLinkURL = deepLink;