@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
@@ -1,12 +1,73 @@
1
1
  import type React from 'react';
2
- import { useCallback, useState, useEffect, useMemo } from 'react';
2
+ import { useCallback, useState, useEffect, useMemo, useRef } from 'react';
3
3
  import { TouchableOpacity, Text, View, StyleSheet, type ViewStyle, type TextStyle, type StyleProp, Platform } from 'react-native';
4
+ import {
5
+ logger,
6
+ generatePkcePair,
7
+ generateOAuthState,
8
+ buildOAuthAuthorizeUrl,
9
+ type PublicApplication,
10
+ } from '@oxyhq/core';
4
11
  import { useAuthStore } from '../stores/authStore';
5
12
  import { useShallow } from 'zustand/react/shallow';
6
13
  import { useTheme } from '@oxyhq/bloom/theme';
7
14
  import { useOxy } from '../context/OxyContext';
8
15
  import OxyLogo from './OxyLogo';
9
- import { showSignInModal, subscribeToSignInModal } from './SignInModal';
16
+ import { subscribeToSignInModal } from '../navigation/accountDialogManager';
17
+ import { redirectToAuthorize, openAuthorizeUrlNative } from './oauthNavigation';
18
+
19
+ /**
20
+ * `sessionStorage` keys under which a third-party "Sign in with Oxy" OAuth flow
21
+ * persists its CSRF `state` and PKCE `code_verifier` across the authorize
22
+ * redirect. The Relying Party's redirect-URI callback reads them back to
23
+ * validate the returned `state` and replay the verifier on the token exchange.
24
+ *
25
+ * Web only: a browser RP navigates away to `auth.oxy.so` and back, so the
26
+ * handshake must survive a full-page redirect. Native completes the flow inside
27
+ * a single `WebBrowser` auth session and surfaces the handshake via
28
+ * {@link OxySignInButtonProps.onOAuthResult} instead.
29
+ */
30
+ export const OXY_OAUTH_STATE_STORAGE_KEY = 'oxy_oauth_state';
31
+ export const OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY = 'oxy_oauth_code_verifier';
32
+
33
+ /**
34
+ * The OAuth handshake surfaced to a NATIVE third-party RP via
35
+ * {@link OxySignInButtonProps.onOAuthResult} so it can finish the code exchange
36
+ * (`POST /auth/oauth/token`). Web RPs read the same `state` / `code_verifier`
37
+ * back from `sessionStorage` across the redirect and do not need this callback.
38
+ */
39
+ export interface OxyOAuthResult {
40
+ /** Deep-link URL the native auth session returned to (`?code=…&state=…`), or `null` if unobserved. */
41
+ redirectUrl: string | null;
42
+ /** The CSRF `state` sent on the authorize request; the RP must match it on return. */
43
+ state: string;
44
+ /** The PKCE `code_verifier` to replay on the token exchange. */
45
+ codeVerifier: string;
46
+ }
47
+
48
+ /**
49
+ * Persist the OAuth CSRF `state` + PKCE `code_verifier` for the RP callback.
50
+ * Returns `false` when the handshake could not be stored — no `sessionStorage`
51
+ * (SSR / non-browser host) or a write that threw (`SecurityError` /
52
+ * `QuotaExceededError`, e.g. Safari private mode) — so the caller aborts the
53
+ * flow cleanly rather than redirect to a callback that cannot validate `state`.
54
+ */
55
+ function persistOAuthHandshake(state: string, codeVerifier: string): boolean {
56
+ const store = (globalThis as { sessionStorage?: Storage }).sessionStorage;
57
+ try {
58
+ if (!store) throw new Error('sessionStorage is unavailable');
59
+ store.setItem(OXY_OAUTH_STATE_STORAGE_KEY, state);
60
+ store.setItem(OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY, codeVerifier);
61
+ return true;
62
+ } catch (error) {
63
+ logger.warn(
64
+ 'OxySignInButton: could not persist the OAuth handshake to sessionStorage; aborting third-party sign-in',
65
+ { component: 'OxySignInButton' },
66
+ error,
67
+ );
68
+ return false;
69
+ }
70
+ }
10
71
 
