@oxyhq/services 5.15.6 → 5.15.9
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/crypto/keyManager.js +11 -11
- package/lib/commonjs/crypto/keyManager.js.map +1 -1
- package/lib/commonjs/crypto/polyfill.js +48 -1
- package/lib/commonjs/crypto/polyfill.js.map +1 -1
- package/lib/commonjs/ui/components/TextField.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/OxyAuthScreen.js +168 -76
- package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/module/crypto/keyManager.js +11 -11
- package/lib/module/crypto/keyManager.js.map +1 -1
- package/lib/module/crypto/polyfill.js +48 -1
- package/lib/module/crypto/polyfill.js.map +1 -1
- package/lib/module/ui/components/TextField.js.map +1 -1
- package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/module/ui/screens/OxyAuthScreen.js +169 -77
- package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/typescript/core/OxyServices.d.ts +1 -1
- package/lib/typescript/core/OxyServices.d.ts.map +1 -1
- package/lib/typescript/crypto/polyfill.d.ts +4 -0
- package/lib/typescript/crypto/polyfill.d.ts.map +1 -1
- package/lib/typescript/ui/screens/OxyAuthScreen.d.ts +1 -0
- package/lib/typescript/ui/screens/OxyAuthScreen.d.ts.map +1 -1
- package/package.json +1 -2
- package/src/crypto/keyManager.ts +11 -11
- package/src/crypto/polyfill.ts +53 -1
- package/src/ui/components/TextField.tsx +2 -2
- package/src/ui/screens/AccountSettingsScreen.tsx +1 -1
- package/src/ui/screens/OxyAuthScreen.tsx +184 -68
- package/lib/typescript/types/expo-random.d.ts +0 -9
- package/src/types/expo-random.d.ts +0 -9
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* 1. Scan QR code with Oxy Accounts app
|
|
7
7
|
* 2. Open Oxy Accounts app directly (via deep link)
|
|
8
8
|
*
|
|
9
|
+
* Uses WebSocket for real-time authorization updates (with polling fallback).
|
|
9
10
|
* The Oxy Accounts app is where users manage their cryptographic identity.
|
|
10
11
|
* This screen should NOT be used within the Accounts app itself.
|
|
11
12
|
*/
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
Platform,
|
|
22
23
|
ActivityIndicator,
|
|
23
24
|
} from 'react-native';
|
|
25
|
+
import io, { type Socket } from 'socket.io-client';
|
|
24
26
|
import type { BaseScreenProps } from '../types/navigation';
|
|
25
27
|
import { useThemeColors } from '../styles';
|
|
26
28
|
import { useOxy } from '../context/OxyContext';
|
|
@@ -34,11 +36,22 @@ const OXY_ACCOUNTS_WEB_URL = 'https://accounts.oxy.so';
|
|
|
34
36
|
// Auth session expiration (5 minutes)
|
|
35
37
|
const AUTH_SESSION_EXPIRY_MS = 5 * 60 * 1000;
|
|
36
38
|
|
|
39
|
+
// Polling interval (fallback if socket fails)
|
|
40
|
+
const POLLING_INTERVAL_MS = 3000;
|
|
41
|
+
|
|
37
42
|
interface AuthSession {
|
|
38
43
|
sessionToken: string;
|
|
39
44
|
expiresAt: number;
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
interface AuthUpdatePayload {
|
|
48
|
+
status: 'authorized' | 'cancelled' | 'expired';
|
|
49
|
+
sessionId?: string;
|
|
50
|
+
publicKey?: string;
|
|
51
|
+
userId?: string;
|
|
52
|
+
username?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
const OxyAuthScreen: React.FC<BaseScreenProps> = ({
|
|
43
56
|
navigate,
|
|
44
57
|
goBack,
|
|
@@ -52,13 +65,151 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
|
|
|
52
65
|
const [authSession, setAuthSession] = useState<AuthSession | null>(null);
|
|
53
66
|
const [isLoading, setIsLoading] = useState(true);
|
|
54
67
|
const [error, setError] = useState<string | null>(null);
|
|
55
|
-
const [
|
|
68
|
+
const [isWaiting, setIsWaiting] = useState(false);
|
|
69
|
+
const [connectionType, setConnectionType] = useState<'socket' | 'polling'>('socket');
|
|
70
|
+
|
|
71
|
+
const socketRef = useRef<Socket | null>(null);
|
|
56
72
|
const pollingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
73
|
+
const isProcessingRef = useRef(false);
|
|
74
|
+
|
|
75
|
+
// Handle successful authorization
|
|
76
|
+
const handleAuthSuccess = useCallback(async (sessionId: string) => {
|
|
77
|
+
if (isProcessingRef.current) return;
|
|
78
|
+
isProcessingRef.current = true;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// Get token and user data
|
|
82
|
+
await oxyServices.getTokenBySession(sessionId);
|
|
83
|
+
const user = await oxyServices.getUserBySession(sessionId);
|
|
84
|
+
|
|
85
|
+
if (onAuthenticated) {
|
|
86
|
+
onAuthenticated(user);
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (__DEV__) {
|
|
90
|
+
console.error('Error completing auth:', err);
|
|
91
|
+
}
|
|
92
|
+
setError('Authorization successful but failed to complete sign in. Please try again.');
|
|
93
|
+
isProcessingRef.current = false;
|
|
94
|
+
}
|
|
95
|
+
}, [oxyServices, onAuthenticated]);
|
|
96
|
+
|
|
97
|
+
// Connect to socket for real-time updates
|
|
98
|
+
const connectSocket = useCallback((sessionToken: string) => {
|
|
99
|
+
const baseURL = oxyServices.getBaseURL();
|
|
100
|
+
|
|
101
|
+
// Connect to the auth-session namespace (no authentication required)
|
|
102
|
+
const socket = io(`${baseURL}/auth-session`, {
|
|
103
|
+
transports: ['websocket', 'polling'],
|
|
104
|
+
reconnection: true,
|
|
105
|
+
reconnectionAttempts: 3,
|
|
106
|
+
reconnectionDelay: 1000,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
socketRef.current = socket;
|
|
110
|
+
|
|
111
|
+
socket.on('connect', () => {
|
|
112
|
+
if (__DEV__) {
|
|
113
|
+
console.log('Auth socket connected');
|
|
114
|
+
}
|
|
115
|
+
// Join the room for this session token
|
|
116
|
+
socket.emit('join', sessionToken);
|
|
117
|
+
setConnectionType('socket');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
socket.on('joined', () => {
|
|
121
|
+
if (__DEV__) {
|
|
122
|
+
console.log('Joined auth session room');
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
socket.on('auth_update', (payload: AuthUpdatePayload) => {
|
|
127
|
+
if (__DEV__) {
|
|
128
|
+
console.log('Auth update received:', payload);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (payload.status === 'authorized' && payload.sessionId) {
|
|
132
|
+
cleanup();
|
|
133
|
+
handleAuthSuccess(payload.sessionId);
|
|
134
|
+
} else if (payload.status === 'cancelled') {
|
|
135
|
+
cleanup();
|
|
136
|
+
setError('Authorization was denied.');
|
|
137
|
+
} else if (payload.status === 'expired') {
|
|
138
|
+
cleanup();
|
|
139
|
+
setError('Session expired. Please try again.');
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
socket.on('connect_error', (err) => {
|
|
144
|
+
if (__DEV__) {
|
|
145
|
+
console.log('Socket connection error, falling back to polling:', err.message);
|
|
146
|
+
}
|
|
147
|
+
// Fall back to polling if socket fails
|
|
148
|
+
socket.disconnect();
|
|
149
|
+
startPolling(sessionToken);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
socket.on('disconnect', () => {
|
|
153
|
+
if (__DEV__) {
|
|
154
|
+
console.log('Auth socket disconnected');
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}, [oxyServices, handleAuthSuccess]);
|
|
158
|
+
|
|
159
|
+
// Start polling for authorization (fallback)
|
|
160
|
+
const startPolling = useCallback((sessionToken: string) => {
|
|
161
|
+
setConnectionType('polling');
|
|
162
|
+
|
|
163
|
+
pollingIntervalRef.current = setInterval(async () => {
|
|
164
|
+
if (isProcessingRef.current) return;
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const response = await oxyServices.makeRequest<{
|
|
168
|
+
authorized: boolean;
|
|
169
|
+
sessionId?: string;
|
|
170
|
+
publicKey?: string;
|
|
171
|
+
status?: string;
|
|
172
|
+
}>('GET', `/api/auth/session/status/${sessionToken}`, undefined, { cache: false });
|
|
173
|
+
|
|
174
|
+
if (response.authorized && response.sessionId) {
|
|
175
|
+
cleanup();
|
|
176
|
+
handleAuthSuccess(response.sessionId);
|
|
177
|
+
} else if (response.status === 'cancelled') {
|
|
178
|
+
cleanup();
|
|
179
|
+
setError('Authorization was denied.');
|
|
180
|
+
} else if (response.status === 'expired') {
|
|
181
|
+
cleanup();
|
|
182
|
+
setError('Session expired. Please try again.');
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
// Silent fail for polling - will retry
|
|
186
|
+
if (__DEV__) {
|
|
187
|
+
console.log('Auth polling error:', err);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}, POLLING_INTERVAL_MS);
|
|
191
|
+
}, [oxyServices, handleAuthSuccess]);
|
|
192
|
+
|
|
193
|
+
// Cleanup socket and polling
|
|
194
|
+
const cleanup = useCallback(() => {
|
|
195
|
+
setIsWaiting(false);
|
|
196
|
+
|
|
197
|
+
if (socketRef.current) {
|
|
198
|
+
socketRef.current.disconnect();
|
|
199
|
+
socketRef.current = null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (pollingIntervalRef.current) {
|
|
203
|
+
clearInterval(pollingIntervalRef.current);
|
|
204
|
+
pollingIntervalRef.current = null;
|
|
205
|
+
}
|
|
206
|
+
}, []);
|
|
57
207
|
|
|
58
208
|
// Generate a new auth session
|
|
59
209
|
const generateAuthSession = useCallback(async () => {
|
|
60
210
|
setIsLoading(true);
|
|
61
211
|
setError(null);
|
|
212
|
+
isProcessingRef.current = false;
|
|
62
213
|
|
|
63
214
|
try {
|
|
64
215
|
// Generate a unique session token for this auth request
|
|
@@ -66,7 +217,6 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
|
|
|
66
217
|
const expiresAt = Date.now() + AUTH_SESSION_EXPIRY_MS;
|
|
67
218
|
|
|
68
219
|
// Register the auth session with the server
|
|
69
|
-
// The server will associate this token with the user when they authorize in Accounts
|
|
70
220
|
await oxyServices.makeRequest('POST', '/api/auth/session/create', {
|
|
71
221
|
sessionToken,
|
|
72
222
|
expiresAt,
|
|
@@ -74,13 +224,16 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
|
|
|
74
224
|
}, { cache: false });
|
|
75
225
|
|
|
76
226
|
setAuthSession({ sessionToken, expiresAt });
|
|
77
|
-
|
|
227
|
+
setIsWaiting(true);
|
|
228
|
+
|
|
229
|
+
// Try socket first, will fall back to polling if needed
|
|
230
|
+
connectSocket(sessionToken);
|
|
78
231
|
} catch (err: any) {
|
|
79
232
|
setError(err.message || 'Failed to create auth session');
|
|
80
233
|
} finally {
|
|
81
234
|
setIsLoading(false);
|
|
82
235
|
}
|
|
83
|
-
}, [oxyServices]);
|
|
236
|
+
}, [oxyServices, connectSocket]);
|
|
84
237
|
|
|
85
238
|
// Generate a random session token
|
|
86
239
|
const generateSessionToken = (): string => {
|
|
@@ -92,54 +245,12 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
|
|
|
92
245
|
return result;
|
|
93
246
|
};
|
|
94
247
|
|
|
95
|
-
// Start polling for authorization
|
|
96
|
-
const startPolling = useCallback((sessionToken: string) => {
|
|
97
|
-
setIsPolling(true);
|
|
98
|
-
|
|
99
|
-
pollingIntervalRef.current = setInterval(async () => {
|
|
100
|
-
try {
|
|
101
|
-
const response = await oxyServices.makeRequest<{
|
|
102
|
-
authorized: boolean;
|
|
103
|
-
sessionId?: string;
|
|
104
|
-
publicKey?: string;
|
|
105
|
-
}>('GET', `/api/auth/session/status/${sessionToken}`, undefined, { cache: false });
|
|
106
|
-
|
|
107
|
-
if (response.authorized && response.sessionId) {
|
|
108
|
-
// Authorization successful!
|
|
109
|
-
stopPolling();
|
|
110
|
-
|
|
111
|
-
// Get token and user data
|
|
112
|
-
await oxyServices.getTokenBySession(response.sessionId);
|
|
113
|
-
const user = await oxyServices.getUserBySession(response.sessionId);
|
|
114
|
-
|
|
115
|
-
if (onAuthenticated) {
|
|
116
|
-
onAuthenticated(user);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
} catch (err) {
|
|
120
|
-
// Silent fail for polling - will retry
|
|
121
|
-
if (__DEV__) {
|
|
122
|
-
console.log('Auth polling error:', err);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}, 2000); // Poll every 2 seconds
|
|
126
|
-
}, [oxyServices, onAuthenticated]);
|
|
127
|
-
|
|
128
|
-
// Stop polling
|
|
129
|
-
const stopPolling = useCallback(() => {
|
|
130
|
-
setIsPolling(false);
|
|
131
|
-
if (pollingIntervalRef.current) {
|
|
132
|
-
clearInterval(pollingIntervalRef.current);
|
|
133
|
-
pollingIntervalRef.current = null;
|
|
134
|
-
}
|
|
135
|
-
}, []);
|
|
136
|
-
|
|
137
248
|
// Clean up on unmount
|
|
138
249
|
useEffect(() => {
|
|
139
250
|
return () => {
|
|
140
|
-
|
|
251
|
+
cleanup();
|
|
141
252
|
};
|
|
142
|
-
}, [
|
|
253
|
+
}, [cleanup]);
|
|
143
254
|
|
|
144
255
|
// Initialize auth session
|
|
145
256
|
useEffect(() => {
|
|
@@ -149,11 +260,11 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
|
|
|
149
260
|
// Check if session expired
|
|
150
261
|
useEffect(() => {
|
|
151
262
|
if (authSession && Date.now() > authSession.expiresAt) {
|
|
152
|
-
|
|
263
|
+
cleanup();
|
|
153
264
|
setAuthSession(null);
|
|
154
265
|
setError('Session expired. Please try again.');
|
|
155
266
|
}
|
|
156
|
-
}, [authSession,
|
|
267
|
+
}, [authSession, cleanup]);
|
|
157
268
|
|
|
158
269
|
// Build the QR code data
|
|
159
270
|
const getQRData = (): string => {
|
|
@@ -191,9 +302,9 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
|
|
|
191
302
|
|
|
192
303
|
// Refresh session
|
|
193
304
|
const handleRefresh = useCallback(() => {
|
|
194
|
-
|
|
305
|
+
cleanup();
|
|
195
306
|
generateAuthSession();
|
|
196
|
-
}, [generateAuthSession,
|
|
307
|
+
}, [generateAuthSession, cleanup]);
|
|
197
308
|
|
|
198
309
|
if (isLoading) {
|
|
199
310
|
return (
|
|
@@ -246,24 +357,29 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
|
|
|
246
357
|
</Text>
|
|
247
358
|
</View>
|
|
248
359
|
|
|
249
|
-
{/* Divider */}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
360
|
+
{/* Divider and Open Accounts Button - Only show on native platforms */}
|
|
361
|
+
{Platform.OS !== 'web' && (
|
|
362
|
+
<>
|
|
363
|
+
{/* Divider */}
|
|
364
|
+
<View style={styles.dividerContainer}>
|
|
365
|
+
<View style={[styles.divider, { backgroundColor: colors.border }]} />
|
|
366
|
+
<Text style={[styles.dividerText, { color: colors.secondaryText }]}>or</Text>
|
|
367
|
+
<View style={[styles.divider, { backgroundColor: colors.border }]} />
|
|
368
|
+
</View>
|
|
369
|
+
|
|
370
|
+
{/* Open Accounts Button */}
|
|
371
|
+
<TouchableOpacity
|
|
372
|
+
style={[styles.button, { backgroundColor: colors.primary }]}
|
|
373
|
+
onPress={handleOpenAccounts}
|
|
374
|
+
>
|
|
375
|
+
<OxyLogo width={20} height={20} fillColor="white" style={styles.buttonIcon} />
|
|
376
|
+
<Text style={styles.buttonText}>Open Oxy Accounts</Text>
|
|
377
|
+
</TouchableOpacity>
|
|
378
|
+
</>
|
|
379
|
+
)}
|
|
264
380
|
|
|
265
381
|
{/* Status */}
|
|
266
|
-
{
|
|
382
|
+
{isWaiting && (
|
|
267
383
|
<View style={styles.statusContainer}>
|
|
268
384
|
<ActivityIndicator size="small" color={colors.primary} />
|
|
269
385
|
<Text style={[styles.statusText, { color: colors.secondaryText }]}>
|