yaver-feedback-react-native 0.5.5 → 0.6.1

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
@@ -16,11 +16,16 @@ Manual fallback:
16
16
  npm install yaver-feedback-react-native
17
17
  ```
18
18
 
19
+ > **Mobile only.** This SDK targets React Native (iOS + Android). A `yaver-feedback-web` package exists for browser apps but currently expects a bring-your-own auth token — the equivalent in-app sign-in UX (Apple / Google / GitHub / GitLab / Microsoft / email) for the web SDK will land in a future release. Open an issue if you need it sooner.
20
+
19
21
  ### Peer dependencies
20
22
 
21
- For full functionality, install these optional peer dependencies:
23
+ For full functionality, install these peer dependencies:
22
24
 
23
25
  ```bash
26
+ # Auth (in-app browser OAuth + native Apple Sign-In)
27
+ npm install expo-web-browser expo-apple-authentication
28
+
24
29
  # Device discovery (stored connections)
25
30
  npm install @react-native-async-storage/async-storage
26
31
 
@@ -31,11 +36,28 @@ npm install react-native-view-shot
31
36
  npm install react-native-audio-recorder-player
32
37
  ```
33
38
 
34
- ## Quick Start (0.5+: zero-config auth)
39
+ `expo-apple-authentication` is optional — if it's missing, "Continue with Apple" falls through to in-app browser OAuth on Android. iOS hosts that want true native Apple Sign-In must also enable the **Sign in with Apple** capability in Xcode.
40
+
41
+ Android hosts must register the OAuth callback in `AndroidManifest.xml`:
42
+
43
+ ```xml
44
+ <intent-filter>
45
+ <action android:name="android.intent.action.VIEW" />
46
+ <category android:name="android.intent.category.DEFAULT" />
47
+ <category android:name="android.intent.category.BROWSABLE" />
48
+ <data android:scheme="yaver" android:host="oauth-callback" />
49
+ </intent-filter>
50
+ ```
51
+
52
+ iOS does not require any URL scheme registration — `ASWebAuthenticationSession` intercepts the redirect inside the auth session.
53
+
54
+ ## Quick Start (0.6+: native auth)
55
+
56
+ Starting with **0.6.0**, the SDK login screen mirrors the Yaver mobile app: native Apple Sign-In on iOS, in-app browser OAuth for Google / GitHub / GitLab / Microsoft (no codes, no leaving the app), and inline email + password. The user never sees a verification code or has to switch to a web browser.
35
57
 
36
- Starting with **0.5.0**, the SDK ships its own login screen + remote machine picker. Drop in `<FeedbackModal />` and the first time a user triggers feedback without a cached session, they get:
58
+ Drop in `<FeedbackModal />` and the first time a user triggers feedback without a cached session, they get:
37
59
 
38
- 1. A full-screen login modal — Apple / Google / GitHub / GitLab / Microsoft via [device-code flow](https://yaver.io/auth/device), or inline email + password.
60
+ 1. A full-screen login modal — Apple (native) / Google / GitHub / GitLab / Microsoft (in-app browser) / email + password (inline).
39
61
  2. A picker of the machines they can reach — **their own dev boxes** plus **guest-shared machines** (where another user invited them).
40
62
 
41
63
  Both screens persist their selection to `AsyncStorage`, so subsequent launches reconnect silently.
@@ -6,9 +6,8 @@ export interface YaverLoginScreenProps {
6
6
  onCancel?: () => void;
7
7
  }
8
8
  /**
9
- * Full-screen in-SDK login. Device-code is the default flow users sign in
10
- * with any OAuth provider (Apple/Google/GitHub/GitLab/Microsoft) or email on
11
- * yaver.io and the SDK polls for the issued session token. Email/password is
12
- * available as an inline fallback for headless environments.
9
+ * Full-screen in-SDK login. Mirrors the Yaver mobile app login UX: native
10
+ * Apple Sign-In on iOS, in-app browser OAuth for Google/GitHub/GitLab/
11
+ * Microsoft (no codes, no leaving the app), and inline email/password.
13
12
  */
14
13
  export declare const YaverLoginScreen: React.FC<YaverLoginScreenProps>;
@@ -37,281 +37,212 @@ exports.YaverLoginScreen = void 0;
37
37
  const react_1 = __importStar(require("react"));
38
38
  const react_native_1 = require("react-native");
39
39
  const auth_1 = require("./auth");
40
- const PROVIDERS = [
41
- { id: 'apple', label: 'Apple', emoji: '' },
42
- { id: 'google', label: 'Google', emoji: 'G' },
43
- { id: 'github', label: 'GitHub', emoji: '' },
44
- { id: 'gitlab', label: 'GitLab', emoji: '' },
45
- { id: 'microsoft', label: 'Microsoft', emoji: 'M' },
46
- ];
47
40
  /**
48
- * Full-screen in-SDK login. Device-code is the default flow users sign in
49
- * with any OAuth provider (Apple/Google/GitHub/GitLab/Microsoft) or email on
50
- * yaver.io and the SDK polls for the issued session token. Email/password is
51
- * available as an inline fallback for headless environments.
41
+ * Full-screen in-SDK login. Mirrors the Yaver mobile app login UX: native
42
+ * Apple Sign-In on iOS, in-app browser OAuth for Google/GitHub/GitLab/
43
+ * Microsoft (no codes, no leaving the app), and inline email/password.
52
44
  */
53
45
  const YaverLoginScreen = ({ onLoggedIn, onCancel, }) => {
54
- const [mode, setMode] = (0, react_1.useState)('device');
55
- const [code, setCode] = (0, react_1.useState)(null);
56
- const [codeError, setCodeError] = (0, react_1.useState)(null);
57
- const [starting, setStarting] = (0, react_1.useState)(false);
58
- const pollRef = (0, react_1.useRef)(null);
59
- const expiredTimerRef = (0, react_1.useRef)(null);
60
- const [emailMode, setEmailMode] = (0, react_1.useState)('login');
46
+ const [busyProvider, setBusyProvider] = (0, react_1.useState)(null);
47
+ const [showEmailForm, setShowEmailForm] = (0, react_1.useState)(false);
48
+ const [isSignUp, setIsSignUp] = (0, react_1.useState)(false);
61
49
  const [fullName, setFullName] = (0, react_1.useState)('');
62
50
  const [email, setEmail] = (0, react_1.useState)('');
63
51
  const [password, setPassword] = (0, react_1.useState)('');
52
+ const [confirmPassword, setConfirmPassword] = (0, react_1.useState)('');
64
53
  const [emailBusy, setEmailBusy] = (0, react_1.useState)(false);
65
- const [emailError, setEmailError] = (0, react_1.useState)(null);
66
- (0, react_1.useEffect)(() => {
67
- if (mode === 'device' && !code && !starting) {
68
- void beginDeviceCode(undefined);
54
+ const [emailError, setEmailError] = (0, react_1.useState)('');
55
+ const finish = async (token) => {
56
+ const user = await (0, auth_1.validateToken)(token);
57
+ await (0, auth_1.saveToken)(token);
58
+ if (user)
59
+ await (0, auth_1.saveUser)(user);
60
+ onLoggedIn(token);
61
+ };
62
+ const handleApple = async () => {
63
+ setBusyProvider('apple');
64
+ try {
65
+ const { token } = await (0, auth_1.signInWithApple)();
66
+ await finish(token);
69
67
  }
70
- return () => {
71
- stopPolling();
72
- };
73
- // eslint-disable-next-line react-hooks/exhaustive-deps
74
- }, [mode]);
75
- const stopPolling = () => {
76
- if (pollRef.current) {
77
- clearInterval(pollRef.current);
78
- pollRef.current = null;
68
+ catch (e) {
69
+ const msg = e instanceof Error ? e.message : 'Apple Sign-In failed';
70
+ if (msg !== 'cancelled')
71
+ react_native_1.Alert.alert('Sign In Failed', msg);
79
72
  }
80
- if (expiredTimerRef.current) {
81
- clearTimeout(expiredTimerRef.current);
82
- expiredTimerRef.current = null;
73
+ finally {
74
+ setBusyProvider(null);
83
75
  }
84
76
  };
85
- const beginDeviceCode = async (preferredProvider) => {
86
- setStarting(true);
87
- setCodeError(null);
88
- stopPolling();
77
+ const handleOAuth = async (provider) => {
78
+ setBusyProvider(provider);
89
79
  try {
90
- const result = await (0, auth_1.startDeviceCode)({
91
- platform: react_native_1.Platform.OS,
92
- machineName: `feedback-sdk-${react_native_1.Platform.OS}`,
93
- preferredProvider,
94
- });
95
- setCode(result);
96
- pollRef.current = setInterval(async () => {
97
- const poll = await (0, auth_1.pollDeviceCode)(result.deviceCode);
98
- if (poll.status === 'authorized') {
99
- stopPolling();
100
- const user = await (0, auth_1.validateToken)(poll.token);
101
- await (0, auth_1.saveToken)(poll.token);
102
- if (user)
103
- await (0, auth_1.saveUser)(user);
104
- onLoggedIn(poll.token);
105
- }
106
- else if (poll.status === 'expired') {
107
- stopPolling();
108
- setCodeError('Kod doldu — tekrar başlat.');
109
- setCode(null);
110
- }
111
- }, 3000);
112
- expiredTimerRef.current = setTimeout(() => {
113
- stopPolling();
114
- setCode(null);
115
- setCodeError('Kod süresi doldu.');
116
- }, Math.max(0, result.expiresAt - Date.now()));
80
+ const { token } = await (0, auth_1.signInWithOAuth)(provider);
81
+ await finish(token);
117
82
  }
118
- catch (err) {
119
- setCodeError(err instanceof Error ? err.message : String(err));
83
+ catch (e) {
84
+ const msg = e instanceof Error ? e.message : 'Sign-in failed';
85
+ if (msg !== 'cancelled')
86
+ react_native_1.Alert.alert('Sign In Failed', msg);
120
87
  }
121
88
  finally {
122
- setStarting(false);
89
+ setBusyProvider(null);
123
90
  }
124
91
  };
125
- const openVerification = () => {
126
- if (!code)
127
- return;
128
- react_native_1.Linking.openURL(code.verificationUrl).catch(() => {
129
- setCodeError('Tarayıcı açılamadı — URL’yi elle aç.');
130
- });
131
- };
132
92
  const handleEmailSubmit = async () => {
133
- setEmailError(null);
134
- if (!email.trim() || !password) {
135
- setEmailError('E-posta ve parola zorunlu.');
136
- return;
93
+ setEmailError('');
94
+ if (isSignUp) {
95
+ if (!fullName.trim())
96
+ return setEmailError('Full name is required');
97
+ if (password !== confirmPassword)
98
+ return setEmailError('Passwords do not match');
99
+ if (password.length < 8)
100
+ return setEmailError('Password must be at least 8 characters');
137
101
  }
102
+ if (!email.trim() || !password)
103
+ return setEmailError('Email and password are required');
138
104
  setEmailBusy(true);
139
105
  try {
140
- const result = emailMode === 'signup'
141
- ? await (0, auth_1.signupWithEmail)(fullName.trim() || email.trim(), email.trim(), password)
106
+ const result = isSignUp
107
+ ? await (0, auth_1.signupWithEmail)(fullName.trim(), email.trim(), password)
142
108
  : await (0, auth_1.loginWithEmail)(email.trim(), password);
143
- const user = await (0, auth_1.validateToken)(result.token);
144
- await (0, auth_1.saveToken)(result.token);
145
- if (user)
146
- await (0, auth_1.saveUser)(user);
147
- onLoggedIn(result.token);
109
+ await finish(result.token);
148
110
  }
149
- catch (err) {
150
- setEmailError(err instanceof Error ? err.message : String(err));
111
+ catch (e) {
112
+ setEmailError(e instanceof Error ? e.message : 'Something went wrong');
151
113
  }
152
114
  finally {
153
115
  setEmailBusy(false);
154
116
  }
155
117
  };
156
- return (<react_native_1.SafeAreaView style={styles.container}>
157
- <react_native_1.ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
158
- <react_native_1.View style={styles.header}>
159
- <react_native_1.Text style={styles.title}>Yaver Girişi</react_native_1.Text>
160
- {onCancel && (<react_native_1.TouchableOpacity onPress={onCancel} style={styles.cancel}>
161
- <react_native_1.Text style={styles.cancelText}>İptal</react_native_1.Text>
162
- </react_native_1.TouchableOpacity>)}
163
- </react_native_1.View>
118
+ const renderProvider = (id, label, onPress) => (<react_native_1.Pressable key={id} style={({ pressed }) => [
119
+ styles.button,
120
+ pressed && styles.buttonPressed,
121
+ busyProvider === id && { opacity: 0.6 },
122
+ ]} onPress={onPress} disabled={busyProvider !== null}>
123
+ {busyProvider === id ? (<react_native_1.ActivityIndicator color="#e0e0e0"/>) : (<react_native_1.Text style={styles.buttonText}>{label}</react_native_1.Text>)}
124
+ </react_native_1.Pressable>);
125
+ return (<react_native_1.SafeAreaView style={styles.safeArea}>
126
+ <react_native_1.KeyboardAvoidingView style={{ flex: 1 }} behavior={react_native_1.Platform.OS === 'ios' ? 'padding' : undefined}>
127
+ <react_native_1.ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled">
128
+ <react_native_1.View style={styles.header}>
129
+ <react_native_1.Text style={styles.logo}>Yaver</react_native_1.Text>
130
+ <react_native_1.Text style={styles.subtitle}>Sign in to send feedback</react_native_1.Text>
131
+ {onCancel && (<react_native_1.Pressable onPress={onCancel} style={styles.cancel}>
132
+ <react_native_1.Text style={styles.cancelText}>Cancel</react_native_1.Text>
133
+ </react_native_1.Pressable>)}
134
+ </react_native_1.View>
164
135
 
165
- <react_native_1.View style={styles.tabRow}>
166
- <react_native_1.TouchableOpacity style={[styles.tab, mode === 'device' && styles.tabActive]} onPress={() => setMode('device')}>
167
- <react_native_1.Text style={[styles.tabText, mode === 'device' && styles.tabTextActive]}>
168
- Hızlı Giriş (OAuth)
169
- </react_native_1.Text>
170
- </react_native_1.TouchableOpacity>
171
- <react_native_1.TouchableOpacity style={[styles.tab, mode === 'email' && styles.tabActive]} onPress={() => setMode('email')}>
172
- <react_native_1.Text style={[styles.tabText, mode === 'email' && styles.tabTextActive]}>
173
- E-posta
174
- </react_native_1.Text>
175
- </react_native_1.TouchableOpacity>
176
- </react_native_1.View>
136
+ <react_native_1.View style={styles.buttons}>
137
+ {react_native_1.Platform.OS === 'ios'
138
+ ? renderProvider('apple', 'Continue with Apple', handleApple)
139
+ : renderProvider('apple', 'Continue with Apple', () => handleOAuth('apple'))}
140
+ {renderProvider('google', 'Continue with Google', () => handleOAuth('google'))}
141
+ {renderProvider('github', 'Continue with GitHub', () => handleOAuth('github'))}
142
+ {renderProvider('gitlab', 'Continue with GitLab', () => handleOAuth('gitlab'))}
143
+ {renderProvider('microsoft', 'Continue with Microsoft', () => handleOAuth('microsoft'))}
177
144
 
178
- {mode === 'device' && (<react_native_1.View style={styles.section}>
179
- {starting ? (<react_native_1.ActivityIndicator color="#6366f1" style={{ marginVertical: 40 }}/>) : code ? (<>
180
- <react_native_1.Text style={styles.hint}>
181
- Tarayıcıda yaver.io/auth/device ve aşağıdaki kodu gir.
182
- Sağlayıcıyla giriş yaptığında buraya otomatik dönecek.
183
- </react_native_1.Text>
184
- <react_native_1.View style={styles.codeBox}>
185
- <react_native_1.Text style={styles.codeText}>{code.userCode}</react_native_1.Text>
145
+ {!showEmailForm ? (<react_native_1.Pressable style={({ pressed }) => [
146
+ styles.button,
147
+ pressed && styles.buttonPressed,
148
+ ]} onPress={() => setShowEmailForm(true)} disabled={busyProvider !== null}>
149
+ <react_native_1.Text style={styles.buttonText}>Continue with Email</react_native_1.Text>
150
+ </react_native_1.Pressable>) : (<>
151
+ <react_native_1.View style={styles.divider}>
152
+ <react_native_1.View style={styles.dividerLine}/>
153
+ <react_native_1.Text style={styles.dividerText}>email</react_native_1.Text>
154
+ <react_native_1.View style={styles.dividerLine}/>
186
155
  </react_native_1.View>
187
- <react_native_1.TouchableOpacity style={styles.primaryButton} onPress={openVerification}>
188
- <react_native_1.Text style={styles.primaryButtonText}>Tarayıcıda Aç</react_native_1.Text>
189
- </react_native_1.TouchableOpacity>
156
+ {isSignUp && (<react_native_1.TextInput style={styles.input} placeholder="Full Name" placeholderTextColor="#666" value={fullName} onChangeText={setFullName} autoCapitalize="words" autoCorrect={false}/>)}
157
+ <react_native_1.TextInput style={styles.input} placeholder="Email" placeholderTextColor="#666" value={email} onChangeText={setEmail} keyboardType="email-address" autoCapitalize="none" autoCorrect={false}/>
158
+ <react_native_1.TextInput style={styles.input} placeholder="Password" placeholderTextColor="#666" value={password} onChangeText={setPassword} secureTextEntry autoCapitalize="none"/>
159
+ {isSignUp && (<react_native_1.TextInput style={styles.input} placeholder="Confirm Password" placeholderTextColor="#666" value={confirmPassword} onChangeText={setConfirmPassword} secureTextEntry/>)}
190
160
 
191
- <react_native_1.Text style={[styles.hint, { marginTop: 20 }]}>
192
- Sağlayıcıyı önceden seçmek istersen:
193
- </react_native_1.Text>
194
- <react_native_1.View style={styles.providerRow}>
195
- {PROVIDERS.map((p) => (<react_native_1.TouchableOpacity key={p.id} style={styles.providerButton} onPress={() => beginDeviceCode(p.id)}>
196
- <react_native_1.Text style={styles.providerButtonText}>{p.label}</react_native_1.Text>
197
- </react_native_1.TouchableOpacity>))}
198
- </react_native_1.View>
199
- </>) : (<react_native_1.TouchableOpacity style={styles.primaryButton} onPress={() => beginDeviceCode(undefined)}>
200
- <react_native_1.Text style={styles.primaryButtonText}>Tekrar Başlat</react_native_1.Text>
201
- </react_native_1.TouchableOpacity>)}
202
- {codeError && <react_native_1.Text style={styles.error}>{codeError}</react_native_1.Text>}
203
- </react_native_1.View>)}
161
+ {emailError ? (<react_native_1.Text style={styles.errorText}>{emailError}</react_native_1.Text>) : null}
204
162
 
205
- {mode === 'email' && (<react_native_1.View style={styles.section}>
206
- <react_native_1.View style={styles.tabRow}>
207
- <react_native_1.TouchableOpacity style={[styles.subTab, emailMode === 'login' && styles.subTabActive]} onPress={() => setEmailMode('login')}>
208
- <react_native_1.Text style={styles.subTabText}>Giriş</react_native_1.Text>
209
- </react_native_1.TouchableOpacity>
210
- <react_native_1.TouchableOpacity style={[styles.subTab, emailMode === 'signup' && styles.subTabActive]} onPress={() => setEmailMode('signup')}>
211
- <react_native_1.Text style={styles.subTabText}>Kayıt</react_native_1.Text>
212
- </react_native_1.TouchableOpacity>
213
- </react_native_1.View>
163
+ <react_native_1.Pressable style={({ pressed }) => [
164
+ styles.submitButton,
165
+ pressed && styles.buttonPressed,
166
+ emailBusy && { opacity: 0.6 },
167
+ ]} onPress={handleEmailSubmit} disabled={emailBusy}>
168
+ {emailBusy ? (<react_native_1.ActivityIndicator color="#fff"/>) : (<react_native_1.Text style={styles.submitButtonText}>
169
+ {isSignUp ? 'Create Account' : 'Sign In'}
170
+ </react_native_1.Text>)}
171
+ </react_native_1.Pressable>
214
172
 
215
- {emailMode === 'signup' && (<>
216
- <react_native_1.Text style={styles.label}>Ad Soyad</react_native_1.Text>
217
- <react_native_1.TextInput style={styles.input} value={fullName} onChangeText={setFullName} placeholder="Adın Soyadın" placeholderTextColor="#666" autoCapitalize="words"/>
173
+ <react_native_1.Pressable onPress={() => {
174
+ setIsSignUp(!isSignUp);
175
+ setEmailError('');
176
+ }}>
177
+ <react_native_1.Text style={styles.toggleText}>
178
+ {isSignUp
179
+ ? 'Already have an account? Sign In'
180
+ : "Don't have an account? Sign Up"}
181
+ </react_native_1.Text>
182
+ </react_native_1.Pressable>
218
183
  </>)}
219
-
220
- <react_native_1.Text style={styles.label}>E-posta</react_native_1.Text>
221
- <react_native_1.TextInput style={styles.input} value={email} onChangeText={setEmail} placeholder="you@example.com" placeholderTextColor="#666" keyboardType="email-address" autoCapitalize="none" autoCorrect={false}/>
222
-
223
- <react_native_1.Text style={styles.label}>Parola</react_native_1.Text>
224
- <react_native_1.TextInput style={styles.input} value={password} onChangeText={setPassword} placeholder="••••••••" placeholderTextColor="#666" secureTextEntry autoCapitalize="none"/>
225
-
226
- <react_native_1.TouchableOpacity style={styles.primaryButton} onPress={handleEmailSubmit} disabled={emailBusy}>
227
- {emailBusy ? (<react_native_1.ActivityIndicator color="#fff"/>) : (<react_native_1.Text style={styles.primaryButtonText}>
228
- {emailMode === 'signup' ? 'Kayıt Ol' : 'Giriş Yap'}
229
- </react_native_1.Text>)}
230
- </react_native_1.TouchableOpacity>
231
-
232
- {emailError && <react_native_1.Text style={styles.error}>{emailError}</react_native_1.Text>}
233
- </react_native_1.View>)}
234
- </react_native_1.ScrollView>
184
+ </react_native_1.View>
185
+ </react_native_1.ScrollView>
186
+ </react_native_1.KeyboardAvoidingView>
235
187
  </react_native_1.SafeAreaView>);
236
188
  };
237
189
  exports.YaverLoginScreen = YaverLoginScreen;
238
190
  const styles = react_native_1.StyleSheet.create({
239
- container: { flex: 1, backgroundColor: '#1a1a2e' },
240
- content: { padding: 24, paddingTop: 16 },
241
- header: {
242
- flexDirection: 'row',
243
- alignItems: 'center',
244
- justifyContent: 'space-between',
245
- marginBottom: 20,
191
+ safeArea: { flex: 1, backgroundColor: '#1a1a2e' },
192
+ scrollContainer: {
193
+ flexGrow: 1,
194
+ paddingHorizontal: 24,
195
+ justifyContent: 'center',
246
196
  },
247
- title: { fontSize: 22, fontWeight: '700', color: '#e0e0e0' },
248
- cancel: { padding: 8 },
197
+ header: { alignItems: 'center', marginBottom: 40 },
198
+ logo: { fontSize: 44, fontWeight: '800', color: '#e0e0e0', letterSpacing: -1 },
199
+ subtitle: { fontSize: 15, color: '#9ca3af', marginTop: 6 },
200
+ cancel: { position: 'absolute', right: 0, top: 0, padding: 8 },
249
201
  cancelText: { color: '#9ca3af', fontSize: 14 },
250
- tabRow: { flexDirection: 'row', gap: 8, marginBottom: 16 },
251
- tab: {
252
- flex: 1,
253
- paddingVertical: 12,
254
- borderRadius: 10,
255
- backgroundColor: 'rgba(255,255,255,0.05)',
256
- alignItems: 'center',
257
- },
258
- tabActive: { backgroundColor: 'rgba(99,102,241,0.25)' },
259
- tabText: { color: '#9ca3af', fontSize: 14, fontWeight: '600' },
260
- tabTextActive: { color: '#e0e0e0' },
261
- subTab: {
262
- flex: 1,
263
- paddingVertical: 8,
264
- borderRadius: 8,
265
- alignItems: 'center',
266
- backgroundColor: 'rgba(255,255,255,0.05)',
267
- },
268
- subTabActive: { backgroundColor: 'rgba(99,102,241,0.2)' },
269
- subTabText: { color: '#e0e0e0', fontSize: 13 },
270
- section: { marginTop: 4 },
271
- hint: { color: '#9ca3af', fontSize: 13, lineHeight: 18 },
272
- codeBox: {
273
- backgroundColor: 'rgba(99,102,241,0.15)',
202
+ buttons: { gap: 12 },
203
+ button: {
204
+ backgroundColor: 'rgba(255,255,255,0.06)',
274
205
  borderWidth: 1,
275
- borderColor: 'rgba(99,102,241,0.35)',
276
- borderRadius: 14,
277
- paddingVertical: 24,
278
- alignItems: 'center',
279
- marginTop: 16,
280
- marginBottom: 16,
281
- },
282
- codeText: {
283
- color: '#e0e7ff',
284
- fontSize: 36,
285
- fontWeight: '800',
286
- letterSpacing: 6,
287
- fontVariant: ['tabular-nums'],
288
- },
289
- primaryButton: {
290
- backgroundColor: '#6366f1',
206
+ borderColor: 'rgba(255,255,255,0.12)',
291
207
  borderRadius: 12,
292
208
  paddingVertical: 14,
293
209
  alignItems: 'center',
294
- marginTop: 8,
210
+ justifyContent: 'center',
295
211
  },
296
- primaryButtonText: { color: '#fff', fontWeight: '700', fontSize: 15 },
297
- providerRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 10 },
298
- providerButton: {
299
- backgroundColor: 'rgba(255,255,255,0.08)',
300
- borderRadius: 10,
301
- paddingVertical: 10,
302
- paddingHorizontal: 14,
212
+ buttonPressed: { opacity: 0.7 },
213
+ buttonText: { color: '#e0e0e0', fontSize: 15, fontWeight: '600' },
214
+ divider: {
215
+ flexDirection: 'row',
216
+ alignItems: 'center',
217
+ marginTop: 16,
218
+ marginBottom: 8,
303
219
  },
304
- providerButtonText: { color: '#e0e0e0', fontSize: 13, fontWeight: '600' },
305
- label: { color: '#9ca3af', fontSize: 12, marginTop: 14, marginBottom: 6 },
220
+ dividerLine: { flex: 1, height: 1, backgroundColor: 'rgba(255,255,255,0.12)' },
221
+ dividerText: { marginHorizontal: 14, fontSize: 12, color: '#6b7280' },
306
222
  input: {
307
- backgroundColor: 'rgba(255,255,255,0.08)',
223
+ backgroundColor: 'rgba(255,255,255,0.06)',
308
224
  borderWidth: 1,
309
- borderColor: 'rgba(255,255,255,0.15)',
310
- borderRadius: 10,
225
+ borderColor: 'rgba(255,255,255,0.12)',
226
+ borderRadius: 12,
311
227
  paddingHorizontal: 14,
312
- paddingVertical: 12,
228
+ paddingVertical: 13,
313
229
  color: '#e0e0e0',
314
230
  fontSize: 15,
315
231
  },
316
- error: { color: '#ef4444', fontSize: 13, marginTop: 12 },
232
+ errorText: { color: '#ef4444', fontSize: 13, textAlign: 'center' },
233
+ submitButton: {
234
+ backgroundColor: '#6366f1',
235
+ borderRadius: 12,
236
+ paddingVertical: 14,
237
+ alignItems: 'center',
238
+ justifyContent: 'center',
239
+ marginTop: 4,
240
+ },
241
+ submitButtonText: { color: '#fff', fontSize: 15, fontWeight: '700' },
242
+ toggleText: {
243
+ color: '#818cf8',
244
+ fontSize: 14,
245
+ textAlign: 'center',
246
+ marginTop: 4,
247
+ },
317
248
  });
@@ -76,6 +76,9 @@ class YaverFeedback {
76
76
  convexSiteUrl: cfg.authConvexSiteUrl,
77
77
  webBaseUrl: cfg.authWebBaseUrl,
78
78
  });
79
+ // Compile-time lockdown: refuse any browser-hop / device-code fallback
80
+ // and force ASWebAuthenticationSession in ephemeral mode for OAuth.
81
+ (0, auth_1.setStrictNativeAuth)(cfg.strictNativeAuth === true);
79
82
  // If no explicit convexUrl was set but we have an auth site URL, use it
80
83
  // so Discovery.discoverFromConvex() has somewhere to look up the user's
81
84
  // machines (works for both LAN-direct and off-LAN relay paths).
package/dist/auth.d.ts CHANGED
@@ -1,22 +1,19 @@
1
1
  /**
2
2
  * Authentication + device/agent discovery API used by the Yaver Feedback SDK.
3
3
  *
4
- * This module is a trimmed SDK-local port of mobile/src/lib/auth.ts. It only
5
- * covers what the embedded login/machine-picker flow needs:
4
+ * Mirrors mobile/src/lib/auth.ts:
6
5
  *
7
- * - Device-code login (`POST /auth/device-code` + `GET /auth/device-code/poll`)
8
- * so users can sign in via any OAuth provider (apple/google/github/gitlab/
9
- * microsoft) on yaver.io without requiring deep-link wiring in the host app.
6
+ * - Native Apple Sign-In (`POST /auth/apple-native`) on iOS via
7
+ * `expo-apple-authentication`.
8
+ * - In-app browser OAuth for Google/Microsoft/GitHub/GitLab via
9
+ * `expo-web-browser`'s `openAuthSessionAsync` — same callback URL
10
+ * (`yaver://oauth-callback`) the Yaver mobile app uses.
10
11
  * - Email / password sign-up + login (no 2FA flow — for SDK simplicity).
