@oxyhq/services 5.18.2 → 5.18.4

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 (157) hide show
  1. package/lib/commonjs/core/mixins/index.js +36 -13
  2. package/lib/commonjs/core/mixins/index.js.map +1 -1
  3. package/lib/commonjs/index.js +16 -0
  4. package/lib/commonjs/index.js.map +1 -1
  5. package/lib/commonjs/ui/client.js +177 -0
  6. package/lib/commonjs/ui/client.js.map +1 -0
  7. package/lib/commonjs/ui/components/WebOxyProvider.js +98 -0
  8. package/lib/commonjs/ui/components/WebOxyProvider.js.map +1 -0
  9. package/lib/commonjs/ui/components/profile/EditFieldModal.js +412 -0
  10. package/lib/commonjs/ui/components/profile/EditFieldModal.js.map +1 -0
  11. package/lib/commonjs/ui/context/OxyContext.js +63 -1
  12. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  13. package/lib/commonjs/ui/hooks/useAuth.js +115 -0
  14. package/lib/commonjs/ui/hooks/useAuth.js.map +1 -0
  15. package/lib/commonjs/ui/hooks/useSettingToggle.js +7 -1
  16. package/lib/commonjs/ui/hooks/useSettingToggle.js.map +1 -1
  17. package/lib/commonjs/ui/hooks/useWebSSO.js +75 -0
  18. package/lib/commonjs/ui/hooks/useWebSSO.js.map +1 -0
  19. package/lib/commonjs/ui/index.js +17 -2
  20. package/lib/commonjs/ui/index.js.map +1 -1
  21. package/lib/commonjs/ui/screens/PrivacySettingsScreen.js +59 -65
  22. package/lib/commonjs/ui/screens/PrivacySettingsScreen.js.map +1 -1
  23. package/lib/commonjs/ui/screens/SearchSettingsScreen.js +38 -58
  24. package/lib/commonjs/ui/screens/SearchSettingsScreen.js.map +1 -1
  25. package/lib/commonjs/ui/server.js +105 -0
  26. package/lib/commonjs/ui/server.js.map +1 -0
  27. package/lib/commonjs/ui/utils/iconNames.js +133 -0
  28. package/lib/commonjs/ui/utils/iconNames.js.map +1 -0
  29. package/lib/commonjs/ui/utils/sessionHelpers.js +7 -0
  30. package/lib/commonjs/ui/utils/sessionHelpers.js.map +1 -1
  31. package/lib/commonjs/utils/requestUtils.js +4 -3
  32. package/lib/commonjs/utils/requestUtils.js.map +1 -1
  33. package/lib/module/core/mixins/index.js +36 -13
  34. package/lib/module/core/mixins/index.js.map +1 -1
  35. package/lib/module/index.js +5 -2
  36. package/lib/module/index.js.map +1 -1
  37. package/lib/module/ui/client.js +48 -0
  38. package/lib/module/ui/client.js.map +1 -0
  39. package/lib/module/ui/components/WebOxyProvider.js +94 -0
  40. package/lib/module/ui/components/WebOxyProvider.js.map +1 -0
  41. package/lib/module/ui/components/profile/EditFieldModal.js +406 -0
  42. package/lib/module/ui/components/profile/EditFieldModal.js.map +1 -0
  43. package/lib/module/ui/context/OxyContext.js +63 -1
  44. package/lib/module/ui/context/OxyContext.js.map +1 -1
  45. package/lib/module/ui/hooks/useAuth.js +106 -0
  46. package/lib/module/ui/hooks/useAuth.js.map +1 -0
  47. package/lib/module/ui/hooks/useSettingToggle.js +7 -1
  48. package/lib/module/ui/hooks/useSettingToggle.js.map +1 -1
  49. package/lib/module/ui/hooks/useWebSSO.js +71 -0
  50. package/lib/module/ui/hooks/useWebSSO.js.map +1 -0
  51. package/lib/module/ui/index.js +17 -3
  52. package/lib/module/ui/index.js.map +1 -1
  53. package/lib/module/ui/screens/PrivacySettingsScreen.js +59 -65
  54. package/lib/module/ui/screens/PrivacySettingsScreen.js.map +1 -1
  55. package/lib/module/ui/screens/SearchSettingsScreen.js +39 -59
  56. package/lib/module/ui/screens/SearchSettingsScreen.js.map +1 -1
  57. package/lib/module/ui/server.js +65 -0
  58. package/lib/module/ui/server.js.map +1 -0
  59. package/lib/module/ui/utils/iconNames.js +124 -0
  60. package/lib/module/ui/utils/iconNames.js.map +1 -0
  61. package/lib/module/ui/utils/sessionHelpers.js +7 -0
  62. package/lib/module/ui/utils/sessionHelpers.js.map +1 -1
  63. package/lib/module/utils/requestUtils.js +4 -2
  64. package/lib/module/utils/requestUtils.js.map +1 -1
  65. package/lib/typescript/commonjs/core/mixins/index.d.ts +18 -1115
  66. package/lib/typescript/commonjs/core/mixins/index.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/index.d.ts +3 -0
  68. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/ui/client.d.ts +34 -0
  70. package/lib/typescript/commonjs/ui/client.d.ts.map +1 -0
  71. package/lib/typescript/commonjs/ui/components/WebOxyProvider.d.ts +44 -0
  72. package/lib/typescript/commonjs/ui/components/WebOxyProvider.d.ts.map +1 -0
  73. package/lib/typescript/commonjs/ui/components/profile/EditFieldModal.d.ts +110 -0
  74. package/lib/typescript/commonjs/ui/components/profile/EditFieldModal.d.ts.map +1 -0
  75. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +3 -0
  76. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +3 -3
  78. package/lib/typescript/commonjs/ui/hooks/queries/useAccountQueries.d.ts +6 -10
  79. package/lib/typescript/commonjs/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/ui/hooks/queries/useSecurityQueries.d.ts +1 -1
  81. package/lib/typescript/commonjs/ui/hooks/queries/useSecurityQueries.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/ui/hooks/queries/useServicesQueries.d.ts +3 -5
  83. package/lib/typescript/commonjs/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/ui/hooks/useAssets.d.ts +1 -1
  85. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts +69 -0
  86. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -0
  87. package/lib/typescript/commonjs/ui/hooks/useSettingToggle.d.ts +4 -2
  88. package/lib/typescript/commonjs/ui/hooks/useSettingToggle.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts +34 -0
  90. package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts.map +1 -0
  91. package/lib/typescript/commonjs/ui/index.d.ts +2 -2
  92. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  93. package/lib/typescript/commonjs/ui/screens/PrivacySettingsScreen.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/ui/screens/SearchSettingsScreen.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/ui/server.d.ts +43 -0
  96. package/lib/typescript/commonjs/ui/server.d.ts.map +1 -0
  97. package/lib/typescript/commonjs/ui/utils/iconNames.d.ts +112 -0
  98. package/lib/typescript/commonjs/ui/utils/iconNames.d.ts.map +1 -0
  99. package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts +8 -3
  100. package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts.map +1 -1
  101. package/lib/typescript/commonjs/utils/requestUtils.d.ts +3 -1
  102. package/lib/typescript/commonjs/utils/requestUtils.d.ts.map +1 -1
  103. package/lib/typescript/module/core/mixins/index.d.ts +18 -1115
  104. package/lib/typescript/module/core/mixins/index.d.ts.map +1 -1
  105. package/lib/typescript/module/index.d.ts +3 -0
  106. package/lib/typescript/module/index.d.ts.map +1 -1
  107. package/lib/typescript/module/ui/client.d.ts +34 -0
  108. package/lib/typescript/module/ui/client.d.ts.map +1 -0
  109. package/lib/typescript/module/ui/components/WebOxyProvider.d.ts +44 -0
  110. package/lib/typescript/module/ui/components/WebOxyProvider.d.ts.map +1 -0
  111. package/lib/typescript/module/ui/components/profile/EditFieldModal.d.ts +110 -0
  112. package/lib/typescript/module/ui/components/profile/EditFieldModal.d.ts.map +1 -0
  113. package/lib/typescript/module/ui/context/OxyContext.d.ts +3 -0
  114. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  115. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +3 -3
  116. package/lib/typescript/module/ui/hooks/queries/useAccountQueries.d.ts +6 -10
  117. package/lib/typescript/module/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
  118. package/lib/typescript/module/ui/hooks/queries/useSecurityQueries.d.ts +1 -1
  119. package/lib/typescript/module/ui/hooks/queries/useSecurityQueries.d.ts.map +1 -1
  120. package/lib/typescript/module/ui/hooks/queries/useServicesQueries.d.ts +3 -5
  121. package/lib/typescript/module/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
  122. package/lib/typescript/module/ui/hooks/useAssets.d.ts +1 -1
  123. package/lib/typescript/module/ui/hooks/useAuth.d.ts +69 -0
  124. package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -0
  125. package/lib/typescript/module/ui/hooks/useSettingToggle.d.ts +4 -2
  126. package/lib/typescript/module/ui/hooks/useSettingToggle.d.ts.map +1 -1
  127. package/lib/typescript/module/ui/hooks/useWebSSO.d.ts +34 -0
  128. package/lib/typescript/module/ui/hooks/useWebSSO.d.ts.map +1 -0
  129. package/lib/typescript/module/ui/index.d.ts +2 -2
  130. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  131. package/lib/typescript/module/ui/screens/PrivacySettingsScreen.d.ts.map +1 -1
  132. package/lib/typescript/module/ui/screens/SearchSettingsScreen.d.ts.map +1 -1
  133. package/lib/typescript/module/ui/server.d.ts +43 -0
  134. package/lib/typescript/module/ui/server.d.ts.map +1 -0
  135. package/lib/typescript/module/ui/utils/iconNames.d.ts +112 -0
  136. package/lib/typescript/module/ui/utils/iconNames.d.ts.map +1 -0
  137. package/lib/typescript/module/ui/utils/sessionHelpers.d.ts +8 -3
  138. package/lib/typescript/module/ui/utils/sessionHelpers.d.ts.map +1 -1
  139. package/lib/typescript/module/utils/requestUtils.d.ts +3 -1
  140. package/lib/typescript/module/utils/requestUtils.d.ts.map +1 -1
  141. package/package.json +1 -1
  142. package/src/core/mixins/index.ts +57 -43
  143. package/src/index.ts +6 -1
  144. package/src/ui/client.ts +56 -0
  145. package/src/ui/components/WebOxyProvider.tsx +109 -0
  146. package/src/ui/components/profile/EditFieldModal.tsx +465 -0
  147. package/src/ui/context/OxyContext.tsx +69 -0
  148. package/src/ui/hooks/useAuth.ts +159 -0
  149. package/src/ui/hooks/useSettingToggle.ts +7 -3
  150. package/src/ui/hooks/useWebSSO.ts +93 -0
  151. package/src/ui/index.ts +17 -2
  152. package/src/ui/screens/PrivacySettingsScreen.tsx +54 -63
  153. package/src/ui/screens/SearchSettingsScreen.tsx +42 -64
  154. package/src/ui/server.ts +70 -0
  155. package/src/ui/utils/iconNames.ts +136 -0
  156. package/src/ui/utils/sessionHelpers.ts +10 -3
  157. package/src/utils/requestUtils.ts +10 -7
