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.
- package/android/build.gradle +1 -1
- package/android/src/main/java/io/cobrowse/reactnative/CobrowseIO.java +22 -0
- package/android/src/main/java/io/cobrowse/reactnative/CobrowseIOModule.java +139 -16
- package/android/src/main/java/io/cobrowse/reactnative/{Utility.java → Conversion.java} +2 -2
- package/android/src/main/java/io/cobrowse/reactnative/TreeUtils.java +43 -0
- package/cobrowse-sdk-react-native.podspec +1 -2
- package/ios/RCTCBIOTreeUtils.h +9 -0
- package/ios/RCTCBIOTreeUtils.m +21 -0
- package/ios/RCTCobrowseIO.h +14 -0
- package/ios/RCTCobrowseIO.m +109 -4
- package/js/Redacted.js +4 -16
- package/js/Unredacted.js +82 -0
- package/js/index.js +1 -0
- package/package.json +10 -11
- 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
|
@@ -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.
|
|
22
|
+
import java.util.HashSet;
|
|
17
23
|
import java.util.List;
|
|
18
|
-
import java.util.
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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,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,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
|
package/ios/RCTCobrowseIO.h
CHANGED
|
@@ -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
|
+
|
package/ios/RCTCobrowseIO.m
CHANGED
|
@@ -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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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": "^
|
|
32
|
-
"@types/react-native": "^0.
|
|
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": "^
|
|
37
|
-
"@types/react-native": "^0.
|
|
38
|
-
"
|
|
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.
|
|
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 {
|
|
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'
|