11
- * - Token validation + refresh.
12
+ * - Token validation.
12
13
  * - `/devices/list` → owned + shared (guest) remote dev machines.
13
14
  *
14
- * All calls target the public Yaver Convex site URL by default; callers may
15
- * override via `init()` config to point at staging.
16
- *
17
- * Token persistence uses `@react-native-async-storage/async-storage` (already
18
- * a peer dep). SecureStore is intentionally avoided to keep the SDK portable
19
- * to any RN host app.
15
+ * Mobile-only. A web equivalent will ship as a separate `yaver-web-feedback`
16
+ * package; do not import this module from a browser bundle.
20
17
  */
21
18
  export declare const DEFAULT_CONVEX_SITE_URL = "https://shocking-echidna-394.eu-west-1.convex.site";
22
19
  export declare const DEFAULT_WEB_BASE_URL = "https://yaver.io";
@@ -25,6 +22,13 @@ export declare function configureAuthEndpoints(opts: {
25
22
  convexSiteUrl?: string;
26
23
  webBaseUrl?: string;
27
24
  }): void;
25
+ /**
26
+ * Enable strict native auth: refuse any fallback that would redirect the
27
+ * user to an external browser (Safari / Chrome) or show a device code.
28
+ * See FeedbackConfig.strictNativeAuth for rationale.
29
+ */
30
+ export declare function setStrictNativeAuth(enabled: boolean): void;
31
+ export declare function isStrictNativeAuth(): boolean;
28
32
  export declare function getConvexSiteUrl(): string;
