@oxyhq/services 16.0.0 → 18.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 (278) hide show
  1. package/README.md +77 -98
  2. package/lib/commonjs/index.js +40 -51
  3. package/lib/commonjs/index.js.map +1 -1
  4. package/lib/commonjs/ui/client.js +0 -14
  5. package/lib/commonjs/ui/client.js.map +1 -1
  6. package/lib/commonjs/ui/components/OxyAccountDialog.js +742 -0
  7. package/lib/commonjs/ui/components/OxyAccountDialog.js.map +1 -0
  8. package/lib/commonjs/ui/components/OxyConsentScreen.js +461 -0
  9. package/lib/commonjs/ui/components/OxyConsentScreen.js.map +1 -0
  10. package/lib/commonjs/ui/components/OxyProvider.js +13 -6
  11. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  12. package/lib/commonjs/ui/components/OxySignInButton.js +180 -22
  13. package/lib/commonjs/ui/components/OxySignInButton.js.map +1 -1
  14. package/lib/commonjs/ui/components/ProfileButton.js +25 -112
  15. package/lib/commonjs/ui/components/ProfileButton.js.map +1 -1
  16. package/lib/commonjs/ui/components/RequireOxyAuth.js +299 -0
  17. package/lib/commonjs/ui/components/RequireOxyAuth.js.map +1 -0
  18. package/lib/commonjs/ui/components/oauthNavigation.js +73 -0
  19. package/lib/commonjs/ui/components/oauthNavigation.js.map +1 -0
  20. package/lib/commonjs/ui/context/OxyContext.js +122 -4
  21. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  22. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +1 -1
  23. package/lib/commonjs/ui/hooks/useAuth.js +19 -30
  24. package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
  25. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +37 -267
  26. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +1 -1
  27. package/lib/commonjs/ui/index.js +5 -40
  28. package/lib/commonjs/ui/index.js.map +1 -1
  29. package/lib/commonjs/ui/navigation/accountDialogManager.js +91 -0
  30. package/lib/commonjs/ui/navigation/accountDialogManager.js.map +1 -0
  31. package/lib/commonjs/ui/navigation/routes.js +0 -2
  32. package/lib/commonjs/ui/navigation/routes.js.map +1 -1
  33. package/lib/commonjs/ui/screens/ManageAccountScreen.js +4 -3
  34. package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
  35. package/lib/commonjs/ui/session/createSessionClient.js +10 -2
  36. package/lib/commonjs/ui/session/createSessionClient.js.map +1 -1
  37. package/lib/module/index.js +19 -16
  38. package/lib/module/index.js.map +1 -1
  39. package/lib/module/ui/client.js +1 -2
  40. package/lib/module/ui/client.js.map +1 -1
  41. package/lib/module/ui/components/OxyAccountDialog.js +739 -0
  42. package/lib/module/ui/components/OxyAccountDialog.js.map +1 -0
  43. package/lib/module/ui/components/OxyConsentScreen.js +454 -0
  44. package/lib/module/ui/components/OxyConsentScreen.js.map +1 -0
  45. package/lib/module/ui/components/OxyProvider.js +13 -6
  46. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  47. package/lib/module/ui/components/OxySignInButton.js +181 -22
  48. package/lib/module/ui/components/OxySignInButton.js.map +1 -1
  49. package/lib/module/ui/components/ProfileButton.js +26 -112
  50. package/lib/module/ui/components/ProfileButton.js.map +1 -1
  51. package/lib/module/ui/components/RequireOxyAuth.js +294 -0
  52. package/lib/module/ui/components/RequireOxyAuth.js.map +1 -0
  53. package/lib/module/ui/components/oauthNavigation.js +70 -0
  54. package/lib/module/ui/components/oauthNavigation.js.map +1 -0
  55. package/lib/module/ui/context/OxyContext.js +123 -5
  56. package/lib/module/ui/context/OxyContext.js.map +1 -1
  57. package/lib/module/ui/hooks/mutations/useAccountMutations.js +1 -1
  58. package/lib/module/ui/hooks/useAuth.js +19 -30
  59. package/lib/module/ui/hooks/useAuth.js.map +1 -1
  60. package/lib/module/ui/hooks/useSwitchableAccounts.js +38 -267
  61. package/lib/module/ui/hooks/useSwitchableAccounts.js.map +1 -1
  62. package/lib/module/ui/index.js +1 -5
  63. package/lib/module/ui/index.js.map +1 -1
  64. package/lib/module/ui/navigation/accountDialogManager.js +81 -0
  65. package/lib/module/ui/navigation/accountDialogManager.js.map +1 -0
  66. package/lib/module/ui/navigation/routes.js +0 -2
  67. package/lib/module/ui/navigation/routes.js.map +1 -1
  68. package/lib/module/ui/screens/ManageAccountScreen.js +4 -3
  69. package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
  70. package/lib/module/ui/session/createSessionClient.js +10 -2
  71. package/lib/module/ui/session/createSessionClient.js.map +1 -1
  72. package/lib/typescript/commonjs/index.d.ts +8 -12
  73. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/ui/client.d.ts +0 -4
  75. package/lib/typescript/commonjs/ui/client.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts +51 -0
  77. package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts.map +1 -0
  78. package/lib/typescript/commonjs/ui/components/OxyConsentScreen.d.ts +45 -0
  79. package/lib/typescript/commonjs/ui/components/OxyConsentScreen.d.ts.map +1 -0
  80. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts +2 -2
  81. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/ui/components/OxySignInButton.d.ts +56 -0
  83. package/lib/typescript/commonjs/ui/components/OxySignInButton.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts +11 -23
  85. package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/ui/components/RequireOxyAuth.d.ts +55 -0
  87. package/lib/typescript/commonjs/ui/components/RequireOxyAuth.d.ts.map +1 -0
  88. package/lib/typescript/commonjs/ui/components/oauthNavigation.d.ts +36 -0
  89. package/lib/typescript/commonjs/ui/components/oauthNavigation.d.ts.map +1 -0
  90. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +27 -1
  91. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  92. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  93. package/lib/typescript/commonjs/ui/hooks/queries/useServicesQueries.d.ts +1 -1
  94. package/lib/typescript/commonjs/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts +13 -152
  97. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +1 -1
  98. package/lib/typescript/commonjs/ui/index.d.ts +2 -5
  99. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/ui/navigation/accountDialogManager.d.ts +49 -0
  101. package/lib/typescript/commonjs/ui/navigation/accountDialogManager.d.ts.map +1 -0
  102. package/lib/typescript/commonjs/ui/navigation/routes.d.ts +1 -1
  103. package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
  104. package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts +1 -1
  105. package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  106. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +9 -2
  107. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -1
  108. package/lib/typescript/commonjs/ui/types/navigation.d.ts +27 -0
  109. package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
  110. package/lib/typescript/module/index.d.ts +8 -12
  111. package/lib/typescript/module/index.d.ts.map +1 -1
  112. package/lib/typescript/module/ui/client.d.ts +0 -4
  113. package/lib/typescript/module/ui/client.d.ts.map +1 -1
  114. package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts +51 -0
  115. package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts.map +1 -0
  116. package/lib/typescript/module/ui/components/OxyConsentScreen.d.ts +45 -0
  117. package/lib/typescript/module/ui/components/OxyConsentScreen.d.ts.map +1 -0
  118. package/lib/typescript/module/ui/components/OxyProvider.d.ts +2 -2
  119. package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
  120. package/lib/typescript/module/ui/components/OxySignInButton.d.ts +56 -0
  121. package/lib/typescript/module/ui/components/OxySignInButton.d.ts.map +1 -1
  122. package/lib/typescript/module/ui/components/ProfileButton.d.ts +11 -23
  123. package/lib/typescript/module/ui/components/ProfileButton.d.ts.map +1 -1
  124. package/lib/typescript/module/ui/components/RequireOxyAuth.d.ts +55 -0
  125. package/lib/typescript/module/ui/components/RequireOxyAuth.d.ts.map +1 -0
  126. package/lib/typescript/module/ui/components/oauthNavigation.d.ts +36 -0
  127. package/lib/typescript/module/ui/components/oauthNavigation.d.ts.map +1 -0
  128. package/lib/typescript/module/ui/context/OxyContext.d.ts +27 -1
  129. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  130. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  131. package/lib/typescript/module/ui/hooks/queries/useServicesQueries.d.ts +1 -1
  132. package/lib/typescript/module/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
  133. package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
  134. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts +13 -152
  135. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +1 -1
  136. package/lib/typescript/module/ui/index.d.ts +2 -5
  137. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  138. package/lib/typescript/module/ui/navigation/accountDialogManager.d.ts +49 -0
  139. package/lib/typescript/module/ui/navigation/accountDialogManager.d.ts.map +1 -0
  140. package/lib/typescript/module/ui/navigation/routes.d.ts +1 -1
  141. package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
  142. package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts +1 -1
  143. package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  144. package/lib/typescript/module/ui/session/createSessionClient.d.ts +9 -2
  145. package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -1
  146. package/lib/typescript/module/ui/types/navigation.d.ts +27 -0
  147. package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
  148. package/package.json +4 -4
  149. package/src/index.ts +33 -27
  150. package/src/ui/client.ts +0 -4
  151. package/src/ui/components/OxyAccountDialog.tsx +735 -0
  152. package/src/ui/components/OxyConsentScreen.tsx +456 -0
  153. package/src/ui/components/OxyProvider.tsx +14 -6
  154. package/src/ui/components/OxySignInButton.tsx +227 -22
  155. package/src/ui/components/ProfileButton.tsx +23 -126
  156. package/src/ui/components/RequireOxyAuth.tsx +266 -0
  157. package/src/ui/components/__tests__/OxyConsentScreen.test.tsx +130 -0
  158. package/src/ui/components/oauthNavigation.ts +89 -0
  159. package/src/ui/context/OxyContext.tsx +176 -9
  160. package/src/ui/context/__tests__/OxyContext.coldBoot.test.tsx +165 -0
  161. package/src/ui/hooks/mutations/useAccountMutations.ts +1 -1
  162. package/src/ui/hooks/useAuth.ts +19 -31
  163. package/src/ui/hooks/useSwitchableAccounts.ts +42 -376
  164. package/src/ui/index.ts +2 -5
  165. package/src/ui/navigation/accountDialogManager.ts +84 -0
  166. package/src/ui/navigation/routes.ts +0 -4
  167. package/src/ui/screens/ManageAccountScreen.tsx +3 -2
  168. package/src/ui/session/createSessionClient.ts +9 -1
  169. package/src/ui/types/navigation.ts +27 -0
  170. package/lib/commonjs/ui/components/AccountMenu.js +0 -598
  171. package/lib/commonjs/ui/components/AccountMenu.js.map +0 -1
  172. package/lib/commonjs/ui/components/AccountMenuButton.js +0 -135
  173. package/lib/commonjs/ui/components/AccountMenuButton.js.map +0 -1
  174. package/lib/commonjs/ui/components/AccountSwitcher.js +0 -780
  175. package/lib/commonjs/ui/components/AccountSwitcher.js.map +0 -1
  176. package/lib/commonjs/ui/components/AnotherDeviceQR.js +0 -116
  177. package/lib/commonjs/ui/components/AnotherDeviceQR.js.map +0 -1
  178. package/lib/commonjs/ui/components/ProfileMenu.js +0 -518
  179. package/lib/commonjs/ui/components/ProfileMenu.js.map +0 -1
  180. package/lib/commonjs/ui/components/SignInAccountChooser.js +0 -189
  181. package/lib/commonjs/ui/components/SignInAccountChooser.js.map +0 -1
  182. package/lib/commonjs/ui/components/SignInModal.js +0 -600
  183. package/lib/commonjs/ui/components/SignInModal.js.map +0 -1
  184. package/lib/commonjs/ui/hooks/useOxyAuthSession.js +0 -501
  185. package/lib/commonjs/ui/hooks/useOxyAuthSession.js.map +0 -1
  186. package/lib/commonjs/ui/hooks/usePasswordSignIn.js +0 -170
  187. package/lib/commonjs/ui/hooks/usePasswordSignIn.js.map +0 -1
  188. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +0 -51
  189. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +0 -1
  190. package/lib/commonjs/ui/screens/OxyAuthScreen.js +0 -312
  191. package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +0 -1
  192. package/lib/commonjs/utils/deviceFlowSignIn.js +0 -109
  193. package/lib/commonjs/utils/deviceFlowSignIn.js.map +0 -1
  194. package/lib/module/ui/components/AccountMenu.js +0 -593
  195. package/lib/module/ui/components/AccountMenu.js.map +0 -1
  196. package/lib/module/ui/components/AccountMenuButton.js +0 -130
  197. package/lib/module/ui/components/AccountMenuButton.js.map +0 -1
  198. package/lib/module/ui/components/AccountSwitcher.js +0 -774
  199. package/lib/module/ui/components/AccountSwitcher.js.map +0 -1
  200. package/lib/module/ui/components/AnotherDeviceQR.js +0 -110
  201. package/lib/module/ui/components/AnotherDeviceQR.js.map +0 -1
  202. package/lib/module/ui/components/ProfileMenu.js +0 -514
  203. package/lib/module/ui/components/ProfileMenu.js.map +0 -1
  204. package/lib/module/ui/components/SignInAccountChooser.js +0 -183
  205. package/lib/module/ui/components/SignInAccountChooser.js.map +0 -1
  206. package/lib/module/ui/components/SignInModal.js +0 -591
  207. package/lib/module/ui/components/SignInModal.js.map +0 -1
  208. package/lib/module/ui/hooks/useOxyAuthSession.js +0 -495
  209. package/lib/module/ui/hooks/useOxyAuthSession.js.map +0 -1
  210. package/lib/module/ui/hooks/usePasswordSignIn.js +0 -165
  211. package/lib/module/ui/hooks/usePasswordSignIn.js.map +0 -1
  212. package/lib/module/ui/screens/AccountSwitcherScreen.js +0 -48
  213. package/lib/module/ui/screens/AccountSwitcherScreen.js.map +0 -1
  214. package/lib/module/ui/screens/OxyAuthScreen.js +0 -306
  215. package/lib/module/ui/screens/OxyAuthScreen.js.map +0 -1
  216. package/lib/module/utils/deviceFlowSignIn.js +0 -106
  217. package/lib/module/utils/deviceFlowSignIn.js.map +0 -1
  218. package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts +0 -47
  219. package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts.map +0 -1
  220. package/lib/typescript/commonjs/ui/components/AccountMenuButton.d.ts +0 -26
  221. package/lib/typescript/commonjs/ui/components/AccountMenuButton.d.ts.map +0 -1
  222. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts +0 -46
  223. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts.map +0 -1
  224. package/lib/typescript/commonjs/ui/components/AnotherDeviceQR.d.ts +0 -34
  225. package/lib/typescript/commonjs/ui/components/AnotherDeviceQR.d.ts.map +0 -1
  226. package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts +0 -49
  227. package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts.map +0 -1
  228. package/lib/typescript/commonjs/ui/components/SignInAccountChooser.d.ts +0 -29
  229. package/lib/typescript/commonjs/ui/components/SignInAccountChooser.d.ts.map +0 -1
  230. package/lib/typescript/commonjs/ui/components/SignInModal.d.ts +0 -28
  231. package/lib/typescript/commonjs/ui/components/SignInModal.d.ts.map +0 -1
  232. package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts +0 -105
  233. package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts.map +0 -1
  234. package/lib/typescript/commonjs/ui/hooks/usePasswordSignIn.d.ts +0 -31
  235. package/lib/typescript/commonjs/ui/hooks/usePasswordSignIn.d.ts.map +0 -1
  236. package/lib/typescript/commonjs/ui/screens/AccountSwitcherScreen.d.ts +0 -11
  237. package/lib/typescript/commonjs/ui/screens/AccountSwitcherScreen.d.ts.map +0 -1
  238. package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts +0 -22
  239. package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts.map +0 -1
  240. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +0 -97
  241. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts.map +0 -1
  242. package/lib/typescript/module/ui/components/AccountMenu.d.ts +0 -47
  243. package/lib/typescript/module/ui/components/AccountMenu.d.ts.map +0 -1
  244. package/lib/typescript/module/ui/components/AccountMenuButton.d.ts +0 -26
  245. package/lib/typescript/module/ui/components/AccountMenuButton.d.ts.map +0 -1
  246. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts +0 -46
  247. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts.map +0 -1
  248. package/lib/typescript/module/ui/components/AnotherDeviceQR.d.ts +0 -34
  249. package/lib/typescript/module/ui/components/AnotherDeviceQR.d.ts.map +0 -1
  250. package/lib/typescript/module/ui/components/ProfileMenu.d.ts +0 -49
  251. package/lib/typescript/module/ui/components/ProfileMenu.d.ts.map +0 -1
  252. package/lib/typescript/module/ui/components/SignInAccountChooser.d.ts +0 -29
  253. package/lib/typescript/module/ui/components/SignInAccountChooser.d.ts.map +0 -1
  254. package/lib/typescript/module/ui/components/SignInModal.d.ts +0 -28
  255. package/lib/typescript/module/ui/components/SignInModal.d.ts.map +0 -1
  256. package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts +0 -105
  257. package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts.map +0 -1
  258. package/lib/typescript/module/ui/hooks/usePasswordSignIn.d.ts +0 -31
  259. package/lib/typescript/module/ui/hooks/usePasswordSignIn.d.ts.map +0 -1
  260. package/lib/typescript/module/ui/screens/AccountSwitcherScreen.d.ts +0 -11
  261. package/lib/typescript/module/ui/screens/AccountSwitcherScreen.d.ts.map +0 -1
  262. package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts +0 -22
  263. package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts.map +0 -1
  264. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +0 -97
  265. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +0 -1
  266. package/src/ui/components/AccountMenu.tsx +0 -645
  267. package/src/ui/components/AccountMenuButton.tsx +0 -126
  268. package/src/ui/components/AccountSwitcher.tsx +0 -751
  269. package/src/ui/components/AnotherDeviceQR.tsx +0 -119
  270. package/src/ui/components/ProfileMenu.tsx +0 -564
  271. package/src/ui/components/SignInAccountChooser.tsx +0 -162
  272. package/src/ui/components/SignInModal.tsx +0 -530
  273. package/src/ui/hooks/useOxyAuthSession.ts +0 -635
  274. package/src/ui/hooks/usePasswordSignIn.ts +0 -207
  275. package/src/ui/screens/AccountSwitcherScreen.tsx +0 -43
  276. package/src/ui/screens/OxyAuthScreen.tsx +0 -324
  277. package/src/utils/__tests__/deviceFlowSignIn.test.ts +0 -231
  278. package/src/utils/deviceFlowSignIn.ts +0 -152
