@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.
Files changed (30) hide show
  1. package/lib/commonjs/crypto/keyManager.js +11 -11
  2. package/lib/commonjs/crypto/keyManager.js.map +1 -1
  3. package/lib/commonjs/crypto/polyfill.js +48 -1
  4. package/lib/commonjs/crypto/polyfill.js.map +1 -1
  5. package/lib/commonjs/ui/components/TextField.js.map +1 -1
  6. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  7. package/lib/commonjs/ui/screens/OxyAuthScreen.js +168 -76
  8. package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
  9. package/lib/module/crypto/keyManager.js +11 -11
  10. package/lib/module/crypto/keyManager.js.map +1 -1
  11. package/lib/module/crypto/polyfill.js +48 -1
  12. package/lib/module/crypto/polyfill.js.map +1 -1
  13. package/lib/module/ui/components/TextField.js.map +1 -1
  14. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  15. package/lib/module/ui/screens/OxyAuthScreen.js +169 -77
  16. package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
  17. package/lib/typescript/core/OxyServices.d.ts +1 -1
  18. package/lib/typescript/core/OxyServices.d.ts.map +1 -1
  19. package/lib/typescript/crypto/polyfill.d.ts +4 -0
  20. package/lib/typescript/crypto/polyfill.d.ts.map +1 -1
  21. package/lib/typescript/ui/screens/OxyAuthScreen.d.ts +1 -0
  22. package/lib/typescript/ui/screens/OxyAuthScreen.d.ts.map +1 -1
  23. package/package.json +1 -2
  24. package/src/crypto/keyManager.ts +11 -11
  25. package/src/crypto/polyfill.ts +53 -1
  26. package/src/ui/components/TextField.tsx +2 -2
  27. package/src/ui/screens/AccountSettingsScreen.tsx +1 -1
  28. package/src/ui/screens/OxyAuthScreen.tsx +184 -68
  29. package/lib/typescript/types/expo-random.d.ts +0 -9
  30. 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 [isPolling, setIsPolling] = useState(false);
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
- startPolling(sessionToken);
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
- stopPolling();
251
+ cleanup();
141
252
  };
142
- }, [stopPolling]);
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
- stopPolling();
263
+ cleanup();
153
264
  setAuthSession(null);
154
265
  setError('Session expired. Please try again.');
155
266
  }
156
- }, [authSession, stopPolling]);
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
- stopPolling();
305
+ cleanup();
195
306
  generateAuthSession();
196
- }, [generateAuthSession, stopPolling]);
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
- <View style={styles.dividerContainer}>
251
- <View style={[styles.divider, { backgroundColor: colors.border }]} />
252
- <Text style={[styles.dividerText, { color: colors.secondaryText }]}>or</Text>
253
- <View style={[styles.divider, { backgroundColor: colors.border }]} />
254
- </View>
255
-
256
- {/* Open Accounts Button */}
257
- <TouchableOpacity
258
- style={[styles.button, { backgroundColor: colors.primary }]}
259
- onPress={handleOpenAccounts}
260
- >
261
- <OxyLogo width={20} height={20} fillColor="white" style={styles.buttonIcon} />
262
- <Text style={styles.buttonText}>Open Oxy Accounts</Text>
263
- </TouchableOpacity>
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
- {isPolling && (
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 }]}>
@@ -1,9 +0,0 @@
1
- declare module 'expo-random' {
2
- export interface RandomOptions {
3
- byteCount?: number;
4
- }
5
-
6
- export function getRandomBytes(byteCount: number): Uint8Array;
7
- export function getRandomBytesAsync(byteCount: number): Promise<Uint8Array>;
8
- }
9
-
@@ -1,9 +0,0 @@
1
- declare module 'expo-random' {
2
- export interface RandomOptions {
3
- byteCount?: number;
4
- }
5
-
6
- export function getRandomBytes(byteCount: number): Uint8Array;
7
- export function getRandomBytesAsync(byteCount: number): Promise<Uint8Array>;
8
- }
9
-