11
72
  export interface OxySignInButtonProps {
12
73
  /**
@@ -48,6 +109,37 @@ export interface OxySignInButtonProps {
48
109
  * @default false
49
110
  */
50
111
  showWhenAuthenticated?: boolean;
112
+
113
+ /**
114
+ * Exact registered redirect URI the OAuth authorization code is returned to.
115
+ * REQUIRED only for third-party (`type: 'third_party'`) applications, which
116
+ * sign in via an OAuth + PKCE redirect to `auth.oxy.so`. First-party /
117
+ * official apps open the in-app dialog and ignore this prop. If a third-party
118
+ * app resolves without it, the button logs an error and does nothing (it will
119
+ * not invent a redirect URI).
120
+ */
121
+ oauthRedirectUri?: string;
122
+
123
+ /**
124
+ * Native only: receives the OAuth handshake after a third-party auth session
125
+ * so the RP can finish the token exchange. On web the handshake is read back
126
+ * from `sessionStorage` across the full-page redirect, so this is not used
127
+ * there. A native third-party sign-in with NO `onOAuthResult` handler cannot
128
+ * complete (the `state` + `code_verifier` are lost) and logs a warning.
129
+ *
130
+ * @example
131
+ * ```tsx
132
+ * <OxySignInButton
133
+ * oauthRedirectUri="myapp://oauth/callback"
134
+ * onOAuthResult={({ redirectUrl, state, codeVerifier }) => {
135
+ * if (!redirectUrl) return;
136
+ * const code = new URL(redirectUrl).searchParams.get('code');
137
+ * // → POST /auth/oauth/token { code, code_verifier: codeVerifier, state }
138
+ * }}
139
+ * />
140
+ * ```
141
+ */
142
+ onOAuthResult?: (result: OxyOAuthResult) => void;
51
143
  }
52
144
 
