@oxyhq/services 10.4.0 → 11.0.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/mutations/useAccountMutations.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/mutations/useAccountMutations.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 +21 -33
- 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
|
@@ -4,98 +4,36 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = void 0;
|
|
7
|
-
var _react = require("react");
|
|
8
7
|
var _reactNative = require("react-native");
|
|
9
|
-
var _socket = _interopRequireDefault(require("socket.io-client"));
|
|
10
8
|
var _theme = require("@oxyhq/bloom/theme");
|
|
11
9
|
var _button = require("@oxyhq/bloom/button");
|
|
12
10
|
var _loading = require("@oxyhq/bloom/loading");
|
|
13
11
|
var _OxyContext = require("../context/OxyContext.js");
|
|
14
|
-
var _reactNativeQrcodeSvg = _interopRequireDefault(require("react-native-qrcode-svg"));
|
|
15
12
|
var _OxyLogo = _interopRequireDefault(require("../components/OxyLogo.js"));
|
|
16
|
-
var
|
|
17
|
-
var
|
|
13
|
+
var _AnotherDeviceQR = _interopRequireDefault(require("../components/AnotherDeviceQR.js"));
|
|
14
|
+
var _useOxyAuthSession = require("../hooks/useOxyAuthSession.js");
|
|
18
15
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
19
16
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
20
17
|
/**
|
|
21
|
-
* OxyAuthScreen - Sign in with Oxy
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
18
|
+
* OxyAuthScreen - Sign in with Oxy (native bottom-sheet container)
|
|
19
|
+
*
|
|
20
|
+
* Used by OTHER apps in the Oxy ecosystem to authenticate users. The primary
|
|
21
|
+
* action is the one-tap "Continue with Oxy" approval flow (opens the Oxy Auth
|
|
22
|
+
* web flow with a deep-link `redirect_uri` so the app→app return path can
|
|
23
|
+
* complete the sign-in). The QR code is demoted to a collapsed "Sign in on
|
|
24
|
+
* another device" disclosure — you can't scan your own screen.
|
|
25
|
+
*
|
|
26
|
+
* ALL of the auth-session machinery (session-token creation, QR data, socket +
|
|
27
|
+
* polling, the native deep-link return path, waiting/error/retry state, the
|
|
28
|
+
* open-auth handler, and cleanup) lives in the shared `useOxyAuthSession` hook,
|
|
29
|
+
* which the web `SignInModal` also consumes — neither container re-implements
|
|
30
|
+
* the transport. The Oxy Accounts app is where users manage their cryptographic
|
|
31
|
+
* identity; this screen should NOT be used within the Accounts app itself.
|
|
31
32
|
*/
|
|
32
33
|
|
|
33
|
-
const debug = (0, _core.createDebugLogger)('OxyAuthScreen');
|
|
34
|
-
const OXY_ACCOUNTS_WEB_URL = 'https://accounts.oxy.so';
|
|
35
|
-
const OXY_AUTH_WEB_URL = 'https://auth.oxy.so';
|
|
36
|
-
|
|
37
|
-
// Auth session expiration (5 minutes)
|
|
38
|
-
const AUTH_SESSION_EXPIRY_MS = 5 * 60 * 1000;
|
|
39
|
-
|
|
40
|
-
// Polling interval (fallback if socket fails)
|
|
41
|
-
const POLLING_INTERVAL_MS = 3000;
|
|
42
|
-
const resolveAuthWebBaseUrl = (baseURL, authWebUrl) => {
|
|
43
|
-
if (authWebUrl) {
|
|
44
|
-
return authWebUrl;
|
|
45
|
-
}
|
|
46
|
-
try {
|
|
47
|
-
const url = new URL(baseURL);
|
|
48
|
-
if (url.port === '3001') {
|
|
49
|
-
url.port = '3002';
|
|
50
|
-
return url.origin;
|
|
51
|
-
}
|
|
52
|
-
if (url.hostname.startsWith('api.')) {
|
|
53
|
-
url.hostname = `auth.${url.hostname.slice(4)}`;
|
|
54
|
-
return url.origin;
|
|
55
|
-
}
|
|
56
|
-
} catch {
|
|
57
|
-
// Ignore parsing errors, fall back to default.
|
|
58
|
-
}
|
|
59
|
-
return OXY_AUTH_WEB_URL;
|
|
60
|
-
};
|
|
61
|
-
const resolveAuthRedirectUri = async authRedirectUri => {
|
|
62
|
-
if (authRedirectUri) {
|
|
63
|
-
return authRedirectUri;
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
const initialUrl = await _reactNative.Linking.getInitialURL();
|
|
67
|
-
if (!initialUrl) {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
const parsed = new URL(initialUrl);
|
|
71
|
-
parsed.search = '';
|
|
72
|
-
parsed.hash = '';
|
|
73
|
-
return parsed.toString();
|
|
74
|
-
} catch {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
const getRedirectParams = url => {
|
|
79
|
-
try {
|
|
80
|
-
const parsed = new URL(url);
|
|
81
|
-
const sessionId = parsed.searchParams.get('session_id') ?? undefined;
|
|
82
|
-
const error = parsed.searchParams.get('error') ?? undefined;
|
|
83
|
-
if (!sessionId && !error) {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
return {
|
|
87
|
-
sessionId,
|
|
88
|
-
error
|
|
89
|
-
};
|
|
90
|
-
} catch {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
34
|
const OxyAuthScreen = ({
|
|
95
|
-
navigate,
|
|
96
35
|
goBack,
|
|
97
|
-
onAuthenticated
|
|
98
|
-
theme
|
|
36
|
+
onAuthenticated
|
|
99
37
|
}) => {
|
|
100
38
|
const bloomTheme = (0, _theme.useTheme)();
|
|
101
39
|
const {
|
|
@@ -103,307 +41,16 @@ const OxyAuthScreen = ({
|
|
|
103
41
|
switchSession,
|
|
104
42
|
clientId
|
|
105
43
|
} = (0, _OxyContext.useOxy)();
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// Handle successful authorization.
|
|
117
|
-
//
|
|
118
|
-
// The auth-session socket / poll (or deep-link return) hands us the
|
|
119
|
-
// authorized `sessionId`. Before any session-management code can touch it we
|
|
120
|
-
// MUST first exchange the secret `sessionToken` (held only by this client,
|
|
121
|
-
// generated for THIS flow) for the first access token via
|
|
122
|
-
// `claimSessionByToken` — the device-flow equivalent of OAuth's
|
|
123
|
-
// code-for-token exchange (RFC 8628 §3.4).
|
|
124
|
-
//
|
|
125
|
-
// Without that exchange the SDK has no bearer token — the session is
|
|
126
|
-
// authorized server-side but the app never becomes authenticated and the
|
|
127
|
-
// sheet sits "Waiting for authorization..." forever. Once
|
|
128
|
-
// `claimSessionByToken` plants the tokens in the HttpService, the rest of the
|
|
129
|
-
// session wiring flows through the normal `switchSession` path. This mirrors
|
|
130
|
-
// `SignInModal`'s web flow exactly.
|
|
131
|
-
const handleAuthSuccess = (0, _react.useCallback)(async (sessionId, sessionToken) => {
|
|
132
|
-
if (isProcessingRef.current) return;
|
|
133
|
-
isProcessingRef.current = true;
|
|
134
|
-
try {
|
|
135
|
-
// Claim the first access token with the secret sessionToken, then
|
|
136
|
-
// hydrate the session. The claim step is what the native screen was
|
|
137
|
-
// previously missing — see `completeDeviceFlowSignIn`. `switchSession`
|
|
138
|
-
// is always provided by the context here; the helper requires it.
|
|
139
|
-
if (!switchSession) {
|
|
140
|
-
throw new Error('Session management unavailable');
|
|
141
|
-
}
|
|
142
|
-
const user = await (0, _deviceFlowSignIn.completeDeviceFlowSignIn)({
|
|
143
|
-
oxyServices,
|
|
144
|
-
sessionId,
|
|
145
|
-
sessionToken,
|
|
146
|
-
switchSession
|
|
147
|
-
});
|
|
148
|
-
if (onAuthenticated) {
|
|
149
|
-
onAuthenticated(user);
|
|
150
|
-
}
|
|
151
|
-
} catch (err) {
|
|
152
|
-
debug.error('Error completing auth:', err);
|
|
153
|
-
setError('Authorization successful but failed to complete sign in. Please try again.');
|
|
154
|
-
isProcessingRef.current = false;
|
|
155
|
-
}
|
|
156
|
-
}, [oxyServices, switchSession, onAuthenticated]);
|
|
157
|
-
|
|
158
|
-
// Connect to socket for real-time updates
|
|
159
|
-
const connectSocket = (0, _react.useCallback)(sessionToken => {
|
|
160
|
-
const baseURL = oxyServices.getBaseURL();
|
|
161
|
-
|
|
162
|
-
// Connect to the auth-session namespace (no authentication required)
|
|
163
|
-
const socket = (0, _socket.default)(`${baseURL}/auth-session`, {
|
|
164
|
-
transports: ['websocket', 'polling'],
|
|
165
|
-
reconnection: true,
|
|
166
|
-
reconnectionAttempts: 3,
|
|
167
|
-
reconnectionDelay: 1000
|
|
168
|
-
});
|
|
169
|
-
socketRef.current = socket;
|
|
170
|
-
socket.on('connect', () => {
|
|
171
|
-
debug.log('Auth socket connected');
|
|
172
|
-
// Join the room for this session token
|
|
173
|
-
socket.emit('join', sessionToken);
|
|
174
|
-
setConnectionType('socket');
|
|
175
|
-
});
|
|
176
|
-
socket.on('joined', () => {
|
|
177
|
-
debug.log('Joined auth session room');
|
|
178
|
-
});
|
|
179
|
-
socket.on('auth_update', payload => {
|
|
180
|
-
debug.log('Auth update received:', payload);
|
|
181
|
-
if (payload.status === 'authorized' && payload.sessionId) {
|
|
182
|
-
cleanup();
|
|
183
|
-
// `sessionToken` is this flow's secret credential (in closure) — pass
|
|
184
|
-
// it through so `handleAuthSuccess` can claim the first access token.
|
|
185
|
-
handleAuthSuccess(payload.sessionId, sessionToken);
|
|
186
|
-
} else if (payload.status === 'cancelled') {
|
|
187
|
-
cleanup();
|
|
188
|
-
setError('Authorization was denied.');
|
|
189
|
-
} else if (payload.status === 'expired') {
|
|
190
|
-
cleanup();
|
|
191
|
-
setError('Session expired. Please try again.');
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
socket.on('connect_error', err => {
|
|
195
|
-
debug.log('Socket connection error, falling back to polling:', err instanceof Error ? err.message : null);
|
|
196
|
-
// Realtime transport errored — reflect the honest connection state. The
|
|
197
|
-
// poll is already running (started unconditionally in
|
|
198
|
-
// `generateAuthSession`), so `startPolling` here is a no-op backstop.
|
|
199
|
-
socket.disconnect();
|
|
200
|
-
setConnectionType('polling');
|
|
201
|
-
startPolling(sessionToken);
|
|
202
|
-
});
|
|
203
|
-
socket.on('disconnect', () => {
|
|
204
|
-
debug.log('Auth socket disconnected');
|
|
205
|
-
});
|
|
206
|
-
}, [oxyServices, handleAuthSuccess]);
|
|
207
|
-
|
|
208
|
-
// Start polling for authorization.
|
|
209
|
-
//
|
|
210
|
-
// Idempotent: if a poll interval is already running this is a no-op, so the
|
|
211
|
-
// `connect_error` path (which also calls this) cannot stack a second interval
|
|
212
|
-
// on top of the always-on poll started in `generateAuthSession`.
|
|
213
|
-
const startPolling = (0, _react.useCallback)(sessionToken => {
|
|
214
|
-
if (pollingIntervalRef.current) return;
|
|
215
|
-
pollingIntervalRef.current = setInterval(async () => {
|
|
216
|
-
if (isProcessingRef.current) return;
|
|
217
|
-
try {
|
|
218
|
-
const response = await oxyServices.makeRequest('GET', `/auth/session/status/${sessionToken}`, undefined, {
|
|
219
|
-
cache: false
|
|
220
|
-
});
|
|
221
|
-
if (response.authorized && response.sessionId) {
|
|
222
|
-
cleanup();
|
|
223
|
-
// Pass the original sessionToken (in closure) through; the claim
|
|
224
|
-
// exchange needs it to mint the first access token.
|
|
225
|
-
handleAuthSuccess(response.sessionId, sessionToken);
|
|
226
|
-
} else if (response.status === 'cancelled') {
|
|
227
|
-
cleanup();
|
|
228
|
-
setError('Authorization was denied.');
|
|
229
|
-
} else if (response.status === 'expired') {
|
|
230
|
-
cleanup();
|
|
231
|
-
setError('Session expired. Please try again.');
|
|
232
|
-
}
|
|
233
|
-
} catch (err) {
|
|
234
|
-
// Silent fail for polling - will retry
|
|
235
|
-
debug.log('Auth polling error:', err);
|
|
236
|
-
}
|
|
237
|
-
}, POLLING_INTERVAL_MS);
|
|
238
|
-
}, [oxyServices, handleAuthSuccess]);
|
|
239
|
-
|
|
240
|
-
// Cleanup socket and polling
|
|
241
|
-
const cleanup = (0, _react.useCallback)(() => {
|
|
242
|
-
setIsWaiting(false);
|
|
243
|
-
if (socketRef.current) {
|
|
244
|
-
socketRef.current.disconnect();
|
|
245
|
-
socketRef.current = null;
|
|
246
|
-
}
|
|
247
|
-
if (pollingIntervalRef.current) {
|
|
248
|
-
clearInterval(pollingIntervalRef.current);
|
|
249
|
-
pollingIntervalRef.current = null;
|
|
250
|
-
}
|
|
251
|
-
}, []);
|
|
252
|
-
|
|
253
|
-
// Generate a new auth session
|
|
254
|
-
const generateAuthSession = (0, _react.useCallback)(async () => {
|
|
255
|
-
setIsLoading(true);
|
|
256
|
-
setError(null);
|
|
257
|
-
isProcessingRef.current = false;
|
|
258
|
-
|
|
259
|
-
// The cross-app device sign-in flow identifies the requesting app by its
|
|
260
|
-
// real registered OAuth client id (ApplicationCredential publicKey).
|
|
261
|
-
// Without it the API cannot resolve the consent identity, so we fail fast
|
|
262
|
-
// with a clear configuration error rather than creating a session the
|
|
263
|
-
// server would reject.
|
|
264
|
-
if (!clientId) {
|
|
265
|
-
setError('This app is not configured for sign-in (missing clientId).');
|
|
266
|
-
setIsLoading(false);
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
try {
|
|
270
|
-
// Generate a unique session token for this auth request
|
|
271
|
-
const sessionToken = generateSessionToken();
|
|
272
|
-
const expiresAt = Date.now() + AUTH_SESSION_EXPIRY_MS;
|
|
273
|
-
|
|
274
|
-
// Register the auth session with the server
|
|
275
|
-
await oxyServices.makeRequest('POST', '/auth/session/create', {
|
|
276
|
-
sessionToken,
|
|
277
|
-
expiresAt,
|
|
278
|
-
clientId
|
|
279
|
-
}, {
|
|
280
|
-
cache: false
|
|
281
|
-
});
|
|
282
|
-
setAuthSession({
|
|
283
|
-
sessionToken,
|
|
284
|
-
expiresAt
|
|
285
|
-
});
|
|
286
|
-
setIsWaiting(true);
|
|
287
|
-
|
|
288
|
-
// Socket is the fast path; the poll is a transport-independent backstop
|
|
289
|
-
// that guarantees completion even if the socket connects but silently
|
|
290
|
-
// never delivers auth_update (RN transport / idle-timeout).
|
|
291
|
-
connectSocket(sessionToken);
|
|
292
|
-
startPolling(sessionToken);
|
|
293
|
-
} catch (err) {
|
|
294
|
-
setError((err instanceof Error ? err.message : null) || 'Failed to create auth session');
|
|
295
|
-
} finally {
|
|
296
|
-
setIsLoading(false);
|
|
297
|
-
}
|
|
298
|
-
}, [oxyServices, connectSocket, startPolling, clientId]);
|
|
299
|
-
|
|
300
|
-
// Generate a random session token
|
|
301
|
-
const generateSessionToken = () => {
|
|
302
|
-
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
303
|
-
let result = '';
|
|
304
|
-
for (let i = 0; i < 32; i++) {
|
|
305
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
306
|
-
}
|
|
307
|
-
return result;
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
// Clean up on unmount
|
|
311
|
-
(0, _react.useEffect)(() => {
|
|
312
|
-
return () => {
|
|
313
|
-
cleanup();
|
|
314
|
-
};
|
|
315
|
-
}, [cleanup]);
|
|
316
|
-
|
|
317
|
-
// Initialize auth session
|
|
318
|
-
(0, _react.useEffect)(() => {
|
|
319
|
-
generateAuthSession();
|
|
320
|
-
}, []);
|
|
321
|
-
|
|
322
|
-
// Check if session expired
|
|
323
|
-
(0, _react.useEffect)(() => {
|
|
324
|
-
if (authSession && Date.now() > authSession.expiresAt) {
|
|
325
|
-
cleanup();
|
|
326
|
-
setAuthSession(null);
|
|
327
|
-
setError('Session expired. Please try again.');
|
|
328
|
-
}
|
|
329
|
-
}, [authSession, cleanup]);
|
|
330
|
-
|
|
331
|
-
// Build the QR code data
|
|
332
|
-
const getQRData = () => {
|
|
333
|
-
if (!authSession) return '';
|
|
334
|
-
|
|
335
|
-
// Format: oxyauth://{sessionToken}
|
|
336
|
-
return `oxyauth://${authSession.sessionToken}`;
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
// Open Oxy Auth web flow
|
|
340
|
-
const handleOpenAuth = (0, _react.useCallback)(async () => {
|
|
341
|
-
if (!authSession) return;
|
|
342
|
-
const authBaseUrl = resolveAuthWebBaseUrl(oxyServices.getBaseURL(), oxyServices.config?.authWebUrl);
|
|
343
|
-
const webUrl = new URL('/authorize', authBaseUrl);
|
|
344
|
-
webUrl.searchParams.set('token', authSession.sessionToken);
|
|
345
|
-
const redirectUri = await resolveAuthRedirectUri(oxyServices.config?.authRedirectUri);
|
|
346
|
-
if (redirectUri) {
|
|
347
|
-
webUrl.searchParams.set('redirect_uri', redirectUri);
|
|
348
|
-
}
|
|
349
|
-
try {
|
|
350
|
-
await _reactNative.Linking.openURL(webUrl.toString());
|
|
351
|
-
} catch (err) {
|
|
352
|
-
setError('Unable to open Oxy Auth. Please try again or use the QR code.');
|
|
353
|
-
}
|
|
354
|
-
}, [authSession, oxyServices]);
|
|
355
|
-
|
|
356
|
-
// Refresh session
|
|
357
|
-
const handleRefresh = (0, _react.useCallback)(() => {
|
|
358
|
-
cleanup();
|
|
359
|
-
generateAuthSession();
|
|
360
|
-
}, [generateAuthSession, cleanup]);
|
|
361
|
-
const handleAuthRedirect = (0, _react.useCallback)(url => {
|
|
362
|
-
const params = getRedirectParams(url);
|
|
363
|
-
if (!params) {
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
if (params.error) {
|
|
367
|
-
cleanup();
|
|
368
|
-
setError('Authorization was denied.');
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
if (params.sessionId) {
|
|
372
|
-
// The deep-link return carries only `session_id` — the secret
|
|
373
|
-
// `sessionToken` for this flow lives in component state (generated in
|
|
374
|
-
// `generateAuthSession`). Without it we cannot claim the first access
|
|
375
|
-
// token, so the flow would 401 in `handleAuthSuccess`. If it is somehow
|
|
376
|
-
// unavailable, fall through to the socket/poll path (which carries the
|
|
377
|
-
// token in closure) rather than attempting an unauthenticated claim.
|
|
378
|
-
const flowSessionToken = authSession?.sessionToken;
|
|
379
|
-
if (!flowSessionToken) {
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
cleanup();
|
|
383
|
-
handleAuthSuccess(params.sessionId, flowSessionToken);
|
|
384
|
-
}
|
|
385
|
-
}, [authSession, cleanup, handleAuthSuccess]);
|
|
386
|
-
(0, _react.useEffect)(() => {
|
|
387
|
-
if (_reactNative.Platform.OS === 'web') {
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
const subscription = _reactNative.Linking.addEventListener('url', ({
|
|
391
|
-
url
|
|
392
|
-
}) => {
|
|
393
|
-
linkingHandledRef.current = true;
|
|
394
|
-
handleAuthRedirect(url);
|
|
395
|
-
});
|
|
396
|
-
_reactNative.Linking.getInitialURL().then(url => {
|
|
397
|
-
if (url && !linkingHandledRef.current) {
|
|
398
|
-
handleAuthRedirect(url);
|
|
399
|
-
}
|
|
400
|
-
}).catch(() => {
|
|
401
|
-
// Ignore linking errors; auth will still resolve via socket/polling.
|
|
402
|
-
});
|
|
403
|
-
return () => {
|
|
404
|
-
subscription.remove();
|
|
405
|
-
};
|
|
406
|
-
}, [handleAuthRedirect]);
|
|
44
|
+
const {
|
|
45
|
+
qrData,
|
|
46
|
+
isLoading,
|
|
47
|
+
error,
|
|
48
|
+
isWaiting,
|
|
49
|
+
openAuthApproval,
|
|
50
|
+
retry
|
|
51
|
+
} = (0, _useOxyAuthSession.useOxyAuthSession)(oxyServices, clientId, switchSession, {
|
|
52
|
+
onSignedIn: onAuthenticated
|
|
53
|
+
});
|
|
407
54
|
if (isLoading) {
|
|
408
55
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
409
56
|
style: [styles.container, {
|
|
@@ -429,11 +76,8 @@ const OxyAuthScreen = ({
|
|
|
429
76
|
children: error
|
|
430
77
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_button.Button, {
|
|
431
78
|
variant: "primary",
|
|
432
|
-
onPress:
|
|
433
|
-
style:
|
|
434
|
-
width: '100%',
|
|
435
|
-
borderRadius: 12
|
|
436
|
-
},
|
|
79
|
+
onPress: retry,
|
|
80
|
+
style: styles.primaryButton,
|
|
437
81
|
children: "Try Again"
|
|
438
82
|
})]
|
|
439
83
|
});
|
|
@@ -450,84 +94,57 @@ const OxyAuthScreen = ({
|
|
|
450
94
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
451
95
|
style: styles.title,
|
|
452
96
|
className: "text-foreground",
|
|
453
|
-
children: "Sign in
|
|
97
|
+
children: "Sign in to Oxy"
|
|
454
98
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
455
99
|
style: styles.subtitle,
|
|
456
100
|
className: "text-muted-foreground",
|
|
457
|
-
children: "
|
|
458
|
-
})]
|
|
459
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
460
|
-
style: styles.qrContainer,
|
|
461
|
-
className: "bg-secondary border-border",
|
|
462
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
463
|
-
style: styles.qrWrapper,
|
|
464
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeQrcodeSvg.default, {
|
|
465
|
-
value: getQRData(),
|
|
466
|
-
size: 200,
|
|
467
|
-
backgroundColor: "white",
|
|
468
|
-
color: "black"
|
|
469
|
-
})
|
|
470
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
471
|
-
style: styles.qrHint,
|
|
472
|
-
className: "text-muted-foreground",
|
|
473
|
-
children: "Scan with Oxy Accounts app"
|
|
474
|
-
})]
|
|
475
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
476
|
-
style: styles.dividerContainer,
|
|
477
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
478
|
-
style: styles.divider,
|
|
479
|
-
className: "bg-border"
|
|
480
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
481
|
-
style: styles.dividerText,
|
|
482
|
-
className: "text-muted-foreground",
|
|
483
|
-
children: "or"
|
|
484
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
485
|
-
style: styles.divider,
|
|
486
|
-
className: "bg-border"
|
|
101
|
+
children: "Continue with your Oxy identity to sign in securely"
|
|
487
102
|
})]
|
|
488
103
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_button.Button, {
|
|
489
104
|
variant: "primary",
|
|
490
|
-
onPress:
|
|
105
|
+
onPress: openAuthApproval,
|
|
491
106
|
icon: /*#__PURE__*/(0, _jsxRuntime.jsx)(_OxyLogo.default, {
|
|
492
107
|
variant: "icon",
|
|
493
108
|
size: 20,
|
|
494
|
-
fillColor: bloomTheme.colors.
|
|
109
|
+
fillColor: bloomTheme.colors.primaryForeground,
|
|
495
110
|
style: styles.buttonIcon
|
|
496
111
|
}),
|
|
497
|
-
style:
|
|
498
|
-
|
|
499
|
-
borderRadius: 12
|
|
500
|
-
},
|
|
501
|
-
children: "Open Oxy Auth"
|
|
112
|
+
style: styles.primaryButton,
|
|
113
|
+
children: "Continue with Oxy"
|
|
502
114
|
}), isWaiting && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
503
115
|
style: styles.statusContainer,
|
|
504
116
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_loading.Loading, {
|
|
505
117
|
size: "small",
|
|
506
|
-
style:
|
|
507
|
-
flex: undefined
|
|
508
|
-
}
|
|
118
|
+
style: styles.statusSpinner
|
|
509
119
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
510
120
|
style: styles.statusText,
|
|
511
121
|
className: "text-muted-foreground",
|
|
512
122
|
children: "Waiting for authorization..."
|
|
513
123
|
})]
|
|
124
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
125
|
+
style: styles.qrSection,
|
|
126
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_AnotherDeviceQR.default, {
|
|
127
|
+
qrData: qrData
|
|
128
|
+
})
|
|
514
129
|
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
515
130
|
style: styles.footer,
|
|
516
131
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
|
|
517
132
|
style: styles.footerText,
|
|
518
133
|
className: "text-muted-foreground",
|
|
519
|
-
children: ["Don't have Oxy
|
|
134
|
+
children: ["Don't have an Oxy account?", ' ']
|
|
520
135
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
521
|
-
onPress: () => _reactNative.Linking.openURL(OXY_ACCOUNTS_WEB_URL),
|
|
136
|
+
onPress: () => _reactNative.Linking.openURL(_useOxyAuthSession.OXY_ACCOUNTS_WEB_URL),
|
|
137
|
+
accessibilityRole: "link",
|
|
522
138
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
523
139
|
style: styles.footerLink,
|
|
524
140
|
className: "text-primary",
|
|
525
|
-
children: "
|
|
141
|
+
children: "Create one"
|
|
526
142
|
})
|
|
527
143
|
})]
|
|
528
144
|
}), goBack && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
529
145
|
style: styles.cancelButton,
|
|
530
146
|
onPress: goBack,
|
|
147
|
+
accessibilityRole: "button",
|
|
531
148
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
532
149
|
style: styles.cancelText,
|
|
533
150
|
className: "text-muted-foreground",
|
|
@@ -545,62 +162,47 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
545
162
|
},
|
|
546
163
|
header: {
|
|
547
164
|
alignItems: 'center',
|
|
548
|
-
marginBottom:
|
|
165
|
+
marginBottom: 28
|
|
549
166
|
},
|
|
550
167
|
title: {
|
|
551
168
|
fontSize: 24,
|
|
552
169
|
fontWeight: 'bold',
|
|
553
|
-
marginTop: 16
|
|
170
|
+
marginTop: 16,
|
|
171
|
+
textAlign: 'center'
|
|
554
172
|
},
|
|
555
173
|
subtitle: {
|
|
556
174
|
fontSize: 14,
|
|
557
175
|
marginTop: 8,
|
|
558
176
|
textAlign: 'center'
|
|
559
177
|
},
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
borderRadius: 16,
|
|
563
|
-
borderWidth: 1,
|
|
564
|
-
alignItems: 'center'
|
|
565
|
-
},
|
|
566
|
-
qrWrapper: {
|
|
567
|
-
padding: 16,
|
|
568
|
-
backgroundColor: 'white',
|
|
178
|
+
primaryButton: {
|
|
179
|
+
width: '100%',
|
|
569
180
|
borderRadius: 12
|
|
570
181
|
},
|
|
571
|
-
qrHint: {
|
|
572
|
-
marginTop: 16,
|
|
573
|
-
fontSize: 12
|
|
574
|
-
},
|
|
575
|
-
dividerContainer: {
|
|
576
|
-
flexDirection: 'row',
|
|
577
|
-
alignItems: 'center',
|
|
578
|
-
marginVertical: 24,
|
|
579
|
-
width: '100%'
|
|
580
|
-
},
|
|
581
|
-
divider: {
|
|
582
|
-
flex: 1,
|
|
583
|
-
height: 1
|
|
584
|
-
},
|
|
585
|
-
dividerText: {
|
|
586
|
-
marginHorizontal: 16,
|
|
587
|
-
fontSize: 14
|
|
588
|
-
},
|
|
589
182
|
buttonIcon: {
|
|
590
183
|
marginRight: 10
|
|
591
184
|
},
|
|
592
185
|
statusContainer: {
|
|
593
186
|
flexDirection: 'row',
|
|
594
187
|
alignItems: 'center',
|
|
595
|
-
marginTop:
|
|
188
|
+
marginTop: 20
|
|
189
|
+
},
|
|
190
|
+
statusSpinner: {
|
|
191
|
+
flex: undefined
|
|
596
192
|
},
|
|
597
193
|
statusText: {
|
|
598
194
|
marginLeft: 8,
|
|
599
195
|
fontSize: 14
|
|
600
196
|
},
|
|
197
|
+
qrSection: {
|
|
198
|
+
width: '100%',
|
|
199
|
+
marginTop: 24
|
|
200
|
+
},
|
|
601
201
|
footer: {
|
|
602
202
|
flexDirection: 'row',
|
|
603
|
-
|
|
203
|
+
flexWrap: 'wrap',
|
|
204
|
+
justifyContent: 'center',
|
|
205
|
+
marginTop: 28
|
|
604
206
|
},
|
|
605
207
|
footerText: {
|
|
606
208
|
fontSize: 14
|