cobrowse-sdk-react-native 2.10.1 → 2.11.2-unredaction.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.17.2'
33
+ implementation 'io.cobrowse:cobrowse-sdk-android:2.21.4'
34
34
  }
@@ -9,56 +9,112 @@ import com.facebook.react.bridge.Promise;
9
9
  import com.facebook.react.bridge.ReactApplicationContext;
10
10
  import com.facebook.react.bridge.ReactContextBaseJavaModule;
11
11
  import com.facebook.react.bridge.ReactMethod;
12
+ import com.facebook.react.bridge.ReadableArray;
12
13
  import com.facebook.react.bridge.ReadableMap;
13
14
  import com.facebook.react.modules.core.DeviceEventManagerModule;
15
+ import com.facebook.react.uimanager.NativeViewHierarchyManager;
16
+ import com.facebook.react.uimanager.UIBlock;
17
+ import com.facebook.react.uimanager.UIManagerModule;
18
+ import com.facebook.react.views.view.ReactViewGroup;
14
19
 
15
20
  import java.util.ArrayList;
16
21
  import java.util.HashMap;
22
+ import java.util.HashSet;
17
23
  import java.util.List;
18
24
  import java.util.Map;
25
+ import java.util.Set;
19
26
 
20
27
  import androidx.annotation.NonNull;
28
+ import androidx.core.util.Predicate;
21
29
  import io.cobrowse.CobrowseIO;
22
30
  import io.cobrowse.Session;
23
31
  import io.cobrowse.CobrowseAccessibilityService;
24
32
 
25
33
  public class CobrowseIOModule extends ReactContextBaseJavaModule
26
34
  implements CobrowseIO.Delegate, CobrowseIO.SessionRequestDelegate,
