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 +26 -4
- package/dist/LoginScreen.d.ts +3 -4
- package/dist/LoginScreen.js +155 -224
- package/dist/YaverFeedback.js +3 -0
- package/dist/auth.d.ts +47 -34
- package/dist/auth.js +138 -53
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -3
- package/dist/types.d.ts +21 -1
- package/package.json +7 -2
- package/src/LoginScreen.tsx +250 -313
- package/src/YaverFeedback.ts +4 -0
- package/src/auth.ts +171 -68
- package/src/index.ts +3 -4
- package/src/types.ts +21 -1
package/src/auth.ts
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Authentication + device/agent discovery API used by the Yaver Feedback SDK.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* covers what the embedded login/machine-picker flow needs:
|
|
4
|
+
* Mirrors mobile/src/lib/auth.ts:
|
|
6
5
|
*
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
12
|
+
* - Token validation.
|
|
12
13
|
* - `/devices/list` → owned + shared (guest) remote dev machines.
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
|
22
19
|
// AsyncStorage is an optional peer dep — degrade gracefully if missing.
|
|
@@ -31,6 +28,41 @@ try {
|
|
|
31
28
|
// not installed — token persistence disabled, caller must pass authToken
|
|
32
29
|
}
|
|
33
30
|
|
|
31
|
+
// Optional peer deps used by native sign-in and in-app browser OAuth. When
|
|
32
|
+
// missing the SDK still works — Apple falls back to in-app browser OAuth, and
|
|
33
|
+
// providers without expo-web-browser surface a clear error.
|
|
34
|
+
type WebBrowserModule = {
|
|
35
|
+
openAuthSessionAsync: (
|
|
36
|
+
url: string,
|
|
37
|
+
redirectUrl: string,
|
|
38
|
+
options?: { showInRecents?: boolean; preferEphemeralSession?: boolean },
|
|
39
|
+
) => Promise<{ type: string; url?: string }>;
|
|
40
|
+
maybeCompleteAuthSession: () => void;
|
|
41
|
+
};
|
|
42
|
+
type AppleAuthModule = {
|
|
43
|
+
isAvailableAsync: () => Promise<boolean>;
|
|
44
|
+
signInAsync: (opts: { requestedScopes: number[] }) => Promise<{
|
|
45
|
+
identityToken: string | null;
|
|
46
|
+
fullName?: { givenName?: string | null; familyName?: string | null } | null;
|
|
47
|
+
}>;
|
|
48
|
+
AppleAuthenticationScope: { FULL_NAME: number; EMAIL: number };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
let WebBrowser: WebBrowserModule | null = null;
|
|
52
|
+
try {
|
|
53
|
+
WebBrowser = require('expo-web-browser');
|
|
54
|
+
WebBrowser?.maybeCompleteAuthSession();
|
|
55
|
+
} catch {
|
|
56
|
+
// optional
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let AppleAuth: AppleAuthModule | null = null;
|
|
60
|
+
try {
|
|
61
|
+
AppleAuth = require('expo-apple-authentication');
|
|
62
|
+
} catch {
|
|
63
|
+
// optional — Apple sign-in falls back to in-app browser OAuth
|
|
64
|
+
}
|
|
65
|
+
|
|
34
66
|
const TOKEN_KEY = 'yaver_feedback_auth_token';
|
|
35
67
|
const USER_KEY = 'yaver_feedback_user';
|
|
36
68
|
const DEVICE_KEY = 'yaver_feedback_selected_device';
|
|
@@ -41,6 +73,7 @@ export const DEFAULT_WEB_BASE_URL = 'https://yaver.io';
|
|
|
41
73
|
|
|
42
74
|
let convexSiteUrl = DEFAULT_CONVEX_SITE_URL;
|
|
43
75
|
let webBaseUrl = DEFAULT_WEB_BASE_URL;
|
|
76
|
+
let strictNativeAuth = false;
|
|
44
77
|
|
|
45
78
|
/** Override the Convex site URL + web base (staging vs prod). */
|
|
46
79
|
export function configureAuthEndpoints(opts: {
|
|
@@ -51,6 +84,19 @@ export function configureAuthEndpoints(opts: {
|
|
|
51
84
|
if (opts.webBaseUrl) webBaseUrl = opts.webBaseUrl;
|
|
52
85
|
}
|
|
53
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Enable strict native auth: refuse any fallback that would redirect the
|
|
89
|
+
* user to an external browser (Safari / Chrome) or show a device code.
|
|
90
|
+
* See FeedbackConfig.strictNativeAuth for rationale.
|
|
91
|
+
*/
|
|
92
|
+
export function setStrictNativeAuth(enabled: boolean): void {
|
|
93
|
+
strictNativeAuth = enabled;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function isStrictNativeAuth(): boolean {
|
|
97
|
+
return strictNativeAuth;
|
|
98
|
+
}
|
|
99
|
+
|
|
54
100
|
export function getConvexSiteUrl(): string {
|
|
55
101
|
return convexSiteUrl;
|
|
56
102
|
}
|
|
@@ -177,74 +223,131 @@ export async function validateToken(token: string): Promise<User | null> {
|
|
|
177
223
|
}
|
|
178
224
|
}
|
|
179
225
|
|
|
180
|
-
// ───
|
|
181
|
-
|
|
182
|
-
export interface DeviceCodeStart {
|
|
183
|
-
userCode: string;
|
|
184
|
-
deviceCode: string;
|
|
185
|
-
expiresAt: number;
|
|
186
|
-
verificationUrl: string;
|
|
187
|
-
}
|
|
226
|
+
// ─── Native Apple Sign-In ─────────────────────────────────────────────
|
|
188
227
|
|
|
189
228
|
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
229
|
+
* Sign in with Apple using the native ASAuthorization flow. Requires
|
|
230
|
+
* `expo-apple-authentication` installed and the host app's bundle to have
|
|
231
|
+
* the "Sign in with Apple" capability enabled. iOS only.
|
|
232
|
+
*
|
|
233
|
+
* Throws `cancelled` if the user dismisses the sheet.
|
|
193
234
|
*/
|
|
194
|
-
export async function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
235
|
+
export async function signInWithApple(): Promise<{ token: string; userId: string }> {
|
|
236
|
+
if (!AppleAuth) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
'expo-apple-authentication is not installed. Add it as a peer dep to enable native Apple Sign-In.',
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
const available = await AppleAuth.isAvailableAsync();
|
|
242
|
+
if (!available) {
|
|
243
|
+
throw new Error('Apple Sign-In is not available on this device');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let credential;
|
|
247
|
+
try {
|
|
248
|
+
credential = await AppleAuth.signInAsync({
|
|
249
|
+
requestedScopes: [
|
|
250
|
+
AppleAuth.AppleAuthenticationScope.FULL_NAME,
|
|
251
|
+
AppleAuth.AppleAuthenticationScope.EMAIL,
|
|
252
|
+
],
|
|
253
|
+
});
|
|
254
|
+
} catch (err) {
|
|
255
|
+
if ((err as { code?: string } | null)?.code === 'ERR_REQUEST_CANCELED') {
|
|
256
|
+
throw new Error('cancelled');
|
|
257
|
+
}
|
|
258
|
+
throw err;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!credential.identityToken) {
|
|
262
|
+
throw new Error('Apple did not return an identity token');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const fullName =
|
|
266
|
+
[credential.fullName?.givenName, credential.fullName?.familyName]
|
|
267
|
+
.filter(Boolean)
|
|
268
|
+
.join(' ') || undefined;
|
|
269
|
+
|
|
270
|
+
const res = await fetch(`${convexSiteUrl}/auth/apple-native`, {
|
|
200
271
|
method: 'POST',
|
|
201
272
|
headers: { 'Content-Type': 'application/json' },
|
|
202
|
-
body: JSON.stringify({
|
|
203
|
-
machineName: opts?.machineName,
|
|
204
|
-
platform: opts?.platform,
|
|
205
|
-
preferredProvider: opts?.preferredProvider,
|
|
206
|
-
environment: 'feedback-sdk',
|
|
207
|
-
}),
|
|
273
|
+
body: JSON.stringify({ identityToken: credential.identityToken, fullName }),
|
|
208
274
|
});
|
|
209
275
|
if (!res.ok) {
|
|
210
|
-
const
|
|
211
|
-
throw new Error(
|
|
276
|
+
const body = await res.text().catch(() => '');
|
|
277
|
+
throw new Error(body || 'Apple sign-in failed');
|
|
212
278
|
}
|
|
213
279
|
const data = await res.json();
|
|
214
|
-
|
|
215
|
-
if (opts?.preferredProvider) {
|
|
216
|
-
params.set('preferredProvider', opts.preferredProvider);
|
|
217
|
-
}
|
|
218
|
-
return {
|
|
219
|
-
userCode: data.userCode,
|
|
220
|
-
deviceCode: data.deviceCode,
|
|
221
|
-
expiresAt: data.expiresAt,
|
|
222
|
-
verificationUrl: `${webBaseUrl}/auth/device?${params.toString()}`,
|
|
223
|
-
};
|
|
280
|
+
return { token: data.token, userId: data.userId };
|
|
224
281
|
}
|
|
225
282
|
|
|
226
|
-
|
|
227
|
-
| { status: 'pending' }
|
|
228
|
-
| { status: 'authorized'; token: string }
|
|
229
|
-
| { status: 'expired' };
|
|
283
|
+
// ─── In-app browser OAuth (Google / GitHub / GitLab / Microsoft) ──────
|
|
230
284
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
285
|
+
/**
|
|
286
|
+
* Default OAuth redirect — the same callback the Yaver mobile app uses
|
|
287
|
+
* (`yaver://oauth-callback`). `WebBrowser.openAuthSessionAsync` intercepts
|
|
288
|
+
* this redirect inside the auth session, so the host app does not need to
|
|
289
|
+
* register the scheme on iOS. On Android, add an `<intent-filter>` for
|
|
290
|
+
* `yaver://oauth-callback` in the host app's AndroidManifest.xml.
|
|
291
|
+
*/
|
|
292
|
+
export const DEFAULT_OAUTH_REDIRECT = 'yaver://oauth-callback';
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Sign in through the in-app browser via yaver.io. Opens
|
|
296
|
+
* `https://yaver.io/api/auth/oauth/<provider>?client=mobile`, the user picks
|
|
297
|
+
* an OAuth provider, and the web callback redirects back to
|
|
298
|
+
* `yaver://oauth-callback?token=...` which `openAuthSessionAsync` captures
|
|
299
|
+
* inside the auth session. No deep-link wiring required on iOS.
|
|
300
|
+
*
|
|
301
|
+
* Throws `cancelled` if the user dismisses the browser.
|
|
302
|
+
*/
|
|
303
|
+
export async function signInWithOAuth(
|
|
304
|
+
provider: OAuthProvider,
|
|
305
|
+
opts?: { redirectUrl?: string; preferEphemeralSession?: boolean },
|
|
306
|
+
): Promise<{ token: string }> {
|
|
307
|
+
if (!WebBrowser) {
|
|
308
|
+
// In strictNativeAuth we hard-fail rather than letting the caller
|
|
309
|
+
// fall back to any homegrown `Linking.openURL(…)` flow that would
|
|
310
|
+
// leave the app for Safari. Without strict mode we still can't
|
|
311
|
+
// proceed (no browser module available) so the behavior is the same
|
|
312
|
+
// — just a clearer error message.
|
|
313
|
+
throw new Error(
|
|
314
|
+
'expo-web-browser is not installed. Add it as a peer dep to enable in-app OAuth sign-in.',
|
|
237
315
|
);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
316
|
+
}
|
|
317
|
+
const redirectUrl = opts?.redirectUrl ?? DEFAULT_OAUTH_REDIRECT;
|
|
318
|
+
const params = new URLSearchParams({ client: 'mobile' });
|
|
319
|
+
const authUrl = `${webBaseUrl}/api/auth/oauth/${provider}?${params.toString()}`;
|
|
320
|
+
|
|
321
|
+
// In strict mode force ephemeral session (ASWebAuthenticationSession
|
|
322
|
+
// with no shared cookie jar) so the OAuth dance is visibly native and
|
|
323
|
+
// can never hand off to the user's default browser.
|
|
324
|
+
const prefer =
|
|
325
|
+
strictNativeAuth || opts?.preferEphemeralSession
|
|
326
|
+
? true
|
|
327
|
+
: false;
|
|
328
|
+
const result = await WebBrowser.openAuthSessionAsync(authUrl, redirectUrl, {
|
|
329
|
+
preferEphemeralSession: prefer,
|
|
330
|
+
showInRecents: !strictNativeAuth,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
if (result.type !== 'success' || !result.url) {
|
|
334
|
+
throw new Error('cancelled');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let token: string | null = null;
|
|
338
|
+
try {
|
|
339
|
+
const parsed = new URL(result.url);
|
|
340
|
+
token = parsed.searchParams.get('token');
|
|
245
341
|
} catch {
|
|
246
|
-
|
|
342
|
+
// best-effort fallback for odd schemes
|
|
343
|
+
const match = result.url.match(/[?&]token=([^&]+)/);
|
|
344
|
+
if (match) token = decodeURIComponent(match[1]);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!token) {
|
|
348
|
+
throw new Error('OAuth callback did not include a token');
|
|
247
349
|
}
|
|
350
|
+
return { token };
|
|
248
351
|
}
|
|
249
352
|
|
|
250
353
|
// ─── Email / password (no 2FA) ────────────────────────────────────────
|
|
@@ -281,10 +384,10 @@ export async function loginWithEmail(
|
|
|
281
384
|
}
|
|
282
385
|
const data = await res.json();
|
|
283
386
|
if (data?.requires2fa) {
|
|
284
|
-
// SDK login surface does not handle 2FA — direct the user to
|
|
285
|
-
//
|
|
387
|
+
// SDK login surface does not handle 2FA — direct the user to sign in
|
|
388
|
+
// through one of the OAuth providers, which complete 2FA on the web.
|
|
286
389
|
throw new Error(
|
|
287
|
-
'2FA is enabled on this account. Sign in
|
|
390
|
+
'2FA is enabled on this account. Sign in with Apple/Google/GitHub/GitLab/Microsoft instead.',
|
|
288
391
|
);
|
|
289
392
|
}
|
|
290
393
|
return { token: data.token, userId: data.userId };
|
package/src/index.ts
CHANGED
|
@@ -52,19 +52,18 @@ export {
|
|
|
52
52
|
saveSelectedDeviceId,
|
|
53
53
|
clearSelectedDeviceId,
|
|
54
54
|
validateToken,
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
signInWithApple,
|
|
56
|
+
signInWithOAuth,
|
|
57
57
|
signupWithEmail,
|
|
58
58
|
loginWithEmail,
|
|
59
59
|
listReachableDevices,
|
|
60
60
|
DEFAULT_CONVEX_SITE_URL,
|
|
61
61
|
DEFAULT_WEB_BASE_URL,
|
|
62
|
+
DEFAULT_OAUTH_REDIRECT,
|
|
62
63
|
} from './auth';
|
|
63
64
|
export type {
|
|
64
65
|
OAuthProvider,
|
|
65
66
|
User,
|
|
66
|
-
DeviceCodeStart,
|
|
67
|
-
DeviceCodePoll,
|
|
68
67
|
RemoteDevice,
|
|
69
68
|
DeviceList,
|
|
70
69
|
} from './auth';
|
package/src/types.ts
CHANGED
|
@@ -4,7 +4,8 @@ export interface FeedbackConfig {
|
|
|
4
4
|
/**
|
|
5
5
|
* Auth token for the Yaver agent. Optional in 0.5+: if omitted, the SDK
|
|
6
6
|
* will hydrate one from AsyncStorage or show its in-app login screen
|
|
7
|
-
* (
|
|
7
|
+
* (Apple native / OAuth in-app browser / email) the first time the user
|
|
8
|
+
* triggers feedback.
|
|
8
9
|
*/
|
|
9
10
|
authToken?: string;
|
|
10
11
|
/**
|
|
@@ -156,6 +157,25 @@ export interface FeedbackConfig {
|
|
|
156
157
|
* Default: false (HTTPS preferred when fingerprint available, HTTP fallback).
|
|
157
158
|
*/
|
|
158
159
|
requireTLS?: boolean;
|
|
160
|
+
/**
|
|
161
|
+
* Compile-time lockdown of the auth flow. When true the SDK refuses to
|
|
162
|
+
* ever open the user's external browser (Safari / Chrome) or show a
|
|
163
|
+
* 6-char device code. Auth happens only via native Apple Sign-In
|
|
164
|
+
* (`expo-apple-authentication`), in-app OAuth (`expo-web-browser`'s
|
|
165
|
+
* `ASWebAuthenticationSession` with `preferEphemeralSession: true`), or
|
|
166
|
+
* the built-in email/password form. If a required peer dep is missing,
|
|
167
|
+
* `signInWithOAuth`/`signInWithApple` throw instead of silently falling
|
|
168
|
+
* back to a web redirect.
|
|
169
|
+
*
|
|
170
|
+
* Recommended for apps that already embed OAuth on the native side and
|
|
171
|
+
* never want their users to see a `yaver.io` landing page. This is the
|
|
172
|
+
* belt-and-suspenders version of what the SDK has done since 0.6; set
|
|
173
|
+
* it to guarantee future regressions can't quietly reintroduce a
|
|
174
|
+
* browser-hop fallback.
|
|
175
|
+
*
|
|
176
|
+
* Default: false (preserve historical behavior).
|
|
177
|
+
*/
|
|
178
|
+
strictNativeAuth?: boolean;
|
|
159
179
|
}
|
|
160
180
|
|
|
161
181
|
export interface FeedbackBundle {
|