@oxyhq/services 10.1.0 → 10.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/README.md +2 -2
  2. package/lib/commonjs/index.js +10 -0
  3. package/lib/commonjs/index.js.map +1 -1
  4. package/lib/commonjs/ui/components/AccountMenu.js +297 -226
  5. package/lib/commonjs/ui/components/AccountMenu.js.map +1 -1
  6. package/lib/commonjs/ui/components/AccountMenuButton.js.map +1 -1
  7. package/lib/commonjs/ui/components/OxySignInButton.js +1 -1
  8. package/lib/commonjs/ui/components/SignInModal.js +23 -14
  9. package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
  10. package/lib/commonjs/ui/components/accountMenuRows.js +18 -30
  11. package/lib/commonjs/ui/components/accountMenuRows.js.map +1 -1
  12. package/lib/commonjs/ui/context/OxyContext.js +33 -65
  13. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  14. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +7 -13
  15. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  16. package/lib/commonjs/ui/hooks/useAuth.js +9 -39
  17. package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
  18. package/lib/commonjs/ui/hooks/useDeviceAccounts.js +285 -0
  19. package/lib/commonjs/ui/hooks/useDeviceAccounts.js.map +1 -0
  20. package/lib/commonjs/ui/hooks/useSessionManagement.js +5 -6
  21. package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
  22. package/lib/commonjs/ui/hooks/useSessionSocket.js +4 -5
  23. package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
  24. package/lib/commonjs/ui/hooks/useWebSSO.js +1 -1
  25. package/lib/commonjs/ui/navigation/routes.js +7 -7
  26. package/lib/commonjs/ui/navigation/routes.js.map +1 -1
  27. package/lib/commonjs/ui/screens/OxyAuthScreen.js +21 -12
  28. package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
  29. package/lib/commonjs/ui/screens/ProfileScreen.js +18 -20
  30. package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
  31. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +4 -4
  32. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  33. package/lib/commonjs/ui/screens/{karma/KarmaAboutScreen.js → trust/TrustAboutScreen.js} +11 -11
  34. package/lib/commonjs/ui/screens/trust/TrustAboutScreen.js.map +1 -0
  35. package/lib/commonjs/ui/screens/{karma/KarmaCenterScreen.js → trust/TrustCenterScreen.js} +91 -41
  36. package/lib/commonjs/ui/screens/trust/TrustCenterScreen.js.map +1 -0
  37. package/lib/commonjs/ui/screens/{karma/KarmaFAQScreen.js → trust/TrustFAQScreen.js} +11 -11
  38. package/lib/commonjs/ui/screens/{karma/KarmaFAQScreen.js.map → trust/TrustFAQScreen.js.map} +1 -1
  39. package/lib/commonjs/ui/screens/{karma/KarmaLeaderboardScreen.js → trust/TrustLeaderboardScreen.js} +63 -42
  40. package/lib/commonjs/ui/screens/trust/TrustLeaderboardScreen.js.map +1 -0
  41. package/lib/commonjs/ui/screens/{karma/KarmaRewardsScreen.js → trust/TrustRewardsScreen.js} +54 -54
  42. package/lib/commonjs/ui/screens/trust/TrustRewardsScreen.js.map +1 -0
  43. package/lib/commonjs/ui/screens/{karma/KarmaRulesScreen.js → trust/TrustRulesScreen.js} +45 -16
  44. package/lib/commonjs/ui/screens/trust/TrustRulesScreen.js.map +1 -0
  45. package/lib/commonjs/ui/screens/trust/trustTier.js +23 -0
  46. package/lib/commonjs/ui/screens/trust/trustTier.js.map +1 -0
  47. package/lib/commonjs/utils/deviceFlowSignIn.js +12 -10
  48. package/lib/commonjs/utils/deviceFlowSignIn.js.map +1 -1
  49. package/lib/module/index.js +3 -0
  50. package/lib/module/index.js.map +1 -1
  51. package/lib/module/ui/components/AccountMenu.js +297 -226
  52. package/lib/module/ui/components/AccountMenu.js.map +1 -1
  53. package/lib/module/ui/components/AccountMenuButton.js.map +1 -1
  54. package/lib/module/ui/components/OxySignInButton.js +1 -1
  55. package/lib/module/ui/components/SignInModal.js +23 -14
  56. package/lib/module/ui/components/SignInModal.js.map +1 -1
  57. package/lib/module/ui/components/accountMenuRows.js +18 -30
  58. package/lib/module/ui/components/accountMenuRows.js.map +1 -1
  59. package/lib/module/ui/context/OxyContext.js +33 -65
  60. package/lib/module/ui/context/OxyContext.js.map +1 -1
  61. package/lib/module/ui/context/hooks/useAuthOperations.js +7 -13
  62. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  63. package/lib/module/ui/hooks/useAuth.js +9 -39
  64. package/lib/module/ui/hooks/useAuth.js.map +1 -1
  65. package/lib/module/ui/hooks/useDeviceAccounts.js +281 -0
  66. package/lib/module/ui/hooks/useDeviceAccounts.js.map +1 -0
  67. package/lib/module/ui/hooks/useSessionManagement.js +5 -6
  68. package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
  69. package/lib/module/ui/hooks/useSessionSocket.js +4 -5
  70. package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
  71. package/lib/module/ui/hooks/useWebSSO.js +1 -1
  72. package/lib/module/ui/navigation/routes.js +7 -7
  73. package/lib/module/ui/navigation/routes.js.map +1 -1
  74. package/lib/module/ui/screens/OxyAuthScreen.js +21 -12
  75. package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
  76. package/lib/module/ui/screens/ProfileScreen.js +18 -20
  77. package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
  78. package/lib/module/ui/screens/WelcomeNewUserScreen.js +4 -4
  79. package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  80. package/lib/module/ui/screens/{karma/KarmaAboutScreen.js → trust/TrustAboutScreen.js} +11 -11
  81. package/lib/module/ui/screens/trust/TrustAboutScreen.js.map +1 -0
  82. package/lib/module/ui/screens/{karma/KarmaCenterScreen.js → trust/TrustCenterScreen.js} +92 -42
  83. package/lib/module/ui/screens/trust/TrustCenterScreen.js.map +1 -0
  84. package/lib/module/ui/screens/{karma/KarmaFAQScreen.js → trust/TrustFAQScreen.js} +11 -11
  85. package/lib/module/ui/screens/{karma/KarmaFAQScreen.js.map → trust/TrustFAQScreen.js.map} +1 -1
  86. package/lib/module/ui/screens/{karma/KarmaLeaderboardScreen.js → trust/TrustLeaderboardScreen.js} +63 -42
  87. package/lib/module/ui/screens/trust/TrustLeaderboardScreen.js.map +1 -0
  88. package/lib/module/ui/screens/{karma/KarmaRewardsScreen.js → trust/TrustRewardsScreen.js} +54 -54
  89. package/lib/module/ui/screens/trust/TrustRewardsScreen.js.map +1 -0
  90. package/lib/module/ui/screens/{karma/KarmaRulesScreen.js → trust/TrustRulesScreen.js} +45 -16
  91. package/lib/module/ui/screens/trust/TrustRulesScreen.js.map +1 -0
  92. package/lib/module/ui/screens/trust/trustTier.js +19 -0
  93. package/lib/module/ui/screens/trust/trustTier.js.map +1 -0
  94. package/lib/module/utils/deviceFlowSignIn.js +13 -10
  95. package/lib/module/utils/deviceFlowSignIn.js.map +1 -1
  96. package/lib/typescript/commonjs/index.d.ts +3 -1
  97. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  98. package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts +30 -10
  99. package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/ui/components/SignInModal.d.ts +1 -1
  101. package/lib/typescript/commonjs/ui/components/SignInModal.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts +19 -12
  103. package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts.map +1 -1
  104. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +3 -3
  105. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  106. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +1 -7
  108. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts +3 -7
  110. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts +133 -0
  112. package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts.map +1 -0
  113. package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts.map +1 -1
  115. package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts +1 -1
  116. package/lib/typescript/commonjs/ui/navigation/routes.d.ts +1 -1
  117. package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts.map +1 -1
  118. package/lib/typescript/commonjs/ui/screens/ProfileScreen.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/ui/screens/trust/TrustAboutScreen.d.ts +5 -0
  120. package/lib/typescript/commonjs/ui/screens/{karma/KarmaAboutScreen.d.ts.map → trust/TrustAboutScreen.d.ts.map} +1 -1
  121. package/lib/typescript/commonjs/ui/screens/trust/TrustCenterScreen.d.ts +5 -0
  122. package/lib/typescript/commonjs/ui/screens/trust/TrustCenterScreen.d.ts.map +1 -0
  123. package/lib/typescript/{module/ui/screens/karma/KarmaFAQScreen.d.ts → commonjs/ui/screens/trust/TrustFAQScreen.d.ts} +1 -1
  124. package/lib/typescript/commonjs/ui/screens/trust/TrustFAQScreen.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/ui/screens/trust/TrustLeaderboardScreen.d.ts +5 -0
  126. package/lib/typescript/commonjs/ui/screens/trust/TrustLeaderboardScreen.d.ts.map +1 -0
  127. package/lib/typescript/commonjs/ui/screens/trust/TrustRewardsScreen.d.ts +5 -0
  128. package/lib/typescript/commonjs/ui/screens/{karma/KarmaRewardsScreen.d.ts.map → trust/TrustRewardsScreen.d.ts.map} +1 -1
  129. package/lib/typescript/commonjs/ui/screens/trust/TrustRulesScreen.d.ts +5 -0
  130. package/lib/typescript/commonjs/ui/screens/trust/TrustRulesScreen.d.ts.map +1 -0
  131. package/lib/typescript/commonjs/ui/screens/trust/trustTier.d.ts +9 -0
  132. package/lib/typescript/commonjs/ui/screens/trust/trustTier.d.ts.map +1 -0
  133. package/lib/typescript/commonjs/ui/types/navigation.d.ts +1 -1
  134. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +11 -9
  135. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts.map +1 -1
  136. package/lib/typescript/module/index.d.ts +3 -1
  137. package/lib/typescript/module/index.d.ts.map +1 -1
  138. package/lib/typescript/module/ui/components/AccountMenu.d.ts +30 -10
  139. package/lib/typescript/module/ui/components/AccountMenu.d.ts.map +1 -1
  140. package/lib/typescript/module/ui/components/SignInModal.d.ts +1 -1
  141. package/lib/typescript/module/ui/components/SignInModal.d.ts.map +1 -1
  142. package/lib/typescript/module/ui/components/accountMenuRows.d.ts +19 -12
  143. package/lib/typescript/module/ui/components/accountMenuRows.d.ts.map +1 -1
  144. package/lib/typescript/module/ui/context/OxyContext.d.ts +3 -3
  145. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  146. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  147. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +1 -7
  148. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  149. package/lib/typescript/module/ui/hooks/useAuth.d.ts +3 -7
  150. package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
  151. package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts +133 -0
  152. package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts.map +1 -0
  153. package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts.map +1 -1
  154. package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts.map +1 -1
  155. package/lib/typescript/module/ui/hooks/useWebSSO.d.ts +1 -1
  156. package/lib/typescript/module/ui/navigation/routes.d.ts +1 -1
  157. package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts.map +1 -1
  158. package/lib/typescript/module/ui/screens/ProfileScreen.d.ts.map +1 -1
  159. package/lib/typescript/module/ui/screens/trust/TrustAboutScreen.d.ts +5 -0
  160. package/lib/typescript/module/ui/screens/{karma/KarmaAboutScreen.d.ts.map → trust/TrustAboutScreen.d.ts.map} +1 -1
  161. package/lib/typescript/module/ui/screens/trust/TrustCenterScreen.d.ts +5 -0
  162. package/lib/typescript/module/ui/screens/trust/TrustCenterScreen.d.ts.map +1 -0
  163. package/lib/typescript/{commonjs/ui/screens/karma/KarmaFAQScreen.d.ts → module/ui/screens/trust/TrustFAQScreen.d.ts} +1 -1
  164. package/lib/typescript/module/ui/screens/trust/TrustFAQScreen.d.ts.map +1 -0
  165. package/lib/typescript/module/ui/screens/trust/TrustLeaderboardScreen.d.ts +5 -0
  166. package/lib/typescript/module/ui/screens/trust/TrustLeaderboardScreen.d.ts.map +1 -0
  167. package/lib/typescript/module/ui/screens/trust/TrustRewardsScreen.d.ts +5 -0
  168. package/lib/typescript/module/ui/screens/{karma/KarmaRewardsScreen.d.ts.map → trust/TrustRewardsScreen.d.ts.map} +1 -1
  169. package/lib/typescript/module/ui/screens/trust/TrustRulesScreen.d.ts +5 -0
  170. package/lib/typescript/module/ui/screens/trust/TrustRulesScreen.d.ts.map +1 -0
  171. package/lib/typescript/module/ui/screens/trust/trustTier.d.ts +9 -0
  172. package/lib/typescript/module/ui/screens/trust/trustTier.d.ts.map +1 -0
  173. package/lib/typescript/module/ui/types/navigation.d.ts +1 -1
  174. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +11 -9
  175. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +1 -1
  176. package/package.json +1 -1
  177. package/src/index.ts +10 -1
  178. package/src/ui/components/AccountMenu.tsx +311 -253
  179. package/src/ui/components/AccountMenuButton.tsx +2 -2
  180. package/src/ui/components/OxySignInButton.tsx +1 -1
  181. package/src/ui/components/SignInModal.tsx +26 -16
  182. package/src/ui/components/accountMenuRows.ts +28 -40
  183. package/src/ui/context/OxyContext.tsx +43 -61
  184. package/src/ui/context/hooks/useAuthOperations.ts +7 -13
  185. package/src/ui/hooks/useAuth.ts +11 -48
  186. package/src/ui/hooks/useDeviceAccounts.ts +348 -0
  187. package/src/ui/hooks/useSessionManagement.ts +5 -14
  188. package/src/ui/hooks/useSessionSocket.ts +4 -5
  189. package/src/ui/hooks/useWebSSO.ts +1 -1
  190. package/src/ui/navigation/routes.ts +13 -13
  191. package/src/ui/screens/OxyAuthScreen.tsx +21 -12
  192. package/src/ui/screens/ProfileScreen.tsx +15 -17
  193. package/src/ui/screens/WelcomeNewUserScreen.tsx +2 -2
  194. package/src/ui/screens/{karma/KarmaAboutScreen.tsx → trust/TrustAboutScreen.tsx} +15 -15
  195. package/src/ui/screens/{karma/KarmaCenterScreen.tsx → trust/TrustCenterScreen.tsx} +87 -41
  196. package/src/ui/screens/{karma/KarmaFAQScreen.tsx → trust/TrustFAQScreen.tsx} +10 -10
  197. package/src/ui/screens/trust/TrustLeaderboardScreen.tsx +101 -0
  198. package/src/ui/screens/{karma/KarmaRewardsScreen.tsx → trust/TrustRewardsScreen.tsx} +54 -54
  199. package/src/ui/screens/{karma/KarmaRulesScreen.tsx → trust/TrustRulesScreen.tsx} +27 -13
  200. package/src/ui/screens/trust/trustTier.ts +20 -0
  201. package/src/ui/types/navigation.ts +1 -2
  202. package/src/utils/__tests__/deviceFlowSignIn.test.ts +2 -3
  203. package/src/utils/deviceFlowSignIn.ts +18 -12
  204. package/lib/commonjs/ui/screens/karma/KarmaAboutScreen.js.map +0 -1
  205. package/lib/commonjs/ui/screens/karma/KarmaCenterScreen.js.map +0 -1
  206. package/lib/commonjs/ui/screens/karma/KarmaLeaderboardScreen.js.map +0 -1
  207. package/lib/commonjs/ui/screens/karma/KarmaRewardsScreen.js.map +0 -1
  208. package/lib/commonjs/ui/screens/karma/KarmaRulesScreen.js.map +0 -1
  209. package/lib/module/ui/screens/karma/KarmaAboutScreen.js.map +0 -1
  210. package/lib/module/ui/screens/karma/KarmaCenterScreen.js.map +0 -1
  211. package/lib/module/ui/screens/karma/KarmaLeaderboardScreen.js.map +0 -1
  212. package/lib/module/ui/screens/karma/KarmaRewardsScreen.js.map +0 -1
  213. package/lib/module/ui/screens/karma/KarmaRulesScreen.js.map +0 -1
  214. package/lib/typescript/commonjs/ui/screens/karma/KarmaAboutScreen.d.ts +0 -5
  215. package/lib/typescript/commonjs/ui/screens/karma/KarmaCenterScreen.d.ts +0 -5
  216. package/lib/typescript/commonjs/ui/screens/karma/KarmaCenterScreen.d.ts.map +0 -1
  217. package/lib/typescript/commonjs/ui/screens/karma/KarmaFAQScreen.d.ts.map +0 -1
  218. package/lib/typescript/commonjs/ui/screens/karma/KarmaLeaderboardScreen.d.ts +0 -5
  219. package/lib/typescript/commonjs/ui/screens/karma/KarmaLeaderboardScreen.d.ts.map +0 -1
  220. package/lib/typescript/commonjs/ui/screens/karma/KarmaRewardsScreen.d.ts +0 -5
  221. package/lib/typescript/commonjs/ui/screens/karma/KarmaRulesScreen.d.ts +0 -5
  222. package/lib/typescript/commonjs/ui/screens/karma/KarmaRulesScreen.d.ts.map +0 -1
  223. package/lib/typescript/module/ui/screens/karma/KarmaAboutScreen.d.ts +0 -5
  224. package/lib/typescript/module/ui/screens/karma/KarmaCenterScreen.d.ts +0 -5
  225. package/lib/typescript/module/ui/screens/karma/KarmaCenterScreen.d.ts.map +0 -1
  226. package/lib/typescript/module/ui/screens/karma/KarmaFAQScreen.d.ts.map +0 -1
  227. package/lib/typescript/module/ui/screens/karma/KarmaLeaderboardScreen.d.ts +0 -5
  228. package/lib/typescript/module/ui/screens/karma/KarmaLeaderboardScreen.d.ts.map +0 -1
  229. package/lib/typescript/module/ui/screens/karma/KarmaRewardsScreen.d.ts +0 -5
  230. package/lib/typescript/module/ui/screens/karma/KarmaRulesScreen.d.ts +0 -5
  231. package/lib/typescript/module/ui/screens/karma/KarmaRulesScreen.d.ts.map +0 -1
  232. package/src/ui/screens/karma/KarmaLeaderboardScreen.tsx +0 -88
