@oxy.so/services 8.2.0 → 8.4.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 (251) hide show
  1. package/lib/commonjs/index.js +2 -0
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/ui/components/BloomLocaleBridge.js +37 -0
  4. package/lib/commonjs/ui/components/BloomLocaleBridge.js.map +1 -0
  5. package/lib/commonjs/ui/components/OxyProvider.js +16 -7
  6. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  7. package/lib/commonjs/ui/components/OxySignInRequestSurface.js +0 -1
  8. package/lib/commonjs/ui/components/OxySignInRequestSurface.js.map +1 -1
  9. package/lib/commonjs/ui/components/RequireOxyAuth.js +9 -1
  10. package/lib/commonjs/ui/components/RequireOxyAuth.js.map +1 -1
  11. package/lib/commonjs/ui/components/SurfaceScreen.js +4 -10
  12. package/lib/commonjs/ui/components/SurfaceScreen.js.map +1 -1
  13. package/lib/commonjs/ui/components/authChooser/TroubleDisclosure.js +0 -1
  14. package/lib/commonjs/ui/components/authChooser/TroubleDisclosure.js.map +1 -1
  15. package/lib/commonjs/ui/components/authChooser/primitives.js +9 -11
  16. package/lib/commonjs/ui/components/authChooser/primitives.js.map +1 -1
  17. package/lib/commonjs/ui/components/authChooser/requestSurfaces.js +13 -10
  18. package/lib/commonjs/ui/components/authChooser/requestSurfaces.js.map +1 -1
  19. package/lib/commonjs/ui/components/authChooser/styles.js +4 -1
  20. package/lib/commonjs/ui/components/authChooser/styles.js.map +1 -1
  21. package/lib/commonjs/ui/components/payment/PaymentDetailsStep.js +2 -2
  22. package/lib/commonjs/ui/components/payment/PaymentDetailsStep.js.map +1 -1
  23. package/lib/commonjs/ui/components/signIn/InlineCommonsQr.js +5 -2
  24. package/lib/commonjs/ui/components/signIn/InlineCommonsQr.js.map +1 -1
  25. package/lib/commonjs/ui/components/signIn/OxyAccountPicker.js +13 -17
  26. package/lib/commonjs/ui/components/signIn/OxyAccountPicker.js.map +1 -1
  27. package/lib/commonjs/ui/components/signIn/OxyAuthScreen.js +9 -8
  28. package/lib/commonjs/ui/components/signIn/OxyAuthScreen.js.map +1 -1
  29. package/lib/commonjs/ui/components/signIn/OxyAuthenticatorPanel.js +38 -42
  30. package/lib/commonjs/ui/components/signIn/OxyAuthenticatorPanel.js.map +1 -1
  31. package/lib/commonjs/ui/components/signIn/OxyDeleteAccountPanel.js +21 -6
  32. package/lib/commonjs/ui/components/signIn/OxyDeleteAccountPanel.js.map +1 -1
  33. package/lib/commonjs/ui/components/signIn/OxyLinkCommonsPanel.js +4 -2
  34. package/lib/commonjs/ui/components/signIn/OxyLinkCommonsPanel.js.map +1 -1
  35. package/lib/commonjs/ui/components/signIn/OxyPasswordPanel.js +5 -16
  36. package/lib/commonjs/ui/components/signIn/OxyPasswordPanel.js.map +1 -1
  37. package/lib/commonjs/ui/components/signIn/OxySignInPanel.js +19 -20
  38. package/lib/commonjs/ui/components/signIn/OxySignInPanel.js.map +1 -1
  39. package/lib/commonjs/ui/components/signIn/OxySignUpPanel.js +12 -8
  40. package/lib/commonjs/ui/components/signIn/OxySignUpPanel.js.map +1 -1
  41. package/lib/commonjs/ui/components/signIn/ReauthStep.js +2 -7
  42. package/lib/commonjs/ui/components/signIn/ReauthStep.js.map +1 -1
  43. package/lib/commonjs/ui/components/signIn/accountFlowParts.js +64 -13
  44. package/lib/commonjs/ui/components/signIn/accountFlowParts.js.map +1 -1
  45. package/lib/commonjs/ui/context/OxyContext.js +25 -4
  46. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  47. package/lib/commonjs/ui/hooks/accountQueryPersistence.js +157 -0
  48. package/lib/commonjs/ui/hooks/accountQueryPersistence.js.map +1 -0
  49. package/lib/commonjs/ui/hooks/queryClient.js +17 -9
  50. package/lib/commonjs/ui/hooks/queryClient.js.map +1 -1
  51. package/lib/commonjs/ui/index.js.map +1 -1
  52. package/lib/commonjs/ui/screens/AppInfoScreen.js +2 -1
  53. package/lib/commonjs/ui/screens/AppInfoScreen.js.map +1 -1
  54. package/lib/commonjs/ui/utils/buildId.js +25 -0
  55. package/lib/commonjs/ui/utils/buildId.js.map +1 -0
  56. package/lib/commonjs/ui/utils/clipboard.js +21 -0
  57. package/lib/commonjs/ui/utils/clipboard.js.map +1 -0
  58. package/lib/commonjs/ui/utils/clipboard.native.js +28 -0
  59. package/lib/commonjs/ui/utils/clipboard.native.js.map +1 -0
  60. package/lib/module/index.js +3 -0
  61. package/lib/module/index.js.map +1 -1
  62. package/lib/module/ui/components/BloomLocaleBridge.js +33 -0
  63. package/lib/module/ui/components/BloomLocaleBridge.js.map +1 -0
  64. package/lib/module/ui/components/OxyProvider.js +16 -7
  65. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  66. package/lib/module/ui/components/OxySignInRequestSurface.js +0 -1
  67. package/lib/module/ui/components/OxySignInRequestSurface.js.map +1 -1
  68. package/lib/module/ui/components/RequireOxyAuth.js +9 -1
  69. package/lib/module/ui/components/RequireOxyAuth.js.map +1 -1
  70. package/lib/module/ui/components/SurfaceScreen.js +5 -11
  71. package/lib/module/ui/components/SurfaceScreen.js.map +1 -1
  72. package/lib/module/ui/components/authChooser/TroubleDisclosure.js +0 -1
  73. package/lib/module/ui/components/authChooser/TroubleDisclosure.js.map +1 -1
  74. package/lib/module/ui/components/authChooser/primitives.js +11 -12
  75. package/lib/module/ui/components/authChooser/primitives.js.map +1 -1
  76. package/lib/module/ui/components/authChooser/requestSurfaces.js +15 -10
  77. package/lib/module/ui/components/authChooser/requestSurfaces.js.map +1 -1
  78. package/lib/module/ui/components/authChooser/styles.js +4 -1
  79. package/lib/module/ui/components/authChooser/styles.js.map +1 -1
  80. package/lib/module/ui/components/payment/PaymentDetailsStep.js +3 -3
  81. package/lib/module/ui/components/payment/PaymentDetailsStep.js.map +1 -1
  82. package/lib/module/ui/components/signIn/InlineCommonsQr.js +6 -3
  83. package/lib/module/ui/components/signIn/InlineCommonsQr.js.map +1 -1
  84. package/lib/module/ui/components/signIn/OxyAccountPicker.js +13 -17
  85. package/lib/module/ui/components/signIn/OxyAccountPicker.js.map +1 -1
  86. package/lib/module/ui/components/signIn/OxyAuthScreen.js +10 -9
  87. package/lib/module/ui/components/signIn/OxyAuthScreen.js.map +1 -1
  88. package/lib/module/ui/components/signIn/OxyAuthenticatorPanel.js +40 -44
  89. package/lib/module/ui/components/signIn/OxyAuthenticatorPanel.js.map +1 -1
  90. package/lib/module/ui/components/signIn/OxyDeleteAccountPanel.js +22 -7
  91. package/lib/module/ui/components/signIn/OxyDeleteAccountPanel.js.map +1 -1
  92. package/lib/module/ui/components/signIn/OxyLinkCommonsPanel.js +5 -3
  93. package/lib/module/ui/components/signIn/OxyLinkCommonsPanel.js.map +1 -1
  94. package/lib/module/ui/components/signIn/OxyPasswordPanel.js +6 -17
  95. package/lib/module/ui/components/signIn/OxyPasswordPanel.js.map +1 -1
  96. package/lib/module/ui/components/signIn/OxySignInPanel.js +20 -21
  97. package/lib/module/ui/components/signIn/OxySignInPanel.js.map +1 -1
  98. package/lib/module/ui/components/signIn/OxySignUpPanel.js +13 -9
  99. package/lib/module/ui/components/signIn/OxySignUpPanel.js.map +1 -1
  100. package/lib/module/ui/components/signIn/ReauthStep.js +3 -8
  101. package/lib/module/ui/components/signIn/ReauthStep.js.map +1 -1
  102. package/lib/module/ui/components/signIn/accountFlowParts.js +60 -11
  103. package/lib/module/ui/components/signIn/accountFlowParts.js.map +1 -1
  104. package/lib/module/ui/context/OxyContext.js +27 -6
  105. package/lib/module/ui/context/OxyContext.js.map +1 -1
  106. package/lib/module/ui/hooks/accountQueryPersistence.js +153 -0
  107. package/lib/module/ui/hooks/accountQueryPersistence.js.map +1 -0
  108. package/lib/module/ui/hooks/queryClient.js +18 -10
  109. package/lib/module/ui/hooks/queryClient.js.map +1 -1
  110. package/lib/module/ui/index.js +3 -0
  111. package/lib/module/ui/index.js.map +1 -1
  112. package/lib/module/ui/screens/AppInfoScreen.js +3 -2
  113. package/lib/module/ui/screens/AppInfoScreen.js.map +1 -1
  114. package/lib/module/ui/utils/buildId.js +21 -0
  115. package/lib/module/ui/utils/buildId.js.map +1 -0
  116. package/lib/module/ui/utils/clipboard.js +17 -0
  117. package/lib/module/ui/utils/clipboard.js.map +1 -0
  118. package/lib/module/ui/utils/clipboard.native.js +23 -0
  119. package/lib/module/ui/utils/clipboard.native.js.map +1 -0
  120. package/lib/typescript/commonjs/index.d.ts +1 -0
  121. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/ui/components/BloomLocaleBridge.d.ts +19 -0
  123. package/lib/typescript/commonjs/ui/components/BloomLocaleBridge.d.ts.map +1 -0
  124. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/ui/components/OxySignInRequestSurface.d.ts.map +1 -1
  126. package/lib/typescript/commonjs/ui/components/RequireOxyAuth.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/ui/components/SurfaceScreen.d.ts.map +1 -1
  128. package/lib/typescript/commonjs/ui/components/authChooser/TroubleDisclosure.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/ui/components/authChooser/primitives.d.ts +4 -2
  130. package/lib/typescript/commonjs/ui/components/authChooser/primitives.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/ui/components/authChooser/requestSurfaces.d.ts +4 -4
  132. package/lib/typescript/commonjs/ui/components/authChooser/requestSurfaces.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/ui/components/authChooser/styles.d.ts +2 -1
  134. package/lib/typescript/commonjs/ui/components/authChooser/styles.d.ts.map +1 -1
  135. package/lib/typescript/commonjs/ui/components/payment/PaymentDetailsStep.d.ts.map +1 -1
  136. package/lib/typescript/commonjs/ui/components/signIn/InlineCommonsQr.d.ts.map +1 -1
  137. package/lib/typescript/commonjs/ui/components/signIn/OxyAccountPicker.d.ts.map +1 -1
  138. package/lib/typescript/commonjs/ui/components/signIn/OxyAuthScreen.d.ts +1 -5
  139. package/lib/typescript/commonjs/ui/components/signIn/OxyAuthScreen.d.ts.map +1 -1
  140. package/lib/typescript/commonjs/ui/components/signIn/OxyAuthenticatorPanel.d.ts.map +1 -1
  141. package/lib/typescript/commonjs/ui/components/signIn/OxyDeleteAccountPanel.d.ts.map +1 -1
  142. package/lib/typescript/commonjs/ui/components/signIn/OxyLinkCommonsPanel.d.ts.map +1 -1
  143. package/lib/typescript/commonjs/ui/components/signIn/OxyPasswordPanel.d.ts.map +1 -1
  144. package/lib/typescript/commonjs/ui/components/signIn/OxySignInPanel.d.ts.map +1 -1
  145. package/lib/typescript/commonjs/ui/components/signIn/OxySignUpPanel.d.ts.map +1 -1
  146. package/lib/typescript/commonjs/ui/components/signIn/ReauthStep.d.ts.map +1 -1
  147. package/lib/typescript/commonjs/ui/components/signIn/accountFlowParts.d.ts +29 -2
  148. package/lib/typescript/commonjs/ui/components/signIn/accountFlowParts.d.ts.map +1 -1
  149. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  150. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +9 -0
  151. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
  152. package/lib/typescript/commonjs/ui/hooks/accountQueryPersistence.d.ts +54 -0
  153. package/lib/typescript/commonjs/ui/hooks/accountQueryPersistence.d.ts.map +1 -0
  154. package/lib/typescript/commonjs/ui/hooks/queryClient.d.ts +8 -2
  155. package/lib/typescript/commonjs/ui/hooks/queryClient.d.ts.map +1 -1
  156. package/lib/typescript/commonjs/ui/icons/Ionicons.d.ts +1 -1
  157. package/lib/typescript/commonjs/ui/icons/MaterialCommunityIcons.d.ts +1 -1
  158. package/lib/typescript/commonjs/ui/index.d.ts +1 -0
  159. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  160. package/lib/typescript/commonjs/ui/screens/AppInfoScreen.d.ts.map +1 -1
  161. package/lib/typescript/commonjs/ui/types/navigation.d.ts +9 -0
  162. package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
  163. package/lib/typescript/commonjs/ui/utils/buildId.d.ts +16 -0
  164. package/lib/typescript/commonjs/ui/utils/buildId.d.ts.map +1 -0
  165. package/lib/typescript/commonjs/ui/utils/clipboard.d.ts +11 -0
  166. package/lib/typescript/commonjs/ui/utils/clipboard.d.ts.map +1 -0
  167. package/lib/typescript/commonjs/ui/utils/clipboard.native.d.ts +18 -0
  168. package/lib/typescript/commonjs/ui/utils/clipboard.native.d.ts.map +1 -0
  169. package/lib/typescript/module/index.d.ts +1 -0
  170. package/lib/typescript/module/index.d.ts.map +1 -1
  171. package/lib/typescript/module/ui/components/BloomLocaleBridge.d.ts +19 -0
  172. package/lib/typescript/module/ui/components/BloomLocaleBridge.d.ts.map +1 -0
  173. package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
  174. package/lib/typescript/module/ui/components/OxySignInRequestSurface.d.ts.map +1 -1
  175. package/lib/typescript/module/ui/components/RequireOxyAuth.d.ts.map +1 -1
  176. package/lib/typescript/module/ui/components/SurfaceScreen.d.ts.map +1 -1
  177. package/lib/typescript/module/ui/components/authChooser/TroubleDisclosure.d.ts.map +1 -1
  178. package/lib/typescript/module/ui/components/authChooser/primitives.d.ts +4 -2
  179. package/lib/typescript/module/ui/components/authChooser/primitives.d.ts.map +1 -1
  180. package/lib/typescript/module/ui/components/authChooser/requestSurfaces.d.ts +4 -4
  181. package/lib/typescript/module/ui/components/authChooser/requestSurfaces.d.ts.map +1 -1
  182. package/lib/typescript/module/ui/components/authChooser/styles.d.ts +2 -1
  183. package/lib/typescript/module/ui/components/authChooser/styles.d.ts.map +1 -1
  184. package/lib/typescript/module/ui/components/payment/PaymentDetailsStep.d.ts.map +1 -1
  185. package/lib/typescript/module/ui/components/signIn/InlineCommonsQr.d.ts.map +1 -1
  186. package/lib/typescript/module/ui/components/signIn/OxyAccountPicker.d.ts.map +1 -1
  187. package/lib/typescript/module/ui/components/signIn/OxyAuthScreen.d.ts +1 -5
  188. package/lib/typescript/module/ui/components/signIn/OxyAuthScreen.d.ts.map +1 -1
  189. package/lib/typescript/module/ui/components/signIn/OxyAuthenticatorPanel.d.ts.map +1 -1
  190. package/lib/typescript/module/ui/components/signIn/OxyDeleteAccountPanel.d.ts.map +1 -1
  191. package/lib/typescript/module/ui/components/signIn/OxyLinkCommonsPanel.d.ts.map +1 -1
  192. package/lib/typescript/module/ui/components/signIn/OxyPasswordPanel.d.ts.map +1 -1
  193. package/lib/typescript/module/ui/components/signIn/OxySignInPanel.d.ts.map +1 -1
  194. package/lib/typescript/module/ui/components/signIn/OxySignUpPanel.d.ts.map +1 -1
  195. package/lib/typescript/module/ui/components/signIn/ReauthStep.d.ts.map +1 -1
  196. package/lib/typescript/module/ui/components/signIn/accountFlowParts.d.ts +29 -2
  197. package/lib/typescript/module/ui/components/signIn/accountFlowParts.d.ts.map +1 -1
  198. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  199. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +9 -0
  200. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
  201. package/lib/typescript/module/ui/hooks/accountQueryPersistence.d.ts +54 -0
  202. package/lib/typescript/module/ui/hooks/accountQueryPersistence.d.ts.map +1 -0
  203. package/lib/typescript/module/ui/hooks/queryClient.d.ts +8 -2
  204. package/lib/typescript/module/ui/hooks/queryClient.d.ts.map +1 -1
  205. package/lib/typescript/module/ui/icons/Ionicons.d.ts +1 -1
  206. package/lib/typescript/module/ui/icons/MaterialCommunityIcons.d.ts +1 -1
  207. package/lib/typescript/module/ui/index.d.ts +1 -0
  208. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  209. package/lib/typescript/module/ui/screens/AppInfoScreen.d.ts.map +1 -1
  210. package/lib/typescript/module/ui/types/navigation.d.ts +9 -0
  211. package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
  212. package/lib/typescript/module/ui/utils/buildId.d.ts +16 -0
  213. package/lib/typescript/module/ui/utils/buildId.d.ts.map +1 -0
  214. package/lib/typescript/module/ui/utils/clipboard.d.ts +11 -0
  215. package/lib/typescript/module/ui/utils/clipboard.d.ts.map +1 -0
  216. package/lib/typescript/module/ui/utils/clipboard.native.d.ts +18 -0
  217. package/lib/typescript/module/ui/utils/clipboard.native.d.ts.map +1 -0
  218. package/package.json +11 -6
  219. package/src/index.ts +2 -0
  220. package/src/ui/components/BloomLocaleBridge.tsx +23 -0
  221. package/src/ui/components/OxyProvider.tsx +18 -9
  222. package/src/ui/components/OxySignInRequestSurface.tsx +0 -1
  223. package/src/ui/components/RequireOxyAuth.tsx +7 -2
  224. package/src/ui/components/SurfaceScreen.tsx +3 -9
  225. package/src/ui/components/__tests__/BloomLocaleBridge.test.tsx +68 -0
  226. package/src/ui/components/authChooser/TroubleDisclosure.tsx +0 -1
  227. package/src/ui/components/authChooser/primitives.tsx +11 -9
  228. package/src/ui/components/authChooser/requestSurfaces.tsx +9 -8
  229. package/src/ui/components/authChooser/styles.ts +4 -1
  230. package/src/ui/components/payment/PaymentDetailsStep.tsx +5 -3
  231. package/src/ui/components/signIn/InlineCommonsQr.tsx +5 -2
  232. package/src/ui/components/signIn/OxyAccountPicker.tsx +14 -13
  233. package/src/ui/components/signIn/OxyAuthScreen.tsx +5 -8
  234. package/src/ui/components/signIn/OxyAuthenticatorPanel.tsx +33 -45
  235. package/src/ui/components/signIn/OxyDeleteAccountPanel.tsx +17 -3
  236. package/src/ui/components/signIn/OxyLinkCommonsPanel.tsx +3 -2
  237. package/src/ui/components/signIn/OxyPasswordPanel.tsx +6 -12
  238. package/src/ui/components/signIn/OxySignInPanel.tsx +18 -15
  239. package/src/ui/components/signIn/OxySignUpPanel.tsx +13 -5
  240. package/src/ui/components/signIn/ReauthStep.tsx +4 -7
  241. package/src/ui/components/signIn/accountFlowParts.tsx +61 -7
  242. package/src/ui/context/OxyContext.tsx +35 -5
  243. package/src/ui/context/oxyContextTypes.ts +9 -0
  244. package/src/ui/hooks/accountQueryPersistence.ts +201 -0
  245. package/src/ui/hooks/queryClient.ts +17 -8
  246. package/src/ui/index.ts +2 -0
  247. package/src/ui/screens/AppInfoScreen.tsx +2 -2
  248. package/src/ui/types/navigation.ts +9 -0
  249. package/src/ui/utils/buildId.ts +18 -0
  250. package/src/ui/utils/clipboard.native.ts +20 -0
  251. package/src/ui/utils/clipboard.ts +14 -0
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The pieces every Oxy sign-in and account screen shares: the error vocabulary,
3
3
  * one labelled field, one one-time-code field, the one primary action, a note,
