@oxyhq/services 5.18.2 → 5.18.3
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/core/mixins/index.js +36 -13
- package/lib/commonjs/core/mixins/index.js.map +1 -1
- package/lib/commonjs/index.js +8 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/client.js +170 -0
- package/lib/commonjs/ui/client.js.map +1 -0
- package/lib/commonjs/ui/components/profile/EditFieldModal.js +412 -0
- package/lib/commonjs/ui/components/profile/EditFieldModal.js.map +1 -0
- package/lib/commonjs/ui/context/OxyContext.js +63 -1
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAuth.js +115 -0
- package/lib/commonjs/ui/hooks/useAuth.js.map +1 -0
- package/lib/commonjs/ui/hooks/useSettingToggle.js +7 -1
- package/lib/commonjs/ui/hooks/useSettingToggle.js.map +1 -1
- package/lib/commonjs/ui/hooks/useWebSSO.js +75 -0
- package/lib/commonjs/ui/hooks/useWebSSO.js.map +1 -0
- package/lib/commonjs/ui/index.js +17 -2
- package/lib/commonjs/ui/index.js.map +1 -1
- package/lib/commonjs/ui/screens/PrivacySettingsScreen.js +59 -65
- package/lib/commonjs/ui/screens/PrivacySettingsScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/SearchSettingsScreen.js +38 -58
- package/lib/commonjs/ui/screens/SearchSettingsScreen.js.map +1 -1
- package/lib/commonjs/ui/server.js +105 -0
- package/lib/commonjs/ui/server.js.map +1 -0
- package/lib/commonjs/ui/utils/iconNames.js +133 -0
- package/lib/commonjs/ui/utils/iconNames.js.map +1 -0
- package/lib/commonjs/ui/utils/sessionHelpers.js +7 -0
- package/lib/commonjs/ui/utils/sessionHelpers.js.map +1 -1
- package/lib/commonjs/utils/requestUtils.js +4 -3
- package/lib/commonjs/utils/requestUtils.js.map +1 -1
- package/lib/module/core/mixins/index.js +36 -13
- package/lib/module/core/mixins/index.js.map +1 -1
- package/lib/module/index.js +2 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/client.js +47 -0
- package/lib/module/ui/client.js.map +1 -0
- package/lib/module/ui/components/profile/EditFieldModal.js +406 -0
- package/lib/module/ui/components/profile/EditFieldModal.js.map +1 -0
- package/lib/module/ui/context/OxyContext.js +63 -1
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/useAuth.js +106 -0
- package/lib/module/ui/hooks/useAuth.js.map +1 -0
- package/lib/module/ui/hooks/useSettingToggle.js +7 -1
- package/lib/module/ui/hooks/useSettingToggle.js.map +1 -1
- package/lib/module/ui/hooks/useWebSSO.js +71 -0
- package/lib/module/ui/hooks/useWebSSO.js.map +1 -0
- package/lib/module/ui/index.js +17 -3
- package/lib/module/ui/index.js.map +1 -1
- package/lib/module/ui/screens/PrivacySettingsScreen.js +59 -65
- package/lib/module/ui/screens/PrivacySettingsScreen.js.map +1 -1
- package/lib/module/ui/screens/SearchSettingsScreen.js +39 -59
- package/lib/module/ui/screens/SearchSettingsScreen.js.map +1 -1
- package/lib/module/ui/server.js +65 -0
- package/lib/module/ui/server.js.map +1 -0
- package/lib/module/ui/utils/iconNames.js +124 -0
- package/lib/module/ui/utils/iconNames.js.map +1 -0
- package/lib/module/ui/utils/sessionHelpers.js +7 -0
- package/lib/module/ui/utils/sessionHelpers.js.map +1 -1
- package/lib/module/utils/requestUtils.js +4 -2
- package/lib/module/utils/requestUtils.js.map +1 -1
- package/lib/typescript/commonjs/core/mixins/index.d.ts +18 -1115
- package/lib/typescript/commonjs/core/mixins/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/client.d.ts +33 -0
- package/lib/typescript/commonjs/ui/client.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/profile/EditFieldModal.d.ts +110 -0
- package/lib/typescript/commonjs/ui/components/profile/EditFieldModal.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +3 -0
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +3 -3
- package/lib/typescript/commonjs/ui/hooks/queries/useAccountQueries.d.ts +6 -10
- package/lib/typescript/commonjs/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/useSecurityQueries.d.ts +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/useSecurityQueries.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/useServicesQueries.d.ts +3 -5
- package/lib/typescript/commonjs/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAssets.d.ts +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts +69 -0
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/useSettingToggle.d.ts +4 -2
- package/lib/typescript/commonjs/ui/hooks/useSettingToggle.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts +34 -0
- package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/index.d.ts +2 -2
- package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/PrivacySettingsScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/SearchSettingsScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/server.d.ts +43 -0
- package/lib/typescript/commonjs/ui/server.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/utils/iconNames.d.ts +112 -0
- package/lib/typescript/commonjs/ui/utils/iconNames.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts +8 -3
- package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/requestUtils.d.ts +3 -1
- package/lib/typescript/commonjs/utils/requestUtils.d.ts.map +1 -1
- package/lib/typescript/module/core/mixins/index.d.ts +18 -1115
- package/lib/typescript/module/core/mixins/index.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/client.d.ts +33 -0
- package/lib/typescript/module/ui/client.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/profile/EditFieldModal.d.ts +110 -0
- package/lib/typescript/module/ui/components/profile/EditFieldModal.d.ts.map +1 -0
- package/lib/typescript/module/ui/context/OxyContext.d.ts +3 -0
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +3 -3
- package/lib/typescript/module/ui/hooks/queries/useAccountQueries.d.ts +6 -10
- package/lib/typescript/module/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/useSecurityQueries.d.ts +1 -1
- package/lib/typescript/module/ui/hooks/queries/useSecurityQueries.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/useServicesQueries.d.ts +3 -5
- package/lib/typescript/module/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useAssets.d.ts +1 -1
- package/lib/typescript/module/ui/hooks/useAuth.d.ts +69 -0
- package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/useSettingToggle.d.ts +4 -2
- package/lib/typescript/module/ui/hooks/useSettingToggle.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useWebSSO.d.ts +34 -0
- package/lib/typescript/module/ui/hooks/useWebSSO.d.ts.map +1 -0
- package/lib/typescript/module/ui/index.d.ts +2 -2
- package/lib/typescript/module/ui/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/PrivacySettingsScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/SearchSettingsScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/server.d.ts +43 -0
- package/lib/typescript/module/ui/server.d.ts.map +1 -0
- package/lib/typescript/module/ui/utils/iconNames.d.ts +112 -0
- package/lib/typescript/module/ui/utils/iconNames.d.ts.map +1 -0
- package/lib/typescript/module/ui/utils/sessionHelpers.d.ts +8 -3
- package/lib/typescript/module/ui/utils/sessionHelpers.d.ts.map +1 -1
- package/lib/typescript/module/utils/requestUtils.d.ts +3 -1
- package/lib/typescript/module/utils/requestUtils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/mixins/index.ts +57 -43
- package/src/index.ts +3 -1
- package/src/ui/client.ts +55 -0
- package/src/ui/components/profile/EditFieldModal.tsx +465 -0
- package/src/ui/context/OxyContext.tsx +69 -0
- package/src/ui/hooks/useAuth.ts +159 -0
- package/src/ui/hooks/useSettingToggle.ts +7 -3
- package/src/ui/hooks/useWebSSO.ts +93 -0
- package/src/ui/index.ts +17 -2
- package/src/ui/screens/PrivacySettingsScreen.tsx +54 -63
- package/src/ui/screens/SearchSettingsScreen.tsx +42 -64
- package/src/ui/server.ts +70 -0
- package/src/ui/utils/iconNames.ts +136 -0
- package/src/ui/utils/sessionHelpers.ts +10 -3
- 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,
|