@oxyhq/services 19.2.0 → 20.0.1

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 (126) hide show
  1. package/lib/commonjs/ui/components/FollowButton.js +1 -2
  2. package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
  3. package/lib/commonjs/ui/components/SettingsIcon.js +1 -2
  4. package/lib/commonjs/ui/components/SettingsIcon.js.map +1 -1
  5. package/lib/commonjs/ui/components/logo/LogoIcon.js +0 -2
  6. package/lib/commonjs/ui/components/logo/LogoIcon.js.map +1 -1
  7. package/lib/commonjs/ui/components/logo/LogoText.js +0 -2
  8. package/lib/commonjs/ui/components/logo/LogoText.js.map +1 -1
  9. package/lib/commonjs/ui/context/OxyContext.js +9 -8
  10. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  11. package/lib/commonjs/ui/context/commitSessionFlow.js +10 -0
  12. package/lib/commonjs/ui/context/commitSessionFlow.js.map +1 -1
  13. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +1 -3
  14. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  15. package/lib/commonjs/ui/hooks/useLanguageManagement.js +109 -28
  16. package/lib/commonjs/ui/hooks/useLanguageManagement.js.map +1 -1
  17. package/lib/commonjs/ui/hooks/useSessionManagement.js +1 -3
  18. package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
  19. package/lib/commonjs/ui/screens/LanguageSelectorScreen.js +151 -88
  20. package/lib/commonjs/ui/screens/LanguageSelectorScreen.js.map +1 -1
  21. package/lib/commonjs/ui/screens/PreferencesScreen.js +5 -6
  22. package/lib/commonjs/ui/screens/PreferencesScreen.js.map +1 -1
  23. package/lib/module/ui/components/FollowButton.js +1 -1
  24. package/lib/module/ui/components/FollowButton.js.map +1 -1
  25. package/lib/module/ui/components/SettingsIcon.js +1 -1
  26. package/lib/module/ui/components/SettingsIcon.js.map +1 -1
  27. package/lib/module/ui/components/logo/LogoIcon.js +0 -1
  28. package/lib/module/ui/components/logo/LogoIcon.js.map +1 -1
  29. package/lib/module/ui/components/logo/LogoText.js +0 -1
  30. package/lib/module/ui/components/logo/LogoText.js.map +1 -1
  31. package/lib/module/ui/context/OxyContext.js +9 -8
  32. package/lib/module/ui/context/OxyContext.js.map +1 -1
  33. package/lib/module/ui/context/commitSessionFlow.js +10 -0
  34. package/lib/module/ui/context/commitSessionFlow.js.map +1 -1
  35. package/lib/module/ui/context/hooks/useAuthOperations.js +1 -3
  36. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  37. package/lib/module/ui/hooks/useLanguageManagement.js +107 -29
  38. package/lib/module/ui/hooks/useLanguageManagement.js.map +1 -1
  39. package/lib/module/ui/hooks/useSessionManagement.js +1 -3
  40. package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
  41. package/lib/module/ui/screens/FeedbackScreen.js.map +1 -1
  42. package/lib/module/ui/screens/LanguageSelectorScreen.js +153 -91
  43. package/lib/module/ui/screens/LanguageSelectorScreen.js.map +1 -1
  44. package/lib/module/ui/screens/PreferencesScreen.js +5 -6
  45. package/lib/module/ui/screens/PreferencesScreen.js.map +1 -1
  46. package/lib/typescript/commonjs/ui/components/FollowButton.d.ts +2 -2
  47. package/lib/typescript/commonjs/ui/components/FollowButton.d.ts.map +1 -1
  48. package/lib/typescript/commonjs/ui/components/OxyPayButton.d.ts +1 -1
  49. package/lib/typescript/commonjs/ui/components/OxyPayButton.d.ts.map +1 -1
  50. package/lib/typescript/commonjs/ui/components/SettingsIcon.d.ts +1 -1
  51. package/lib/typescript/commonjs/ui/components/SettingsIcon.d.ts.map +1 -1
  52. package/lib/typescript/commonjs/ui/components/logo/LogoIcon.d.ts +1 -1
  53. package/lib/typescript/commonjs/ui/components/logo/LogoIcon.d.ts.map +1 -1
  54. package/lib/typescript/commonjs/ui/components/logo/LogoText.d.ts +1 -1
  55. package/lib/typescript/commonjs/ui/components/logo/LogoText.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +1 -1
  57. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/ui/context/commitSessionFlow.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +1 -2
  60. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +3 -0
  62. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +5 -3
  64. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts +1 -1
  66. package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/ui/hooks/useLanguageManagement.d.ts +52 -8
  68. package/lib/typescript/commonjs/ui/hooks/useLanguageManagement.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts +1 -2
  70. package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/ui/screens/LanguageSelectorScreen.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/ui/screens/PreferencesScreen.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/ui/screens/fileManagement/FileListSection.d.ts +1 -1
  74. package/lib/typescript/commonjs/ui/screens/fileManagement/FileListSection.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +1 -1
  76. package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
  77. package/lib/typescript/module/ui/components/FollowButton.d.ts +2 -2
  78. package/lib/typescript/module/ui/components/FollowButton.d.ts.map +1 -1
  79. package/lib/typescript/module/ui/components/OxyPayButton.d.ts +1 -1
  80. package/lib/typescript/module/ui/components/OxyPayButton.d.ts.map +1 -1
  81. package/lib/typescript/module/ui/components/SettingsIcon.d.ts +1 -1
  82. package/lib/typescript/module/ui/components/SettingsIcon.d.ts.map +1 -1
  83. package/lib/typescript/module/ui/components/logo/LogoIcon.d.ts +1 -1
  84. package/lib/typescript/module/ui/components/logo/LogoIcon.d.ts.map +1 -1
  85. package/lib/typescript/module/ui/components/logo/LogoText.d.ts +1 -1
  86. package/lib/typescript/module/ui/components/logo/LogoText.d.ts.map +1 -1
  87. package/lib/typescript/module/ui/context/OxyContext.d.ts +1 -1
  88. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  89. package/lib/typescript/module/ui/context/commitSessionFlow.d.ts.map +1 -1
  90. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +1 -2
  91. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  92. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +3 -0
  93. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
  94. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +5 -3
  95. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  96. package/lib/typescript/module/ui/hooks/useFollow.d.ts +1 -1
  97. package/lib/typescript/module/ui/hooks/useFollow.d.ts.map +1 -1
  98. package/lib/typescript/module/ui/hooks/useLanguageManagement.d.ts +52 -8
  99. package/lib/typescript/module/ui/hooks/useLanguageManagement.d.ts.map +1 -1
  100. package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts +1 -2
  101. package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts.map +1 -1
  102. package/lib/typescript/module/ui/screens/LanguageSelectorScreen.d.ts.map +1 -1
  103. package/lib/typescript/module/ui/screens/PreferencesScreen.d.ts.map +1 -1
  104. package/lib/typescript/module/ui/screens/fileManagement/FileListSection.d.ts +1 -1
  105. package/lib/typescript/module/ui/screens/fileManagement/FileListSection.d.ts.map +1 -1
  106. package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +1 -1
  107. package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
  108. package/package.json +18 -17
  109. package/src/ui/components/FollowButton.tsx +6 -5
  110. package/src/ui/components/OxyPayButton.tsx +1 -1
  111. package/src/ui/components/SettingsIcon.tsx +2 -1
  112. package/src/ui/components/logo/LogoIcon.tsx +2 -1
  113. package/src/ui/components/logo/LogoText.tsx +2 -1
  114. package/src/ui/context/OxyContext.tsx +13 -11
  115. package/src/ui/context/__tests__/commitSessionFlow.test.ts +27 -0
  116. package/src/ui/context/commitSessionFlow.ts +10 -0
  117. package/src/ui/context/hooks/useAuthOperations.ts +1 -5
  118. package/src/ui/context/oxyContextTypes.ts +3 -0
  119. package/src/ui/hooks/useFollow.ts +1 -1
  120. package/src/ui/hooks/useLanguageManagement.ts +144 -42
  121. package/src/ui/hooks/useSessionManagement.ts +1 -4
  122. package/src/ui/screens/FeedbackScreen.tsx +1 -1
  123. package/src/ui/screens/LanguageSelectorScreen.tsx +195 -104
  124. package/src/ui/screens/PreferencesScreen.tsx +4 -6
  125. package/src/ui/screens/fileManagement/FileListSection.tsx +1 -1
  126. package/src/ui/screens/fileManagement/hooks/useFileUploadState.ts +1 -1
