@oxyhq/services 10.4.0 → 10.5.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.
Files changed (37) hide show
  1. package/lib/commonjs/ui/components/AnotherDeviceQR.js +121 -0
  2. package/lib/commonjs/ui/components/AnotherDeviceQR.js.map +1 -0
  3. package/lib/commonjs/ui/components/SignInModal.js +145 -356
  4. package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
  5. package/lib/commonjs/ui/hooks/useOxyAuthSession.js +458 -0
  6. package/lib/commonjs/ui/hooks/useOxyAuthSession.js.map +1 -0
  7. package/lib/commonjs/ui/screens/OxyAuthScreen.js +62 -460
  8. package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
  9. package/lib/module/ui/components/AnotherDeviceQR.js +116 -0
  10. package/lib/module/ui/components/AnotherDeviceQR.js.map +1 -0
  11. package/lib/module/ui/components/SignInModal.js +149 -358
  12. package/lib/module/ui/components/SignInModal.js.map +1 -1
  13. package/lib/module/ui/hooks/useOxyAuthSession.js +452 -0
  14. package/lib/module/ui/hooks/useOxyAuthSession.js.map +1 -0
  15. package/lib/module/ui/screens/OxyAuthScreen.js +62 -460
  16. package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
  17. package/lib/typescript/commonjs/ui/components/AnotherDeviceQR.d.ts +24 -0
  18. package/lib/typescript/commonjs/ui/components/AnotherDeviceQR.d.ts.map +1 -0
  19. package/lib/typescript/commonjs/ui/components/SignInModal.d.ts +10 -5
  20. package/lib/typescript/commonjs/ui/components/SignInModal.d.ts.map +1 -1
  21. package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts +83 -0
  22. package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts.map +1 -0
  23. package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts +12 -8
  24. package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts.map +1 -1
  25. package/lib/typescript/module/ui/components/AnotherDeviceQR.d.ts +24 -0
  26. package/lib/typescript/module/ui/components/AnotherDeviceQR.d.ts.map +1 -0
  27. package/lib/typescript/module/ui/components/SignInModal.d.ts +10 -5
  28. package/lib/typescript/module/ui/components/SignInModal.d.ts.map +1 -1
  29. package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts +83 -0
  30. package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts.map +1 -0
  31. package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts +12 -8
  32. package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts.map +1 -1
  33. package/package.json +1 -1
  34. package/src/ui/components/AnotherDeviceQR.tsx +111 -0
  35. package/src/ui/components/SignInModal.tsx +142 -381
  36. package/src/ui/hooks/useOxyAuthSession.ts +556 -0
  37. package/src/ui/screens/OxyAuthScreen.tsx +61 -506
@@ -1,41 +1,35 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * SignInModal - Full screen sign-in modal with QR code
4
+ * SignInModal - Web-first centered sign-in modal (Continue with Oxy)
5
5
  *
