yaver-feedback-react-native 0.9.7 → 0.9.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/README.md +39 -7
- package/android/src/main/java/io/yaver/feedback/YaverDogfoodGestureModule.java +241 -0
- package/android/src/main/java/io/yaver/feedback/YaverHotReloadPackage.java +1 -0
- package/app.plugin.js +12 -2
- package/dist/AuthOverlay.js +13 -2
- package/dist/DogfoodQuickControls.d.ts +8 -0
- package/dist/DogfoodQuickControls.js +401 -0
- package/dist/DogfoodSessionUi.d.ts +50 -0
- package/dist/DogfoodSessionUi.js +135 -0
- package/dist/FeedbackModal.js +50 -22
- package/dist/YaverFeedback.d.ts +35 -1
- package/dist/YaverFeedback.js +312 -12
- package/dist/__tests__/NativeDogfoodShortcut.test.js +14 -0
- package/dist/__tests__/YaverFeedback.test.js +173 -0
- package/dist/auth.d.ts +15 -0
- package/dist/auth.js +41 -2
- package/dist/dogfoodPolicy.d.ts +22 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +8 -2
- package/dist/preferences.d.ts +13 -0
- package/dist/preferences.js +84 -0
- package/ios/YaverDogfoodGesture.m +14 -0
- package/ios/YaverDogfoodGesture.swift +175 -0
- package/package.json +1 -1
- package/src/AuthOverlay.tsx +20 -3
- package/src/DogfoodQuickControls.tsx +461 -0
- package/src/DogfoodSessionUi.tsx +213 -0
- package/src/FeedbackModal.tsx +62 -34
- package/src/YaverFeedback.ts +343 -12
- package/src/__tests__/NativeDogfoodShortcut.test.ts +15 -0
- package/src/__tests__/YaverFeedback.test.ts +183 -0
- package/src/auth.ts +53 -2
- package/src/dogfoodPolicy.ts +22 -1
- package/src/index.ts +13 -1
- package/src/preferences.ts +89 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DeviceEventEmitter, NativeModules } from 'react-native';
|
|
2
2
|
import { YaverFeedback } from '../YaverFeedback';
|
|
3
|
+
import { getDogfoodAccountAccess, setDogfoodControlPreference } from '../auth';
|
|
3
4
|
|
|
4
5
|
// Mock react-native: DeviceEventEmitter for event dispatch + Platform so
|
|
5
6
|
// ShakeDetector.start() can branch on iOS without hitting a real RN runtime.
|
|
@@ -14,6 +15,10 @@ jest.mock('react-native', () => ({
|
|
|
14
15
|
setDogfoodShortcut: jest.fn(async () => true),
|
|
15
16
|
consumeDogfoodShortcut: jest.fn(async () => false),
|
|
16
17
|
},
|
|
18
|
+
YaverDogfoodGesture: {
|
|
19
|
+
getCapability: jest.fn(async () => ({ supported: true, enabled: false, reason: 'supported', platform: 'ios' })),
|
|
20
|
+
setEnabled: jest.fn(async (next: boolean) => ({ supported: true, enabled: next, reason: 'supported', platform: 'ios' })),
|
|
21
|
+
},
|
|
17
22
|
},
|
|
18
23
|
AppState: { addEventListener: jest.fn(() => ({ remove: jest.fn() })) },
|
|
19
24
|
}));
|
|
@@ -33,8 +38,10 @@ jest.mock('../auth', () => ({
|
|
|
33
38
|
getDogfoodAccountAccess: jest.fn(async (_appId: string, token: string) => ({
|
|
34
39
|
authenticated: token === 'owner-token',
|
|
35
40
|
ownerAuthorized: token === 'owner-token',
|
|
41
|
+
accountAuthorized: token === 'owner-token',
|
|
36
42
|
installationAuthorized: token === 'owner-token',
|
|
37
43
|
})),
|
|
44
|
+
setDogfoodControlPreference: jest.fn(async () => true),
|
|
38
45
|
saveSelectedDeviceId: jest.fn(async () => {}),
|
|
39
46
|
clearToken: jest.fn(async () => {}),
|
|
40
47
|
clearSelectedDeviceId: jest.fn(async () => {}),
|
|
@@ -65,6 +72,27 @@ beforeEach(() => {
|
|
|
65
72
|
jest.clearAllMocks();
|
|
66
73
|
});
|
|
67
74
|
|
|
75
|
+
function activeDogfood(controlGesture: true | { durationMs: number } = true) {
|
|
76
|
+
return {
|
|
77
|
+
enabled: true,
|
|
78
|
+
appId: 'io.example.app',
|
|
79
|
+
installationId: 'phone-1',
|
|
80
|
+
installationStatus: 'active' as const,
|
|
81
|
+
controlGesture,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function initActiveDogfood(options: { authToken?: string; controlGesture?: true | { durationMs: number } } = {}) {
|
|
86
|
+
const controlGesture = options.controlGesture ?? true;
|
|
87
|
+
YaverFeedback.init({
|
|
88
|
+
authToken: options.authToken,
|
|
89
|
+
bundleId: 'io.example.app',
|
|
90
|
+
dogfood: { controlGesture },
|
|
91
|
+
});
|
|
92
|
+
Object.assign(YaverFeedback.getConfig()!.dogfood!, activeDogfood(controlGesture));
|
|
93
|
+
jest.clearAllMocks();
|
|
94
|
+
}
|
|
95
|
+
|
|
68
96
|
describe('YaverFeedback', () => {
|
|
69
97
|
describe('Dogfood onboarding', () => {
|
|
70
98
|
it('starts with Yaver OAuth when the host has no session', async () => {
|
|
@@ -103,6 +131,18 @@ describe('YaverFeedback', () => {
|
|
|
103
131
|
expect(states[states.length - 1]).toBe('auth-required');
|
|
104
132
|
});
|
|
105
133
|
|
|
134
|
+
it('returns a visible structured error when access verification fails', async () => {
|
|
135
|
+
YaverFeedback.init({ enabled: true, authToken: 'owner-token' });
|
|
136
|
+
(getDogfoodAccountAccess as jest.Mock).mockRejectedValueOnce(new Error('Access service unavailable'));
|
|
137
|
+
const state = await YaverFeedback.beginDogfoodOnboarding({ appId: 'io.example.app' });
|
|
138
|
+
expect(state).toEqual({
|
|
139
|
+
phase: 'error',
|
|
140
|
+
appId: 'io.example.app',
|
|
141
|
+
error: 'Access service unavailable',
|
|
142
|
+
});
|
|
143
|
+
expect(DeviceEventEmitter.emit).not.toHaveBeenCalledWith('yaverFeedback:startReport');
|
|
144
|
+
});
|
|
145
|
+
|
|
106
146
|
it('lets a host ACL hide its affordance without opening auth UI', async () => {
|
|
107
147
|
YaverFeedback.init({
|
|
108
148
|
enabled: true,
|
|
@@ -121,6 +161,7 @@ describe('YaverFeedback', () => {
|
|
|
121
161
|
appId: 'io.example.app',
|
|
122
162
|
yaverAuthenticated: true,
|
|
123
163
|
ownerAuthorized: false,
|
|
164
|
+
accountAuthorized: true,
|
|
124
165
|
installationId: 'phone-1',
|
|
125
166
|
deviceState: 'active',
|
|
126
167
|
authorized: true,
|
|
@@ -137,6 +178,7 @@ describe('YaverFeedback', () => {
|
|
|
137
178
|
appId: 'io.example.app',
|
|
138
179
|
yaverAuthenticated: false,
|
|
139
180
|
ownerAuthorized: false,
|
|
181
|
+
accountAuthorized: true,
|
|
140
182
|
installationId: 'phone-1',
|
|
141
183
|
deviceState: 'active',
|
|
142
184
|
authorized: false,
|
|
@@ -155,6 +197,7 @@ describe('YaverFeedback', () => {
|
|
|
155
197
|
appId: 'io.example.app',
|
|
156
198
|
yaverAuthenticated: true,
|
|
157
199
|
ownerAuthorized: false,
|
|
200
|
+
accountAuthorized: true,
|
|
158
201
|
installationId: 'phone-1',
|
|
159
202
|
deviceState: 'active',
|
|
160
203
|
authorized: true,
|
|
@@ -163,6 +206,146 @@ describe('YaverFeedback', () => {
|
|
|
163
206
|
expect((NativeModules as any).YaverHotReload.setDogfoodShortcut)
|
|
164
207
|
.toHaveBeenCalledWith(false, 'Dogfood');
|
|
165
208
|
});
|
|
209
|
+
|
|
210
|
+
it('keeps the Y visible until the authorized phone has completed onboarding', async () => {
|
|
211
|
+
initActiveDogfood();
|
|
212
|
+
jest.spyOn(YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
|
|
213
|
+
appId: 'io.example.app',
|
|
214
|
+
yaverAuthenticated: true,
|
|
215
|
+
ownerAuthorized: false,
|
|
216
|
+
accountAuthorized: true,
|
|
217
|
+
installationId: 'phone-1',
|
|
218
|
+
deviceState: 'active',
|
|
219
|
+
authorized: true,
|
|
220
|
+
});
|
|
221
|
+
const state = await YaverFeedback.syncDogfoodControlGesture();
|
|
222
|
+
expect(state).toMatchObject({
|
|
223
|
+
onboardingSeen: false,
|
|
224
|
+
presentation: 'minimized-y',
|
|
225
|
+
gestureSupported: true,
|
|
226
|
+
gestureEnabled: false,
|
|
227
|
+
fallbackVisible: true,
|
|
228
|
+
});
|
|
229
|
+
expect((NativeModules as any).YaverDogfoodGesture.setEnabled).toHaveBeenCalledWith(false, 900);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('uses the invisible three-finger hold only after onboarding selects it', async () => {
|
|
233
|
+
initActiveDogfood();
|
|
234
|
+
jest.spyOn(YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
|
|
235
|
+
appId: 'io.example.app',
|
|
236
|
+
yaverAuthenticated: true,
|
|
237
|
+
ownerAuthorized: false,
|
|
238
|
+
accountAuthorized: true,
|
|
239
|
+
installationId: 'phone-1',
|
|
240
|
+
deviceState: 'active',
|
|
241
|
+
authorized: true,
|
|
242
|
+
controlPresentation: 'auto',
|
|
243
|
+
controlOnboardingSeen: true,
|
|
244
|
+
});
|
|
245
|
+
const state = await YaverFeedback.syncDogfoodControlGesture();
|
|
246
|
+
expect(state).toMatchObject({
|
|
247
|
+
onboardingSeen: true,
|
|
248
|
+
presentation: 'auto',
|
|
249
|
+
gestureSupported: true,
|
|
250
|
+
gestureEnabled: true,
|
|
251
|
+
fallbackVisible: false,
|
|
252
|
+
});
|
|
253
|
+
expect((NativeModules as any).YaverDogfoodGesture.setEnabled).toHaveBeenCalledWith(true, 900);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('keeps the Y when native capability reports support but enabling the gesture fails', async () => {
|
|
257
|
+
initActiveDogfood();
|
|
258
|
+
jest.spyOn(YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
|
|
259
|
+
appId: 'io.example.app',
|
|
260
|
+
yaverAuthenticated: true,
|
|
261
|
+
ownerAuthorized: false,
|
|
262
|
+
accountAuthorized: true,
|
|
263
|
+
installationId: 'phone-1',
|
|
264
|
+
deviceState: 'active',
|
|
265
|
+
authorized: true,
|
|
266
|
+
controlPresentation: 'auto',
|
|
267
|
+
controlOnboardingSeen: true,
|
|
268
|
+
});
|
|
269
|
+
(NativeModules as any).YaverDogfoodGesture.setEnabled.mockResolvedValueOnce({
|
|
270
|
+
supported: true,
|
|
271
|
+
enabled: false,
|
|
272
|
+
reason: 'supported',
|
|
273
|
+
platform: 'ios',
|
|
274
|
+
});
|
|
275
|
+
const state = await YaverFeedback.syncDogfoodControlGesture();
|
|
276
|
+
expect(state).toMatchObject({
|
|
277
|
+
gestureSupported: true,
|
|
278
|
+
gestureEnabled: false,
|
|
279
|
+
fallbackVisible: true,
|
|
280
|
+
reason: 'gesture-enable-failed',
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('persists first-run completion for the exact account app installation', async () => {
|
|
285
|
+
initActiveDogfood({ authToken: 'owner-token' });
|
|
286
|
+
jest.spyOn(YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
|
|
287
|
+
appId: 'io.example.app',
|
|
288
|
+
yaverAuthenticated: true,
|
|
289
|
+
ownerAuthorized: true,
|
|
290
|
+
accountAuthorized: true,
|
|
291
|
+
installationId: 'phone-1',
|
|
292
|
+
deviceState: 'active',
|
|
293
|
+
authorized: true,
|
|
294
|
+
controlOnboardingSeen: false,
|
|
295
|
+
});
|
|
296
|
+
const state = await YaverFeedback.setDogfoodControlPresentation('minimized-y');
|
|
297
|
+
expect(state).toMatchObject({ onboardingSeen: true, presentation: 'minimized-y', fallbackVisible: true });
|
|
298
|
+
expect(setDogfoodControlPreference).toHaveBeenCalledWith(expect.objectContaining({
|
|
299
|
+
appId: 'io.example.app',
|
|
300
|
+
installationId: 'phone-1',
|
|
301
|
+
token: 'owner-token',
|
|
302
|
+
presentation: 'minimized-y',
|
|
303
|
+
controlOnboardingSeen: true,
|
|
304
|
+
}));
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('falls back to the minimized Y when accessibility owns multi-touch', async () => {
|
|
308
|
+
initActiveDogfood({ controlGesture: { durationMs: 1200 } });
|
|
309
|
+
jest.spyOn(YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
|
|
310
|
+
appId: 'io.example.app',
|
|
311
|
+
yaverAuthenticated: true,
|
|
312
|
+
ownerAuthorized: false,
|
|
313
|
+
accountAuthorized: true,
|
|
314
|
+
installationId: 'phone-1',
|
|
315
|
+
deviceState: 'active',
|
|
316
|
+
authorized: true,
|
|
317
|
+
});
|
|
318
|
+
(NativeModules as any).YaverDogfoodGesture.getCapability.mockResolvedValueOnce({
|
|
319
|
+
supported: false,
|
|
320
|
+
enabled: false,
|
|
321
|
+
reason: 'accessibility-touch-exploration',
|
|
322
|
+
platform: 'ios',
|
|
323
|
+
});
|
|
324
|
+
const state = await YaverFeedback.syncDogfoodControlGesture();
|
|
325
|
+
expect(state).toMatchObject({ gestureSupported: false, gestureEnabled: false, fallbackVisible: true });
|
|
326
|
+
expect((NativeModules as any).YaverDogfoodGesture.setEnabled).toHaveBeenCalledWith(false, 1200);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('suppresses guest controls inside the Yaver split-view container', async () => {
|
|
330
|
+
initActiveDogfood();
|
|
331
|
+
(NativeModules as any).YaverInfo = { isYaver: true };
|
|
332
|
+
const state = await YaverFeedback.syncDogfoodControlGesture();
|
|
333
|
+
delete (NativeModules as any).YaverInfo;
|
|
334
|
+
expect(state).toMatchObject({ gestureEnabled: false, fallbackVisible: false, reason: 'yaver-host-owns-controls' });
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('removes the gesture and fallback Y when Dogfood exits', async () => {
|
|
338
|
+
initActiveDogfood();
|
|
339
|
+
await YaverFeedback.exitDogfoodMode();
|
|
340
|
+
const state = await YaverFeedback.syncDogfoodControlGesture();
|
|
341
|
+
expect(state).toMatchObject({
|
|
342
|
+
authorized: false,
|
|
343
|
+
gestureEnabled: false,
|
|
344
|
+
fallbackVisible: false,
|
|
345
|
+
reason: 'dogfood-session-inactive',
|
|
346
|
+
});
|
|
347
|
+
expect((NativeModules as any).YaverDogfoodGesture.setEnabled).toHaveBeenCalledWith(false, 900);
|
|
348
|
+
});
|
|
166
349
|
});
|
|
167
350
|
|
|
168
351
|
describe('init()', () => {
|
package/src/auth.ts
CHANGED
|
@@ -207,7 +207,12 @@ export async function clearSelectedDeviceId(): Promise<void> {
|
|
|
207
207
|
export async function getDogfoodAccountAccess(appId: string, token: string, installationId?: string): Promise<{
|
|
208
208
|
authenticated: boolean;
|
|
209
209
|
ownerAuthorized: boolean;
|
|
210
|
+
accountAuthorized: boolean;
|
|
210
211
|
installationAuthorized: boolean;
|
|
212
|
+
controlPresentation?: 'auto' | 'minimized-y';
|
|
213
|
+
gestureSupported?: boolean;
|
|
214
|
+
gestureCapabilityReason?: string;
|
|
215
|
+
controlOnboardingSeen?: boolean;
|
|
211
216
|
}> {
|
|
212
217
|
try {
|
|
213
218
|
const controller = new AbortController();
|
|
@@ -219,15 +224,61 @@ export async function getDogfoodAccountAccess(appId: string, token: string, inst
|
|
|
219
224
|
signal: controller.signal,
|
|
220
225
|
});
|
|
221
226
|
clearTimeout(timeout);
|
|
222
|
-
if (!response.ok) return { authenticated: false, ownerAuthorized: false, installationAuthorized: false };
|
|
227
|
+
if (!response.ok) return { authenticated: false, ownerAuthorized: false, accountAuthorized: false, installationAuthorized: false };
|
|
223
228
|
const result = await response.json();
|
|
224
229
|
return {
|
|
225
230
|
authenticated: result?.authenticated === true,
|
|
226
231
|
ownerAuthorized: result?.ownerAuthorized === true,
|
|
232
|
+
accountAuthorized: result?.accountAuthorized === true,
|
|
227
233
|
installationAuthorized: result?.installationAuthorized === true,
|
|
234
|
+
controlPresentation: result?.controlPresentation === 'minimized-y' ? 'minimized-y'
|
|
235
|
+
: result?.controlPresentation === 'auto' ? 'auto' : undefined,
|
|
236
|
+
gestureSupported: typeof result?.gestureSupported === 'boolean' ? result.gestureSupported : undefined,
|
|
237
|
+
gestureCapabilityReason: typeof result?.gestureCapabilityReason === 'string'
|
|
238
|
+
? result.gestureCapabilityReason
|
|
239
|
+
: undefined,
|
|
240
|
+
controlOnboardingSeen: result?.controlOnboardingSeen === true,
|
|
228
241
|
};
|
|
229
242
|
} catch {
|
|
230
|
-
return { authenticated: false, ownerAuthorized: false, installationAuthorized: false };
|
|
243
|
+
return { authenticated: false, ownerAuthorized: false, accountAuthorized: false, installationAuthorized: false };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function setDogfoodControlPreference(input: {
|
|
248
|
+
appId: string;
|
|
249
|
+
installationId: string;
|
|
250
|
+
token: string;
|
|
251
|
+
presentation: 'auto' | 'minimized-y';
|
|
252
|
+
gestureSupported: boolean;
|
|
253
|
+
gestureCapabilityReason: string;
|
|
254
|
+
gesturePlatform: string;
|
|
255
|
+
controlOnboardingSeen?: boolean;
|
|
256
|
+
}): Promise<boolean> {
|
|
257
|
+
const controller = new AbortController();
|
|
258
|
+
const timeout = setTimeout(() => controller.abort(), 5_000);
|
|
259
|
+
try {
|
|
260
|
+
const response = await fetch(`${convexSiteUrl}/dogfood/control-preference`, {
|
|
261
|
+
method: 'POST',
|
|
262
|
+
headers: {
|
|
263
|
+
Authorization: `Bearer ${input.token}`,
|
|
264
|
+
'Content-Type': 'application/json',
|
|
265
|
+
},
|
|
266
|
+
body: JSON.stringify({
|
|
267
|
+
appId: input.appId,
|
|
268
|
+
installationId: input.installationId,
|
|
269
|
+
presentation: input.presentation,
|
|
270
|
+
gestureSupported: input.gestureSupported,
|
|
271
|
+
gestureCapabilityReason: input.gestureCapabilityReason,
|
|
272
|
+
gesturePlatform: input.gesturePlatform,
|
|
273
|
+
controlOnboardingSeen: input.controlOnboardingSeen === true,
|
|
274
|
+
}),
|
|
275
|
+
signal: controller.signal,
|
|
276
|
+
});
|
|
277
|
+
return response.ok;
|
|
278
|
+
} catch {
|
|
279
|
+
return false;
|
|
280
|
+
} finally {
|
|
281
|
+
clearTimeout(timeout);
|
|
231
282
|
}
|
|
232
283
|
}
|
|
233
284
|
|
package/src/dogfoodPolicy.ts
CHANGED
|
@@ -4,10 +4,18 @@ export interface DogfoodAccessSnapshot {
|
|
|
4
4
|
yaverAuthenticated: boolean;
|
|
5
5
|
/** Backend-confirmed owner/maintainer of this exact appId. */
|
|
6
6
|
ownerAuthorized: boolean;
|
|
7
|
+
/** Backend-confirmed app assignment for this Yaver account. App owners are
|
|
8
|
+
* assigned implicitly; other accounts require an active owner grant. */
|
|
9
|
+
accountAuthorized: boolean;
|
|
7
10
|
installationId?: string;
|
|
8
11
|
deviceState: 'unknown' | 'unregistered' | 'pending' | 'active' | 'cancelled' | 'revoked' | 'superseded';
|
|
9
|
-
/** True only for
|
|
12
|
+
/** True only for an assigned account and this backend-approved device key. */
|
|
10
13
|
authorized: boolean;
|
|
14
|
+
/** OAuth-backed preference scoped to this user + app + installation. */
|
|
15
|
+
controlPresentation?: 'auto' | 'minimized-y';
|
|
16
|
+
gestureSupported?: boolean;
|
|
17
|
+
gestureCapabilityReason?: string;
|
|
18
|
+
controlOnboardingSeen?: boolean;
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
export interface DogfoodFlowSnapshot {
|
|
@@ -44,6 +52,19 @@ export interface SDKDogfoodConfig {
|
|
|
44
52
|
/** ACL-backed Home Screen/App Shortcut. Dynamic and absent until Yaver
|
|
45
53
|
* owner auth or this installation's approved key authorizes it. */
|
|
46
54
|
appShortcut?: boolean | { label?: string };
|
|
55
|
+
/** Smart in-app quick controls. On supported standalone iOS/Android builds,
|
|
56
|
+
* a three-finger hold opens a two-action card with no persistent overlay.
|
|
57
|
+
* Unsupported/accessibility-conflicted devices may show a minimized,
|
|
58
|
+
* draggable Y fallback. Yaver host/container mode suppresses both. */
|
|
59
|
+
controlGesture?: boolean | {
|
|
60
|
+
/** Hold duration, clamped to 650–2000 ms. Default 900 ms. */
|
|
61
|
+
durationMs?: number;
|
|
62
|
+
/** Default `minimized-y`; use `none` when Settings is the only fallback. */
|
|
63
|
+
fallback?: 'minimized-y' | 'none';
|
|
64
|
+
/** Initial user-facing presentation before their persisted SDK preference
|
|
65
|
+
* exists. `auto` keeps pixels clear when the gesture works. */
|
|
66
|
+
defaultPresentation?: 'auto' | 'minimized-y';
|
|
67
|
+
};
|
|
47
68
|
}
|
|
48
69
|
|
|
49
70
|
export interface SDKDogfoodStatus {
|
package/src/index.ts
CHANGED
|
@@ -29,7 +29,12 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
export { YaverFeedback } from './YaverFeedback';
|
|
32
|
-
export type {
|
|
32
|
+
export type {
|
|
33
|
+
DogfoodOnboardingOptions,
|
|
34
|
+
DogfoodFlowPhase,
|
|
35
|
+
DogfoodFlowState,
|
|
36
|
+
DogfoodControlTriggerState,
|
|
37
|
+
} from './YaverFeedback';
|
|
33
38
|
export { captureStoreScreenshots } from './storeShots';
|
|
34
39
|
export type {
|
|
35
40
|
CaptureStoreScreenshotsOptions,
|
|
@@ -106,7 +111,14 @@ export type {
|
|
|
106
111
|
DogfoodRunContext,
|
|
107
112
|
DogfoodSnapshot,
|
|
108
113
|
} from './DogfoodRuntime';
|
|
114
|
+
export { DogfoodLanePicker, DogfoodLiveConsole, DogfoodStatusRail } from './DogfoodSessionUi';
|
|
115
|
+
export type {
|
|
116
|
+
DogfoodStatusStep,
|
|
117
|
+
DogfoodStatusTone,
|
|
118
|
+
DogfoodUiColors,
|
|
119
|
+
} from './DogfoodSessionUi';
|
|
109
120
|
export { FeedbackModal } from './FeedbackModal';
|
|
121
|
+
export { DogfoodQuickControls } from './DogfoodQuickControls';
|
|
110
122
|
export { QuickActionIcon } from './QuickActionIcon';
|
|
111
123
|
export type { QuickActionIconProps } from './QuickActionIcon';
|
|
112
124
|
export { FixReport } from './FixReport';
|
package/src/preferences.ts
CHANGED
|
@@ -26,6 +26,95 @@ try {
|
|
|
26
26
|
|
|
27
27
|
const QUICK_ICON_DISABLED_KEY = 'yaver_feedback_quickicon_disabled';
|
|
28
28
|
const QUICK_ICON_COLOR_KEY = 'yaver_feedback_quickicon_color';
|
|
29
|
+
const DOGFOOD_CONTROL_PRESENTATION_KEY = 'yaver_dogfood_control_presentation';
|
|
30
|
+
const DOGFOOD_CONTROL_POSITION_PREFIX = 'yaver_dogfood_control_position_';
|
|
31
|
+
const DOGFOOD_CONTROL_ONBOARDING_PREFIX = 'yaver_dogfood_control_onboarding_';
|
|
32
|
+
|
|
33
|
+
export type DogfoodControlPresentation = 'auto' | 'minimized-y';
|
|
34
|
+
export type DogfoodControlEdge = 'left' | 'right';
|
|
35
|
+
export interface DogfoodControlPosition {
|
|
36
|
+
edge: DogfoodControlEdge;
|
|
37
|
+
/** 0–1 within the safe vertical travel area. */
|
|
38
|
+
yRatio: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function dogfoodPreferenceKey(base: string, scope?: string): string {
|
|
42
|
+
const normalized = String(scope || 'legacy').trim() || 'legacy';
|
|
43
|
+
return `${base}_${encodeURIComponent(normalized)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function getDogfoodControlPresentation(scope?: string): Promise<DogfoodControlPresentation | null> {
|
|
47
|
+
if (!AsyncStorage) return null;
|
|
48
|
+
try {
|
|
49
|
+
const value = await AsyncStorage.getItem(dogfoodPreferenceKey(DOGFOOD_CONTROL_PRESENTATION_KEY, scope));
|
|
50
|
+
return value === 'auto' || value === 'minimized-y' ? value : null;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function setDogfoodControlPresentation(value: DogfoodControlPresentation, scope?: string): Promise<void> {
|
|
57
|
+
if (!AsyncStorage) return;
|
|
58
|
+
try {
|
|
59
|
+
await AsyncStorage.setItem(dogfoodPreferenceKey(DOGFOOD_CONTROL_PRESENTATION_KEY, scope), value);
|
|
60
|
+
} catch {
|
|
61
|
+
// best-effort
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function getDogfoodControlPosition(
|
|
66
|
+
orientation: 'portrait' | 'landscape',
|
|
67
|
+
scope?: string,
|
|
68
|
+
): Promise<DogfoodControlPosition | null> {
|
|
69
|
+
if (!AsyncStorage) return null;
|
|
70
|
+
try {
|
|
71
|
+
const raw = await AsyncStorage.getItem(
|
|
72
|
+
dogfoodPreferenceKey(`${DOGFOOD_CONTROL_POSITION_PREFIX}${orientation}`, scope),
|
|
73
|
+
);
|
|
74
|
+
if (!raw) return null;
|
|
75
|
+
const parsed = JSON.parse(raw) as Partial<DogfoodControlPosition>;
|
|
76
|
+
if ((parsed.edge !== 'left' && parsed.edge !== 'right') || typeof parsed.yRatio !== 'number') return null;
|
|
77
|
+
return { edge: parsed.edge, yRatio: Math.max(0, Math.min(1, parsed.yRatio)) };
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function setDogfoodControlPosition(
|
|
84
|
+
orientation: 'portrait' | 'landscape',
|
|
85
|
+
position: DogfoodControlPosition,
|
|
86
|
+
scope?: string,
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
if (!AsyncStorage) return;
|
|
89
|
+
try {
|
|
90
|
+
await AsyncStorage.setItem(
|
|
91
|
+
dogfoodPreferenceKey(`${DOGFOOD_CONTROL_POSITION_PREFIX}${orientation}`, scope),
|
|
92
|
+
JSON.stringify(position),
|
|
93
|
+
);
|
|
94
|
+
} catch {
|
|
95
|
+
// best-effort
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function getDogfoodControlOnboardingSeen(scope?: string): Promise<boolean> {
|
|
100
|
+
if (!AsyncStorage) return false;
|
|
101
|
+
try {
|
|
102
|
+
return await AsyncStorage.getItem(dogfoodPreferenceKey(DOGFOOD_CONTROL_ONBOARDING_PREFIX, scope)) === '1';
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function setDogfoodControlOnboardingSeen(seen: boolean, scope?: string): Promise<void> {
|
|
109
|
+
if (!AsyncStorage) return;
|
|
110
|
+
try {
|
|
111
|
+
const key = dogfoodPreferenceKey(DOGFOOD_CONTROL_ONBOARDING_PREFIX, scope);
|
|
112
|
+
if (seen) await AsyncStorage.setItem(key, '1');
|
|
113
|
+
else await AsyncStorage.removeItem(key);
|
|
114
|
+
} catch {
|
|
115
|
+
// best-effort startup cache; Convex remains authoritative.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
29
118
|
|
|
30
119
|
export type QuickIconColorPreset =
|
|
31
120
|
| 'orange'
|