@multiplayer-app/session-recorder-react-native 0.0.1-beta.7 → 0.0.1-beta.9
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/app.plugin.js +42 -0
- package/docs/NATIVE_MODULE_SETUP.md +177 -0
- package/ios/SessionRecorderNative.podspec +5 -0
- package/package.json +10 -1
- package/plugin/package.json +20 -0
- package/plugin/src/index.js +42 -0
- package/android/src/main/AndroidManifest.xml +0 -2
- package/android/src/main/java/com/multiplayer/sessionrecorder/ScreenMaskingModule.kt +0 -202
- package/android/src/main/java/com/multiplayer/sessionrecorder/ScreenMaskingPackage.kt +0 -16
- package/android/src/main/java/com/multiplayer/sessionrecorder/SessionRecorderModule.kt +0 -202
- package/android/src/main/java/com/multiplayer/sessionrecorder/SessionRecorderPackage.kt +0 -16
- package/babel.config.js +0 -13
- package/docs/AUTO_METADATA_DETECTION.md +0 -108
- package/docs/TROUBLESHOOTING.md +0 -168
- package/ios/ScreenMasking.m +0 -12
- package/ios/ScreenMasking.podspec +0 -21
- package/ios/ScreenMasking.swift +0 -205
- package/ios/SessionRecorder.podspec +0 -21
- package/scripts/generate-app-metadata.js +0 -173
- package/src/components/GestureCaptureWrapper/GestureCaptureWrapper.tsx +0 -86
- package/src/components/GestureCaptureWrapper/index.ts +0 -1
- package/src/components/ScreenRecorderView/ScreenRecorderView.tsx +0 -72
- package/src/components/ScreenRecorderView/index.ts +0 -1
- package/src/components/SessionRecorderWidget/FinalPopover.tsx +0 -62
- package/src/components/SessionRecorderWidget/FloatingButton.tsx +0 -136
- package/src/components/SessionRecorderWidget/InitialPopover.tsx +0 -89
- package/src/components/SessionRecorderWidget/ModalContainer.tsx +0 -128
- package/src/components/SessionRecorderWidget/ModalHeader.tsx +0 -24
- package/src/components/SessionRecorderWidget/SessionRecorderWidget.tsx +0 -109
- package/src/components/SessionRecorderWidget/icons.tsx +0 -52
- package/src/components/SessionRecorderWidget/index.ts +0 -3
- package/src/components/SessionRecorderWidget/styles.ts +0 -150
- package/src/components/index.ts +0 -3
- package/src/config/constants.ts +0 -60
- package/src/config/defaults.ts +0 -83
- package/src/config/index.ts +0 -6
- package/src/config/masking.ts +0 -28
- package/src/config/session-recorder.ts +0 -55
- package/src/config/validators.ts +0 -31
- package/src/context/SessionRecorderContext.tsx +0 -53
- package/src/index.ts +0 -9
- package/src/native/ScreenMasking.ts +0 -34
- package/src/native/SessionRecorderNative.ts +0 -34
- package/src/otel/helpers.ts +0 -275
- package/src/otel/index.ts +0 -138
- package/src/otel/instrumentations/index.ts +0 -115
- package/src/patch/index.ts +0 -1
- package/src/patch/xhr.ts +0 -141
- package/src/recorder/eventExporter.ts +0 -141
- package/src/recorder/gestureRecorder.ts +0 -498
- package/src/recorder/index.ts +0 -179
- package/src/recorder/navigationTracker.ts +0 -449
- package/src/recorder/screenRecorder.ts +0 -527
- package/src/services/api.service.ts +0 -203
- package/src/services/screenMaskingService.ts +0 -118
- package/src/services/storage.service.ts +0 -199
- package/src/session-recorder.ts +0 -606
- package/src/types/expo.d.ts +0 -23
- package/src/types/index.ts +0 -28
- package/src/types/session-recorder.ts +0 -429
- package/src/types/session.ts +0 -65
- package/src/utils/app-metadata.ts +0 -31
- package/src/utils/index.ts +0 -8
- package/src/utils/logger.ts +0 -225
- package/src/utils/nativeModuleTest.ts +0 -60
- package/src/utils/platform.ts +0 -384
- package/src/utils/request-utils.ts +0 -61
- package/src/utils/rrweb-events.ts +0 -309
- package/src/utils/session.ts +0 -18
- package/src/utils/time.ts +0 -17
- package/src/utils/type-utils.ts +0 -75
- package/src/version.ts +0 -1
- package/tsconfig.json +0 -24
- /package/ios/{SessionRecorder.m → SessionRecorderNative.m} +0 -0
- /package/ios/{SessionRecorder.swift → SessionRecorderNative.swift} +0 -0
package/ios/ScreenMasking.swift
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import UIKit
|
|
2
|
-
import React
|
|
3
|
-
|
|
4
|
-
@objc(SessionRecorder)
|
|
5
|
-
class SessionRecorder: NSObject {
|
|
6
|
-
|
|
7
|
-
@objc func captureAndMask(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
8
|
-
DispatchQueue.main.async {
|
|
9
|
-
guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else {
|
|
10
|
-
reject("NO_WINDOW", "Unable to get key window", nil)
|
|
11
|
-
return
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
UIGraphicsBeginImageContextWithOptions(window.bounds.size, false, UIScreen.main.scale)
|
|
15
|
-
window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
|
|
16
|
-
let screenshot = UIGraphicsGetImageFromCurrentImageContext()
|
|
17
|
-
UIGraphicsEndImageContext()
|
|
18
|
-
|
|
19
|
-
guard let image = screenshot else {
|
|
20
|
-
reject("CAPTURE_FAILED", "Failed to capture screen", nil)
|
|
21
|
-
return
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Apply masking to sensitive elements
|
|
25
|
-
let maskedImage = self.applyMasking(to: image, in: window)
|
|
26
|
-
|
|
27
|
-
if let data = maskedImage.jpegData(compressionQuality: 0.5) {
|
|
28
|
-
let base64 = data.base64EncodedString()
|
|
29
|
-
resolve(base64)
|
|
30
|
-
} else {
|
|
31
|
-
reject("ENCODING_FAILED", "Failed to encode image", nil)
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
@objc func captureAndMaskWithOptions(_ options: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
37
|
-
DispatchQueue.main.async {
|
|
38
|
-
guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else {
|
|
39
|
-
reject("NO_WINDOW", "Unable to get key window", nil)
|
|
40
|
-
return
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
UIGraphicsBeginImageContextWithOptions(window.bounds.size, false, UIScreen.main.scale)
|
|
44
|
-
window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
|
|
45
|
-
let screenshot = UIGraphicsGetImageFromCurrentImageContext()
|
|
46
|
-
UIGraphicsEndImageContext()
|
|
47
|
-
|
|
48
|
-
guard let image = screenshot else {
|
|
49
|
-
reject("CAPTURE_FAILED", "Failed to capture screen", nil)
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Apply masking with custom options
|
|
54
|
-
let maskedImage = self.applyMaskingWithOptions(to: image, in: window, options: options)
|
|
55
|
-
|
|
56
|
-
if let data = maskedImage.jpegData(compressionQuality: 0.5) {
|
|
57
|
-
let base64 = data.base64EncodedString()
|
|
58
|
-
resolve(base64)
|
|
59
|
-
} else {
|
|
60
|
-
reject("ENCODING_FAILED", "Failed to encode image", nil)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
private func applyMasking(to image: UIImage, in window: UIWindow) -> UIImage {
|
|
66
|
-
return applyMaskingWithOptions(to: image, in: window, options: [:])
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
private func applyMaskingWithOptions(to image: UIImage, in window: UIWindow, options: NSDictionary) -> UIImage {
|
|
70
|
-
UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale)
|
|
71
|
-
guard let context = UIGraphicsGetCurrentContext() else { return image }
|
|
72
|
-
|
|
73
|
-
// Draw the original image
|
|
74
|
-
image.draw(in: CGRect(origin: .zero, size: image.size))
|
|
75
|
-
|
|
76
|
-
// Find and mask sensitive elements
|
|
77
|
-
let sensitiveElements = findSensitiveElements(in: window)
|
|
78
|
-
|
|
79
|
-
for element in sensitiveElements {
|
|
80
|
-
let frame = element.frame
|
|
81
|
-
let maskingType = getMaskingType(for: element)
|
|
82
|
-
|
|
83
|
-
switch maskingType {
|
|
84
|
-
case .blur:
|
|
85
|
-
applyBlurMask(in: context, frame: frame)
|
|
86
|
-
case .rectangle:
|
|
87
|
-
applyRectangleMask(in: context, frame: frame)
|
|
88
|
-
case .pixelate:
|
|
89
|
-
applyPixelateMask(in: context, frame: frame, image: image)
|
|
90
|
-
case .none:
|
|
91
|
-
break
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
let maskedImage = UIGraphicsGetImageFromCurrentImageContext() ?? image
|
|
96
|
-
UIGraphicsEndImageContext()
|
|
97
|
-
|
|
98
|
-
return maskedImage
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
private func findSensitiveElements(in view: UIView) -> [UIView] {
|
|
102
|
-
var sensitiveElements: [UIView] = []
|
|
103
|
-
|
|
104
|
-
func traverseView(_ view: UIView) {
|
|
105
|
-
// Check if this view should be masked
|
|
106
|
-
if shouldMaskView(view) {
|
|
107
|
-
sensitiveElements.append(view)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Recursively check subviews
|
|
111
|
-
for subview in view.subviews {
|
|
112
|
-
traverseView(subview)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
traverseView(view)
|
|
117
|
-
return sensitiveElements
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
private func shouldMaskView(_ view: UIView) -> Bool {
|
|
121
|
-
// Check for UITextField - mask all text fields when inputMasking is enabled
|
|
122
|
-
if view is UITextField {
|
|
123
|
-
return true
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Check for UITextView - mask all text views when inputMasking is enabled
|
|
127
|
-
if view is UITextView {
|
|
128
|
-
return true
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return false
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
private func getMaskingType(for view: UIView) -> MaskingType {
|
|
135
|
-
// Default masking type for all text inputs
|
|
136
|
-
return .rectangle
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
private func applyBlurMask(in context: CGContext, frame: CGRect) {
|
|
140
|
-
// Create a blur effect
|
|
141
|
-
context.setFillColor(UIColor.black.withAlphaComponent(0.8).cgColor)
|
|
142
|
-
context.fill(frame)
|
|
143
|
-
|
|
144
|
-
// Add some noise to make it look blurred
|
|
145
|
-
context.setFillColor(UIColor.white.withAlphaComponent(0.3).cgColor)
|
|
146
|
-
for _ in 0..<20 {
|
|
147
|
-
let randomX = frame.origin.x + CGFloat.random(in: 0...frame.width)
|
|
148
|
-
let randomY = frame.origin.y + CGFloat.random(in: 0...frame.height)
|
|
149
|
-
let randomSize = CGFloat.random(in: 2...8)
|
|
150
|
-
context.fillEllipse(in: CGRect(x: randomX, y: randomY, width: randomSize, height: randomSize))
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
private func applyRectangleMask(in context: CGContext, frame: CGRect) {
|
|
155
|
-
// Simple rectangle fill
|
|
156
|
-
context.setFillColor(UIColor.gray.cgColor)
|
|
157
|
-
context.fill(frame)
|
|
158
|
-
|
|
159
|
-
// Add some text-like pattern
|
|
160
|
-
context.setFillColor(UIColor.darkGray.cgColor)
|
|
161
|
-
let lineHeight: CGFloat = 4
|
|
162
|
-
let spacing: CGFloat = 8
|
|
163
|
-
|
|
164
|
-
for i in stride(from: frame.origin.y + spacing, to: frame.origin.y + frame.height - spacing, by: lineHeight + spacing) {
|
|
165
|
-
let lineWidth = CGFloat.random(in: frame.width * 0.3...frame.width * 0.8)
|
|
166
|
-
let lineX = frame.origin.x + CGFloat.random(in: 0...(frame.width - lineWidth))
|
|
167
|
-
context.fill(CGRect(x: lineX, y: i, width: lineWidth, height: lineHeight))
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
private func applyPixelateMask(in context: CGContext, frame: CGRect, image: UIImage) {
|
|
172
|
-
// Create a pixelated effect
|
|
173
|
-
let pixelSize: CGFloat = 8
|
|
174
|
-
let pixelCountX = Int(frame.width / pixelSize)
|
|
175
|
-
let pixelCountY = Int(frame.height / pixelSize)
|
|
176
|
-
|
|
177
|
-
for x in 0..<pixelCountX {
|
|
178
|
-
for y in 0..<pixelCountY {
|
|
179
|
-
let pixelFrame = CGRect(
|
|
180
|
-
x: frame.origin.x + CGFloat(x) * pixelSize,
|
|
181
|
-
y: frame.origin.y + CGFloat(y) * pixelSize,
|
|
182
|
-
width: pixelSize,
|
|
183
|
-
height: pixelSize
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
// Use a random color for each pixel
|
|
187
|
-
let randomColor = UIColor(
|
|
188
|
-
red: CGFloat.random(in: 0...1),
|
|
189
|
-
green: CGFloat.random(in: 0...1),
|
|
190
|
-
blue: CGFloat.random(in: 0...1),
|
|
191
|
-
alpha: 1.0
|
|
192
|
-
)
|
|
193
|
-
context.setFillColor(randomColor.cgColor)
|
|
194
|
-
context.fill(pixelFrame)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
private enum MaskingType {
|
|
201
|
-
case blur
|
|
202
|
-
case rectangle
|
|
203
|
-
case pixelate
|
|
204
|
-
case none
|
|
205
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
|
|
3
|
-
package = JSON.parse(File.read(File.join(__dir__, "..", "package.json")))
|
|
4
|
-
|
|
5
|
-
Pod::Spec.new do |s|
|
|
6
|
-
s.name = "SessionRecorderNative"
|
|
7
|
-
s.version = package["version"]
|
|
8
|
-
s.summary = "Native session recorder module for React Native"
|
|
9
|
-
s.description = "A native module that provides session recording with automatic masking of sensitive UI elements"
|
|
10
|
-
s.homepage = "https://github.com/multiplayer-app/multiplayer-session-recorder-javascript"
|
|
11
|
-
s.license = "MIT"
|
|
12
|
-
s.authors = { "Multiplayer Software, Inc." => "https://www.multiplayer.app" }
|
|
13
|
-
s.platforms = { :ios => "12.0" }
|
|
14
|
-
s.source = { :git => "https://github.com/multiplayer-app/multiplayer-session-recorder-javascript.git", :tag => "#{s.version}" }
|
|
15
|
-
|
|
16
|
-
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
17
|
-
s.requires_arc = true
|
|
18
|
-
|
|
19
|
-
s.dependency "React-Core"
|
|
20
|
-
s.dependency "React"
|
|
21
|
-
end
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Build script to automatically extract app metadata from configuration files
|
|
5
|
-
* This runs without developer intervention and generates app-metadata.ts
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const fs = require('fs')
|
|
9
|
-
const path = require('path')
|
|
10
|
-
|
|
11
|
-
function findProjectRoot() {
|
|
12
|
-
let currentDir = process.cwd()
|
|
13
|
-
|
|
14
|
-
// Look for package.json going up the directory tree
|
|
15
|
-
while (currentDir !== path.dirname(currentDir)) {
|
|
16
|
-
if (fs.existsSync(path.join(currentDir, 'package.json'))) {
|
|
17
|
-
return currentDir
|
|
18
|
-
}
|
|
19
|
-
currentDir = path.dirname(currentDir)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return process.cwd()
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function extractAppMetadata(projectRoot) {
|
|
26
|
-
const metadata = {
|
|
27
|
-
name: undefined,
|
|
28
|
-
version: undefined,
|
|
29
|
-
bundleId: undefined,
|
|
30
|
-
buildNumber: undefined,
|
|
31
|
-
displayName: undefined,
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
// Method 1: Try app.json
|
|
36
|
-
const appJsonPath = path.join(projectRoot, 'app.json')
|
|
37
|
-
if (fs.existsSync(appJsonPath)) {
|
|
38
|
-
const appConfig = JSON.parse(fs.readFileSync(appJsonPath, 'utf8'))
|
|
39
|
-
|
|
40
|
-
metadata.name = appConfig.name || appConfig.displayName
|
|
41
|
-
metadata.version = appConfig.version
|
|
42
|
-
metadata.displayName = appConfig.displayName
|
|
43
|
-
|
|
44
|
-
// Extract bundle ID from platform-specific configs
|
|
45
|
-
if (appConfig.ios?.bundleIdentifier) {
|
|
46
|
-
metadata.bundleId = appConfig.ios.bundleIdentifier
|
|
47
|
-
} else if (appConfig.android?.package) {
|
|
48
|
-
metadata.bundleId = appConfig.android.package
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (appConfig.ios?.buildNumber) {
|
|
52
|
-
metadata.buildNumber = appConfig.ios.buildNumber.toString()
|
|
53
|
-
} else if (appConfig.android?.versionCode) {
|
|
54
|
-
metadata.buildNumber = appConfig.android.versionCode.toString()
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
console.log('✅ Extracted metadata from app.json')
|
|
58
|
-
return metadata
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Method 2: Try app.config.js
|
|
62
|
-
const appConfigJsPath = path.join(projectRoot, 'app.config.js')
|
|
63
|
-
if (fs.existsSync(appConfigJsPath)) {
|
|
64
|
-
try {
|
|
65
|
-
// Clear require cache to get fresh config
|
|
66
|
-
delete require.cache[require.resolve(appConfigJsPath)]
|
|
67
|
-
const appConfig = require(appConfigJsPath)
|
|
68
|
-
|
|
69
|
-
metadata.name = appConfig.name || appConfig.displayName
|
|
70
|
-
metadata.version = appConfig.version
|
|
71
|
-
metadata.displayName = appConfig.displayName
|
|
72
|
-
|
|
73
|
-
// Extract bundle ID from platform-specific configs
|
|
74
|
-
if (appConfig.ios?.bundleIdentifier) {
|
|
75
|
-
metadata.bundleId = appConfig.ios.bundleIdentifier
|
|
76
|
-
} else if (appConfig.android?.package) {
|
|
77
|
-
metadata.bundleId = appConfig.android.package
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (appConfig.ios?.buildNumber) {
|
|
81
|
-
metadata.buildNumber = appConfig.ios.buildNumber.toString()
|
|
82
|
-
} else if (appConfig.android?.versionCode) {
|
|
83
|
-
metadata.buildNumber = appConfig.android.versionCode.toString()
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
console.log('✅ Extracted metadata from app.config.js')
|
|
87
|
-
return metadata
|
|
88
|
-
} catch (error) {
|
|
89
|
-
console.warn('⚠️ Could not parse app.config.js:', error.message)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Method 3: Fallback to package.json
|
|
94
|
-
const packageJsonPath = path.join(projectRoot, 'package.json')
|
|
95
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
96
|
-
const packageConfig = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
|
97
|
-
|
|
98
|
-
metadata.name = packageConfig.name
|
|
99
|
-
metadata.version = packageConfig.version
|
|
100
|
-
|
|
101
|
-
console.log('✅ Extracted metadata from package.json')
|
|
102
|
-
return metadata
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
} catch (error) {
|
|
106
|
-
console.warn('⚠️ Error extracting app metadata:', error.message)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return metadata
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function generateAppMetadataFile(metadata, outputPath) {
|
|
113
|
-
const content = `/**
|
|
114
|
-
* Auto-generated app metadata
|
|
115
|
-
* This file is generated at build time to provide app metadata without developer intervention
|
|
116
|
-
*/
|
|
117
|
-
|
|
118
|
-
// This file is automatically generated by the build process
|
|
119
|
-
// It extracts metadata from app.json, app.config.js, or package.json
|
|
120
|
-
|
|
121
|
-
export interface AppMetadata {
|
|
122
|
-
name?: string
|
|
123
|
-
version?: string
|
|
124
|
-
bundleId?: string
|
|
125
|
-
buildNumber?: string
|
|
126
|
-
displayName?: string
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Auto-detected values from project configuration files
|
|
130
|
-
export const APP_METADATA: AppMetadata = {
|
|
131
|
-
name: ${metadata.name ? `'${metadata.name}'` : 'undefined'},
|
|
132
|
-
version: ${metadata.version ? `'${metadata.version}'` : 'undefined'},
|
|
133
|
-
bundleId: ${metadata.bundleId ? `'${metadata.bundleId}'` : 'undefined'},
|
|
134
|
-
buildNumber: ${metadata.buildNumber ? `'${metadata.buildNumber}'` : 'undefined'},
|
|
135
|
-
displayName: ${metadata.displayName ? `'${metadata.displayName}'` : 'undefined'},
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Get auto-detected app metadata
|
|
140
|
-
*/
|
|
141
|
-
export function getAutoDetectedAppMetadata(): AppMetadata {
|
|
142
|
-
return { ...APP_METADATA }
|
|
143
|
-
}
|
|
144
|
-
`
|
|
145
|
-
|
|
146
|
-
fs.writeFileSync(outputPath, content, 'utf8')
|
|
147
|
-
console.log(`✅ Generated app-metadata.ts`)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function main() {
|
|
151
|
-
const projectRoot = findProjectRoot()
|
|
152
|
-
console.log(`🔍 Looking for app metadata in: ${projectRoot}`)
|
|
153
|
-
|
|
154
|
-
const metadata = extractAppMetadata(projectRoot)
|
|
155
|
-
|
|
156
|
-
// Show what was detected
|
|
157
|
-
console.log('📋 Detected metadata:')
|
|
158
|
-
Object.entries(metadata).forEach(([key, value]) => {
|
|
159
|
-
if (value) {
|
|
160
|
-
console.log(` ${key}: ${value}`)
|
|
161
|
-
}
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
// Generate the TypeScript file
|
|
165
|
-
const outputPath = path.join(__dirname, '../src/utils/app-metadata.ts')
|
|
166
|
-
generateAppMetadataFile(metadata, outputPath)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (require.main === module) {
|
|
170
|
-
main()
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
module.exports = { extractAppMetadata, generateAppMetadataFile }
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import React, { ReactNode, useCallback, useMemo } from 'react'
|
|
2
|
-
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'
|
|
3
|
-
|
|
4
|
-
export interface GestureCaptureWrapperProps {
|
|
5
|
-
children: ReactNode
|
|
6
|
-
onGestureRecord: (gestureType: string, data: any) => void
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const GestureCaptureWrapper: React.FC<GestureCaptureWrapperProps> = ({ children, onGestureRecord }) => {
|
|
10
|
-
const recordGesture = useCallback(
|
|
11
|
-
(gestureType: string, data: any) => {
|
|
12
|
-
// Record with session recorder
|
|
13
|
-
onGestureRecord(gestureType, data)
|
|
14
|
-
},
|
|
15
|
-
[onGestureRecord]
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
// Create tap gesture
|
|
19
|
-
const tapGesture = useMemo(() => {
|
|
20
|
-
return Gesture.Tap()
|
|
21
|
-
.runOnJS(true)
|
|
22
|
-
.onStart((event) => {
|
|
23
|
-
recordGesture('tap', {
|
|
24
|
-
x: event.x,
|
|
25
|
-
y: event.y,
|
|
26
|
-
timestamp: Date.now()
|
|
27
|
-
})
|
|
28
|
-
})
|
|
29
|
-
}, [recordGesture])
|
|
30
|
-
|
|
31
|
-
// Create pan gesture (for swipes and drags)
|
|
32
|
-
const panGesture = useMemo(() => {
|
|
33
|
-
return Gesture.Pan()
|
|
34
|
-
.runOnJS(true)
|
|
35
|
-
.onStart((event) => {
|
|
36
|
-
recordGesture('pan_start', {
|
|
37
|
-
x: event.x,
|
|
38
|
-
y: event.y,
|
|
39
|
-
timestamp: Date.now()
|
|
40
|
-
})
|
|
41
|
-
})
|
|
42
|
-
.onUpdate((event) => {
|
|
43
|
-
recordGesture('pan_update', {
|
|
44
|
-
x: event.x,
|
|
45
|
-
y: event.y,
|
|
46
|
-
translationX: event.translationX,
|
|
47
|
-
translationY: event.translationY,
|
|
48
|
-
velocityX: event.velocityX,
|
|
49
|
-
velocityY: event.velocityY,
|
|
50
|
-
timestamp: Date.now()
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
.onEnd((event) => {
|
|
54
|
-
recordGesture('pan_end', {
|
|
55
|
-
x: event.x,
|
|
56
|
-
y: event.y,
|
|
57
|
-
translationX: event.translationX,
|
|
58
|
-
translationY: event.translationY,
|
|
59
|
-
velocityX: event.velocityX,
|
|
60
|
-
velocityY: event.velocityY,
|
|
61
|
-
timestamp: Date.now()
|
|
62
|
-
})
|
|
63
|
-
})
|
|
64
|
-
}, [recordGesture])
|
|
65
|
-
|
|
66
|
-
// Create long press gesture
|
|
67
|
-
const longPressGesture = useMemo(() => {
|
|
68
|
-
return Gesture.LongPress()
|
|
69
|
-
.runOnJS(true)
|
|
70
|
-
.minDuration(500)
|
|
71
|
-
.onStart((event) => {
|
|
72
|
-
recordGesture('long_press', {
|
|
73
|
-
x: event.x,
|
|
74
|
-
y: event.y,
|
|
75
|
-
duration: 500,
|
|
76
|
-
timestamp: Date.now()
|
|
77
|
-
})
|
|
78
|
-
})
|
|
79
|
-
}, [recordGesture])
|
|
80
|
-
|
|
81
|
-
return (
|
|
82
|
-
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
83
|
-
<GestureDetector gesture={Gesture.Simultaneous(tapGesture, panGesture, longPressGesture)}>{children}</GestureDetector>
|
|
84
|
-
</GestureHandlerRootView>
|
|
85
|
-
)
|
|
86
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./GestureCaptureWrapper";
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import SessionRecorder from '@multiplayer-app/session-recorder-react-native'
|
|
2
|
-
import React, { PropsWithChildren, useCallback } from 'react'
|
|
3
|
-
import { View } from 'react-native'
|
|
4
|
-
import { SessionState } from '../../types'
|
|
5
|
-
import { logger } from '../../utils'
|
|
6
|
-
import { GestureCaptureWrapper } from '../GestureCaptureWrapper'
|
|
7
|
-
|
|
8
|
-
interface ScreenRecorderViewProps extends PropsWithChildren {}
|
|
9
|
-
|
|
10
|
-
export const ScreenRecorderView = ({ children }: ScreenRecorderViewProps) => {
|
|
11
|
-
// Set up gesture recording callback
|
|
12
|
-
const handleGestureRecord = useCallback((gestureType: string, data: any) => {
|
|
13
|
-
if (SessionRecorder.sessionState !== SessionState.started) {
|
|
14
|
-
logger.debug('SessionRecorderContext', 'Gesture recording skipped', {
|
|
15
|
-
client: !!SessionRecorder.sessionState,
|
|
16
|
-
sessionState: SessionRecorder.sessionState
|
|
17
|
-
})
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
logger.debug('SessionRecorderContext', 'Gesture recorded', { gestureType, data })
|
|
21
|
-
try {
|
|
22
|
-
// Record gesture as appropriate touch events
|
|
23
|
-
switch (gestureType) {
|
|
24
|
-
case 'tap':
|
|
25
|
-
// For tap, record both touch start and end
|
|
26
|
-
logger.debug('SessionRecorderContext', 'Recording tap as touch start + end')
|
|
27
|
-
SessionRecorder.recordTouchStart?.(data.x, data.y, undefined, 1.0)
|
|
28
|
-
SessionRecorder.recordTouchEnd?.(data.x, data.y, undefined, 1.0)
|
|
29
|
-
break
|
|
30
|
-
|
|
31
|
-
case 'pan_start':
|
|
32
|
-
logger.debug('SessionRecorderContext', 'Recording pan_start as touch start')
|
|
33
|
-
SessionRecorder.recordTouchStart?.(data.x, data.y, undefined, 1.0)
|
|
34
|
-
break
|
|
35
|
-
|
|
36
|
-
case 'pan_update':
|
|
37
|
-
logger.debug('SessionRecorderContext', 'Recording pan_update as touch move')
|
|
38
|
-
SessionRecorder.recordTouchMove?.(data.x, data.y, undefined, 1.0)
|
|
39
|
-
break
|
|
40
|
-
|
|
41
|
-
case 'pan_end':
|
|
42
|
-
logger.debug('SessionRecorderContext', 'Recording pan_end as touch end')
|
|
43
|
-
SessionRecorder.recordTouchEnd?.(data.x, data.y, undefined, 1.0)
|
|
44
|
-
break
|
|
45
|
-
|
|
46
|
-
case 'long_press':
|
|
47
|
-
logger.debug('SessionRecorderContext', 'Recording long_press as touch start + end')
|
|
48
|
-
SessionRecorder.recordTouchStart?.(data.x, data.y, undefined, 1.0)
|
|
49
|
-
SessionRecorder.recordTouchEnd?.(data.x, data.y, undefined, 1.0)
|
|
50
|
-
break
|
|
51
|
-
default:
|
|
52
|
-
}
|
|
53
|
-
} catch (error) {
|
|
54
|
-
logger.error('SessionRecorderContext', 'Failed to record gesture event', error)
|
|
55
|
-
}
|
|
56
|
-
}, [])
|
|
57
|
-
|
|
58
|
-
// Callback ref to set the viewshot ref immediately when available
|
|
59
|
-
const setViewShotRef = (ref: View | null) => {
|
|
60
|
-
if (ref) {
|
|
61
|
-
SessionRecorder.setViewShotRef?.(ref)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return (
|
|
66
|
-
<GestureCaptureWrapper onGestureRecord={handleGestureRecord}>
|
|
67
|
-
<View ref={setViewShotRef} style={{ flex: 1 }}>
|
|
68
|
-
{children}
|
|
69
|
-
</View>
|
|
70
|
-
</GestureCaptureWrapper>
|
|
71
|
-
)
|
|
72
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./ScreenRecorderView";
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
2
|
-
import { View, Text, Pressable, TextInput, Alert } from 'react-native'
|
|
3
|
-
import { WidgetTextOverridesConfig } from '../../types'
|
|
4
|
-
import { sharedStyles } from './styles'
|
|
5
|
-
import ModalHeader from './ModalHeader'
|
|
6
|
-
|
|
7
|
-
interface FinalPopoverProps {
|
|
8
|
-
textOverrides: WidgetTextOverridesConfig
|
|
9
|
-
onStopRecording: (comment: string) => void
|
|
10
|
-
onCancelSession: () => void
|
|
11
|
-
onClose: () => void
|
|
12
|
-
isSubmitting: boolean
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const FinalPopover: React.FC<FinalPopoverProps> = ({ textOverrides, onStopRecording, onCancelSession, isSubmitting }) => {
|
|
16
|
-
const [comment, setComment] = useState('')
|
|
17
|
-
|
|
18
|
-
const handleStopRecording = async () => {
|
|
19
|
-
try {
|
|
20
|
-
await onStopRecording(comment)
|
|
21
|
-
} catch (error) {
|
|
22
|
-
Alert.alert('Error', 'Failed to save session')
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<View style={sharedStyles.popoverContent}>
|
|
28
|
-
<ModalHeader>
|
|
29
|
-
<Pressable onPress={onCancelSession} style={sharedStyles.cancelButton}>
|
|
30
|
-
<Text style={sharedStyles.cancelButtonText}>{textOverrides.cancelButtonText}</Text>
|
|
31
|
-
</Pressable>
|
|
32
|
-
</ModalHeader>
|
|
33
|
-
|
|
34
|
-
<View style={sharedStyles.popoverBody}>
|
|
35
|
-
<Text style={sharedStyles.title}>{textOverrides.finalTitle}</Text>
|
|
36
|
-
<Text style={sharedStyles.description}>{textOverrides.finalDescription}</Text>
|
|
37
|
-
|
|
38
|
-
<TextInput
|
|
39
|
-
style={sharedStyles.commentInput}
|
|
40
|
-
placeholder={textOverrides.commentPlaceholder}
|
|
41
|
-
value={comment}
|
|
42
|
-
onChangeText={setComment}
|
|
43
|
-
multiline
|
|
44
|
-
numberOfLines={3}
|
|
45
|
-
textAlignVertical='top'
|
|
46
|
-
/>
|
|
47
|
-
|
|
48
|
-
<View style={sharedStyles.popoverFooter}>
|
|
49
|
-
<Pressable
|
|
50
|
-
style={[sharedStyles.actionButton, sharedStyles.stopButton]}
|
|
51
|
-
onPress={handleStopRecording}
|
|
52
|
-
disabled={isSubmitting}
|
|
53
|
-
>
|
|
54
|
-
<Text style={sharedStyles.actionButtonText}>{isSubmitting ? 'Saving...' : textOverrides.saveButtonText}</Text>
|
|
55
|
-
</Pressable>
|
|
56
|
-
</View>
|
|
57
|
-
</View>
|
|
58
|
-
</View>
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export default FinalPopover
|