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.
- package/android/build.gradle +1 -1
- package/android/src/main/java/io/cobrowse/reactnative/CobrowseIOModule.java +107 -10
- package/android/src/main/java/io/cobrowse/reactnative/{Utility.java → Conversion.java} +2 -2
- package/android/src/main/java/io/cobrowse/reactnative/TreeUtils.java +69 -0
- package/cobrowse-sdk-react-native.podspec +1 -2
- package/ios/RCTCBIOTreeUtils.h +15 -0
- package/ios/RCTCBIOTreeUtils.m +47 -0
- package/ios/RCTCobrowseIO.m +87 -4
- package/js/Redacted.js +4 -16
- package/js/Unredacted.js +82 -0
- package/js/index.js +1 -0
- package/package.json +5 -6
- package/ts/Redacted.d.ts +6 -5
- package/ts/Unredacted.d.ts +15 -0
- package/ts/index.d.ts +1 -0
- package/ios/RCTCobrowseIO.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- package/ios/RCTCobrowseIO.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
package/android/build.gradle
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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",
|
|
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
|
package/ios/RCTCobrowseIO.m
CHANGED
|
@@ -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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
|
2
|
-
import {
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { requireNativeComponent } from 'react-native'
|
|
3
3
|
const CBIOCobrowseRedacted = requireNativeComponent('CBIOCobrowseRedacted')
|
|
4
4
|
|
|
5
|
-
|
|
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
|
}
|
package/js/Unredacted.js
ADDED
|
@@ -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.
|
|
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 {
|
|
1
|
+
import type { ReactNode, ReactElement } from 'react'
|
|
2
2
|
|
|
3
|
-
type Props = Readonly<{}> &
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
type Props = Readonly<{}> &
|
|
4
|
+
Readonly<{
|
|
5
|
+
children?: ReactNode | undefined
|
|
6
|
+
}>
|
|
6
7
|
|
|
7
|
-
export default function (props: Props):
|
|
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'
|