@@ -0,0 +1,465 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ TextInput,
6
+ TouchableOpacity,
7
+ StyleSheet,
8
+ Modal,
9
+ Platform,
10
+ ScrollView,
11
+ TextInputProps,
12
+ } from 'react-native';
13
+ import { Ionicons } from '@expo/vector-icons';
14
+ import { useThemeStyles } from '../../hooks/useThemeStyles';
15
+ import { useColorScheme } from '../../hooks/use-color-scheme';
16
+ import { useI18n } from '../../hooks/useI18n';
17
+ import { fontFamilies } from '../../styles/fonts';
18
+ import { useProfileEditing } from '../../hooks/useProfileEditing';
19
+
20
+ /**
21
+ * Field configuration for single/multi field variants
22
+ */
23
+ export interface FieldConfig {
24
+ /** Unique key for the field */
25
+ key: string;
26
+ /** Label displayed above the input */
27
+ label: string;
28
+ /** Initial value for the field */
29
+ initialValue: string;
30
+ /** Placeholder text */
31
+ placeholder?: string;
32
+ /** Validation function - returns error message or undefined */
33
+ validation?: (value: string) => string | undefined;
34
+ /** Additional TextInput props (multiline, keyboardType, etc.) */
35
+ inputProps?: Partial<TextInputProps>;
36
+ }
37
+
38
+ /**
39
+ * List item for list variant
40
+ */
41
+ export interface ListItem {
42
+ id: string;
43
+ [key: string]: unknown;
44
+ }
45
+
46
+ /**
47
+ * Configuration for list variant
48
+ */
49
+ export interface ListConfig<T extends ListItem> {
50
+ /** Initial items */
51
+ items: T[];
52
+ /** Render function for each item */
53
+ renderItem: (item: T, onRemove: () => void, colors: Record<string, string>) => React.ReactNode;
54
+ /** Placeholder for add input */
55
+ addItemPlaceholder: string;
56
+ /** Label for add input section */
57
+ addItemLabel?: string;
58
+ /** Function to create a new item from input value */
59
+ createItem: (value: string) => T;
60
+ /** List title shown above items */
61
+ listTitle?: string;
62
+ }
63
+
64
+ /**
65
+ * Props for EditFieldModal
66
+ */
67
+ export interface EditFieldModalProps<T extends ListItem = ListItem> {
68
+ /** Whether the modal is visible */
69
+ visible: boolean;
70
+ /** Close handler */
71
+ onClose: () => void;
72
+ /** Modal title */
73
+ title: string;
74
+ /** Theme override */
75
+ theme?: 'light' | 'dark';
76
+ /** Called after successful save */
77
+ onSave?: () => void;
78
+
79
+ /** Modal variant: single input, multiple inputs, or list management */
80
+ variant: 'single' | 'multi' | 'list';
81
+
82
+ /** Field configuration for single/multi variants */
83
+ fields?: FieldConfig[];
84
+
85
+ /** List configuration for list variant */
86
+ listConfig?: ListConfig<T>;
87
+
88
+ /** Custom submit handler - receives field values or list items */
89
+ onSubmit: (data: Record<string, unknown>) => Promise<boolean>;
90
+
91
+ /** Whether save button should be disabled */
92
+ saveDisabled?: boolean;
93
+ }
94
+
95
+ /**
96
+ * Generic modal component for editing profile fields.
97
+ *
98
+ * Supports three variants:
99
+ * - single: Single text input (username, email, bio)
100
+ * - multi: Multiple text inputs (display name with first/last)
101
+ * - list: Add/remove list items (links, locations)
102
+ *
103
+ * @example
104
+ * // Single field (bio)
105
+ * <EditFieldModal
106
+ * visible={showBioModal}
107
+ * onClose={() => setShowBioModal(false)}
108
+ * title="Bio"
109
+ * variant="single"
110
+ * fields={[{
111
+ * key: 'bio',
112
+ * label: 'Bio',
113
+ * initialValue: user.bio,
114
+ * placeholder: 'Tell people about yourself...',
115
+ * inputProps: { multiline: true, numberOfLines: 6 }
116
+ * }]}
117
+ * onSubmit={async (data) => updateField('bio', data.bio)}
118
+ * />
119
+ *
120
+ * @example
121
+ * // Multi field (display name)
122
+ * <EditFieldModal
123
+ * visible={showNameModal}
124
+ * onClose={() => setShowNameModal(false)}
125
+ * title="Display Name"
126
+ * variant="multi"
127
+ * fields={[
128
+ * { key: 'firstName', label: 'First Name', initialValue: user.name?.first },
129
+ * { key: 'lastName', label: 'Last Name', initialValue: user.name?.last }
130
+ * ]}
131
+ * onSubmit={async (data) => saveProfile({ displayName: data.firstName, lastName: data.lastName })}
132
+ * />
133
+ */
134
+ export function EditFieldModal<T extends ListItem = ListItem>({
135
+ visible,
136
+ onClose,
137
+ title,
138
+ theme = 'light',
139
+ onSave,
140
+ variant,
141
+ fields = [],
142
+ listConfig,
143
+ onSubmit,
144
+ saveDisabled = false,
145
+ }: EditFieldModalProps<T>): React.ReactElement {
146
+ const { t } = useI18n();
147
+ const colorScheme = useColorScheme();
148
+ const themeStyles = useThemeStyles(theme || 'light', colorScheme);
149
+ const colors = themeStyles.colors;
150
+ const { isSaving } = useProfileEditing();
151
+
152
+ // State for field values (single/multi variants)
153
+ const [fieldValues, setFieldValues] = useState<Record<string, string>>({});
154
+ const [fieldErrors, setFieldErrors] = useState<Record<string, string | undefined>>({});
155
+
156
+ // State for list items (list variant)
157
+ const [listItems, setListItems] = useState<T[]>([]);
158
+ const [newItemValue, setNewItemValue] = useState('');
159
+
160
+ // Initialize field values when modal opens
161
+ useEffect(() => {
162
+ if (visible) {
163
+ if (variant === 'list' && listConfig) {
164
+ setListItems(listConfig.items);
165
+ setNewItemValue('');
166
+ } else if (fields.length > 0) {
167
+ const initialValues: Record<string, string> = {};
168
+ fields.forEach(field => {
169
+ initialValues[field.key] = field.initialValue || '';
170
+ });
171
+ setFieldValues(initialValues);
172
+ setFieldErrors({});
173
+ }
174
+ }
175
+ }, [visible, variant, fields, listConfig]);
176
+
177
+ // Field change handler with validation
178
+ const handleFieldChange = useCallback((key: string, value: string) => {
179
+ setFieldValues(prev => ({ ...prev, [key]: value }));
180
+
181
+ // Clear error on change
182
+ if (fieldErrors[key]) {
183
+ setFieldErrors(prev => ({ ...prev, [key]: undefined }));
184
+ }
185
+ }, [fieldErrors]);
186
+
187
+ // Validate all fields
188
+ const validateFields = useCallback((): boolean => {
189
+ const errors: Record<string, string | undefined> = {};
190
+ let isValid = true;
191
+
192
+ fields.forEach(field => {
193
+ if (field.validation) {
194
+ const error = field.validation(fieldValues[field.key] || '');
195
+ if (error) {
196
+ errors[field.key] = error;
197
+ isValid = false;
198
+ }
199
+ }
200
+ });
201
+
202
+ setFieldErrors(errors);
203
+ return isValid;
204
+ }, [fields, fieldValues]);
205
+
206
+ // Add item to list
207
+ const handleAddItem = useCallback(() => {
208
+ if (!newItemValue.trim() || !listConfig) return;
209
+
210
+ const newItem = listConfig.createItem(newItemValue.trim());
211
+ setListItems(prev => [...prev, newItem]);
212
+ setNewItemValue('');
213
+ }, [newItemValue, listConfig]);
214
+
215
+ // Remove item from list
216
+ const handleRemoveItem = useCallback((id: string) => {
217
+ setListItems(prev => prev.filter(item => item.id !== id));
218
+ }, []);
219
+
220
+ // Save handler
221
+ const handleSave = async () => {
222
+ if (variant === 'list') {
223
+ const success = await onSubmit({ items: listItems });
224
+ if (success) {
225
+ onSave?.();
226
+ onClose();
227
+ }
228
+ } else {
229
+ if (!validateFields()) return;
230
+
231
+ const success = await onSubmit(fieldValues);
232
+ if (success) {
233
+ onSave?.();
234
+ onClose();
235
+ }
236
+ }
237
+ };
238
+
239
+ // Render field inputs for single/multi variants
240
+ const renderFields = () => (
241
+ <View style={styles.modalBody}>
242
+ {fields.map((field, index) => (
243
+ <View key={field.key} style={styles.inputGroup}>
244
+ <Text style={[styles.label, { color: colors.text }]}>
245
+ {field.label}
246
+ </Text>
247
+ <TextInput
248
+ style={[
249
+ field.inputProps?.multiline ? styles.textArea : styles.input,
250
+ {
251
+ backgroundColor: colors.card,
252
+ color: colors.text,
253
+ borderColor: fieldErrors[field.key] ? '#FF3B30' : colors.border,
254
+ },
255
+ ]}
256
+ value={fieldValues[field.key] || ''}
257
+ onChangeText={(value) => handleFieldChange(field.key, value)}
258
+ placeholder={field.placeholder}
259
+ placeholderTextColor={colors.secondaryText}
260
+ autoFocus={index === 0}
261
+ selectionColor={colors.tint}
262
+ textAlignVertical={field.inputProps?.multiline ? 'top' : 'center'}
263
+ {...field.inputProps}
264
+ />
265
+ {fieldErrors[field.key] && (
266
+ <Text style={styles.errorText}>{fieldErrors[field.key]}</Text>
267
+ )}
268
+ </View>
269
+ ))}
270
+ </View>
271
+ );
272
+
273
+ // Render list for list variant
274
+ const renderList = () => {
275
+ if (!listConfig) return null;
276
+
277
+ return (
278
+ <ScrollView style={styles.modalBody}>
279
+ <View style={styles.inputGroup}>
280
+ <Text style={[styles.label, { color: colors.text }]}>
281
+ {listConfig.addItemLabel || t('common.add') || 'Add'}
282
+ </Text>
283
+ <View style={styles.addItemRow}>
284
+ <TextInput
285
+ style={[
286
+ styles.input,
287
+ {
288
+ backgroundColor: colors.card,
289
+ color: colors.text,
290
+ borderColor: colors.border,
291
+ flex: 1,
292
+ },
293
+ ]}
294
+ value={newItemValue}
295
+ onChangeText={setNewItemValue}
296
+ placeholder={listConfig.addItemPlaceholder}
297
+ placeholderTextColor={colors.secondaryText}
298
+ selectionColor={colors.tint}
299
+ autoCapitalize="none"
300
+ autoCorrect={false}
301
+ />
302
+ <TouchableOpacity
303
+ style={[styles.addButton, { backgroundColor: colors.tint }]}
304
+ onPress={handleAddItem}
305
+ disabled={!newItemValue.trim()}
306
+ >
307
+ <Ionicons name="add" size={20} color="#fff" />
308
+ </TouchableOpacity>
309
+ </View>
310
+ </View>
311
+
312
+ {listItems.length > 0 && (
313
+ <View style={styles.listSection}>
314
+ {listConfig.listTitle && (
315
+ <Text style={[styles.listTitle, { color: colors.text }]}>
316
+ {listConfig.listTitle} ({listItems.length})
317
+ </Text>
318
+ )}
319
+ {listItems.map((item) => (
320
+ <View key={item.id}>
321
+ {listConfig.renderItem(
322
+ item,
323
+ () => handleRemoveItem(item.id),
324
+ colors
325
+ )}
326
+ </View>
327
+ ))}
328
+ </View>
329
+ )}
330
+ </ScrollView>
331
+ );
332
+ };
333
+
334
+ return (
335
+ <Modal
336
+ visible={visible}
337
+ animationType="slide"
338
+ transparent={true}
339
+ onRequestClose={onClose}
340
+ >
341
+ <View style={styles.modalOverlay}>
342
+ <View style={[styles.modalContent, { backgroundColor: colors.background }]}>
343
+ <View style={styles.modalHeader}>
344
+ <TouchableOpacity onPress={onClose} style={styles.closeButton}>
345
+ <Ionicons name="close" size={24} color={colors.text} />
346
+ </TouchableOpacity>
347
+ <Text style={[styles.modalTitle, { color: colors.text }]}>
348
+ {title}
349
+ </Text>
350
+ <TouchableOpacity
351
+ onPress={handleSave}
352
+ disabled={isSaving || saveDisabled}
353
+ style={[styles.saveButton, { opacity: (isSaving || saveDisabled) ? 0.5 : 1 }]}
354
+ >
355
+ <Text style={[styles.saveButtonText, { color: colors.tint }]}>
356
+ {isSaving ? (t('common.saving') || 'Saving...') : (t('common.save') || 'Save')}
357
+ </Text>
358
+ </TouchableOpacity>
359
+ </View>
360
+
361
+ {variant === 'list' ? renderList() : renderFields()}
362
+ </View>
363
+ </View>
364
+ </Modal>
365
+ );
366
+ }
367
+
368
+ const styles = StyleSheet.create({
369
+ modalOverlay: {
370
+ flex: 1,
371
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
372
+ justifyContent: 'flex-end',
373
+ },
374
+ modalContent: {
375
+ borderTopLeftRadius: 20,
376
+ borderTopRightRadius: 20,
377
+ paddingTop: Platform.OS === 'ios' ? 20 : 16,
378
+ maxHeight: '80%',
379
+ },
380
+ modalHeader: {
381
+ flexDirection: 'row',
382
+ alignItems: 'center',
383
+ justifyContent: 'space-between',
384
+ paddingHorizontal: 16,
385
+ paddingBottom: 16,
386
+ borderBottomWidth: StyleSheet.hairlineWidth,
387
+ borderBottomColor: '#E5E5EA',
388
+ },
389
+ closeButton: {
390
+ width: 40,
391
+ height: 40,
392
+ alignItems: 'center',
393
+ justifyContent: 'center',
394
+ },
395
+ modalTitle: {
396
+ fontSize: 18,
397
+ fontWeight: '600',
398
+ fontFamily: fontFamilies.phuduSemiBold,
399
+ flex: 1,
400
+ textAlign: 'center',
401
+ },
402
+ saveButton: {
403
+ paddingHorizontal: 16,
404
+ paddingVertical: 8,
405
+ },
406
+ saveButtonText: {
407
+ fontSize: 16,
408
+ fontWeight: '600',
409
+ fontFamily: fontFamilies.phuduSemiBold,
410
+ },
411
+ modalBody: {
412
+ padding: 16,
413
+ gap: 16,
414
+ },
415
+ inputGroup: {
416
+ gap: 8,
417
+ },
418
+ label: {
419
+ fontSize: 14,
420
+ fontWeight: '600',
421
+ fontFamily: fontFamilies.phuduSemiBold,
422
+ },
423
+ input: {
424
+ borderWidth: StyleSheet.hairlineWidth,
425
+ borderRadius: 12,
426
+ padding: 16,
427
+ fontSize: 16,
428
+ minHeight: 52,
429
+ },
430
+ textArea: {
431
+ borderWidth: StyleSheet.hairlineWidth,
432
+ borderRadius: 12,
433
+ padding: 16,
434
+ fontSize: 16,
435
+ minHeight: 120,
436
+ },
437
+ errorText: {
438
+ fontSize: 12,
439
+ color: '#FF3B30',
440
+ },
441
+ addItemRow: {
442
+ flexDirection: 'row',
443
+ gap: 8,
444
+ alignItems: 'center',
445
+ },
446
+ addButton: {
447
+ width: 52,
448
+ height: 52,
449
+ borderRadius: 12,
450
+ alignItems: 'center',
451
+ justifyContent: 'center',
452
+ },
453
+ listSection: {
454
+ gap: 8,
455
+ marginTop: 16,
456
+ },
457
+ listTitle: {
458
+ fontSize: 16,
459
+ fontWeight: '600',
460
+ fontFamily: fontFamilies.phuduSemiBold,
461
+ marginBottom: 8,
462
+ },
463
+ });
464
+
465
+ export default EditFieldModal;
@@ -11,6 +11,7 @@ import {
11
11
  } from 'react';