29
33
  export declare function getWebBaseUrl(): string;
30
34
  export type OAuthProvider = 'google' | 'microsoft' | 'apple' | 'github' | 'gitlab';
@@ -44,31 +48,40 @@ export declare function getSelectedDeviceId(): Promise<string | null>;
44
48
  export declare function saveSelectedDeviceId(deviceId: string): Promise<void>;
45
49
  export declare function clearSelectedDeviceId(): Promise<void>;
46
50
  export declare function validateToken(token: string): Promise<User | null>;
47
- export interface DeviceCodeStart {
48
- userCode: string;
49
- deviceCode: string;
50
- expiresAt: number;
51
- verificationUrl: string;
52
- }
53
51
  /**
54
- * Start a device-code flow. The user opens `verificationUrl`, signs in with
55
- * any OAuth provider on yaver.io, and the SDK polls `pollDeviceCode` until
56
- * a session token is issued.
52
+ * Sign in with Apple using the native ASAuthorization flow. Requires
53
+ * `expo-apple-authentication` installed and the host app's bundle to have
54
+ * the "Sign in with Apple" capability enabled. iOS only.
55
+ *
56
+ * Throws `cancelled` if the user dismisses the sheet.
57
+ */
58
+ export declare function signInWithApple(): Promise<{
59
+ token: string;
60
+ userId: string;
61
+ }>;
62
+ /**
63
+ * Default OAuth redirect — the same callback the Yaver mobile app uses
64
+ * (`yaver://oauth-callback`). `WebBrowser.openAuthSessionAsync` intercepts
65
+ * this redirect inside the auth session, so the host app does not need to
66
+ * register the scheme on iOS. On Android, add an `<intent-filter>` for
67
+ * `yaver://oauth-callback` in the host app's AndroidManifest.xml.
57
68
  */