53
145
  /**
@@ -82,39 +174,152 @@ export const OxySignInButton: React.FC<OxySignInButtonProps> = ({
82
174
  text = 'Sign in with Oxy',
83
175
  disabled = false,
84
176
  showWhenAuthenticated = false,
177
+ oauthRedirectUri,
178
+ onOAuthResult,
85
179
  }) => {
86
180
  const theme = useTheme();
87
- const { showBottomSheet } = useOxy();
181
+ const { openAccountDialog, oxyServices, clientId } = useOxy();
88
182
  const { isAuthenticated, isLoading } = useAuthStore(
89
183
  useShallow((state) => ({ isAuthenticated: state.isAuthenticated, isLoading: state.isLoading }))
90
184
  );
91
- // Tracks visibility of the web full-screen sign-in modal so we can show
92
- // "Signing in..." while it's open. On native we open a bottom sheet whose
93
- // visibility is owned by the sheet manager — no subscription needed.
185
+ // Tracks whether the unified account dialog is open so we can show
186
+ // "Signing in..." while it is. The manager reports visibility on every
187
+ // change regardless of platform or what opened/closed it.
94
188
  const [isModalOpen, setIsModalOpen] = useState(false);
95
189
 
96
- // Subscribe to web modal visibility only — bottom sheet manages its own state.
97
- useEffect(() => {
98
- if (Platform.OS !== 'web') return;
99
- return subscribeToSignInModal(setIsModalOpen);
100
- }, []);
190
+ useEffect(() => subscribeToSignInModal(setIsModalOpen), []);
191
+
192
+ // The application's public identity is resolved lazily on first press and its
193
+ // promise cached, so rapid taps share one in-flight resolve. The cache is
194
+ // KEYED on the identity inputs (clientId + the oxyServices instance): if
195
+ // either changes the cache is invalidated and re-resolved — without a
196
+ // useEffect. A rejected resolve clears the cache so a later press can retry.
197
+ const appResolutionRef = useRef<{
198
+ clientId: string;
199
+ oxyServices: typeof oxyServices;
200
+ promise: Promise<PublicApplication>;
201
+ } | null>(null);
202
+ // Re-entrancy guard: a routing pass may await network + crypto before it
203
+ // redirects, so block a second concurrent press from racing the sessionStorage
204
+ // handshake against a different PKCE pair.
205
+ const routingRef = useRef(false);
206
+
207
+ const resolvePublicApplication = useCallback((): Promise<PublicApplication> | null => {
208
+ if (!clientId) return null;
209
+ const cached = appResolutionRef.current;
210
+ if (cached && cached.clientId === clientId && cached.oxyServices === oxyServices) {
211
+ return cached.promise;
212
+ }
213
+ const promise = oxyServices.getPublicApplication(clientId).catch((error) => {
214
+ // Only clear if this is still the live entry (a later resolve may have
215
+ // replaced it after a clientId/oxyServices change).
216
+ if (appResolutionRef.current?.promise === promise) {
217
+ appResolutionRef.current = null;
218
+ }
219
+ throw error;
220
+ });
221
+ appResolutionRef.current = { clientId, oxyServices, promise };
222
+ return promise;
223
+ }, [clientId, oxyServices]);
224
+
225
+ // Official / first-party surface: the in-app account + sign-in dialog.
226
+ const startOfficialSignIn = useCallback(() => {
227
+ openAccountDialog('signin');
228
+ }, [openAccountDialog]);
229
+
230
+ // Third-party surface: an OAuth 2.0 authorization-code + PKCE redirect to
231
+ // auth.oxy.so. No FedCM, no SSO bounce, no Oxy session cookies.
232
+ const startThirdPartyOAuth = useCallback(
233
+ async (app: PublicApplication): Promise<void> => {
234
+ if (!clientId) {
235
+ startOfficialSignIn();
236
+ return;
237
+ }
238
+ if (!oauthRedirectUri) {
239
+ logger.error(
240
+ 'OxySignInButton: a third_party application requires the `oauthRedirectUri` prop to start the OAuth redirect; sign-in aborted',
241
+ undefined,
242
+ { component: 'OxySignInButton', clientId, application: app.name },
243
+ );
244
+ return;
245
+ }
246
+
247
+ const [pkce, state] = await Promise.all([generatePkcePair(), generateOAuthState()]);
248
+ const authorizeUrl = buildOAuthAuthorizeUrl({
249
+ clientId,
250
+ redirectUri: oauthRedirectUri,
251
+ state,
252
+ codeChallenge: pkce.codeChallenge,
253
+ });
254
+
255
+ if (Platform.OS === 'web') {
256
+ // Persist the handshake for the RP callback, then hand the
257
+ // top-level document to the IdP. Without storage the callback
258
+ // cannot validate `state`, so abort cleanly rather than redirect.
259
+ if (!persistOAuthHandshake(state, pkce.codeVerifier)) {
260
+ return;
261
+ }
262
+ redirectToAuthorize(authorizeUrl);
263
+ return;
264
+ }
265
+
266
+ // Native: open the in-app auth session, then hand the handshake to the
267
+ // RP so it can complete the token exchange from its deep-link callback.
268
+ const { redirectUrl } = await openAuthorizeUrlNative(authorizeUrl, oauthRedirectUri);
269
+ if (onOAuthResult) {
270
+ onOAuthResult({ redirectUrl, state, codeVerifier: pkce.codeVerifier });
271
+ return;
272
+ }
273
+ logger.warn(
274
+ 'OxySignInButton: native third-party sign-in cannot complete without an `onOAuthResult` handler; the code exchange is the RP\'s responsibility (state + code_verifier were not surfaced)',
275
+ { component: 'OxySignInButton', application: app.name },
276
+ );
277
+ },
278
+ [clientId, oauthRedirectUri, onOAuthResult, startOfficialSignIn],
279
+ );
101
280
 
102
- // Handle button press
103
- // - Web: full-screen modal (dialog UX fits desktop / browser)
104
- // - Native: bottom sheet (sheet UX fits iOS/Android)
281
+ // Resolve the Application once, then route: third-party → OAuth redirect;
282
+ // first-party / official / unresolved the in-app dialog. Resolution failure
283
+ // NEVER breaks an official app's sign-in it falls back to the dialog.
284
+ const routeSignIn = useCallback(async (): Promise<void> => {
285
+ if (routingRef.current) return;
286
+ routingRef.current = true;
287
+ try {
288
+ const resolving = resolvePublicApplication();
289
+ if (!resolving) {
290
+ startOfficialSignIn();
291
+ return;
292
+ }
293
+ let app: PublicApplication;
294
+ try {
295
+ app = await resolving;
296
+ } catch (error) {
297
+ logger.warn(
298
+ 'OxySignInButton: could not resolve the application; opening the sign-in dialog',
299
+ { component: 'OxySignInButton', clientId },
300
+ error,
301
+ );
302
+ startOfficialSignIn();
303
+ return;
304
+ }
305
+ if (app.type === 'third_party' && !app.isOfficial) {
306
+ await startThirdPartyOAuth(app);
307
+ return;
308
+ }
309
+ startOfficialSignIn();
310
+ } finally {
311
+ routingRef.current = false;
312
+ }
313
+ }, [resolvePublicApplication, startOfficialSignIn, startThirdPartyOAuth, clientId]);
314
+
315
+ // Defer to a caller-supplied handler, otherwise route by application type.
105
316
  const handlePress = useCallback(() => {
106
317
  if (onPress) {
107
318
  onPress();
108
319
  return;
109
320
  }
110
-
111
- if (Platform.OS === 'web') {
112
- setIsModalOpen(true);
113
- showSignInModal();
114
- } else {
115
- showBottomSheet?.('OxyAuth');
116
- }
117
- }, [onPress, showBottomSheet]);
321
+ void routeSignIn();
322
+ }, [onPress, routeSignIn]);
118
323
 
119
324
  const themedStyles = useMemo(() => StyleSheet.create({
120
325
  button: {
@@ -1,5 +1,5 @@
1
1
  import type React from 'react';
2
- import { useCallback, useRef, useState } from 'react';
2
+ import { useCallback, useState } from 'react';
3
3
  import {
4
4
  View,
5
5
  Pressable,
@@ -14,21 +14,11 @@ import { Text } from '@oxyhq/bloom/typography';
14
14
  import { useTheme } from '@oxyhq/bloom/theme';
15
15
  import { getAccountDisplayName, getAccountFallbackHandle } from '@oxyhq/core';
16
16
  import { useAuth } from '../hooks/useAuth';
17
+ import { useOxy } from '../context/OxyContext';
17
18
  import { useI18n } from '../hooks/useI18n';
18
- import ProfileMenu, { type ProfileMenuAnchor } from './ProfileMenu';
19
19
 
20
20
  const isWeb = Platform.OS === 'web';
21
21
 
22
- /**
23
- * Fixed popover width used by {@link ProfileMenu} on web. `ProfileButton`
24
- * mirrors this constant when computing the trigger-relative anchor so the
25
- * left-edge clamp keeps the panel fully on-screen. Keep the two in sync.
26
- */
27
- const MENU_WIDTH = 300;
28
-
29
- /** Gutter, in px, kept between the popover and the viewport edges on web. */
30
- const VIEWPORT_GUTTER = 8;
31
-
32
22
  /**
33
23
  * Web-only Bluesky-style hover animation timings, mirrored from
34
24
  * `social-app/src/view/shell/desktop/LeftNav.tsx`. The row background and the
@@ -67,23 +57,15 @@ export interface ProfileButtonProps {
67
57
  */