12
12
  import { OxyServices } from '../../core';
13
13
  import type { User, ApiError } from '../../models/interfaces';
14
+ import { KeyManager } from '../../crypto/keyManager';
14
15
  import type { ClientSession } from '../../models/session';
15
16
  import { toast } from '../../lib/sonner';
16
17
  import { useAuthStore, type AuthState } from '../stores/authStore';
@@ -31,6 +32,7 @@ import { translate } from '../../i18n';
31
32
  import { updateAvatarVisibility, updateProfileWithAvatar } from '../utils/avatarUtils';
32
33
  import { useAccountStore } from '../stores/accountStore';
33
34
  import { logger as loggerUtil } from '../../utils/loggerUtils';
35
+ import { useWebSSO, isWebBrowser } from '../hooks/useWebSSO';
34
36
 
35
37
  export interface OxyContextState {
36
38
  user: User | null;
@@ -39,12 +41,17 @@ export interface OxyContextState {
39
41
  isAuthenticated: boolean;
40
42
  isLoading: boolean;
41
43
  isTokenReady: boolean;
44
+ isStorageReady: boolean;
42
45
  error: string | null;
43
46
  currentLanguage: string;
44
47
  currentLanguageMetadata: ReturnType<typeof useLanguageManagement>['metadata'];
45
48
  currentLanguageName: string;
46
49
  currentNativeLanguageName: string;
47
50
 
51
+ // Identity (cryptographic key pair)
52
+ hasIdentity: () => Promise<boolean>;
53
+ getPublicKey: () => Promise<string | null>;
54
+
48
55
  // Authentication
49
56
  signIn: (publicKey: string, deviceName?: string) => Promise<User>;
50
57
 
@@ -406,6 +413,53 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
406
413
  void restoreSessionsFromStorage();
407
414
  }, [restoreSessionsFromStorage, storage]);