27
- CobrowseIO.RedactionDelegate, CobrowseIO.RemoteControlRequestDelegate {
35
+ CobrowseIO.SessionLoadDelegate, CobrowseIO.RedactionDelegate,
36
+ CobrowseIO.RemoteControlRequestDelegate {
28
37
 
38
+ private static final String SESSION_LOADED = "session.loaded";
29
39
  private static final String SESSION_UPDATED = "session.updated";
30
40
  private static final String SESSION_ENDED = "session.ended";
31
41
  private static final String SESSION_REQUESTED = "session.requested";
32
42
 
43
+ private final HashSet<Integer> unredactedTags = new HashSet<>();
44
+ private NativeViewHierarchyManager nodeManager;
45
+
33
46
  CobrowseIOModule(ReactApplicationContext reactContext) {
34
47
  super(reactContext);
35
48
  CobrowseIO.instance().setDelegate(this);
36
49
  }
37
50
 
51
+ private void findNodeManager() {
52
+ if (nodeManager != null) return;
53
+ final UIManagerModule uiManager = getReactApplicationContext().getNativeModule(UIManagerModule.class);
54
+ assert uiManager != null;
55
+ uiManager.prependUIBlock(new UIBlock() {
56
+ @Override
57
+ public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
58
+ nodeManager = nativeViewHierarchyManager;
59
+ }
60
+ });
61
+ }
62
+
38
63
  @NonNull
39
64
  public String getName() {
40
65
  return "CobrowseIO";
41
66
  }
42
67
 
68
+ @Override
69
+ public void sessionDidLoad(@NonNull Session session) {
70
+ getReactApplicationContext()
71
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
72
+ .emit(SESSION_LOADED, Conversion.convert(session));
73
+ }
74
+
43
75
  @Override
44
76
  public void sessionDidUpdate(@NonNull Session session) {
45
77
  getReactApplicationContext()
46
78
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
47
- .emit(SESSION_UPDATED, Utility.convert(session));
79
+ .emit(SESSION_UPDATED, Conversion.convert(session));
48
80
  }
49
81
 
50
82
  @Override
51
83
  public void sessionDidEnd(@NonNull Session session) {
52
84
  getReactApplicationContext()
53
85
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
54
- .emit(SESSION_ENDED, Utility.convert(session));
86
+ .emit(SESSION_ENDED, Conversion.convert(session));
55
87
  }
56
88
 
57
89
  @Override
58
90
  public void handleSessionRequest(@NonNull Activity activity, @NonNull Session session) {
59
91
  getReactApplicationContext()
60
92
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
61
- .emit(SESSION_REQUESTED, Utility.convert(session));
93
+ .emit(SESSION_REQUESTED, Conversion.convert(session));
94
+ }
95
+
96
+ @ReactMethod
97
+ public void setUnredactedTags(final ReadableArray reactTags, final Promise promise) {
98
+ synchronized (unredactedTags) {
99
+ unredactedTags.clear();
100
+ for (int i = 0; i < reactTags.size(); i++)
101
+ unredactedTags.add(reactTags.getInt(i));
102
+ promise.resolve(null);
103
+ }
104
+ }
105
+
106
+ private Set<View> unredactedViews() {
107
+ synchronized (unredactedTags) {
108
+ HashSet<View> unredacted = new HashSet<>();
109
+ for (Integer i : unredactedTags) {
110
+ try {
111
+ unredacted.add(nodeManager.resolveView(i));
112
+ } catch (Exception e) {
113
+ Log.i("CobrowseIO", "Failed to find unredacted view for tag " + i + ", error = " + e.getMessage());
114
+ }
115
+ }
116
+ return unredacted;
117
+ }
62
118
  }
63
119
 
64
120
  @Override
@@ -69,11 +125,61 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
69
125
 
70
126
  @Override
71
127
  public List<View> redactedViews(@NonNull final Activity activity) {
72
- return new ArrayList<>(RedactedViewManager.redactedViews.keySet());
128
+ HashSet<View> redacted = new HashSet<>();
129
+ Set<View> unredacted = unredactedViews();
130
+ // By default everything is redacted for Activities that contain
131
+ // a ReactRootView.
132
+ Set<View> rootViews = TreeUtils.findAllClosest(
133
+ activity.getWindow().getDecorView().getRootView(),
134
+ new Predicate<View>() {
135
+ @Override
136
+ public boolean test(View view) {
137
+ return TreeUtils.isReactView(view);
138
+ }
139
+ });
140
+ for (View root : rootViews) redacted.addAll(TreeUtils.directChildren(root));
141
+
142
+ // Now we can actually start working out what should be unredacted
143
+ // First work out the set of all parents of unredact()'ed nodes
144
+ // Ignores nested unredacted nodes
145
+ HashSet<View> unredactedParents = new HashSet<>();
146
+ for (View v : unredacted) {
147
+ if (!TreeUtils.hasAnyParent(v, unredacted))
148
+ unredactedParents.addAll(TreeUtils.reactParents(v));
149
+ }
150
+
151
+ // Then work out the set of all direct children of any unredacted parents
152
+ // This should give us the set including unredacted nodes, their siblings,
153
+ // and all their parents.
154
+ for (View parent : unredactedParents) redacted.addAll(TreeUtils.directChildren(parent));
155
+
156
+ // Then we can subtract the set of unredacted parents to find just the
157
+ // set of unredacted nodes that are leaves of the parent subtree
158
+ redacted.removeAll(unredactedParents);
159
+
160
+ // Finally we can subtract the set of unredacted views to get the minimal
161
+ // set of redactions that will redact everything that's not explicitly unredacted
162
+ // whilst allowing the unredacted views to be visible
163
+ redacted.removeAll(unredacted);
164
+
165
+ // Remove any empty ReactViewGroup from the redacted set, they're often used for wrapping
166
+ // or sizing other elements, and do not usually need to be redacted
167
+ // If it's absolutely necessary they are redacted, they can always be replaced with
168
+ // a <Redacted> tag instead
169
+ for (View v : new HashSet<>(redacted))
170
+ if (v instanceof ReactViewGroup && ((ReactViewGroup) v).getChildCount() == 0)
171
+ redacted.remove(v);
172
+
173
+ // Any explicitly redacted views surrounded by <Redacted> tags take precedence, so
174
+ // re-add any tagged as such that the process above might have removed
175
+ redacted.addAll(RedactedViewManager.redactedViews.keySet());
176
+
177
+ return new ArrayList<>(redacted);
73
178
  }
74
179
 
75
180
  @ReactMethod
76
181
  public void start() {
182
+ findNodeManager();
77
183
  final Activity activity = getReactApplicationContext().getCurrentActivity();
78
184
  if (activity != null)
79
185
  activity.runOnUiThread(new Runnable() {
@@ -116,7 +222,7 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
116
222
 
117
223
  @ReactMethod
118
224
  public void currentSession(final Promise promise) {
119
- promise.resolve(Utility.convert(CobrowseIO.instance().currentSession()));
225
+ promise.resolve(Conversion.convert(CobrowseIO.instance().currentSession()));
120
226
  }
121
227
 
122
228
  @ReactMethod
@@ -128,7 +234,7 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
128
234
  @Override
129
235
  public void call(Error error, Session session) {
130
236
  if (error != null) promise.reject("cbio_create_session_failed", error);
131
- else promise.resolve(Utility.convert(session));
237
+ else promise.resolve(Conversion.convert(session));
132
238
  }
133
239
  });
134
240
  }
@@ -144,7 +250,7 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
144
250
  @Override
145
251
  public void call(Error error, Session session) {
146
252
  if (error != null) promise.reject("cbio_get_session_failed", error);
147
- else promise.resolve(Utility.convert(session));
253
+ else promise.resolve(Conversion.convert(session));
148
254
  }
149
255
  });
