yaver-feedback-react-native 0.7.11 → 0.7.13
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.
|
@@ -42,6 +42,9 @@ public class YaverHotReloadModule extends ReactContextBaseJavaModule {
|
|
|
42
42
|
private static final String BUNDLE_FILE = "index.android.bundle";
|
|
43
43
|
private static final String PREFS_NAME = "yaver_hot_reload";
|
|
44
44
|
private static final String PREFS_KEY_BUNDLE = "bundle_path";
|
|
45
|
+
private static final String PREFS_KEY_BOOT_ATTEMPTS = "boot_attempts";
|
|
46
|
+
private static final String PREFS_KEY_BUNDLE_MTIME = "bundle_mtime";
|
|
47
|
+
private static final int MAX_BOOT_ATTEMPTS = 3;
|
|
45
48
|
|
|
46
49
|
public YaverHotReloadModule(ReactApplicationContext context) {
|
|
47
50
|
super(context);
|
|
@@ -173,16 +176,72 @@ public class YaverHotReloadModule extends ReactContextBaseJavaModule {
|
|
|
173
176
|
// MARK: - Static helpers for Application/MainApplication
|
|
174
177
|
|
|
175
178
|
/**
|
|
176
|
-
* Returns the hot-reloaded bundle file if it exists
|
|
177
|
-
*
|
|
179
|
+
* Returns the hot-reloaded bundle file if it exists AND has not
|
|
180
|
+
* crashed on boot {@link #MAX_BOOT_ATTEMPTS} times in a row.
|
|
181
|
+
* Call from MainApplication.getJSBundleFile() to load the hot bundle
|
|
182
|
+
* on startup.
|
|
183
|
+
*
|
|
184
|
+
* Safety net for the vibe-coding loop: if a pushed bundle crashes
|
|
185
|
+
* on boot, without this guard the saved bundle persists across
|
|
186
|
+
* cold starts and bricks the app. Counter increments on each cold
|
|
187
|
+
* start, resets on {@link #markBootSuccessful(Context)} (called
|
|
188
|
+
* from MainApplication's ReactInstanceEventListener after JS
|
|
189
|
+
* context init, and a 10-s fallback timer). If the counter hits
|
|
190
|
+
* {@link #MAX_BOOT_ATTEMPTS}, delete the saved bundle and return
|
|
191
|
+
* null so MainApplication falls back to the APK-bundled bundle.
|
|
192
|
+
* See YaverHotReload.swift for the matching iOS implementation.
|
|
178
193
|
*/
|
|
179
194
|
public static File getSavedBundleFile(Context context) {
|
|
180
195
|
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
181
196
|
String path = prefs.getString(PREFS_KEY_BUNDLE, null);
|
|
182
|
-
if (path
|
|
183
|
-
|
|
184
|
-
|
|
197
|
+
if (path == null) return null;
|
|
198
|
+
File f = new File(path);
|
|
199
|
+
if (!f.exists()) return null;
|
|
200
|
+
|
|
201
|
+
long currentMtime = f.lastModified();
|
|
202
|
+
long lastMtime = prefs.getLong(PREFS_KEY_BUNDLE_MTIME, 0);
|
|
203
|
+
SharedPreferences.Editor editor = prefs.edit();
|
|
204
|
+
|
|
205
|
+
// Fresh bundle since the counter was last reset → start over.
|
|
206
|
+
if (currentMtime != lastMtime) {
|
|
207
|
+
editor.putInt(PREFS_KEY_BOOT_ATTEMPTS, 0);
|
|
208
|
+
editor.putLong(PREFS_KEY_BUNDLE_MTIME, currentMtime);
|
|
185
209
|
}
|
|
186
|
-
|
|
210
|
+
|
|
211
|
+
int attempts = prefs.getInt(PREFS_KEY_BOOT_ATTEMPTS, 0);
|
|
212
|
+
if (attempts >= MAX_BOOT_ATTEMPTS) {
|
|
213
|
+
Log.w(TAG, "hot bundle failed " + attempts + " consecutive boot attempts — reverting to APK-bundled bundle.");
|
|
214
|
+
File dir = new File(context.getFilesDir(), BUNDLE_DIR);
|
|
215
|
+
if (dir.exists()) {
|
|
216
|
+
File[] list = dir.listFiles();
|
|
217
|
+
if (list != null) for (File child : list) child.delete();
|
|
218
|
+
dir.delete();
|
|
219
|
+
}
|
|
220
|
+
editor.remove(PREFS_KEY_BOOT_ATTEMPTS)
|
|
221
|
+
.remove(PREFS_KEY_BUNDLE_MTIME)
|
|
222
|
+
.remove(PREFS_KEY_BUNDLE)
|
|
223
|
+
.apply();
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Pre-increment: this boot counts as a failure unless the JS
|
|
228
|
+
// side reaches context-initialized and calls markBootSuccessful.
|
|
229
|
+
editor.putInt(PREFS_KEY_BOOT_ATTEMPTS, attempts + 1).apply();
|
|
230
|
+
Log.i(TAG, "loading hot bundle (boot attempt " + (attempts + 1) + "/" + MAX_BOOT_ATTEMPTS + ")");
|
|
231
|
+
return f;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Clear the boot-attempt counter. MainApplication should call this
|
|
236
|
+
* from a ReactInstanceEventListener after JS context init, AND via
|
|
237
|
+
* a 10-s fallback Handler in case the listener never fires.
|
|
238
|
+
*/
|
|
239
|
+
public static void markBootSuccessful(Context context) {
|
|
240
|
+
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
241
|
+
int attempts = prefs.getInt(PREFS_KEY_BOOT_ATTEMPTS, 0);
|
|
242
|
+
if (attempts > 0) {
|
|
243
|
+
Log.i(TAG, "boot confirmed successful — reset boot-attempt counter.");
|
|
244
|
+
}
|
|
245
|
+
prefs.edit().putInt(PREFS_KEY_BOOT_ATTEMPTS, 0).apply();
|
|
187
246
|
}
|
|
188
247
|
}
|
package/app.plugin.js
CHANGED
|
@@ -70,16 +70,24 @@ function withYaverFeedbackAndroid(config) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
* Copy YaverHotReload native module files into the iOS project directory
|
|
74
|
-
*
|
|
75
|
-
*
|
|
73
|
+
* Copy YaverHotReload native module files into the iOS project directory
|
|
74
|
+
* AND register them in project.pbxproj so Xcode actually compiles them.
|
|
75
|
+
*
|
|
76
|
+
* The file-copy step uses withDangerousMod; the project registration
|
|
77
|
+
* uses withXcodeProject. Both are needed — if we only copy the files
|
|
78
|
+
* they sit in the filesystem unreferenced, Xcode ignores them, the
|
|
79
|
+
* final .ipa has no `YaverHotReload` class, and the agent's
|
|
80
|
+
* `reload_bundle` broadcast silently fails to load a new Hermes bundle.
|
|
81
|
+
* This is a bug we've hit repeatedly; keep both steps wired.
|
|
76
82
|
*/
|
|
77
83
|
function withYaverHotReloadNativeModule(config) {
|
|
78
|
-
|
|
84
|
+
// Step 1 — copy source files onto disk.
|
|
85
|
+
config = withDangerousMod(config, [
|
|
79
86
|
"ios",
|
|
80
87
|
(config) => {
|
|
81
88
|
const sdkIosDir = path.resolve(__dirname, "ios");
|
|
82
|
-
const appName = config.modRequest.projectName
|
|
89
|
+
const appName = config.modRequest.projectName;
|
|
90
|
+
if (!appName) return config;
|
|
83
91
|
const targetDir = path.join(
|
|
84
92
|
config.modRequest.platformProjectRoot,
|
|
85
93
|
appName
|
|
@@ -89,7 +97,10 @@ function withYaverHotReloadNativeModule(config) {
|
|
|
89
97
|
for (const fileName of filesToCopy) {
|
|
90
98
|
const src = path.join(sdkIosDir, fileName);
|
|
91
99
|
const dst = path.join(targetDir, fileName);
|
|
92
|
-
if (fs.existsSync(src)
|
|
100
|
+
if (fs.existsSync(src)) {
|
|
101
|
+
// Always overwrite so bumping the SDK version actually
|
|
102
|
+
// picks up the new native code on the next prebuild
|
|
103
|
+
// instead of silently keeping a stale copy.
|
|
93
104
|
fs.copyFileSync(src, dst);
|
|
94
105
|
}
|
|
95
106
|
}
|
|
@@ -97,6 +108,53 @@ function withYaverHotReloadNativeModule(config) {
|
|
|
97
108
|
return config;
|
|
98
109
|
},
|
|
99
110
|
]);
|
|
111
|
+
|
|
112
|
+
// Step 2 — register the files in project.pbxproj so Xcode compiles
|
|
113
|
+
// them into the app target. Without this, the files exist on disk
|
|
114
|
+
// but are invisible to the build system.
|
|
115
|
+
config = withXcodeProject(config, (config) => {
|
|
116
|
+
const proj = config.modResults;
|
|
117
|
+
const appName = config.modRequest.projectName;
|
|
118
|
+
if (!appName) return config;
|
|
119
|
+
|
|
120
|
+
// Look up the group key for the app's source folder (same group
|
|
121
|
+
// that holds AppDelegate.swift). Expo's naming is consistent —
|
|
122
|
+
// projectName === group name in the tree.
|
|
123
|
+
const groupKey =
|
|
124
|
+
proj.findPBXGroupKey({ name: appName }) ||
|
|
125
|
+
proj.findPBXGroupKey({ path: appName });
|
|
126
|
+
if (!groupKey) return config;
|
|
127
|
+
|
|
128
|
+
// Target UUID — getFirstTarget is the app target in a standard
|
|
129
|
+
// Expo project. For multi-target projects, users can opt out via
|
|
130
|
+
// enableHotReload: false.
|
|
131
|
+
const target = proj.getFirstTarget();
|
|
132
|
+
if (!target || !target.uuid) return config;
|
|
133
|
+
|
|
134
|
+
const filesToAdd = ["YaverHotReload.swift", "YaverHotReload.m"];
|
|
135
|
+
for (const fileName of filesToAdd) {
|
|
136
|
+
const relPath = `${appName}/${fileName}`;
|
|
137
|
+
// addSourceFile registers PBXFileReference + PBXBuildFile, adds
|
|
138
|
+
// the file to the group, and wires it into the target's
|
|
139
|
+
// Sources build phase — which is exactly what we need. It's
|
|
140
|
+
// idempotent in practice because the sdk-level copyFileSync
|
|
141
|
+
// step always writes the file, and addSourceFile is a no-op if
|
|
142
|
+
// the reference already exists.
|
|
143
|
+
if (!proj.hasFile || !proj.hasFile(relPath)) {
|
|
144
|
+
try {
|
|
145
|
+
proj.addSourceFile(relPath, { target: target.uuid }, groupKey);
|
|
146
|
+
} catch (e) {
|
|
147
|
+
// If a duplicate slips through, xcode-lib throws; safe to
|
|
148
|
+
// ignore since the file already being registered is the
|
|
149
|
+
// desired state.
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return config;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return config;
|
|
100
158
|
}
|
|
101
159
|
|
|
102
160
|
/**
|
|
@@ -146,6 +204,20 @@ function withYaverAppDelegateHook(config) {
|
|
|
146
204
|
name: Notification.Name("YaverHotReloadBundle"),
|
|
147
205
|
object: nil
|
|
148
206
|
)
|
|
207
|
+
// Crash-revert safety net: clear the boot-attempt counter once
|
|
208
|
+
// RN renders its first frame, OR after 10 s of uptime — whichever
|
|
209
|
+
// fires first. If neither fires (bundle crashes before render),
|
|
210
|
+
// YaverHotReload.bundleURL() will eventually revert to the
|
|
211
|
+
// TestFlight-installed bundle after 3 failed boots. See
|
|
212
|
+
// YaverHotReload.swift for the full state machine.
|
|
213
|
+
NotificationCenter.default.addObserver(
|
|
214
|
+
forName: NSNotification.Name(rawValue: "RCTContentDidAppearNotification"),
|
|
215
|
+
object: nil,
|
|
216
|
+
queue: .main
|
|
217
|
+
) { _ in YaverHotReload.markBootSuccessful() }
|
|
218
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
|
|
219
|
+
YaverHotReload.markBootSuccessful()
|
|
220
|
+
}
|
|
149
221
|
}
|
|
150
222
|
|
|
151
223
|
@objc private func yaverHandleHotReload(_ notification: Notification) {
|
|
@@ -297,6 +369,44 @@ function withYaverAndroidHotReload(config) {
|
|
|
297
369
|
contents.slice(methodStart);
|
|
298
370
|
}
|
|
299
371
|
|
|
372
|
+
// Crash-revert safety net: clear the boot-attempt counter once
|
|
373
|
+
// the React context initializes (bundle loaded successfully), AND
|
|
374
|
+
// via a 10-s fallback Handler in case that listener never fires
|
|
375
|
+
// (e.g. infinite loop in root component). If neither fires,
|
|
376
|
+
// YaverHotReloadModule.getSavedBundleFile() reverts to the
|
|
377
|
+
// APK-bundled bundle after 3 failed cold starts. Parity with
|
|
378
|
+
// YaverHotReload.swift on iOS.
|
|
379
|
+
if (contents.includes("onCreate()") && !contents.includes("yaverHotReloadBootListener")) {
|
|
380
|
+
const onCreateIdx = contents.indexOf("onCreate()");
|
|
381
|
+
const braceIdx = contents.indexOf("{", onCreateIdx);
|
|
382
|
+
const insertionPoint = braceIdx + 1;
|
|
383
|
+
const bootGuard = `
|
|
384
|
+
// Yaver Feedback SDK hot-reload crash-revert safety net
|
|
385
|
+
final android.content.Context yaverHotReloadCtx = getApplicationContext();
|
|
386
|
+
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(
|
|
387
|
+
new Runnable() {
|
|
388
|
+
@Override public void run() {
|
|
389
|
+
io.yaver.feedback.YaverHotReloadModule.markBootSuccessful(yaverHotReloadCtx);
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
10000
|
|
393
|
+
);
|
|
394
|
+
try {
|
|
395
|
+
com.facebook.react.ReactInstanceManager yaverRim = getReactNativeHost().getReactInstanceManager();
|
|
396
|
+
yaverRim.addReactInstanceEventListener(new com.facebook.react.ReactInstanceEventListener() {
|
|
397
|
+
@Override public void onReactContextInitialized(com.facebook.react.bridge.ReactContext ctx) {
|
|
398
|
+
io.yaver.feedback.YaverHotReloadModule.markBootSuccessful(yaverHotReloadCtx);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
} catch (Throwable yaverHotReloadBootListener) {
|
|
402
|
+
// Older/newer RN versions may not expose ReactInstanceEventListener
|
|
403
|
+
// exactly like this; the 10-s fallback above still covers us.
|
|
404
|
+
}
|
|
405
|
+
`;
|
|
406
|
+
contents =
|
|
407
|
+
contents.slice(0, insertionPoint) + bootGuard + contents.slice(insertionPoint);
|
|
408
|
+
}
|
|
409
|
+
|
|
300
410
|
config.modResults.contents = contents;
|
|
301
411
|
return config;
|
|
302
412
|
});
|
|
@@ -308,10 +418,15 @@ function withYaverFeedback(config, props) {
|
|
|
308
418
|
config = withYaverFeedbackIOS(config);
|
|
309
419
|
config = withYaverFeedbackAndroid(config);
|
|
310
420
|
|
|
311
|
-
// Hot reload native module is
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
|
|
421
|
+
// Hot reload native module is ON by default — it's the SDK's whole
|
|
422
|
+
// point. TestFlight / Play Store standalone builds have no Metro
|
|
423
|
+
// dev server, so without the YaverHotReload native module the
|
|
424
|
+
// agent's `reload_bundle` broadcast silently no-ops: the SDK falls
|
|
425
|
+
// through to DevSettings.reload() which does nothing in Release
|
|
426
|
+
// builds. Apps that specifically don't want the plugin mutating
|
|
427
|
+
// their AppDelegate / MainApplication can opt out with
|
|
428
|
+
// ["yaver-feedback-react-native", { "enableHotReload": false }]
|
|
429
|
+
const enableHotReload = props?.enableHotReload !== false;
|
|
315
430
|
if (enableHotReload) {
|
|
316
431
|
config = withYaverHotReloadNativeModule(config);
|
|
317
432
|
config = withYaverAppDelegateHook(config);
|
package/dist/LoginScreen.js
CHANGED
|
@@ -43,6 +43,10 @@ const PROVIDER_THEME = {
|
|
|
43
43
|
github: { ion: 'logo-github', letter: 'G', fg: '#FFFFFF', bg: 'transparent' },
|
|
44
44
|
gitlab: { ion: 'logo-gitlab', letter: 'G', fg: '#FC6D26', bg: 'transparent' },
|
|
45
45
|
microsoft: { ion: 'logo-microsoft', letter: 'M', fg: '#00A4EF', bg: 'transparent' },
|
|
46
|
+
// Email is the universal fallback option — render a mail glyph so
|
|
47
|
+
// it visually matches the OAuth rows instead of being a naked
|
|
48
|
+
// label in a row of iconed buttons.
|
|
49
|
+
email: { ion: 'mail-outline', letter: '@', fg: '#E0E0E0', bg: 'transparent' },
|
|
46
50
|
};
|
|
47
51
|
const ProviderLogo = ({ provider, }) => {
|
|
48
52
|
const theme = PROVIDER_THEME[provider] ?? PROVIDER_THEME.apple;
|
|
@@ -194,7 +198,10 @@ const YaverLoginScreen = ({ onLoggedIn, onCancel, }) => {
|
|
|
194
198
|
styles.button,
|
|
195
199
|
pressed && styles.buttonPressed,
|
|
196
200
|
]} onPress={() => setShowEmailForm(true)} disabled={busyProvider !== null}>
|
|
197
|
-
<react_native_1.
|
|
201
|
+
<react_native_1.View style={styles.buttonContent}>
|
|
202
|
+
<ProviderLogo provider="email"/>
|
|
203
|
+
<react_native_1.Text style={[styles.buttonText, styles.buttonTextWithIcon]}>Continue with Email</react_native_1.Text>
|
|
204
|
+
</react_native_1.View>
|
|
198
205
|
</react_native_1.Pressable>) : (<>
|
|
199
206
|
<react_native_1.View style={styles.divider}>
|
|
200
207
|
<react_native_1.View style={styles.dividerLine}/>
|
package/ios/YaverHotReload.swift
CHANGED
|
@@ -104,9 +104,68 @@ class YaverHotReload: NSObject {
|
|
|
104
104
|
.appendingPathComponent(bundleFile)
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/// Returns the hot-reloaded bundle URL if one exists
|
|
107
|
+
/// Returns the hot-reloaded bundle URL if one exists AND hasn't
|
|
108
|
+
/// crashed on boot N times in a row.
|
|
109
|
+
///
|
|
110
|
+
/// Vibe-coding loop: a developer pushes dozens of Hermes bundles over
|
|
111
|
+
/// the course of a session; eventually one of them crashes on boot
|
|
112
|
+
/// (missing native module, syntax error, TurboModule assert). Without
|
|
113
|
+
/// this guard, the crashing bundle persists across cold starts and
|
|
114
|
+
/// bricks the app — user has to delete + reinstall from TestFlight.
|
|
115
|
+
///
|
|
116
|
+
/// Guard: on each cold-start bundleURL() call, increment a boot
|
|
117
|
+
/// counter. Reset it on first successful render (RCTContentDidAppear)
|
|
118
|
+
/// or after 10 s of uptime. If the counter hits kMaxBootAttempts
|
|
119
|
+
/// without being reset, the bundle has crashed every boot so far —
|
|
120
|
+
/// delete it and return nil so the app falls back to the
|
|
121
|
+
/// TestFlight-installed bundle.
|
|
122
|
+
///
|
|
123
|
+
/// Bundle mtime is tracked separately: pushing a NEW bundle resets
|
|
124
|
+
/// the counter so each pushed bundle gets its own 3 attempts.
|
|
108
125
|
@objc static func bundleURL() -> URL? {
|
|
109
126
|
let path = savedBundlePath()
|
|
110
|
-
|
|
127
|
+
guard FileManager.default.fileExists(atPath: path.path) else { return nil }
|
|
128
|
+
|
|
129
|
+
let defaults = UserDefaults.standard
|
|
130
|
+
let attrs = try? FileManager.default.attributesOfItem(atPath: path.path)
|
|
131
|
+
let currentMtime = (attrs?[.modificationDate] as? Date)?.timeIntervalSince1970 ?? 0
|
|
132
|
+
let lastMtime = defaults.double(forKey: kKeyBundleMtime)
|
|
133
|
+
|
|
134
|
+
// Fresh bundle pushed since the counter was last reset → start over.
|
|
135
|
+
if currentMtime != lastMtime {
|
|
136
|
+
defaults.set(0, forKey: kKeyBootAttempts)
|
|
137
|
+
defaults.set(currentMtime, forKey: kKeyBundleMtime)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let attempts = defaults.integer(forKey: kKeyBootAttempts)
|
|
141
|
+
if attempts >= kMaxBootAttempts {
|
|
142
|
+
NSLog("[YaverHotReload] hot bundle failed %d consecutive boot attempts — reverting to app-bundled TestFlight bundle.", attempts)
|
|
143
|
+
let dir = path.deletingLastPathComponent()
|
|
144
|
+
try? FileManager.default.removeItem(at: dir)
|
|
145
|
+
defaults.removeObject(forKey: kKeyBootAttempts)
|
|
146
|
+
defaults.removeObject(forKey: kKeyBundleMtime)
|
|
147
|
+
return nil
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Pre-increment: this boot will count as a failure unless the JS
|
|
151
|
+
// side reaches first render and calls markBootSuccessful().
|
|
152
|
+
defaults.set(attempts + 1, forKey: kKeyBootAttempts)
|
|
153
|
+
NSLog("[YaverHotReload] loading hot bundle (boot attempt %d/%d)", attempts + 1, kMaxBootAttempts)
|
|
154
|
+
return path
|
|
111
155
|
}
|
|
156
|
+
|
|
157
|
+
/// Clear the boot-attempt counter. Call this from AppDelegate after
|
|
158
|
+
/// first successful RN render (RCTContentDidAppearNotification) or
|
|
159
|
+
/// after a short uptime safety timer — whichever fires first.
|
|
160
|
+
@objc static func markBootSuccessful() {
|
|
161
|
+
let defaults = UserDefaults.standard
|
|
162
|
+
if defaults.integer(forKey: kKeyBootAttempts) > 0 {
|
|
163
|
+
NSLog("[YaverHotReload] boot confirmed successful — reset boot-attempt counter.")
|
|
164
|
+
}
|
|
165
|
+
defaults.set(0, forKey: kKeyBootAttempts)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
static let kKeyBootAttempts = "yaverHotReloadBootAttempts"
|
|
169
|
+
static let kKeyBundleMtime = "yaverHotReloadBundleMtime"
|
|
170
|
+
static let kMaxBootAttempts = 3
|
|
112
171
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yaver-feedback-react-native",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.13",
|
|
4
4
|
"description": "Visual feedback SDK for Yaver — bug reports, screen recording, voice annotations, and local-first developer workflows",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/src/LoginScreen.tsx
CHANGED
|
@@ -42,15 +42,21 @@ type ProviderTheme = {
|
|
|
42
42
|
bg: string;
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
type LoginOption = OAuthProvider | 'apple' | 'email';
|
|
46
|
+
|
|
47
|
+
const PROVIDER_THEME: Record<LoginOption, ProviderTheme> = {
|
|
46
48
|
apple: { ion: 'logo-apple', letter: '', fg: '#FFFFFF', bg: 'transparent' },
|
|
47
49
|
google: { ion: 'logo-google', letter: 'G', fg: '#EA4335', bg: 'transparent' },
|
|
48
50
|
github: { ion: 'logo-github', letter: 'G', fg: '#FFFFFF', bg: 'transparent' },
|
|
49
51
|
gitlab: { ion: 'logo-gitlab', letter: 'G', fg: '#FC6D26', bg: 'transparent' },
|
|
50
52
|
microsoft: { ion: 'logo-microsoft', letter: 'M', fg: '#00A4EF', bg: 'transparent' },
|
|
53
|
+
// Email is the universal fallback option — render a mail glyph so
|
|
54
|
+
// it visually matches the OAuth rows instead of being a naked
|
|
55
|
+
// label in a row of iconed buttons.
|
|
56
|
+
email: { ion: 'mail-outline', letter: '@', fg: '#E0E0E0', bg: 'transparent' },
|
|
51
57
|
};
|
|
52
58
|
|
|
53
|
-
const ProviderLogo: React.FC<{ provider:
|
|
59
|
+
const ProviderLogo: React.FC<{ provider: LoginOption }> = ({
|
|
54
60
|
provider,
|
|
55
61
|
}) => {
|
|
56
62
|
const theme = PROVIDER_THEME[provider] ?? PROVIDER_THEME.apple;
|
|
@@ -263,7 +269,10 @@ export const YaverLoginScreen: React.FC<YaverLoginScreenProps> = ({
|
|
|
263
269
|
onPress={() => setShowEmailForm(true)}
|
|
264
270
|
disabled={busyProvider !== null}
|
|
265
271
|
>
|
|
266
|
-
<
|
|
272
|
+
<View style={styles.buttonContent}>
|
|
273
|
+
<ProviderLogo provider="email" />
|
|
274
|
+
<Text style={[styles.buttonText, styles.buttonTextWithIcon]}>Continue with Email</Text>
|
|
275
|
+
</View>
|
|
267
276
|
</Pressable>
|
|
268
277
|
) : (
|
|
269
278
|
<>
|