408
415
 
416
+ // Web SSO: Automatically check for cross-domain session on web platforms
417
+ const handleWebSSOSession = useCallback(async (session: any) => {
418
+ if (!session?.user || !session?.sessionId) {
419
+ return;
420
+ }
421
+
422
+ // Update sessions state
423
+ const clientSession = {
424
+ sessionId: session.sessionId,
425
+ deviceId: session.deviceId || '',
426
+ expiresAt: session.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
427
+ lastActive: new Date().toISOString(),
428
+ userId: session.user.id?.toString() ?? '',
429
+ isCurrent: true,
430
+ };
431
+
432
+ updateSessions([clientSession], { merge: true });
433
+ setActiveSessionId(session.sessionId);
434
+ loginSuccess(session.user);
435
+ onAuthStateChange?.(session.user);
436
+
437
+ // Persist to storage
438
+ if (storage) {
439
+ await storage.setItem(storageKeys.activeSessionId, session.sessionId);
440
+ const existingIds = await storage.getItem(storageKeys.sessionIds);
441
+ const sessionIds = existingIds ? JSON.parse(existingIds) : [];
442
+ if (!sessionIds.includes(session.sessionId)) {
443
+ sessionIds.push(session.sessionId);
444
+ await storage.setItem(storageKeys.sessionIds, JSON.stringify(sessionIds));
445
+ }
446
+ }
447
+ }, [updateSessions, setActiveSessionId, loginSuccess, onAuthStateChange, storage, storageKeys]);
448
+
449
+ // Enable web SSO only after local storage check completes and no user found
450
+ const shouldTryWebSSO = isWebBrowser() && tokenReady && !user && initializedRef.current;
451
+
452
+ useWebSSO({
453
+ oxyServices,
454
+ onSessionFound: handleWebSSOSession,
455
+ onError: (error) => {
456
+ if (__DEV__) {
457
+ loggerUtil.debug('Web SSO check failed (non-critical)', { component: 'OxyContext' }, error);
458
+ }
459
+ },
460
+ enabled: shouldTryWebSSO,
461
+ });
462
+
409
463
  const activeSession = activeSessionId