58
- export declare function startDeviceCode(opts?: {
59
- machineName?: string;
60
- platform?: string;
61
- preferredProvider?: OAuthProvider;
62
- }): Promise<DeviceCodeStart>;
63
- export type DeviceCodePoll = {
64
- status: 'pending';
65
- } | {
66
- status: 'authorized';
69
+ export declare const DEFAULT_OAUTH_REDIRECT = "yaver://oauth-callback";
70
+ /**
71
+ * Sign in through the in-app browser via yaver.io. Opens
72
+ * `https://yaver.io/api/auth/oauth/<provider>?client=mobile`, the user picks
73
+ * an OAuth provider, and the web callback redirects back to
74
+ * `yaver://oauth-callback?token=...` which `openAuthSessionAsync` captures
75
+ * inside the auth session. No deep-link wiring required on iOS.
76
+ *
77
+ * Throws `cancelled` if the user dismisses the browser.
78
+ */
79
+ export declare function signInWithOAuth(provider: OAuthProvider, opts?: {
80
+ redirectUrl?: string;
81
+ preferEphemeralSession?: boolean;
82
+ }): Promise<{
67
83
  token: string;
68
- } | {
69
- status: 'expired';
70
- };
71
- export declare function pollDeviceCode(deviceCode: string): Promise<DeviceCodePoll>;
84
+ }>;
72
85
  export declare function signupWithEmail(fullName: string, email: string, password: string): Promise<{
73
86
  token: string;
74
87
  userId: string;