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 CHANGED
@@ -113,8 +113,9 @@ still require the user's normal Yaver authentication.
113
113
 
114
114
  ### Device-enrolled Dogfood for apps without their own OAuth/backend
115
115
 
116
- Mount `FeedbackModal` once and configure the ACL-backed dynamic app shortcut.
117
- No permanent Settings row or shake gesture is required:
116
+ Mount `FeedbackModal` once and configure the ACL-backed dynamic app shortcut
117
+ plus smart quick controls. Keep a native Settings/Profile row as the universal
118
+ enrollment and recovery route; no shake gesture is needed:
118
119
 
119
120
  ```tsx
120
121
  YaverFeedback.init({
@@ -128,20 +129,51 @@ YaverFeedback.init({
128
129
  label: 'Example',
129
130
  framework: 'expo',
130
131
  appShortcut: { label: 'Dogfood Example' },
132
+ controlGesture: {
133
+ durationMs: 900,
134
+ fallback: 'minimized-y',
135
+ },
131
136
  // Optional host presentation ACL. Backend account + phone-key approval
132
137
  // remains mandatory even when this returns true.
133
138
  canShow: (access) => appUser.isAdmin && access.authorized,
134
139
  },
135
140
  });
141
+
142
+ // The host owns only placement of the entry point. The SDK owns OAuth,
143
+ // continuation, machine/project/runner/model/lane selection and live logs.
144
+ const onDogfoodPress = () => YaverFeedback.openDogfood();
136
145
  ```
137
146
 
138
147
  For first-time setup, a signed-in user opens the app from Yaver's Dogfood app
139
148
  catalog. The public deep link contains no token. The SDK runs stateful Yaver
140
149
  OAuth, machine, coding runner/model, project, and Browser/Hermes/WebRTC setup.
150
+ OAuth returns directly to that setup in the currently running app; it does not
151
+ restart the host. A valid cached Yaver session and prior selections skip steps
152
+ that are already satisfied. Expo and React Native projects default to the
153
+ Browser lane, while every selection remains changeable from Session setup.
141
154
  After the owner approves that exact account + app ID + phone key, iOS and
142
155
  Android add the long-press shortcut. Signing out, revocation, or host ACL denial
143
156
  removes it on the next foreground sync.
144
157
 
158
+ Every newly authorized standalone installation starts with a partially
159
+ edge-docked, fading Y, even when the phone supports three-finger input. Its
160
+ one-time `Dogfood ready` card explains the entry point and records
161
+ `controlOnboardingSeenAt` in Convex for the exact Yaver user + app +
162
+ installation. After Continue, Settings in the compact card can switch between
163
+ `Always show Y` and a three-finger hold; that preference is stored in the same
164
+ Convex row and cached locally for cold start. Settings also reopens the session
165
+ setup for machine, coding runner/model, and Browser/Hermes/WebRTC lane changes.
166
+
167
+ In three-finger mode, a hold opens one compact card whose primary actions are
168
+ exactly `Fast Reload` and `Chat`; nothing is rendered while the card is closed.
169
+ The native observer does not consume ordinary app touches. If the native module
170
+ is absent, the iOS Simulator cannot supply genuine three-finger input, or
171
+ VoiceOver/TalkBack owns multi-touch, the SDK keeps the draggable Y and omits the
172
+ irrelevant mode preference. Inside Yaver's own
173
+ container both are suppressed: the host already renders preview/lane controls
174
+ beside its chat/vibing surface. Tablets follow the same host-vs-standalone rule
175
+ as phones; viewport size never decides ownership.
176
+
145
177
  The app creates a random installation ID and Ed25519 key inside SecureStore.
146
178
  The ID is a public handle, not a credential: enrollment and each short session
147
179
  require a server challenge signed by the private key. An owner approves,