@@ -1,110 +1,162 @@
1
- import React, { useState, useMemo, useCallback } from 'react';
2
- import { View, StyleSheet, ScrollView } from 'react-native';
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import { View, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
3
3
  import type { BaseScreenProps } from '../types/navigation';
4
4
  import { useTheme } from '@oxyhq/bloom/theme';
5
- import { normalizeTheme } from '@oxyhq/core';
6
5
  import { Ionicons } from '@expo/vector-icons';
7
6
  import { toast } from '@oxyhq/bloom';
8
7
  import { H1, Text } from '@oxyhq/bloom/typography';
9
8
  import { SearchInput } from '@oxyhq/bloom/search-input';
10
9
  import { SettingsListGroup, SettingsListItem } from '@oxyhq/bloom/settings-list';
10
+ import {
11
+ SUPPORTED_LANGUAGES,
12
+ getNativeLanguageName,
13
+ type SupportedLanguage,
14
+ } from '@oxyhq/core';
11
15
  import Header from '../components/Header';
12
16
  import { useI18n } from '../hooks/useI18n';
13
- import { SUPPORTED_LANGUAGES } from '@oxyhq/core';
14
17
  import { useOxy } from '../context/OxyContext';
18
+ import { addLocale, removeLocale, setPrimaryLocale } from '../hooks/useLanguageManagement';
19
+ import { useUpdateProfile } from '../hooks/mutations/useAccountMutations';
15
20
 
16
21
  interface LanguageSelectorScreenProps extends BaseScreenProps { }
17
22
 
18
- // Size (in dp) of the circular flag chip rendered as each row's leading icon.
19
- const FLAG_CHIP_SIZE = 40;
20
- // Selected-row check indicator size (dp).
21
- const CHECK_ICON_SIZE = 24;
23
+ // Size (in dp) of the circular chip rendered as each row's leading icon.
24
+ const CHIP_SIZE = 40;
25
+ // Trailing remove-affordance icon size (dp).
26
+ const REMOVE_ICON_SIZE = 24;
27
+
28
+ /** O(1) catalog lookup by canonical locale code. */
29
+ const CATALOG_BY_CODE: ReadonlyMap<string, SupportedLanguage> = new Map(
30
+ SUPPORTED_LANGUAGES.map((entry) => [entry.code, entry]),
31
+ );
22
32
 
23
33
  /**
24
- * LanguageSelectorScreen - Optimized for performance
34
+ * LanguageSelectorScreen — multi-select over the supported BCP-47 locales.
25
35
  *
26
- * Performance optimizations:
27
- * - useMemo for the filtered language list to prevent recreation on every render
28
- * - useCallback for handlers to prevent unnecessary re-renders
29
- * - Memoized component to prevent unnecessary re-renders
36
+ * The user picks one or more locales; the FIRST (primary) drives the app's UI
37
+ * language. When signed in the ordered selection is persisted to the account
38
+ * (`updateProfile({ languages })`, primary first) and every Oxy app follows it;
39
+ * when signed out a single local override is stored on-device.
30
40
  */
31
41
  const LanguageSelectorScreen: React.FC<LanguageSelectorScreenProps> = ({
32
42
  goBack,
33
43
  onClose,
34
- theme,
35
44
  }) => {
36
- // Use useOxy() hook for OxyContext values. The language preference persists
37
- // on the ACTIVE account's profile — the active session IS that account — so
38
- // gate the server sync on the active account.
39
- const { user, currentLanguage, setLanguage, oxyServices, isAuthenticated } = useOxy();
45
+ const { user, isAuthenticated, currentLanguage, currentLanguages, setLanguage } = useOxy();
40
46
  const { t } = useI18n();
41
47
  const bloomTheme = useTheme();
42
- const normalizedTheme = normalizeTheme(theme);
43
- const [isLoading, setIsLoading] = useState(false);
48
+ const updateProfile = useUpdateProfile();
44
49
  const [query, setQuery] = useState('');
50
+ const [isSubmitting, setIsSubmitting] = useState(false);
45
51
 
46
- // Memoize the language select handler to prevent recreation on every render
47
- const handleLanguageSelect = useCallback(async (languageId: string) => {
48
- if (languageId === currentLanguage || isLoading) {
49
- return; // Already selected or loading
50
- }
51
-
52
- setIsLoading(true);
53
-
54
- try {
55
- let serverSyncFailed = false;
56
-
57
- // If signed in, persist preference to backend user settings
58
- if (isAuthenticated && user?.id) {
59
- try {
60
- await oxyServices.updateProfile({ language: languageId });
61
- } catch (e: unknown) {
62
- // Server sync failed, but we'll save locally anyway
63
- serverSyncFailed = true;
64
- if (__DEV__) {
65
- console.warn('Failed to sync language to server (will save locally only):', e instanceof Error ? e.message : e);
66
- }
67
- }
68
- }
52
+ // The current ordered selection: the account locales when signed in, else
53
+ // the single guest/device locale. Always non-empty (`currentLanguage`
54
+ // resolves to the device or fallback locale).
55
+ const selected = useMemo<string[]>(
56
+ () => (currentLanguages.length > 0 ? currentLanguages : [currentLanguage]),
57
+ [currentLanguages, currentLanguage],
58
+ );
59
+
60
+ const selectedSet = useMemo(() => new Set(selected), [selected]);
69
61
 
70
- // Always persist locally for immediate UX and for guests
71
- await setLanguage(languageId);
62
+ const selectedEntries = useMemo<SupportedLanguage[]>(
63
+ () =>
64
+ selected
65
+ .map((code) => CATALOG_BY_CODE.get(code))
66
+ .filter((entry): entry is SupportedLanguage => entry !== undefined),
67
+ [selected],
68
+ );
72
69
 
73
- const selectedLang = SUPPORTED_LANGUAGES.find(lang => lang.id === languageId);
70
+ // Available = supported locales not already selected, filtered by the query
71
+ // (matches English name + native name).
72
+ const availableEntries = useMemo<SupportedLanguage[]>(() => {
73
+ const normalizedQuery = query.trim().toLowerCase();
74
+ return SUPPORTED_LANGUAGES.filter((entry) => {
75
+ if (selectedSet.has(entry.code)) {
76
+ return false;
77
+ }
78
+ if (!normalizedQuery) {
79
+ return true;
80
+ }
81
+ return (
82
+ entry.name.toLowerCase().includes(normalizedQuery) ||
83
+ entry.nativeName.toLowerCase().includes(normalizedQuery)
84
+ );
85
+ });
86
+ }, [query, selectedSet]);
74
87
 
75
- // Show success message (language is saved locally regardless of server sync)
76
- toast.success(t('language.changed', { lang: selectedLang?.name || languageId }));
88
+ const isBusy = isSubmitting || updateProfile.isPending;
77
89
 
78
- // Log server sync failure only in dev mode (user experience is still good - saved locally)
79
- if (serverSyncFailed && __DEV__) {
80
- console.warn('Language saved locally but server sync failed');
90
+ // Persist the next ordered selection. Signed-in accounts write the full
91
+ // ordered array; guests store only the primary locally.
92
+ const commitSelection = useCallback(
93
+ async (next: string[], announceCode: string): Promise<void> => {
94
+ if (isBusy || next.length === 0) {
95
+ return;
96
+ }
97
+ setIsSubmitting(true);
98
+ try {
99
+ if (isAuthenticated && user?.id) {
100
+ await updateProfile.mutateAsync({ languages: next });
101
+ } else {
102
+ await setLanguage(next[0]);
103
+ }
104
+ toast.success(t('language.changed', { lang: getNativeLanguageName(announceCode) }));
105
+ } catch (error) {
106
+ // `useUpdateProfile` already surfaces a toast on failure; only the
107
+ // local (guest) path needs its own error toast.
108
+ if (!(isAuthenticated && user?.id)) {
109
+ toast.error(t('language.saveFailed'));
110
+ }
111
+ if (__DEV__) {
112
+ console.error('Error saving language preference:', error);
113
+ }
114
+ } finally {
115
+ setIsSubmitting(false);
81
116
  }
117
+ },
118
+ [isAuthenticated, user?.id, updateProfile, setLanguage, t, isBusy],
119
+ );
120
+
121
+ // Adding a locale: signed-in appends (non-primary); a guest can only hold a
122
+ // single locale, so the tapped locale becomes their primary.
123
+ const handleAdd = useCallback(
124
+ (code: string) => {
125
+ const next = isAuthenticated && user?.id ? addLocale(selected, code) : [code];
126
+ void commitSelection(next, code);
127
+ },
128
+ [isAuthenticated, user?.id, selected, commitSelection],
129
+ );
82
130
 
83
- setIsLoading(false);
84
- // Close the bottom sheet if possible; otherwise, go back
85
- if (onClose) onClose(); else goBack?.();
131
+ const handleSetPrimary = useCallback(
132
+ (code: string) => {
133
+ if (selected[0] === code) {
134
+ return;
135
+ }
136
+ void commitSelection(setPrimaryLocale(selected, code), code);
137
+ },
138
+ [selected, commitSelection],
139
+ );
86
140
 
87
- } catch (error) {
88
- // Only show error if local storage also failed
89
- if (__DEV__) {
90
- console.error('Error saving language preference:', error);
141
+ const handleRemove = useCallback(
142
+ (code: string) => {
143
+ if (selected.length <= 1) {
144
+ return;
91
145
  }
92
- toast.error(t('language.saveFailed'));
93
- setIsLoading(false);
94
- }
95
- }, [currentLanguage, isLoading, isAuthenticated, user?.id, oxyServices, setLanguage, t, onClose, goBack]);
96
-
97
- // Filter the supported languages by the search query (name + native name).
98
- const filteredLanguages = useMemo(() => {
99
- const normalized = query.trim().toLowerCase();
100
- if (!normalized) {
101
- return SUPPORTED_LANGUAGES;
102
- }
103
- return SUPPORTED_LANGUAGES.filter((language) =>
104
- language.name.toLowerCase().includes(normalized) ||
105
- language.nativeName.toLowerCase().includes(normalized)
106
- );
107
- }, [query]);
146
+ const next = removeLocale(selected, code);
147
+ const announceCode = next[0] ?? code;
148
+ void commitSelection(next, announceCode);
149
+ },
150
+ [selected, commitSelection],
151
+ );
152
+
153
+ const renderChip = (entry: SupportedLanguage) => (
154
+ <View className="bg-fill-secondary items-center justify-center" style={styles.chip}>
155
+ <Text className="text-text-secondary" style={styles.chipLabel}>
156
+ {entry.language.toUpperCase()}
157
+ </Text>
158
+ </View>
159
+ );
108
160
 
109
161
  return (
110
162
  <View className="flex-1 bg-bg">
@@ -120,10 +172,8 @@ const LanguageSelectorScreen: React.FC<LanguageSelectorScreenProps> = ({
120
172
  className="flex-1"
121
173
  contentContainerClassName="px-screen-margin pt-space-24 pb-space-24"
122
174
  showsVerticalScrollIndicator={false}
123
- removeClippedSubviews={true}
124
175
  keyboardShouldPersistTaps="handled"
125
176
  >
126
- {/* Big Title */}
127
177
  <View className="mb-space-16">
128
178
  <H1 className="text-text" style={styles.bigTitle}>
129
179
  {t('language.title')}
@@ -135,7 +185,6 @@ const LanguageSelectorScreen: React.FC<LanguageSelectorScreenProps> = ({
135
185
  ) : null}
136
186
  </View>
137
187
 
138
- {/* Search / filter */}
139
188
  <View className="mb-space-16">
140
189
  <SearchInput
141
190
  value={query}
@@ -145,31 +194,45 @@ const LanguageSelectorScreen: React.FC<LanguageSelectorScreenProps> = ({
145
194
  />
146
195
  </View>
147
196
 
148
- {/* Available languages - Main section */}
149
- <SettingsListGroup>
150
- {filteredLanguages.map((language) => {
151
- const isSelected = currentLanguage === language.id;
197
+ {/* Selected locales ordered, primary first. */}
198
+ <SettingsListGroup title={t('language.selected')}>
199
+ {selectedEntries.map((entry, index) => {
200
+ const isPrimary = index === 0;
201
+ const canRemove = selected.length > 1;
152
202
  return (
153
203
  <SettingsListItem
154
- key={language.id}
155
- icon={
156
- <View
157
- className="bg-fill-secondary items-center justify-center"
158
- style={styles.languageFlag}
159
- >
160
- <Text style={styles.flagEmoji}>{language.flag}</Text>
161
- </View>
204
+ key={entry.code}
205
+ icon={renderChip(entry)}
206
+ title={entry.nativeName}
207
+ description={entry.name}
208
+ onPress={isPrimary ? undefined : () => handleSetPrimary(entry.code)}
209
+ disabled={isBusy}
210
+ accessibilityLabel={
211
+ isPrimary
212
+ ? `${entry.name} — ${t('language.primary')}`
213
+ : entry.name
162
214
  }
163
- title={language.name}
164
- description={language.nativeName}
165
- onPress={() => handleLanguageSelect(language.id)}
166
215
  rightElement={
167
- isSelected ? (
168
- <Ionicons
169
- name="checkmark-circle"
170
- size={CHECK_ICON_SIZE}
171
- color={bloomTheme.colors.primary}
172
- />
216
+ isPrimary ? (
217
+ <View className="bg-fill-secondary rounded-full px-space-8 py-space-2">
218
+ <Text className="text-primary" style={styles.primaryBadge}>
219
+ {t('language.primary')}
220
+ </Text>
221
+ </View>
222
+ ) : canRemove ? (
223
+ <TouchableOpacity
224
+ onPress={() => handleRemove(entry.code)}
225
+ disabled={isBusy}
226
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
227
+ accessibilityRole="button"
228
+ accessibilityLabel={`${t('language.remove')} ${entry.name}`}
229
+ >
230
+ <Ionicons
231
+ name="close-circle"
232
+ size={REMOVE_ICON_SIZE}
233
+ color={bloomTheme.colors.textSecondary}
234
+ />
235
+ </TouchableOpacity>
173
236
  ) : undefined
174
237
  }
175
238
  showChevron={false}
@@ -177,6 +240,29 @@ const LanguageSelectorScreen: React.FC<LanguageSelectorScreenProps> = ({
177
240
  );
178
241
  })}
179
242
  </SettingsListGroup>
243
+
244
+ {/* Available locales. */}
245
+ <SettingsListGroup title={t('language.available')}>
246
+ {availableEntries.map((entry) => (
247
+ <SettingsListItem
248
+ key={entry.code}
249
+ icon={renderChip(entry)}
250
+ title={entry.nativeName}
251
+ description={entry.name}
252
+ onPress={() => handleAdd(entry.code)}
253
+ disabled={isBusy}
254
+ accessibilityLabel={entry.name}
255
+ rightElement={
256
+ <Ionicons
257
+ name="add-circle-outline"
258
+ size={REMOVE_ICON_SIZE}
259
+ color={bloomTheme.colors.textSecondary}
260
+ />
261
+ }
262
+ showChevron={false}
263
+ />
264
+ ))}
265
+ </SettingsListGroup>
180
266
  </ScrollView>
181
267
  </View>
182
268
  );
@@ -193,13 +279,18 @@ const styles = StyleSheet.create({
193
279
  fontSize: 16,
194
280
  lineHeight: 22,
195
281
  },
196
- languageFlag: {
197
- width: FLAG_CHIP_SIZE,
198
- height: FLAG_CHIP_SIZE,
199
- borderRadius: FLAG_CHIP_SIZE / 2,
282
+ chip: {
283
+ width: CHIP_SIZE,
284
+ height: CHIP_SIZE,
285
+ borderRadius: CHIP_SIZE / 2,
286
+ },
287
+ chipLabel: {
288
+ fontSize: 13,
289
+ fontWeight: '600',
200
290
  },
201
- flagEmoji: {
202
- fontSize: 20,
291
+ primaryBadge: {
292
+ fontSize: 12,
293
+ fontWeight: '600',
203
294
  },
204
295
  });
205
296
 
@@ -4,6 +4,7 @@ import { SettingsListGroup, SettingsListItem } from '@oxyhq/bloom/settings-list'
4
4
  import { Switch } from '@oxyhq/bloom/switch';
5
5
  import { useTheme } from '@oxyhq/bloom/theme';
6
6
  import type { UserPreferences } from '@oxyhq/core';
7
+ import { getNativeLanguageName } from '@oxyhq/core';
7
8
  import type { BaseScreenProps } from '../types/navigation';
8
9
  import Header from '../components/Header';
9
10
  import { SettingsIcon } from '../components/SettingsIcon';
@@ -112,12 +113,9 @@ const PreferencesScreen: React.FC<BaseScreenProps> = ({
112
113
  const nextTheme: ThemePreference =
113
114
  THEME_ORDER[(THEME_ORDER.indexOf(themePref) + 1) % THEME_ORDER.length] ?? 'system';
114
115
 
115
- const languageDescription = useMemo(() => {
116
- if (prefs?.language) {
117
- return prefs.language;
118
- }
119
- return locale;
120
- }, [prefs?.language, locale]);
116
+ // The active UI locale is driven by the account's primary language when
117
+ // signed in (else the device locale) — surfaced here via the i18n `locale`.
118
+ const languageDescription = useMemo(() => getNativeLanguageName(locale), [locale]);
121
119
 
122
120
  const isSaving = updateMutation.isPending;
123
121
 
@@ -10,7 +10,7 @@ import {
10
10
  import { Ionicons } from '@expo/vector-icons';
11
11
  import type { FileMetadata } from '@oxyhq/core';
12
12
  import { SettingsListGroup, SettingsListItem } from '@oxyhq/bloom/settings-list';
13
- import { useTheme } from '@oxyhq/bloom/theme';
13
+ import type { useTheme } from '@oxyhq/bloom/theme';
14
14
  import { fileManagementStyles } from '../../components/fileManagement/styles';
15
15
 
16
16
  type ThemeColors = ReturnType<typeof useTheme>['colors'];
@@ -3,7 +3,7 @@ import { useCallback, useRef, useState } from 'react';
3
3
  import { toast } from '@oxyhq/bloom';
4
4
  import type { AssetUploadInput, FileMetadata } from '@oxyhq/core';
5
5
  import { useFileStore } from '../../../stores/fileStore';
6
- import { useUploadFile } from '../../../hooks/mutations/useAccountMutations';
6
+ import type { useUploadFile } from '../../../hooks/mutations/useAccountMutations';
7
7
  import { convertDocumentPickerAssetToFile, formatFileSize } from '../../../utils/fileManagement';
8
8
  import {
9
9
  type PendingUploadFile,