6
- * A semi-transparent full-screen modal that displays:
7
- * - QR code for scanning with Oxy Accounts app
8
- * - Button to open the Oxy Auth approval page
6
+ * A semi-transparent full-screen modal whose primary action is the one-tap
7
+ * "Continue with Oxy" approval flow. The QR code is demoted to a collapsed
8
+ * "Sign in on another device" disclosure (you can't scan your own screen).
9
9
  *
10
- * Animates with fade-in effect.
10
+ * ALL of the auth-session machinery (session-token creation, QR data, socket +
11
+ * polling, waiting/error/retry state, the open-auth handler, deep-link return,
12
+ * and cleanup) lives in the shared `useOxyAuthSession` hook, which the native
13
+ * `OxyAuthScreen` also consumes — neither container re-implements the transport.
14
+ *
15
+ * Animates with a fade + scale effect.
11
16
  */
12
17
 
13
- import { useState, useEffect, useCallback, useRef } from 'react';
14
- import { View, Text, TouchableOpacity, StyleSheet, Modal, Dimensions, Platform, Linking } from 'react-native';
18
+ import { useState, useEffect, useCallback } from 'react';
19
+ import { View, Text, TouchableOpacity, StyleSheet, Modal } from 'react-native';
15
20
  import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated';
16
21
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
17
- import io from 'socket.io-client';
18
- import QRCode from 'react-native-qrcode-svg';
19
22
  import { useTheme } from '@oxyhq/bloom/theme';
20
23
  import { Button } from '@oxyhq/bloom/button';
21
24
  import { Loading } from '@oxyhq/bloom/loading';
25
+ import { Linking } from 'react-native';
22
26
  import { useOxy } from "../context/OxyContext.js";
23
27
  import OxyLogo from "./OxyLogo.js";
24
- import { createDebugLogger } from '@oxyhq/core';
25
- import { completeDeviceFlowSignIn } from "../../utils/deviceFlowSignIn.js";
26
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
27
- const debug = createDebugLogger('SignInModal');
28
- const {
29
- width: SCREEN_WIDTH,
30
- height: SCREEN_HEIGHT
31
- } = Dimensions.get('window');
32
-
33
- // Auth session expiration (5 minutes)
34
- const AUTH_SESSION_EXPIRY_MS = 5 * 60 * 1000;
28
+ import AnotherDeviceQR from "./AnotherDeviceQR.js";
29
+ import { useOxyAuthSession, OXY_ACCOUNTS_WEB_URL } from "../hooks/useOxyAuthSession.js";
35
30
 
36
- // Polling interval (fallback if socket fails)
37
- const POLLING_INTERVAL_MS = 3000;
38
31
  // Store for modal visibility with subscription support
32
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
39
33
  let modalVisible = false;
40
34
  let setModalVisibleCallback = null;
41
35
  const visibilityListeners = new Set();
@@ -58,10 +52,6 @@ export const subscribeToSignInModal = listener => {
58
52
  };
59
53
  const SignInModal = () => {
60
54
  const [visible, setVisible] = useState(false);
61
- const [authSession, setAuthSession] = useState(null);
62
- const [isLoading, setIsLoading] = useState(false);
63
- const [error, setError] = useState(null);
64
- const [isWaiting, setIsWaiting] = useState(false);
65
55
  const insets = useSafeAreaInsets();
66
56
  const theme = useTheme();
67
57
  const {
@@ -69,42 +59,59 @@ const SignInModal = () => {
69
59
  switchSession,
70
60
  clientId
71
61
  } = useOxy();
72
- const socketRef = useRef(null);
73
- const pollingIntervalRef = useRef(null);
74
- const isProcessingRef = useRef(false);
75
-
76
- // Animation values
77
- const opacity = useSharedValue(0);
78
- const scale = useSharedValue(0.9);
79
62
 
80
- // Register callback
63
+ // Register the imperative visibility callback.
81
64
  useEffect(() => {
82
65
  setModalVisibleCallback = setVisible;
83
66
  return () => {
84
67
  setModalVisibleCallback = null;
85
68
  };
86
69
  }, []);
70
+ if (!visible) return null;
71
+
72
+ // The hook owns the whole auth-session lifecycle. Mounting it only while the
73
+ // modal is visible matches the previous behavior (session created on open,
74
+ // cleaned up on close) — the inner component is unmounted when `visible`
75
+ // flips false, which runs the hook's unmount cleanup.
76
+ return /*#__PURE__*/_jsx(SignInModalContent, {
77
+ theme: theme,
78
+ insets: insets,
79
+ oxyServices: oxyServices,
80
+ switchSession: switchSession,
81
+ clientId: clientId
82
+ });
83
+ };
84
+ const SignInModalContent = ({
85
+ theme,
86
+ insets,
87
+ oxyServices,
88
+ switchSession,
89
+ clientId
90
+ }) => {
91
+ const {
92
+ qrData,
93
+ isLoading,
94
+ error,
95
+ isWaiting,
96
+ openAuthApproval,
97
+ retry
98
+ } = useOxyAuthSession(oxyServices, clientId, switchSession, {
99
+ onSignedIn: hideSignInModal
100
+ });
87
101
 
88
- // Animate in/out
89
- // biome-ignore lint/correctness/useExhaustiveDependencies: opacity and scale are Reanimated SharedValues (stable refs) that should not be listed as dependencies
102
+ // Entrance animation.
103
+ const opacity = useSharedValue(0);
104
+ const scale = useSharedValue(0.96);
105
+
106
+ // biome-ignore lint/correctness/useExhaustiveDependencies: opacity/scale are Reanimated SharedValues (stable refs) and must not be listed as deps; this runs once on mount to play the entrance.
90
107
  useEffect(() => {
91
- if (visible) {
92
- opacity.value = withTiming(1, {
93
- duration: 250
94
- });
95
- scale.value = withTiming(1, {
96
- duration: 250
97
- });
98
- generateAuthSession();
99
- } else {
100
- opacity.value = withTiming(0, {
101
- duration: 200
102
- });
103
- scale.value = withTiming(0.9, {
104
- duration: 200
105
- });
106
- }
107
- }, [visible]);
108
+ opacity.value = withTiming(1, {
109
+ duration: 250
110
+ });
111
+ scale.value = withTiming(1, {
112
+ duration: 250
113
+ });
114
+ }, []);
108
115
  const backdropStyle = useAnimatedStyle(() => ({
109
116
  opacity: opacity.value
110
117
  }));
@@ -114,245 +121,14 @@ const SignInModal = () => {
114
121
  scale: scale.value
115
122
  }]