68
58
  avatarSize?: number;
69
59
  /** Navigate to the "Manage account" surface (settings). */
70
- onNavigateManage: () => void;
60
+ onNavigateManage?: () => void;
71
61
  /** Start the add-account / sign-in flow for an additional account. */
72
- onAddAccount: () => void;
62
+ onAddAccount?: () => void;
73
63
  /** Optional: navigate to the signed-in user's own profile. */
74
64
  onNavigateProfile?: () => void;
75
- /** Called before the active identity changes so apps can clear scoped state. */
76
- onBeforeSessionChange?: () => void | Promise<void>;
77
65
  /**
78
- * Web-only popover direction relative to the trigger:
79
- * - `'up'` (default): the menu opens UPWARD, for a trigger placed at the
80
- * BOTTOM of a sidebar (the footer pattern). Pixel-identical to prior
81
- * behavior.
82
- * - `'down'`: the menu opens DOWNWARD, for a trigger placed at the TOP of a
83
- * sidebar (the accounts app pattern).
84
- * - `'auto'`: opens downward when there is more room below the trigger than
85
- * above, otherwise upward.
86
- * Native (bottom-sheet) is unaffected — this only influences the web popover.
66
+ * Retained for source compatibility. The trigger now opens the unified
67
+ * `OxyAccountDialog` (a centered / bottom-sheet modal) rather than an
68
+ * anchored popover, so popover placement no longer applies.
87
69
  */
88
70
  placement?: 'up' | 'down' | 'auto';
