@lucida-analytics/react-native 0.0.0-stage → 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.
@@ -0,0 +1,10 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ @interface RCT_EXTERN_MODULE (LucidaReplay, NSObject)
4
+ RCT_EXTERN_METHOD(captureWireframe : (NSDictionary *)options resolver : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
5
+ RCT_EXTERN_METHOD(installInfo : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
6
+ // Blocking synchronous: the JS queue storage is synchronous (src/native-cipher.ts).
7
+ RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(encryptQueue : (NSString *)plain)
8
+ RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(decryptQueue : (NSString *)value)
9
+ RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(encryptSensitive : (NSString *)pkcs1 n : (NSString *)n e : (NSString *)e plain : (NSString *)plain)
10
+ @end
@@ -0,0 +1,61 @@
1
+ import Foundation
2
+ #if canImport(AdServices)
3
+ import AdServices
4
+ #endif
5
+ import Lucida // the Lucida iOS SDK (sdks/ios): ReplayBridge encodes the UIKit hierarchy
6
+
7
+ /// React Native module `NativeModules.LucidaReplay`. Contract: see src/replay.ts (NativeReplayModule).
8
+ /// Not compiled in this repository's CI (needs the React Native toolchain); the logic it calls
9
+ /// (ReplayBridge, RRWebWireframe, UIKitViewHierarchy) is covered by `swift test` in sdks/ios.
10
+ @objc(LucidaReplay)
11
+ final class LucidaReplay: NSObject {
12
+ @objc static func requiresMainQueueSetup() -> Bool { false }
13
+
14
+ // Keychain-held AES-256-GCM key (generated on first use); same ciphertext format as the iOS SDK's queue cipher.
15
+ private let cipher = KeychainQueueCipher()
16
+
17
+ /// Blocking synchronous (see src/native-cipher.ts). Fails closed with "".
18
+ @objc(encryptQueue:)
19
+ func encryptQueue(_ plain: String) -> String { cipher.encrypt(plain) }
20
+
21
+ /// nil when the value cannot be decrypted (key lost or rotated); the queue then starts empty.
22
+ @objc(decryptQueue:)
23
+ func decryptQueue(_ value: String) -> String? { cipher.decrypt(value) }
24
+
25
+ /// Blocking synchronous (P-L12, src/sensitive.ts): RSA-OAEP-SHA256 with the PKCS#1 DER key; "" on failure.
26
+ @objc(encryptSensitive:n:e:plain:)
27
+ func encryptSensitive(_ pkcs1: String, n: String, e: String, plain: String) -> String {
28
+ ReplayBridge.encryptSensitive(pkcs1: pkcs1, n: n, e: e, plain: plain)
29
+ }
30
+
31
+ /// Install attribution (src/install.ts): resolves `{platform: "ios", token}`; token is NSNull when AdServices is unavailable.
32
+ @objc(installInfo:rejecter:)
33
+ func installInfo(_ resolve: @escaping (Any?) -> Void, rejecter reject: @escaping (String?, String?, Error?) -> Void) {
34
+ DispatchQueue.global(qos: .utility).async {
35
+ var token: String?
36
+ #if canImport(AdServices)
37
+ if #available(iOS 14.3, *) { token = try? AAAttribution.attributionToken() }
38
+ #endif
39
+ resolve(["platform": "ios", "token": token as Any])
40
+ }
41
+ }
42
+
43
+ /// Resolves the JSON text of one wireframe (or masked screenshot) capture, or "" when there is no key window.
44
+ @objc(captureWireframe:resolver:rejecter:)
45
+ func captureWireframe(_ options: NSDictionary, resolver resolve: @escaping (Any?) -> Void, rejecter reject: @escaping (String?, String?, Error?) -> Void) {
46
+ let screen = (options["screen"] as? String) ?? "Unknown"
47
+ let maskText = (options["maskText"] as? Bool) ?? true
48
+ let maskInputs = (options["maskAllTextInputs"] as? Bool) ?? true
49
+ let maskImages = (options["maskAllImages"] as? Bool) ?? true
50
+ let maskSandboxed = (options["maskSandboxedViews"] as? Bool) ?? true
51
+ // Screenshot mode: UIKitScreenshot paints the mask plan over the drawn window before JPEG encoding.
52
+ let quality: Double? = (options["mode"] as? String) == "screenshot" ? ((options["screenshotQuality"] as? Double) ?? 0.4) : nil
53
+ let maskScreen = (options["maskScreen"] as? Bool) ?? false // P-M17
54
+ let patterns = (options["maskTextPatterns"] as? [String]) ?? [] // P-M46
55
+ // UIKitViewHierarchy hops to the main thread itself; this runs off it so the JS bridge never blocks.
56
+ DispatchQueue.global(qos: .utility).async {
57
+ resolve(ReplayBridge.capture(screen: screen, maskText: maskText, maskAllTextInputs: maskInputs, maskAllImages: maskImages, maskSandboxedViews: maskSandboxed,
58
+ screenshotQuality: quality, maskScreen: maskScreen, maskTextPatterns: patterns) ?? "")
59
+ }
60
+ }
61
+ }
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@lucida-analytics/react-native",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "license": "Apache-2.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/lucidaanalytics/lucida-js.git",
8
+ "directory": "packages/react-native"
9
+ },
10
+ "homepage": "https://github.com/lucidaanalytics/lucida-js/tree/main/packages/react-native",
11
+ "bugs": "https://github.com/lucidaanalytics/lucida-js/issues",
12
+ "type": "module",
13
+ "sideEffects": false,
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "ios",
25
+ "android"
26
+ ],
27
+ "peerDependencies": {
28
+ "react": ">=18"
29
+ },
30
+ "dependencies": {
31
+ "@lucida-analytics/sdk-core": "0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^22.0.0",
35
+ "@types/react": "^18.3.0",
36
+ "@types/react-dom": "^18.3.0",
37
+ "jsdom": "^26.1.0",
38
+ "react": "^18.3.1",
39
+ "react-dom": "^18.3.1",
40
+ "tsup": "^8.5.0"
41
+ },
42
+ "scripts": {
43
+ "build": "tsup",
44
+ "typecheck": "tsgo --noEmit",
45
+ "test": "vitest run"
46
+ }
6
47
  }