116
123
  }));
117
-
118
- // Handle successful authorization.
119
- //
120
- // The auth-session socket (or fallback poll) hands us the new
121
- // `sessionId`. Before any session-management code can touch it we
122
- // MUST first exchange the secret `sessionToken` (held only by this
123
- // client) for the first access token via `claimSessionByToken` —
124
- // this is the device-flow equivalent of OAuth's code-for-token
125
- // exchange (RFC 8628 §3.4).
126
- //
127
- // Without that exchange the SDK has no bearer token and the app never
128
- // becomes authenticated even though the session is authorized server-side.
129
- // Once `claimSessionByToken` plants the tokens in the HttpService, the rest
130
- // of the session wiring (state, persistence, language preference) flows
131
- // through the normal `switchSession` path.
132
- const handleAuthSuccess = useCallback(async (sessionId, sessionToken) => {
133
- if (isProcessingRef.current) return;
134
- isProcessingRef.current = true;
135
- try {
136
- // Claim the first access token with the secret sessionToken, then
137
- // hydrate the session. Shared with the native `OxyAuthScreen` via
138
- // `completeDeviceFlowSignIn` so the two paths cannot drift.
139
- if (!switchSession) {
140
- throw new Error('Session management unavailable');
141
- }
142
- await completeDeviceFlowSignIn({
143
- oxyServices,
144
- sessionId,
145
- sessionToken,
146
- switchSession
147
- });
148
- hideSignInModal();
149
- } catch (err) {
150
- debug.error('Error completing auth:', err);
151
- setError('Authorization successful but failed to complete sign in. Please try again.');
152
- isProcessingRef.current = false;
153
- }
154
- }, [oxyServices, switchSession]);
155
-
156
- // Cleanup socket and polling
157
- const cleanup = useCallback(() => {
158
- setIsWaiting(false);
159
- if (socketRef.current) {
160
- socketRef.current.disconnect();
161
- socketRef.current = null;
162
- }
163
- if (pollingIntervalRef.current) {
164
- clearInterval(pollingIntervalRef.current);
165
- pollingIntervalRef.current = null;
166
- }
167
- }, []);
168
-
169
- // Connect to socket for real-time updates
170
- const connectSocket = useCallback(sessionToken => {
171
- const baseURL = oxyServices.getBaseURL();
172
- const socket = io(`${baseURL}/auth-session`, {
173
- transports: ['websocket', 'polling'],
174
- reconnection: true,
175
- reconnectionAttempts: 3,
176
- reconnectionDelay: 1000
177
- });
178
- socketRef.current = socket;
179
- socket.on('connect', () => {
180
- debug.log('Auth socket connected');
181
- socket.emit('join', sessionToken);
182
- });
183
- socket.on('auth_update', payload => {
184
- debug.log('Auth update received:', payload);
185
- if (payload.status === 'authorized' && payload.sessionId) {
186
- cleanup();
187
- // `sessionToken` was generated by this client and is in scope
188
- // via the `sessionToken` arg — pass it through so the claim
189
- // call can exchange it for the first access token.
190
- handleAuthSuccess(payload.sessionId, sessionToken);
191
- } else if (payload.status === 'cancelled') {
192
- cleanup();
193
- setError('Authorization was denied.');
194
- } else if (payload.status === 'expired') {
195
- cleanup();
196
- setError('Session expired. Please try again.');
197
- }
198
- });
199
- socket.on('connect_error', err => {
200
- debug.log('Socket connection error, falling back to polling:', err instanceof Error ? err.message : null);
201
- socket.disconnect();
202
- startPolling(sessionToken);
203
- });
204
- }, [oxyServices, handleAuthSuccess, cleanup]);
205
-
206
- // Start polling for authorization.
207
- //
208
- // Idempotent: if a poll interval is already running this is a no-op, so the
209
- // `connect_error` path (which also calls this) cannot stack a second
210
- // interval on top of the always-on poll started in `generateAuthSession`.
211
- const startPolling = useCallback(sessionToken => {
212
- if (pollingIntervalRef.current) return;
213
- pollingIntervalRef.current = setInterval(async () => {
214
- if (isProcessingRef.current) return;
215
- try {
216
- const response = await oxyServices.makeRequest('GET', `/auth/session/status/${sessionToken}`, undefined, {
217
- cache: false
218
- });
219
- if (response.authorized && response.sessionId) {
220
- cleanup();
221
- // Pass the original sessionToken through; the claim
222
- // exchange needs it to mint the first access token.
223
- handleAuthSuccess(response.sessionId, sessionToken);
224
- } else if (response.status === 'cancelled') {
225
- cleanup();
226
- setError('Authorization was denied.');
227
- } else if (response.status === 'expired') {
228
- cleanup();
229
- setError('Session expired. Please try again.');
230
- }
231
- } catch (err) {
232
- debug.log('Auth polling error:', err);
233
- }
234
- }, POLLING_INTERVAL_MS);
235
- }, [oxyServices, handleAuthSuccess, cleanup]);
236
-
237
- // Generate a new auth session
238
- const generateAuthSession = useCallback(async () => {
239
- setIsLoading(true);
240
- setError(null);
241
- isProcessingRef.current = false;
242
-
243
- // The cross-app device sign-in flow identifies the requesting app by its
244
- // real registered OAuth client id (ApplicationCredential publicKey).
245
- // Without it the API cannot resolve the consent identity, so we fail
246
- // fast with a clear configuration error rather than creating a session
247
- // the server would reject.
248
- if (!clientId) {
249
- setError('This app is not configured for sign-in (missing clientId).');
250
- setIsLoading(false);
251
- return;
252
- }
253
- try {
254
- const sessionToken = generateSessionToken();
255
- const expiresAt = Date.now() + AUTH_SESSION_EXPIRY_MS;
256
- await oxyServices.makeRequest('POST', '/auth/session/create', {
257
- sessionToken,
258
- expiresAt,
259
- clientId
260
- }, {
261
- cache: false
262
- });
263
- setAuthSession({
264
- sessionToken,
265
- expiresAt
266
- });
267
- setIsWaiting(true);
268
- // Socket is the fast path; the poll is a transport-independent
269
- // backstop that guarantees completion even if the socket connects
270
- // but silently never delivers auth_update (RN transport /
271
- // idle-timeout).
272
- connectSocket(sessionToken);
273
- startPolling(sessionToken);
274
- } catch (err) {
275
- setError((err instanceof Error ? err.message : null) || 'Failed to create auth session');
276
- } finally {
277
- setIsLoading(false);
278
- }
279
- }, [oxyServices, connectSocket, startPolling, clientId]);
280
-
281
- // Generate a cryptographically random session token.
282
- // 16 random bytes -> 32 hex chars (128 bits of entropy) — unguessable.
283
- // crypto.getRandomValues is guaranteed available because importing
284
- // @oxyhq/core installs a polyfill via expo-crypto on React Native.
285
- const generateSessionToken = () => {
286
- const bytes = new Uint8Array(16);
287
- crypto.getRandomValues(bytes);
288
- return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
289
- };
290
-
291
- // Build the QR code data
292
- const getQRData = () => {
293
- if (!authSession) return '';
294
- return `oxyauth://${authSession.sessionToken}`;
295
- };
296
-
297
- // Open Oxy Auth approval page for this device-flow session.
298
- const handleOpenAuthApproval = useCallback(async () => {
299
- if (!authSession) return;
300
- const baseURL = oxyServices.getBaseURL();
301
- // Resolve auth web URL
302
- let authWebUrl = oxyServices.config?.authWebUrl;
303
- if (!authWebUrl) {
304
- try {
305
- const url = new URL(baseURL);
306
- if (url.port === '3001') {
307
- url.port = '3002';
308
- authWebUrl = url.origin;
309
- } else if (url.hostname.startsWith('api.')) {
310
- url.hostname = `auth.${url.hostname.slice(4)}`;
311
- authWebUrl = url.origin;
312
- }
313
- } catch {
314
- authWebUrl = 'https://auth.oxy.so';
315
- }
316
- }
317
- authWebUrl = authWebUrl || 'https://auth.oxy.so';
318
- const webUrl = new URL('/authorize', authWebUrl);
319
- webUrl.searchParams.set('token', authSession.sessionToken);
320
- if (Platform.OS === 'web') {
321
- // Open a separate approval window on web for the device-flow token.
322
- const width = 500;
323
- const height = 650;
324
- const screenWidth = window.screen?.width ?? width;
325
- const screenHeight = window.screen?.height ?? height;
326
- const left = (screenWidth - width) / 2;
327
- const top = (screenHeight - height) / 2;
328
- window.open(webUrl.toString(), 'oxy-auth-approval', `width=${width},height=${height},left=${left},top=${top}`);
329
- } else {
330
- // Open in browser on native
331
- Linking.openURL(webUrl.toString());
332
- }
333
- }, [authSession, oxyServices]);
334
-
335
- // Refresh session
336
- const handleRefresh = useCallback(() => {
337
- cleanup();
338
- generateAuthSession();
339
- }, [generateAuthSession, cleanup]);
340
-
341
- // Handle close
342
124
  const handleClose = useCallback(() => {
343
- cleanup();
344
125
  hideSignInModal();
345
- }, [cleanup]);
346
-
347
- // Clean up on unmount
348
- useEffect(() => {
349
- return () => {
350
- cleanup();
351
- };
352
- }, [cleanup]);
353
- if (!visible) return null;
126
+ }, []);
127
+ const handleCreateAccount = useCallback(() => {
128
+ Linking.openURL(OXY_ACCOUNTS_WEB_URL);
129
+ }, []);
354
130
  return /*#__PURE__*/_jsx(Modal, {
355
- visible: visible,
131
+ visible: true,
356
132
  transparent: true,
357
133
  animationType: "none",
358
134
  statusBarTranslucent: true,
@@ -366,15 +142,22 @@ const SignInModal = () => {
366
142
  onPress: handleClose,
367
143
  activeOpacity: 1
368
144
  }), /*#__PURE__*/_jsxs(Animated.View, {
369
- style: [styles.content, contentStyle, {
370
- paddingTop: insets.top + 20,
371
- paddingBottom: insets.bottom + 20
372
- }],
145
+ style: [styles.card, {
146
+ backgroundColor: theme.colors.card,
147
+ paddingTop: insets.top + 24,
148
+ paddingBottom: insets.bottom + 24
149
+ }, contentStyle],
373
150
  children: [/*#__PURE__*/_jsx(TouchableOpacity, {
374
- style: styles.closeButton,
151
+ style: [styles.closeButton, {
152
+ backgroundColor: theme.colors.backgroundSecondary
153
+ }],
375
154
  onPress: handleClose,
155
+ accessibilityRole: "button",
156
+ accessibilityLabel: "Close sign in",
376
157
  children: /*#__PURE__*/_jsx(Text, {
377
- style: styles.closeButtonText,
158
+ style: [styles.closeButtonText, {
159
+ color: theme.colors.textSecondary
160
+ }],
378
161
  children: "\xD7"
379
162
  })
380
163
  }), /*#__PURE__*/_jsxs(View, {
@@ -385,11 +168,11 @@ const SignInModal = () => {
385
168
  }), /*#__PURE__*/_jsx(Text, {
386
169
  className: "text-foreground",
387
170
  style: styles.title,
388
- children: "Sign in with Oxy"
171
+ children: "Sign in to Oxy"
389
172
  }), /*#__PURE__*/_jsx(Text, {
390
173
  className: "text-muted-foreground",
391
174
  style: styles.subtitle,
392
- children: "Scan with Oxy Accounts app or use the button below"
175
+ children: "Continue with your Oxy identity to sign in securely"
393
176
  })]
394
177
  }), isLoading ? /*#__PURE__*/_jsxs(View, {
395
178
  style: styles.loadingContainer,
@@ -407,54 +190,54 @@ const SignInModal = () => {
407
190
  style: styles.errorText,
408
191
  children: error
409
192
  }), /*#__PURE__*/_jsx(Button, {
410
- onPress: handleRefresh,
193
+ variant: "primary",
194
+ onPress: retry,
195
+ style: styles.primaryButton,
411
196
  children: "Try Again"
412
197
  })]
413
198
  }) : /*#__PURE__*/_jsxs(_Fragment, {
414
- children: [/*#__PURE__*/_jsx(View, {
415
- style: [styles.qrContainer, {
416
- backgroundColor: 'white'
417
- }],
418
- children: authSession ? /*#__PURE__*/_jsx(QRCode, {
419
- value: getQRData(),
420
- size: 200,
421
- backgroundColor: "white",
422
- color: "black"
423
- }) : /*#__PURE__*/_jsx(Loading, {
424
- size: "large"
425
- })
426
- }), /*#__PURE__*/_jsxs(View, {
427
- style: styles.dividerContainer,
428
- children: [/*#__PURE__*/_jsx(View, {
429
- style: [styles.divider, {
430
- backgroundColor: 'rgba(255,255,255,0.3)'
431
- }]
432
- }), /*#__PURE__*/_jsx(Text, {
433
- style: [styles.dividerText, {
434
- color: 'rgba(255,255,255,0.7)'
435
- }],
436
- children: "or"
437
- }), /*#__PURE__*/_jsx(View, {
438
- style: [styles.divider, {
439
- backgroundColor: 'rgba(255,255,255,0.3)'
440
- }]
441
- })]
442
- }), /*#__PURE__*/_jsx(Button, {
443
- onPress: handleOpenAuthApproval,
199
+ children: [/*#__PURE__*/_jsx(Button, {
200
+ variant: "primary",
201
+ onPress: openAuthApproval,
444
202
  icon: /*#__PURE__*/_jsx(OxyLogo, {
445
203
  variant: "icon",
446
204
  size: 20,
447
- fillColor: theme.colors.card
205
+ fillColor: theme.colors.primaryForeground,
206
+ style: styles.buttonIcon
448
207
  }),
449
- children: "Open Oxy Auth"
208
+ style: styles.primaryButton,
209
+ children: "Continue with Oxy"
450
210
  }), isWaiting && /*#__PURE__*/_jsxs(View, {
451
211
  style: styles.statusContainer,
452
212
  children: [/*#__PURE__*/_jsx(Loading, {
453
- size: "small"
213
+ size: "small",
214
+ style: styles.statusSpinner
454
215
  }), /*#__PURE__*/_jsx(Text, {
455
- style: styles.statusText,
216
+ style: [styles.statusText, {
217
+ color: theme.colors.textSecondary
218
+ }],
456
219
  children: "Waiting for authorization..."
457
220
  })]
221
+ }), /*#__PURE__*/_jsx(View, {
222
+ style: styles.qrSection,
223
+ children: /*#__PURE__*/_jsx(AnotherDeviceQR, {
224
+ qrData: qrData
225
+ })
226
+ })]
227
+ }), /*#__PURE__*/_jsxs(View, {
228
+ style: styles.footer,
229
+ children: [/*#__PURE__*/_jsxs(Text, {
230
+ style: styles.footerText,
231
+ className: "text-muted-foreground",
232
+ children: ["Don't have an Oxy account?", ' ']
233
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
234
+ onPress: handleCreateAccount,
235
+ accessibilityRole: "link",
236
+ children: /*#__PURE__*/_jsx(Text, {
237
+ style: styles.footerLink,
238
+ className: "text-primary",
239
+ children: "Create one"
240
+ })
458
241
  })]
459
242
  })]
460
243
  })]