@@ -753,11 +785,11 @@ YaverFeedback.init({
753
785
 
754
786
  When a third-party app is loaded through Yaver's Hermes-push flow (so the
755
787
  Yaver mobile app is the runtime container for your app), the SDK detects
756
- that situation via the `YaverInfo` native module and **automatically
757
- no-ops `YaverFeedback.init()`** no ShakeDetector, no FeedbackModal, no
758
- BlackBox stream from inside the guest. The user only ever sees Yaver's
759
- native shake overlay ("Reload" + "Back to Yaver") and uses Yaver's
760
- built-in feedback flow instead.
788
+ that situation via the `YaverInfo` native module and suppresses guest-owned
789
+ triggers and overlays: no three-finger listener, minimized Y, shake detector,
790
+ or duplicate quick controls. Yaver owns the workspace, with the app preview
791
+ (Browser/WebRTC/Hermes lane) beside the chat/vibing surface on wider screens
792
+ and in a responsive stacked layout on phones.
761
793
 
762
794
  Standalone installs (TestFlight / App Store / Play from your own dev
763
795
  account) are unaffected — the SDK behaves exactly as it did before.
@@ -0,0 +1,241 @@
1
+ package io.yaver.feedback;
2
+
3
+ import android.app.Activity;
4
+ import android.content.Context;
5
+ import android.os.Handler;
6
+ import android.os.Looper;
7
+ import android.view.HapticFeedbackConstants;
8
+ import android.view.MotionEvent;
9
+ import android.view.View;
10
+ import android.view.ViewConfiguration;
11
+ import android.view.Window;
12
+ import android.view.accessibility.AccessibilityManager;
13
+
14
+ import androidx.annotation.NonNull;
15
+
16
+ import com.facebook.react.bridge.Arguments;
17
+ import com.facebook.react.bridge.LifecycleEventListener;
18
+ import com.facebook.react.bridge.Promise;
19
+ import com.facebook.react.bridge.ReactApplicationContext;
20
+ import com.facebook.react.bridge.ReactContextBaseJavaModule;
21
+ import com.facebook.react.bridge.ReactMethod;
22
+ import com.facebook.react.bridge.WritableMap;
23
+ import com.facebook.react.modules.core.DeviceEventManagerModule;
24
+
25
+ import java.lang.reflect.InvocationTargetException;
26
+ import java.lang.reflect.Proxy;
27
+ import java.util.HashMap;
28
+ import java.util.Map;
29
+
30
+ /** Passive three-finger hold observer for the Dogfood two-action card. */
31
+ public final class YaverDogfoodGestureModule extends ReactContextBaseJavaModule
32
+ implements LifecycleEventListener,
33
+ AccessibilityManager.TouchExplorationStateChangeListener {
34
+
35
+ private static final String MODULE_NAME = "YaverDogfoodGesture";
36
+ private static final String TRIGGER_EVENT = "yaverDogfoodControlGesture";
37
+ private static final String CAPABILITY_EVENT = "yaverDogfoodControlCapability";
38
+
39
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
40
+ private final AccessibilityManager accessibilityManager;
41
+ private final Map<Integer, float[]> initialPoints = new HashMap<>();
42
+ private boolean requestedEnabled = false;
43
+ private long durationMs = 900;
44
+ private boolean fired = false;
45
+ private float touchSlop = 16f;
46
+ private Activity attachedActivity;
47
+ private Window attachedWindow;
48
+ private Window.Callback originalCallback;
49
+ private Window.Callback observingCallback;
50
+
51
+ private final Runnable fireGesture = () -> {
52
+ if (!requestedEnabled || fired || initialPoints.size() < 3 || accessibilityConflict()) return;
53
+ fired = true;
54
+ if (attachedWindow != null) {
55
+ View decor = attachedWindow.getDecorView();
56
+ decor.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
57
+ }
58
+ WritableMap event = Arguments.createMap();
59
+ event.putString("source", "three-finger-hold");
60
+ emit(TRIGGER_EVENT, event);
61
+ };
62
+
63
+ public YaverDogfoodGestureModule(ReactApplicationContext context) {
64
+ super(context);
65
+ accessibilityManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
66
+ context.addLifecycleEventListener(this);
67
+ if (accessibilityManager != null) {
68
+ accessibilityManager.addTouchExplorationStateChangeListener(this);
69
+ }
70
+ }
71
+
72
+ @Override
73
+ @NonNull
74
+ public String getName() {
75
+ return MODULE_NAME;
76
+ }
77
+
78
+ @ReactMethod
79
+ public void getCapability(Promise promise) {
80
+ mainHandler.post(() -> promise.resolve(status()));
81
+ }
82
+
83
+ @ReactMethod
84
+ public void setEnabled(boolean enabled, double requestedDurationMs, Promise promise) {
85
+ mainHandler.post(() -> {
86
+ requestedEnabled = enabled;
87
+ durationMs = Math.max(650L, Math.min(2000L, Math.round(requestedDurationMs)));
88
+ reconcile();
89
+ promise.resolve(status());
90
+ });
91
+ }
92
+
93
+ private boolean accessibilityConflict() {
94
+ return accessibilityManager != null && accessibilityManager.isTouchExplorationEnabled();
95
+ }
96
+
97
+ private WritableMap status() {
98
+ WritableMap result = Arguments.createMap();
99
+ boolean supported = !accessibilityConflict();
100
+ result.putBoolean("supported", supported);
101
+ result.putBoolean("enabled", requestedEnabled && supported && observingCallback != null);
102
+ result.putString("reason", !supported
103
+ ? "accessibility-touch-exploration"
104
+ : (getCurrentActivity() == null ? "window-unavailable" : "supported"));
105
+ result.putString("platform", "android");
106
+ return result;
107
+ }
108
+
109
+ private void reconcile() {
110
+ Activity current = getCurrentActivity();
111
+ if (!requestedEnabled || accessibilityConflict() || current == null) {
112
+ detach();
113
+ return;
114
+ }
115
+ Window window = current.getWindow();
116
+ if (attachedActivity == current && attachedWindow == window
117
+ && window.getCallback() == observingCallback) return;
118
+ detach();
119
+ Window.Callback callback = window.getCallback();
120
+ if (callback == null) return;
121
+ touchSlop = ViewConfiguration.get(current).getScaledTouchSlop();
122
+ Window.Callback proxy = (Window.Callback) Proxy.newProxyInstance(
123
+ YaverDogfoodGestureModule.class.getClassLoader(),
124
+ new Class<?>[]{Window.Callback.class},
125
+ (ignored, method, args) -> {
126
+ if ("dispatchTouchEvent".equals(method.getName())
127
+ && args != null && args.length == 1 && args[0] instanceof MotionEvent) {
128
+ observe((MotionEvent) args[0]);
129
+ }
130
+ try {
131
+ return method.invoke(callback, args);
132
+ } catch (InvocationTargetException error) {
133
+ throw error.getCause();
134
+ }
135
+ });
136
+ attachedActivity = current;
137
+ attachedWindow = window;
138
+ originalCallback = callback;
139
+ observingCallback = proxy;
140
+ window.setCallback(proxy);
141
+ }
142
+
143
+ private void detach() {
144
+ cancelGesture();
145
+ if (attachedWindow != null && attachedWindow.getCallback() == observingCallback
146
+ && originalCallback != null) {
147
+ attachedWindow.setCallback(originalCallback);
148
+ }
149
+ attachedActivity = null;
150
+ attachedWindow = null;
151
+ originalCallback = null;
152
+ observingCallback = null;
153
+ }
154
+
155
+ /** Observe without consuming: the original Window.Callback always receives the event. */
156
+ private void observe(MotionEvent event) {
157
+ if (!requestedEnabled || accessibilityConflict()) return;
158
+ switch (event.getActionMasked()) {
159
+ case MotionEvent.ACTION_DOWN:
160
+ cancelGesture();
161
+ rememberPointers(event);
162
+ break;
163
+ case MotionEvent.ACTION_POINTER_DOWN:
164
+ rememberPointers(event);
165
+ if (event.getPointerCount() >= 3 && !fired) {
166
+ mainHandler.removeCallbacks(fireGesture);
167
+ mainHandler.postDelayed(fireGesture, durationMs);
168
+ }
169
+ break;
170
+ case MotionEvent.ACTION_MOVE:
171
+ if (movedTooFar(event)) cancelGesture();
172
+ break;
173
+ case MotionEvent.ACTION_POINTER_UP:
174
+ case MotionEvent.ACTION_UP:
175
+ case MotionEvent.ACTION_CANCEL:
176
+ cancelGesture();
177
+ break;
178
+ default:
179
+ break;
180
+ }
181
+ }
182
+
183
+ private void rememberPointers(MotionEvent event) {
184
+ for (int index = 0; index < event.getPointerCount(); index++) {
185
+ int id = event.getPointerId(index);
186
+ if (!initialPoints.containsKey(id)) {
187
+ initialPoints.put(id, new float[]{event.getX(index), event.getY(index)});
188
+ }
189
+ }
190
+ }
191
+
192
+ private boolean movedTooFar(MotionEvent event) {
193
+ for (int index = 0; index < event.getPointerCount(); index++) {
194
+ float[] start = initialPoints.get(event.getPointerId(index));
195
+ if (start == null) continue;
196
+ if (Math.abs(event.getX(index) - start[0]) > touchSlop
197
+ || Math.abs(event.getY(index) - start[1]) > touchSlop) return true;
198
+ }
199
+ return false;
200
+ }
201
+
202
+ private void cancelGesture() {
203
+ mainHandler.removeCallbacks(fireGesture);
204
+ initialPoints.clear();
205
+ fired = false;
206
+ }
207
+
208
+ private void emit(String name, WritableMap body) {
209
+ try {
210
+ getReactApplicationContext()
211
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
212
+ .emit(name, body);
213
+ } catch (RuntimeException ignored) {
214
+ // No JS bridge/listener yet; a trigger must never break the host app.
215
+ }
216
+ }
217
+
218
+ private void emitCapability() {
219
+ emit(CAPABILITY_EVENT, status());
220
+ }
221
+
222
+ @Override
223
+ public void onHostResume() {
224
+ mainHandler.post(() -> {
225
+ reconcile();
226
+ emitCapability();
227
+ });
228
+ }
229
+
230
+ @Override public void onHostPause() { cancelGesture(); }
231
+
232
+ @Override public void onHostDestroy() { detach(); }
233
+
234
+ @Override
235
+ public void onTouchExplorationStateChanged(boolean enabled) {
236
+ mainHandler.post(() -> {
237
+ reconcile();
238
+ emitCapability();
239
+ });
240
+ }
241
+ }
@@ -22,6 +22,7 @@ public class YaverHotReloadPackage implements ReactPackage {
22
22
  public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
23
23
  List<NativeModule> modules = new ArrayList<>();
24
24
  modules.add(new YaverHotReloadModule(reactContext));
25
+ modules.add(new YaverDogfoodGestureModule(reactContext));
25
26
  return modules;
26
27
  }
27
28
 
package/app.plugin.js CHANGED
@@ -137,7 +137,12 @@ function withYaverHotReloadNativeModule(config) {
137
137
  appName
138
138
  );
139
139
 
140
- const filesToCopy = ["YaverHotReload.swift", "YaverHotReload.m"];
140
+ const filesToCopy = [
141
+ "YaverHotReload.swift",
142
+ "YaverHotReload.m",
143
+ "YaverDogfoodGesture.swift",
144
+ "YaverDogfoodGesture.m",
145
+ ];
141
146
  for (const fileName of filesToCopy) {
142
147
  const src = path.join(sdkIosDir, fileName);
143
148
  const dst = path.join(targetDir, fileName);
@@ -175,7 +180,12 @@ function withYaverHotReloadNativeModule(config) {
175
180
  const target = proj.getFirstTarget();
176
181
  if (!target || !target.uuid) return config;
177
182
 
178
- const filesToAdd = ["YaverHotReload.swift", "YaverHotReload.m"];
183
+ const filesToAdd = [
184
+ "YaverHotReload.swift",
185
+ "YaverHotReload.m",
186
+ "YaverDogfoodGesture.swift",
187
+ "YaverDogfoodGesture.m",
188
+ ];
179
189
  for (const fileName of filesToAdd) {
180
190
  const relPath = `${appName}/${fileName}`;
181
191
  // addSourceFile registers PBXFileReference + PBXBuildFile, adds
@@ -61,6 +61,17 @@ const AuthOverlay = () => {
61
61
  setLoginVisible(false);
62
62
  setPickerVisible(false);
63
63
  }, []);
64
+ const continueDogfood = (0, react_1.useCallback)(async () => {
65
+ try {
66
+ const state = await YaverFeedback_1.YaverFeedback.continueDogfoodOnboarding();
67
+ if (state.phase === 'denied' || state.phase === 'error') {
68
+ react_native_1.Alert.alert('Dogfood unavailable', state.error || 'This Yaver account or device is not enabled for this app.');
69
+ }
70
+ }
71
+ catch (cause) {
72
+ react_native_1.Alert.alert('Dogfood unavailable', cause instanceof Error ? cause.message : 'Yaver could not continue Dogfood setup.');
73
+ }
74
+ }, []);
64
75
  (0, react_1.useEffect)(() => {
65
76
  let mounted = true;
66
77
  void (0, auth_1.getToken)().then((cached) => {
@@ -91,7 +102,7 @@ const AuthOverlay = () => {
91
102
  await YaverFeedback_1.YaverFeedback.setAuthToken(newToken);
92
103
  if (YaverFeedback_1.YaverFeedback.getDogfoodOnboarding()) {
93
104
  closeAll();
94
- await YaverFeedback_1.YaverFeedback.continueDogfoodOnboarding();
105
+ await continueDogfood();
95
106
  }
96
107
  else {
97
108
  openPicker();
@@ -101,7 +112,7 @@ const AuthOverlay = () => {
101
112
  await YaverFeedback_1.YaverFeedback.setPreferredDevice(device.deviceId);
102
113
  closeAll();
103
114
  if (YaverFeedback_1.YaverFeedback.getDogfoodOnboarding()) {
104
- await YaverFeedback_1.YaverFeedback.continueDogfoodOnboarding();
115
+ await continueDogfood();
105
116
  }
106
117
  else {
107
118
  react_native_1.DeviceEventEmitter.emit('yaverFeedback:startReport');
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ /**
3
+ * The standalone SDK's only persistent chrome. A newly authorized tester sees
4
+ * this edge-docked Y until first-run onboarding is completed in Convex. After
5
+ * that, capable devices default to a passive three-finger hold; unsupported
6
+ * devices keep the Y. Both entry points open exactly the same compact card.
7
+ */
8
+ export declare const DogfoodQuickControls: React.FC;