89
71
  /**
@@ -98,36 +80,27 @@ export interface ProfileButtonProps {
98
80
 
99
81
  /**
100
82
  * Self-contained sidebar account trigger, modeled on Bluesky's ProfileCard and
101
- * the Oxy inbox's `MailboxDrawer` footer. Owns its own open state + anchor
102
- * measurement, and renders {@link ProfileMenu} (the device-account switcher).
83
+ * the Oxy inbox's `MailboxDrawer` footer. Pressing it opens the unified
84
+ * {@link OxyAccountDialog} the single account switcher + sign-in surface — via
85
+ * `useOxy().openAccountDialog`.
103
86
  *
104
87
  * Three auth states from {@link useAuth}:
105
88
  * - **Undetermined** (`!isAuthResolved || isPrivateApiPending`): a neutral
106
89
  * avatar-sized skeleton circle, no press.
107
90
  * - **Signed in**: a pressable row — Bloom `Avatar` + display name + `@handle`
108
- * + an `unfold-more-horizontal` chevron. Press opens the menu upward (web)
109
- * against the measured trigger, or as a bottom sheet (native).
110
- * - **Signed out**: a "Sign in" row that calls `useAuth().signIn()`.
91
+ * + a chevron. Press opens the dialog on its `accounts` view.
92
+ * - **Signed out**: a "Sign in" row that calls `useAuth().signIn()` (which opens
93
+ * the dialog on its `signin` view).
111
94
  *
112
95
  * Styling uses react-native `StyleSheet` + the Bloom theme (via `useTheme`) so
113
96
  * the layout renders identically in EVERY consumer — including apps that do not
114
97
  * use NativeWind (e.g. the accounts app). Only the web hover animation keeps
115
98
  * dynamic inline `style` (the CSS transition/transform values), which is what
116
99
  * the `react-native-web-style.d.ts` augmentation exists for.
117
- *
118
- * All display strings resolve through `@oxyhq/core`'s
119
- * `getAccountDisplayName` / `getAccountFallbackHandle` — no hand-rolled name
120
- * fallbacks. Avatars pass the bare file id as `source` + `variant="thumb"`, so
121
- * the app's registered `ImageResolver` builds the thumbnail URL.
122
100
  */
123
101
  const ProfileButton: React.FC<ProfileButtonProps> = ({
124
102
  expanded = true,
125
103
  avatarSize,
126
- onNavigateManage,
127
- onAddAccount,
128
- onNavigateProfile,
129
- onBeforeSessionChange,
130
- placement = 'up',
131
104
  className,
132
105
  style,
133
106
  }) => {
@@ -138,76 +111,20 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
138
111
  isPrivateApiPending,
139
112
  signIn,
140
113
  } = useAuth();
114
+ const { openAccountDialog } = useOxy();
141
115
  const { colors } = useTheme();
142
116
  const { t, locale } = useI18n();
143
117
 
144
- const [menuOpen, setMenuOpen] = useState(false);
145
- const [anchor, setAnchor] = useState<ProfileMenuAnchor | null>(null);
146
-
147
118
  // Web-only hover/focus tracking for the Bluesky-style reveal animation.
148
119
  // Native has no hover, so these stay false and the row renders statically.
149
120
  const [hovered, setHovered] = useState(false);
150
121
  const [focused, setFocused] = useState(false);
151
122
 
152
- // Trigger ref for the web anchor measurement. RN-Web exposes `measure()` on
153
- // host views, so we anchor the popover to the button rect and open upward.
154
- const triggerRef = useRef<View | null>(null);
155
-
156
123
  const resolvedAvatarSize = avatarSize ?? (expanded ? 40 : 32);
157
124
 
