cobrowse-sdk-react-native 2.11.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.18.2'
33
+ implementation 'io.cobrowse:cobrowse-sdk-android:2.21.4'
34
34
  }
@@ -9,15 +9,23 @@ 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;
@@ -32,11 +40,26 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
32
40
  private static final String SESSION_ENDED = "session.ended";
33
41
  private static final String SESSION_REQUESTED = "session.requested";
34
42
 
43
+ private final HashSet<Integer> unredactedTags = new HashSet<>();
44
+ private NativeViewHierarchyManager nodeManager;
45
+
35
46
  CobrowseIOModule(ReactApplicationContext reactContext) {
36
47
  super(reactContext);
37
48
  CobrowseIO.instance().setDelegate(this);
38
49
  }
39
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
+
40
63
  @NonNull
41
64
  public String getName() {
42
65
  return "CobrowseIO";
@@ -46,28 +69,52 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
46
69
  public void sessionDidLoad(@NonNull Session session) {
47
70
  getReactApplicationContext()
48
71
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
49
- .emit(SESSION_LOADED, Utility.convert(session));
72
+ .emit(SESSION_LOADED, Conversion.convert(session));
50
73
  }
51
74
 
52
75
  @Override
53
76
  public void sessionDidUpdate(@NonNull Session session) {
54
77
  getReactApplicationContext()
55
78
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
56
- .emit(SESSION_UPDATED, Utility.convert(session));
79
+ .emit(SESSION_UPDATED, Conversion.convert(session));
57
80
  }
58
81
 
59
82
  @Override
60
83
  public void sessionDidEnd(@NonNull Session session) {
61
84
  getReactApplicationContext()
62
85
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
63
- .emit(SESSION_ENDED, Utility.convert(session));
86
+ .emit(SESSION_ENDED, Conversion.convert(session));
64
87
  }
65
88
 
66
89
  @Override
67
90
  public void handleSessionRequest(@NonNull Activity activity, @NonNull Session session) {
68
91
  getReactApplicationContext()
69
92
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
70
- .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
+ }
71
118
  }
72
119
 
73
120
  @Override
@@ -78,11 +125,61 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
78
125
 
79
126
  @Override
80
127
  public List<View> redactedViews(@NonNull final Activity activity) {
81
- 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);
82
178
  }
83
179
 
84
180
  @ReactMethod
85
181
  public void start() {
182
+ findNodeManager();
86
183
  final Activity activity = getReactApplicationContext().getCurrentActivity();
87
184
  if (activity != null)
88
185
  activity.runOnUiThread(new Runnable() {
@@ -125,7 +222,7 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
125
222
 
126
223
  @ReactMethod
127
224
  public void currentSession(final Promise promise) {
128
- promise.resolve(Utility.convert(CobrowseIO.instance().currentSession()));
225
+ promise.resolve(Conversion.convert(CobrowseIO.instance().currentSession()));
129
226
  }
130
227
 
131
228
  @ReactMethod
@@ -137,7 +234,7 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
137
234
  @Override
138
235
  public void call(Error error, Session session) {
139
236
  if (error != null) promise.reject("cbio_create_session_failed", error);
140
- else promise.resolve(Utility.convert(session));
237
+ else promise.resolve(Conversion.convert(session));
141
238
  }
142
239
  });
143
240
  }
@@ -153,7 +250,7 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
153
250
  @Override
154
251
  public void call(Error error, Session session) {
155
252
  if (error != null) promise.reject("cbio_get_session_failed", error);
156
- else promise.resolve(Utility.convert(session));
253
+ else promise.resolve(Conversion.convert(session));
157
254
  }
158
255
  });
159
256
  }
@@ -174,7 +271,7 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
174
271
  @Override
175
272
  public void call(Error error, Session session) {
176
273
  if (error != null) promise.reject("cbio_activate_session_failed", error);
177
- else promise.resolve(Utility.convert(session));
274
+ else promise.resolve(Conversion.convert(session));
178
275
  }
179
276
  });
180
277
  }
@@ -231,7 +328,7 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule
231
328
  if (options.hasKey("remote_control")) {
232
329
  String remoteControl = options.getString("remote_control");
233
330
 
234
- current.setRemoteControl(Utility.remoteControl(remoteControl), new io.cobrowse.Callback<Error, Session>() {
331
+ current.setRemoteControl(Conversion.remoteControl(remoteControl), new io.cobrowse.Callback<Error, Session>() {
235
332
  @Override
236
333
  public void call(Error error, Session session) {
237
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.17.1'
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,7 +5,9 @@
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"
@@ -16,6 +18,7 @@
16
18
 
17
19
  @implementation RCTCobrowseIO {
18
20
  bool hasListeners;
21
+ NSMutableSet* unredactedTags;
19
22
  }
20
23
 
21
24
  RCT_EXPORT_MODULE();
@@ -24,6 +27,7 @@ RCT_EXPORT_MODULE();
24
27
  self = [super init];
25
28
  if (self) {
26
29
  [CobrowseIO.instance setDelegate:self];
30
+ unredactedTags = [NSMutableSet set];
27
31
  }
28
32
  return self;
29
33
  }
@@ -52,6 +56,17 @@ RCT_EXPORT_MODULE();
52
56
  if (hasListeners) [self sendEventWithName:@SESSION_LOADED body:[session toDict]];
53
57
  }
54
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;
68
+ }
69
+
55
70
  -(void)cobrowseSessionDidUpdate:(CBIOSession *)session {
56
71
  if (hasListeners) [self sendEventWithName:@SESSION_UPDATED body:[session toDict]];
57
72
  }
@@ -70,11 +85,59 @@ RCT_EXPORT_MODULE();
70
85
  }
71
86
 
72
87
  -(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];
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]];
76
114
  }
77
- 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;;
78
141
  }
79
142
 
80
143
  - (bool) cobrowseShouldCaptureWindow:(UIWindow *)window {
@@ -85,6 +148,14 @@ RCT_EXPORT_MODULE();
85
148
  }
86
149
  }
87
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
+
88
159
  RCT_EXPORT_METHOD(start) {
89
160
  [CobrowseIO.instance start];
90
161
  }
@@ -106,6 +177,18 @@ RCT_EXPORT_METHOD(api: (NSString*) api) {
106
177
  CobrowseIO.instance.api = api;
107
178
  }
108
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
+
109
192
  RCT_EXPORT_METHOD(customData: (NSDictionary*) customData) {
110
193
  CobrowseIO.instance.customData = customData;
111
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.11.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>