@@ -467,11 +250,12 @@ const styles = StyleSheet.create({
467
250
  justifyContent: 'center',
468
251
  alignItems: 'center'
469
252
  },
470
- content: {
253
+ card: {
471
254
  width: '100%',
472
255
  maxWidth: 400,
473
256
  alignItems: 'center',
474
- paddingHorizontal: 24
257
+ paddingHorizontal: 24,
258
+ borderRadius: 24
475
259
  },
476
260
  closeButton: {
477
261
  position: 'absolute',
@@ -480,58 +264,52 @@ const styles = StyleSheet.create({
480
264
  width: 40,
481
265
  height: 40,
482
266
  borderRadius: 20,
483
- backgroundColor: 'rgba(255, 255, 255, 0.1)',
484
267
  justifyContent: 'center',
485
268
  alignItems: 'center',
486
269
  zIndex: 10
487
270
  },
488
271
  closeButtonText: {
489
- color: 'white',
490
272
  fontSize: 28,
491
273
  fontWeight: '300',
492
274
  lineHeight: 32
493
275
  },
494
276
  header: {
495
277
  alignItems: 'center',
496
- marginBottom: 32
278
+ marginBottom: 28
497
279
  },
498
280
  title: {
499
- fontSize: 28,
281
+ fontSize: 26,
500
282
  fontWeight: 'bold',
501
- marginTop: 16
283
+ marginTop: 16,
284
+ textAlign: 'center'
502
285
  },
503
286
  subtitle: {
504
287
  fontSize: 15,
505
288
  marginTop: 8,
506
289
  textAlign: 'center'
507
290
  },
508
- qrContainer: {
509
- padding: 20,
510
- borderRadius: 16
511
- },
512
- dividerContainer: {
513
- flexDirection: 'row',
514
- alignItems: 'center',
515
- marginVertical: 24,
516
- width: '100%'
517
- },
518
- divider: {
519
- flex: 1,
520
- height: 1
291
+ primaryButton: {
292
+ width: '100%',
293
+ borderRadius: 12
521
294
  },
522
- dividerText: {
523
- marginHorizontal: 16,
524
- fontSize: 14
295
+ buttonIcon: {
296
+ marginRight: 10
525
297
  },
526
298
  statusContainer: {
527
299
  flexDirection: 'row',
528
300
  alignItems: 'center',
529
- marginTop: 24
301
+ marginTop: 20
302
+ },
303
+ statusSpinner: {
304
+ flex: undefined
530
305
  },
531
306
  statusText: {
532
307
  marginLeft: 8,
533
- fontSize: 14,
534
- color: 'rgba(255, 255, 255, 0.7)'
308
+ fontSize: 14
309
+ },
310
+ qrSection: {
311
+ width: '100%',
312
+ marginTop: 24
535
313
  },
536
314
  loadingContainer: {
537
315
  alignItems: 'center',
@@ -550,6 +328,19 @@ const styles = StyleSheet.create({
550
328
  fontSize: 14,
551
329
  textAlign: 'center',
552
330
  marginBottom: 16
331
+ },
332
+ footer: {
333
+ flexDirection: 'row',
334
+ flexWrap: 'wrap',
335
+ justifyContent: 'center',
336
+ marginTop: 28
337
+ },
338
+ footerText: {
339
+ fontSize: 14
340
+ },
341
+ footerLink: {
342
+ fontSize: 14,
343
+ fontWeight: '600'
553
344
  }
554
345
  });
555
346
  export default SignInModal;