150
256
  }
@@ -165,7 +271,7 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
165
271
  @Override
166
272
  public void call(Error error, Session session) {
167
273
  if (error != null) promise.reject("cbio_activate_session_failed", error);
168
- else promise.resolve(Utility.convert(session));
274
+ else promise.resolve(Conversion.convert(session));
169
275
  }
170
276
  });
171
277
  }
@@ -222,7 +328,7 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
222
328
  if (options.hasKey("remote_control")) {
223
329
  String remoteControl = options.getString("remote_control");
224
330
 
225
- current.setRemoteControl(Utility.remoteControl(remoteControl), new io.cobrowse.Callback<Error, Session>() {
331
+ current.setRemoteControl(Conversion.remoteControl(remoteControl), new io.cobrowse.Callback<Error, Session>() {
226
332
  @Override
227
333
  public void call(Error error, Session session) {
228
334
  if (error != null) promise.reject("cbio_remote_control_failed", error);
@@ -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,69 @@
1
+ package io.cobrowse.reactnative;
2
+
3
+ import android.view.View;
4
+ import android.view.ViewGroup;
5
+ import android.view.ViewParent;
6
+
7
+ import com.facebook.react.ReactRootView;
8
+ import com.facebook.react.views.view.ReactViewGroup;
9
+
10
+ import java.util.ArrayList;
11
+ import java.util.Collections;
12
+ import java.util.HashSet;
13
+ import java.util.List;
14
+ import java.util.Set;
15
+
16
+ import androidx.core.util.Predicate;
17
+
18
+ class TreeUtils {
19
+
20
+ public static Set<View> directChildren(View root) {
21
+ HashSet<View> children = new HashSet<>();
22
+ if (root instanceof ViewGroup) {
23
+ ViewGroup viewGroup = (ViewGroup) root;
24
+ for (int i = 0; i < viewGroup.getChildCount(); i++) {
25
+ children.add(viewGroup.getChildAt(i));
26
+ }
27
+ }
28
+ return children;
29
+ }
30
+
31
+ public static List<View> allParents(View root) {
32
+ ArrayList<View> parents = new ArrayList<>();
33
+ ViewParent target = root.getParent();
34
+ while (target != null) {
35
+ if (target instanceof View) parents.add(0, (View) target);
36
+ target = target.getParent();
37
+ }
38
+ return parents;
39
+ }
40
+
41
+ public static List<View> reactParents(View root) {
42
+ List<View> parents = allParents(root);
43
+ ArrayList<View> reactParents = new ArrayList<>(parents);
44
+ for (View v : parents) {
45
+ if (isReactView(v)) break;
46
+ reactParents.remove(v);
47
+ }
48
+ return reactParents;
49
+ }
50
+
51
+ public static Set<View> findAllClosest(View root, Predicate<View> predicate) {
52
+ HashSet<View> found = new HashSet<>();
53
+ if (predicate.test(root)) found.add(root);
54
+ else {
55
+ for (View v : directChildren(root)) {
56
+ found.addAll(findAllClosest(v, predicate));
57
+ }
58
+ }
59
+ return found;
60
+ }
61
+
62
+ public static boolean isReactView(View view) {
63
+ return view instanceof ReactRootView || view instanceof ReactViewGroup;
64
+ }
65
+
66
+ public static boolean hasAnyParent(View node, Set<View> matches) {
67
+ return !Collections.disjoint(TreeUtils.allParents(node), matches);
68
+ }
69
+ }
@@ -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.16.4'
14
+ s.dependency 'CobrowseIO/XCFramework', '2.19.0'
16
15
  s.dependency 'React'
17
16
  s.source_files = 'ios/*.{h,m}'
18
17
  end
@@ -0,0 +1,15 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ @interface RCTCBIOTreeUtils : NSObject
4
+
5
+ +(NSArray*) allParents: (UIView*) root;
6
+
7
+ +(NSArray*) reactParents: (UIView*) root;
8
+
9
+ +(NSMutableSet*) findAllClosest: (BOOL (^)(UIView* view))predicate under: (UIView*) root;
10
+
11
+ +(bool) isReactView: (UIView*) view;
12
+
13
+ +(bool) hasAnyParent: (UIView*) view matches: (NSSet*) matches;
14
+
15
+ @end
@@ -0,0 +1,47 @@
1
+ #import "RCTCBIOTreeUtils.h"
2
+ #import <React/RCTRootView.h>
3
+ #import <React/RCTView.h>
4
+
5
+ @implementation RCTCBIOTreeUtils
6
+
7
+ +(NSArray*) allParents: (UIView*) root {
8
+ NSMutableArray* parents = [NSMutableArray array];
9
+ UIView* target = root.superview;
10
+ while (target) {
11
+ [parents insertObject:target atIndex:0];
12
+ target = target.superview;
13
+ }
14
+ return parents;
15
+ }
16
+
17
+ +(NSArray*) reactParents: (UIView*) root {
18
+ NSArray* allParents = [self allParents: root];
19
+ NSMutableArray* reactParents = [allParents mutableCopy];
20
+ for (id view in allParents) {
21
+ if ([self isReactView:view]) break;
22
+ [reactParents removeObject: view];
23
+ }
24
+ return reactParents;
25
+ }
26
+
27
+ +(NSMutableSet*) findAllClosest: (BOOL (^)(UIView* view))predicate under: (UIView*) root {
28
+ NSMutableSet* found = [NSMutableSet set];
29
+ if (predicate(root)) [found addObject:root];
30
+ else {
31
+ for (UIView* child in root.subviews) {
32
+ [found addObjectsFromArray: [self findAllClosest:predicate under:child].allObjects];
33
+ }
34
+ }
35
+ return found;
36
+ }
37
+
38
+ +(bool) isReactView: (UIView*) view {
39
+ return [view isKindOfClass:RCTRootView.class] || [view isKindOfClass: RCTView.class];
40
+ }
41
+
42
+ +(bool) hasAnyParent: (UIView*) view matches: (NSSet*) matches {
43
+ NSSet* parents = [NSSet setWithArray: [self allParents:view]];
44
+ return [parents intersectsSet: matches];
45
+ }
46
+
47
+ @end
@@ -5,8 +5,11 @@
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
 
12
+ #define SESSION_LOADED "session.loaded"
10
13
  #define SESSION_UPDATED "session.updated"
11
14
  #define SESSION_ENDED "session.ended"
12
15
  #define SESSION_REQUESTED "session.requested"
@@ -15,6 +18,7 @@
15
18
 
16
19
  @implementation RCTCobrowseIO {
17
20
  bool hasListeners;
21
+ NSMutableSet* unredactedTags;
18
22
  }
19
23
 
20
24
  RCT_EXPORT_MODULE();
@@ -23,6 +27,7 @@ RCT_EXPORT_MODULE();
23
27
  self = [super init];
24
28
  if (self) {
25
29
  [CobrowseIO.instance setDelegate:self];
30
+ unredactedTags = [NSMutableSet set];
26
31
  }
27
32
  return self;
28
33
  }
@@ -44,7 +49,22 @@ RCT_EXPORT_MODULE();
44
49
  }
45
50
 
46
51
  -(NSArray<NSString *> *)supportedEvents {
47
- return @[@SESSION_UPDATED, @SESSION_ENDED, @SESSION_REQUESTED];
52
+ return @[@SESSION_LOADED, @SESSION_UPDATED, @SESSION_ENDED, @SESSION_REQUESTED];
53
+ }
54
+
55
+ -(void)cobrowseSessionDidLoad:(CBIOSession *)session {
56
+ if (hasListeners) [self sendEventWithName:@SESSION_LOADED body:[session toDict]];
57
+ }
58
+
59
+ -(NSSet<UIView*>*) unredactedViews {
60
+ NSMutableSet* views = [NSMutableSet set];
61
+ @synchronized(unredactedTags) {
62
+ for (id tag in unredactedTags) {
63
+ UIView* v = [self.bridge.uiManager viewForReactTag: tag];
64
+ if (v != nil) [views addObject:v];
65
+ }
66
+ }
67
+ return views;
48
68
  }
49
69
 
50
70
  -(void)cobrowseSessionDidUpdate:(CBIOSession *)session {
@@ -65,11 +85,59 @@ RCT_EXPORT_MODULE();
65
85
  }
66
86
 
67
87
  -(NSArray<UIView *> *)cobrowseRedactedViewsForViewController:(UIViewController *)vc {
68
- NSMutableArray* views = [NSMutableArray array];
69
- for (UIView* v in CBIOCobrowseRedactedManager.redactedViews.allObjects) {
70
- if ([v isDescendantOfView:vc.view]) [views addObject:v];
88
+ NSMutableSet* redacted = [NSMutableSet set];
89
+ NSSet* unredacted = self.unredactedViews;
90
+
91
+ // By default everything managed by react is redacted for view controllers
92
+ // that contain react views. If we were to always redact vc.view this would lead
93
+ // to instances where windows that do not contain a RN context could
94
+ // not be unredacted.
95
+ // A simple example of this is the overlay window that cobrowse adds to
96
+ // render its annotations. This window sits on top of all the other windows
97
+ // and would always be redacted, effecivley redacting the entire screen all
98
+ // the time.
99
+ // To get around this, we only redact views below RCTViews or RCTRootViews
100
+ // (not inclusive), as then it's always possible to add an unredact()'ed component
101
+ // around a subview in the react tree to make parent visible.
102
+ NSSet* rootViews = [RCTCBIOTreeUtils findAllClosest:^BOOL(UIView *view) {
103
+ return [RCTCBIOTreeUtils isReactView: view];
104
+ } under: vc.view];
105
+ for (UIView* v in rootViews) [redacted addObjectsFromArray: v.subviews];
106
+
107
+ // Now we can actually start working out what should be unredacted
108
+ // First work out the set of all parents of unredact()'ed nodes
109
+ // that are inside a react view (ignoring any nested unredaction)
110
+ NSMutableSet* unredactedParents = [NSMutableSet set];
111
+ for (id view in unredacted) {
112
+ if (![RCTCBIOTreeUtils hasAnyParent:view matches:unredacted])
113
+ [unredactedParents addObjectsFromArray: [RCTCBIOTreeUtils reactParents: view]];
71
114
  }
72
- return views;
115
+ // Then work out the set of all direct children of any unredacted parents
116
+ // This should give us the set including unredacted nodes, their siblings,
117
+ // and all their parents.
118
+ for (UIView* parent in unredactedParents) [redacted addObjectsFromArray: parent.subviews];
119
+
120
+ // Then we can subtract the set of unredacted parents to find just the
121
+ // set of unredacted nodes that are leaves of the parent subtree
122
+ for (id v in unredactedParents) [redacted removeObject:v];
123
+
124
+ // Finally we can subtract the set of unredacted views to get the minimal
125
+ // set of redactions that will redact everything that's not explicitly unredacted
126
+ // whilst allowing the unredacted views to be visible
127
+ for (UIView* v in unredacted) [redacted removeObject:v];
128
+
129
+ // Remove any empty RCTViews from the redacted set, they're often used for wrapping
130
+ // or sizing other elements, and do not usually need to be redacted
131
+ // If it's absolutely necessary they are redacted, they can always be replaced with
132
+ // a <Redacted> tag instead
133
+ for (UIView* v in [redacted copy])
134
+ if ([v isKindOfClass:RCTView.class] && v.subviews.count == 0) [redacted removeObject: v];
135
+
136
+ // Any explicitly redacted views surroudned by <Redacted> tags take precedence, so
137
+ // re-add any tagged as such that the process above might have removed
138
+ for (id v in CBIOCobrowseRedactedManager.redactedViews.allObjects) [redacted addObject: v];
139
+
140
+ return redacted.allObjects;;
73
141
  }
74
142
 
75
143
  - (bool) cobrowseShouldCaptureWindow:(UIWindow *)window {
@@ -80,6 +148,14 @@ RCT_EXPORT_MODULE();
80
148
  }
81
149
  }
82
150
 
151
+ -(void) forceRedactionUpdate {
152
+ // TODO: expose an API for forcing redaction updates?
153
+ static UIView* v;
154
+ if (!v) v = [[UIView alloc] init];
155
+ [UIApplication.sharedApplication.keyWindow addSubview: v];
156
+ [v removeFromSuperview];
157
+ }
158
+
83
159
  RCT_EXPORT_METHOD(start) {
84
160
  [CobrowseIO.instance start];
85
161
  }
@@ -101,6 +177,18 @@ RCT_EXPORT_METHOD(api: (NSString*) api) {
101
177
  CobrowseIO.instance.api = api;
102
178
  }
103
179
 
180
+ RCT_REMAP_METHOD(setUnredactedTags,
181
+ setUnredactedTags: (NSArray*) reactTags
182
+ resolver:(RCTPromiseResolveBlock)resolve
183
+ rejecter:(RCTPromiseRejectBlock)reject) {
184
+ @synchronized(unredactedTags) {
185
+ [unredactedTags removeAllObjects];
186
+ [unredactedTags addObjectsFromArray:reactTags];
187
+ }
188
+ [self forceRedactionUpdate];
189
+ resolve(nil);
190
+ }
191
+
104
192
  RCT_EXPORT_METHOD(customData: (NSDictionary*) customData) {
105
193
  CobrowseIO.instance.customData = customData;
106
194
  }
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.10.1",
3
+ "version": "2.11.2-unredaction.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": {
@@ -31,14 +30,14 @@
31
30
  "@types/react": "^17.0.38",
32
31
  "@types/react-native": "^0.66.15"
33
32
  },
34
- "dependencies": {},
33
+ "dependencies": {
34
+ "lodash": "^4.17.21"
35
+ },
35
36
  "devDependencies": {
36
37
  "@types/react": "^17.0.38",
37
38
  "@types/react-native": "^0.66.15",
38
- "managed-service-daemon": "^1.2.1",
39
39
  "nodemon": "^2.0.15",
40
40
  "ts-standard": "^11.0.0",
41
- "typescript": "^4.5.5",
42
- "watchr": "^6.11.0"
41
+ "typescript": "^4.5.5"
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>