410
464
  ? sessions.find((session) => session.sessionId === activeSessionId)
411
465
  : undefined;
@@ -449,6 +503,15 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
449
503
  [switchSession],
450
504
  );
451
505
 
506
+ // Identity management wrappers (delegate to KeyManager)
507
+ const hasIdentity = useCallback(async (): Promise<boolean> => {
508
+ return KeyManager.hasIdentity();
509
+ }, []);
510
+
511
+ const getPublicKey = useCallback(async (): Promise<string | null> => {
512
+ return KeyManager.getPublicKey();
513
+ }, []);
514
+
452
515
  // Create showBottomSheet function that uses the global function
453
516
  const showBottomSheetForContext = useCallback(
454
517
  (screenOrConfig: RouteName | { screen: RouteName; props?: Record<string, unknown> }) => {
@@ -499,11 +562,14 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
499
562
  isAuthenticated,
500
563
  isLoading,
501
564
  isTokenReady: tokenReady,
565
+ isStorageReady: storage !== null,
502
566
  error,
503
567
  currentLanguage,
504
568
  currentLanguageMetadata,
505
569
  currentLanguageName,
506
570
  currentNativeLanguageName,
571
+ hasIdentity,
572
+ getPublicKey,
507
573
  signIn,
508
574
  logout,
509
575
  logoutAll,
@@ -529,6 +595,8 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
529
595
  currentNativeLanguageName,
530
596
  error,
531
597
  getDeviceSessions,
598
+ getPublicKey,
599
+ hasIdentity,
532
600
  isAuthenticated,
533
601
  isLoading,
534
602
  logout,
@@ -538,6 +606,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
538
606
  refreshSessionsWithUser,
539
607
  sessions,
540
608
  setLanguage,
609
+ storage,
541
610
  switchSessionForContext,
542
611
  tokenReady,
543
612
  updateDeviceName,