clintplay-react-native 0.1.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/README.md +222 -0
- package/android/build.gradle +50 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/digital/clint/clintplay/ClintplayRNModule.kt +362 -0
- package/android/src/main/java/digital/clint/clintplay/ClintplayRNPackage.kt +49 -0
- package/app.plugin.js +29 -0
- package/clintplay-react-native.podspec +40 -0
- package/dist/index.d.mts +280 -0
- package/dist/index.d.ts +280 -0
- package/dist/index.js +1480 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1461 -0
- package/dist/index.mjs.map +1 -0
- package/ios/ClintplayRN.h +25 -0
- package/ios/ClintplayRN.mm +361 -0
- package/ios/PrivacyInfo.xcprivacy +52 -0
- package/package.json +67 -0
- package/react-native.config.js +22 -0
- package/src/spec/NativeClintplayRN.ts +25 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// Implementation of the Clintplay React Native TurboModule on iOS.
|
|
2
|
+
//
|
|
3
|
+
// Walks the UIKit view hierarchy from the active key window and
|
|
4
|
+
// serializes each UIView into an rrweb-shaped wireframe snapshot with
|
|
5
|
+
// coordinate conversion, background colors, extracted text, and
|
|
6
|
+
// privacy auto-masking (clintplay-mask and secure text inputs).
|
|
7
|
+
//
|
|
8
|
+
// Both `RCT_NEW_ARCH_ENABLED` and legacy architecture paths are supported.
|
|
9
|
+
|
|
10
|
+
#import "ClintplayRN.h"
|
|
11
|
+
#import <React/RCTBridgeModule.h>
|
|
12
|
+
#import <UIKit/UIKit.h>
|
|
13
|
+
|
|
14
|
+
static UIWindow *FindKeyWindow(void) {
|
|
15
|
+
if (@available(iOS 13.0, *)) {
|
|
16
|
+
for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) {
|
|
17
|
+
if (scene.activationState == UISceneActivationStateForegroundActive &&
|
|
18
|
+
[scene isKindOfClass:[UIWindowScene class]]) {
|
|
19
|
+
UIWindowScene *windowScene = (UIWindowScene *)scene;
|
|
20
|
+
for (UIWindow *window in windowScene.windows) {
|
|
21
|
+
if (window.isKeyWindow) {
|
|
22
|
+
return window;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Fallback: active foreground window scene first window
|
|
28
|
+
for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) {
|
|
29
|
+
if (scene.activationState == UISceneActivationStateForegroundActive &&
|
|
30
|
+
[scene isKindOfClass:[UIWindowScene class]]) {
|
|
31
|
+
UIWindowScene *windowScene = (UIWindowScene *)scene;
|
|
32
|
+
if (windowScene.windows.count > 0) {
|
|
33
|
+
return windowScene.windows.firstObject;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Fallback: any window scene key window
|
|
38
|
+
for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) {
|
|
39
|
+
if ([scene isKindOfClass:[UIWindowScene class]]) {
|
|
40
|
+
UIWindowScene *windowScene = (UIWindowScene *)scene;
|
|
41
|
+
for (UIWindow *window in windowScene.windows) {
|
|
42
|
+
if (window.isKeyWindow) {
|
|
43
|
+
return window;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#pragma clang diagnostic push
|
|
51
|
+
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
52
|
+
UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
|
|
53
|
+
if (keyWindow) {
|
|
54
|
+
return keyWindow;
|
|
55
|
+
}
|
|
56
|
+
NSArray<UIWindow *> *windows = [UIApplication sharedApplication].windows;
|
|
57
|
+
if (windows.count > 0) {
|
|
58
|
+
return windows.firstObject;
|
|
59
|
+
}
|
|
60
|
+
#pragma clang diagnostic pop
|
|
61
|
+
|
|
62
|
+
return nil;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static NSString *HexColorStringFromUIColor(UIColor *color) {
|
|
66
|
+
if (!color) {
|
|
67
|
+
return nil;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
CGFloat red = 0.0;
|
|
71
|
+
CGFloat green = 0.0;
|
|
72
|
+
CGFloat blue = 0.0;
|
|
73
|
+
CGFloat alpha = 0.0;
|
|
74
|
+
|
|
75
|
+
@try {
|
|
76
|
+
if ([color getRed:&red green:&green blue:&blue alpha:&alpha]) {
|
|
77
|
+
int r = (int)round(fmax(0.0, fmin(1.0, red)) * 255.0);
|
|
78
|
+
int g = (int)round(fmax(0.0, fmin(1.0, green)) * 255.0);
|
|
79
|
+
int b = (int)round(fmax(0.0, fmin(1.0, blue)) * 255.0);
|
|
80
|
+
int a = (int)round(fmax(0.0, fmin(1.0, alpha)) * 255.0);
|
|
81
|
+
return [NSString stringWithFormat:@"#%02x%02x%02x%02x", r, g, b, a];
|
|
82
|
+
}
|
|
83
|
+
} @catch (NSException *e) {
|
|
84
|
+
// Non-RGB colors (e.g. pattern colors or system dynamic colors) throw if getRed:... is invoked.
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
CGFloat white = 0.0;
|
|
88
|
+
@try {
|
|
89
|
+
if ([color getWhite:&white alpha:&alpha]) {
|
|
90
|
+
int w = (int)round(fmax(0.0, fmin(1.0, white)) * 255.0);
|
|
91
|
+
int a = (int)round(fmax(0.0, fmin(1.0, alpha)) * 255.0);
|
|
92
|
+
return [NSString stringWithFormat:@"#%02x%02x%02x%02x", w, w, w, a];
|
|
93
|
+
}
|
|
94
|
+
} @catch (NSException *e) {
|
|
95
|
+
// Fall through safely
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@try {
|
|
99
|
+
CGColorRef cgColor = color.CGColor;
|
|
100
|
+
if (cgColor) {
|
|
101
|
+
size_t numComponents = CGColorGetNumberOfComponents(cgColor);
|
|
102
|
+
const CGFloat *components = CGColorGetComponents(cgColor);
|
|
103
|
+
if (components) {
|
|
104
|
+
if (numComponents >= 4) {
|
|
105
|
+
int r = (int)round(fmax(0.0, fmin(1.0, components[0])) * 255.0);
|
|
106
|
+
int g = (int)round(fmax(0.0, fmin(1.0, components[1])) * 255.0);
|
|
107
|
+
int b = (int)round(fmax(0.0, fmin(1.0, components[2])) * 255.0);
|
|
108
|
+
int a = (int)round(fmax(0.0, fmin(1.0, components[3])) * 255.0);
|
|
109
|
+
return [NSString stringWithFormat:@"#%02x%02x%02x%02x", r, g, b, a];
|
|
110
|
+
} else if (numComponents >= 2) {
|
|
111
|
+
int w = (int)round(fmax(0.0, fmin(1.0, components[0])) * 255.0);
|
|
112
|
+
int a = (int)round(fmax(0.0, fmin(1.0, components[1])) * 255.0);
|
|
113
|
+
return [NSString stringWithFormat:@"#%02x%02x%02x%02x", w, w, w, a];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} @catch (NSException *e) {
|
|
118
|
+
// Non-convertible CGColor spaces fall through safely
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return nil;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static NSDictionary *SerializeView(UIView *view, NSInteger *nodeCounter, NSInteger depth) {
|
|
125
|
+
if (depth > 64) {
|
|
126
|
+
return nil;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!view || view.isHidden || view.alpha <= 0.001) {
|
|
130
|
+
return nil;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
NSInteger nodeId = (*nodeCounter)++;
|
|
134
|
+
NSMutableDictionary *nodeDict = [NSMutableDictionary dictionary];
|
|
135
|
+
nodeDict[@"id"] = @(nodeId);
|
|
136
|
+
nodeDict[@"type"] = NSStringFromClass([view class]);
|
|
137
|
+
nodeDict[@"className"] = NSStringFromClass([view class]);
|
|
138
|
+
|
|
139
|
+
// Frame in window coordinates
|
|
140
|
+
CGRect frameInWindow = CGRectZero;
|
|
141
|
+
@try {
|
|
142
|
+
frameInWindow = [view convertRect:view.bounds toView:nil];
|
|
143
|
+
} @catch (NSException *exception) {
|
|
144
|
+
frameInWindow = view.frame;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (isnan(frameInWindow.origin.x) || isinf(frameInWindow.origin.x)) frameInWindow.origin.x = 0;
|
|
148
|
+
if (isnan(frameInWindow.origin.y) || isinf(frameInWindow.origin.y)) frameInWindow.origin.y = 0;
|
|
149
|
+
if (isnan(frameInWindow.size.width) || isinf(frameInWindow.size.width)) frameInWindow.size.width = 0;
|
|
150
|
+
if (isnan(frameInWindow.size.height) || isinf(frameInWindow.size.height)) frameInWindow.size.height = 0;
|
|
151
|
+
|
|
152
|
+
double x = round(frameInWindow.origin.x * 100.0) / 100.0;
|
|
153
|
+
double y = round(frameInWindow.origin.y * 100.0) / 100.0;
|
|
154
|
+
double w = round(frameInWindow.size.width * 100.0) / 100.0;
|
|
155
|
+
double h = round(frameInWindow.size.height * 100.0) / 100.0;
|
|
156
|
+
|
|
157
|
+
nodeDict[@"x"] = @(x);
|
|
158
|
+
nodeDict[@"y"] = @(y);
|
|
159
|
+
nodeDict[@"width"] = @(w);
|
|
160
|
+
nodeDict[@"height"] = @(h);
|
|
161
|
+
nodeDict[@"w"] = @(w);
|
|
162
|
+
nodeDict[@"h"] = @(h);
|
|
163
|
+
|
|
164
|
+
if (view.tag != 0) {
|
|
165
|
+
nodeDict[@"tag"] = @(view.tag);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Background color
|
|
169
|
+
NSString *bgHex = HexColorStringFromUIColor(view.backgroundColor);
|
|
170
|
+
if (bgHex) {
|
|
171
|
+
nodeDict[@"backgroundColor"] = bgHex;
|
|
172
|
+
nodeDict[@"bg"] = bgHex;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Privacy Auto-Masking
|
|
176
|
+
BOOL isMasked = NO;
|
|
177
|
+
NSString *accId = view.accessibilityIdentifier;
|
|
178
|
+
NSString *accLabel = view.accessibilityLabel;
|
|
179
|
+
NSString *nativeId = nil;
|
|
180
|
+
if ([view respondsToSelector:@selector(nativeID)]) {
|
|
181
|
+
nativeId = [(id)view nativeID];
|
|
182
|
+
} else if ([view respondsToSelector:@selector(nativeId)]) {
|
|
183
|
+
nativeId = [(id)view nativeId];
|
|
184
|
+
}
|
|
185
|
+
if ((accId && [accId isEqualToString:@"clintplay-mask"]) ||
|
|
186
|
+
(nativeId && [nativeId isEqualToString:@"clintplay-mask"]) ||
|
|
187
|
+
(accLabel && [accLabel isEqualToString:@"clintplay-mask"])) {
|
|
188
|
+
isMasked = YES;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if ([view respondsToSelector:@selector(isSecureTextEntry)]) {
|
|
192
|
+
@try {
|
|
193
|
+
if (((id<UITextInputTraits>)view).isSecureTextEntry) {
|
|
194
|
+
isMasked = YES;
|
|
195
|
+
}
|
|
196
|
+
} @catch (NSException *e) {
|
|
197
|
+
// Ignored
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if ([view respondsToSelector:@selector(textContentType)]) {
|
|
202
|
+
@try {
|
|
203
|
+
UITextContentType type = ((id<UITextInputTraits>)view).textContentType;
|
|
204
|
+
if (type != nil) {
|
|
205
|
+
if ([type isEqualToString:UITextContentTypePassword] ||
|
|
206
|
+
[type isEqualToString:UITextContentTypeNewPassword] ||
|
|
207
|
+
[type isEqualToString:UITextContentTypeCreditCardNumber] ||
|
|
208
|
+
[type isEqualToString:UITextContentTypeOneTimeCode]) {
|
|
209
|
+
isMasked = YES;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} @catch (NSException *e) {
|
|
213
|
+
// Ignored
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
NSString *identifier = accId ?: nativeId;
|
|
218
|
+
if (identifier.length > 0) {
|
|
219
|
+
nodeDict[@"accessibilityIdentifier"] = identifier;
|
|
220
|
+
}
|
|
221
|
+
if (view.accessibilityLabel.length > 0) {
|
|
222
|
+
nodeDict[@"accessibilityLabel"] = isMasked ? @"***" : view.accessibilityLabel;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Text extraction from UILabel, UITextField, UITextView, UIButton
|
|
226
|
+
NSString *extractedText = nil;
|
|
227
|
+
if ([view isKindOfClass:[UILabel class]]) {
|
|
228
|
+
UILabel *label = (UILabel *)view;
|
|
229
|
+
extractedText = label.text ?: (label.attributedText ? label.attributedText.string : nil);
|
|
230
|
+
} else if ([view isKindOfClass:[UITextField class]]) {
|
|
231
|
+
UITextField *textField = (UITextField *)view;
|
|
232
|
+
extractedText = textField.text ?: (textField.attributedText ? textField.attributedText.string : nil);
|
|
233
|
+
} else if ([view isKindOfClass:[UITextView class]]) {
|
|
234
|
+
UITextView *textView = (UITextView *)view;
|
|
235
|
+
extractedText = textView.text ?: (textView.attributedText ? textView.attributedText.string : nil);
|
|
236
|
+
} else if ([view isKindOfClass:[UIButton class]]) {
|
|
237
|
+
UIButton *button = (UIButton *)view;
|
|
238
|
+
if (@available(iOS 15.0, *)) {
|
|
239
|
+
if ([button respondsToSelector:@selector(configuration)] && button.configuration != nil) {
|
|
240
|
+
extractedText = button.configuration.title ?: (button.configuration.attributedTitle ? button.configuration.attributedTitle.string : nil);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (!extractedText) {
|
|
244
|
+
extractedText = [button titleForState:UIControlStateNormal] ?: (button.currentTitle ?: (button.currentAttributedTitle ? button.currentAttributedTitle.string : nil));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (extractedText) {
|
|
249
|
+
nodeDict[@"text"] = isMasked ? @"***" : extractedText;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (isMasked) {
|
|
253
|
+
nodeDict[@"masked"] = @(YES);
|
|
254
|
+
// Masked node replaces children with a single placeholder rect / no subview traversal
|
|
255
|
+
nodeDict[@"children"] = @[];
|
|
256
|
+
return nodeDict;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
nodeDict[@"masked"] = @(NO);
|
|
260
|
+
|
|
261
|
+
// Traverse child subviews wrapped in autoreleasepool to control memory footprint
|
|
262
|
+
NSMutableArray<NSDictionary *> *childList = [NSMutableArray array];
|
|
263
|
+
for (UIView *subview in view.subviews) {
|
|
264
|
+
@autoreleasepool {
|
|
265
|
+
NSDictionary *childDict = SerializeView(subview, nodeCounter, depth + 1);
|
|
266
|
+
if (childDict) {
|
|
267
|
+
[childList addObject:childDict];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
nodeDict[@"children"] = childList;
|
|
272
|
+
|
|
273
|
+
return nodeDict;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
@implementation ClintplayRN
|
|
277
|
+
|
|
278
|
+
RCT_EXPORT_MODULE(ClintplayRN)
|
|
279
|
+
|
|
280
|
+
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getVersion) {
|
|
281
|
+
return @"0.1.0";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
- (NSDictionary *)extractHierarchyDictionaryOnMainThread {
|
|
285
|
+
@autoreleasepool {
|
|
286
|
+
UIWindow *window = FindKeyWindow();
|
|
287
|
+
if (!window) {
|
|
288
|
+
return nil;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
NSInteger nodeCounter = 1;
|
|
292
|
+
NSDictionary *rootNode = SerializeView(window, &nodeCounter, 0);
|
|
293
|
+
if (!rootNode) {
|
|
294
|
+
return nil;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
long long timestampMs = (long long)([[NSDate date] timeIntervalSince1970] * 1000.0);
|
|
298
|
+
return @{
|
|
299
|
+
@"version": @(1),
|
|
300
|
+
@"ts": @(timestampMs),
|
|
301
|
+
@"timestamp": @(timestampMs),
|
|
302
|
+
@"nodes": @[ rootNode ]
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
- (NSString *)serializePayloadToJSON:(NSDictionary *)payload {
|
|
308
|
+
if (!payload) {
|
|
309
|
+
return @"{\"version\":1,\"nodes\":[]}";
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
@try {
|
|
313
|
+
if (![NSJSONSerialization isValidJSONObject:payload]) {
|
|
314
|
+
return @"{\"version\":1,\"nodes\":[]}";
|
|
315
|
+
}
|
|
316
|
+
NSError *error = nil;
|
|
317
|
+
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&error];
|
|
318
|
+
if (error || !jsonData) {
|
|
319
|
+
return @"{\"version\":1,\"nodes\":[]}";
|
|
320
|
+
}
|
|
321
|
+
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
322
|
+
return jsonString ?: @"{\"version\":1,\"nodes\":[]}";
|
|
323
|
+
} @catch (NSException *exception) {
|
|
324
|
+
return @"{\"version\":1,\"nodes\":[]}";
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(captureViewHierarchy) {
|
|
329
|
+
if ([NSThread isMainThread]) {
|
|
330
|
+
NSDictionary *payload = [self extractHierarchyDictionaryOnMainThread];
|
|
331
|
+
return [self serializePayloadToJSON:payload];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
__block NSDictionary *dict = nil;
|
|
335
|
+
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
|
336
|
+
|
|
337
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
338
|
+
@autoreleasepool {
|
|
339
|
+
dict = [self extractHierarchyDictionaryOnMainThread];
|
|
340
|
+
}
|
|
341
|
+
dispatch_semaphore_signal(sema);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(250 * NSEC_PER_MSEC));
|
|
345
|
+
long timedOut = dispatch_semaphore_wait(sema, timeout);
|
|
346
|
+
if (timedOut != 0) {
|
|
347
|
+
// 250ms timeout expired waiting for main thread: return fallback empty hierarchy
|
|
348
|
+
return @"{\"version\":1,\"nodes\":[]}";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return [self serializePayloadToJSON:dict];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
355
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
356
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params {
|
|
357
|
+
return std::make_shared<facebook::react::NativeClintplayRNSpecJSI>(params);
|
|
358
|
+
}
|
|
359
|
+
#endif
|
|
360
|
+
|
|
361
|
+
@end
|
|
@@ -0,0 +1,52 @@
|
|
|
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>NSPrivacyTracking</key>
|
|
6
|
+
<false/>
|
|
7
|
+
<key>NSPrivacyTrackingDomains</key>
|
|
8
|
+
<array/>
|
|
9
|
+
<key>NSPrivacyCollectedDataTypes</key>
|
|
10
|
+
<array>
|
|
11
|
+
<dict>
|
|
12
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
13
|
+
<string>NSPrivacyCollectedDataTypeProductInteraction</string>
|
|
14
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
15
|
+
<false/>
|
|
16
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
17
|
+
<false/>
|
|
18
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
19
|
+
<array>
|
|
20
|
+
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
|
21
|
+
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
|
22
|
+
</array>
|
|
23
|
+
</dict>
|
|
24
|
+
<dict>
|
|
25
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
26
|
+
<string>NSPrivacyCollectedDataTypePerformanceData</string>
|
|
27
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
28
|
+
<false/>
|
|
29
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
30
|
+
<false/>
|
|
31
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
32
|
+
<array>
|
|
33
|
+
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
|
34
|
+
</array>
|
|
35
|
+
</dict>
|
|
36
|
+
<dict>
|
|
37
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
38
|
+
<string>NSPrivacyCollectedDataTypeOtherDiagnosticData</string>
|
|
39
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
40
|
+
<false/>
|
|
41
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
42
|
+
<false/>
|
|
43
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
44
|
+
<array>
|
|
45
|
+
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
|
46
|
+
</array>
|
|
47
|
+
</dict>
|
|
48
|
+
</array>
|
|
49
|
+
<key>NSPrivacyAccessedAPITypes</key>
|
|
50
|
+
<array/>
|
|
51
|
+
</dict>
|
|
52
|
+
</plist>
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clintplay-react-native",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Clintplay React Native SDK — session capture for iOS and Android apps. Companion to clintplay-sdk (web).",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"app.plugin.js",
|
|
18
|
+
"ios",
|
|
19
|
+
"android",
|
|
20
|
+
"src/spec",
|
|
21
|
+
"clintplay-react-native.podspec",
|
|
22
|
+
"react-native.config.js"
|
|
23
|
+
],
|
|
24
|
+
"codegenConfig": {
|
|
25
|
+
"name": "ClintplayRNSpec",
|
|
26
|
+
"type": "modules",
|
|
27
|
+
"jsSrcsDir": "src/spec",
|
|
28
|
+
"android": {
|
|
29
|
+
"javaPackageName": "digital.clint.clintplay"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/clintdigital/clintplay",
|
|
35
|
+
"directory": "packages/sdk-react-native"
|
|
36
|
+
},
|
|
37
|
+
"author": "ThreadCode",
|
|
38
|
+
"homepage": "https://github.com/clintdigital/clintplay#readme",
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:watch": "vitest",
|
|
43
|
+
"typecheck": "tsc --noEmit"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"session-replay",
|
|
47
|
+
"react-native",
|
|
48
|
+
"expo",
|
|
49
|
+
"monitoring",
|
|
50
|
+
"clintplay"
|
|
51
|
+
],
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"packageManager": "pnpm@10.30.0",
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"react": ">=18",
|
|
56
|
+
"react-native": ">=0.74"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^25.5.2",
|
|
60
|
+
"@types/react": "^19.1.0",
|
|
61
|
+
"react": "^19.1.0",
|
|
62
|
+
"react-native": "^0.81.5",
|
|
63
|
+
"tsup": "^8.5.1",
|
|
64
|
+
"typescript": "^6.0.2",
|
|
65
|
+
"vitest": "^4.1.3"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// React Native autolink config. RN's CLI reads this file when the
|
|
2
|
+
// consumer app runs `pod install` (iOS) or builds the Android side,
|
|
3
|
+
// and wires our `.podspec` / `android/build.gradle` into the host
|
|
4
|
+
// project without any manual `link` step.
|
|
5
|
+
//
|
|
6
|
+
// `dependency.platforms.ios.podspecPath` is omitted because RN's
|
|
7
|
+
// autolink finds the `.podspec` by name at the package root — we
|
|
8
|
+
// just need to confirm the iOS dependency is enabled. On Android,
|
|
9
|
+
// `packageImportPath` is what gets dropped into MainApplication.kt
|
|
10
|
+
// so it can register `ClintplayRNPackage()`.
|
|
11
|
+
module.exports = {
|
|
12
|
+
dependency: {
|
|
13
|
+
platforms: {
|
|
14
|
+
ios: {},
|
|
15
|
+
android: {
|
|
16
|
+
packageImportPath:
|
|
17
|
+
'import digital.clint.clintplay.ClintplayRNPackage;',
|
|
18
|
+
packageInstance: 'new ClintplayRNPackage()',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TurboModule } from 'react-native';
|
|
2
|
+
import { TurboModuleRegistry } from 'react-native';
|
|
3
|
+
|
|
4
|
+
// TurboModule contract. RN codegen reads this file at build time to
|
|
5
|
+
// emit the C++ glue that bridges to the iOS / Android implementations
|
|
6
|
+
// in `ios/` and `android/`. Method signatures here MUST match the
|
|
7
|
+
// native impls exactly — any drift causes a runtime "method not found"
|
|
8
|
+
// when the consumer invokes them.
|
|
9
|
+
//
|
|
10
|
+
// MVP surface:
|
|
11
|
+
// - getVersion(): native side reports its package version so the JS
|
|
12
|
+
// layer can detect a version mismatch.
|
|
13
|
+
// - captureViewHierarchy(): returns a JSON-stringified rrweb-shaped
|
|
14
|
+
// wireframe of the current view tree. Stubbed in PR 1 (returns
|
|
15
|
+
// "") and implemented in a follow-up that adds the real iOS UIView
|
|
16
|
+
// / Android View traversal.
|
|
17
|
+
export interface Spec extends TurboModule {
|
|
18
|
+
getVersion(): string;
|
|
19
|
+
captureViewHierarchy(): string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// `getEnforcing` throws if the module isn't autolinked. The graceful
|
|
23
|
+
// runtime fallback lives in `src/native.ts` — that's what the SDK
|
|
24
|
+
// internals call. This default export exists so codegen can find it.
|
|
25
|
+
export default TurboModuleRegistry.getEnforcing<Spec>('ClintplayRN');
|