@onairos/react-native 3.0.20 → 3.0.22

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.
@@ -1,306 +1,366 @@
1
- import React, { useState, useCallback, forwardRef, useImperativeHandle } from 'react';
2
- import {
3
- TouchableOpacity,
4
- Text,
5
- StyleSheet,
6
- View,
7
- ViewStyle,
8
- TextStyle,
9
- ActivityIndicator,
10
- Image,
11
- } from 'react-native';
12
- import { UniversalOnboarding } from './UniversalOnboarding';
13
- import { Overlay } from './Overlay';
14
- import { COLORS } from '../constants';
15
- import type { OnairosButtonProps } from '../types';
16
- import { hasCredentials, getCredentials, deleteCredentials as clearCredentials } from '../utils/secureStorage';
17
- import { onairosApi } from '../api';
18
- import { Portal } from '../utils/Portal';
19
-
20
- export interface OnairosButtonRef {
21
- trigger: () => Promise<void>;
22
- reset: () => Promise<void>;
23
- }
24
-
25
- /**
26
- * OnairosButton Component - A sign-in button similar to Google/Apple sign-in
27
- */
28
- export const OnairosButton = forwardRef<OnairosButtonRef, OnairosButtonProps>(({
29
- returnLink,
30
- prefillUrl,
31
- AppName,
32
- buttonType = 'normal',
33
- requestData,
34
- buttonWidth = 180,
35
- buttonHeight = 48,
36
- hasStroke = false,
37
- enabled = true,
38
- buttonForm = 'default',
39
- onRejection,
40
- onResolved,
41
- preCheck,
42
- color,
43
- swerv = false,
44
- debug = false,
45
- darkMode = false,
46
- preferredPlatform,
47
- testMode = false,
48
- }, ref) => {
49
- const [showOnboarding, setShowOnboarding] = useState(false);
50
- const [showOverlay, setShowOverlay] = useState(false);
51
- const [storedCredentials, setStoredCredentials] = useState<any>(null);
52
- const [isLoading, setIsLoading] = useState(false);
53
- const [isPressed, setIsPressed] = useState(false);
54
-
55
- // Expose methods via ref
56
- useImperativeHandle(ref, () => ({
57
- trigger: async () => {
58
- await handlePress();
59
- },
60
- reset: async () => {
61
- await clearCredentials();
62
- }
63
- }));
64
-
65
- // Compute button text based on buttonForm
66
- const getButtonText = () => {
67
- if (buttonForm === 'connect' || buttonForm === 'signup') {
68
- return 'Connect with Onairos';
69
- } else {
70
- return 'Sign in with Onairos';
71
- }
72
- };
73
-
74
- // Calculate background color based on props and state
75
- const getBackgroundColor = (): string => {
76
- if (!enabled) {
77
- return darkMode ? '#3A3A3A' : '#e0e0e0';
78
- }
79
-
80
- if (isPressed) {
81
- return color ?
82
- (typeof color === 'string' ? `${color}80` : color) :
83
- (darkMode ? '#32323280' : '#f5f5f580');
84
- }
85
-
86
- return color || (darkMode ? '#2A2A2A' : '#ffffff');
87
- };
88
-
89
- // Calculate text color based on background luminance
90
- const getTextColor = (): string => {
91
- if (!enabled) {
92
- return darkMode ? '#777777' : '#AAAAAA';
93
- }
94
-
95
- if (darkMode) {
96
- return '#FFFFFF';
97
- }
98
-
99
- const bgColor = getBackgroundColor();
100
- // Simple luminance check
101
- return bgColor === '#ffffff' || bgColor === '#f5f5f580' || bgColor.includes('#f') ? '#000000' : '#FFFFFF';
102
- };
103
-
104
- const handlePress = async () => {
105
- if (!enabled || isLoading) return;
106
-
107
- setIsLoading(true);
108
-
109
- try {
110
- if (preCheck) {
111
- const shouldProceed = await preCheck();
112
- if (!shouldProceed) {
113
- onRejection?.('Precheck validation failed');
114
- setIsLoading(false);
115
- return;
116
- }
117
- }
118
-
119
- // Check if credentials exist
120
- const hasStoredCreds = await hasCredentials();
121
-
122
- if (hasStoredCreds) {
123
- // If credentials exist, fetch them and verify
124
- const credentials = await getCredentials();
125
-
126
- if (!credentials || !credentials.username) {
127
- // Invalid credentials, clear and start fresh
128
- await clearCredentials();
129
- setShowOnboarding(true);
130
- setIsLoading(false);
131
- return;
132
- }
133
-
134
- try {
135
- // Validate credentials with server
136
- const isValid = await onairosApi.validateCredentials(credentials.username);
137
-
138
- if (!isValid) {
139
- // Clear invalid credentials
140
- await clearCredentials();
141
- setShowOnboarding(true);
142
- setIsLoading(false);
143
- return;
144
- }
145
-
146
- // Store and display overlay with valid credentials
147
- setStoredCredentials(credentials);
148
- setShowOverlay(true);
149
- } catch (validationError) {
150
- console.warn('Validation error, proceeding to onboarding:', validationError);
151
- setShowOnboarding(true);
152
- }
153
- } else {
154
- // If no credentials, show onboarding
155
- setShowOnboarding(true);
156
- }
157
- } catch (error) {
158
- console.error('Error during button press flow:', error);
159
- // Fall back to onboarding on error
160
- setShowOnboarding(true);
161
- onRejection?.(error instanceof Error ? error.message : 'Unknown error');
162
- } finally {
163
- setIsLoading(false);
164
- }
165
- };
166
-
167
- const handleOnboardingComplete = useCallback((apiUrl: string, token: string, data: any) => {
168
- setShowOnboarding(false);
169
- if (onResolved) {
170
- onResolved(apiUrl, token, data);
171
- }
172
- }, [onResolved]);
173
-
174
- const handleOverlayResolved = useCallback((apiUrl: string, token: string, data: any) => {
175
- setShowOverlay(false);
176
- if (onResolved) {
177
- onResolved(apiUrl, token, data);
178
- }
179
- }, [onResolved]);
180
-
181
- // Calculate button styles based on props
182
- const buttonStyle: ViewStyle[] = [
183
- styles.button,
184
- buttonType === 'pill' && styles.pillButton,
185
- hasStroke && styles.strokedButton,
186
- {
187
- width: buttonWidth,
188
- height: buttonHeight,
189
- backgroundColor: getBackgroundColor(),
190
- borderColor: darkMode ? '#555555' : '#000000',
191
- },
192
- swerv && styles.swervButton,
193
- !enabled && styles.disabledButton
194
- ].filter(Boolean) as ViewStyle[];
195
-
196
- // Calculate text color
197
- const textColor = getTextColor();
198
-
199
- // Render components
200
- return (
201
- <>
202
- <TouchableOpacity
203
- style={buttonStyle}
204
- onPress={handlePress}
205
- disabled={!enabled || isLoading}
206
- accessibilityLabel={`Sign in with Onairos`}
207
- onPressIn={() => setIsPressed(true)}
208
- onPressOut={() => setIsPressed(false)}
209
- >
210
- {isLoading ? (
211
- <ActivityIndicator
212
- size="small"
213
- color={textColor}
214
- />
215
- ) : (
216
- <View style={styles.buttonContent}>
217
- <Image
218
- source={require('../assets/images/onairos_logo.png')}
219
- style={styles.logo}
220
- resizeMode="contain"
221
- />
222
- <Text style={[styles.buttonText, { color: textColor }]}>
223
- {getButtonText()}
224
- </Text>
225
- </View>
226
- )}
227
- </TouchableOpacity>
228
-
229
- {/* Overlay and Onboarding components rendered outside the button */}
230
- {showOnboarding && (
231
- <UniversalOnboarding
232
- visible={showOnboarding}
233
- onClose={() => {
234
- setShowOnboarding(false);
235
- onRejection?.('User closed onboarding');
236
- }}
237
- AppName={AppName}
238
- requestData={requestData as any}
239
- returnLink={returnLink || ''}
240
- onComplete={handleOnboardingComplete}
241
- debug={debug}
242
- test={testMode}
243
- preferredPlatform={preferredPlatform}
244
- />
245
- )}
246
-
247
- {/* Overlay rendered through Portal to ensure it appears at root level */}
248
- {showOverlay && storedCredentials && (
249
- <Portal>
250
- <Overlay
251
- data={requestData || {}}
252
- username={storedCredentials.username}
253
- modelKey={storedCredentials.userPin || ''}
254
- onResolved={handleOverlayResolved}
255
- appName={AppName}
256
- darkMode={darkMode}
257
- />
258
- </Portal>
259
- )}
260
- </>
261
- );
262
- });
263
-
264
- const styles = StyleSheet.create({
265
- button: {
266
- flexDirection: 'row',
267
- alignItems: 'center',
268
- justifyContent: 'center',
269
- paddingVertical: 12,
270
- paddingHorizontal: 16,
271
- borderRadius: 8,
272
- shadowColor: '#000',
273
- shadowOffset: { width: 0, height: 2 },
274
- shadowOpacity: 0.1,
275
- shadowRadius: 4,
276
- elevation: 2,
277
- },
278
- buttonContent: {
279
- flexDirection: 'row',
280
- alignItems: 'center',
281
- justifyContent: 'center',
282
- },
283
- logo: {
284
- width: 24,
285
- height: 24,
286
- marginRight: 8,
287
- },
288
- pillButton: {
289
- borderRadius: 24,
290
- },
291
- strokedButton: {
292
- backgroundColor: 'transparent',
293
- borderWidth: 1,
294
- },
295
- swervButton: {
296
- transform: [{ rotate: '-2deg' }],
297
- },
298
- disabledButton: {
299
- opacity: 0.6,
300
- },
301
- buttonText: {
302
- fontSize: 16,
303
- fontWeight: '600',
304
- textAlign: 'center',
305
- },
1
+ import React, { useState, useCallback, forwardRef, useImperativeHandle } from 'react';
2
+ import {
3
+ TouchableOpacity,
4
+ Text,
5
+ StyleSheet,
6
+ View,
7
+ ViewStyle,
8
+ TextStyle,
9
+ ActivityIndicator,
10
+ Image,
11
+ } from 'react-native';
12
+ import { UniversalOnboarding } from './UniversalOnboarding';
13
+ import { Overlay } from './Overlay';
14
+ import { COLORS } from '../constants';
15
+ import type { OnairosButtonProps } from '../types';
16
+ import { hasCredentials, getCredentials, deleteCredentials as clearCredentials } from '../utils/secureStorage';
17
+ import { onairosApi } from '../api';
18
+ import { Portal } from '../utils/Portal';
19
+
20
+ export interface OnairosButtonRef {
21
+ trigger: () => Promise<void>;
22
+ reset: () => Promise<void>;
23
+ }
24
+
25
+ /**
26
+ * OnairosButton Component - A sign-in button similar to Google/Apple sign-in
27
+ */
28
+ export const OnairosButton = forwardRef<OnairosButtonRef, OnairosButtonProps>(({
29
+ returnLink,
30
+ prefillUrl,
31
+ AppName,
32
+ buttonType = 'normal',
33
+ requestData,
34
+ buttonWidth = 180,
35
+ buttonHeight = 48,
36
+ hasStroke = false,
37
+ enabled = true,
38
+ buttonForm = 'default',
39
+ onRejection,
40
+ onResolved,
41
+ preCheck,
42
+ color,
43
+ swerv = false,
44
+ debug = false,
45
+ darkMode = false,
46
+ preferredPlatform,
47
+ testMode = false,
48
+ autoFetch = false,
49
+ inferenceData,
50
+ textLayout,
51
+ textColor: customTextColor,
52
+ proofMode = false,
53
+ webpageName,
54
+ }, ref) => {
55
+ const [showOnboarding, setShowOnboarding] = useState(false);
56
+ const [showOverlay, setShowOverlay] = useState(false);
57
+ const [storedCredentials, setStoredCredentials] = useState<any>(null);
58
+ const [isLoading, setIsLoading] = useState(false);
59
+ const [isPressed, setIsPressed] = useState(false);
60
+
61
+ // Expose methods via ref
62
+ useImperativeHandle(ref, () => ({
63
+ trigger: async () => {
64
+ await handlePress();
65
+ },
66
+ reset: async () => {
67
+ await clearCredentials();
68
+ }
69
+ }));
70
+
71
+ // Compute button text based on buttonForm
72
+ const getButtonText = () => {
73
+ if (buttonForm === 'connect' || buttonForm === 'signup') {
74
+ return 'Connect with Onairos';
75
+ } else {
76
+ return 'Sign in with Onairos';
77
+ }
78
+ };
79
+
80
+ // Calculate background color based on props and state
81
+ const getBackgroundColor = (): string => {
82
+ if (!enabled) {
83
+ return darkMode ? '#3A3A3A' : '#e0e0e0';
84
+ }
85
+
86
+ if (isPressed) {
87
+ return color ?
88
+ (typeof color === 'string' ? `${color}80` : color) :
89
+ (darkMode ? '#32323280' : '#f5f5f580');
90
+ }
91
+
92
+ return color || (darkMode ? '#2A2A2A' : '#ffffff');
93
+ };
94
+
95
+ // Calculate text color based on background luminance
96
+ const getTextColor = (): string => {
97
+ if (!enabled) {
98
+ return darkMode ? '#777777' : '#AAAAAA';
99
+ }
100
+
101
+ if (darkMode) {
102
+ return '#FFFFFF';
103
+ }
104
+
105
+ const bgColor = getBackgroundColor();
106
+ // Simple luminance check
107
+ return bgColor === '#ffffff' || bgColor === '#f5f5f580' || bgColor.includes('#f') ? '#000000' : '#FFFFFF';
108
+ };
109
+
110
+ const handlePress = async () => {
111
+ if (!enabled || isLoading) return;
112
+
113
+ setIsLoading(true);
114
+
115
+ try {
116
+ // If autoFetch is enabled, directly call the API with inferenceData
117
+ if (autoFetch) {
118
+ if (!inferenceData) {
119
+ throw new Error('inferenceData is required when autoFetch is enabled');
120
+ }
121
+
122
+ try {
123
+ // Check if credentials exist
124
+ const hasStoredCreds = await hasCredentials();
125
+
126
+ if (hasStoredCreds) {
127
+ const credentials = await getCredentials();
128
+
129
+ if (!credentials || !credentials.username) {
130
+ throw new Error('Invalid credentials');
131
+ }
132
+
133
+ // Call the API directly with inferenceData
134
+ // Using a custom API call for inference
135
+ const result = await fetch(`https://api.onairos.ai/inference`, {
136
+ method: 'POST',
137
+ headers: {
138
+ 'Content-Type': 'application/json',
139
+ 'Authorization': `Bearer ${credentials.userPin || ''}`
140
+ },
141
+ body: JSON.stringify({
142
+ username: credentials.username,
143
+ modelKey: credentials.userPin || '',
144
+ data: inferenceData,
145
+ requestData: requestData || {},
146
+ proofMode: proofMode,
147
+ webpageName: webpageName || AppName
148
+ })
149
+ }).then(res => res.json());
150
+
151
+ if (result && result.success) {
152
+ if (onResolved) {
153
+ onResolved(result.apiUrl || '', result.token || '', result.data || {});
154
+ }
155
+ setIsLoading(false);
156
+ return;
157
+ } else {
158
+ throw new Error(result?.error || 'API call failed');
159
+ }
160
+ } else {
161
+ // No credentials, proceed with normal flow
162
+ console.log('No credentials found, proceeding with normal flow');
163
+ }
164
+ } catch (autoFetchError) {
165
+ console.error('AutoFetch error:', autoFetchError);
166
+ // Fall back to normal flow on error
167
+ }
168
+ }
169
+
170
+ if (preCheck) {
171
+ const shouldProceed = await preCheck();
172
+ if (!shouldProceed) {
173
+ onRejection?.('Precheck validation failed');
174
+ setIsLoading(false);
175
+ return;
176
+ }
177
+ }
178
+
179
+ // Check if credentials exist
180
+ const hasStoredCreds = await hasCredentials();
181
+
182
+ if (hasStoredCreds) {
183
+ // If credentials exist, fetch them and verify
184
+ const credentials = await getCredentials();
185
+
186
+ if (!credentials || !credentials.username) {
187
+ // Invalid credentials, clear and start fresh
188
+ await clearCredentials();
189
+ setShowOnboarding(true);
190
+ setIsLoading(false);
191
+ return;
192
+ }
193
+
194
+ try {
195
+ // Validate credentials with server
196
+ const isValid = await onairosApi.validateCredentials(credentials.username);
197
+
198
+ if (!isValid) {
199
+ // Clear invalid credentials
200
+ await clearCredentials();
201
+ setShowOnboarding(true);
202
+ setIsLoading(false);
203
+ return;
204
+ }
205
+
206
+ // Store and display overlay with valid credentials
207
+ setStoredCredentials(credentials);
208
+ setShowOverlay(true);
209
+ } catch (validationError) {
210
+ console.warn('Validation error, proceeding to onboarding:', validationError);
211
+ setShowOnboarding(true);
212
+ }
213
+ } else {
214
+ // If no credentials, show onboarding
215
+ setShowOnboarding(true);
216
+ }
217
+ } catch (error) {
218
+ console.error('Error during button press flow:', error);
219
+ // Fall back to onboarding on error
220
+ setShowOnboarding(true);
221
+ onRejection?.(error instanceof Error ? error.message : 'Unknown error');
222
+ } finally {
223
+ setIsLoading(false);
224
+ }
225
+ };
226
+
227
+ const handleOnboardingComplete = useCallback((apiUrl: string, token: string, data: any) => {
228
+ setShowOnboarding(false);
229
+ if (onResolved) {
230
+ onResolved(apiUrl, token, data);
231
+ }
232
+ }, [onResolved]);
233
+
234
+ const handleOverlayResolved = useCallback((apiUrl: string, token: string, data: any) => {
235
+ setShowOverlay(false);
236
+ if (onResolved) {
237
+ onResolved(apiUrl, token, data);
238
+ }
239
+ }, [onResolved]);
240
+
241
+ // Calculate button styles based on props
242
+ const buttonStyle: ViewStyle[] = [
243
+ styles.button,
244
+ buttonType === 'pill' && styles.pillButton,
245
+ hasStroke && styles.strokedButton,
246
+ {
247
+ width: buttonWidth,
248
+ height: buttonHeight,
249
+ backgroundColor: getBackgroundColor(),
250
+ borderColor: darkMode ? '#555555' : '#000000',
251
+ },
252
+ swerv && styles.swervButton,
253
+ !enabled && styles.disabledButton
254
+ ].filter(Boolean) as ViewStyle[];
255
+
256
+ // Calculate button text color
257
+ const buttonTextColor = getTextColor();
258
+
259
+ // Render components
260
+ return (
261
+ <>
262
+ <TouchableOpacity
263
+ style={buttonStyle}
264
+ onPress={handlePress}
265
+ disabled={!enabled || isLoading}
266
+ accessibilityLabel={`Sign in with Onairos`}
267
+ onPressIn={() => setIsPressed(true)}
268
+ onPressOut={() => setIsPressed(false)}
269
+ >
270
+ {isLoading ? (
271
+ <ActivityIndicator
272
+ size="small"
273
+ color={buttonTextColor}
274
+ />
275
+ ) : (
276
+ <View style={styles.buttonContent}>
277
+ <Image
278
+ source={require('../assets/images/onairos_logo.png')}
279
+ style={styles.logo}
280
+ resizeMode="contain"
281
+ />
282
+ <Text style={[styles.buttonText, { color: buttonTextColor }]}>
283
+ {getButtonText()}
284
+ </Text>
285
+ </View>
286
+ )}
287
+ </TouchableOpacity>
288
+
289
+ {/* Overlay and Onboarding components rendered outside the button */}
290
+ {showOnboarding && (
291
+ <UniversalOnboarding
292
+ visible={showOnboarding}
293
+ onClose={() => {
294
+ setShowOnboarding(false);
295
+ onRejection?.('User closed onboarding');
296
+ }}
297
+ AppName={AppName}
298
+ requestData={requestData as any}
299
+ returnLink={returnLink || ''}
300
+ onComplete={handleOnboardingComplete}
301
+ debug={debug}
302
+ test={testMode}
303
+ preferredPlatform={preferredPlatform}
304
+ />
305
+ )}
306
+
307
+ {/* Overlay rendered through Portal to ensure it appears at root level */}
308
+ {showOverlay && storedCredentials && (
309
+ <Portal>
310
+ <Overlay
311
+ data={requestData || {}}
312
+ username={storedCredentials.username}
313
+ modelKey={storedCredentials.userPin || ''}
314
+ onResolved={handleOverlayResolved}
315
+ appName={AppName}
316
+ darkMode={darkMode}
317
+ />
318
+ </Portal>
319
+ )}
320
+ </>
321
+ );
322
+ });
323
+
324
+ const styles = StyleSheet.create({
325
+ button: {
326
+ flexDirection: 'row',
327
+ alignItems: 'center',
328
+ justifyContent: 'center',
329
+ paddingVertical: 12,
330
+ paddingHorizontal: 16,
331
+ borderRadius: 8,
332
+ shadowColor: '#000',
333
+ shadowOffset: { width: 0, height: 2 },
334
+ shadowOpacity: 0.1,
335
+ shadowRadius: 4,
336
+ elevation: 2,
337
+ },
338
+ buttonContent: {
339
+ flexDirection: 'row',
340
+ alignItems: 'center',
341
+ justifyContent: 'center',
342
+ },
343
+ logo: {
344
+ width: 24,
345
+ height: 24,
346
+ marginRight: 8,
347
+ },
348
+ pillButton: {
349
+ borderRadius: 24,
350
+ },
351
+ strokedButton: {
352
+ backgroundColor: 'transparent',
353
+ borderWidth: 1,
354
+ },
355
+ swervButton: {
356
+ transform: [{ rotate: '-2deg' }],
357
+ },
358
+ disabledButton: {
359
+ opacity: 0.6,
360
+ },
361
+ buttonText: {
362
+ fontSize: 16,
363
+ fontWeight: '600',
364
+ textAlign: 'center',
365
+ },
306
366
  });