158
- const openMenu = useCallback(() => {
159
- if (!isWeb) {
160
- setAnchor(null);
161
- setMenuOpen(true);
162
- return;
163
- }
164
- const node = triggerRef.current;
165
- if (!node || typeof node.measure !== 'function' || typeof window === 'undefined') {
166
- setAnchor(null);
167
- setMenuOpen(true);
168
- return;
169
- }
170
- node.measure((_x, _y, _w, height, pageX, pageY) => {
171
- if (typeof pageX !== 'number' || typeof pageY !== 'number') {
172
- setAnchor(null);
173
- } else {
174
- // Clamp the panel's left edge inside the viewport gutter.
175
- const maxLeft = Math.max(
176
- VIEWPORT_GUTTER,
177
- window.innerWidth - MENU_WIDTH - VIEWPORT_GUTTER,
178
- );
179
- const left = Math.min(Math.max(VIEWPORT_GUTTER, pageX), maxLeft);
180
- // Bottom edge of the trigger in page coordinates. `height` is a
181
- // number for host views; guard defensively for non-web shims.
182
- const triggerBottom = pageY + (typeof height === 'number' ? height : 0);
183
- // Resolve `'auto'` by comparing the room below the trigger with
184
- // the room above it; pick whichever direction has more space.
185
- const openDown =
186
- placement === 'down' ||
187
- (placement === 'auto' &&
188
- window.innerHeight - triggerBottom > pageY);
189
- if (openDown) {
190
- // Anchor the panel's TOP edge just below the trigger bottom so
191
- // the menu opens downward from a top-of-sidebar trigger.
192
- const top = Math.max(VIEWPORT_GUTTER, triggerBottom + VIEWPORT_GUTTER);
193
- setAnchor({ left, top });
194
- } else {
195
- // Anchor the panel's BOTTOM edge just above the trigger top so
196
- // the menu opens upward from this footer button.
197
- const bottom = Math.max(
198
- VIEWPORT_GUTTER,
199
- window.innerHeight - pageY + VIEWPORT_GUTTER,
200
- );
201
- setAnchor({ left, bottom });
202
- }
203
- }
204
- setMenuOpen(true);
205
- });
206
- }, [placement]);
207
-
208
- const handleClose = useCallback(() => {
209
- setMenuOpen(false);
210
- }, []);
125
+ const openDialog = useCallback(() => {
126
+ openAccountDialog('accounts');
127
+ }, [openAccountDialog]);
211
128
 
212
129
  // ── Undetermined: skeleton circle, no interaction. ──────────────────────
213
130
  if (!isAuthResolved || isPrivateApiPending) {
@@ -285,10 +202,10 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
285
202
  />
286
203
  );
287
204
 
288
- // `active` combines hover, keyboard focus, and menu-open — exactly like
289
- // Bluesky's `state.hovered || state.focused || control.isOpen`. Only web
290
- // ever sets `hovered`/`focused`, so on native this reduces to `menuOpen`.
291
- const active = hovered || focused || menuOpen;
205
+ // `active` combines hover and keyboard focus — exactly like Bluesky's
206
+ // `state.hovered || state.focused`. Only web ever sets these, so on native
207
+ // the row renders statically.
208
+ const active = hovered || focused;
292
209
  const accountLabel = t('accountSwitcher.switchWhileSignedInAs', { name: displayName })
293
210
  || `Switch account, signed in as ${displayName}`;
294
211
 
@@ -318,24 +235,14 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
318
235
  return (
319
236
  <View className={className} style={style}>
320
237
  <Pressable
321
- ref={triggerRef}
322
238
  style={[styles.collapsedTrigger, collapsedBgStyle]}
323
- onPress={openMenu}
239
+ onPress={openDialog}
324
240
  accessibilityRole="button"
325
241
  accessibilityLabel={accountLabel}
326
242
  {...webInteractionProps}
327
243
  >
328
244
  {avatarNode}
329
245
  </Pressable>
330
- <ProfileMenu
331
- open={menuOpen}
332
- onClose={handleClose}
333
- anchor={anchor}
334
- onNavigateManage={onNavigateManage}
335
- onAddAccount={onAddAccount}
336
- onNavigateProfile={onNavigateProfile}
337
- onBeforeSessionChange={onBeforeSessionChange}
338
- />
339
246
  </View>
340
247
  );
341
248
  }
@@ -402,9 +309,8 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
402
309
  return (
403
310
  <View className={className} style={[styles.fullWidth, style]}>
404
311
  <Pressable
405
- ref={triggerRef}
406
312
  style={[styles.row, styles.rowExpanded, rowStyle]}
407
- onPress={openMenu}
313
+ onPress={openDialog}
408
314
  accessibilityRole="button"
409
315
  accessibilityLabel={accountLabel}
410
316
  {...webInteractionProps}
@@ -434,15 +340,6 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
434
340
  />
435
341
  </View>
436
342
  </Pressable>
437
- <ProfileMenu
438
- open={menuOpen}
439
- onClose={handleClose}
440
- anchor={anchor}
441
- onNavigateManage={onNavigateManage}
442
- onAddAccount={onAddAccount}
443
- onNavigateProfile={onNavigateProfile}
444
- onBeforeSessionChange={onBeforeSessionChange}
445
- />
446
343
  </View>
447
344
  );
448
345
  };