@@ -3,7 +3,7 @@ import { useCallback, useRef, useState } from 'react';
3
3
  import { TouchableOpacity, StyleSheet, Platform, type LayoutChangeEvent } from 'react-native';
4
4
  import { getAccountDisplayName } from '@oxyhq/core';
5
5
  import Avatar from './Avatar';
6
- import AccountMenu from './AccountMenu';
6
+ import AccountMenu, { type AccountMenuAnchor } from './AccountMenu';
7
7
  import { useOxy } from '../context/OxyContext';
8
8
  import { useI18n } from '../hooks/useI18n';
9
9
 
@@ -33,7 +33,7 @@ const AccountMenuButton: React.FC<AccountMenuButtonProps> = ({
33
33
  const { user, oxyServices, isAuthenticated } = useOxy();
34
34
  const { t, locale } = useI18n();
35
35
  const [open, setOpen] = useState(false);
36
- const [anchor, setAnchor] = useState<{ top: number; right: number } | null>(null);
36
+ const [anchor, setAnchor] = useState<AccountMenuAnchor | null>(null);
37
37
  const triggerRef = useRef<React.ComponentRef<typeof TouchableOpacity>>(null);
38
38
 
39
39
  const measureAnchor = useCallback(() => {
@@ -100,7 +100,7 @@ export const OxySignInButton: React.FC<OxySignInButtonProps> = ({
100
100
  }, []);
101
101
 
102
102
  // Handle button press
103
- // - Web: legacy full-screen Modal (dialog UX fits desktop / browser)
103
+ // - Web: full-screen modal (dialog UX fits desktop / browser)
104
104
  // - Native: bottom sheet (sheet UX fits iOS/Android)
105
105
  const handlePress = useCallback(() => {
106
106
  if (onPress) {
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * A semi-transparent full-screen modal that displays:
5
5
  * - QR code for scanning with Oxy Accounts app
6
- * - Button to open Oxy Auth popup
6
+ * - Button to open the Oxy Auth approval page
7
7
  *
8
8
  * Animates with fade-in effect.
9
9
  */
@@ -143,12 +143,11 @@ const SignInModal: React.FC = () => {
143
143
  // this is the device-flow equivalent of OAuth's code-for-token
144
144
  // exchange (RFC 8628 §3.4).
145
145
  //
146
- // Without that exchange the SDK has no bearer token and every
147
- // subsequent call (including `switchSession` -> `getTokenBySession`)
148
- // would 401 against the C1-hardened API. Once `claimSessionByToken`
149
- // plants the tokens in the HttpService, the rest of the session
150
- // wiring (state, persistence, language preference) flows through
151
- // the normal `switchSession` path.
146
+ // Without that exchange the SDK has no bearer token and the app never
147
+ // becomes authenticated even though the session is authorized server-side.
148
+ // Once `claimSessionByToken` plants the tokens in the HttpService, the rest
149
+ // of the session wiring (state, persistence, language preference) flows
150
+ // through the normal `switchSession` path.
152
151
  const handleAuthSuccess = useCallback(async (sessionId: string, sessionToken: string) => {
153
152
  if (isProcessingRef.current) return;
154
153
  isProcessingRef.current = true;
@@ -233,8 +232,14 @@ const SignInModal: React.FC = () => {
233
232
  });
234
233
  }, [oxyServices, handleAuthSuccess, cleanup]);
235
234
 
236
- // Start polling for authorization (fallback)
235
+ // Start polling for authorization.
236
+ //
237
+ // Idempotent: if a poll interval is already running this is a no-op, so the
238
+ // `connect_error` path (which also calls this) cannot stack a second
239
+ // interval on top of the always-on poll started in `generateAuthSession`.
237
240
  const startPolling = useCallback((sessionToken: string) => {
241
+ if (pollingIntervalRef.current) return;
242
+
238
243
  pollingIntervalRef.current = setInterval(async () => {
239
244
  if (isProcessingRef.current) return;
240
245
 
@@ -292,13 +297,18 @@ const SignInModal: React.FC = () => {
292
297
 
293
298
  setAuthSession({ sessionToken, expiresAt });
294
299
  setIsWaiting(true);
300
+ // Socket is the fast path; the poll is a transport-independent
301
+ // backstop that guarantees completion even if the socket connects
302
+ // but silently never delivers auth_update (RN transport /
303
+ // idle-timeout).
295
304
  connectSocket(sessionToken);
305
+ startPolling(sessionToken);
296
306
  } catch (err: unknown) {
297
307
  setError((err instanceof Error ? err.message : null) || 'Failed to create auth session');
298
308
  } finally {
299
309
  setIsLoading(false);
300
310
  }
301
- }, [oxyServices, connectSocket, clientId]);
311
+ }, [oxyServices, connectSocket, startPolling, clientId]);
302
312
 
303
313
  // Generate a cryptographically random session token.
304
314
  // 16 random bytes -> 32 hex chars (128 bits of entropy) — unguessable.
@@ -316,8 +326,8 @@ const SignInModal: React.FC = () => {
316
326
  return `oxyauth://${authSession.sessionToken}`;
317
327
  };
318
328
 
319
- // Open Oxy Auth popup
320
- const handleOpenAuthPopup = useCallback(async () => {
329
+ // Open Oxy Auth approval page for this device-flow session.
330
+ const handleOpenAuthApproval = useCallback(async () => {
321
331
  if (!authSession) return;
322
332
 
323
333
  const baseURL = oxyServices.getBaseURL();
@@ -343,7 +353,7 @@ const SignInModal: React.FC = () => {
343
353
  webUrl.searchParams.set('token', authSession.sessionToken);
344
354
 
345
355
  if (Platform.OS === 'web') {
346
- // Open popup window on web
356
+ // Open a separate approval window on web for the device-flow token.
347
357
  const width = 500;
348
358
  const height = 650;
349
359
  const screenWidth = window.screen?.width ?? width;
@@ -353,8 +363,8 @@ const SignInModal: React.FC = () => {
353
363
 
354
364
  window.open(
355
365
  webUrl.toString(),
356
- 'oxy-auth-popup',
357
- `width=${width},height=${height},left=${left},top=${top},popup=1`
366
+ 'oxy-auth-approval',
367
+ `width=${width},height=${height},left=${left},top=${top}`
358
368
  );
359
369
  } else {
360
370
  // Open in browser on native
@@ -438,9 +448,9 @@ const SignInModal: React.FC = () => {
438
448
  <View style={[styles.divider, { backgroundColor: 'rgba(255,255,255,0.3)' }]} />
439
449
  </View>
440
450
 
441
- {/* Open Auth Popup Button */}
451
+ {/* Open approval window */}
442
452
  <Button
443
- onPress={handleOpenAuthPopup}
453
+ onPress={handleOpenAuthApproval}
444
454
  icon={<OxyLogo variant="icon" size={20} fillColor={theme.colors.card} />}
445
455
  >
446
456
  Open Oxy Auth
@@ -1,60 +1,48 @@
1
- import type { User, ClientSession } from '@oxyhq/core';
2
- import { getAccountDisplayName, getAccountFallbackHandle } from '@oxyhq/core';
1
+ import type { DeviceAccount, DeviceAccountUser } from '../hooks/useDeviceAccounts';
3
2
 
4
3
  export interface AccountRow {
5
4
  sessionId: string;
5
+ /** Device-local refresh-cookie slot index (web silent-switch). */
6
+ authuser?: number;
6
7
  isActive: boolean;
7
8
  displayName: string;
8
9
  secondary: string | null;
9
10
  avatarUri?: string;
10
- user: User | null;
11
+ user: DeviceAccountUser | null;
11
12
  }
12
13
 
13
14
  export interface BuildAccountRowsInput {
14
- sessions: ClientSession[] | null | undefined;
15
- activeSessionId: string | null | undefined;
16
- user: User | null | undefined;
17
- locale: string;
18
- getAvatarUrl: (avatarId: string) => string;
15
+ /**
16
+ * Per-device account entries from {@link useDeviceAccounts}. Each entry
17
+ * already carries real per-account `displayName` / `email` / `avatarUrl` /
18
+ * `color`, so EVERY row (not just the active one) renders full identity.
19
+ */
20
+ accounts: DeviceAccount[];
19
21
  }
20
22
 
21
23
  /**
22
24
  * Pure builder for `AccountMenu` rows. Extracted so the multi-account display
23
25
  * logic can be unit-tested without rendering React Native.
24
26
  *
25
- * Each `sessions[i]` becomes one row. Only the row matching `activeSessionId`
26
- * carries the loaded `user` payload the others are placeholders shown by
27
- * fallback handle. This mirrors how `OxyContext` only hydrates one user at a
28
- * time.
27
+ * Maps each {@link DeviceAccount} (sourced from `useDeviceAccounts()`, which
28
+ * hydrates EVERY account with real name/email/avatar/color from the shared
29
+ * apex `refresh-all` path or the local fallback) into an `AccountRow`.
30
+ *
31
+ * `secondary` is the account's real email when present; otherwise it falls
32
+ * back to the `@handle` line. A missing email is NEVER synthesized into a fake
33
+ * `username@oxy.so` — the device-account layer already resolved `email` to the
34
+ * real value or the `@handle` fallback.
29
35
  */
30
36
  export function buildAccountRows({
31
- sessions,
32
- activeSessionId,
33
- user,
34
- locale,
35
- getAvatarUrl,
37
+ accounts,
36
38
  }: BuildAccountRowsInput): AccountRow[] {
37
- return (sessions ?? []).map((session: ClientSession) => {
38
- const isActive = session.sessionId === activeSessionId;
39
- const candidate: Partial<User> | null = isActive ? user ?? null : null;
40
- const displayName = getAccountDisplayName(
41
- candidate ?? { username: undefined },
42
- locale,
43
- );
44
- const handle = getAccountFallbackHandle(candidate ?? { username: undefined });
45
- const secondary = (candidate?.email)
46
- ?? (handle && candidate?.username ? `@${handle}` : handle)
47
- ?? null;
48
- const avatarUri = candidate?.avatar
49
- ? getAvatarUrl(candidate.avatar)
50
- : undefined;
51
- return {
52
- sessionId: session.sessionId,
53
- isActive,
54
- displayName,
55
- secondary,
56
- avatarUri,
57
- user: isActive ? user ?? null : null,
58
- };
59
- });
39
+ return accounts.map((account: DeviceAccount): AccountRow => ({
40
+ sessionId: account.sessionId,
41
+ authuser: account.authuser,
42
+ isActive: account.isCurrent,
43
+ displayName: account.displayName,
44
+ secondary: account.email,
45
+ avatarUri: account.avatarUrl,
46
+ user: account.user,
47
+ }));
60
48
  }
@@ -89,10 +89,10 @@ export interface OxyContextState {
89
89
  signIn: (publicKey: string, deviceName?: string) => Promise<User>;
90
90
 
91
91
  /**
92
- * Handle session from popup authentication
93
- * Updates auth state, persists session to storage
92
+ * Handle a session returned by web SSO.
93
+ * Updates auth state, persists session metadata to storage.
94
94
  */
95
- handlePopupSession: (session: SessionLoginResponse) => Promise<void>;
95
+ handleWebSession: (session: SessionLoginResponse) => Promise<void>;
96
96
 
97
97
  // Session management
98
98
  logout: (targetSessionId?: string) => Promise<void>;
@@ -206,7 +206,7 @@ function silentColdBootKey(oxyServices: OxyServices): string {
206
206
  * iframe never posts a message, so the full wait would be dead latency in front
207
207
  * of the terminal `/sso` bounce. `silentSignIn` already fails fast on a load
208
208
  * error via `iframe.onerror`; this caps the no-message case. 2.5s is well above
209
- * a same-origin iframe handshake yet a fraction of the legacy 5s default.
209
+ * a same-origin iframe handshake without blocking cold boot for several seconds.
210
210
  */
211
211
  const SILENT_IFRAME_TIMEOUT = 2500;
212
212
 
@@ -403,8 +403,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
403
403
  // plumbing and regardless of which auth code path fired.
404
404
  //
405
405
  // When the app passed the singleton itself as `oxyServices` (Mention's
406
- // pattern), `oxyServices === oxyClient`, so we skip the redundant self-write
407
- // and the subscription is a no-op mirror — fully backward compatible.
406
+ // pattern), `oxyServices === oxyClient`, so we skip the redundant self-write.
408
407
  useEffect(() => {
409
408
  if (oxyServices === oxyClient) {
410
409
  return;
@@ -635,7 +634,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
635
634
  // init. Callers MUST invoke this BEFORE any work that can trigger a route
636
635
  // navigation (`onAuthStateChange`) — navigation can interrupt a still-pending
637
636
  // async write, which is exactly what once left `session_ids` empty after a
638
- // successful sign-in. Shared by the FedCM/popup path and the cold-boot
637
+ // successful sign-in. Shared by the FedCM/SSO path and the cold-boot
639
638
  // refresh-cookie restore so both land the same durable record.
640
639
  const persistSessionDurably = useCallback(async (sessionId: string): Promise<void> => {
641
640
  const readyStorage = await getReadyStorage();
@@ -663,7 +662,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
663
662
  // Idempotent and monotonic via `authResolvedRef`: the first call wins and the
664
663
  // setters fire at most once, so the restore `finally` backstop becomes a no-op
665
664
  // once a commit site has already marked resolution. Called from EVERY place a
666
- // user is actually committed (the FedCM/iframe/redirect/SSO path
665
+ // user is actually committed (the FedCM/iframe/SSO path
667
666
  // `handleWebSSOSession`, the cookie-restore path, and the stored-session path)
668
667
  // so the common reload case unblocks the loading gate without sitting behind
669
668
  // the remaining (now-skipped) cold-boot steps.
@@ -691,10 +690,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
691
690
  // Calls `oxyServices.refreshAllSessions()` → `POST /auth/refresh-all` with
692
691
  // `credentials: 'include'`. The server rotates every device-local
693
692
  // `oxy_rt_${authuser}` cookie in parallel and returns one entry per valid
694
- // account (Google-style multi-account). On an older server that lacks the
695
- // multi-account endpoint, the SDK transparently falls back to the legacy
696
- // `/auth/refresh` single-account path and wraps the result in the same
697
- // shape, so this caller doesn't branch.
693
+ // account (Google-style multi-account).
698
694
  //
699
695
  // Active-account selection: the persisted `oxy_active_authuser` slot index
700
696
  // wins when it matches a returned account; otherwise the lowest `authuser`
@@ -787,23 +783,31 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
787
783
  // Native (and offline) stored-session restore — the ONLY restore path that
788
784
  // runs on React Native, and the web fallback when no cross-domain step won.
789
785
  //
790
- // Verbatim-extracted from the previous `restoreSessionsFromStorage` body: it
791
- // reads the durable `session_ids` / `active_session_id` slots, validates each
792
- // stored session in parallel (bearer `validateSession`), and switches to the
793
- // stored active session via the session-management `switchSession`. This body
794
- // is platform-agnostic and gated by NO `enabled()` predicate so it runs on
795
- // every platform — on native it is reached unconditionally (every web-only
796
- // step ahead of it is disabled by `isWebBrowser()`), so native restore is
797
- // exactly this and nothing else (no FedCM / iframe / refresh-all /
798
- // handleAuthCallback).
786
+ // Stored-session restore. Web uses this only as a fast local winner after
787
+ // URL-return handling; native uses it as the durable SecureStore path. Native
788
+ // first plants the shared access token from KeyManager, then validates the
789
+ // stored session ids with the bearer already in memory.
799
790
  const restoreStoredSession = useCallback(async (): Promise<boolean> => {
800
791
  if (!storage) {
801
792
  return false;
802
793
  }
803
794
 
804
795
  const storedSessionIdsJson = await storage.getItem(storageKeys.sessionIds);
805
- const storedSessionIds: string[] = storedSessionIdsJson ? JSON.parse(storedSessionIdsJson) : [];
806
- const storedActiveSessionId = await storage.getItem(storageKeys.activeSessionId);
796
+ const storedSessionIdsFromStorage: string[] = storedSessionIdsJson ? JSON.parse(storedSessionIdsJson) : [];
797
+ let storedActiveSessionId = await storage.getItem(storageKeys.activeSessionId);
798
+
799
+ const nativeSharedSession = !isWebBrowser()
800
+ ? await KeyManager.getSharedSession().catch(() => null)
801
+ : null;
802
+ if (nativeSharedSession?.accessToken) {
803
+ oxyServices.setTokens(nativeSharedSession.accessToken);
804
+ storedActiveSessionId = storedActiveSessionId ?? nativeSharedSession.sessionId;
805
+ }
806
+
807
+ const storedSessionIds = Array.from(new Set([
808
+ ...storedSessionIdsFromStorage,
809
+ ...(nativeSharedSession?.sessionId ? [nativeSharedSession.sessionId] : []),
810
+ ]));
807
811
 
808
812
  let validSessions: ClientSession[] = [];
809
813
 
@@ -964,7 +968,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
964
968
  // web-only step is gated by `isWebBrowser()`, so on native ONLY
965
969
  // `stored-session` runs.
966
970
  //
967
- // Order (web): redirect callback → SSO return → stored session → FedCM silent
971
+ // Order (web): SSO return → stored session → FedCM silent
968
972
  // (central) → silent iframe (per-apex, the durable reload path) → cookie
969
973
  // restore → SSO bounce (terminal).
970
974
  //
@@ -972,8 +976,8 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
972
976
  // (`fedcm-silent`, `silent-iframe`, `cookie-restore`). On a normal reload the
973
977
  // local bearer validates in one round-trip and wins, so `runColdBoot`
974
978
  // short-circuits and never sits through those probes' timeouts (the prior
975
- // serial sum was a ~20-30s stall). `redirect` and `sso-return` MUST stay
976
- // first — they consume the URL fragment before anything can strip it. On a
979
+ // serial sum was a ~20-30s stall). `sso-return` MUST stay first — it consumes
980
+ // the URL fragment before anything can strip it. On a
977
981
  // first visit with no local session, `stored-session` skips and the
978
982
  // cross-domain fallback chain (fedcm → iframe → cookie → sso-bounce) runs
979
983
  // exactly as before; the per-apex silent iframe still restores a durable
@@ -1006,26 +1010,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1006
1010
  const outcome = await runColdBoot<true>({
1007
1011
  steps: [
1008
1012
  {
1009
- // 0) Redirect callback wins: a popup/redirect sign-in just landed
1010
- // back on this page with `access_token`/`session_id` query params.
1011
- // `handleAuthCallback` plants the token but returns a PLACEHOLDER
1012
- // user (empty id), so we hydrate the REAL user via `getCurrentUser`
1013
- // and commit through `handleWebSSOSession` before claiming a
1014
- // session — never expose a placeholder user (R4).
1015
- id: 'redirect',
1016
- enabled: () => isWebBrowser(),
1017
- run: async () => {
1018
- const callbackSession = oxyServices.handleAuthCallback?.();
1019
- if (!callbackSession || !commitWebSession) {
1020
- return { kind: 'skip' };
1021
- }
1022
- const fullUser = await oxyServices.getCurrentUser();
1023
- await commitWebSession({ ...callbackSession, user: fullUser });
1024
- return { kind: 'session', session: true };
1025
- },
1026
- },
1027
- {
1028
- // 1) Central SSO return: we are landing back from an `auth.oxy.so/sso`
1013
+ // 0) Central SSO return: we are landing back from an `auth.oxy.so/sso`
1029
1014
  // bounce with the result in the URL fragment. Parse it, validate the
1030
1015
  // CSRF state, exchange the opaque code, and commit. On any non-ok
1031
1016
  // outcome `runSsoReturn` sets the per-origin NO_SESSION flag so the
@@ -1048,8 +1033,8 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1048
1033
  // normal reload the local bearer validates in one round-trip and
1049
1034
  // wins; `runColdBoot` then short-circuits and never even evaluates
1050
1035
  // the slow no-redirect probes that would otherwise time out (the
1051
- // ~20-30s serial stall). The `redirect` and `sso-return` steps stay
1052
- // AHEAD of this one — they must consume the URL fragment before any
1036
+ // ~20-30s serial stall). The `sso-return` step stays AHEAD of this
1037
+ // one — it must consume the URL fragment before any
1053
1038
  // later step (or anything else) strips it. On a first visit with no
1054
1039
  // local session this step skips and the cross-domain fallback chain
1055
1040
  // (fedcm → iframe → cookie → sso-bounce) runs exactly as before.
@@ -1348,8 +1333,8 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1348
1333
  });
1349
1334
  }, []);
1350
1335
 
1351
- // Web SSO: Automatically check for cross-domain session on web platforms
1352
- // Also used for popup auth - updates all state and persists session
1336
+ // Web SSO: automatically check for cross-domain session on web platforms.
1337
+ // Updates all state and persists session metadata.
1353
1338
  const handleWebSSOSession = useCallback(async (session: SessionLoginResponse) => {
1354
1339
  if (!session?.user || !session?.sessionId) {
1355
1340
  if (__DEV__) {
@@ -1358,12 +1343,10 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1358
1343
  return;
1359
1344
  }
1360
1345
 
1361
- // Set the access token on the HTTP client before updating UI state
1362
- if (session.accessToken) {
1363
- oxyServices.httpService.setTokens(session.accessToken);
1364
- } else {
1365
- await oxyServices.getTokenBySession(session.sessionId);
1346
+ if (!session.accessToken) {
1347
+ throw new Error('Session response did not include an access token');
1366
1348
  }
1349
+ oxyServices.httpService.setTokens(session.accessToken);
1367
1350
 
1368
1351
  const clientSession = {
1369
1352
  sessionId: session.sessionId,
@@ -1405,14 +1388,14 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1405
1388
  fullUser = session.user as unknown as User;
1406
1389
  }
1407
1390
  loginSuccess(fullUser);
1408
- // A session is now committed (FedCM silent / per-apex iframe / redirect /
1409
- // SSO-return / popup all funnel through here) — unblock the auth-resolution
1391
+ // A session is now committed (FedCM silent / per-apex iframe /
1392
+ // SSO-return all funnel through here) — unblock the auth-resolution
1410
1393
  // gate immediately, ahead of the cold-boot chain returning (idempotent).
1411
1394
  markAuthResolvedRef.current();
1412
1395
  onAuthStateChange?.(fullUser);
1413
1396
  }, [oxyServices, updateSessions, setActiveSessionId, loginSuccess, onAuthStateChange, persistSessionDurably]);
1414
1397
 
1415
- // Expose `handleWebSSOSession` to the cold-boot FedCM/iframe/redirect steps,
1398
+ // Expose `handleWebSSOSession` to the cold-boot FedCM/iframe/SSO steps,
1416
1399
  // which reference it through a ref because they are declared above this
1417
1400
  // callback. Assigned synchronously on every render so the ref is populated
1418
1401
  // before the cold-boot effect (gated on `storage`/`initialized`) can fire.
@@ -1670,7 +1653,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1670
1653
  hasIdentity,
1671
1654
  getPublicKey,
1672
1655
  signIn,
1673
- handlePopupSession: handleWebSSOSession,
1656
+ handleWebSession: handleWebSSOSession,
1674
1657
  logout,
1675
1658
  logoutAll,
1676
1659
  switchSession: switchSessionForContext,
@@ -1777,7 +1760,7 @@ const LOADING_STATE: OxyContextState = {
1777
1760
  hasIdentity: () => Promise.resolve(false),
1778
1761
  getPublicKey: () => Promise.resolve(null),
1779
1762
  signIn: () => rejectMissingProvider<User>(),
1780
- handlePopupSession: () => rejectMissingProvider<void>(),
1763
+ handleWebSession: () => rejectMissingProvider<void>(),
1781
1764
  logout: () => rejectMissingProvider<void>(),
1782
1765
  logoutAll: () => rejectMissingProvider<void>(),
1783
1766
  switchSession: () => rejectMissingProvider<User>(),
@@ -1809,4 +1792,3 @@ export const useOxy = (): OxyContextState => {
1809
1792
  };
1810
1793
 
1811
1794
  export default OxyContext;
1812
-
@@ -184,12 +184,9 @@ export const useAuthOperations = ({
184
184
  // Verify and create session. `verifyChallenge` plants the first
185
185
  // access token (and refresh token) from the `/auth/verify` response
186
186
  // body internally — mirroring `claimSessionByToken` — so the client is
187
- // authenticated as soon as this resolves. We deliberately do NOT fall
188
- // back to the bearer-protected `GET /session/token/:sessionId` (C1
189
- // hardening): for a brand-new identity with no bearer yet that route
190
- // 401s, which previously broke the entire new-identity onboarding
191
- // flow. A token-less verify response simply leaves the client without
192
- // a bearer here rather than triggering that 401.
187
+ // authenticated as soon as this resolves. Session IDs are not public
188
+ // token-minting credentials; a token-less verify response simply leaves
189
+ // the client without a bearer here.
193
190
  sessionResponse = await oxyServices.verifyChallenge(
194
191
  publicKey,
195
192
  challenge,
@@ -302,11 +299,8 @@ export const useAuthOperations = ({
302
299
 
303
300
  try {
304
301
  const sessionToLogout = targetSessionId || activeSessionId;
305
- // Web multi-account: when the target session carries an `authuser`
306
- // slot index it is backed by an httpOnly `oxy_rt_${n}` cookie. Use
307
- // the cookie-cleared logout endpoint so the server can `Set-Cookie`
308
- // an immediate expiry alongside revoking the family. Native and
309
- // legacy sessions (no `authuser` plumbed yet) fall through to the
302
+ // Web multi-account sessions carry an `authuser` slot index backed by
303
+ // an httpOnly `oxy_rt_${n}` cookie. Native sessions fall through to the
310
304
  // bearer-protected endpoint.
311
305
  const targetSession = sessionsRef.current.find((s) => s.sessionId === sessionToLogout);
312
306
  const targetAuthuser = targetSession?.authuser;
@@ -378,8 +372,8 @@ export const useAuthOperations = ({
378
372
  // - Web: "Sign out of all accounts" = sign out every device-local
379
373
  // account on THIS device. The cookie endpoint is the only path
380
374
  // that can `Set-Cookie` an immediate expiry on every
381
- // `oxy_rt_${n}` slot (plus legacy `oxy_rt`) AND revoke every
382
- // presented family server-side. The bearer-protected
375
+ // `oxy_rt_${n}` slots AND revoke every presented family server-side.
376
+ // The bearer-protected
383
377
  // `logoutAllSessions(activeSessionId)` would only revoke the
384
378
  // active user's sessions across devices and leave sibling
385
379
  // accounts' cookies sitting on this device — wrong UX for the
@@ -20,7 +20,7 @@
20
20
  * Cross-domain SSO:
21
21
  * - Web: Automatic via FedCM (Chrome 108+, Safari 16.4+)
22
22
  * - Native: Automatic via shared Keychain/Account Manager
23
- * - Manual sign-in: signIn() opens popup (web) or auth sheet (native)
23
+ * - Manual sign-in: signIn() redirects to the IdP (web) or opens the auth sheet (native)
24
24
  */
25
25
 
26
26
  import { useCallback, useState } from 'react';
@@ -60,16 +60,12 @@ export interface AuthState {
60
60
  export interface AuthActions {
61
61
  /**
62
62
  * Sign in
63
- * - Web: Opens popup to auth.oxy.so (no public key needed)
63
+ * - Web: Redirects to auth.oxy.so (no public key needed)
64
64
  * - Native: Uses cryptographic identity from keychain
65
65
  *
66
66
  * @param publicKey - Native: identity public key. Ignored on web.
67
- * @param preOpenedPopup - Web only: a popup the caller already opened
68
- * SYNCHRONOUSLY on the raw click via `oxyServices.openBlankPopup()`. This
69
- * keeps Chrome's user-activation alive across any prior `await` and is
70
- * the only reliable way to avoid the popup blocker on cross-domain flows.
71
67
  */
72
- signIn: (publicKey?: string, preOpenedPopup?: Window | null) => Promise<User>;
68
+ signIn: (publicKey?: string) => Promise<User>;
73
69
 
74
70
  /**
75
71
  * Sign out current session
@@ -114,7 +110,6 @@ export function useAuth(): UseAuthReturn {
114
110
  isAuthResolved,
115
111
  error,
116
112
  signIn: oxySignIn,
117
- handlePopupSession,
118
113
  logout,
119
114
  logoutAll,
120
115
  refreshSessions,
@@ -125,7 +120,7 @@ export function useAuth(): UseAuthReturn {
125
120
  openAvatarPicker,
126
121
  } = useOxy();
127
122
 
128
- const signIn = useCallback(async (publicKey?: string, preOpenedPopup?: Window | null): Promise<User> => {
123
+ const signIn = useCallback(async (publicKey?: string): Promise<User> => {
129
124
  // Check if we're on the identity provider itself
130
125
  // Only the IdP has local login forms - other apps are client apps
131
126
  const authWebUrl = oxyServices.config?.authWebUrl;
@@ -136,44 +131,13 @@ export function useAuth(): UseAuthReturn {
136
131
  const isIdentityProvider = isWebBrowser() &&
137
132
  window.location.hostname === idpHostname;
138
133
 
139
- // Web (not on IdP): Use popup-based authentication
140
- // We go straight to popup to preserve the "user gesture" (click event)
141
- // FedCM silent SSO already runs on page load via useWebSSO
142
- // If user is clicking "Sign In", they need interactive auth NOW
134
+ // Web (not on IdP): use the tokenless redirect SSO flow. FedCM / silent SSO
135
+ // already run on page load; an explicit click needs interactive auth.
143
136
  if (isWebBrowser() && !publicKey && !isIdentityProvider) {
144
- // Open the popup SYNCHRONOUSLY before any `await` runs so Chrome's
145
- // transient user-activation is preserved across the async chain
146
- // (React state updates + the `await` into `signInWithPopup`). If the
147
- // caller already pre-opened one (e.g. they did so to be extra safe
148
- // ahead of their own awaits), reuse it.
149
- const popup: Window | null = preOpenedPopup
150
- ?? oxyServices.openBlankPopup?.()
151
- ?? null;
152
- try {
153
- const popupSession = await oxyServices.signInWithPopup?.({
154
- popup,
155
- });
156
- if (popupSession?.user) {
157
- // The popup auth flow fetches full user data, so the session user
158
- // contains full User fields even though the base type is MinimalUserData.
159
- // Cast to the expected shape for handlePopupSession.
160
- const sessionWithUser = {
161
- ...popupSession,
162
- user: popupSession.user as unknown as User,
163
- };
164
- await handlePopupSession(sessionWithUser);
165
- return sessionWithUser.user;
166
- }
167
- throw new Error('Sign-in failed. Please try again.');
168
- } catch (popupError) {
169
- if (popup && !popup.closed) {
170
- popup.close();
171
- }
172
- if (popupError instanceof Error && popupError.message.includes('blocked')) {
173
- throw new Error('Popup blocked. Please allow popups for this site.');
174
- }
175
- throw popupError;
176
- }
137
+ oxyServices.signInWithRedirect?.({
138
+ redirectUri: window.location.href,
139
+ });
140
+ return new Promise<User>(() => undefined);
177
141
  }
178
142
 
179
143
  // Native: Use cryptographic identity
@@ -212,7 +176,7 @@ export function useAuth(): UseAuthReturn {
212
176
  }
213
177
 
214
178
  throw new Error('No authentication method available');
215
- }, [oxySignIn, hasIdentity, getPublicKey, showBottomSheet, oxyServices, handlePopupSession]);
179
+ }, [oxySignIn, hasIdentity, getPublicKey, showBottomSheet, oxyServices]);
216
180
 
217
181
  const signOut = useCallback(async (): Promise<void> => {
218
182
  await logout();
@@ -247,4 +211,3 @@ export function useAuth(): UseAuthReturn {
247
211
  openAvatarPicker,
248
212
  };
249
213
  }
250
-