4
- * and the email-code step.
4
+ * a notice, and the email-code step.
5
5
  * Each screen is built from the same shell as `OxySignInPanel`
6
6
  * (`OxyAuthScreen`), so they read as one product in the account dialog, in an
7
7
  * app's settings and on auth.oxy.so.
@@ -10,12 +10,14 @@
10
10
  import type React from 'react';
11
11
  import { useState } from 'react';
12
12
  import { StyleSheet, View } from 'react-native';
13
+ import { Admonition } from '@oxy.so/bloom/admonition';
13
14
  import { Button } from '@oxy.so/bloom/button';
14
15
  import { Field } from '@oxy.so/bloom/field';
15
16
  import { InputOtp } from '@oxy.so/bloom/input-otp';
16
17
  import { useTheme } from '@oxy.so/bloom/theme';
17
18
  import { TextField, TextFieldHint, TextFieldInput, TextFieldLabel } from '@oxy.so/bloom/text-field';
18
19
  import { Text } from '@oxy.so/bloom/typography';
20
+ import { WizardProgress } from '@oxy.so/bloom/wizard';
19
21
  import {
20
22
  EMAIL_CODE_LENGTH,
21
23
  EMAIL_SIGNIN_LONG_CODE_ALPHABET,
@@ -119,7 +121,11 @@ export function isCompleteSignInCode(typed: string): boolean {
119
121
  /** Characters in a backup code (`xxxxx-xxxxx`, its dash not counted). */
120
122
  export const BACKUP_CODE_LENGTH = 10;
121
123
 
122
- /** One labelled input, the way the sign-in screen draws its fields. */
124
+ /**
125
+ * One labelled input, the way the sign-in screen draws its fields. A
126
+ * `secureTextEntry` field is always revealable: Bloom's eye button shows and
127
+ * hides the password, named in the locale Bloom speaks (`OxyProvider` sets it).
128
+ */
123
129
  export const AccountFlowField: React.FC<{
124
130
  label: string;
125
131
  value: string;
@@ -161,6 +167,7 @@ export const AccountFlowField: React.FC<{
161
167
  autoComplete={autoComplete}
162
168
  keyboardType={keyboardType}
163
169
  secureTextEntry={secureTextEntry}
170
+ revealable={secureTextEntry}
164
171
  autoCapitalize="none"
165
172
  autoCorrect={false}
166
173
  autoFocus={autoFocus}
@@ -257,6 +264,44 @@ export const AccountFlowNote: React.FC<{ children: React.ReactNode; testID?: str
257
264
  );
258
265
  };
259
266
 
267
+ /**
268
+ * Something the person should know before acting, above the action it is
269
+ * about: a wait before trying again, a code that was just sent, a warning. A
270
+ * Bloom `Admonition`, not a field's error hint — nothing they typed is wrong.
271
+ */
272
+ export const AccountFlowNotice: React.FC<{
273
+ children: React.ReactNode;
274
+ type?: 'info' | 'warning' | 'error';
275
+ testID?: string;
276
+ }> = ({ children, type = 'info', testID }) => (
277
+ <View testID={testID}>
278
+ <Admonition type={type}>{children}</Admonition>
279
+ </View>
280
+ );
281
+
282
+ /**
283
+ * Where a step sits in a flow of several (sign-up, turning on the
284
+ * authenticator): Bloom's segmented `WizardProgress` and "Step 2 of 3". The
285
+ * screen's own header names the step, so the progress draws no title; the
286
+ * titles still name the bar for assistive technology.
287
+ */
288
+ export const AccountFlowProgress: React.FC<{ steps: readonly string[]; current: number; testID?: string }> = ({
289
+ steps,
290
+ current,
291
+ testID,
292
+ }) => {
293
+ const { t } = useI18n();
294
+ return (
295
+ <WizardProgress
296
+ steps={steps.map((title, index) => ({ key: String(index), title }))}
297
+ current={current}
298
+ hideTitle
299
+ formatStepCount={(step, total) => t('common.stepOf', { step, total })}
300
+ testID={testID}
301
+ />
302
+ );
303
+ };
304
+
260
305
  /** Why a step failed, where it happened. */
261
306
  export const AccountFlowErrorLine: React.FC<{ message: string }> = ({ message }) => {
262
307
  const theme = useTheme();
@@ -277,11 +322,19 @@ export interface EmailCodeStepProps {
277
322
  onResend: () => Promise<void>;
278
323
  /** "Use another email" / "Back" — the step before. */
279
324
  back: { label: string; onPress: () => void };
325
+ /** Where this step sits in a longer flow (a `WizardProgress`), under the header. */
326
+ progress?: React.ReactNode;
280
327
  }
281
328
 
282
329
  /** "Check your email": the 6-digit code an email verification sent. Submits itself once complete. */
283
- export const EmailCodeStep: React.FC<EmailCodeStepProps> = ({ description, verificationId, onConfirmed, onResend, back }) => {
284
- const theme = useTheme();
330
+ export const EmailCodeStep: React.FC<EmailCodeStepProps> = ({
331
+ description,
332
+ verificationId,
333
+ onConfirmed,
334
+ onResend,
335
+ back,
336
+ progress,
337
+ }) => {
285
338
  const { t } = useI18n();
286
339
  const { oxyServices } = useOxy();
287
340
  const [code, setCode] = useState('');
@@ -317,6 +370,7 @@ export const EmailCodeStep: React.FC<EmailCodeStepProps> = ({ description, verif
317
370
  return (
318
371
  <OxyAuthScreen>
319
372
  <OxyAuthScreenHeader title={t('emailCode.title')} description={description} />
373
+ {progress}
320
374
  <AccountFlowCodeField
321
375
  label={t('emailCode.label')}
322
376
  value={code}
@@ -330,10 +384,10 @@ export const EmailCodeStep: React.FC<EmailCodeStepProps> = ({ description, verif
330
384
  length={EMAIL_CODE_LENGTH}
331
385
  testID="email-code"
332
386
  />
333
- {notice ? <Text style={[styles.note, { color: theme.colors.textSecondary }]}>{notice}</Text> : null}
387
+ {notice ? <AccountFlowNotice testID="email-code-notice">{notice}</AccountFlowNotice> : null}
334
388
  <AccountFlowAction label={t('signin.actions.continue')} onPress={() => confirm(code)} pending={pending} testID="email-code-continue" />
335
- <SubtleLink label={t('emailCode.resend')} theme={theme} onPress={resend} disabled={pending} testID="email-code-resend" />
336
- <SubtleLink label={back.label} theme={theme} onPress={back.onPress} disabled={pending} testID="email-code-back" />
389
+ <SubtleLink label={t('emailCode.resend')} onPress={resend} disabled={pending} testID="email-code-resend" />
390
+ <SubtleLink label={back.label} onPress={back.onPress} disabled={pending} testID="email-code-back" />
337
391
  </OxyAuthScreen>
338
392
  );
339
393
  };
@@ -7,6 +7,7 @@ import {
7
7
  useMemo,
8
8
  useRef,
9
9
  useState,
10
+ useSyncExternalStore,
10
11
  } from 'react';
11
12
  import { Linking, Platform } from 'react-native';
12
13
  import { OxyServices } from '@oxy.so/core';
@@ -43,7 +44,7 @@ import { useLanguageManagement } from '../hooks/useLanguageManagement';
43
44
  import { useSessionManagement } from '../hooks/useSessionManagement';
44
45
  import { useAuthOperations, clearPersistedAuthSafe } from './hooks/useAuthOperations';
45
46
  import { useDeviceManagement } from '../hooks/useDeviceManagement';
46
- import { getStorageKeys, createPlatformStorage, type StorageInterface } from '../utils/storageHelpers';
47
+ import { getStorageKeys, createMemoryStorage, createPlatformStorage, type StorageInterface } from '../utils/storageHelpers';
47
48
  import type { RouteName } from '../navigation/routes';
48
49
  import { showBottomSheet as globalShowBottomSheet } from '../navigation/bottomSheetManager';
49
50
  import {
@@ -54,6 +55,7 @@ import {
54
55
  } from '../navigation/surfaces';
55
56
  import { useQueryClient, onlineManager } from '@tanstack/react-query';
56
57
  import { clearQueryCache } from '../hooks/queryClient';
58
+ import { createAccountQueryPersistence, type AccountQueryPersistence } from '../hooks/accountQueryPersistence';
57
59
  import { useAvatarPicker } from '../hooks/useAvatarPicker';
58
60
  import { resetSessionScopedStores } from '../stores/resetSessionScopedStores';
59
61
  import { ASSET_DOWNLOAD_URLS_QUERY_KEY } from '../hooks/useResolvedFileUrls';
@@ -94,6 +96,8 @@ export type { LogoutResult, OxyContextState, OxyRuntimeProviderProps } from './o
94
96
 
95
97
  const OxyRuntimeContext = createContext<OxyContextState | null>(null);
96
98
 
99
+ const subscribeNever = (): (() => void) => () => undefined;
100
+
97
101
  type OxyAuthActionsContextState = Pick<
98
102
  OxyContextState,
99
103
  | 'signIn'
@@ -135,6 +139,7 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
135
139
  webAuthMode = 'popup',
136
140
  backgroundSession = false,
137
141
  platformStorage,
142
+ accountQueries,
138
143
  onAuthStateChange,
139
144
  onError,
140
145
  }) => {
@@ -283,6 +288,24 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
283
288
  // Local display/bookkeeping storage (session id list, language). Distinct
284
289
  // from `authStore` (the durable credential blob). Exposed as an awaitable so
285
290
  // a persistence write is never dropped because it raced storage init.
291
+ const queryClient = useQueryClient();
292
+
293
+ // The app's per-account offline cache (`accountQueries`). Built once, like the
294
+ // runtime. `handleSubjectChange` activates it BEFORE anyone is woken, so a
295
+ // consumer of the new account never reads the previous account's rows.
296
+ const accountQueryPersistenceRef = useRef<AccountQueryPersistence | null | undefined>(undefined);
297
+ if (accountQueryPersistenceRef.current === undefined) {
298
+ accountQueryPersistenceRef.current = accountQueries
299
+ ? createAccountQueryPersistence(queryClient, accountQueries)
300
+ : null;
301
+ }
302
+ const accountQueryPersistence = accountQueryPersistenceRef.current;
303
+ const isAccountCacheReady = useSyncExternalStore(
304
+ accountQueryPersistence ? accountQueryPersistence.subscribe : subscribeNever,
305
+ () => accountQueryPersistence?.isReady() ?? true,
306
+ () => accountQueryPersistence?.isReady() ?? true,
307
+ );
308
+
286
309
  const storageRef = useRef<StorageInterface | null>(null);
287
310
  const [storage, setStorage] = useState<StorageInterface | null>(null);
288
311
  const buildStorageDeferred = () => {
@@ -313,12 +336,16 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
313
336
  .then((storageInstance) => {
314
337
  storageRef.current = storageInstance;
315
338
  storageReady.resolve(storageInstance);
339
+ accountQueryPersistence?.setStorage(storageInstance);
316
340
  if (mounted) {
317
341
  setStorage(storageInstance);
318
342
  runtime.setStorageReady(true);
319
343
  }
320
344
  })
321
345
  .catch((err) => {
346
+ // Nothing persisted can be restored; the account cache still has to
347
+ // become ready, or RequireOxyAuth would wait forever.
348
+ accountQueryPersistence?.setStorage(createMemoryStorage());
322
349
  if (mounted) {
323
350
  logger('Failed to initialize storage', err);
324
351
  onError?.({ message: 'Failed to initialize storage', code: 'STORAGE_INIT_ERROR', status: 500 });
@@ -327,7 +354,7 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
327
354
  return () => {
328
355
  mounted = false;
329
356
  };
330
- }, [logger, onError, platformStorage, runtime, storageReady]);
357
+ }, [accountQueryPersistence, logger, onError, platformStorage, runtime, storageReady]);
331
358
 
332
359
  const {
333
360
  currentLanguage,
@@ -344,8 +371,6 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
344
371
  logger,
345
372
  });
346
373
 
347
- const queryClient = useQueryClient();
348
-
349
374
  const {
350
375
  saveSessionIds,
351
376
  switchSession,
@@ -1207,6 +1232,9 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
1207
1232
  // buy a refetch storm at first paint.
1208
1233
  const handleSubjectChange = useCallback(
1209
1234
  ({ previous, next }: SubjectTransition): void => {
1235
+ // Every transition, the first sign-in included: the account cache is
1236
+ // keyed by the subject, and a sign-out deletes the signed-out one's.
1237
+ accountQueryPersistence?.activate(next);
1210
1238
  if (previous === null) {
1211
1239
  return;
1212
1240
  }
@@ -1224,7 +1252,7 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
1224
1252
  queryClient.invalidateQueries();
1225
1253
  void refreshAccounts();
1226
1254
  },
1227
- [queryClient, refreshAccounts],
1255
+ [accountQueryPersistence, queryClient, refreshAccounts],
1228
1256
  );
1229
1257
  subjectChangeRef.current = handleSubjectChange;
1230
1258
 
@@ -1256,6 +1284,7 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
1256
1284
  isPrivateApiPending,
1257
1285
  isAuthResolved: authResolved,
1258
1286
  isStorageReady: snapshot.storageReady,
1287
+ isAccountCacheReady,
1259
1288
  sessionMode: isIdentityBound ? 'identity' : 'account',
1260
1289
  webAuthMode,
1261
1290
  error,
@@ -1310,6 +1339,7 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
1310
1339
  isPrivateApiPending,
1311
1340
  authResolved,
1312
1341
  snapshot.storageReady,
1342
+ isAccountCacheReady,
1313
1343
  isIdentityBound,
1314
1344
  webAuthMode,
1315
1345
  error,
@@ -12,6 +12,7 @@ import type {
12
12
  } from '../oauth/explicitOAuthConsent';
13
13
  import type { WebAuthMode, WebOAuthSignInResult } from '../oauth/types';
14
14
  import type { StorageInterface } from '../utils/storageHelpers';
15
+ import type { AccountQueriesConfig } from '../hooks/accountQueryPersistence';
15
16
 
16
17
  /**
17
18
  * The outcome of `logout()`. It never rejects: a caller reads this instead.
@@ -48,6 +49,12 @@ export interface OxyContextState {
48
49
  */
49
50
  isAuthResolved: boolean;
50
51
  isStorageReady: boolean;
52
+ /**
53
+ * Whether the signed-in account's persisted `accountQueries` cache has been
54
+ * restored. Always `true` signed out or when the app declared none.
55
+ * `RequireOxyAuth` waits on it.
56
+ */
57
+ isAccountCacheReady: boolean;
51
58
  /**
52
59
  * Who owns this provider's session (see `OxyProviderProps.sessionMode`).
53
60
  *
@@ -200,6 +207,8 @@ export interface OxyRuntimeProviderProps {
200
207
  onError?: (error: ApiError) => void;
201
208
  /** Storage instance owned by the public provider; internal composition seam. */
202
209
  platformStorage?: StorageInterface | null;
210
+ /** See `OxyProviderProps.accountQueries`. Read once, at mount. */
211
+ accountQueries?: AccountQueriesConfig;
203
212
  }
204
213
 
205
214
  /** Internal commit input — session plus zero-cookie device credential. */
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Offline persistence for an app's PRIVATE, per-account queries.
3
+ *
4
+ * The SDK's own persister (`attachQueryPersistence`) keeps account-agnostic
5
+ * data. An app's mail, library or drafts belong to one account, and every app
6
+ * that wanted them to survive a restart had to scope a persister to the session
7
+ * itself. The SDK owns the session, so it owns this too:
8
+ *
9
+ * - Storage is keyed by the account (`oxy_account_queries:<accountId>`), so a
10
+ * restore can only ever hydrate the account that is signed in.
11
+ * - The buster is the build (`getOxyBuildId`), so no cache written by an older
12
+ * bundle, in an older shape, is ever served.
13
+ * - On a subject change the declared roots, memory-only ones included, and the
14
+ * account's paused mutations are dropped
15
+ * from memory synchronously, before anyone is woken, and the new account's
16
+ * blob is restored. `isReady()` is false until that restore settles, and
17
+ * `RequireOxyAuth` waits on it, so another account's rows never render.
18
+ * - On sign-out the signed-out account's blob is deleted.
19
+ */
20
+
21
+ import { hashKey, type Mutation, type Query, type QueryClient, type QueryKey } from '@tanstack/react-query';
22
+ import { persistQueryClient } from '@tanstack/react-query-persist-client';
23
+ import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
24
+ import type { StorageInterface } from '../utils/storageHelpers';
25
+ import { getOxyBuildId } from '../utils/buildId';
26
+
27
+ /** An app's declaration of which data is private to the signed-in account. */
28
+ export interface AccountQueriesConfig {
29
+ /**
30
+ * Query-key roots (`queryKey[0]`) whose data belongs to the signed-in account
31
+ * and survives a restart.
32
+ */
33
+ roots: readonly string[];
34
+ /**
35
+ * Roots that belong to the account too, but must not be written to disk
36
+ * (signed URLs, AI output, search results): dropped on a switch, never persisted.
37
+ */
38
+ memoryOnlyRoots?: readonly string[];
39
+ /**
40
+ * Mutation keys whose PAUSED (offline) instances replay after a restart for
41
+ * the same account. Register their `mutationFn` with `setMutationDefaults`.
42
+ */
43
+ mutationKeys?: readonly QueryKey[];
44
+ }
45
+
46
+ export const ACCOUNT_QUERY_CACHE_KEY = 'oxy_account_queries';
47
+ const ACCOUNT_QUERY_CACHE_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days
48
+ const ACCOUNT_QUERY_PERSIST_THROTTLE_MS = 1_000;
49
+
50
+ export interface AccountQueryPersistence {
51
+ /** Storage became available (it initializes asynchronously). */
52
+ setStorage(storage: StorageInterface): void;
53
+ /** The signed-in account changed. `null` is a sign-out. Synchronous in-memory reset. */
54
+ activate(accountId: string | null): void;
55
+ /** True once the active account's cache is restored (always true signed out). */
56
+ isReady(): boolean;
57
+ subscribe(listener: () => void): () => void;
58
+ /** Whether a mutation is account-scoped (so the SDK's own persister skips it). */
59
+ ownsMutation(mutation: Mutation): boolean;
60
+ dispose(): void;
61
+ }
62
+
63
+ function storageKeyFor(accountId: string): string {
64
+ return `${ACCOUNT_QUERY_CACHE_KEY}:${encodeURIComponent(accountId)}`;
65
+ }
66
+
67
+ export function createAccountQueryPersistence(
68
+ queryClient: QueryClient,
69
+ config: AccountQueriesConfig,
70
+ ): AccountQueryPersistence {
71
+ const persistedRoots = new Set(config.roots);
72
+ const accountRoots = new Set([...config.roots, ...(config.memoryOnlyRoots ?? [])]);
73
+ const mutationHashes = new Set((config.mutationKeys ?? []).map((key) => hashKey(key)));
74
+ const listeners = new Set<() => void>();
75
+
76
+ let storage: StorageInterface | null = null;
77
+ let accountId: string | null = null;
78
+ let restoredFor: string | null = null;
79
+ let generation = 0;
80
+ let unsubscribePersist: (() => void) | null = null;
81
+ // The in-flight restore. A new account waits for it, so a late restore can
82
+ // never hydrate the previous account's rows after the switch.
83
+ let restoring: Promise<void> = Promise.resolve();
84
+
85
+ const notify = () => {
86
+ for (const listener of listeners) listener();
87
+ };
88
+
89
+ const rootOf = (query: Query): string | null => {
90
+ const head = query.queryKey[0];
91
+ return typeof head === 'string' ? head : null;
92
+ };
93
+ const ownsQuery = (query: Query): boolean => accountRoots.has(rootOf(query) ?? '');
94
+ const persistsQuery = (query: Query): boolean => persistedRoots.has(rootOf(query) ?? '');
95
+
96
+ const ownsMutation = (mutation: Mutation): boolean => {
97
+ const key = mutation.options.mutationKey;
98
+ return key !== undefined && mutationHashes.has(hashKey(key));
99
+ };
100
+
101
+ const dropScopedData = () => {
102
+ queryClient.removeQueries({ predicate: ownsQuery });
103
+ const mutations = queryClient.getMutationCache();
104
+ for (const mutation of mutations.getAll()) {
105
+ if (ownsMutation(mutation)) mutations.remove(mutation);
106
+ }
107
+ };
108
+
109
+ const persisterFor = (id: string, target: StorageInterface) =>
110
+ createAsyncStoragePersister({
111
+ storage: {
112
+ getItem: (key) => target.getItem(key),
113
+ setItem: (key, value) => target.setItem(key, value),
114
+ removeItem: (key) => target.removeItem(key),
115
+ },
116
+ key: storageKeyFor(id),
117
+ throttleTime: ACCOUNT_QUERY_PERSIST_THROTTLE_MS,
118
+ });
119
+
120
+ const start = () => {
121
+ const id = accountId;
122
+ const target = storage;
123
+ if (!id || !target || unsubscribePersist) return;
124
+ const current = generation;
125
+ const [unsubscribe, restored] = persistQueryClient({
126
+ queryClient,
127
+ persister: persisterFor(id, target),
128
+ maxAge: ACCOUNT_QUERY_CACHE_MAX_AGE,
129
+ buster: getOxyBuildId(),
130
+ dehydrateOptions: {
131
+ shouldDehydrateQuery: (query) => query.state.status === 'success' && persistsQuery(query),
132
+ shouldDehydrateMutation: (mutation) => mutation.state.isPaused && ownsMutation(mutation),
133
+ },
134
+ });
135
+ unsubscribePersist = unsubscribe;
136
+ restoring = restored.catch(() => undefined).then(() => {
137
+ if (current !== generation) return;
138
+ restoredFor = id;
139
+ notify();
140
+ });
141
+ };
142
+
143
+ const stop = () => {
144
+ unsubscribePersist?.();
145
+ unsubscribePersist = null;
146
+ };
147
+
148
+ return {
149
+ setStorage(next) {
150
+ if (storage) return;
151
+ storage = next;
152
+ const current = generation;
153
+ void restoring.then(() => {
154
+ if (current === generation) start();
155
+ });
156
+ },
157
+
158
+ activate(next) {
159
+ if (next === accountId) return;
160
+ const previous = accountId;
161
+ generation += 1;
162
+ const current = generation;
163
+ stop();
164
+ dropScopedData();
165
+ accountId = next;
166
+ restoredFor = null;
167
+ notify();
168
+
169
+ if (next === null && previous !== null && storage) {
170
+ void persisterFor(previous, storage).removeClient();
171
+ }
172
+
173
+ void restoring.then(() => {
174
+ if (current !== generation) return;
175
+ // A restore that was already in flight may have hydrated the previous
176
+ // account's rows; drop them again before this account's restore.
177
+ dropScopedData();
178
+ start();
179
+ });
180
+ },
181
+
182
+ isReady() {
183
+ return accountId === null || restoredFor === accountId;
184
+ },
185
+
186
+ subscribe(listener) {
187
+ listeners.add(listener);
188
+ return () => {
189
+ listeners.delete(listener);
190
+ };
191
+ },
192
+
193
+ ownsMutation,
194
+
195
+ dispose() {
196
+ generation += 1;
197
+ stop();
198
+ listeners.clear();
199
+ },
200
+ };
201
+ }
@@ -24,7 +24,7 @@
24
24
  * when introducing reads that should survive restart.
25
25
  */
26
26
 
27
- import { QueryClient, onlineManager, type Query, type Mutation } from '@tanstack/react-query';
27
+ import { QueryClient, hashKey, onlineManager, type Query, type Mutation, type QueryKey } from '@tanstack/react-query';
28
28
  import {
29
29
  persistQueryClient,
30
30
  type PersistedClient,
@@ -32,6 +32,7 @@ import {
32
32
  import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
33
33
  import { isDev } from '@oxy.so/core';
34
34
  import type { StorageInterface } from '../utils/storageHelpers';
35
+ import { getOxyBuildId } from '../utils/buildId';
35
36
 
36
37
  const QUERY_CACHE_KEY = 'oxy_query_cache_v3';
37
38
  const QUERY_CACHE_MAX_AGE = 30 * 24 * 60 * 60 * 1000; // 30 days
@@ -106,13 +107,6 @@ function shouldDehydrateQuery(query: Query): boolean {
106
107
  return PERSISTED_QUERY_PREFIXES.includes(head);
107
108
  }
108
109
 
109
- /**
110
- * Persist every mutation regardless of status — paused mutations are
111
- * exactly the ones that must survive restart to replay when online.
112
- */
113
- function shouldDehydrateMutation(_mutation: Mutation): boolean {
114
- return true;
115
- }
116
110
 
117
111
  /**
118
112
  * Create a QueryClient with offline-first defaults.
@@ -191,6 +185,12 @@ export interface AttachPersistenceResult {
191
185
  export const attachQueryPersistence = (
192
186
  queryClient: QueryClient,
193
187
  storage: StorageInterface | null | undefined,
188
+ /**
189
+ * Mutation keys the app declared account-scoped (`accountQueries`). Those are
190
+ * persisted per account by `createAccountQueryPersistence`, never here, so a
191
+ * paused mutation cannot replay twice or under another account.
192
+ */
193
+ accountMutationKeys: readonly QueryKey[] = [],
194
194
  ): AttachPersistenceResult => {
195
195
  if (!storage) {
196
196
  return {
@@ -205,10 +205,19 @@ export const attachQueryPersistence = (
205
205
  throttleTime: QUERY_PERSIST_THROTTLE_MS,
206
206
  });
207
207
 
208
+ // Persist every other mutation regardless of status: paused mutations are
209
+ // exactly the ones that must survive restart to replay when online.
210
+ const accountScoped = new Set(accountMutationKeys.map((key) => hashKey(key)));
211
+ const shouldDehydrateMutation = (mutation: Mutation): boolean => {
212
+ const key = mutation.options.mutationKey;
213
+ return key === undefined || !accountScoped.has(hashKey(key));
214
+ };
215
+
208
216
  const [unsubscribe, restored] = persistQueryClient({
209
217
  queryClient,
210
218
  persister,
211
219
  maxAge: QUERY_CACHE_MAX_AGE,
220
+ buster: getOxyBuildId(),
212
221
  dehydrateOptions: {
213
222
  shouldDehydrateQuery,
214
223
  shouldDehydrateMutation,
package/src/ui/index.ts CHANGED
@@ -24,6 +24,8 @@ export { LogoIcon } from './components/logo/LogoIcon';
24
24
  export { LogoText } from './components/logo/LogoText';
25
25
  export { RequireOxyAuth } from './components/RequireOxyAuth';
26
26
  export type { RequireOxyAuthProps, RequireOxyAuthPrompt } from './components/RequireOxyAuth';
27
+ // `OxyProvider`'s `accountQueries`: the app's per-account offline cache.
28
+ export type { AccountQueriesConfig } from './hooks/accountQueryPersistence';
27
29
  export { default as FollowButton } from './components/FollowButton';
28
30
  export { default as PeableButton } from './components/PeableButton';
29
31
  export type { PeableButtonProps } from './components/PeableButton';
@@ -4,12 +4,12 @@ import {
4
4
  View,
5
5
  Platform,
6
6
  Dimensions,
7
- Clipboard,
8
7
  ActivityIndicator,
9
8
  } from 'react-native';
10
9
  import type { BaseScreenProps } from '../types/navigation';
11
10
  import { packageInfo } from '@oxy.so/core';
12
11
  import { toast } from '@oxy.so/bloom/toast';
12
+ import { copyText } from '../utils/clipboard';
13
13
  import OxyServicesLogo from '../../assets/icons/OxyServices';
14
14
  import { SettingsIcon } from '../components/SettingsIcon';
15
15
  import { useTheme } from '@oxy.so/bloom/theme';
@@ -93,7 +93,7 @@ const AppInfoScreen: React.FC<BaseScreenProps> = ({
93
93
 
94
94
  const copyToClipboard = async (text: string, label: string) => {
95
95
  try {
96
- await Clipboard.setString(text);
96
+ await copyText(text);
97
97
  toast.success(t('appInfo.toasts.copiedToClipboard', { label }));
98
98
  } catch (error) {
99
99
  toast.error(t('appInfo.toasts.copyFailed'));
@@ -1,5 +1,6 @@
1
1
  import type { ReactNode, RefObject } from 'react';
2
2
  import type { QueryClient } from '@tanstack/react-query';
3
+ import type { AccountQueriesConfig } from '../hooks/accountQueryPersistence';
3
4
  import type { RouteName } from '../navigation/routes';
4
5
  import type { User } from '@oxy.so/core';
5
6
  import type { ClientSession } from '@oxy.so/core';
@@ -131,6 +132,14 @@ export interface OxyProviderProps {
131
132
  */
132
133
  webAuthMode?: WebAuthMode;
133
134
  queryClient?: QueryClient;
135
+ /**
136
+ * The app's private, per-account data, persisted offline by the SDK: stored
137
+ * per account, discarded by every new build, dropped from memory on an
138
+ * account switch and deleted on sign-out. `RequireOxyAuth` waits for the
139
+ * signed-in account's cache to restore, so another account's rows never
140
+ * render. Works with the SDK's client or one passed as `queryClient`.
141
+ */
142
+ accountQueries?: AccountQueriesConfig;
134
143
  /**
135
144
  * Convenience: wrap the whole app subtree in `<RequireOxyAuth prompt=...>`.
136
145
  * `off` (default) renders children unconditionally; `soft` adds a dismissible
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The identity of the JS bundle that is running, used as the buster for every
3
+ * persisted query cache the SDK owns.
4
+ *
5
+ * A persisted cache is restored and served before any refetch (offline-first),
6
+ * so data written by an older build reaches components in whatever shape that
7
+ * build used. Busting on the build means no app ever has to remember to bump a
8
+ * version when a query's shape changes: a new build simply starts from the
9
+ * network. Inbox shipped exactly that crash when a hand-kept buster was missed.
10
+ *
11
+ * `@oxy.so/app-preset`'s Babel config replaces `process.env.OXY_BUILD_ID` with
12
+ * the commit at bundle time, node_modules included. An app that does not use
13
+ * the preset gets a constant, which behaves like a buster that is never bumped.
14
+ */
15
+ export function getOxyBuildId(): string {
16
+ const buildId = typeof process === 'undefined' ? undefined : process.env.OXY_BUILD_ID;
17
+ return buildId || 'unversioned';
18
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Put text on the clipboard on iOS and Android, through `expo-clipboard`.
3
+ * React Native's own `Clipboard` is deprecated and gone from newer releases.
4
+ *
5
+ * `expo-clipboard` is an OPTIONAL peer, loaded with `import()` so an app
6
+ * without it (or with the JS but not the native module, before a rebuild)
7
+ * still bundles and boots: the copy rejects here and the caller says nothing
8
+ * was copied. A static import would instead fail the Metro build or throw
9
+ * while the root barrel evaluates.
10
+ *
11
+ * Only this NATIVE fork names the package; the default fork (`clipboard.ts`,
12
+ * which `tsc`, Vite and SSR resolve) names nothing, so a consumer's type graph
13
+ * reaches `expo-clipboard` only when it typechecks `.native` files
14
+ * (`moduleSuffixes`) — the case package-rules.md#package-boundaries warns
15
+ * about. `clipboardIsolation.test.ts` keeps it that way.
16
+ */
17
+ export async function copyText(text: string): Promise<void> {
18
+ const clipboard = await import('expo-clipboard');
19
+ await clipboard.setStringAsync(text);
20
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Put text on the clipboard: the browser's asynchronous Clipboard API. Rejects
3
+ * where there is none (an insecure origin, SSR, a denied permission), so the
4
+ * caller reports only a copy that happened.
5
+ *
6
+ * The native fork (`clipboard.native.ts`) uses `expo-clipboard`. React
7
+ * Native's own `Clipboard` is deprecated and gone from newer releases, so
8
+ * neither fork reaches for it.
9
+ */
10
+ export async function copyText(text: string): Promise<void> {
11
+ const clipboard = typeof navigator === 'undefined' ? undefined : navigator.clipboard;
12
+ if (!clipboard?.writeText) throw new Error('clipboard unavailable');
13
+ await clipboard.writeText(text);
14
+ }