@oxyhq/services 14.1.0 → 15.0.0

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 (262) hide show
  1. package/lib/commonjs/index.js +0 -17
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/ui/components/SignInAccountChooser.js +189 -0
  4. package/lib/commonjs/ui/components/SignInAccountChooser.js.map +1 -0
  5. package/lib/commonjs/ui/components/SignInModal.js +350 -131
  6. package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
  7. package/lib/commonjs/ui/context/OxyContext.js +340 -1547
  8. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  9. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +61 -64
  10. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  11. package/lib/commonjs/ui/hooks/useAuth.js +14 -33
  12. package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
  13. package/lib/commonjs/ui/hooks/usePasswordSignIn.js +170 -0
  14. package/lib/commonjs/ui/hooks/usePasswordSignIn.js.map +1 -0
  15. package/lib/commonjs/ui/hooks/useProfileEditing.js +2 -5
  16. package/lib/commonjs/ui/hooks/useProfileEditing.js.map +1 -1
  17. package/lib/commonjs/ui/hooks/useSessionManagement.js +2 -2
  18. package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
  19. package/lib/commonjs/ui/navigation/routes.js +1 -0
  20. package/lib/commonjs/ui/navigation/routes.js.map +1 -1
  21. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js +93 -68
  22. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js.map +1 -1
  23. package/lib/commonjs/ui/screens/EditProfileScreen.js +187 -0
  24. package/lib/commonjs/ui/screens/EditProfileScreen.js.map +1 -0
  25. package/lib/commonjs/ui/screens/ManageAccountScreen.js +3 -7
  26. package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
  27. package/lib/commonjs/ui/screens/OxyAuthScreen.js +231 -95
  28. package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
  29. package/lib/commonjs/ui/screens/ProfileScreen.js +15 -2
  30. package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
  31. package/lib/commonjs/ui/screens/linkFormat.js +38 -0
  32. package/lib/commonjs/ui/screens/linkFormat.js.map +1 -0
  33. package/lib/commonjs/ui/session/authStore.js +146 -0
  34. package/lib/commonjs/ui/session/authStore.js.map +1 -0
  35. package/lib/commonjs/ui/session/createSessionClient.js +23 -17
  36. package/lib/commonjs/ui/session/createSessionClient.js.map +1 -1
  37. package/lib/commonjs/ui/session/index.js +13 -7
  38. package/lib/commonjs/ui/session/index.js.map +1 -1
  39. package/lib/commonjs/ui/session/tokenTransport.js +19 -31
  40. package/lib/commonjs/ui/session/tokenTransport.js.map +1 -1
  41. package/lib/commonjs/ui/utils/isWebBrowser.js +13 -0
  42. package/lib/commonjs/ui/utils/isWebBrowser.js.map +1 -0
  43. package/lib/commonjs/utils/deviceFlowSignIn.js +13 -16
  44. package/lib/commonjs/utils/deviceFlowSignIn.js.map +1 -1
  45. package/lib/module/index.js +0 -5
  46. package/lib/module/index.js.map +1 -1
  47. package/lib/module/ui/components/SignInAccountChooser.js +183 -0
  48. package/lib/module/ui/components/SignInAccountChooser.js.map +1 -0
  49. package/lib/module/ui/components/SignInModal.js +352 -134
  50. package/lib/module/ui/components/SignInModal.js.map +1 -1
  51. package/lib/module/ui/context/OxyContext.js +342 -1551
  52. package/lib/module/ui/context/OxyContext.js.map +1 -1
  53. package/lib/module/ui/context/hooks/useAuthOperations.js +60 -63
  54. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  55. package/lib/module/ui/hooks/useAuth.js +14 -33
  56. package/lib/module/ui/hooks/useAuth.js.map +1 -1
  57. package/lib/module/ui/hooks/usePasswordSignIn.js +165 -0
  58. package/lib/module/ui/hooks/usePasswordSignIn.js.map +1 -0
  59. package/lib/module/ui/hooks/useProfileEditing.js +2 -5
  60. package/lib/module/ui/hooks/useProfileEditing.js.map +1 -1
  61. package/lib/module/ui/hooks/useSessionManagement.js +1 -1
  62. package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
  63. package/lib/module/ui/navigation/routes.js +1 -0
  64. package/lib/module/ui/navigation/routes.js.map +1 -1
  65. package/lib/module/ui/screens/EditProfileFieldScreen.js +91 -66
  66. package/lib/module/ui/screens/EditProfileFieldScreen.js.map +1 -1
  67. package/lib/module/ui/screens/EditProfileScreen.js +182 -0
  68. package/lib/module/ui/screens/EditProfileScreen.js.map +1 -0
  69. package/lib/module/ui/screens/ManageAccountScreen.js +3 -7
  70. package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
  71. package/lib/module/ui/screens/OxyAuthScreen.js +233 -97
  72. package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
  73. package/lib/module/ui/screens/ProfileScreen.js +15 -2
  74. package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
  75. package/lib/module/ui/screens/linkFormat.js +31 -0
  76. package/lib/module/ui/screens/linkFormat.js.map +1 -0
  77. package/lib/module/ui/session/authStore.js +143 -0
  78. package/lib/module/ui/session/authStore.js.map +1 -0
  79. package/lib/module/ui/session/createSessionClient.js +22 -16
  80. package/lib/module/ui/session/createSessionClient.js.map +1 -1
  81. package/lib/module/ui/session/index.js +9 -9
  82. package/lib/module/ui/session/index.js.map +1 -1
  83. package/lib/module/ui/session/tokenTransport.js +20 -33
  84. package/lib/module/ui/session/tokenTransport.js.map +1 -1
  85. package/lib/module/ui/utils/isWebBrowser.js +11 -0
  86. package/lib/module/ui/utils/isWebBrowser.js.map +1 -0
  87. package/lib/module/utils/deviceFlowSignIn.js +14 -17
  88. package/lib/module/utils/deviceFlowSignIn.js.map +1 -1
  89. package/lib/typescript/commonjs/index.d.ts +0 -1
  90. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/ui/components/SignInAccountChooser.d.ts +29 -0
  92. package/lib/typescript/commonjs/ui/components/SignInAccountChooser.d.ts.map +1 -0
  93. package/lib/typescript/commonjs/ui/components/SignInModal.d.ts +15 -10
  94. package/lib/typescript/commonjs/ui/components/SignInModal.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +36 -42
  96. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  97. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +18 -23
  98. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +0 -2
  100. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  101. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/ui/hooks/usePasswordSignIn.d.ts +31 -0
  103. package/lib/typescript/commonjs/ui/hooks/usePasswordSignIn.d.ts.map +1 -0
  104. package/lib/typescript/commonjs/ui/hooks/useProfileEditing.d.ts +0 -1
  105. package/lib/typescript/commonjs/ui/hooks/useProfileEditing.d.ts.map +1 -1
  106. package/lib/typescript/commonjs/ui/navigation/routes.d.ts +1 -1
  107. package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
  108. package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/ui/screens/EditProfileScreen.d.ts +14 -0
  110. package/lib/typescript/commonjs/ui/screens/EditProfileScreen.d.ts.map +1 -0
  111. package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts +1 -1
  112. package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts +13 -12
  114. package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts.map +1 -1
  115. package/lib/typescript/commonjs/ui/screens/ProfileScreen.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/ui/screens/linkFormat.d.ts +23 -0
  117. package/lib/typescript/commonjs/ui/screens/linkFormat.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/ui/session/authStore.d.ts +33 -0
  119. package/lib/typescript/commonjs/ui/session/authStore.d.ts.map +1 -0
  120. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +19 -15
  121. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/ui/session/index.d.ts +9 -9
  123. package/lib/typescript/commonjs/ui/session/index.d.ts.map +1 -1
  124. package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts +13 -19
  125. package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts.map +1 -1
  126. package/lib/typescript/commonjs/ui/utils/isWebBrowser.d.ts +9 -0
  127. package/lib/typescript/commonjs/ui/utils/isWebBrowser.d.ts.map +1 -0
  128. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +15 -13
  129. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts.map +1 -1
  130. package/lib/typescript/module/index.d.ts +0 -1
  131. package/lib/typescript/module/index.d.ts.map +1 -1
  132. package/lib/typescript/module/ui/components/SignInAccountChooser.d.ts +29 -0
  133. package/lib/typescript/module/ui/components/SignInAccountChooser.d.ts.map +1 -0
  134. package/lib/typescript/module/ui/components/SignInModal.d.ts +15 -10
  135. package/lib/typescript/module/ui/components/SignInModal.d.ts.map +1 -1
  136. package/lib/typescript/module/ui/context/OxyContext.d.ts +36 -42
  137. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  138. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +18 -23
  139. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  140. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +0 -2
  141. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  142. package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
  143. package/lib/typescript/module/ui/hooks/usePasswordSignIn.d.ts +31 -0
  144. package/lib/typescript/module/ui/hooks/usePasswordSignIn.d.ts.map +1 -0
  145. package/lib/typescript/module/ui/hooks/useProfileEditing.d.ts +0 -1
  146. package/lib/typescript/module/ui/hooks/useProfileEditing.d.ts.map +1 -1
  147. package/lib/typescript/module/ui/navigation/routes.d.ts +1 -1
  148. package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
  149. package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  150. package/lib/typescript/module/ui/screens/EditProfileScreen.d.ts +14 -0
  151. package/lib/typescript/module/ui/screens/EditProfileScreen.d.ts.map +1 -0
  152. package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts +1 -1
  153. package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  154. package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts +13 -12
  155. package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts.map +1 -1
  156. package/lib/typescript/module/ui/screens/ProfileScreen.d.ts.map +1 -1
  157. package/lib/typescript/module/ui/screens/linkFormat.d.ts +23 -0
  158. package/lib/typescript/module/ui/screens/linkFormat.d.ts.map +1 -0
  159. package/lib/typescript/module/ui/session/authStore.d.ts +33 -0
  160. package/lib/typescript/module/ui/session/authStore.d.ts.map +1 -0
  161. package/lib/typescript/module/ui/session/createSessionClient.d.ts +19 -15
  162. package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -1
  163. package/lib/typescript/module/ui/session/index.d.ts +9 -9
  164. package/lib/typescript/module/ui/session/index.d.ts.map +1 -1
  165. package/lib/typescript/module/ui/session/tokenTransport.d.ts +13 -19
  166. package/lib/typescript/module/ui/session/tokenTransport.d.ts.map +1 -1
  167. package/lib/typescript/module/ui/utils/isWebBrowser.d.ts +9 -0
  168. package/lib/typescript/module/ui/utils/isWebBrowser.d.ts.map +1 -0
  169. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +15 -13
  170. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +1 -1
  171. package/package.json +4 -4
  172. package/src/index.ts +0 -4
  173. package/src/ui/components/SignInAccountChooser.tsx +162 -0
  174. package/src/ui/components/SignInModal.tsx +309 -139
  175. package/src/ui/context/OxyContext.tsx +532 -1785
  176. package/src/ui/context/hooks/useAuthOperations.ts +65 -76
  177. package/src/ui/hooks/useAuth.ts +14 -35
  178. package/src/ui/hooks/usePasswordSignIn.ts +207 -0
  179. package/src/ui/hooks/useProfileEditing.ts +2 -8
  180. package/src/ui/hooks/useSessionManagement.ts +1 -1
  181. package/src/ui/navigation/routes.ts +2 -0
  182. package/src/ui/screens/EditProfileFieldScreen.tsx +102 -66
  183. package/src/ui/screens/EditProfileScreen.tsx +155 -0
  184. package/src/ui/screens/ManageAccountScreen.tsx +3 -7
  185. package/src/ui/screens/OxyAuthScreen.tsx +259 -112
  186. package/src/ui/screens/ProfileScreen.tsx +15 -2
  187. package/src/ui/screens/__tests__/linkFormat.test.ts +73 -0
  188. package/src/ui/screens/linkFormat.ts +37 -0
  189. package/src/ui/session/__tests__/createSessionClient.test.ts +22 -7
  190. package/src/ui/session/__tests__/tokenTransport.test.ts +37 -49
  191. package/src/ui/session/authStore.ts +164 -0
  192. package/src/ui/session/createSessionClient.ts +30 -17
  193. package/src/ui/session/index.ts +10 -9
  194. package/src/ui/session/tokenTransport.ts +26 -36
  195. package/src/ui/utils/isWebBrowser.ts +8 -0
  196. package/src/utils/__tests__/deviceFlowSignIn.test.ts +50 -131
  197. package/src/utils/deviceFlowSignIn.ts +19 -26
  198. package/lib/commonjs/ui/context/inSessionTokenRefresh.js +0 -243
  199. package/lib/commonjs/ui/context/inSessionTokenRefresh.js.map +0 -1
  200. package/lib/commonjs/ui/context/silentSessionRestore.js +0 -56
  201. package/lib/commonjs/ui/context/silentSessionRestore.js.map +0 -1
  202. package/lib/commonjs/ui/hooks/useWebSSO.js +0 -28
  203. package/lib/commonjs/ui/hooks/useWebSSO.js.map +0 -1
  204. package/lib/commonjs/ui/session/projectSessionState.js +0 -86
  205. package/lib/commonjs/ui/session/projectSessionState.js.map +0 -1
  206. package/lib/commonjs/ui/session/sessionClientHost.js +0 -30
  207. package/lib/commonjs/ui/session/sessionClientHost.js.map +0 -1
  208. package/lib/commonjs/ui/utils/activeAuthuser.js +0 -142
  209. package/lib/commonjs/ui/utils/activeAuthuser.js.map +0 -1
  210. package/lib/commonjs/utils/crossApex.js +0 -74
  211. package/lib/commonjs/utils/crossApex.js.map +0 -1
  212. package/lib/module/ui/context/inSessionTokenRefresh.js +0 -238
  213. package/lib/module/ui/context/inSessionTokenRefresh.js.map +0 -1
  214. package/lib/module/ui/context/silentSessionRestore.js +0 -53
  215. package/lib/module/ui/context/silentSessionRestore.js.map +0 -1
  216. package/lib/module/ui/hooks/useWebSSO.js +0 -24
  217. package/lib/module/ui/hooks/useWebSSO.js.map +0 -1
  218. package/lib/module/ui/session/projectSessionState.js +0 -79
  219. package/lib/module/ui/session/projectSessionState.js.map +0 -1
  220. package/lib/module/ui/session/sessionClientHost.js +0 -26
  221. package/lib/module/ui/session/sessionClientHost.js.map +0 -1
  222. package/lib/module/ui/utils/activeAuthuser.js +0 -134
  223. package/lib/module/ui/utils/activeAuthuser.js.map +0 -1
  224. package/lib/module/utils/crossApex.js +0 -69
  225. package/lib/module/utils/crossApex.js.map +0 -1
  226. package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts +0 -106
  227. package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts.map +0 -1
  228. package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts +0 -36
  229. package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts.map +0 -1
  230. package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts +0 -19
  231. package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts.map +0 -1
  232. package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts +0 -45
  233. package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts.map +0 -1
  234. package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts +0 -14
  235. package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts.map +0 -1
  236. package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts +0 -67
  237. package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts.map +0 -1
  238. package/lib/typescript/commonjs/utils/crossApex.d.ts +0 -55
  239. package/lib/typescript/commonjs/utils/crossApex.d.ts.map +0 -1
  240. package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts +0 -106
  241. package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts.map +0 -1
  242. package/lib/typescript/module/ui/context/silentSessionRestore.d.ts +0 -36
  243. package/lib/typescript/module/ui/context/silentSessionRestore.d.ts.map +0 -1
  244. package/lib/typescript/module/ui/hooks/useWebSSO.d.ts +0 -19
  245. package/lib/typescript/module/ui/hooks/useWebSSO.d.ts.map +0 -1
  246. package/lib/typescript/module/ui/session/projectSessionState.d.ts +0 -45
  247. package/lib/typescript/module/ui/session/projectSessionState.d.ts.map +0 -1
  248. package/lib/typescript/module/ui/session/sessionClientHost.d.ts +0 -14
  249. package/lib/typescript/module/ui/session/sessionClientHost.d.ts.map +0 -1
  250. package/lib/typescript/module/ui/utils/activeAuthuser.d.ts +0 -67
  251. package/lib/typescript/module/ui/utils/activeAuthuser.d.ts.map +0 -1
  252. package/lib/typescript/module/utils/crossApex.d.ts +0 -55
  253. package/lib/typescript/module/utils/crossApex.d.ts.map +0 -1
  254. package/src/ui/context/inSessionTokenRefresh.ts +0 -264
  255. package/src/ui/context/silentSessionRestore.ts +0 -53
  256. package/src/ui/hooks/useWebSSO.ts +0 -23
  257. package/src/ui/session/__tests__/projectSessionState.test.ts +0 -114
  258. package/src/ui/session/__tests__/sessionClientHost.test.ts +0 -43
  259. package/src/ui/session/projectSessionState.ts +0 -85
  260. package/src/ui/session/sessionClientHost.ts +0 -27
  261. package/src/ui/utils/activeAuthuser.ts +0 -142
  262. package/src/utils/crossApex.ts +0 -75
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useCallback, useMemo } from 'react';
1
+ import React, { useState, useCallback, useMemo } from 'react';
2
2
  import {
3
3
  View,
4
4
  Platform,
@@ -16,12 +16,14 @@ import { H1, Text } from '@oxyhq/bloom/typography';
16
16
  import { Button } from '@oxyhq/bloom/button';
17
17
  import { TextField, TextFieldInput } from '@oxyhq/bloom/text-field';
18
18
  import { normalizeTheme } from '@oxyhq/core';
19
+ import type { User } from '@oxyhq/core';
19
20
  import Header from '../components/Header';
20
21
  import { useI18n } from '../hooks/useI18n';
21
22
  import { useOxy } from '../context/OxyContext';
22
23
  import { useProfileEditing } from '../hooks/useProfileEditing';
23
24
  import { toast } from '@oxyhq/bloom';
24
- import { EMAIL_REGEX } from '@oxyhq/core';
25
+ import { EMAIL_REGEX, isValidDisplayName } from '@oxyhq/core';
26
+ import { getLinkTitle, getLinkDescription, linksToListItems } from './linkFormat';
25
27
 
26
28
  /**
27
29
  * Field types supported by EditProfileFieldScreen
@@ -71,8 +73,82 @@ type EditableListItem = {
71
73
  coordinates?: { lat: number; lon: number };
72
74
  };
73
75
 
74
- const getLinkTitle = (url: string) => url.replace(/^https?:\/\//, '').replace(/\/$/, '');
75
- const getLinkDescription = (url: string) => `Link to ${url}`;
76
+
77
+ /**
78
+ * Pure seeding function: derives the initial `fieldValues` / `listItems` for a
79
+ * given field type from the active account snapshot. Called ONCE per state via
80
+ * lazy `useState` initializers — never from an effect — so a background
81
+ * `refreshSessions()` / `useCurrentUser()` swap of the `user` reference can't
82
+ * wipe in-progress typing. Each editor mounts with a fixed `fieldType`, so the
83
+ * seed is stable for the lifetime of the mount.
84
+ */
85
+ function buildInitialProfileState(
86
+ user: User | null,
87
+ fieldType: ProfileFieldType,
88
+ ): { fieldValues: Record<string, string>; listItems: EditableListItem[] } {
89
+ if (!user) {
90
+ return { fieldValues: {}, listItems: [] };
91
+ }
92
+ const userData = user;
93
+
94
+ if (fieldType === 'locations') {
95
+ const locations = Array.isArray(userData.locations) ? userData.locations : [];
96
+ return {
97
+ fieldValues: {},
98
+ listItems: locations.map((loc, i) => ({
99
+ id: String(loc.id || `location-${i}`),
100
+ name: String(loc.name || ''),
101
+ ...loc,
102
+ })),
103
+ };
104
+ }
105
+
106
+ if (fieldType === 'links') {
107
+ const linksMetadata = Array.isArray(userData.linksMetadata) ? userData.linksMetadata : [];
108
+ const links = Array.isArray(userData.links) ? userData.links : [];
109
+ // Prefer rich link metadata; fall back to the plain links array.
110
+ if (linksMetadata.length > 0) {
111
+ return {
112
+ fieldValues: {},
113
+ listItems: linksMetadata.map((link, i) => ({
114
+ ...link,
115
+ id: String(link.id || `link-${i}`),
116
+ url: String(link.url || ''),
117
+ title: String(link.title || getLinkTitle(String(link.url || ''))),
118
+ description: String(link.description || getLinkDescription(String(link.url || ''))),
119
+ })),
120
+ };
121
+ }
122
+ return {
123
+ fieldValues: {},
124
+ listItems: linksToListItems(links),
125
+ };
126
+ }
127
+
128
+ // Scalar fields: seed only the keys this field type edits.
129
+ const fieldValues: Record<string, string> = {};
130
+ switch (fieldType) {
131
+ case 'displayName':
132
+ fieldValues.firstName = String(userData.name?.first || '');
133
+ fieldValues.lastName = String(userData.lastName || userData.name?.last || '');
134
+ break;
135
+ case 'birthday':
136
+ fieldValues.birthday = String(userData.birthday || userData.dateOfBirth || '');
137
+ break;
138
+ case 'address':
139
+ fieldValues.address = String(userData.address || '');
140
+ break;
141
+ case 'username':
142
+ case 'email':
143
+ case 'bio':
144
+ case 'phone':
145
+ fieldValues[fieldType] = String(userData[fieldType] || '');
146
+ break;
147
+ default:
148
+ break;
149
+ }
150
+ return { fieldValues, listItems: [] };
151
+ }
76
152
 
77
153
  /**
78
154
  * EditProfileFieldScreen - A dedicated screen for editing profile fields
@@ -98,12 +174,18 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
98
174
  const bloomTheme = useTheme();
99
175
  const normalizedTheme = normalizeTheme(theme);
100
176
 
101
- // State for field values
102
- const [fieldValues, setFieldValues] = useState<Record<string, string>>({});
177
+ // State for field values — seeded ONCE from the active account snapshot at
178
+ // mount via lazy initializers. See buildInitialProfileState: no effect
179
+ // reseeds these, so a background user-ref swap never wipes typing.
180
+ const [fieldValues, setFieldValues] = useState<Record<string, string>>(
181
+ () => buildInitialProfileState(user, fieldType).fieldValues,
182
+ );
103
183
  const [fieldErrors, setFieldErrors] = useState<Record<string, string | undefined>>({});
104
184
 
105
- // State for list fields (locations, links)
106
- const [listItems, setListItems] = useState<EditableListItem[]>([]);
185
+ // State for list fields (locations, links) — same one-time mount seeding.
186
+ const [listItems, setListItems] = useState<EditableListItem[]>(
187
+ () => buildInitialProfileState(user, fieldType).listItems,
188
+ );
107
189
  const [newItemValue, setNewItemValue] = useState('');
108
190
 
109
191
  // Get field configuration based on fieldType
@@ -118,11 +200,21 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
118
200
  key: 'firstName',
119
201
  label: t('editProfile.items.displayName.firstName') || 'First Name',
120
202
  placeholder: t('editProfile.items.displayName.firstNamePlaceholder') || 'Enter first name',
203
+ validation: (value) =>
204
+ isValidDisplayName(value)
205
+ ? undefined
206
+ : (t('editProfile.items.displayName.invalidChars')
207
+ || 'Use letters and spaces only'),
121
208
  },
122
209
  {
123
210
  key: 'lastName',
124
211
  label: t('editProfile.items.displayName.lastName') || 'Last Name',
125
212
  placeholder: t('editProfile.items.displayName.lastNamePlaceholder') || 'Enter last name (optional)',
213
+ validation: (value) =>
214
+ isValidDisplayName(value)
215
+ ? undefined
216
+ : (t('editProfile.items.displayName.invalidChars')
217
+ || 'Use letters and spaces only'),
126
218
  },
127
219
  ],
128
220
  };
@@ -266,62 +358,6 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
266
358
  }
267
359
  }, [fieldType, t]);
268
360
 
269
- // Initialize field values from the active account's data
270
- useEffect(() => {
271
- if (!user) return;
272
-
273
- const userData = user;
274
-
275
- if (fieldConfig.isList) {
276
- if (fieldType === 'locations') {
277
- const locations = Array.isArray(userData.locations) ? userData.locations : [];
278
- setListItems(locations.map((loc, i) => ({
279
- id: String(loc.id || `location-${i}`),
280
- name: String(loc.name || ''),
281
- ...loc,
282
- })));
283
- } else if (fieldType === 'links') {
284
- const linksMetadata = Array.isArray(userData.linksMetadata) ? userData.linksMetadata : [];
285
- const links = Array.isArray(userData.links) ? userData.links : [];
286
- // Use linksMetadata if available, otherwise convert links array
287
- if (linksMetadata.length > 0) {
288
- setListItems(linksMetadata.map((link, i) => ({
289
- ...link,
290
- id: String(link.id || `link-${i}`),
291
- url: String(link.url || ''),
292
- title: String(link.title || getLinkTitle(String(link.url || ''))),
293
- description: String(link.description || getLinkDescription(String(link.url || ''))),
294
- })));
295
- } else {
296
- setListItems(links.map((item, i) => {
297
- return {
298
- id: `link-${i}`,
299
- url: item,
300
- title: getLinkTitle(item),
301
- description: getLinkDescription(item),
302
- };
303
- }));
304
- }
305
- }
306
- } else {
307
- const initialValues: Record<string, string> = {};
308
- fieldConfig.fields.forEach(field => {
309
- if (field.key === 'firstName') {
310
- initialValues[field.key] = String(userData.name?.first || '');
311
- } else if (field.key === 'lastName') {
312
- initialValues[field.key] = String(userData.lastName || userData.name?.last || '');
313
- } else if (field.key === 'birthday') {
314
- initialValues[field.key] = String(userData.birthday || userData.dateOfBirth || '');
315
- } else if (field.key === 'address') {
316
- initialValues[field.key] = String(userData.address || userData.location || '');
317
- } else {
318
- initialValues[field.key] = String(userData[field.key] || '');
319
- }
320
- });
321
- setFieldValues(initialValues);
322
- }
323
- }, [user, fieldConfig, fieldType]);
324
-
325
361
  // Field change handler
326
362
  const handleFieldChange = useCallback((key: string, value: string) => {
327
363
  setFieldValues(prev => ({ ...prev, [key]: value }));
@@ -335,7 +371,7 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
335
371
  const errors: Record<string, string | undefined> = {};
336
372
  let isValid = true;
337
373
 
338
- fieldConfig.fields.forEach(field => {
374
+ for (const field of fieldConfig.fields) {
339
375
  if (field.validation) {
340
376
  const error = field.validation(fieldValues[field.key] || '');
341
377
  if (error) {
@@ -343,7 +379,7 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
343
379
  isValid = false;
344
380
  }
345
381
  }
346
- });
382
+ }
347
383
 
348
384
  setFieldErrors(errors);
349
385
  return isValid;
@@ -0,0 +1,155 @@
1
+ import type React from 'react';
2
+ import { useMemo } from 'react';
3
+ import { View, ScrollView } from 'react-native';
4
+ import { useTheme } from '@oxyhq/bloom/theme';
5
+ import { H4, Text } from '@oxyhq/bloom/typography';
6
+ import { SettingsListGroup, SettingsListItem } from '@oxyhq/bloom/settings-list';
7
+ import { getAccountDisplayName } from '@oxyhq/core';
8
+ import type { BaseScreenProps } from '../types/navigation';
9
+ import Header from '../components/Header';
10
+ import Avatar from '../components/Avatar';
11
+ import { SettingsIcon } from '../components/SettingsIcon';
12
+ import { useOxy } from '../context/OxyContext';
13
+ import { useI18n } from '../hooks/useI18n';
14
+ import type { ProfileFieldType } from './EditProfileFieldScreen';
15
+
16
+ /**
17
+ * EditProfileScreen — the profile-editing HUB.
18
+ *
19
+ * Lists every editable profile field as its own row; each row deep-links into
20
+ * {@link EditProfileFieldScreen} with the matching `fieldType`. The avatar row
21
+ * reuses the exact same `openAvatarPicker` entry point that
22
+ * {@link ManageAccountScreen} uses. This is the single entry into per-field
23
+ * editing — reached from ManageAccount's "Edit profile" row.
24
+ */
25
+ const EditProfileScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, navigate }) => {
26
+ const bloomTheme = useTheme();
27
+ const { t, locale } = useI18n();
28
+ const { user, oxyServices, openAvatarPicker } = useOxy();
29
+
30
+ const displayName = useMemo(() => getAccountDisplayName(user, locale), [user, locale]);
31
+ const avatarUri = useMemo(
32
+ () => (user?.avatar ? oxyServices.getFileDownloadUrl(user.avatar, 'thumb') : undefined),
33
+ [user?.avatar, oxyServices],
34
+ );
35
+
36
+ const notSet = t('editProfile.notSet') || 'Not set';
37
+
38
+ // Current value previews for each row. Typed User fields resolve directly;
39
+ // `locations` is only reachable via the User index signature (typed
40
+ // `unknown`), so it is narrowed defensively.
41
+ const linkPreview = user?.linksMetadata?.[0]?.url ?? user?.links?.[0] ?? notSet;
42
+ const locationPreview = ((): string => {
43
+ const locations = user?.locations;
44
+ const first: unknown = Array.isArray(locations) ? locations[0] : undefined;
45
+ if (first && typeof first === 'object' && 'name' in first && typeof first.name === 'string' && first.name) {
46
+ return first.name;
47
+ }
48
+ return notSet;
49
+ })();
50
+
51
+ const goToField = (fieldType: ProfileFieldType) => navigate?.('EditProfileField', { fieldType });
52
+
53
+ return (
54
+ <View className="flex-1 bg-bg">
55
+ <Header
56
+ title={t('editProfile.title') || 'Edit Profile'}
57
+ onBack={goBack || onClose}
58
+ elevation="subtle"
59
+ />
60
+ <ScrollView
61
+ className="flex-1"
62
+ contentContainerClassName="px-screen-margin pb-space-24"
63
+ showsVerticalScrollIndicator={false}
64
+ >
65
+ {/* Profile card */}
66
+ <View className="items-center bg-fill-secondary rounded-radius-20 px-space-20 py-space-24 mb-space-16">
67
+ <Avatar uri={avatarUri} name={displayName} size={72} />
68
+ <H4 className="text-text mt-space-12" numberOfLines={1}>
69
+ {displayName}
70
+ </H4>
71
+ {user?.username ? (
72
+ <Text className="text-text-secondary text-sm mt-space-2" numberOfLines={1}>
73
+ @{user.username}
74
+ </Text>
75
+ ) : null}
76
+ </View>
77
+
78
+ {/* Profile picture */}
79
+ <SettingsListGroup title={t('editProfile.sections.profilePicture') || 'Profile Picture'}>
80
+ <SettingsListItem
81
+ icon={<SettingsIcon name="camera" color={bloomTheme.colors.primary} />}
82
+ title={t('editProfile.changeAvatar') || 'Change avatar'}
83
+ description={t('editProfile.items.avatar.subtitle') || 'Update your profile photo'}
84
+ onPress={openAvatarPicker}
85
+ rightElement={<Avatar uri={avatarUri} name={displayName} size={32} />}
86
+ />
87
+ </SettingsListGroup>
88
+
89
+ {/* Basic information */}
90
+ <SettingsListGroup title={t('editProfile.sections.basicInfo') || 'Basic Information'}>
91
+ <SettingsListItem
92
+ icon={<SettingsIcon name="account" color={bloomTheme.colors.primary} />}
93
+ title={t('editProfile.items.displayName.title') || 'Display Name'}
94
+ description={displayName || notSet}
95
+ onPress={() => goToField('displayName')}
96
+ />
97
+ <SettingsListItem
98
+ icon={<SettingsIcon name="at" color={bloomTheme.colors.info} />}
99
+ title={t('editProfile.items.username.title') || 'Username'}
100
+ description={user?.username ? `@${user.username}` : notSet}
101
+ onPress={() => goToField('username')}
102
+ />
103
+ <SettingsListItem
104
+ icon={<SettingsIcon name="email" color={bloomTheme.colors.success} />}
105
+ title={t('editProfile.items.email.title') || 'Email'}
106
+ description={user?.email || notSet}
107
+ onPress={() => goToField('email')}
108
+ />
109
+ <SettingsListItem
110
+ icon={<SettingsIcon name="phone" color={bloomTheme.colors.warning} />}
111
+ title={t('editProfile.items.phone.title') || 'Phone Number'}
112
+ description={user?.phone || notSet}
113
+ onPress={() => goToField('phone')}
114
+ />
115
+ </SettingsListGroup>
116
+
117
+ {/* About you */}
118
+ <SettingsListGroup title={t('editProfile.sections.about') || 'About You'}>
119
+ <SettingsListItem
120
+ icon={<SettingsIcon name="text" color={bloomTheme.colors.primary} />}
121
+ title={t('editProfile.items.bio.title') || 'Bio'}
122
+ description={user?.bio || notSet}
123
+ onPress={() => goToField('bio')}
124
+ />
125
+ <SettingsListItem
126
+ icon={<SettingsIcon name="map-marker" color={bloomTheme.colors.info} />}
127
+ title={t('editProfile.items.address.title') || 'Address'}
128
+ description={user?.address || notSet}
129
+ onPress={() => goToField('address')}
130
+ />
131
+ <SettingsListItem
132
+ icon={<SettingsIcon name="cake-variant" color={bloomTheme.colors.warning} />}
133
+ title={t('editProfile.items.birthday.title') || 'Birthday'}
134
+ description={user?.birthday || notSet}
135
+ onPress={() => goToField('birthday')}
136
+ />
137
+ <SettingsListItem
138
+ icon={<SettingsIcon name="link-variant" color={bloomTheme.colors.success} />}
139
+ title={t('editProfile.items.links.title') || 'Links'}
140
+ description={linkPreview}
141
+ onPress={() => goToField('links')}
142
+ />
143
+ <SettingsListItem
144
+ icon={<SettingsIcon name="map-marker-multiple" color={bloomTheme.colors.primary} />}
145
+ title={t('editProfile.items.locations.title') || 'Locations'}
146
+ description={locationPreview}
147
+ onPress={() => goToField('locations')}
148
+ />
149
+ </SettingsListGroup>
150
+ </ScrollView>
151
+ </View>
152
+ );
153
+ };
154
+
155
+ export default EditProfileScreen;
@@ -78,7 +78,7 @@ const formatRelative = (dateString?: string): string => {
78
78
  * Replaces AccountOverview + AccountSettings + the per-account half of
79
79
  * SessionManagement. Lists ONLY the active user's profile, sessions on this
80
80
  * device, and security/destructive actions for THIS account. Multi-account
81
- * surface lives in {@link AccountMenu} — keep these concerns separate.
81
+ * surface lives in {@link AccountSwitcher} — keep these concerns separate.
82
82
  */
83
83
  const ManageAccountScreen: React.FC<BaseScreenProps> = ({
84
84
  onClose,
@@ -384,9 +384,7 @@ const ManageAccountScreen: React.FC<BaseScreenProps> = ({
384
384
  t('manageAccount.items.editProfile.subtitle')
385
385
  || 'Name, username, bio, links'
386
386
  }
387
- onPress={() =>
388
- navigate?.('EditProfileField', { field: 'username' })
389
- }
387
+ onPress={() => navigate?.('EditProfile')}
390
388
  />
391
389
  <SettingsListItem
392
390
  icon={
@@ -400,9 +398,7 @@ const ManageAccountScreen: React.FC<BaseScreenProps> = ({
400
398
  t('manageAccount.items.theme.subtitle')
401
399
  || 'Personalize your Bloom color'
402
400
  }
403
- onPress={() =>
404
- navigate?.('EditProfileField', { field: 'color' })
405
- }
401
+ onPress={() => navigate?.('Preferences')}
406
402
  />
407
403
  <SettingsListItem
408
404
  icon={