cobrowse-sdk-react-native 2.11.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,5 +30,5 @@ android {
30
30
 
31
31
  dependencies {
32
32
  implementation 'com.facebook.react:react-native:+' // support react-native-v0.22-rc+
33
- implementation 'io.cobrowse:cobrowse-sdk-android:2.18.1'
33
+ implementation 'io.cobrowse:cobrowse-sdk-android:2.23.0'
34
34
  }
@@ -0,0 +1,22 @@
1
+ package io.cobrowse.reactnative;
2
+
3
+ import android.app.Activity;
4
+ import android.view.View;
5
+
6
+ import java.util.List;
7
+
8
+ import androidx.annotation.NonNull;
9
+ import androidx.annotation.Nullable;
10
+
11
+ public abstract class CobrowseIO {
12
+ public interface Delegate {
13
+ }
14
+
15
+ public interface RedactionDelegate extends Delegate {
16
+ @Nullable
17
+ List<View> redactedViews(@NonNull final Activity activity);
18
+
19
+ @Nullable
20
+ List<View> unredactedViews(@NonNull final Activity activity);
21
+ }
22
+ }
@@ -4,20 +4,27 @@ import android.app.Activity;
4
4
  import android.os.Handler;
5
5
  import android.util.Log;
6
6
  import android.view.View;
7
+ import android.view.ViewGroup;
8
+ import android.view.ViewParent;
7
9
 
8
10
  import com.facebook.react.bridge.Promise;
9
11
  import com.facebook.react.bridge.ReactApplicationContext;
10
12
  import com.facebook.react.bridge.ReactContextBaseJavaModule;
11
13
  import com.facebook.react.bridge.ReactMethod;
14
+ import com.facebook.react.bridge.ReadableArray;
12
15
  import com.facebook.react.bridge.ReadableMap;
13
16
  import com.facebook.react.modules.core.DeviceEventManagerModule;
17
+ import com.facebook.react.uimanager.NativeViewHierarchyManager;
18
+ import com.facebook.react.uimanager.UIBlock;
19
+ import com.facebook.react.uimanager.UIManagerModule;
14
20
 
15
21
  import java.util.ArrayList;
16
- import java.util.HashMap;
22
+ import java.util.HashSet;
17
23
  import java.util.List;
18
- import java.util.Map;
24
+ import java.util.Set;
19
25
 
20
26
  import androidx.annotation.NonNull;
27
+ import androidx.core.util.Predicate;
21
28
  import io.cobrowse.CobrowseIO;
22
29
  import io.cobrowse.Session;
23
30
  import io.cobrowse.CobrowseAccessibilityService;
@@ -27,16 +34,33 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
27
34
  CobrowseIO.SessionLoadDelegate, CobrowseIO.RedactionDelegate,
28
35
  CobrowseIO.RemoteControlRequestDelegate {
29
36
 
30
- private static final String SESSION_LOADED = "session.loaded";
37
+ private static final String SESSION_LOADED = "session.loaded";
31
38
  private static final String SESSION_UPDATED = "session.updated";
32
39
  private static final String SESSION_ENDED = "session.ended";
33
40
  private static final String SESSION_REQUESTED = "session.requested";
34
41
 
42
+ private final HashSet<Integer> unredactedTags = new HashSet<>();
43
+ private NativeViewHierarchyManager nodeManager;
44
+
35
45
  CobrowseIOModule(ReactApplicationContext reactContext) {
36
46
  super(reactContext);
37
47
  CobrowseIO.instance().setDelegate(this);
38
48
  }
39
49
 
50
+ private void findNodeManager() {
51
+ if (nodeManager != null) return;
52
+ final UIManagerModule uiManager = getReactApplicationContext().getNativeModule(UIManagerModule.class);
53
+ assert uiManager != null;
54
+ uiManager.prependUIBlock(new UIBlock() {
55
+ @Override
56
+ public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
57
+ nodeManager = nativeViewHierarchyManager;
58
+ }
59
+ });
60
+ }
61
+
62
+ public static io.cobrowse.reactnative.CobrowseIO.Delegate delegate;
63
+
40
64
  @NonNull
41
65
  public String getName() {
42
66
  return "CobrowseIO";
@@ -46,43 +70,141 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
46
70
  public void sessionDidLoad(@NonNull Session session) {
47
71
  getReactApplicationContext()
48
72
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
49
- .emit(SESSION_LOADED, Utility.convert(session));
73
+ .emit(SESSION_LOADED, Conversion.convert(session));
50
74
  }
51
75
 
52
76
  @Override
53
77
  public void sessionDidUpdate(@NonNull Session session) {
54
78
  getReactApplicationContext()
55
79
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
56
- .emit(SESSION_UPDATED, Utility.convert(session));
80
+ .emit(SESSION_UPDATED, Conversion.convert(session));
57
81
  }
58
82
 
59
83
  @Override
60
84
  public void sessionDidEnd(@NonNull Session session) {
61
85
  getReactApplicationContext()
62
86
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
63
- .emit(SESSION_ENDED, Utility.convert(session));
87
+ .emit(SESSION_ENDED, Conversion.convert(session));
64
88
  }
65
89
 
66
90
  @Override
67
91
  public void handleSessionRequest(@NonNull Activity activity, @NonNull Session session) {
68
92
  getReactApplicationContext()
69
93
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
70
- .emit(SESSION_REQUESTED, Utility.convert(session));
94
+ .emit(SESSION_REQUESTED, Conversion.convert(session));
71
95
  }
72
96
 
73
97
  @Override
74
98
  public void handleRemoteControlRequest(@NonNull Activity activity, @NonNull Session session) {
75
- // no-op, this will be handed on the JS side via an "updated" event handler
76
- // this stub just disables the default native prompt in the SDK
99
+ // no-op, this will be handed on the JS side via an "updated" event handler
100
+ // this stub just disables the default native prompt in the SDK
101
+ }
102
+ @ReactMethod
103
+ public void setUnredactedTags(final ReadableArray reactTags, final Promise promise) {
104
+ synchronized (unredactedTags) {
105
+ unredactedTags.clear();
106
+ for (int i = 0; i < reactTags.size(); i++)
107
+ unredactedTags.add(reactTags.getInt(i));
108
+ promise.resolve(null);
109
+ }
110
+ }
111
+
112
+ private Set<View> getUnredactedViews(@NonNull final Activity activity) {
113
+ synchronized (unredactedTags) {
114
+ HashSet<View> unredacted = new HashSet<>();
115
+ for (Integer i : unredactedTags) {
116
+ try {
117
+ unredacted.add(nodeManager.resolveView(i));
118
+ } catch (Exception e) {
119
+ Log.i("CobrowseIO", "Failed to find unredacted view for tag " + i + ", error = " + e.getMessage());
120
+ }
121
+ }
122
+ if (delegate instanceof io.cobrowse.reactnative.CobrowseIO.RedactionDelegate) {
123
+ List<View> views = ((io.cobrowse.reactnative.CobrowseIO.RedactionDelegate) delegate).unredactedViews(activity);
124
+ if (views != null) unredacted.addAll(views);
125
+ }
126
+ return unredacted;
127
+ }
128
+ }
129
+
130
+ private Set<View> getRedactedViews(@NonNull final Activity activity) {
131
+ HashSet<View> redacted = new HashSet<>(RedactedViewManager.redactedViews.keySet());
132
+ if (delegate instanceof io.cobrowse.reactnative.CobrowseIO.RedactionDelegate) {
133
+ List<View> views = ((io.cobrowse.reactnative.CobrowseIO.RedactionDelegate) delegate).redactedViews(activity);
134
+ if (views != null) redacted.addAll(views);
135
+ }
136
+ return redacted;
77
137
  }
78
138
 
79
139
  @Override
80
- public List<View> redactedViews(@NonNull final Activity activity) {
81
- return new ArrayList<>(RedactedViewManager.redactedViews.keySet());
140
+ public List<View> redactedViews(@NonNull final Activity activity) {
141
+ final Set<View> redacted = this.getRedactedViews(activity);
142
+ final Set<View> unredacted = this.getUnredactedViews(activity);
143
+
144
+ // Project all unredactions to the root to get the full set of unredacted nodes
145
+ HashSet<View> projectedUnredacted = new HashSet<>();
146
+ for (View view : unredacted) {
147
+ projectedUnredacted.add(view);
148
+ projectedUnredacted.addAll(TreeUtils.allParents(view));
149
+ }
150
+
151
+ // Work out which nodes are explicitly redacted but need to be unredacted
152
+ // due to a nested unredaction tag (these redactions will be moved towards the
153
+ // leaves of the view hierarchy instead)
154
+ HashSet<View> redactedProjectedUnredactions = new HashSet<>(redacted);
155
+ redactedProjectedUnredactions.retainAll(projectedUnredacted);
156
+
157
+ // Start to build the set of redactions we should actually apply. We start off with
158
+ // the set of explicitly redacted nodes
159
+ HashSet<View> toRedact = new HashSet<>(redacted);
160
+
161
+ // remove any redactions that have unredacted decendants (i.e. any that appear in the
162
+ // projection of the unredaction). These are the redacted nodes we need to move towards
163
+ // the leaves of the tree
164
+ toRedact.removeAll(redactedProjectedUnredactions);
165
+
166
+ // Work out the set of all nodes that are siblings of any projected unredacted node
167
+ HashSet<View> projectedUnredactionSiblings = new HashSet<>();
168
+ for (View view : projectedUnredacted) {
169
+ ViewParent parent = view.getParent();
170
+ if (parent instanceof ViewGroup)
171
+ projectedUnredactionSiblings.addAll(TreeUtils.directChildren((ViewGroup) parent));
172
+ }
173
+
174
+ // Subtract the projected unredactions from the set of all siblings of projected
175
+ // unredactions. i.e. subtract things that should be definitely unredacted to leave
176
+ // a set we're not sure yet whether to redact or not
177
+ HashSet<View> potentiallyRedactedUnredactedSiblings = new HashSet<View>(projectedUnredactionSiblings);
178
+ potentiallyRedactedUnredactedSiblings.removeAll(projectedUnredacted);
179
+
180
+ // for each node we're not sure about yet, check if the closest redacted or unredacted ancestor
181
+ // is a redacted node, if so this descendant should also be redacted
182
+ for (View view : potentiallyRedactedUnredactedSiblings) {
183
+ View closest = TreeUtils.closest(view, new Predicate<View>() {
184
+ @Override
185
+ public boolean test(View view) {
186
+ return redacted.contains(view) || unredacted.contains(view);
187
+ }
188
+ });
189
+ if (redacted.contains(closest)) toRedact.add(view);
190
+ }
191
+
192
+ // Remove any empty ViewGroup from the redacted set, they're often used for wrapping
193
+ // or sizing other elements, and do not usually need to be redacted
194
+ // If it's absolutely necessary they are redacted, they can always be replaced with
195
+ // a <Redacted> tag instead
196
+ for (View v : new HashSet<>(toRedact)) {
197
+ if (v instanceof ViewGroup && ((ViewGroup) v).getChildCount() == 0) {
198
+ toRedact.remove(v);
199
+ }
200
+ }
201
+
202
+ return new ArrayList<>(toRedact);
82
203
  }
83
204
 
84
205
  @ReactMethod
85
206
  public void start() {
207
+ findNodeManager();
86
208
  final Activity activity = getReactApplicationContext().getCurrentActivity();
87
209
  if (activity != null)
88
210
  activity.runOnUiThread(new Runnable() {
@@ -125,7 +247,7 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
125
247
 
126
248
  @ReactMethod
127
249
  public void currentSession(final Promise promise) {
128
- promise.resolve(Utility.convert(CobrowseIO.instance().currentSession()));
250
+ promise.resolve(Conversion.convert(CobrowseIO.instance().currentSession()));
129
251
  }
130
252
 
131
253
  @ReactMethod
@@ -137,7 +259,7 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
137
259
  @Override
138
260
  public void call(Error error, Session session) {
139
261
  if (error != null) promise.reject("cbio_create_session_failed", error);
140
- else promise.resolve(Utility.convert(session));
262
+ else promise.resolve(Conversion.convert(session));
141
263
  }
142
264
  });
143
265
  }
@@ -153,7 +275,7 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
153
275
  @Override
154
276
  public void call(Error error, Session session) {
155
277
  if (error != null) promise.reject("cbio_get_session_failed", error);
156
- else promise.resolve(Utility.convert(session));
278
+ else promise.resolve(Conversion.convert(session));
157
279
  }
158
280
  });
159
281
  }
