yaver-feedback-react-native 0.9.6 → 0.9.8
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 +101 -3
- package/dist/AuthOverlay.js +13 -2
- package/dist/DogfoodQuickControls.d.ts +8 -0
- package/dist/DogfoodQuickControls.js +394 -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 +302 -12
- package/dist/__tests__/NativeDogfoodShortcut.test.js +32 -0
- package/dist/__tests__/YaverFeedback.test.js +148 -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 +454 -0
- package/src/DogfoodSessionUi.tsx +213 -0
- package/src/FeedbackModal.tsx +62 -34
- package/src/YaverFeedback.ts +333 -12
- package/src/__tests__/NativeDogfoodShortcut.test.ts +38 -1
- package/src/__tests__/YaverFeedback.test.ts +155 -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 () => {}),
|
|
@@ -103,6 +110,18 @@ describe('YaverFeedback', () => {
|
|
|
103
110
|
expect(states[states.length - 1]).toBe('auth-required');
|
|
104
111
|
});
|
|
105
112
|
|
|
113
|
+
it('returns a visible structured error when access verification fails', async () => {
|
|
114
|
+
YaverFeedback.init({ enabled: true, authToken: 'owner-token' });
|
|
115
|
+
(getDogfoodAccountAccess as jest.Mock).mockRejectedValueOnce(new Error('Access service unavailable'));
|
|
116
|
+
const state = await YaverFeedback.beginDogfoodOnboarding({ appId: 'io.example.app' });
|
|
117
|
+
expect(state).toEqual({
|
|
118
|
+
phase: 'error',
|
|
119
|
+
appId: 'io.example.app',
|
|
120
|
+
error: 'Access service unavailable',
|
|
121
|
+
});
|
|
122
|
+
expect(DeviceEventEmitter.emit).not.toHaveBeenCalledWith('yaverFeedback:startReport');
|
|
123
|
+
});
|
|
124
|
+
|
|
106
125
|
it('lets a host ACL hide its affordance without opening auth UI', async () => {
|
|
107
126
|
YaverFeedback.init({
|
|
108
127
|
enabled: true,
|
|
@@ -121,6 +140,7 @@ describe('YaverFeedback', () => {
|
|
|
121
140
|
appId: 'io.example.app',
|
|
122
141
|
yaverAuthenticated: true,
|
|
123
142
|
ownerAuthorized: false,
|
|
143
|
+
accountAuthorized: true,
|
|
124
144
|
installationId: 'phone-1',
|
|
125
145
|
deviceState: 'active',
|
|
126
146
|
authorized: true,
|
|
@@ -137,6 +157,7 @@ describe('YaverFeedback', () => {
|
|
|
137
157
|
appId: 'io.example.app',
|
|
138
158
|
yaverAuthenticated: false,
|
|
139
159
|
ownerAuthorized: false,
|
|
160
|
+
accountAuthorized: true,
|
|
140
161
|
installationId: 'phone-1',
|
|
141
162
|
deviceState: 'active',
|
|
142
163
|
authorized: false,
|
|
@@ -155,6 +176,7 @@ describe('YaverFeedback', () => {
|
|
|
155
176
|
appId: 'io.example.app',
|
|
156
177
|
yaverAuthenticated: true,
|
|
157
178
|
ownerAuthorized: false,
|
|
179
|
+
accountAuthorized: true,
|
|
158
180
|
installationId: 'phone-1',
|
|
159
181
|
deviceState: 'active',
|
|
160
182
|
authorized: true,
|
|
@@ -163,6 +185,139 @@ describe('YaverFeedback', () => {
|
|
|
163
185
|
expect((NativeModules as any).YaverHotReload.setDogfoodShortcut)
|
|
164
186
|
.toHaveBeenCalledWith(false, 'Dogfood');
|
|
165
187
|
});
|
|
188
|
+
|
|
189
|
+
it('keeps the Y visible until the authorized phone has completed onboarding', async () => {
|
|
190
|
+
YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
|
|
191
|
+
YaverFeedback.getConfig()!.dogfood!.controlGesture = true;
|
|
192
|
+
jest.spyOn(YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
|
|
193
|
+
appId: 'io.example.app',
|
|
194
|
+
yaverAuthenticated: true,
|
|
195
|
+
ownerAuthorized: false,
|
|
196
|
+
accountAuthorized: true,
|
|
197
|
+
installationId: 'phone-1',
|
|
198
|
+
deviceState: 'active',
|
|
199
|
+
authorized: true,
|
|
200
|
+
});
|
|
201
|
+
const state = await YaverFeedback.syncDogfoodControlGesture();
|
|
202
|
+
expect(state).toMatchObject({
|
|
203
|
+
onboardingSeen: false,
|
|
204
|
+
presentation: 'minimized-y',
|
|
205
|
+
gestureSupported: true,
|
|
206
|
+
gestureEnabled: false,
|
|
207
|
+
fallbackVisible: true,
|
|
208
|
+
});
|
|
209
|
+
expect((NativeModules as any).YaverDogfoodGesture.setEnabled).toHaveBeenCalledWith(false, 900);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('uses the invisible three-finger hold only after onboarding selects it', async () => {
|
|
213
|
+
YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
|
|
214
|
+
YaverFeedback.getConfig()!.dogfood!.controlGesture = true;
|
|
215
|
+
jest.spyOn(YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
|
|
216
|
+
appId: 'io.example.app',
|
|
217
|
+
yaverAuthenticated: true,
|
|
218
|
+
ownerAuthorized: false,
|
|
219
|
+
accountAuthorized: true,
|
|
220
|
+
installationId: 'phone-1',
|
|
221
|
+
deviceState: 'active',
|
|
222
|
+
authorized: true,
|
|
223
|
+
controlPresentation: 'auto',
|
|
224
|
+
controlOnboardingSeen: true,
|
|
225
|
+
});
|
|
226
|
+
const state = await YaverFeedback.syncDogfoodControlGesture();
|
|
227
|
+
expect(state).toMatchObject({
|
|
228
|
+
onboardingSeen: true,
|
|
229
|
+
presentation: 'auto',
|
|
230
|
+
gestureSupported: true,
|
|
231
|
+
gestureEnabled: true,
|
|
232
|
+
fallbackVisible: false,
|
|
233
|
+
});
|
|
234
|
+
expect((NativeModules as any).YaverDogfoodGesture.setEnabled).toHaveBeenCalledWith(true, 900);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('keeps the Y when native capability reports support but enabling the gesture fails', async () => {
|
|
238
|
+
YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
|
|
239
|
+
YaverFeedback.getConfig()!.dogfood!.controlGesture = true;
|
|
240
|
+
jest.spyOn(YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
|
|
241
|
+
appId: 'io.example.app',
|
|
242
|
+
yaverAuthenticated: true,
|
|
243
|
+
ownerAuthorized: false,
|
|
244
|
+
accountAuthorized: true,
|
|
245
|
+
installationId: 'phone-1',
|
|
246
|
+
deviceState: 'active',
|
|
247
|
+
authorized: true,
|
|
248
|
+
controlPresentation: 'auto',
|
|
249
|
+
controlOnboardingSeen: true,
|
|
250
|
+
});
|
|
251
|
+
(NativeModules as any).YaverDogfoodGesture.setEnabled.mockResolvedValueOnce({
|
|
252
|
+
supported: true,
|
|
253
|
+
enabled: false,
|
|
254
|
+
reason: 'supported',
|
|
255
|
+
platform: 'ios',
|
|
256
|
+
});
|
|
257
|
+
const state = await YaverFeedback.syncDogfoodControlGesture();
|
|
258
|
+
expect(state).toMatchObject({
|
|
259
|
+
gestureSupported: true,
|
|
260
|
+
gestureEnabled: false,
|
|
261
|
+
fallbackVisible: true,
|
|
262
|
+
reason: 'gesture-enable-failed',
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('persists first-run completion for the exact account app installation', async () => {
|
|
267
|
+
YaverFeedback.init({ authToken: 'owner-token', bundleId: 'io.example.app', dogfood: {} });
|
|
268
|
+
YaverFeedback.getConfig()!.dogfood!.controlGesture = true;
|
|
269
|
+
jest.spyOn(YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
|
|
270
|
+
appId: 'io.example.app',
|
|
271
|
+
yaverAuthenticated: true,
|
|
272
|
+
ownerAuthorized: true,
|
|
273
|
+
accountAuthorized: true,
|
|
274
|
+
installationId: 'phone-1',
|
|
275
|
+
deviceState: 'active',
|
|
276
|
+
authorized: true,
|
|
277
|
+
controlOnboardingSeen: false,
|
|
278
|
+
});
|
|
279
|
+
const state = await YaverFeedback.setDogfoodControlPresentation('minimized-y');
|
|
280
|
+
expect(state).toMatchObject({ onboardingSeen: true, presentation: 'minimized-y', fallbackVisible: true });
|
|
281
|
+
expect(setDogfoodControlPreference).toHaveBeenCalledWith(expect.objectContaining({
|
|
282
|
+
appId: 'io.example.app',
|
|
283
|
+
installationId: 'phone-1',
|
|
284
|
+
token: 'owner-token',
|
|
285
|
+
presentation: 'minimized-y',
|
|
286
|
+
controlOnboardingSeen: true,
|
|
287
|
+
}));
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('falls back to the minimized Y when accessibility owns multi-touch', async () => {
|
|
291
|
+
YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
|
|
292
|
+
YaverFeedback.getConfig()!.dogfood!.controlGesture = { durationMs: 1200 };
|
|
293
|
+
jest.spyOn(YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
|
|
294
|
+
appId: 'io.example.app',
|
|
295
|
+
yaverAuthenticated: true,
|
|
296
|
+
ownerAuthorized: false,
|
|
297
|
+
accountAuthorized: true,
|
|
298
|
+
installationId: 'phone-1',
|
|
299
|
+
deviceState: 'active',
|
|
300
|
+
authorized: true,
|
|
301
|
+
});
|
|
302
|
+
(NativeModules as any).YaverDogfoodGesture.getCapability.mockResolvedValueOnce({
|
|
303
|
+
supported: false,
|
|
304
|
+
enabled: false,
|
|
305
|
+
reason: 'accessibility-touch-exploration',
|
|
306
|
+
platform: 'ios',
|
|
307
|
+
});
|
|
308
|
+
const state = await YaverFeedback.syncDogfoodControlGesture();
|
|
309
|
+
expect(state).toMatchObject({ gestureSupported: false, gestureEnabled: false, fallbackVisible: true });
|
|
310
|
+
expect((NativeModules as any).YaverDogfoodGesture.setEnabled).toHaveBeenCalledWith(false, 1200);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('suppresses guest controls inside the Yaver split-view container', async () => {
|
|
314
|
+
YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
|
|
315
|
+
YaverFeedback.getConfig()!.dogfood!.controlGesture = true;
|
|
316
|
+
(NativeModules as any).YaverInfo = { isYaver: true };
|
|
317
|
+
const state = await YaverFeedback.syncDogfoodControlGesture();
|
|
318
|
+
delete (NativeModules as any).YaverInfo;
|
|
319
|
+
expect(state).toMatchObject({ gestureEnabled: false, fallbackVisible: false, reason: 'yaver-host-owns-controls' });
|
|
320
|
+
});
|
|
166
321
|
});
|
|
167
322
|
|
|
168
323
|
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'
|