@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.
- package/lib/commonjs/ui/components/AnotherDeviceQR.js +121 -0
- package/lib/commonjs/ui/components/AnotherDeviceQR.js.map +1 -0
- package/lib/commonjs/ui/components/SignInModal.js +145 -356
- package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
- package/lib/commonjs/ui/hooks/useOxyAuthSession.js +458 -0
- package/lib/commonjs/ui/hooks/useOxyAuthSession.js.map +1 -0
- package/lib/commonjs/ui/screens/OxyAuthScreen.js +62 -460
- package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/module/ui/components/AnotherDeviceQR.js +116 -0
- package/lib/module/ui/components/AnotherDeviceQR.js.map +1 -0
- package/lib/module/ui/components/SignInModal.js +149 -358
- package/lib/module/ui/components/SignInModal.js.map +1 -1
- package/lib/module/ui/hooks/useOxyAuthSession.js +452 -0
- package/lib/module/ui/hooks/useOxyAuthSession.js.map +1 -0
- package/lib/module/ui/screens/OxyAuthScreen.js +62 -460
- package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/typescript/commonjs/ui/components/AnotherDeviceQR.d.ts +24 -0
- package/lib/typescript/commonjs/ui/components/AnotherDeviceQR.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/SignInModal.d.ts +10 -5
- package/lib/typescript/commonjs/ui/components/SignInModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts +83 -0
- package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts +12 -8
- package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/AnotherDeviceQR.d.ts +24 -0
- package/lib/typescript/module/ui/components/AnotherDeviceQR.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/SignInModal.d.ts +10 -5
- package/lib/typescript/module/ui/components/SignInModal.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts +83 -0
- package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts +12 -8
- package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ui/components/AnotherDeviceQR.tsx +111 -0
- package/src/ui/components/SignInModal.tsx +142 -381
- package/src/ui/hooks/useOxyAuthSession.ts +556 -0
- package/src/ui/screens/OxyAuthScreen.tsx +61 -506
|
@@ -1,41 +1,35 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* SignInModal -
|
|
4
|
+
* SignInModal - Web-first centered sign-in modal (Continue with Oxy)
|
|
5
5
|
*
|
|
6
|
-
* A semi-transparent full-screen modal
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
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
|
|
14
|
-
import { View, Text, TouchableOpacity, StyleSheet, Modal
|
|
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
|
|
25
|
-
import {
|
|
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
|
-
//
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
}, [
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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:
|
|
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.
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
171
|
+
children: "Sign in to Oxy"
|
|
389
172
|
}), /*#__PURE__*/_jsx(Text, {
|
|
390
173
|
className: "text-muted-foreground",
|
|
391
174
|
style: styles.subtitle,
|
|
392
|
-
children: "
|
|
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
|
-
|
|
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(
|
|
415
|
-
|
|
416
|
-
|
|
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.
|
|
205
|
+
fillColor: theme.colors.primaryForeground,
|
|
206
|
+
style: styles.buttonIcon
|
|
448
207
|
}),
|
|
449
|
-
|
|
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
|
-
|
|
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:
|
|
278
|
+
marginBottom: 28
|
|
497
279
|
},
|
|
498
280
|
title: {
|
|
499
|
-
fontSize:
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
borderRadius:
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
fontSize: 14
|
|
295
|
+
buttonIcon: {
|
|
296
|
+
marginRight: 10
|
|
525
297
|
},
|
|
526
298
|
statusContainer: {
|
|
527
299
|
flexDirection: 'row',
|
|
528
300
|
alignItems: 'center',
|
|
529
|
-
marginTop:
|
|
301
|
+
marginTop: 20
|
|
302
|
+
},
|
|
303
|
+
statusSpinner: {
|
|
304
|
+
flex: undefined
|
|
530
305
|
},
|
|
531
306
|
statusText: {
|
|
532
307
|
marginLeft: 8,
|
|
533
|
-
fontSize: 14
|
|
534
|
-
|
|
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;
|