yaver-feedback-react-native 0.2.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 +690 -0
- package/app.plugin.js +70 -0
- package/package.json +39 -0
- package/src/BlackBox.ts +317 -0
- package/src/ConnectionScreen.tsx +400 -0
- package/src/Discovery.ts +223 -0
- package/src/FeedbackModal.tsx +678 -0
- package/src/FixReport.tsx +313 -0
- package/src/FloatingButton.tsx +860 -0
- package/src/P2PClient.ts +303 -0
- package/src/ShakeDetector.ts +57 -0
- package/src/YaverFeedback.ts +345 -0
- package/src/__tests__/Discovery.test.ts +187 -0
- package/src/__tests__/P2PClient.test.ts +218 -0
- package/src/__tests__/SDKToken.test.ts +268 -0
- package/src/__tests__/YaverFeedback.test.ts +189 -0
- package/src/__tests__/types.test.ts +247 -0
- package/src/capture.ts +84 -0
- package/src/expo.ts +62 -0
- package/src/index.ts +54 -0
- package/src/types.ts +251 -0
- package/src/upload.ts +74 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { YaverFeedback } from '../YaverFeedback';
|
|
2
|
+
|
|
3
|
+
// Mock react-native DeviceEventEmitter
|
|
4
|
+
jest.mock('react-native', () => ({
|
|
5
|
+
DeviceEventEmitter: {
|
|
6
|
+
emit: jest.fn(),
|
|
7
|
+
},
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
// Mock Discovery
|
|
11
|
+
jest.mock('../Discovery', () => ({
|
|
12
|
+
YaverDiscovery: {
|
|
13
|
+
discover: jest.fn(),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Reset module-level state between tests by re-requiring
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
// YaverFeedback uses module-level variables (config, enabled, p2pClient).
|
|
20
|
+
// We reset them by calling init with a known state or relying on isInitialized checks.
|
|
21
|
+
// For a clean slate, we re-init with enabled=false then verify.
|
|
22
|
+
jest.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('YaverFeedback', () => {
|
|
26
|
+
describe('init()', () => {
|
|
27
|
+
it('sets config correctly with defaults', () => {
|
|
28
|
+
YaverFeedback.init({
|
|
29
|
+
authToken: 'test-token',
|
|
30
|
+
agentUrl: 'http://localhost:18080',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const cfg = YaverFeedback.getConfig();
|
|
34
|
+
expect(cfg).not.toBeNull();
|
|
35
|
+
expect(cfg!.authToken).toBe('test-token');
|
|
36
|
+
expect(cfg!.agentUrl).toBe('http://localhost:18080');
|
|
37
|
+
expect(cfg!.trigger).toBe('shake');
|
|
38
|
+
expect(cfg!.maxRecordingDuration).toBe(120);
|
|
39
|
+
expect(cfg!.feedbackMode).toBe('batch');
|
|
40
|
+
expect(cfg!.agentCommentaryLevel).toBe(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('respects user-provided values over defaults', () => {
|
|
44
|
+
YaverFeedback.init({
|
|
45
|
+
authToken: 'tok',
|
|
46
|
+
trigger: 'floating-button',
|
|
47
|
+
maxRecordingDuration: 60,
|
|
48
|
+
feedbackMode: 'live',
|
|
49
|
+
agentCommentaryLevel: 7,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const cfg = YaverFeedback.getConfig();
|
|
53
|
+
expect(cfg!.trigger).toBe('floating-button');
|
|
54
|
+
expect(cfg!.maxRecordingDuration).toBe(60);
|
|
55
|
+
expect(cfg!.feedbackMode).toBe('live');
|
|
56
|
+
expect(cfg!.agentCommentaryLevel).toBe(7);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('with enabled=false sets enabled to false', () => {
|
|
60
|
+
YaverFeedback.init({
|
|
61
|
+
authToken: 'tok',
|
|
62
|
+
enabled: false,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(YaverFeedback.isInitialized()).toBe(true);
|
|
66
|
+
expect(YaverFeedback.isEnabled()).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('with enabled=true sets enabled to true', () => {
|
|
70
|
+
YaverFeedback.init({
|
|
71
|
+
authToken: 'tok',
|
|
72
|
+
enabled: true,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(YaverFeedback.isEnabled()).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('creates P2PClient when agentUrl is provided', () => {
|
|
79
|
+
YaverFeedback.init({
|
|
80
|
+
authToken: 'tok',
|
|
81
|
+
agentUrl: 'http://192.168.1.10:18080',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(YaverFeedback.getP2PClient()).not.toBeNull();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('does not create P2PClient when agentUrl is omitted', () => {
|
|
88
|
+
YaverFeedback.init({
|
|
89
|
+
authToken: 'tok',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(YaverFeedback.getP2PClient()).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('isInitialized()', () => {
|
|
97
|
+
it('returns true after init()', () => {
|
|
98
|
+
YaverFeedback.init({ authToken: 'tok' });
|
|
99
|
+
expect(YaverFeedback.isInitialized()).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('setEnabled()', () => {
|
|
104
|
+
it('toggles enabled state', () => {
|
|
105
|
+
YaverFeedback.init({ authToken: 'tok', enabled: true });
|
|
106
|
+
expect(YaverFeedback.isEnabled()).toBe(true);
|
|
107
|
+
|
|
108
|
+
YaverFeedback.setEnabled(false);
|
|
109
|
+
expect(YaverFeedback.isEnabled()).toBe(false);
|
|
110
|
+
|
|
111
|
+
YaverFeedback.setEnabled(true);
|
|
112
|
+
expect(YaverFeedback.isEnabled()).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('getConfig()', () => {
|
|
117
|
+
it('returns the config after init', () => {
|
|
118
|
+
YaverFeedback.init({
|
|
119
|
+
authToken: 'my-token',
|
|
120
|
+
agentUrl: 'http://10.0.0.1:18080',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const cfg = YaverFeedback.getConfig();
|
|
124
|
+
expect(cfg).toBeDefined();
|
|
125
|
+
expect(cfg!.authToken).toBe('my-token');
|
|
126
|
+
expect(cfg!.agentUrl).toBe('http://10.0.0.1:18080');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('getFeedbackMode()', () => {
|
|
131
|
+
it('defaults to batch when no config', () => {
|
|
132
|
+
// After any init, feedbackMode defaults to 'batch'
|
|
133
|
+
YaverFeedback.init({ authToken: 'tok' });
|
|
134
|
+
expect(YaverFeedback.getFeedbackMode()).toBe('batch');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('returns configured mode', () => {
|
|
138
|
+
YaverFeedback.init({ authToken: 'tok', feedbackMode: 'narrated' });
|
|
139
|
+
expect(YaverFeedback.getFeedbackMode()).toBe('narrated');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('returns live when configured', () => {
|
|
143
|
+
YaverFeedback.init({ authToken: 'tok', feedbackMode: 'live' });
|
|
144
|
+
expect(YaverFeedback.getFeedbackMode()).toBe('live');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('getCommentaryLevel()', () => {
|
|
149
|
+
it('defaults to 0', () => {
|
|
150
|
+
YaverFeedback.init({ authToken: 'tok' });
|
|
151
|
+
expect(YaverFeedback.getCommentaryLevel()).toBe(0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('returns configured level', () => {
|
|
155
|
+
YaverFeedback.init({ authToken: 'tok', agentCommentaryLevel: 5 });
|
|
156
|
+
expect(YaverFeedback.getCommentaryLevel()).toBe(5);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('returns max level when set to 10', () => {
|
|
160
|
+
YaverFeedback.init({ authToken: 'tok', agentCommentaryLevel: 10 });
|
|
161
|
+
expect(YaverFeedback.getCommentaryLevel()).toBe(10);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('startReport()', () => {
|
|
166
|
+
it('does nothing when not enabled', async () => {
|
|
167
|
+
YaverFeedback.init({ authToken: 'tok', enabled: false });
|
|
168
|
+
|
|
169
|
+
// Should not throw
|
|
170
|
+
await YaverFeedback.startReport();
|
|
171
|
+
|
|
172
|
+
const { DeviceEventEmitter } = require('react-native');
|
|
173
|
+
expect(DeviceEventEmitter.emit).not.toHaveBeenCalled();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('emits event when enabled and agentUrl is set', async () => {
|
|
177
|
+
YaverFeedback.init({
|
|
178
|
+
authToken: 'tok',
|
|
179
|
+
enabled: true,
|
|
180
|
+
agentUrl: 'http://localhost:18080',
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await YaverFeedback.startReport();
|
|
184
|
+
|
|
185
|
+
const { DeviceEventEmitter } = require('react-native');
|
|
186
|
+
expect(DeviceEventEmitter.emit).toHaveBeenCalledWith('yaverFeedback:startReport');
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FeedbackConfig,
|
|
3
|
+
FeedbackBundle,
|
|
4
|
+
FeedbackMetadata,
|
|
5
|
+
TimelineEvent,
|
|
6
|
+
DeviceInfo,
|
|
7
|
+
AppInfo,
|
|
8
|
+
FeedbackReport,
|
|
9
|
+
AgentCommentary,
|
|
10
|
+
FeedbackStreamEvent,
|
|
11
|
+
} from '../types';
|
|
12
|
+
|
|
13
|
+
describe('React Native SDK types', () => {
|
|
14
|
+
describe('FeedbackConfig', () => {
|
|
15
|
+
it('can be constructed with only required fields', () => {
|
|
16
|
+
const config: FeedbackConfig = {
|
|
17
|
+
authToken: 'test-token',
|
|
18
|
+
};
|
|
19
|
+
expect(config.authToken).toBe('test-token');
|
|
20
|
+
expect(config.agentUrl).toBeUndefined();
|
|
21
|
+
expect(config.trigger).toBeUndefined();
|
|
22
|
+
expect(config.enabled).toBeUndefined();
|
|
23
|
+
expect(config.maxRecordingDuration).toBeUndefined();
|
|
24
|
+
expect(config.feedbackMode).toBeUndefined();
|
|
25
|
+
expect(config.agentCommentaryLevel).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('can be constructed with all optional fields', () => {
|
|
29
|
+
const config: FeedbackConfig = {
|
|
30
|
+
authToken: 'tok',
|
|
31
|
+
agentUrl: 'http://192.168.1.10:18080',
|
|
32
|
+
trigger: 'shake',
|
|
33
|
+
enabled: true,
|
|
34
|
+
maxRecordingDuration: 60,
|
|
35
|
+
feedbackMode: 'live',
|
|
36
|
+
agentCommentaryLevel: 7,
|
|
37
|
+
};
|
|
38
|
+
expect(config.trigger).toBe('shake');
|
|
39
|
+
expect(config.feedbackMode).toBe('live');
|
|
40
|
+
expect(config.agentCommentaryLevel).toBe(7);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('accepts all trigger types', () => {
|
|
44
|
+
const triggers: FeedbackConfig['trigger'][] = ['shake', 'floating-button', 'manual'];
|
|
45
|
+
triggers.forEach((trigger) => {
|
|
46
|
+
const config: FeedbackConfig = { authToken: 'tok', trigger };
|
|
47
|
+
expect(config.trigger).toBe(trigger);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('accepts all feedback modes', () => {
|
|
52
|
+
const modes: FeedbackConfig['feedbackMode'][] = ['live', 'narrated', 'batch'];
|
|
53
|
+
modes.forEach((mode) => {
|
|
54
|
+
const config: FeedbackConfig = { authToken: 'tok', feedbackMode: mode };
|
|
55
|
+
expect(config.feedbackMode).toBe(mode);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('FeedbackBundle', () => {
|
|
61
|
+
it('can be constructed with required fields', () => {
|
|
62
|
+
const bundle: FeedbackBundle = {
|
|
63
|
+
metadata: {
|
|
64
|
+
timestamp: '2026-03-24T12:00:00Z',
|
|
65
|
+
device: {
|
|
66
|
+
platform: 'ios',
|
|
67
|
+
osVersion: '18.0',
|
|
68
|
+
model: 'iPhone 16 Pro',
|
|
69
|
+
screenWidth: 393,
|
|
70
|
+
screenHeight: 852,
|
|
71
|
+
},
|
|
72
|
+
app: {
|
|
73
|
+
bundleId: 'com.example.app',
|
|
74
|
+
version: '1.0.0',
|
|
75
|
+
buildNumber: '42',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
screenshots: [],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
expect(bundle.metadata.timestamp).toBe('2026-03-24T12:00:00Z');
|
|
82
|
+
expect(bundle.metadata.device.platform).toBe('ios');
|
|
83
|
+
expect(bundle.screenshots).toEqual([]);
|
|
84
|
+
expect(bundle.video).toBeUndefined();
|
|
85
|
+
expect(bundle.audio).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('can include optional video, audio, and screenshots', () => {
|
|
89
|
+
const bundle: FeedbackBundle = {
|
|
90
|
+
metadata: {
|
|
91
|
+
timestamp: '2026-03-24T12:00:00Z',
|
|
92
|
+
device: {
|
|
93
|
+
platform: 'android',
|
|
94
|
+
osVersion: '15',
|
|
95
|
+
model: 'Pixel 9',
|
|
96
|
+
screenWidth: 412,
|
|
97
|
+
screenHeight: 915,
|
|
98
|
+
},
|
|
99
|
+
app: {},
|
|
100
|
+
userNote: 'This button does not work',
|
|
101
|
+
},
|
|
102
|
+
video: '/tmp/recording.mp4',
|
|
103
|
+
audio: '/tmp/voice.m4a',
|
|
104
|
+
screenshots: ['/tmp/ss1.png', '/tmp/ss2.png'],
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
expect(bundle.video).toBe('/tmp/recording.mp4');
|
|
108
|
+
expect(bundle.audio).toBe('/tmp/voice.m4a');
|
|
109
|
+
expect(bundle.screenshots).toHaveLength(2);
|
|
110
|
+
expect(bundle.metadata.userNote).toBe('This button does not work');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('TimelineEvent', () => {
|
|
115
|
+
it('has correct structure for screenshot event', () => {
|
|
116
|
+
const event: TimelineEvent = {
|
|
117
|
+
type: 'screenshot',
|
|
118
|
+
path: '/tmp/screenshot.png',
|
|
119
|
+
timestamp: '2026-03-24T12:00:05Z',
|
|
120
|
+
};
|
|
121
|
+
expect(event.type).toBe('screenshot');
|
|
122
|
+
expect(event.path).toBe('/tmp/screenshot.png');
|
|
123
|
+
expect(event.duration).toBeUndefined();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('supports optional duration for audio/video events', () => {
|
|
127
|
+
const event: TimelineEvent = {
|
|
128
|
+
type: 'audio',
|
|
129
|
+
path: '/tmp/voice.m4a',
|
|
130
|
+
timestamp: '2026-03-24T12:00:10Z',
|
|
131
|
+
duration: 15.5,
|
|
132
|
+
};
|
|
133
|
+
expect(event.duration).toBe(15.5);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('accepts all event types', () => {
|
|
137
|
+
const types: TimelineEvent['type'][] = ['screenshot', 'audio', 'video'];
|
|
138
|
+
types.forEach((type) => {
|
|
139
|
+
const event: TimelineEvent = { type, path: '/tmp/file', timestamp: 'now' };
|
|
140
|
+
expect(event.type).toBe(type);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('DeviceInfo', () => {
|
|
146
|
+
it('has all required fields', () => {
|
|
147
|
+
const device: DeviceInfo = {
|
|
148
|
+
platform: 'ios',
|
|
149
|
+
osVersion: '18.0',
|
|
150
|
+
model: 'iPhone 16 Pro Max',
|
|
151
|
+
screenWidth: 430,
|
|
152
|
+
screenHeight: 932,
|
|
153
|
+
};
|
|
154
|
+
expect(device.platform).toBe('ios');
|
|
155
|
+
expect(device.screenWidth).toBe(430);
|
|
156
|
+
expect(device.screenHeight).toBe(932);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('AppInfo', () => {
|
|
161
|
+
it('can be empty (all fields optional)', () => {
|
|
162
|
+
const app: AppInfo = {};
|
|
163
|
+
expect(app.bundleId).toBeUndefined();
|
|
164
|
+
expect(app.version).toBeUndefined();
|
|
165
|
+
expect(app.buildNumber).toBeUndefined();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('can have all fields populated', () => {
|
|
169
|
+
const app: AppInfo = {
|
|
170
|
+
bundleId: 'com.yaver.test',
|
|
171
|
+
version: '2.0.0',
|
|
172
|
+
buildNumber: '100',
|
|
173
|
+
};
|
|
174
|
+
expect(app.bundleId).toBe('com.yaver.test');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('FeedbackReport', () => {
|
|
179
|
+
it('has correct structure', () => {
|
|
180
|
+
const report: FeedbackReport = {
|
|
181
|
+
id: 'report-123',
|
|
182
|
+
bundle: {
|
|
183
|
+
metadata: {
|
|
184
|
+
timestamp: 'now',
|
|
185
|
+
device: { platform: 'ios', osVersion: '18', model: 'iPhone', screenWidth: 393, screenHeight: 852 },
|
|
186
|
+
app: {},
|
|
187
|
+
},
|
|
188
|
+
screenshots: [],
|
|
189
|
+
},
|
|
190
|
+
status: 'pending',
|
|
191
|
+
};
|
|
192
|
+
expect(report.status).toBe('pending');
|
|
193
|
+
expect(report.error).toBeUndefined();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('accepts all status values', () => {
|
|
197
|
+
const statuses: FeedbackReport['status'][] = ['pending', 'uploading', 'uploaded', 'failed'];
|
|
198
|
+
statuses.forEach((status) => {
|
|
199
|
+
const report: FeedbackReport = {
|
|
200
|
+
id: '1',
|
|
201
|
+
bundle: {
|
|
202
|
+
metadata: {
|
|
203
|
+
timestamp: 'now',
|
|
204
|
+
device: { platform: 'ios', osVersion: '18', model: 'iPhone', screenWidth: 393, screenHeight: 852 },
|
|
205
|
+
app: {},
|
|
206
|
+
},
|
|
207
|
+
screenshots: [],
|
|
208
|
+
},
|
|
209
|
+
status,
|
|
210
|
+
};
|
|
211
|
+
expect(report.status).toBe(status);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('AgentCommentary', () => {
|
|
217
|
+
it('has correct structure', () => {
|
|
218
|
+
const commentary: AgentCommentary = {
|
|
219
|
+
id: 'cmt-1',
|
|
220
|
+
timestamp: '2026-03-24T12:00:00Z',
|
|
221
|
+
message: 'I see a layout issue on the login screen',
|
|
222
|
+
type: 'observation',
|
|
223
|
+
};
|
|
224
|
+
expect(commentary.type).toBe('observation');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('accepts all commentary types', () => {
|
|
228
|
+
const types: AgentCommentary['type'][] = ['observation', 'suggestion', 'question', 'action'];
|
|
229
|
+
types.forEach((type) => {
|
|
230
|
+
const c: AgentCommentary = { id: '1', timestamp: 'now', message: 'test', type };
|
|
231
|
+
expect(c.type).toBe(type);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('FeedbackStreamEvent', () => {
|
|
237
|
+
it('has correct structure', () => {
|
|
238
|
+
const event: FeedbackStreamEvent = {
|
|
239
|
+
type: 'tap',
|
|
240
|
+
timestamp: '2026-03-24T12:00:00Z',
|
|
241
|
+
data: { x: 100, y: 200 },
|
|
242
|
+
};
|
|
243
|
+
expect(event.type).toBe('tap');
|
|
244
|
+
expect(event.data.x).toBe(100);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
package/src/capture.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen capture and audio recording helpers.
|
|
3
|
+
*
|
|
4
|
+
* Screenshot capture requires `react-native-view-shot` as a peer dependency.
|
|
5
|
+
* Audio recording requires `react-native-audio-recorder-player` or a
|
|
6
|
+
* similar library — the implementation below uses a minimal approach
|
|
7
|
+
* that works when one of those is available.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
let audioRecorderModule: any = null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Capture the current screen as a PNG image.
|
|
14
|
+
* Requires `react-native-view-shot` to be installed.
|
|
15
|
+
* @returns File path of the captured screenshot.
|
|
16
|
+
*/
|
|
17
|
+
export async function captureScreenshot(): Promise<string> {
|
|
18
|
+
try {
|
|
19
|
+
const ViewShot = require('react-native-view-shot');
|
|
20
|
+
const uri = await ViewShot.captureScreen({
|
|
21
|
+
format: 'png',
|
|
22
|
+
quality: 0.9,
|
|
23
|
+
});
|
|
24
|
+
return uri;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
'[YaverFeedback] Screenshot capture failed. Make sure react-native-view-shot is installed. ' +
|
|
28
|
+
String(err),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Start recording an audio voice note.
|
|
35
|
+
* Requires `react-native-audio-recorder-player` to be installed.
|
|
36
|
+
*/
|
|
37
|
+
export async function startAudioRecording(): Promise<void> {
|
|
38
|
+
try {
|
|
39
|
+
const AudioRecorderPlayer =
|
|
40
|
+
require('react-native-audio-recorder-player').default;
|
|
41
|
+
audioRecorderModule = new AudioRecorderPlayer();
|
|
42
|
+
await audioRecorderModule.startRecorder();
|
|
43
|
+
} catch (err) {
|
|
44
|
+
audioRecorderModule = null;
|
|
45
|
+
throw new Error(
|
|
46
|
+
'[YaverFeedback] Audio recording failed to start. Make sure react-native-audio-recorder-player is installed. ' +
|
|
47
|
+
String(err),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Stop the current audio recording.
|
|
54
|
+
* @returns Object with the file path and duration in seconds.
|
|
55
|
+
*/
|
|
56
|
+
export async function stopAudioRecording(): Promise<{
|
|
57
|
+
path: string;
|
|
58
|
+
duration: number;
|
|
59
|
+
}> {
|
|
60
|
+
if (!audioRecorderModule) {
|
|
61
|
+
throw new Error('[YaverFeedback] No audio recording in progress.');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const result = await audioRecorderModule.stopRecorder();
|
|
66
|
+
const recorder = audioRecorderModule;
|
|
67
|
+
audioRecorderModule = null;
|
|
68
|
+
|
|
69
|
+
// result is the file path on most implementations
|
|
70
|
+
const path = typeof result === 'string' ? result : result?.uri ?? '';
|
|
71
|
+
// Duration tracking — recorder-player provides currentPosition in ms
|
|
72
|
+
const durationMs =
|
|
73
|
+
typeof recorder.currentPosition === 'number'
|
|
74
|
+
? recorder.currentPosition
|
|
75
|
+
: 0;
|
|
76
|
+
|
|
77
|
+
return { path, duration: durationMs / 1000 };
|
|
78
|
+
} catch (err) {
|
|
79
|
+
audioRecorderModule = null;
|
|
80
|
+
throw new Error(
|
|
81
|
+
'[YaverFeedback] Failed to stop audio recording. ' + String(err),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/expo.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expo-specific auto-initialization for the Yaver Feedback SDK.
|
|
3
|
+
*
|
|
4
|
+
* Reads agent URL from Expo Constants manifest extra if available,
|
|
5
|
+
* otherwise falls back to LAN auto-discovery.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { initExpo, FeedbackModal } from '@yaver/feedback-react-native';
|
|
10
|
+
*
|
|
11
|
+
* initExpo(); // auto-discovers your dev machine
|
|
12
|
+
*
|
|
13
|
+
* function App() {
|
|
14
|
+
* return (
|
|
15
|
+
* <>
|
|
16
|
+
* <YourApp />
|
|
17
|
+
* <FeedbackModal />
|
|
18
|
+
* </>
|
|
19
|
+
* );
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import { YaverFeedback } from './YaverFeedback';
|
|
24
|
+
import type { FeedbackConfig } from './types';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialize the Yaver Feedback SDK for Expo projects.
|
|
28
|
+
*
|
|
29
|
+
* Attempts to read the agent URL from Expo Constants (`expo.extra.yaverAgentUrl`
|
|
30
|
+
* in app.json). If not set, the SDK auto-discovers agents on the local network.
|
|
31
|
+
*
|
|
32
|
+
* Defaults:
|
|
33
|
+
* - trigger: 'shake'
|
|
34
|
+
* - feedbackMode: 'batch'
|
|
35
|
+
* - enabled: __DEV__ (only active in development)
|
|
36
|
+
*
|
|
37
|
+
* @param overrides - Optional partial config to override defaults
|
|
38
|
+
*/
|
|
39
|
+
export function initExpo(overrides?: Partial<FeedbackConfig>): void {
|
|
40
|
+
let agentUrl: string | undefined;
|
|
41
|
+
|
|
42
|
+
// Try reading agent URL from Expo Constants manifest extra
|
|
43
|
+
try {
|
|
44
|
+
// Dynamic require so this doesn't hard-fail if expo-constants isn't installed
|
|
45
|
+
const Constants = require('expo-constants').default;
|
|
46
|
+
agentUrl =
|
|
47
|
+
Constants.expoConfig?.extra?.yaverAgentUrl ??
|
|
48
|
+
Constants.manifest?.extra?.yaverAgentUrl ??
|
|
49
|
+
Constants.manifest2?.extra?.expoClient?.extra?.yaverAgentUrl;
|
|
50
|
+
} catch {
|
|
51
|
+
// expo-constants not available — auto-discovery will be used
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
YaverFeedback.init({
|
|
55
|
+
authToken: '', // LAN auto-discovery doesn't require a token
|
|
56
|
+
trigger: 'shake',
|
|
57
|
+
feedbackMode: 'batch',
|
|
58
|
+
enabled: __DEV__,
|
|
59
|
+
...overrides,
|
|
60
|
+
...(agentUrl ? { agentUrl } : {}),
|
|
61
|
+
});
|
|
62
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @yaver/feedback-react-native — Visual feedback SDK for Yaver.
|
|
3
|
+
*
|
|
4
|
+
* Shake-to-report, screenshots, voice annotations, P2P connection,
|
|
5
|
+
* device discovery, and live/narrated/batch feedback modes for vibe coding.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { YaverFeedback, FeedbackProvider } from '@yaver/feedback-react-native';
|
|
10
|
+
*
|
|
11
|
+
* YaverFeedback.init({
|
|
12
|
+
* agentUrl: 'http://192.168.1.10:18080',
|
|
13
|
+
* authToken: 'your-token',
|
|
14
|
+
* trigger: 'shake',
|
|
15
|
+
* feedbackMode: 'live',
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* // Wrap your app root:
|
|
19
|
+
* <FeedbackProvider>
|
|
20
|
+
* <App />
|
|
21
|
+
* </FeedbackProvider>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export { YaverFeedback } from './YaverFeedback';
|
|
26
|
+
export { BlackBox } from './BlackBox';
|
|
27
|
+
export { initExpo } from './expo';
|
|
28
|
+
export { YaverDiscovery } from './Discovery';
|
|
29
|
+
export { P2PClient } from './P2PClient';
|
|
30
|
+
export { YaverConnectionScreen } from './ConnectionScreen';
|
|
31
|
+
export { ShakeDetector } from './ShakeDetector';
|
|
32
|
+
export { FloatingButton } from './FloatingButton';
|
|
33
|
+
export { FeedbackModal } from './FeedbackModal';
|
|
34
|
+
export { FixReport } from './FixReport';
|
|
35
|
+
export { captureScreenshot, startAudioRecording, stopAudioRecording } from './capture';
|
|
36
|
+
export { uploadFeedback } from './upload';
|
|
37
|
+
export type {
|
|
38
|
+
FeedbackConfig,
|
|
39
|
+
FeedbackBundle,
|
|
40
|
+
FeedbackMetadata,
|
|
41
|
+
DeviceInfo,
|
|
42
|
+
AppInfo,
|
|
43
|
+
TimelineEvent,
|
|
44
|
+
FeedbackReport,
|
|
45
|
+
AgentCommentary,
|
|
46
|
+
FeedbackStreamEvent,
|
|
47
|
+
VoiceCapability,
|
|
48
|
+
CapturedError,
|
|
49
|
+
TestFix,
|
|
50
|
+
TestSession,
|
|
51
|
+
} from './types';
|
|
52
|
+
export type { BlackBoxEvent, BlackBoxConfig } from './BlackBox';
|
|
53
|
+
export type { DiscoveryResult } from './Discovery';
|
|
54
|
+
export type { FeedbackEvent } from './P2PClient';
|