@@ -174,7 +296,7 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
174
296
  @Override
175
297
  public void call(Error error, Session session) {
176
298
  if (error != null) promise.reject("cbio_activate_session_failed", error);
177
- else promise.resolve(Utility.convert(session));
299
+ else promise.resolve(Conversion.convert(session));
178
300
  }
179
301
  });
180
302
  }
@@ -231,7 +353,7 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
231
353
  if (options.hasKey("remote_control")) {
232
354
  String remoteControl = options.getString("remote_control");
233
355
 
234
- current.setRemoteControl(Utility.remoteControl(remoteControl), new io.cobrowse.Callback<Error, Session>() {
356
+ current.setRemoteControl(Conversion.remoteControl(remoteControl), new io.cobrowse.Callback<Error, Session>() {
235
357
  @Override
236
358
  public void call(Error error, Session session) {
237
359
  if (error != null) promise.reject("cbio_remote_control_failed", error);
@@ -253,4 +375,5 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
253
375
  public void accessibilityServiceIsRunning(final Promise promise) {
254
376
  promise.resolve(CobrowseAccessibilityService.isRunning(getReactApplicationContext()));
255
377
  }
378
+
256
379
  }
@@ -7,7 +7,7 @@ import io.cobrowse.Agent;
7
7
  import io.cobrowse.Session;
8
8
  import io.cobrowse.Session.RemoteControlState;
9
9
 
10
- final class Utility {
10
+ final class Conversion {
11
11
 
12
12
  static WritableMap convert(Session session) {
13
13
  WritableMap map = Arguments.createMap();
@@ -16,7 +16,7 @@ final class Utility {
16
16
  map.putString("state", session.state());
17
17
  map.putString("id", session.id());
18
18
  map.putBoolean("full_device", session.fullDevice());
19
- map.putString("remote_control", Utility.remoteControl(session.remoteControl()));
19
+ map.putString("remote_control", Conversion.remoteControl(session.remoteControl()));
20
20
 
21
21
  Agent agent = session.agent();
22
22
  if (agent != null) {
@@ -0,0 +1,43 @@
1
+ package io.cobrowse.reactnative;
2
+
3
+ import android.view.View;
4
+ import android.view.ViewGroup;
5
+ import android.view.ViewParent;
6
+
7
+ import java.util.ArrayList;
8
+ import java.util.HashSet;
9
+ import java.util.List;
10
+ import java.util.Set;
11
+
12
+ import androidx.core.util.Predicate;
13
+
14
+ class TreeUtils {
15
+
16
+ public static Set<View> directChildren(View root) {
17
+ HashSet<View> children = new HashSet<>();
18
+ if (root instanceof ViewGroup) {
19
+ ViewGroup viewGroup = (ViewGroup) root;
20
+ for (int i = 0; i < viewGroup.getChildCount(); i++) {
21
+ children.add(viewGroup.getChildAt(i));
22
+ }
23
+ }
24
+ return children;
25
+ }
26
+
27
+ public static List<View> allParents(View root) {
28
+ ArrayList<View> parents = new ArrayList<>();
29
+ ViewParent target = root.getParent();
30
+ while (target != null) {
31
+ if (target instanceof View) parents.add(0, (View) target);
32
+ target = target.getParent();
33
+ }
34
+ return parents;
35
+ }
36
+
37
+ public static View closest(View root, Predicate<View> predicate) {
38
+ if (predicate.test(root)) return root;
39
+ ViewParent parent = root.getParent();
40
+ if (parent instanceof ViewGroup) return closest((ViewGroup)parent, predicate);
41
+ return null;
42
+ }
43
+ }
@@ -11,8 +11,7 @@ Pod::Spec.new do |s|
11
11
  s.homepage = package["homepage"]
12
12
  s.source = { :git => 'https://github.com/cobrowseio/cobrowse-sdk-react-native.git' }
13
13
  s.platform = :ios, '9.0'
14
-
15
- s.dependency 'CobrowseIO/Framework', '2.17.1'
14
+ s.dependency 'CobrowseIO/XCFramework', '2.21.2'
16
15
  s.dependency 'React'
17
16
  s.source_files = 'ios/*.{h,m}'
18
17
  end
@@ -0,0 +1,9 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ @interface RCTCBIOTreeUtils : NSObject
4
+
5
+ +(NSArray*) allParents: (UIView*) root;
6
+
7
+ +(UIView*) closest: (BOOL (^)(UIView* view))predicate from: (UIView*) root;
8
+
9
+ @end
@@ -0,0 +1,21 @@
1
+ #import "RCTCBIOTreeUtils.h"
2
+
3
+ @implementation RCTCBIOTreeUtils
4
+
5
+ +(NSArray*) allParents: (UIView*) root {
6
+ NSMutableArray* parents = [NSMutableArray array];
7
+ UIView* target = root.superview;
8
+ while (target) {
9
+ [parents insertObject:target atIndex:0];
10
+ target = target.superview;
11
+ }
12
+ return parents;
13
+ }
14
+
15
+ +(UIView*) closest: (BOOL (^)(UIView* view))predicate from: (UIView*) root {
16
+ if (predicate(root)) return root;
17
+ else if (!root.superview) return nil;
18
+ else return [self closest:predicate from:root.superview];
19
+ }
20
+
21
+ @end
@@ -4,6 +4,20 @@
4
4
 
5
5
  @import CobrowseIO;
6
6
 
7
+
8
+ @protocol RCTCobrowseIODelegate <NSObject>
9
+
10
+ @optional
11
+
12
+ -(nonnull NSArray<UIView*>*) cobrowseRedactedViewsForViewController: (nonnull UIViewController*) vc;
13
+ -(nonnull NSArray<UIView*>*) cobrowseUnredactedViewsForViewController: (nonnull UIViewController*) vc;
14
+
15
+ @end
16
+
17
+
7
18
  @interface RCTCobrowseIO: RCTEventEmitter <RCTBridgeModule, CobrowseIODelegate>
8
19
 
20
+ @property (class, nullable) id<RCTCobrowseIODelegate> delegate;
21
+
9
22
  @end
23
+
@@ -5,17 +5,22 @@
5
5
  #import <React/RCTUtils.h>
6
6
  #import <React/RCTView.h>
7
7
  #import <React/RCTBridge.h>
8
+ #import <React/RCTUIManager.h>
8
9
  #import "RCTCobrowseIO.h"
10
+ #import "RCTCBIOTreeUtils.h"
9
11
 
10
12
  #define SESSION_LOADED "session.loaded"
11
13
  #define SESSION_UPDATED "session.updated"
12
14
  #define SESSION_ENDED "session.ended"
13
15
  #define SESSION_REQUESTED "session.requested"
14
16
 
17
+ static id<RCTCobrowseIODelegate> _Nullable _delegate;
18
+
15
19
  @import CobrowseIO;
16
20
 
17
21
  @implementation RCTCobrowseIO {
18
22
  bool hasListeners;
23
+ NSMutableSet* unredactedTags;
19
24
  }
20
25
 
21
26
  RCT_EXPORT_MODULE();
@@ -24,10 +29,19 @@ RCT_EXPORT_MODULE();
24
29
  self = [super init];
25
30
  if (self) {
26
31
  [CobrowseIO.instance setDelegate:self];
32
+ unredactedTags = [NSMutableSet set];
27
33
  }
28
34
  return self;
29
35
  }
30
36
 
37
+ +(id<RCTCobrowseIODelegate>)delegate {
38
+ return _delegate;
39
+ }
40
+
41
+ +(void)setDelegate:(id<RCTCobrowseIODelegate>)delegate {
42
+ _delegate = delegate;
43
+ }
44
+
31
45
  + (BOOL)requiresMainQueueSetup {
32
46
  return YES;
33
47
  }
@@ -52,6 +66,28 @@ RCT_EXPORT_MODULE();
52
66
  if (hasListeners) [self sendEventWithName:@SESSION_LOADED body:[session toDict]];
53
67
  }
54
68
 
69
+ -(NSSet<UIView*>*) redactedViews: (UIViewController*) vc {
70
+ NSMutableSet* views = [CBIOCobrowseRedactedManager.redactedViews mutableCopy];
71
+ if ([RCTCobrowseIO.delegate respondsToSelector:@selector(cobrowseRedactedViewsForViewController:)]) {
72
+ [views addObjectsFromArray: [RCTCobrowseIO.delegate cobrowseRedactedViewsForViewController:vc]];
73
+ }
74
+ return views;
75
+ }
76
+
77
+ -(NSSet<UIView*>*) unredactedViews: (UIViewController*) vc {
78
+ NSMutableSet* views = [NSMutableSet set];
79
+ @synchronized(unredactedTags) {
80
+ for (id tag in unredactedTags) {
81
+ UIView* v = [self.bridge.uiManager viewForReactTag: tag];
82
+ if (v != nil) [views addObject:v];
83
+ }
84
+ }
85
+ if ([RCTCobrowseIO.delegate respondsToSelector:@selector(cobrowseUnredactedViewsForViewController:)]) {
86
+ [views addObjectsFromArray: [RCTCobrowseIO.delegate cobrowseUnredactedViewsForViewController:vc]];
87
+ }
88
+ return views;
89
+ }
90
+
55
91
  -(void)cobrowseSessionDidUpdate:(CBIOSession *)session {
56
92
  if (hasListeners) [self sendEventWithName:@SESSION_UPDATED body:[session toDict]];
57
93
  }
@@ -70,11 +106,60 @@ RCT_EXPORT_MODULE();
70
106
  }
71
107
 
72
108
  -(NSArray<UIView *> *)cobrowseRedactedViewsForViewController:(UIViewController *)vc {
73
- NSMutableArray* views = [NSMutableArray array];
74
- for (UIView* v in CBIOCobrowseRedactedManager.redactedViews.allObjects) {
75
- if ([v isDescendantOfView:vc.view]) [views addObject:v];
109
+ NSSet* redacted = [self redactedViews: vc];
110
+ NSSet* unredacted = [self unredactedViews: vc];
111
+
112
+ // Project all unredactions to the root to get the full set of unredacted nodes
113
+ NSMutableSet* projectedUnredacted = [NSMutableSet set];
114
+ for (id view in unredacted) {
115
+ [projectedUnredacted addObject:view];
116
+ [projectedUnredacted addObjectsFromArray: [RCTCBIOTreeUtils allParents: view]];
76
117
  }
77
- return views;
118
+
119
+ // Work out which nodes are explicitly redacted but need to be unredacted
120
+ // due to a nested unredaction tag (these redactions will be moved towards the
121
+ // leaves of the view hierarchy instead)
122
+ NSMutableSet* redactedProjectedUnredactions = [redacted mutableCopy];
123
+ [redactedProjectedUnredactions intersectSet: projectedUnredacted];
124
+
125
+ // Start to build the set of redactions we should actually apply. We start off with
126
+ // the set of explicitly redacted nodes
127
+ NSMutableSet* toRedact = [redacted mutableCopy];
128
+
129
+ // remove any redactions that have unredacted decendants (i.e. any that appear in the
130
+ // projection of the unredaction). These are the redacted nodes we need to move towards
131
+ // the leaves of the tree
132
+ [toRedact minusSet: redactedProjectedUnredactions];
133
+
134
+ // Work out the set of all nodes that are siblings of any projected unredacted node
135
+ NSMutableSet* projectedUnredactionSiblings = [NSMutableSet set];
136
+ for (UIView* view in projectedUnredacted) {
137
+ [projectedUnredactionSiblings addObjectsFromArray: view.superview.subviews];
138
+ }
139
+
140
+ // Subtract the projected unredactions from the set of all siblings of projected
141
+ // unredactions. i.e. subtract things that should be definitely unredacted to leave
142
+ // a set we're not sure yet whether to redact or not
143
+ NSMutableSet* potentiallyRedactedUnredactedSiblings = [projectedUnredactionSiblings mutableCopy];
144
+ [potentiallyRedactedUnredactedSiblings minusSet:projectedUnredacted];
145
+
146
+ // for each node we're not sure about yet, check if the closest redacted or unredacted ancestor
147
+ // is a redacted node, if so this descendant should also be redacted
148
+ for (UIView* view in potentiallyRedactedUnredactedSiblings) {
149
+ UIView* closest = [RCTCBIOTreeUtils closest:^BOOL(UIView *v) {
150
+ return [redacted containsObject:v] || [unredacted containsObject:v];
151
+ } from: view];
152
+ if ([redacted containsObject: closest]) [toRedact addObject: view];
153
+ }
154
+
155
+ // Remove any empty RCTViews from the redacted set, they're often used for wrapping
156
+ // or sizing other elements, and do not usually need to be redacted
157
+ // If it's absolutely necessary they are redacted, they can always be replaced with
158
+ // a <Redacted> tag instead
159
+ for (UIView* v in [toRedact copy])
160
+ if ([v isKindOfClass:RCTView.class] && v.subviews.count == 0) [toRedact removeObject: v];
161
+
162
+ return toRedact.allObjects;
78
163
  }
79
164
 
80
165
  - (bool) cobrowseShouldCaptureWindow:(UIWindow *)window {
@@ -85,6 +170,14 @@ RCT_EXPORT_MODULE();
85
170
  }
86
171
  }
87
172
 
173
+ -(void) forceRedactionUpdate {
174
+ // TODO: expose an API for forcing redaction updates?
175
+ static UIView* v;
176
+ if (!v) v = [[UIView alloc] init];
177
+ [UIApplication.sharedApplication.keyWindow addSubview: v];
178
+ [v removeFromSuperview];
179
+ }
180
+
88
181
  RCT_EXPORT_METHOD(start) {
89
182
  [CobrowseIO.instance start];
90
183
  }
@@ -106,6 +199,18 @@ RCT_EXPORT_METHOD(api: (NSString*) api) {
106
199
  CobrowseIO.instance.api = api;
107
200
  }
108
201
 
202
+ RCT_REMAP_METHOD(setUnredactedTags,
203
+ setUnredactedTags: (NSArray*) reactTags
204
+ resolver:(RCTPromiseResolveBlock)resolve
205
+ rejecter:(RCTPromiseRejectBlock)reject) {
206
+ @synchronized(unredactedTags) {
207
+ [unredactedTags removeAllObjects];
208
+ [unredactedTags addObjectsFromArray:reactTags];
209
+ }
210
+ [self forceRedactionUpdate];
211
+ resolve(nil);
212
+ }
213
+
109
214
  RCT_EXPORT_METHOD(customData: (NSDictionary*) customData) {
110
215
  CobrowseIO.instance.customData = customData;
111
216
  }
package/js/Redacted.js CHANGED
@@ -1,19 +1,7 @@
1
- import React, { useContext } from 'react'
2
- import { View, requireNativeComponent } from 'react-native'
1
+ import React from 'react'
2
+ import { requireNativeComponent } from 'react-native'
3
3
  const CBIOCobrowseRedacted = requireNativeComponent('CBIOCobrowseRedacted')
4
4
 
5
- const RedactionContext = React.createContext(false)
6
-
7
- export default function (props) {
8
- const alreadyRedacted = useContext(RedactionContext)
9
- if (!alreadyRedacted) {
10
- return (
11
- <RedactionContext.Provider value>
12
- <CBIOCobrowseRedacted style={props.style}>
13
- <View>{props.children}</View>
14
- </CBIOCobrowseRedacted>
15
- </RedactionContext.Provider>
16
- )
17
- }
18
- return props.children
5
+ module.exports = function (props) {
6
+ return <CBIOCobrowseRedacted {...props}>{props.children}</CBIOCobrowseRedacted>
19
7
  }
@@ -0,0 +1,82 @@
1
+ import React, { useRef, useEffect, useCallback, useMemo } from 'react'
2
+ import { View, findNodeHandle } from 'react-native'
3
+ import { throttle } from 'lodash'
4
+ const CobrowseIONative = require('react-native').NativeModules.CobrowseIO
5
+
6
+ function mergeRefs (refs) {
7
+ return (value) => {
8
+ refs.forEach((ref) => {
9
+ if (typeof ref === 'function') {
10
+ ref(value)
11
+ } else if (ref != null) {
12
+ ref.current = value
13
+ }
14
+ })
15
+ }
16
+ }
17
+
18
+ const unredactedTags = new Set()
19
+ const sendUnredactionUpdates = throttle(() => {
20
+ CobrowseIONative.setUnredactedTags([...unredactedTags])
21
+ }, 50, { leading: false })
22
+
23
+ const removeUnredactedView = (view) => {
24
+ if (view) {
25
+ unredactedTags.delete(view)
26
+ sendUnredactionUpdates()
27
+ }
28
+ }
29
+
30
+ export const useUnredaction = (shouldWarnUnhandledRefs = true, componentName = '') => {
31
+ const ref = useRef(null)
32
+
33
+ const setRef = useCallback((node) => {
34
+ let hasRemovedRef = false
35
+ if (ref.current) {
36
+ hasRemovedRef = true
37
+ removeUnredactedView(ref.current)
38
+ }
39
+
40
+ if (node) {
41
+ const view = findNodeHandle(node)
42
+
43
+ if (view) {
44
+ unredactedTags.add(view)
45
+ sendUnredactionUpdates()
46
+
47
+ ref.current = view
48
+ } else {
49
+ console.warn(`Failed to apply unredact() to ${componentName} due to view not found`)
50
+ }
51
+ } else if (!hasRemovedRef) {
52
+ console.warn(
53
+ `Failed to apply unredact() to ${componentName} due to null node handle – make sure you are forwarding refs`
54
+ )
55
+ }
56
+ }, [])
57
+
58
+ useEffect(() => {
59
+ if (shouldWarnUnhandledRefs && !ref.current) {
60
+ console.warn(
61
+ `Failed to apply unredact() to ${componentName} due to null node handle – make sure the setRef function is called with the ref`
62
+ )
63
+ }
64
+
65
+ return () => removeUnredactedView(ref.current)
66
+ }, [])
67
+
68
+ return setRef
69
+ }
70
+
71
+ // HOC for adding unredaction to a whole component class
72
+ export function unredact(Component) {
73
+ return React.forwardRef(function Redacted(props, ref) {
74
+ const localRef = useUnredaction(true, Component?.name)
75
+ const refs = useMemo(() => mergeRefs([localRef, ref]), [localRef, ref])
76
+ return <Component {...props} collapsable={false} ref={refs} />
77
+ })
78
+ }
79
+
80
+ // also expose a basic Component based on a View
81
+ const Unredacted = unredact(View)
82
+ export default Unredacted
package/js/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { default } from './CobrowseIO'
2
2
  export { default as CobrowseView } from './CobrowseView'
3
3
  export { default as Redacted } from './Redacted'
4
+ export { default as Unredacted, unredact, useUnredaction } from './Unredacted'
4
5
  export { default as SessionControl } from './SessionControl'
5
6
  export { default as CobrowseAccessibilityService } from './CobrowseAccessibilityService'
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "cobrowse-sdk-react-native",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "Cobrowse SDK for React Native",
5
5
  "main": "js/index.js",
6
6
  "types": "ts/index.d.ts",
7
7
  "scripts": {
8
8
  "lint-fix": "ts-standard ts/*.d.ts --fix",
9
- "watch": "node scripts/watch.mjs",
10
9
  "start:dev": "nodemon --exec ./scripts/copy.sh"
11
10
  },
12
11
  "author": {
@@ -28,17 +27,17 @@
28
27
  ],
29
28
  "license": "Apache-2.0",
30
29
  "optionalDependencies": {
31
- "@types/react": "^17.0.38",
32
- "@types/react-native": "^0.66.15"
30
+ "@types/react": "^18.0.18",
31
+ "@types/react-native": "^0.69.6"
32
+ },
33
+ "dependencies": {
34
+ "lodash": "^4.17.21"
33
35
  },
34
- "dependencies": {},
35
36
  "devDependencies": {
36
- "@types/react": "^17.0.38",
37
- "@types/react-native": "^0.66.15",
38
- "managed-service-daemon": "^1.2.1",
39
- "nodemon": "^2.0.15",
37
+ "@types/react": "^18.0.18",
38
+ "@types/react-native": "^0.69.6",
39
+ "nodemon": "^2.0.19",
40
40
  "ts-standard": "^11.0.0",
41
- "typescript": "^4.5.5",
42
- "watchr": "^6.11.0"
41
+ "typescript": "^4.8.2"
43
42
  }
44
43
  }
package/ts/Redacted.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import type { Provider, ReactNode } from 'react'
1
+ import type { ReactNode, ReactElement } from 'react'
2
2
 
3
- type Props = Readonly<{}> & Readonly<{
4
- children?: ReactNode
5
- }>
3
+ type Props = Readonly<{}> &
4
+ Readonly<{
5
+ children?: ReactNode | undefined
6
+ }>
6
7
 
7
- export default function (props: Props): Provider<boolean>
8
+ export default function (props: Props): ReactElement | null
@@ -0,0 +1,15 @@
1
+ import React, { ReactElement, ForwardRefRenderFunction } from 'react'
2
+ import { View, ScrollView, Text, Image, FlatList, SectionList } from 'react-native'
3
+
4
+ export function useUnredaction<
5
+ T = View | ScrollView | Text | Image | FlatList | SectionList
6
+ >(shouldWarnUnhandledRefs?: boolean , componentName?: string): (elem: T) => void
7
+
8
+ type ForwardedRefType<T, P> = typeof React.forwardRef
9
+
10
+ // HOC for adding unredaction to a whole component class
11
+ export function unredact(Component: ReactElement): ForwardedRefType<View, Record<string, unknown>>
12
+
13
+ // also expose a basic Component based on a View
14
+ declare const Unredacted: ForwardRefRenderFunction<View, Record<string, unknown>>
15
+ export default Unredacted
package/ts/index.d.ts CHANGED
@@ -2,4 +2,5 @@ export { default } from './CobrowseIO'
2
2
  export { default as CobrowseView } from './CobrowseView'
3
3
  export { default as Redacted } from './Redacted'
4
4
  export { default as SessionControl } from './SessionControl'
5
+ export { default as Unredacted, unredact, useUnredaction } from './Unredacted'
5
6
  export { default as CobrowseAccessibilityService } from './CobrowseAccessibilityService'
@@ -1,7 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <Workspace
3
- version = "1.0">
4
- <FileRef
5
- location = "self:">
6
- </FileRef>
7
- </Workspace>
@@ -1,8 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
- <plist version="1.0">
4
- <dict>
5
- <key>IDEDidComputeMac32BitWarning</key>
6
- <true/>
7
- </dict>
8
- </plist>