yaver-feedback-react-native 0.5.4 → 0.6.0

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/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";
@@ -44,31 +41,40 @@ export declare function getSelectedDeviceId(): Promise<string | null>;
44
41
  export declare function saveSelectedDeviceId(deviceId: string): Promise<void>;
45
42
  export declare function clearSelectedDeviceId(): Promise<void>;
46
43
  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
44
  /**
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.
45
+ * Sign in with Apple using the native ASAuthorization flow. Requires
46
+ * `expo-apple-authentication` installed and the host app's bundle to have
47
+ * the "Sign in with Apple" capability enabled. iOS only.
48
+ *
49
+ * Throws `cancelled` if the user dismisses the sheet.
50
+ */
51
+ export declare function signInWithApple(): Promise<{
52
+ token: string;
53
+ userId: string;
54
+ }>;
55
+ /**
56
+ * Default OAuth redirect — the same callback the Yaver mobile app uses
57
+ * (`yaver://oauth-callback`). `WebBrowser.openAuthSessionAsync` intercepts
58
+ * this redirect inside the auth session, so the host app does not need to
59
+ * register the scheme on iOS. On Android, add an `<intent-filter>` for
60
+ * `yaver://oauth-callback` in the host app's AndroidManifest.xml.
61
+ */
62
+ export declare const DEFAULT_OAUTH_REDIRECT = "yaver://oauth-callback";
63
+ /**
64
+ * Sign in through the in-app browser via yaver.io. Opens
65
+ * `https://yaver.io/api/auth/oauth/<provider>?client=mobile`, the user picks
66
+ * an OAuth provider, and the web callback redirects back to
67
+ * `yaver://oauth-callback?token=...` which `openAuthSessionAsync` captures
68
+ * inside the auth session. No deep-link wiring required on iOS.
69
+ *
70
+ * Throws `cancelled` if the user dismisses the browser.
57
71
  */
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';
72
+ export declare function signInWithOAuth(provider: OAuthProvider, opts?: {
73
+ redirectUrl?: string;
74
+ preferEphemeralSession?: boolean;
75
+ }): Promise<{
67
76
  token: string;
68
- } | {
69
- status: 'expired';
70
- };
71
- export declare function pollDeviceCode(deviceCode: string): Promise<DeviceCodePoll>;
77
+ }>;
72
78
  export declare function signupWithEmail(fullName: string, email: string, password: string): Promise<{
73
79
  token: string;
74
80
  userId: string;
package/dist/auth.js CHANGED
@@ -2,25 +2,22 @@
2
2
  /**
3
3
  * Authentication + device/agent discovery API used by the Yaver Feedback SDK.
4
4
  *
5
- * This module is a trimmed SDK-local port of mobile/src/lib/auth.ts. It only
6
- * covers what the embedded login/machine-picker flow needs:
5
+ * Mirrors mobile/src/lib/auth.ts:
7
6
  *
8
- * - Device-code login (`POST /auth/device-code` + `GET /auth/device-code/poll`)
9
- * so users can sign in via any OAuth provider (apple/google/github/gitlab/
10
- * microsoft) on yaver.io without requiring deep-link wiring in the host app.
7
+ * - Native Apple Sign-In (`POST /auth/apple-native`) on iOS via
8
+ * `expo-apple-authentication`.
9
+ * - In-app browser OAuth for Google/Microsoft/GitHub/GitLab via
10
+ * `expo-web-browser`'s `openAuthSessionAsync` — same callback URL
11
+ * (`yaver://oauth-callback`) the Yaver mobile app uses.
11
12
  * - Email / password sign-up + login (no 2FA flow — for SDK simplicity).
12
- * - Token validation + refresh.
13
+ * - Token validation.
13
14
  * - `/devices/list` → owned + shared (guest) remote dev machines.
14
15
  *
15
- * All calls target the public Yaver Convex site URL by default; callers may
16
- * override via `init()` config to point at staging.
17
- *
18
- * Token persistence uses `@react-native-async-storage/async-storage` (already
19
- * a peer dep). SecureStore is intentionally avoided to keep the SDK portable
20
- * to any RN host app.
16
+ * Mobile-only. A web equivalent will ship as a separate `yaver-web-feedback`
17
+ * package; do not import this module from a browser bundle.
21
18
  */
22
19
  Object.defineProperty(exports, "__esModule", { value: true });
23
- exports.DEFAULT_WEB_BASE_URL = exports.DEFAULT_CONVEX_SITE_URL = void 0;
20
+ exports.DEFAULT_OAUTH_REDIRECT = exports.DEFAULT_WEB_BASE_URL = exports.DEFAULT_CONVEX_SITE_URL = void 0;
24
21
  exports.configureAuthEndpoints = configureAuthEndpoints;
25
22
  exports.getConvexSiteUrl = getConvexSiteUrl;
26
23
  exports.getWebBaseUrl = getWebBaseUrl;
@@ -33,8 +30,8 @@ exports.getSelectedDeviceId = getSelectedDeviceId;
33
30
  exports.saveSelectedDeviceId = saveSelectedDeviceId;
34
31
  exports.clearSelectedDeviceId = clearSelectedDeviceId;
35
32
  exports.validateToken = validateToken;
36
- exports.startDeviceCode = startDeviceCode;
37
- exports.pollDeviceCode = pollDeviceCode;
33
+ exports.signInWithApple = signInWithApple;
34
+ exports.signInWithOAuth = signInWithOAuth;
38
35
  exports.signupWithEmail = signupWithEmail;
39
36
  exports.loginWithEmail = loginWithEmail;
40
37
  exports.listReachableDevices = listReachableDevices;
@@ -46,6 +43,21 @@ try {
46
43
  catch {
47
44
  // not installed — token persistence disabled, caller must pass authToken
48
45
  }
46
+ let WebBrowser = null;
47
+ try {
48
+ WebBrowser = require('expo-web-browser');
49
+ WebBrowser?.maybeCompleteAuthSession();
50
+ }
51
+ catch {
52
+ // optional
53
+ }
54
+ let AppleAuth = null;
55
+ try {
56
+ AppleAuth = require('expo-apple-authentication');
57
+ }
58
+ catch {
59
+ // optional — Apple sign-in falls back to in-app browser OAuth
60
+ }
49
61
  const TOKEN_KEY = 'yaver_feedback_auth_token';
50
62
  const USER_KEY = 'yaver_feedback_user';
51
63
  const DEVICE_KEY = 'yaver_feedback_selected_device';
@@ -178,54 +190,102 @@ async function validateToken(token) {
178
190
  return null;
179
191
  }
180
192
  }
193
+ // ─── Native Apple Sign-In ─────────────────────────────────────────────
181
194
  /**
182
- * Start a device-code flow. The user opens `verificationUrl`, signs in with
183
- * any OAuth provider on yaver.io, and the SDK polls `pollDeviceCode` until
184
- * a session token is issued.
195
+ * Sign in with Apple using the native ASAuthorization flow. Requires
196
+ * `expo-apple-authentication` installed and the host app's bundle to have
197
+ * the "Sign in with Apple" capability enabled. iOS only.
198
+ *
199
+ * Throws `cancelled` if the user dismisses the sheet.
185
200
  */
186
- async function startDeviceCode(opts) {
187
- const res = await fetch(`${convexSiteUrl}/auth/device-code`, {
201
+ async function signInWithApple() {
202
+ if (!AppleAuth) {
203
+ throw new Error('expo-apple-authentication is not installed. Add it as a peer dep to enable native Apple Sign-In.');
204
+ }
205
+ const available = await AppleAuth.isAvailableAsync();
206
+ if (!available) {
207
+ throw new Error('Apple Sign-In is not available on this device');
208
+ }
209
+ let credential;
210
+ try {
211
+ credential = await AppleAuth.signInAsync({
212
+ requestedScopes: [
213
+ AppleAuth.AppleAuthenticationScope.FULL_NAME,
214
+ AppleAuth.AppleAuthenticationScope.EMAIL,
215
+ ],
216
+ });
217
+ }
218
+ catch (err) {
219
+ if (err?.code === 'ERR_REQUEST_CANCELED') {
220
+ throw new Error('cancelled');
221
+ }
222
+ throw err;
223
+ }
224
+ if (!credential.identityToken) {
225
+ throw new Error('Apple did not return an identity token');
226
+ }
227
+ const fullName = [credential.fullName?.givenName, credential.fullName?.familyName]
228
+ .filter(Boolean)
229
+ .join(' ') || undefined;
230
+ const res = await fetch(`${convexSiteUrl}/auth/apple-native`, {
188
231
  method: 'POST',
189
232
  headers: { 'Content-Type': 'application/json' },
190
- body: JSON.stringify({
191
- machineName: opts?.machineName,
192
- platform: opts?.platform,
193
- preferredProvider: opts?.preferredProvider,
194
- environment: 'feedback-sdk',
195
- }),
233
+ body: JSON.stringify({ identityToken: credential.identityToken, fullName }),
196
234
  });
197
235
  if (!res.ok) {
198
- const data = await res.json().catch(() => ({}));
199
- throw new Error(data.error ?? 'Failed to start device-code');
236
+ const body = await res.text().catch(() => '');
237
+ throw new Error(body || 'Apple sign-in failed');
200
238
  }
201
239
  const data = await res.json();
202
- const params = new URLSearchParams({ code: data.userCode });
203
- if (opts?.preferredProvider) {
204
- params.set('preferredProvider', opts.preferredProvider);
205
- }
206
- return {
207
- userCode: data.userCode,
208
- deviceCode: data.deviceCode,
209
- expiresAt: data.expiresAt,
210
- verificationUrl: `${webBaseUrl}/auth/device?${params.toString()}`,
211
- };
240
+ return { token: data.token, userId: data.userId };
212
241
  }
213
- async function pollDeviceCode(deviceCode) {
242
+ // ─── In-app browser OAuth (Google / GitHub / GitLab / Microsoft) ──────
243
+ /**
244
+ * Default OAuth redirect — the same callback the Yaver mobile app uses
245
+ * (`yaver://oauth-callback`). `WebBrowser.openAuthSessionAsync` intercepts
246
+ * this redirect inside the auth session, so the host app does not need to
247
+ * register the scheme on iOS. On Android, add an `<intent-filter>` for
248
+ * `yaver://oauth-callback` in the host app's AndroidManifest.xml.
249
+ */
250
+ exports.DEFAULT_OAUTH_REDIRECT = 'yaver://oauth-callback';
251
+ /**
252
+ * Sign in through the in-app browser via yaver.io. Opens
253
+ * `https://yaver.io/api/auth/oauth/<provider>?client=mobile`, the user picks
254
+ * an OAuth provider, and the web callback redirects back to
255
+ * `yaver://oauth-callback?token=...` which `openAuthSessionAsync` captures
256
+ * inside the auth session. No deep-link wiring required on iOS.
257
+ *
258
+ * Throws `cancelled` if the user dismisses the browser.
259
+ */
260
+ async function signInWithOAuth(provider, opts) {
261
+ if (!WebBrowser) {
262
+ throw new Error('expo-web-browser is not installed. Add it as a peer dep to enable OAuth sign-in.');
263
+ }
264
+ const redirectUrl = opts?.redirectUrl ?? exports.DEFAULT_OAUTH_REDIRECT;
265
+ const params = new URLSearchParams({ client: 'mobile' });
266
+ const authUrl = `${webBaseUrl}/api/auth/oauth/${provider}?${params.toString()}`;
267
+ const result = await WebBrowser.openAuthSessionAsync(authUrl, redirectUrl, {
268
+ preferEphemeralSession: opts?.preferEphemeralSession ?? false,
269
+ showInRecents: true,
270
+ });
271
+ if (result.type !== 'success' || !result.url) {
272
+ throw new Error('cancelled');
273
+ }
274
+ let token = null;
214
275
  try {
215
- const res = await fetch(`${convexSiteUrl}/auth/device-code/poll?device_code=${encodeURIComponent(deviceCode)}`);
216
- if (!res.ok)
217
- return { status: 'expired' };
218
- const data = await res.json();
219
- if (data.status === 'authorized' && typeof data.token === 'string') {
220
- return { status: 'authorized', token: data.token };
221
- }
222
- if (data.status === 'pending')
223
- return { status: 'pending' };
224
- return { status: 'expired' };
276
+ const parsed = new URL(result.url);
277
+ token = parsed.searchParams.get('token');
225
278
  }
226
279
  catch {
227
- return { status: 'pending' }; // network blip let caller keep polling
280
+ // best-effort fallback for odd schemes
281
+ const match = result.url.match(/[?&]token=([^&]+)/);
282
+ if (match)
283
+ token = decodeURIComponent(match[1]);
284
+ }
285
+ if (!token) {
286
+ throw new Error('OAuth callback did not include a token');
228
287
  }
288
+ return { token };
229
289
  }
230
290
  // ─── Email / password (no 2FA) ────────────────────────────────────────
231
291
  async function signupWithEmail(fullName, email, password) {
@@ -252,9 +312,9 @@ async function loginWithEmail(email, password) {
252
312
  }
253
313
  const data = await res.json();
254
314
  if (data?.requires2fa) {
255
- // SDK login surface does not handle 2FA — direct the user to complete
256
- // sign-in through the web flow (device-code) which supports it.
257
- throw new Error('2FA is enabled on this account. Sign in via the device-code flow instead.');
315
+ // SDK login surface does not handle 2FA — direct the user to sign in
316
+ // through one of the OAuth providers, which complete 2FA on the web.
317
+ throw new Error('2FA is enabled on this account. Sign in with Apple/Google/GitHub/GitLab/Microsoft instead.');
258
318
  }
259
319
  return { token: data.token, userId: data.userId };
260
320
  }
package/dist/index.d.ts CHANGED
@@ -38,8 +38,8 @@ export { ShakeDetector } from './ShakeDetector';
38
38
  export { FloatingButton } from './FloatingButton';
39
39
  export { FeedbackModal } from './FeedbackModal';
40
40
  export { FixReport } from './FixReport';
41
- export { configureAuthEndpoints, getConvexSiteUrl, getWebBaseUrl, getToken, saveToken, clearToken, getUser, saveUser, getSelectedDeviceId, saveSelectedDeviceId, clearSelectedDeviceId, validateToken, startDeviceCode, pollDeviceCode, signupWithEmail, loginWithEmail, listReachableDevices, DEFAULT_CONVEX_SITE_URL, DEFAULT_WEB_BASE_URL, } from './auth';
42
- export type { OAuthProvider, User, DeviceCodeStart, DeviceCodePoll, RemoteDevice, DeviceList, } from './auth';
41
+ export { configureAuthEndpoints, getConvexSiteUrl, getWebBaseUrl, getToken, saveToken, clearToken, getUser, saveUser, getSelectedDeviceId, saveSelectedDeviceId, clearSelectedDeviceId, validateToken, signInWithApple, signInWithOAuth, signupWithEmail, loginWithEmail, listReachableDevices, DEFAULT_CONVEX_SITE_URL, DEFAULT_WEB_BASE_URL, DEFAULT_OAUTH_REDIRECT, } from './auth';
42
+ export type { OAuthProvider, User, RemoteDevice, DeviceList, } from './auth';
43
43
  export { captureScreenshot, startAudioRecording, stopAudioRecording } from './capture';
44
44
  export { uploadFeedback } from './upload';
45
45
  export type { FeedbackConfig, FeedbackBundle, FeedbackMetadata, DeviceInfo, AppInfo, TimelineEvent, FeedbackReport, AgentCommentary, FeedbackStreamEvent, VoiceCapability, CapturedError, TestFix, TestSession, } from './types';
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@
23
23
  * ```
24
24
  */
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.uploadFeedback = exports.stopAudioRecording = exports.startAudioRecording = exports.captureScreenshot = exports.DEFAULT_WEB_BASE_URL = exports.DEFAULT_CONVEX_SITE_URL = exports.listReachableDevices = exports.loginWithEmail = exports.signupWithEmail = exports.pollDeviceCode = exports.startDeviceCode = exports.validateToken = exports.clearSelectedDeviceId = exports.saveSelectedDeviceId = exports.getSelectedDeviceId = exports.saveUser = exports.getUser = exports.clearToken = exports.saveToken = exports.getToken = exports.getWebBaseUrl = exports.getConvexSiteUrl = exports.configureAuthEndpoints = exports.FixReport = exports.FeedbackModal = exports.FloatingButton = exports.ShakeDetector = exports.AuthOverlay = exports.YaverMachinePickerScreen = exports.YaverLoginScreen = exports.YaverConnectionScreen = exports.P2PClient = exports.YaverDiscovery = exports.initExpo = exports.YaverUpdates = exports.BlackBox = exports.YaverFeedback = void 0;
26
+ exports.uploadFeedback = exports.stopAudioRecording = exports.startAudioRecording = exports.captureScreenshot = exports.DEFAULT_OAUTH_REDIRECT = exports.DEFAULT_WEB_BASE_URL = exports.DEFAULT_CONVEX_SITE_URL = exports.listReachableDevices = exports.loginWithEmail = exports.signupWithEmail = exports.signInWithOAuth = exports.signInWithApple = exports.validateToken = exports.clearSelectedDeviceId = exports.saveSelectedDeviceId = exports.getSelectedDeviceId = exports.saveUser = exports.getUser = exports.clearToken = exports.saveToken = exports.getToken = exports.getWebBaseUrl = exports.getConvexSiteUrl = exports.configureAuthEndpoints = exports.FixReport = exports.FeedbackModal = exports.FloatingButton = exports.ShakeDetector = exports.AuthOverlay = exports.YaverMachinePickerScreen = exports.YaverLoginScreen = exports.YaverConnectionScreen = exports.P2PClient = exports.YaverDiscovery = exports.initExpo = exports.YaverUpdates = exports.BlackBox = exports.YaverFeedback = void 0;
27
27
  var YaverFeedback_1 = require("./YaverFeedback");
28
28
  Object.defineProperty(exports, "YaverFeedback", { enumerable: true, get: function () { return YaverFeedback_1.YaverFeedback; } });
29
29
  var BlackBox_1 = require("./BlackBox");
@@ -65,13 +65,14 @@ Object.defineProperty(exports, "getSelectedDeviceId", { enumerable: true, get: f
65
65
  Object.defineProperty(exports, "saveSelectedDeviceId", { enumerable: true, get: function () { return auth_1.saveSelectedDeviceId; } });
66
66
  Object.defineProperty(exports, "clearSelectedDeviceId", { enumerable: true, get: function () { return auth_1.clearSelectedDeviceId; } });
67
67
  Object.defineProperty(exports, "validateToken", { enumerable: true, get: function () { return auth_1.validateToken; } });
68
- Object.defineProperty(exports, "startDeviceCode", { enumerable: true, get: function () { return auth_1.startDeviceCode; } });
69
- Object.defineProperty(exports, "pollDeviceCode", { enumerable: true, get: function () { return auth_1.pollDeviceCode; } });
68
+ Object.defineProperty(exports, "signInWithApple", { enumerable: true, get: function () { return auth_1.signInWithApple; } });
69
+ Object.defineProperty(exports, "signInWithOAuth", { enumerable: true, get: function () { return auth_1.signInWithOAuth; } });
70
70
  Object.defineProperty(exports, "signupWithEmail", { enumerable: true, get: function () { return auth_1.signupWithEmail; } });
71
71
  Object.defineProperty(exports, "loginWithEmail", { enumerable: true, get: function () { return auth_1.loginWithEmail; } });
72
72
  Object.defineProperty(exports, "listReachableDevices", { enumerable: true, get: function () { return auth_1.listReachableDevices; } });
73
73
  Object.defineProperty(exports, "DEFAULT_CONVEX_SITE_URL", { enumerable: true, get: function () { return auth_1.DEFAULT_CONVEX_SITE_URL; } });
74
74
  Object.defineProperty(exports, "DEFAULT_WEB_BASE_URL", { enumerable: true, get: function () { return auth_1.DEFAULT_WEB_BASE_URL; } });
75
+ Object.defineProperty(exports, "DEFAULT_OAUTH_REDIRECT", { enumerable: true, get: function () { return auth_1.DEFAULT_OAUTH_REDIRECT; } });
75
76
  var capture_1 = require("./capture");
76
77
  Object.defineProperty(exports, "captureScreenshot", { enumerable: true, get: function () { return capture_1.captureScreenshot; } });
77
78
  Object.defineProperty(exports, "startAudioRecording", { enumerable: true, get: function () { return capture_1.startAudioRecording; } });
package/dist/types.d.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
- * (device-code / email / OAuth) the first time the user triggers feedback.
7
+ * (Apple native / OAuth in-app browser / email) the first time the user
8
+ * triggers feedback.
8
9
  */
9
10
  authToken?: string;
10
11
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yaver-feedback-react-native",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
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",
@@ -20,7 +20,9 @@
20
20
  "peerDependencies": {
21
21
  "react": ">=18.0.0",
22
22
  "react-native": ">=0.70.0",
23
- "@react-native-async-storage/async-storage": ">=1.17.0"
23
+ "@react-native-async-storage/async-storage": ">=1.17.0",
24
+ "expo-web-browser": ">=12.0.0",
25
+ "expo-apple-authentication": ">=6.0.0"
24
26
  },
25
27
  "peerDependenciesMeta": {
26
28
  "@expo/config-plugins": {
@@ -28,6 +30,9 @@
28
30
  },
29
31
  "expo-constants": {
30
32
  "optional": true
33
+ },
34
+ "expo-apple-authentication": {
35
+ "optional": true
31
36
  }
32
37
  },
33
38
  "devDependencies": {