@@ -0,0 +1,266 @@
1
+ /**
2
+ * RequireOxyAuth — the optional signed-out gate primitive (React Native).
3
+ *
4
+ * ONE shared way for any Oxy app to opt into a signed-out gate. It is entirely
5
+ * OPTIONAL: a public app renders its children untouched (`prompt="off"`); an
6
+ * auth-required app blocks them behind a signed-out state (`prompt="hard"`); an
7
+ * app that mostly works signed-out but wants a nudge shows a dismissible banner
8
+ * (`prompt="soft"`). Every mode reuses the ONE account dialog the provider
9
+ * already mounts — opening the sign-in surface is always
10
+ * `useOxy().openAccountDialog('signin')`; there is NO second dialog.
11
+ *
12
+ * Readiness gating (CRITICAL): the gate keys on the SDK's own readiness state
13
+ * (`useOxy().canUsePrivateApi` / `isPrivateApiPending`), NEVER on app-local
14
+ * hooks. While auth is still resolving (`isPrivateApiPending`) it renders a
15
+ * neutral loading state so the signed-out wall never flashes before the
16
+ * device-first cold boot concludes (the documented cold-boot race).
17
+ *
18
+ * Styling follows the sibling `OxyAccountDialog`/`OxySignInButton` pattern —
19
+ * `useTheme()` + `StyleSheet` (NOT NativeWind), so the gate renders correctly in
20
+ * EVERY consumer, including apps that do not run NativeWind (e.g. the accounts
21
+ * app).
22
+ */
23
+
24
+ import type React from 'react';
25
+ import { useState, type ReactNode } from 'react';
26
+ import { ActivityIndicator, Pressable, StyleSheet, View } from 'react-native';
27
+ import { useTheme } from '@oxyhq/bloom/theme';
28
+ import { Text } from '@oxyhq/bloom/typography';
29
+ import { useOxy } from '../context/OxyContext';
30
+ import { OxySignInButton } from './OxySignInButton';
31
+ import OxyLogo from './OxyLogo';
32
+
33
+ /**
34
+ * How `RequireOxyAuth` treats a signed-out (or still-resolving) session.
35
+ * - `off` — always render children (public app; a no-op provided for symmetry).
36
+ * - `soft` — render children, plus a dismissible sign-in banner when signed out.
37
+ * - `hard` — block children behind a centered signed-out state until signed in.
38
+ */
39
+ export type RequireOxyAuthPrompt = 'off' | 'soft' | 'hard';
40
+
41
+ export interface RequireOxyAuthProps {
42
+ children: ReactNode;
43
+ /** Gate behavior. @default 'hard' */
44
+ prompt?: RequireOxyAuthPrompt;
45
+ /** Replaces the neutral loading UI shown while auth is still resolving. */
46
+ loadingFallback?: ReactNode;
47
+ /** Replaces the entire default signed-out wall (`prompt="hard"`). */
48
+ signedOutFallback?: ReactNode;
49
+ /** Title for the default `prompt="hard"` wall. */
50
+ title?: string;
51
+ /** Subtitle for the default `prompt="hard"` wall. */
52
+ subtitle?: string;
53
+ /** Message for the `prompt="soft"` banner. */
54
+ bannerMessage?: string;
55
+ /** CTA label for the `prompt="soft"` banner. */
56
+ bannerActionLabel?: string;
57
+ }
58
+
59
+ const DEFAULT_TITLE = 'Sign in to continue';
60
+ const DEFAULT_SUBTITLE = 'Sign in with your Oxy account to continue.';
61
+ const DEFAULT_BANNER_MESSAGE = "You're browsing signed out.";
62
+ const DEFAULT_BANNER_ACTION = 'Sign in';
63
+
64
+ /**
65
+ * Optional signed-out gate. Wrap any subtree (or the whole app via the
66
+ * provider's `requireAuth` prop) to opt into a shared, readiness-safe wall.
67
+ */
68
+ export const RequireOxyAuth: React.FC<RequireOxyAuthProps> = ({
69
+ children,
70
+ prompt = 'hard',
71
+ loadingFallback,
72
+ signedOutFallback,
73
+ title,
74
+ subtitle,
75
+ bannerMessage,
76
+ bannerActionLabel,
77
+ }) => {
78
+ const { canUsePrivateApi, isPrivateApiPending, openAccountDialog } = useOxy();
79
+
80
+ // Public app: render straight through. Cheap enough to short-circuit before
81
+ // touching any gate UI.
82
+ if (prompt === 'off') {
83
+ return <>{children}</>;
84
+ }
85
+
86
+ // Signed in (and token ready): render the protected subtree in every mode.
87
+ if (canUsePrivateApi) {
88
+ return <>{children}</>;
89
+ }
90
+
91
+ if (prompt === 'soft') {
92
+ return (
93
+ <SoftGate
94
+ pending={isPrivateApiPending}
95
+ message={bannerMessage ?? DEFAULT_BANNER_MESSAGE}
96
+ actionLabel={bannerActionLabel ?? DEFAULT_BANNER_ACTION}
97
+ onSignIn={() => openAccountDialog('signin')}
98
+ >
99
+ {children}
100
+ </SoftGate>
101
+ );
102
+ }
103
+
104
+ // prompt === 'hard'
105
+ if (isPrivateApiPending) {
106
+ return loadingFallback ? <>{loadingFallback}</> : <NeutralLoading />;
107
+ }
108
+ if (signedOutFallback) {
109
+ return <>{signedOutFallback}</>;
110
+ }
111
+ return <HardWall title={title ?? DEFAULT_TITLE} subtitle={subtitle ?? DEFAULT_SUBTITLE} />;
112
+ };
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Soft gate — children + a dismissible sign-in banner while signed out.
116
+ // ---------------------------------------------------------------------------
117
+
118
+ interface SoftGateProps {
119
+ children: ReactNode;
120
+ pending: boolean;
121
+ message: string;
122
+ actionLabel: string;
123
+ onSignIn: () => void;
124
+ }
125
+
126
+ const SoftGate: React.FC<SoftGateProps> = ({ children, pending, message, actionLabel, onSignIn }) => {
127
+ const theme = useTheme();
128
+ const [dismissed, setDismissed] = useState(false);
129
+
130
+ // Never surface the nudge while auth is still resolving — only once the boot
131
+ // has concluded signed out and the user has not dismissed it.
132
+ const showBanner = !pending && !dismissed;
133
+
134
+ return (
135
+ <View style={styles.softRoot}>
136
+ {showBanner ? (
137
+ <View style={[styles.banner, { backgroundColor: theme.colors.card, borderColor: theme.colors.border }]}>
138
+ <OxyLogo variant="icon" size={20} fillColor={theme.colors.primary} />
139
+ <Text style={[styles.bannerText, { color: theme.colors.text }]} numberOfLines={2}>
140
+ {message}
141
+ </Text>
142
+ <Pressable
143
+ onPress={onSignIn}
144
+ style={[styles.bannerCta, { backgroundColor: theme.colors.primary }]}
145
+ accessibilityRole="button"
146
+ accessibilityLabel={actionLabel}
147
+ >
148
+ <Text style={styles.bannerCtaText}>{actionLabel}</Text>
149
+ </Pressable>
150
+ <Pressable
151
+ onPress={() => setDismissed(true)}
152
+ style={styles.bannerDismiss}
153
+ accessibilityRole="button"
154
+ accessibilityLabel="Dismiss"
155
+ hitSlop={8}
156
+ >
157
+ <Text style={[styles.bannerDismissText, { color: theme.colors.textSecondary }]}>✕</Text>
158
+ </Pressable>
159
+ </View>
160
+ ) : null}
161
+ <View style={styles.softBody}>{children}</View>
162
+ </View>
163
+ );
164
+ };
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Hard wall — centered signed-out state with the "Sign in with Oxy" CTA.
168
+ // ---------------------------------------------------------------------------
169
+
170
+ const HardWall: React.FC<{ title: string; subtitle: string }> = ({ title, subtitle }) => {
171
+ const theme = useTheme();
172
+ return (
173
+ <View style={[styles.wallRoot, { backgroundColor: theme.colors.background }]}>
174
+ <View style={[styles.logoBadge, { backgroundColor: theme.colors.primarySubtle }]}>
175
+ <OxyLogo variant="icon" size={44} fillColor={theme.colors.primary} />
176
+ </View>
177
+ <Text style={[styles.wallTitle, { color: theme.colors.text }]}>{title}</Text>
178
+ <Text style={[styles.wallSubtitle, { color: theme.colors.textSecondary }]}>{subtitle}</Text>
179
+ <OxySignInButton variant="contained" style={styles.wallCta} />
180
+ </View>
181
+ );
182
+ };
183
+
184
+ const NeutralLoading: React.FC = () => {
185
+ const theme = useTheme();
186
+ return (
187
+ <View style={[styles.wallRoot, { backgroundColor: theme.colors.background }]}>
188
+ <ActivityIndicator color={theme.colors.primary} />
189
+ </View>
190
+ );
191
+ };
192
+
193
+ const styles = StyleSheet.create({
194
+ softRoot: {
195
+ flex: 1,
196
+ },
197
+ softBody: {
198
+ flex: 1,
199
+ },
200
+ banner: {
201
+ flexDirection: 'row',
202
+ alignItems: 'center',
203
+ gap: 10,
204
+ paddingVertical: 8,
205
+ paddingHorizontal: 12,
206
+ borderBottomWidth: StyleSheet.hairlineWidth,
207
+ },
208
+ bannerText: {
209
+ flex: 1,
210
+ fontSize: 13.5,
211
+ },
212
+ bannerCta: {
213
+ paddingVertical: 6,
214
+ paddingHorizontal: 14,
215
+ borderRadius: 999,
216
+ },
217
+ bannerCtaText: {
218
+ color: '#FFFFFF',
219
+ fontSize: 13,
220
+ fontWeight: '700',
221
+ },
222
+ bannerDismiss: {
223
+ width: 28,
224
+ height: 28,
225
+ alignItems: 'center',
226
+ justifyContent: 'center',
227
+ },
228
+ bannerDismissText: {
229
+ fontSize: 15,
230
+ fontWeight: '600',
231
+ },
232
+ wallRoot: {
233
+ flex: 1,
234
+ alignItems: 'center',
235
+ justifyContent: 'center',
236
+ padding: 24,
237
+ gap: 16,
238
+ },
239
+ logoBadge: {
240
+ width: 88,
241
+ height: 88,
242
+ borderRadius: 44,
243
+ alignItems: 'center',
244
+ justifyContent: 'center',
245
+ marginBottom: 4,
246
+ },
247
+ wallTitle: {
248
+ fontSize: 24,
249
+ fontWeight: '800',
250
+ letterSpacing: -0.4,
251
+ textAlign: 'center',
252
+ },
253
+ wallSubtitle: {
254
+ fontSize: 15,
255
+ lineHeight: 21,
256
+ textAlign: 'center',
257
+ maxWidth: 340,
258
+ },
259
+ wallCta: {
260
+ marginTop: 8,
261
+ width: '100%',
262
+ maxWidth: 320,
263
+ },
264
+ });
265
+
266
+ export default RequireOxyAuth;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * `OxyConsentScreen` — the unified OAuth consent surface. These tests exercise
3
+ * the pure presentational contract: scopes render, privacy/terms links appear
4
+ * only when supplied, allow/deny fire their handlers, `busy` disables both
5
+ * actions, and the account badge follows the display-name rule (`displayName`
6
+ * else `handle`).
7
+ */
8
+ import { render, fireEvent } from '@testing-library/react';
9
+ import {
10
+ OxyConsentScreen,
11
+ type OxyConsentApplication,
12
+ type OxyConsentScreenProps,
13
+ } from '../OxyConsentScreen';
14
+
15
+ // OxyLogo renders through react-native-svg, which is not meaningful (and not
16
+ // resolvable) under the jsdom RN stub — replace it with an inert node.
17
+ jest.mock('../OxyLogo', () => ({ __esModule: true, default: () => null }));
18
+
19
+ const baseApp: OxyConsentApplication = { name: 'Acme Notes' };
20
+
21
+ function renderScreen(props: Partial<OxyConsentScreenProps> = {}) {
22
+ const onAllow = jest.fn();
23
+ const onDeny = jest.fn();
24
+ const view = render(
25
+ <OxyConsentScreen
26
+ application={props.application ?? baseApp}
27
+ scopes={props.scopes ?? ['openid', 'profile']}
28
+ user={props.user}
29
+ onAllow={props.onAllow ?? onAllow}
30
+ onDeny={props.onDeny ?? onDeny}
31
+ busy={props.busy}
32
+ error={props.error}
33
+ />,
34
+ );
35
+ return { ...view, onAllow: props.onAllow ?? onAllow, onDeny: props.onDeny ?? onDeny };
36
+ }
37
+
38
+ describe('OxyConsentScreen', () => {
39
+ it('renders every requested scope with a friendly label and falls back to the raw scope for unknown ones', () => {
40
+ const { getByTestId } = renderScreen({ scopes: ['openid', 'files:write', 'custom:thing'] });
41
+
42
+ expect(getByTestId('consent-scope-openid').textContent).toContain('Confirm your identity');
43
+ expect(getByTestId('consent-scope-files:write').textContent).toContain('Upload and modify your files');
44
+ // Unknown scope → the raw scope string is shown, never an empty row.
45
+ expect(getByTestId('consent-scope-custom:thing').textContent).toContain('custom:thing');
46
+ });
47
+
48
+ it('shows the basic-permissions fallback when no scopes are requested', () => {
49
+ const { getByTestId, queryByTestId } = renderScreen({ scopes: [] });
50
+ expect(getByTestId('consent-scope-basic').textContent).toContain('Sign you in and read your basic profile');
51
+ expect(queryByTestId('consent-scope-openid')).toBeNull();
52
+ });
53
+
54
+ it('renders the provenance line for an official application', () => {
55
+ const { getByTestId } = renderScreen({ application: { name: 'Oxy Console', isOfficial: true } });
56
+ expect(getByTestId('consent-provenance').textContent).toContain('Official Oxy application');
57
+ });
58
+
59
+ it('renders "Published by …" provenance for a third-party app with a developer', () => {
60
+ const { getByTestId } = renderScreen({
61
+ application: { name: 'Third Party', developerName: 'Widgets Inc' },
62
+ });
63
+ expect(getByTestId('consent-provenance').textContent).toContain('Widgets Inc');
64
+ });
65
+
66
+ it('omits privacy/terms links when the application does not supply them', () => {
67
+ const { queryByTestId } = renderScreen();
68
+ expect(queryByTestId('consent-link-privacy')).toBeNull();
69
+ expect(queryByTestId('consent-link-terms')).toBeNull();
70
+ });
71
+
72
+ it('renders privacy and terms links when supplied (field 2b)', () => {
73
+ const { getByTestId } = renderScreen({
74
+ application: {
75
+ name: 'Acme Notes',
76
+ privacyPolicyUrl: 'https://acme.example/privacy',
77
+ termsUrl: 'https://acme.example/terms',
78
+ },
79
+ });
80
+ expect(getByTestId('consent-link-privacy')).not.toBeNull();
81
+ expect(getByTestId('consent-link-terms')).not.toBeNull();
82
+ });
83
+
84
+ it('fires onAllow and onDeny from the decision buttons', () => {
85
+ const { getByTestId, onAllow, onDeny } = renderScreen();
86
+
87
+ fireEvent.click(getByTestId('consent-allow'));
88
+ expect(onAllow).toHaveBeenCalledTimes(1);
89
+
90
+ fireEvent.click(getByTestId('consent-deny'));
91
+ expect(onDeny).toHaveBeenCalledTimes(1);
92
+ });
93
+
94
+ it('disables both actions while busy', () => {
95
+ const { getByTestId, onAllow, onDeny } = renderScreen({ busy: true });
96
+
97
+ const allow = getByTestId('consent-allow') as HTMLButtonElement;
98
+ const deny = getByTestId('consent-deny') as HTMLButtonElement;
99
+ expect(allow.disabled).toBe(true);
100
+ expect(deny.disabled).toBe(true);
101
+
102
+ // A disabled jsdom <button> does not fire click — the handlers stay untouched.
103
+ fireEvent.click(allow);
104
+ fireEvent.click(deny);
105
+ expect(onAllow).not.toHaveBeenCalled();
106
+ expect(onDeny).not.toHaveBeenCalled();
107
+ });
108
+
109
+ it('renders the authorizing account using the display name when present', () => {
110
+ const { getByTestId } = renderScreen({
111
+ user: { displayName: ' Alice A ', handle: 'alice', avatarUri: 'file-1' },
112
+ });
113
+ expect(getByTestId('consent-account-name').textContent).toBe('Alice A');
114
+ });
115
+
116
+ it('falls back to the handle when the account has no display name (D5)', () => {
117
+ const { getByTestId } = renderScreen({ user: { handle: 'bob' } });
118
+ expect(getByTestId('consent-account-name').textContent).toBe('bob');
119
+ });
120
+
121
+ it('omits the account badge entirely when no user is supplied', () => {
122
+ const { queryByTestId } = renderScreen({ user: undefined });
123
+ expect(queryByTestId('consent-account')).toBeNull();
124
+ });
125
+
126
+ it('renders a blocking error when supplied', () => {
127
+ const { getByTestId } = renderScreen({ error: 'This request has expired.' });
128
+ expect(getByTestId('consent-error').textContent).toContain('This request has expired.');
129
+ });
130
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Platform navigation for the third-party "Sign in with Oxy" OAuth flow.
3
+ *
4
+ * Kept out of `OxySignInButton` so the button's routing logic can be unit-tested
5
+ * without driving a real browser navigation (jsdom's `location.assign` is
6
+ * non-configurable and cannot be spied). Both entry points hand a fully-built
7
+ * `auth.oxy.so/authorize` URL to the platform — never FedCM, an SSO bounce, or
8
+ * an Oxy session cookie.
9
+ */
10
+
11
+ import { Linking } from 'react-native';
12
+ import { logger } from '@oxyhq/core';
13
+
14
+ /** Minimal shape of the optional `expo-web-browser` auth-session result. */
15
+ interface WebBrowserAuthResult {
16
+ type?: string;
17
+ url?: string;
18
+ }
19
+
20
+ /** Minimal shape of the optional `expo-web-browser` native module we depend on. */
21
+ interface WebBrowserModule {
22
+ openAuthSessionAsync?: (url: string, redirectUrl: string) => Promise<WebBrowserAuthResult>;
23
+ }
24
+
25
+ /** Outcome of opening the authorize URL on native. */
26
+ export interface OpenAuthorizeResult {
27
+ /**
28
+ * The deep-link URL the auth session returned to (carries `?code=…&state=…`)
29
+ * when `expo-web-browser` observed it, else `null`. `null` means the RP must
30
+ * complete the exchange from its own deep-link handler (e.g. after the
31
+ * `Linking.openURL` fallback, which cannot observe the return URL).
32
+ */
33
+ redirectUrl: string | null;
34
+ }
35
+
36
+ /**
37
+ * Web: hand the TOP-LEVEL document to the OAuth authorize URL (a full-page
38
+ * redirect, not a popup) so the RP returns to its registered `redirect_uri`.
39
+ * No-op where `location` is unavailable (SSR / non-browser hosts).
40
+ */
41
+ export function redirectToAuthorize(url: string): void {
42
+ (globalThis as { location?: Location }).location?.assign(url);
43
+ }
44
+
45
+ /**
46
+ * Native: open the authorize URL in an in-app auth session via the optional
47
+ * `expo-web-browser` module (`openAuthSessionAsync` returns to `redirectUri`),
48
+ * degrading to `Linking.openURL` when the module is not installed — the same
49
+ * dynamic-import-with-fallback pattern services uses for haptics/netinfo.
50
+ *
51
+ * Returns the deep-link URL the session came back to when it can be observed, so
52
+ * the caller can hand `?code=…&state=…` back to the RP for the token exchange.
53
+ */
54
+ export async function openAuthorizeUrlNative(
55
+ url: string,
56
+ redirectUri: string,
57
+ ): Promise<OpenAuthorizeResult> {
58
+ try {
59
+ const mod = (await import('expo-web-browser')) as unknown as WebBrowserModule;
60
+ if (mod && typeof mod.openAuthSessionAsync === 'function') {
61
+ const result = await mod.openAuthSessionAsync(url, redirectUri);
62
+ const redirectUrl =
63
+ result && result.type === 'success' && typeof result.url === 'string'
64
+ ? result.url
65
+ : null;
66
+ return { redirectUrl };
67
+ }
68
+ } catch (error) {
69
+ logger.warn(
70
+ 'OxySignInButton: expo-web-browser auth session failed; falling back to Linking.openURL',
71
+ { component: 'oauthNavigation' },
72
+ error,
73
+ );
74
+ }
75
+
76
+ // Fallback: Linking cannot observe the return URL, so the RP completes the
77
+ // exchange from its own deep-link handler. A rejected openURL (e.g. an
78
+ // unregistered scheme) must not throw out of the sign-in flow.
79
+ try {
80
+ await Linking.openURL(url);
81
+ } catch (error) {
82
+ logger.warn(
83
+ 'OxySignInButton: Linking.openURL rejected the authorize URL',
84
+ { component: 'oauthNavigation' },
85
+ error,
86
+ );
87
+ }
88
+ return { redirectUrl: null };
89
+ }